Repository navigation
Credit before(:all) hooks to their own spec file - #1320
Conversation
sferik
left a comment
There was a problem hiding this comment.
Thanks! Hooks like before(:all) and let_it_be being credited to the previous spec file is a real gap and a boundary before each spec file's setup is the right fix. A few inline comments. The main one is that nested groups still have the problem.
This also touches rspec_example_id's file, and #1319 changes that method, so whichever lands second will need a rebase.
| # Only a top-level group starts its spec file; a nested group's file | ||
| # already started. | ||
| def rspec_group_starting(group) | ||
| return unless group.top_level? |
There was a problem hiding this comment.
This leaves the same problem in nested groups. Take this spec file:
RSpec.describe Widget do
it("a") { ... } # line 5
context "when set up" do
before(:all) { Setup.call }
it("b") { ... } # line 12
end
endThe nested before(:all) runs after a finishes, outside any test, so at :test granularity Setup's lines go to spec/b_spec.rb:5. A change to Setup then selects only a, which never runs that hook, and b is never selected.
Since the hook runs outside any test either way, could every group mark the boundary, not just top-level ones? It would credit nested setup to the file too. A sandbox case with a nested before(:all) after an example would cover it.
| SimpleCov.track_test(TestTracker.rspec_example_id(example)) { example.run } | ||
| end | ||
| config.prepend_before(:suite) { SimpleCov.test_tracker&.suite_starting } | ||
| config.prepend_before(:context) { TestTracker.rspec_group_starting(self.class) } |
There was a problem hiding this comment.
prepend_before only gets ahead of hooks added after it. Two kinds still run first:
- A config hook added later with
config.prepend_before(:all), for example inspec_helper.rbafterSimpleCov.start. RSpec unshifts it in front of ours. - A group's own
prepend_before(:all). RSpec copies the global context hooks into a top-level group when it defines the group, before it evaluates the group body, so a prepend in the body goes in front of them.
Either one runs while the previous segment is still open, so its lines still go to the previous spec file.
ExampleGroup.run calls reporter.example_group_started(group) before any context hook, so a reporter listener on example_group_started would mark the boundary no matter when or where hooks are added. It also fires for nested groups, which would cover the comment above.
| def rspec_group_starting(group) | ||
| return unless group.top_level? | ||
|
|
||
| SimpleCov.test_tracker&.test_file_starting(group.metadata.fetch(:rerun_file_path).delete_prefix("./")) |
There was a problem hiding this comment.
This repeats the delete_prefix("./") from rspec_example_id. The two have to produce the same file id, or at :file granularity a file's setup and its examples become two contexts. A shared helper would keep them in step.
Also, the PR description says this is the same id file loading uses. test_file_loading builds it relative to SimpleCov.root, but rerun_file_path is relative to the working directory, so they differ when rspec runs from a subdirectory (an engine, for example). Example ids already have that mismatch, so it isn't new here, but a shared helper would be the place to fix it for both.
| spec file triggers with `require "rails_helper"` belongs to every test rather than the spec file that came first. | ||
| Test files are recognized by name (`*_spec.rb`, `*_test.rb`, and `test_*.rb` other than `test_helper.rb`). JRuby | ||
| cannot report a file as it loads, so there a test file's loading belongs to no test. | ||
| cannot report a file as it loads, so there a test file's loading belongs to no test. What an RSpec spec file's |
There was a problem hiding this comment.
This is true only for top-level groups (see the comment on rspec_group_starting). If that stays, could the docs say so? Also, coming right after the JRuby sentence, "too" reads as if it were about JRuby. Starting a new paragraph, or dropping "too", would avoid that.
| SimpleCov.track_test(TestTracker.rspec_example_id(example)) { example.run } | ||
| end | ||
| config.prepend_before(:suite) { SimpleCov.test_tracker&.suite_starting } | ||
| config.prepend_before(:context) { TestTracker.rspec_group_starting(self.class) } |
There was a problem hiding this comment.
At :test granularity this adds one Coverage.peek_result per spec file, since the file's segment and its first example have different ids. That happens even in suites with no before(:all) at all. It's one per file, not per example, so the cost is modest, but it might be worth checking whether the group has any before(:context) hooks first, if RSpec exposes that cleanly.
| end | ||
|
|
||
| # Only a top-level group starts its spec file; a nested group's file | ||
| # already started. |
There was a problem hiding this comment.
Nit: could this be two sentences instead of a semicolon? "Only a top-level group starts its spec file. A nested group's file already started."
6a6ac21 to
591bb08
Compare
|
Thanks for the review. I reworked the change around RSpec's reporter, as you suggested. 1 and 2. Nested groups, and hooks that run before ours: the tracker no longer adds a
I squashed the changes into the one commit. #1319 also changes |
RSpec runs a group's before(:all) hooks once, before the group's first example starts. track_tests records each example, so the lines those hooks ran went to the spec file that ran before, or to no test for the first file. A change to code only such a hook ran then made `simplecov affected` pick that earlier spec file and skip the one that needs the hook. RSpec's reporter announces each group, nested ones too, before any of its hooks run, whatever order they were added in. The tracker listens for that and starts the group's spec file there, so what runs from then until the next example is credited to the file. Under granularity: :file this costs no extra peek; at :test it costs one peek per group. Minitest has no such hook. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
591bb08 to
07febce
Compare
|
Thanks again! |
The schema and the Formatters and Redundant_Tests docs described a spec file's bare path as only its loading. Since before(:all) hooks are credited to the same id, say that it holds what the file ran outside its tests: its loading and its groups' before(:all) hooks. Also link the CHANGELOG entry to its pull request. Follows up #1320.
RSpec runs a group's
before(:all)hooks once, before the group's first example starts.track_testsrecords each example, so their lines stayed in the segment still open from the spec file that ran before, or went to no test for the first file. So when code that only such a hook runs was changed,simplecov affectedpicked that earlier spec file, not the spec file that uses the hook.When a top-level group starts, its spec file now starts too, before the group's own hooks run (a config-level
prepend_before(:context)). What runs from there is credited to the file, asspec/b_spec.rb, the same id that file loading uses.This matters when some code runs only in
before(:all). A common case is records created once per file with test-prof'slet_it_be: their model callbacks run there and in no example.Reproduction
After
rspec --order defined, a commit, andsed -i 's/n = 1$/n = 2/' lib/widget.rb:affectedaffected:file, a then bspec/a_spec.rb/spec/a_spec.rbspec/b_before_all_spec.rb/ same:file, b alonespec/b_before_all_spec.rb/ same:test, a then bspec/a_spec.rb:3/spec/a_spec.rbspec/b_before_all_spec.rb/ sameNotes
:filethe new segment has the id of the file's examples and merges with them, so no extra peek. At:testit is oneCoverage.peek_resultper top-level group.before(:all)at:teststill goes to the previous example of the same file, andafter(:all)goes to the file's last example (at:file, to the file). Minitest has no hook outside a test.open_segmentsaid code between two tracks goes to the later one. It goes to the earlier one, which is how this bug happened, so the comment now says that.Verification
rspecrun wherespec/setting_up_spec.rbcallsSetupOnce.callfrombefore(:all).covering(lib/setup_once.rb, 3)was["spec/meta_magic_spec.rb:14"], from the spec file that ran before, on main and is["spec/setting_up_spec.rb"]here.rake spec,rubocop,rake rbs,steep check, andrake 'mutant:since[origin/main]'(0 alive) pass.