From a491da62a00487dcc6efe5cdcb1378ac951bba77 Mon Sep 17 00:00:00 2001 From: Philipp Thun Date: Fri, 25 Sep 2026 09:58:14 +0200 Subject: [PATCH] Fail specs that inherit a leaked Timecop clock A spec that freezes or travels time without returning it corrupts every following example that derives a value from Time.now, such as the decorator specs that set created_at to Time.now.utc - 1.second to assert an ordering. The leaked clock reverses that ordering, producing flaky failures far from the spec that actually leaked. The global after(:each) that returns time silently absorbs such a leak. Add a prepend_before that detects an already-active Timecop clock at example entry and fails immediately, naming the example that inherited the leak so the real culprit can be bisected out. --- spec/spec_helper_helper.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/spec/spec_helper_helper.rb b/spec/spec_helper_helper.rb index 99aa7e9dd81..5872308c880 100644 --- a/spec/spec_helper_helper.rb +++ b/spec/spec_helper_helper.rb @@ -169,6 +169,25 @@ def self.each_run TmpdirCleaner.clean end + # Guard against a leaked Timecop clock. A spec that freezes or travels + # time without returning it corrupts every following example that derives + # a value from Time.now (e.g. `created_at: Time.now.utc - 1.second`), + # producing order-dependent flakes far from the real culprit. The global + # `after { Timecop.return }` below would silently absorb such a leak, so + # detect it at example entry and fail loudly, naming the example that + # inherited the leaked clock -- its predecessor is the one to fix. + rspec_config.prepend_before do + if Timecop.top_stack_item + leaked = Timecop.top_stack_item + Timecop.return + raise 'Timecop clock leaked into this example from a predecessor ' \ + "(mock type=#{leaked.mock_type}, mocked now=#{Time.now.utc.iso8601}). " \ + 'A prior spec froze or travelled time without returning it. Find the ' \ + 'predecessor (run with the same --seed and bisect) and pair its ' \ + 'Timecop.freeze/travel with Timecop.return (or use the block form).' + end + end + rspec_config.after do Timecop.return end