rspec-cover_it 0.0.3 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +18 -9
- data/lib/rspec/cover_it/context.rb +33 -4
- data/lib/rspec/cover_it/context_coverage.rb +28 -4
- data/lib/rspec/cover_it/coverage_state.rb +17 -5
- data/lib/rspec/cover_it/example_group_completeness_checker.rb +34 -0
- data/lib/rspec/cover_it/pretest_coverage.rb +1 -1
- data/lib/rspec/cover_it/version.rb +1 -1
- data/lib/rspec/cover_it.rb +3 -2
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b3efab53f0b9f05be2f5a8ae7f170e4a3a8e886439a751e025926c61bf9ac8f1
|
4
|
+
data.tar.gz: 569fbdcc5f328d5d0656eb3fe20265c9cd164455cfbf290c7764b92d15454fbf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e9d198749c87a093cf8cbf5d2e5a4f20bfdb1a4c312579a541d8335fa4ccab88781fdbee073195cf8d5c682d921019c9fb86ede9fe951e57f83df24722934c59
|
7
|
+
data.tar.gz: 604d4808f4a3d8afacf52cfe0651d4bbae46ee510ca95e123cd14304b071cbf1c726bf37741002efdd0d9dc636625b47d149f0a17226c26f42cdad0ba531be7f
|
data/README.md
CHANGED
@@ -58,13 +58,20 @@ In example groups, you can use metadata to control the behavior of
|
|
58
58
|
this ExampleGroup. If numeric, it's enabling, and specifying the coverage
|
59
59
|
threshold at the same time (as a percentage - `cover_it: 95` requires 95%
|
60
60
|
coverage of the target class by this example group).
|
61
|
+
* `covers_path`: The path (relative to the spec file!) of the code the spec is
|
62
|
+
intending to cover. Later, this can be an array of paths, for the multi-spec
|
63
|
+
case `covers` is intended for as well. This is an annoying work-around for
|
64
|
+
the fact that we can't perfectly infer the location of the source code in
|
65
|
+
some cases - in particular, `lib/foo/version.rb` tends to cause a problem
|
66
|
+
for specs on `foo.rb`, since the version file is invariably loaded first.
|
61
67
|
* `covers`: An array of classes and modules that this example groups _thinks
|
62
68
|
it is completely testing_. Ideally, you'd have a separate test file for each,
|
63
69
|
but sometimes that's hard to do - you can let one spec claim responsibility
|
64
70
|
for multiple classes and modules (such as Concerns) using this. Be default
|
65
71
|
it is just `[described_class]`. Additionally, if your top-level example
|
66
72
|
group _does not describe a Class or Module_, you may use `covers` to let it
|
67
|
-
invoke `rspec-cover_it` anyway
|
73
|
+
invoke `rspec-cover_it` anyway - some people `describe "a descriptive string"`
|
74
|
+
instead of `describe AClass`, and .. fine.
|
68
75
|
|
69
76
|
## Implementation
|
70
77
|
|
@@ -115,14 +122,16 @@ That's not the goal here, and I'm not going to worry about it.
|
|
115
122
|
As initially implemented, it fails your tests if you don't run the entire test
|
116
123
|
file. `rspec spec/foo_spec.rb:32` will error, because .. running only one of
|
117
124
|
your tests _doesn't cover the class_. I have a solution for this, but it uses
|
118
|
-
some non-public bits of RSpec, so I'm trying to find a better answer
|
125
|
+
some non-public bits of RSpec, so I'm trying to find a better answer still.
|
119
126
|
(Conversation started in their
|
120
127
|
[issue tracker](https://github.com/rspec/rspec-core/issues/3037))
|
121
128
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
+
We're using `Object.const_source_location` to find the path of the source file
|
130
|
+
defining a given constant. That _mostly_ works, but it actually gives the path
|
131
|
+
of the _first_ source file that defined that constant. So if your gem defines
|
132
|
+
its version in `lib/foo/version.rb` (as an example), in a separate file from
|
133
|
+
lib/foo.rb, the _path_ for `Foo` may end up being the former. Which is.. not
|
134
|
+
going to have much coverable code, of course. This is an edge case, but one
|
135
|
+
that is likely to occur fairly regularly. I haven't thought of a _good_ solution
|
136
|
+
yet. Perhaps if the `covers` array includes a string, we should treat it as a
|
137
|
+
relative path?
|
@@ -1,16 +1,23 @@
|
|
1
1
|
module RSpec
|
2
2
|
module CoverIt
|
3
3
|
class Context
|
4
|
-
def initialize(scope:, rspec_context:)
|
5
|
-
@scope, @rspec_context = scope, rspec_context
|
4
|
+
def initialize(scope:, rspec_context:, autoenforce:)
|
5
|
+
@scope, @rspec_context, @autoenforce = scope, rspec_context, autoenforce
|
6
6
|
end
|
7
7
|
|
8
8
|
def cover_it?
|
9
|
-
target_class &&
|
9
|
+
target_class &&
|
10
|
+
metadata.fetch(:cover_it, autoenforce?) &&
|
11
|
+
completeness_checker.running_all_examples?
|
12
|
+
end
|
13
|
+
|
14
|
+
def specific_threshold
|
15
|
+
meta_value = metadata.fetch(:cover_it, nil)
|
16
|
+
meta_value.is_a?(Numeric) ? meta_value / 100.0 : nil
|
10
17
|
end
|
11
18
|
|
12
19
|
def target_path
|
13
|
-
|
20
|
+
metadata.key?(:covers_path) ? metadata_path : inferred_path
|
14
21
|
end
|
15
22
|
|
16
23
|
def target_class
|
@@ -21,13 +28,35 @@ module RSpec
|
|
21
28
|
target_class.name
|
22
29
|
end
|
23
30
|
|
31
|
+
def scope_name
|
32
|
+
scope.file_path
|
33
|
+
end
|
34
|
+
|
24
35
|
private
|
25
36
|
|
26
37
|
attr_reader :scope, :rspec_context
|
27
38
|
|
39
|
+
def autoenforce?
|
40
|
+
@autoenforce
|
41
|
+
end
|
42
|
+
|
28
43
|
def metadata
|
29
44
|
scope.metadata
|
30
45
|
end
|
46
|
+
|
47
|
+
def completeness_checker
|
48
|
+
@_completeness_checker ||= ExampleGroupCompletenessChecker.new(scope)
|
49
|
+
end
|
50
|
+
|
51
|
+
def metadata_path
|
52
|
+
supplied_path = metadata.fetch(:covers_path)
|
53
|
+
spec_directory = File.dirname(scope.file_path)
|
54
|
+
File.expand_path(supplied_path, spec_directory)
|
55
|
+
end
|
56
|
+
|
57
|
+
def inferred_path
|
58
|
+
Object.const_source_location(target_class_name).first
|
59
|
+
end
|
31
60
|
end
|
32
61
|
end
|
33
62
|
end
|
@@ -24,8 +24,29 @@ module RSpec
|
|
24
24
|
covered_line_count.to_f / coverable_line_count.to_f
|
25
25
|
end
|
26
26
|
|
27
|
-
def enforce!
|
28
|
-
|
27
|
+
def enforce!(default_threshold:)
|
28
|
+
if precovered?
|
29
|
+
fail_with_missing_code!
|
30
|
+
elsif local_coverage_rate < (context.specific_threshold || default_threshold)
|
31
|
+
fail_with_missing_coverage!
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def fail_with_missing_code!
|
38
|
+
fail(MissingCode, <<~MESSAGE.tr("\n", " "))
|
39
|
+
Example group `#{context.scope_name}` is attempting to cover the code for class
|
40
|
+
`#{context.target_class}`, but it was located at `#{context.target_path}`,
|
41
|
+
and does not appear to have any code to cover (or it was all executed before the
|
42
|
+
tests started). If this is not the correct path for the code under test, please
|
43
|
+
specify the correct path using the `covers:` spec metadata - sometimes the
|
44
|
+
rspec-cover_it gem isn't properly able to infer the correct source path for a
|
45
|
+
class.
|
46
|
+
MESSAGE
|
47
|
+
end
|
48
|
+
|
49
|
+
def fail_with_missing_coverage!
|
29
50
|
lines = local_coverage.each_with_index.select { |v, _i| v&.zero? }.map(&:last)
|
30
51
|
|
31
52
|
summary =
|
@@ -40,8 +61,6 @@ module RSpec
|
|
40
61
|
fail(MissingCoverage, message)
|
41
62
|
end
|
42
63
|
|
43
|
-
private
|
44
|
-
|
45
64
|
attr_reader :context, :pretest_results
|
46
65
|
|
47
66
|
def target_path
|
@@ -65,6 +84,11 @@ module RSpec
|
|
65
84
|
return nil unless local_coverage
|
66
85
|
local_coverage.count { |executions| executions && executions > 0 }
|
67
86
|
end
|
87
|
+
|
88
|
+
def precovered?
|
89
|
+
return @_precovered if defined?(@_precovered)
|
90
|
+
@_precovered = pretest_coverage.nil? || pretest_coverage.none? { |n| n&.zero? }
|
91
|
+
end
|
68
92
|
end
|
69
93
|
end
|
70
94
|
end
|
@@ -3,8 +3,8 @@ module RSpec
|
|
3
3
|
class CoverageState
|
4
4
|
attr_reader :filter
|
5
5
|
|
6
|
-
def initialize(filter:)
|
7
|
-
@filter = filter
|
6
|
+
def initialize(filter: nil, autoenforce: false, default_threshold: 100.0)
|
7
|
+
@filter, @autoenforce, @default_threshold = filter, autoenforce, default_threshold
|
8
8
|
@pretest_results = nil
|
9
9
|
@context_coverages = {}
|
10
10
|
end
|
@@ -20,7 +20,7 @@ module RSpec
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def start_tracking_for(scope, rspec_context)
|
23
|
-
context =
|
23
|
+
context = context_for(scope, rspec_context)
|
24
24
|
return unless context.cover_it?
|
25
25
|
|
26
26
|
context_coverage_for(context).tap do |context_coverage|
|
@@ -29,12 +29,12 @@ module RSpec
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def finish_tracking_for(scope, rspec_context)
|
32
|
-
context =
|
32
|
+
context = context_for(scope, rspec_context)
|
33
33
|
return unless context.cover_it?
|
34
34
|
|
35
35
|
context_coverage_for(context).tap do |context_coverage|
|
36
36
|
context_coverage.postcontext_coverage = Coverage.peek_result[context.target_path]
|
37
|
-
context_coverage.enforce!
|
37
|
+
context_coverage.enforce!(default_threshold: default_threshold_rate)
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
@@ -42,6 +42,18 @@ module RSpec
|
|
42
42
|
|
43
43
|
attr_reader :pretest_results
|
44
44
|
|
45
|
+
def autoenforce?
|
46
|
+
@autoenforce
|
47
|
+
end
|
48
|
+
|
49
|
+
def default_threshold_rate
|
50
|
+
@default_threshold / 100.0
|
51
|
+
end
|
52
|
+
|
53
|
+
def context_for(scope, rspec_context)
|
54
|
+
Context.new(scope: scope, rspec_context: rspec_context, autoenforce: autoenforce?)
|
55
|
+
end
|
56
|
+
|
45
57
|
def context_coverage_for(context)
|
46
58
|
@context_coverages[context.target_class] ||= ContextCoverage.new(
|
47
59
|
context: context,
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module CoverIt
|
5
|
+
class ExampleGroupCompletenessChecker
|
6
|
+
# This class uses some bits of the RSpec::Core::ExampleGroup api that are
|
7
|
+
# not documented, and are marked `@private` using YARD notation. But I
|
8
|
+
# found no other reasonable way to answer this question, so I've isolated
|
9
|
+
# my intrusion into this class - hopefully, there will be a more
|
10
|
+
# appropriate way to determine this information in the future; I've begun
|
11
|
+
# that conversation here: https://github.com/rspec/rspec-core/issues/3037
|
12
|
+
|
13
|
+
def initialize(example_group)
|
14
|
+
@example_group = example_group
|
15
|
+
end
|
16
|
+
|
17
|
+
def running_all_examples?
|
18
|
+
all_examples == filtered_examples
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :example_group
|
24
|
+
|
25
|
+
def all_examples
|
26
|
+
@_all_examples ||= example_group.descendants.flat_map(&:examples).to_set
|
27
|
+
end
|
28
|
+
|
29
|
+
def filtered_examples
|
30
|
+
@_filtered_examples ||= example_group.descendant_filtered_examples.to_set
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
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
|
+
MissingCode = Class.new(Error)
|
6
7
|
MissingCoverage = Class.new(Error)
|
7
8
|
end
|
8
9
|
end
|
@@ -16,8 +17,8 @@ module RSpec
|
|
16
17
|
attr_accessor :state
|
17
18
|
end
|
18
19
|
|
19
|
-
def self.setup(filter:)
|
20
|
-
RSpec::CoverIt.state = CoverageState.new(filter: filter)
|
20
|
+
def self.setup(filter: nil, autoenforce: false)
|
21
|
+
RSpec::CoverIt.state = CoverageState.new(filter: filter, autoenforce: autoenforce)
|
21
22
|
RSpec::CoverIt.state.start_tracking
|
22
23
|
|
23
24
|
RSpec.configure do |config|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
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.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eric Mueller
|
@@ -99,6 +99,7 @@ files:
|
|
99
99
|
- lib/rspec/cover_it/context.rb
|
100
100
|
- lib/rspec/cover_it/context_coverage.rb
|
101
101
|
- lib/rspec/cover_it/coverage_state.rb
|
102
|
+
- lib/rspec/cover_it/example_group_completeness_checker.rb
|
102
103
|
- lib/rspec/cover_it/pretest_coverage.rb
|
103
104
|
- lib/rspec/cover_it/version.rb
|
104
105
|
- rspec-cover_it.gemspec
|