Skip to content

Commit

Permalink
Adds mocks for Process.clock_gettime for both monotonic and realtime …
Browse files Browse the repository at this point in the history
…clocks

Handles travisjeffery#220
  • Loading branch information
wishdev committed Jul 30, 2020
1 parent b794cf6 commit 09900ff
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 0 deletions.
55 changes: 55 additions & 0 deletions lib/timecop/time_extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,58 @@ def mocked_time_stack_item
end
end
end

module Process #:nodoc:
class << self
alias_method :clock_gettime_without_mock, :clock_gettime

def clock_gettime_mock_time(clock_id, unit = :float_second)
mock_time = case clock_id
when Process::CLOCK_MONOTONIC
mock_time_monotonic
when Process::CLOCK_REALTIME
mock_time_realtime
end

return clock_gettime_without_mock(clock_id, unit) unless mock_time

divisor = case unit
when :float_second
1_000_000_000.0
when :second
1_000_000_000
when :float_millisecond
1_000_000.0
when :millisecond
1_000_000
when :float_microsecond
1000.0
when :microsecond
1000
when :nanosecond
1
end

(mock_time / divisor)
end

alias_method :clock_gettime, :clock_gettime_mock_time

private

def mock_time_monotonic
mocked_time_stack_item = Timecop.top_stack_item
mocked_time_stack_item.nil? ? nil : mocked_time_stack_item.monotonic
end

def mock_time_realtime
mocked_time_stack_item = Timecop.top_stack_item

return nil if mocked_time_stack_item.nil?

t = mocked_time_stack_item.time
t.to_i * 1_000_000_000 + t.nsec
end
end
end

16 changes: 16 additions & 0 deletions lib/timecop/time_stack_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ def initialize(mock_type, *args)
@travel_offset = @scaling_factor = nil
@scaling_factor = args.shift if mock_type == :scale
@mock_type = mock_type
@monotonic = current_monotonic
@time = parse_time(*args)
@time_was = Time.now_without_mock_time
@travel_offset = compute_travel_offset
Expand Down Expand Up @@ -54,6 +55,20 @@ def scaling_factor
@scaling_factor
end

def monotonic
if travel_offset.nil?
@monotonic
elsif scaling_factor.nil?
current_monotonic + travel_offset * (10 ** 9)
else
(@monotonic + (current_monotonic - @monotonic) * scaling_factor).to_i
end
end

def current_monotonic
Process.clock_gettime_without_mock(Process::CLOCK_MONOTONIC, :nanosecond)
end

def time(time_klass = Time) #:nodoc:
if @time.respond_to?(:in_time_zone)
time = time_klass.at(@time.dup.localtime)
Expand Down Expand Up @@ -106,6 +121,7 @@ def parse_time(*args)
elsif Object.const_defined?(:Date) && arg.is_a?(Date)
time_klass.local(arg.year, arg.month, arg.day, 0, 0, 0)
elsif args.empty? && (arg.kind_of?(Integer) || arg.kind_of?(Float))
@monotonic += arg * 1_000_000_000
time_klass.now + arg
elsif arg.nil?
time_klass.now
Expand Down
6 changes: 6 additions & 0 deletions lib/timecop/timecop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ class << self
# previous values after the block has finished executing. This allows us to nest multiple
# calls to Timecop.travel and have each block maintain it's concept of "now."
#
# The Process.clock_gettime call mocks both CLOCK::MONOTIC and CLOCK::REALTIME
#
# CLOCK::MONOTONIC works slightly differently than other clocks. This clock cannot move to a
# particular date/time. So the only option that changes this clock is #4 which will move the
# clock the requested offset. Otherwise the clock is frozen to the current tick.
#
# * Note: Timecop.freeze will actually freeze time. This can cause unanticipated problems if
# benchmark or other timing calls are executed, which implicitly expect Time to actually move
# forward.
Expand Down
80 changes: 80 additions & 0 deletions test/timecop_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -599,8 +599,88 @@ def test_thread_safe_timecop
Timecop.thread_safe = false
end

def test_process_clock_gettime_monotonic
Timecop.freeze do
assert_equal monotonic, monotonic, "CLOCK_MONOTONIC is not frozen"
end

current = monotonic
Timecop.freeze(-0.5) do
assert monotonic < current, "CLOCK_MONOTONIC is not traveeling back in time"
end
end

def test_process_clock_gettime_monotonic_travel
current = monotonic
Timecop.travel do
refute_equal monotonic, monotonic, "CLOCK_MONOTONIC is frozen"
assert monotonic > current, "CLOCK_MONOTONIC is not moving forward"
end

Timecop.travel(-0.5) do
refute_equal monotonic, monotonic, "CLOCK_MONOTONIC is frozen"
assert monotonic < current, "CLOCK_MONOTONIC is not travelling properly"
sleep 0.5
assert monotonic > current, "CLOCK_MONOTONIC is not travelling properly"
end
end

def test_process_clock_gettime_monotonic_scale
scale = 4
sleep_length = 0.25
Timecop.scale(scale) do
current = monotonic
sleep(sleep_length)
assert current + scale * sleep_length < monotonic, "CLOCK_MONOTONIC is not scaling"
end
end

def test_process_clock_gettime_realtime
Timecop.freeze do
assert_equal realtime, realtime, "CLOCK_REALTIME is not frozen"
end

current = realtime
Timecop.freeze(-20) do
assert realtime < current, "CLOCK_REALTIME is not traveeling back in time"
end
end

def test_process_clock_gettime_realtime_travel
current = realtime
Timecop.travel do
refute_equal realtime, realtime, "CLOCK_REALTIME is frozen"
assert realtime > current, "CLOCK_REALTIME is not moving forward"
end

Timecop.travel(Time.now - 0.5) do
refute_equal realtime, realtime, "CLOCK_REALTIME is frozen"
assert realtime < current, "CLOCK_REALTIME is not travelling properly"
sleep 0.5
assert realtime > current, "CLOCK_REALTIME is not travelling properly"
end
end

def test_process_clock_gettime_realtime_scale
scale = 4
sleep_length = 0.25
Timecop.scale(scale) do
current = realtime
sleep(sleep_length)
assert current + scale * sleep_length < realtime, "CLOCK_REALTIME is not scaling"
end
end

private

def monotonic
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end

def realtime
Process.clock_gettime(Process::CLOCK_REALTIME)
end

def with_safe_mode(enabled=true)
mode = Timecop.safe_mode?
Timecop.safe_mode = enabled
Expand Down

0 comments on commit 09900ff

Please sign in to comment.