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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7646cf84b3b24df43ca171de7628e65347bbdc8270dd4ff1de2c3bdc6d44596f
4
- data.tar.gz: 47e86f853552fadfa9f64a478871fad62db530e4840d8887a6513593a6505032
3
+ metadata.gz: b3efab53f0b9f05be2f5a8ae7f170e4a3a8e886439a751e025926c61bf9ac8f1
4
+ data.tar.gz: 569fbdcc5f328d5d0656eb3fe20265c9cd164455cfbf290c7764b92d15454fbf
5
5
  SHA512:
6
- metadata.gz: 114cb6e33e6867b1c4e4dadc43690e67feeb01cbcdab39bcd24d3d563e65323e43eb09dd2d78f8c5056f1b27faad3ba0f9383a31b5fb216674c92998dc718258
7
- data.tar.gz: d89f76882f68ffbf6978407fa6f588e29fa87cf66f6cd64bc985af9510232168204d81925975f739e9cf2961e345b871a45c7ac8894e75f0ea0db6855a2d60d1
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 first.
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
- So far, I haven't found a _great_ way to report that there's a problem. The
123
- output from having insufficient coverage is _raising an exception from an
124
- `after(:context)` hook_, which RSpec rescues and formats for itself, and there
125
- aren't a lot of controls - it works, but it's a bit ugly and doesn't really
126
- give the right immediate impression. I'm contemplating using an `after(:suite)`
127
- hook and aggregating them myself, but at the end of the day RSpec is in control
128
- of the output stream, and we don't entirely fit its metaphor.
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 && metadata.fetch(:cover_it, nil)
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
- Object.const_source_location(target_class_name).first
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
- return if local_coverage_rate >= 1.0
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 = Context.new(scope: scope, rspec_context: rspec_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 = Context.new(scope: scope, rspec_context: rspec_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
@@ -4,7 +4,7 @@ module RSpec
4
4
  def initialize(filter:, results:)
5
5
  @filter = filter
6
6
  @results = results
7
- .select { |k, _v| k.start_with?(filter) }
7
+ .select { |k, _v| filter.nil? || k.start_with?(filter) }
8
8
  .map { |k, v| [k, v.dup] }
9
9
  .to_h
10
10
  end
@@ -1,5 +1,5 @@
1
1
  module RSpec
2
2
  module CoverIt
3
- VERSION = "0.0.3"
3
+ VERSION = "0.0.5"
4
4
  end
5
5
  end
@@ -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.3
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