Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
jch committed Aug 14, 2024
2 parents c301095 + ce3e6bb commit 1007bdc
Show file tree
Hide file tree
Showing 9 changed files with 320 additions and 3 deletions.
1 change: 1 addition & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
matrix:
ruby:
- head
- '3.3'
- '3.2'
- '3.1'
- '3.0'
Expand Down
10 changes: 9 additions & 1 deletion History.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@

## Unreleased

- Add `travelled?` and `scaled?` methods to allow checking if Timecop is in their respective states ([#414](https://github.com/travisjeffery/timecop/pull/414))
## v0.9.10

- Make Process.clock_gettime configurable and turned off by default (for backwards compatability) ([#427](https://github.com/travisjeffery/timecop/pull/427))

## v0.9.9

- Add `travelled?` and `scaled?` methods to allow checking if Timecop is in their respective states ([#414](https://github.com/travisjeffery/timecop/pull/414))
- Fix cases with DateTime parse not working right ([#415](https://github.com/travisjeffery/timecop/pull/415))
- Fix another case where DateTime parse not working right ([#417](https://github.com/travisjeffery/timecop/pull/417))
- Support travel and freeze for Process.clock_gettime ([#419](https://github.com/travisjeffery/timecop/pull/419))

## v0.9.8

Expand Down
11 changes: 10 additions & 1 deletion README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

## DESCRIPTION

A gem providing "time travel" and "time freezing" capabilities, making it dead simple to test time-dependent code. It provides a unified method to mock `Time.now`, `Date.today`, and `DateTime.now` in a single call.
A gem providing "time travel" and "time freezing" capabilities, making it dead simple to test time-dependent code. It provides a unified method to mock `Time.now`, `Date.today`, `DateTime.now`, and `Process.clock_gettime` in a single call.

## INSTALL

Expand Down Expand Up @@ -129,6 +129,15 @@ Timecop.freeze
# => Timecop::SafeModeException: Safe mode is enabled, only calls passing a block are allowed.
```

### Configuring Mocking Process.clock_gettime

By default Timecop does not mock Process.clock_gettime. You must enable it like this:

``` ruby
# turn on
Timecop.mock_process_clock = true
```

### Rails v Ruby Date/Time libraries

Sometimes [Rails Date/Time methods don't play nicely with Ruby Date/Time methods.](https://rails.lighthouseapp.com/projects/8994/tickets/6410-dateyesterday-datetoday)
Expand Down
65 changes: 65 additions & 0 deletions lib/timecop/time_extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,17 @@ def parse_with_mock_date(*args)
DateTime.new(mocked_time_stack_item.year, date_hash[:mon], date_hash[:mday])
when date_hash[:mday]
DateTime.new(mocked_time_stack_item.year, mocked_time_stack_item.month, date_hash[:mday])
when date_hash[:wday] && date_hash[:hour] && date_hash[:min]
closest_date = Date.closest_wday(date_hash[:wday]).to_datetime

DateTime.new(
closest_date.year, closest_date.month, closest_date.day,
date_hash[:hour], date_hash[:min]
)
when date_hash[:wday]
Date.closest_wday(date_hash[:wday]).to_datetime
when date_hash[:hour] && date_hash[:min] && date_hash[:sec]
DateTime.new(mocked_time_stack_item.year, mocked_time_stack_item.month, mocked_time_stack_item.day, date_hash[:hour], date_hash[:min], date_hash[:sec])
else
parsed_date + mocked_time_stack_item.travel_offset_days
end
Expand All @@ -158,3 +167,59 @@ def mocked_time_stack_item
end
end
end

if RUBY_VERSION >= '2.1.0'
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 Timecop.mock_process_clock? && 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
end
31 changes: 31 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 = parse_monotonic_time(*args) if RUBY_VERSION >= '2.1.0'
@time = parse_time(*args)
@time_was = Time.now_without_mock_time
@travel_offset = compute_travel_offset
Expand Down Expand Up @@ -54,6 +55,26 @@ def scaling_factor
@scaling_factor
end

if RUBY_VERSION >= '2.1.0'
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 current_monotonic_with_mock
Process.clock_gettime_mock_time(Process::CLOCK_MONOTONIC, :nanosecond)
end
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 @@ -97,6 +118,16 @@ def utc_offset_to_rational(utc_offset)
Rational(utc_offset, 24 * 60 * 60)
end

def parse_monotonic_time(*args)
arg = args.shift
offset_in_nanoseconds = if args.empty? && (arg.kind_of?(Integer) || arg.kind_of?(Float))
arg * 1_000_000_000
else
0
end
current_monotonic_with_mock + offset_in_nanoseconds
end

def parse_time(*args)
arg = args.shift
if arg.is_a?(Time)
Expand Down
14 changes: 14 additions & 0 deletions lib/timecop/timecop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,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 Expand Up @@ -136,6 +142,14 @@ def scaled?
!instance.stack.empty? && instance.stack.last.mock_type == :scale
end

def mock_process_clock=(mock)
@mock_process_clock = mock
end

def mock_process_clock?
@mock_process_clock ||= false
end

private
def send_travel(mock_type, *args, &block)
val = instance.travel(mock_type, *args, &block)
Expand Down
2 changes: 1 addition & 1 deletion lib/timecop/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
class Timecop
VERSION = "0.9.8"
VERSION = "0.9.10"
end
8 changes: 8 additions & 0 deletions test/date_time_parse_scenarios.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,18 @@ def test_date_time_parse_Date_10_slash_10
assert_equal DateTime.parse("2008-10-10"), DateTime.parse('Date 10/10')
end

def test_date_time_parse_time_only_scenario
assert_equal DateTime.parse("2008-09-01T15:00:00"), DateTime.parse('15:00:00')
end

def test_date_time_parse_month_year
assert_equal DateTime.parse("2012-12-01"), DateTime.parse('DEC 2012')
end

def test_date_time_parse_wday_with_hour
assert_equal DateTime.parse("2008-09-06T13:00:00"), DateTime.parse('Saturday 13:00')
end

def test_date_time_parse_non_string_raises_expected_error
assert_raises(TypeError) { DateTime.parse(Object.new) }
end
Expand Down
Loading

0 comments on commit 1007bdc

Please sign in to comment.