timecop 0.8.0 → 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/LICENSE +1 -1
- data/README.markdown +19 -12
- data/Rakefile +9 -3
- data/lib/timecop/time_extensions.rb +76 -31
- data/lib/timecop/time_stack_item.rb +110 -107
- data/lib/timecop/timecop.rb +85 -27
- data/lib/timecop/version.rb +1 -1
- metadata +12 -24
- data/test/run_tests.sh +0 -10
- data/test/test_helper.rb +0 -58
- data/test/time_stack_item_test.rb +0 -277
- data/test/timecop_test.rb +0 -548
- data/test/timecop_without_date_but_with_time_test.rb +0 -10
- data/test/timecop_without_date_test.rb +0 -118
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ad1963ee820d4d3519b4e078879aaa3914b2e9802460480fd9ca3c4a239677e4
|
4
|
+
data.tar.gz: 49a827fc02c8580d33d6861f51974dd046996e7281442c33f89c527d4d4c5af0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d18895efbc69f3768fcf68fa372759e727c30869e5ff1a77c8b7a2901e4a89dd49fe61df38c0a5c57dd9604250e0c49733c990c32757ac9a374a56fb3962d441
|
7
|
+
data.tar.gz: 6938b5dfc45c545d8c0e83b757f1b99d9b69b2cfe23d5e1d0f9b4fc4ff56282af201a982bcf52084f05b0de88ea36256ecbe54e2988fc944693940f5179dcb97
|
data/LICENSE
CHANGED
data/README.markdown
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
# timecop
|
2
2
|
|
3
|
-
[](https://rubygems.org/gems/timecop)
|
4
|
+
[](https://github.com/travisjeffery/timecop/actions?query=workflow%3ACI)
|
4
5
|
|
5
6
|
## DESCRIPTION
|
6
7
|
|
7
|
-
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
|
8
|
+
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.
|
8
9
|
|
9
10
|
## INSTALL
|
10
11
|
|
11
|
-
`
|
12
|
+
`bundle add timecop`
|
12
13
|
|
13
14
|
## FEATURES
|
14
15
|
|
@@ -16,13 +17,13 @@ A gem providing "time travel" and "time freezing" capabilities, making it dead s
|
|
16
17
|
- Travel back to a specific point in time, but allow time to continue moving forward from there.
|
17
18
|
- Scale time by a given scaling factor that will cause time to move at an accelerated pace.
|
18
19
|
- No dependencies, can be used with _any_ ruby project
|
19
|
-
- Timecop api allows arguments to be passed into
|
20
|
+
- Timecop api allows arguments to be passed into `#freeze` and `#travel` as one of the following:
|
20
21
|
- Time instance
|
21
22
|
- DateTime instance
|
22
23
|
- Date instance
|
23
24
|
- individual arguments (year, month, day, hour, minute, second)
|
24
|
-
- a single integer argument that is interpreted as an offset in seconds from Time.now
|
25
|
-
- Nested calls to Timecop#travel and Timecop#freeze are supported -- each block will maintain its interpretation of now.
|
25
|
+
- a single integer argument that is interpreted as an offset in seconds from `Time.now`
|
26
|
+
- Nested calls to `Timecop#travel` and `Timecop#freeze` are supported -- each block will maintain its interpretation of now.
|
26
27
|
- Works with regular Ruby projects, and Ruby on Rails projects
|
27
28
|
|
28
29
|
## USAGE
|
@@ -61,7 +62,7 @@ helpful if your whole application is time-sensitive. It allows you to build
|
|
61
62
|
your test data at a single point in time, and to move in/out of that time as
|
62
63
|
appropriate (within your tests)
|
63
64
|
|
64
|
-
in config/environments/test.rb
|
65
|
+
in `config/environments/test.rb`
|
65
66
|
|
66
67
|
```ruby
|
67
68
|
config.after_initialize do
|
@@ -73,10 +74,10 @@ end
|
|
73
74
|
|
74
75
|
### The difference between Timecop.freeze and Timecop.travel
|
75
76
|
|
76
|
-
freeze is used to statically mock the concept of now. As your program executes,
|
77
|
-
Time.now will not change unless you make subsequent calls into the Timecop API.
|
78
|
-
travel
|
79
|
-
Time.now is (recall that we support nested traveling) and the time passed in.
|
77
|
+
`freeze` is used to statically mock the concept of now. As your program executes,
|
78
|
+
`Time.now` will not change unless you make subsequent calls into the Timecop API.
|
79
|
+
`travel`, on the other hand, computes an offset between what we currently think
|
80
|
+
`Time.now` is (recall that we support nested traveling) and the time passed in.
|
80
81
|
It uses this offset to simulate the passage of time. To demonstrate, consider
|
81
82
|
the following code snippets:
|
82
83
|
|
@@ -104,7 +105,7 @@ being able to simulate activity via subsequent calls to your application.
|
|
104
105
|
Timecop.scale(3600)
|
105
106
|
Time.now
|
106
107
|
# => 2012-09-20 21:23:25 -0500
|
107
|
-
# seconds later, hours have
|
108
|
+
# seconds later, hours have passed and it's gone from 9pm at night to 6am in the morning
|
108
109
|
Time.now
|
109
110
|
# => 2012-09-21 06:22:59 -0500
|
110
111
|
```
|
@@ -128,6 +129,12 @@ Timecop.freeze
|
|
128
129
|
# => Timecop::SafeModeException: Safe mode is enabled, only calls passing a block are allowed.
|
129
130
|
```
|
130
131
|
|
132
|
+
### Rails v Ruby Date/Time libraries
|
133
|
+
|
134
|
+
Sometimes [Rails Date/Time methods don't play nicely with Ruby Date/Time methods.](https://rails.lighthouseapp.com/projects/8994/tickets/6410-dateyesterday-datetoday)
|
135
|
+
|
136
|
+
Be careful mixing Ruby `Date.today` with Rails `Date.tomorrow` / `Date.yesterday` as things might break.
|
137
|
+
|
131
138
|
## Contribute
|
132
139
|
|
133
140
|
timecop is maintained by [travisjeffery](http://github.com/travisjeffery), and
|
data/Rakefile
CHANGED
@@ -3,8 +3,6 @@ require 'bundler/gem_tasks'
|
|
3
3
|
require 'rake/testtask'
|
4
4
|
require 'rdoc/task'
|
5
5
|
|
6
|
-
$LOAD_PATH.unshift("lib")
|
7
|
-
|
8
6
|
Rake::RDocTask.new do |rdoc|
|
9
7
|
if File.exist?('VERSION')
|
10
8
|
version = File.read('VERSION')
|
@@ -21,7 +19,15 @@ Rake::RDocTask.new do |rdoc|
|
|
21
19
|
end
|
22
20
|
|
23
21
|
task :test do
|
24
|
-
|
22
|
+
failed = Dir["test/*_test.rb"].map do |test|
|
23
|
+
command = "ruby #{test}"
|
24
|
+
puts
|
25
|
+
puts command
|
26
|
+
command unless system(command)
|
27
|
+
end.compact
|
28
|
+
if failed.any?
|
29
|
+
abort "#{failed.count} Tests failed\n#{failed.join("\n")}"
|
30
|
+
end
|
25
31
|
end
|
26
32
|
|
27
33
|
desc 'Default: run tests'
|
@@ -22,24 +22,15 @@ class Time #:nodoc:
|
|
22
22
|
args.size <= 0 ? now : new_without_mock_time(*args)
|
23
23
|
end
|
24
24
|
|
25
|
+
ruby2_keywords :new_with_mock_time if Module.private_method_defined?(:ruby2_keywords)
|
26
|
+
|
25
27
|
alias_method :new, :new_with_mock_time
|
26
28
|
end
|
27
29
|
end
|
28
30
|
|
29
31
|
class Date #:nodoc:
|
30
|
-
WEEKDAYS = {
|
31
|
-
"sunday" => 0,
|
32
|
-
"monday" => 1,
|
33
|
-
"tuesday" => 2,
|
34
|
-
"wednesday" => 3,
|
35
|
-
"thursday" => 4,
|
36
|
-
"friday" => 5,
|
37
|
-
"saturday" => 6
|
38
|
-
}
|
39
|
-
|
40
32
|
class << self
|
41
33
|
def mock_date
|
42
|
-
mocked_time_stack_item = Timecop.top_stack_item
|
43
34
|
mocked_time_stack_item.nil? ? nil : mocked_time_stack_item.date(self)
|
44
35
|
end
|
45
36
|
|
@@ -54,37 +45,81 @@ class Date #:nodoc:
|
|
54
45
|
alias_method :strptime_without_mock_date, :strptime
|
55
46
|
|
56
47
|
def strptime_with_mock_date(str = '-4712-01-01', fmt = '%F', start = Date::ITALY)
|
57
|
-
|
58
|
-
|
59
|
-
|
48
|
+
#If date is not valid the following line raises
|
49
|
+
Date.strptime_without_mock_date(str, fmt, start)
|
50
|
+
|
51
|
+
d = Date._strptime(str, fmt)
|
52
|
+
now = Time.now.to_date
|
53
|
+
year = d[:year] || d[:cwyear] || now.year
|
54
|
+
mon = d[:mon] || now.mon
|
55
|
+
if d.keys == [:year]
|
56
|
+
Date.new(year, 1, 1, start)
|
57
|
+
elsif d[:mday]
|
58
|
+
Date.new(year, mon, d[:mday], start)
|
59
|
+
elsif d[:yday]
|
60
|
+
Date.new(year, 1, 1, start).next_day(d[:yday] - 1)
|
61
|
+
elsif d[:cwyear] || d[:cweek] || d[:wnum0] || d[:wnum1] || d[:wday] || d[:cwday]
|
62
|
+
week = d[:cweek] || d[:wnum1] || d[:wnum0] || now.strftime('%W').to_i
|
63
|
+
if d[:wnum0] #Week of year where week starts on sunday
|
64
|
+
if d[:cwday] #monday based day of week
|
65
|
+
Date.strptime_without_mock_date("#{year} #{week} #{d[:cwday]}", '%Y %U %u', start)
|
66
|
+
else
|
67
|
+
Date.strptime_without_mock_date("#{year} #{week} #{d[:wday] || 0}", '%Y %U %w', start)
|
68
|
+
end
|
69
|
+
else #Week of year where week starts on monday
|
70
|
+
if d[:wday] #sunday based day of week
|
71
|
+
Date.strptime_without_mock_date("#{year} #{week} #{d[:wday]}", '%Y %W %w', start)
|
72
|
+
else
|
73
|
+
Date.strptime_without_mock_date("#{year} #{week} #{d[:cwday] || 1}", '%Y %W %u', start)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
elsif d[:seconds]
|
77
|
+
Time.at(d[:seconds]).to_date
|
78
|
+
else
|
79
|
+
Date.new(year, mon, 1, start)
|
60
80
|
end
|
61
|
-
|
62
|
-
Time.strptime(str, fmt).to_date
|
63
81
|
end
|
64
82
|
|
65
83
|
alias_method :strptime, :strptime_with_mock_date
|
66
84
|
|
67
85
|
def parse_with_mock_date(*args)
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
86
|
+
parsed_date = parse_without_mock_date(*args)
|
87
|
+
return parsed_date unless mocked_time_stack_item
|
88
|
+
date_hash = Date._parse(*args)
|
89
|
+
|
90
|
+
case
|
91
|
+
when date_hash[:year] && date_hash[:mon]
|
92
|
+
parsed_date
|
93
|
+
when date_hash[:mon] && date_hash[:mday]
|
94
|
+
Date.new(mocked_time_stack_item.year, date_hash[:mon], date_hash[:mday])
|
95
|
+
when date_hash[:mday]
|
96
|
+
Date.new(mocked_time_stack_item.year, mocked_time_stack_item.month, date_hash[:mday])
|
97
|
+
when date_hash[:wday]
|
98
|
+
closest_wday(date_hash[:wday])
|
73
99
|
else
|
74
|
-
|
100
|
+
parsed_date + mocked_time_stack_item.travel_offset_days
|
75
101
|
end
|
76
102
|
end
|
77
103
|
|
78
104
|
alias_method :parse_without_mock_date, :parse
|
79
105
|
alias_method :parse, :parse_with_mock_date
|
80
106
|
|
107
|
+
def mocked_time_stack_item
|
108
|
+
Timecop.top_stack_item
|
109
|
+
end
|
110
|
+
|
111
|
+
def closest_wday(wday)
|
112
|
+
today = Date.today
|
113
|
+
result = today - today.wday
|
114
|
+
result += 1 until wday == result.wday
|
115
|
+
result
|
116
|
+
end
|
81
117
|
end
|
82
118
|
end
|
83
119
|
|
84
120
|
class DateTime #:nodoc:
|
85
121
|
class << self
|
86
122
|
def mock_time
|
87
|
-
mocked_time_stack_item = Timecop.top_stack_item
|
88
123
|
mocked_time_stack_item.nil? ? nil : mocked_time_stack_item.datetime(self)
|
89
124
|
end
|
90
125
|
|
@@ -97,19 +132,29 @@ class DateTime #:nodoc:
|
|
97
132
|
alias_method :now, :now_with_mock_time
|
98
133
|
|
99
134
|
def parse_with_mock_date(*args)
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
135
|
+
parsed_date = parse_without_mock_date(*args)
|
136
|
+
return parsed_date unless mocked_time_stack_item
|
137
|
+
date_hash = DateTime._parse(*args)
|
138
|
+
|
139
|
+
case
|
140
|
+
when date_hash[:year] && date_hash[:mon]
|
141
|
+
parsed_date
|
142
|
+
when date_hash[:mon] && date_hash[:mday]
|
143
|
+
DateTime.new(mocked_time_stack_item.year, date_hash[:mon], date_hash[:mday])
|
144
|
+
when date_hash[:mday]
|
145
|
+
DateTime.new(mocked_time_stack_item.year, mocked_time_stack_item.month, date_hash[:mday])
|
146
|
+
when date_hash[:wday]
|
147
|
+
Date.closest_wday(date_hash[:wday]).to_datetime
|
107
148
|
else
|
108
|
-
|
149
|
+
parsed_date + mocked_time_stack_item.travel_offset_days
|
109
150
|
end
|
110
151
|
end
|
111
152
|
|
112
153
|
alias_method :parse_without_mock_date, :parse
|
113
154
|
alias_method :parse, :parse_with_mock_date
|
155
|
+
|
156
|
+
def mocked_time_stack_item
|
157
|
+
Timecop.top_stack_item
|
158
|
+
end
|
114
159
|
end
|
115
160
|
end
|
@@ -1,137 +1,140 @@
|
|
1
1
|
class Timecop
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
2
|
+
# A data class for carrying around "time movement" objects. Makes it easy to keep track of the time
|
3
|
+
# movements on a simple stack.
|
4
|
+
class TimeStackItem #:nodoc:
|
5
|
+
attr_reader :mock_type
|
6
|
+
|
7
|
+
def initialize(mock_type, *args)
|
8
|
+
raise "Unknown mock_type #{mock_type}" unless [:freeze, :travel, :scale].include?(mock_type)
|
9
|
+
@travel_offset = @scaling_factor = nil
|
10
|
+
@scaling_factor = args.shift if mock_type == :scale
|
11
|
+
@mock_type = mock_type
|
12
|
+
@time = parse_time(*args)
|
13
|
+
@time_was = Time.now_without_mock_time
|
14
|
+
@travel_offset = compute_travel_offset
|
15
|
+
end
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
def year
|
18
|
+
time.year
|
19
|
+
end
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
def month
|
22
|
+
time.month
|
23
|
+
end
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
def day
|
26
|
+
time.day
|
27
|
+
end
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
29
|
+
def hour
|
30
|
+
time.hour
|
31
|
+
end
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
def min
|
34
|
+
time.min
|
35
|
+
end
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
|
37
|
+
def sec
|
38
|
+
time.sec
|
39
|
+
end
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
41
|
+
def utc_offset
|
42
|
+
time.utc_offset
|
43
|
+
end
|
44
44
|
|
45
|
-
|
46
|
-
|
47
|
-
|
45
|
+
def travel_offset
|
46
|
+
@travel_offset unless mock_type == :freeze
|
47
|
+
end
|
48
48
|
|
49
|
-
|
50
|
-
|
51
|
-
|
49
|
+
def travel_offset_days
|
50
|
+
(@travel_offset / 60 / 60 / 24).round
|
51
|
+
end
|
52
52
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
else
|
57
|
-
time = time_klass.at(@time)
|
58
|
-
end
|
53
|
+
def scaling_factor
|
54
|
+
@scaling_factor
|
55
|
+
end
|
59
56
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
time_klass.at(scaled_time)
|
66
|
-
end
|
57
|
+
def time(time_klass = Time) #:nodoc:
|
58
|
+
if @time.respond_to?(:in_time_zone)
|
59
|
+
time = time_klass.at(@time.dup.localtime)
|
60
|
+
else
|
61
|
+
time = time_klass.at(@time)
|
67
62
|
end
|
68
63
|
|
69
|
-
|
70
|
-
|
64
|
+
if travel_offset.nil?
|
65
|
+
time
|
66
|
+
elsif scaling_factor.nil?
|
67
|
+
time_klass.at(Time.now_without_mock_time + travel_offset)
|
68
|
+
else
|
69
|
+
time_klass.at(scaled_time)
|
71
70
|
end
|
71
|
+
end
|
72
72
|
|
73
|
-
|
74
|
-
|
75
|
-
|
73
|
+
def scaled_time
|
74
|
+
(@time + (Time.now_without_mock_time - @time_was) * scaling_factor).to_f
|
75
|
+
end
|
76
76
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
77
|
+
def date(date_klass = Date)
|
78
|
+
date_klass.jd(time.__send__(:to_date).jd)
|
79
|
+
end
|
80
|
+
|
81
|
+
def datetime(datetime_klass = DateTime)
|
82
|
+
if Float.method_defined?(:to_r)
|
83
|
+
fractions_of_a_second = time.to_f % 1
|
84
|
+
datetime_klass.new(year, month, day, hour, min, (fractions_of_a_second + sec), utc_offset_to_rational(utc_offset))
|
85
|
+
else
|
86
|
+
datetime_klass.new(year, month, day, hour, min, sec, utc_offset_to_rational(utc_offset))
|
84
87
|
end
|
88
|
+
end
|
85
89
|
|
86
|
-
|
90
|
+
private
|
87
91
|
|
88
|
-
|
89
|
-
|
90
|
-
|
92
|
+
def rational_to_utc_offset(rational)
|
93
|
+
((24.0 / rational.denominator) * rational.numerator) * (60 * 60)
|
94
|
+
end
|
91
95
|
|
92
|
-
|
93
|
-
|
94
|
-
|
96
|
+
def utc_offset_to_rational(utc_offset)
|
97
|
+
Rational(utc_offset, 24 * 60 * 60)
|
98
|
+
end
|
95
99
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
100
|
+
def parse_time(*args)
|
101
|
+
arg = args.shift
|
102
|
+
if arg.is_a?(Time)
|
103
|
+
arg
|
104
|
+
elsif Object.const_defined?(:DateTime) && arg.is_a?(DateTime)
|
105
|
+
time_klass.at(arg.to_time.to_f).getlocal
|
106
|
+
elsif Object.const_defined?(:Date) && arg.is_a?(Date)
|
107
|
+
time_klass.local(arg.year, arg.month, arg.day, 0, 0, 0)
|
108
|
+
elsif args.empty? && (arg.kind_of?(Integer) || arg.kind_of?(Float))
|
109
|
+
time_klass.now + arg
|
110
|
+
elsif arg.nil?
|
111
|
+
time_klass.now
|
112
|
+
else
|
113
|
+
if arg.is_a?(String) && Time.respond_to?(:parse)
|
114
|
+
time_klass.parse(arg)
|
108
115
|
else
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
minute = args.shift || 0
|
118
|
-
second = args.shift || 0
|
119
|
-
time_klass.local(year, month, day, hour, minute, second)
|
120
|
-
end
|
116
|
+
# we'll just assume it's a list of y/m/d/h/m/s
|
117
|
+
year = arg || 2000
|
118
|
+
month = args.shift || 1
|
119
|
+
day = args.shift || 1
|
120
|
+
hour = args.shift || 0
|
121
|
+
minute = args.shift || 0
|
122
|
+
second = args.shift || 0
|
123
|
+
time_klass.local(year, month, day, hour, minute, second)
|
121
124
|
end
|
122
125
|
end
|
126
|
+
end
|
123
127
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
end
|
128
|
+
def compute_travel_offset
|
129
|
+
time - Time.now_without_mock_time
|
130
|
+
end
|
128
131
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
+
def times_are_equal_within_epsilon t1, t2, epsilon_in_seconds
|
133
|
+
(t1 - t2).abs < epsilon_in_seconds
|
134
|
+
end
|
132
135
|
|
133
|
-
|
134
|
-
|
135
|
-
end
|
136
|
+
def time_klass
|
137
|
+
Time.respond_to?(:zone) && Time.zone ? Time.zone : Time
|
136
138
|
end
|
139
|
+
end
|
137
140
|
end
|