Class: RuboCop::Cop::DevDoc::Test::RequireGlibTravelBlock

Inherits:
Base
  • Object
show all
Defined in:
lib/rubocop/cop/dev_doc/test/require_glib_travel_block.rb

Overview

glib_travel must be called with a block — the bare anchor form leaks frozen time past the test.

Rationale

glib_travel(point) do ... end auto-returns to real time at the end of the block, so a test cannot leak a frozen clock into the next one. The bare anchor form glib_travel(point) (no block) freezes time until a matching glib_travel_back, which is easy to forget in teardown and then silently corrupts every test that runs afterward. This is the form that has repeatedly slipped in alongside the correct block form.

What is NOT flagged

  • The block form glib_travel(point) do ... end (the correct form).
  • A bare DURATION argument that advances the frozen clock across multiple requests within one test, e.g. glib_travel(61.seconds), glib_travel(1.hour), glib_travel(-5.minutes), or glib_travel(5.minutes + 30.seconds). This is the accepted non-block form for rate-limiting / clock-tick tests; it is recognized by a Numeric#second(s)/minute(s)/hour(s)/day(s)/ week(s)/month(s)/year(s) leaf, optionally unary-negated (-5.minutes) or composed additively/subtractively (5.minutes + 30.seconds), and is paired with an explicit glib_travel_back. An absolute point expression such as Time.current, DateTime.new(...), or 2.hours.from_now is NOT a duration and is flagged. A duration reached through anything the matcher cannot see statically — a constant or variable holding a duration (glib_travel(SOME_DURATION)), a method call, or multiplication/division (glib_travel(5.minutes * 2)) — is NOT recognized; inline the literal expression or # rubocop:disable with a reason.
  • glib_travel_freeze (covered by DevDoc/Test/AvoidGlibTravelFreeze).

Inline disable

For the rare case where the bare anchor form is genuinely required, add a rubocop:disable DevDoc/Test/RequireGlibTravelBlock comment with a written reason.

Examples:

# bad (bare anchor — leaks frozen time without glib_travel_back)
glib_travel(FROZEN_TIME)
get items_url(format: :json)

# good
glib_travel(FROZEN_TIME) do
  get items_url(format: :json)
end

# good (bare duration tick — advances the clock, paired with travel_back)
glib_travel(61.seconds)

Constant Summary collapse

MSG =
'Use the block form `glib_travel(time) do ... end` — the bare anchor ' \
'form leaks frozen time past the test unless paired with ' \
'`glib_travel_back`. Use `# rubocop:disable ' \
'DevDoc/Test/RequireGlibTravelBlock` with a reason if the bare form ' \
'is genuinely required.'.freeze
RESTRICT_ON_SEND =
%i[glib_travel].freeze

Instance Method Summary collapse

Instance Method Details

#duration_argument?(node) ⇒ Object

A duration unit — the leaf of a duration expression. Both singular and plural ActiveSupport forms (1.hour, 2.minutes). Receiver is unconstrained; in practice it is always a Numeric literal.



67
68
69
# File 'lib/rubocop/cop/dev_doc/test/require_glib_travel_block.rb', line 67

def_node_matcher :duration_argument?, <<~PATTERN
  (send _ {:second :seconds :minute :minutes :hour :hours :day :days :week :weeks :month :months :year :years})
PATTERN

#on_send(node) ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/rubocop/cop/dev_doc/test/require_glib_travel_block.rb', line 71

def on_send(node)
  # `glib_travel` is a global test helper; a receiver-bearing call
  # (`obj.glib_travel`) is a different method and none of our
  # business — but `self.glib_travel` IS the same call and must not
  # become a one-token dodge of the block-form rule.
  return if node.receiver && !node.receiver.self_type?

  return if block_form?(node)
  return if bare_duration_tick?(node)

  add_offense(node.loc.selector)
end