rspec-cover_it 0.0.7 → 0.1.0

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: 63e1b79c2f0b0d52869caa48e5d7dcfee100a726f1fb4698bcbef5d228179ec4
4
- data.tar.gz: 24d36270a6320b920f30134c8bb354b77325b16e4a311b1fb018470c72f0c5a7
3
+ metadata.gz: e0d27633b32f88f6dcc94af45b0ecd536b785c6229e3808ef0a1b95058f20b62
4
+ data.tar.gz: 0ade00b1ddfa4db191e9d927f6361bfcf8de243b543f2592ecbefd4a0b7f84de
5
5
  SHA512:
6
- metadata.gz: 3f8774a17ea6e1170471182cf768cbd7fc0e3f69a8cf2d04445d7a7ae0e7e04947b6a75205aa2d53eaaeef104597447608b3873413b73bfda2d9b83ce015bec6
7
- data.tar.gz: 3cf13d18a7658e6769d672e1d130ace9e3842ce1e3e7edfdf21e3c64753fb47668da6c7fa16690bc78e6741aa58cd03bdf1344d7c98b50a6fdbf51de03ba5ec1
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
@@ -0,0 +1,2 @@
1
+ all
2
+ rule "MD013", ignore_code_blocks: true
data/.mdlrc ADDED
@@ -0,0 +1,2 @@
1
+ style File.expand_path("../.mdl_rules.rb", __FILE__)
2
+ git_recurse true
@@ -0,0 +1,8 @@
1
+ ---
2
+ default_tools: ["standardrb", "rubocop", "markdown_lint", "rspec"]
3
+ executor: concurrent
4
+ comparison_branch: main
5
+ changed_files: false
6
+ filter_messages: false
7
+ logging: light
8
+ colorize: true
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
- Still a work in progress, but this gem is intended to give us a simpler and
4
- more _targeted_ way to manage coverage. The usual system for a large project
5
- is a single overarching SimpleCov target; with good tooling, there will be
6
- something checking that all of the lines of each PR get covered by the tests
7
- in that PR, and there are checks that the overall coverage number doesn't drop
8
- too far.. but it's a _sloppy, messy, lossy_ approach. Code loses coverage,
9
- code gets added with no tests, tests get removed and other code becomes less
10
- covered, and a lot of the time, that's because the _coverage is a lie_.
11
-
12
- Your coverage tool is telling you.. how many times each line of code _got run_
13
- during your entire test suite. But it's not telling you what is doing the
14
- running, which means that often large swathes of code are actually only covered
15
- incidentally, through tests that aren't intended to exercise that code, but
16
- _happens to_.
17
-
18
- This approach was moderately inspired by https://github.com/jamesdabbs/rspec-coverage,
19
- which was itself apparently inspired by a rubyconf 2016 talk by Ryan Davis. That
20
- talk and library are mostly concerned with making sure that we don't _overcount_
21
- coverage though, while this one has three goals:
22
-
23
- 1. Make coverage enforcement simpler
24
- 1. Enforce coverage _in the test suite_ (or nearly so)
25
- 1. Make coverage enforcement more local and specific.
26
-
27
- ## Tentative Usage
28
-
29
- Note that this bit isn't really implemented, but is more of an outline of how I
30
- intend the library to work. This is currently a spike, and not a functioning gem.
31
-
32
- You set up `RSpec::CoverIt` in your `spec_helper.rb` - require `rspec/cover_it`,
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
- rspec
98
-
99
- Randomized with seed 29392
100
- ...............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
101
- An error occurred in an `after(:context)` hook.
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
- ## Drawbacks and Shortcomings
115
-
116
- There's nothing in here that stops you from failing to write tests for a class
117
- at all! If you're using SimpleCov and you've got 100% coverage already, that's
118
- one of the benefits.. I could pretty reasonably include some kind of
119
- `after(:suite)` hook that optionally checks net coverage, but.. simplecov does
120
- that, and the concurrent-testing game makes this a _painful_ topic in reality.
121
- That's not the goal here, and I'm not going to worry about it.
122
-
123
- As initially implemented, it fails your tests if you don't run the entire test
124
- file. `rspec spec/foo_spec.rb:32` will error, because .. running only one of
125
- your tests _doesn't cover the class_. I have a solution for this, but it uses
126
- some non-public bits of RSpec, so I'm trying to find a better answer still.
127
- (Conversation started in their
128
- [issue tracker](https://github.com/rspec/rspec-core/issues/3037))
129
-
130
- We're using `Object.const_source_location` to find the path of the source file
131
- defining a given constant. That _mostly_ works, but it actually gives the path
132
- of the _first_ source file that defined that constant. So if your gem defines
133
- its version in `lib/foo/version.rb` (as an example), in a separate file from
134
- lib/foo.rb, the _path_ for `Foo` may end up being the former. Which is.. not
135
- going to have much coverable code, of course. This is an edge case, but one
136
- that is likely to occur fairly regularly. I haven't thought of a _good_ solution
137
- yet. Perhaps if the `covers` array includes a string, we should treat it as a
138
- relative path?
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
- Object.const_source_location(target_class_name).first
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
- return nil unless precontext_coverage && postcontext_coverage
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
- lines = local_coverage.each_with_index.select { |v, _i| v&.zero? }.map(&:last)
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}` #{summary}
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
- attr_reader :filter
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
- context_coverage_for(context).tap do |context_coverage|
27
- context_coverage.precontext_coverage = get_current_coverage(context.target_path)
28
- end
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
- context_coverage_for(context).tap do |context_coverage|
36
- context_coverage.postcontext_coverage = get_current_coverage(context.target_path)
37
- context_coverage.enforce!(default_threshold: default_threshold_rate)
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
- if result.any? { |_k, v| v.is_a?(Hash) }
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
@@ -1,5 +1,5 @@
1
1
  module RSpec
2
2
  module CoverIt
3
- VERSION = "0.0.7"
3
+ VERSION = "0.1.0"
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
+ 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
- RSpec::CoverIt.state.start_tracking
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
@@ -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.7
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-07 00:00:00.000000000 Z
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