arborist 0.0.1.pre20160106113421

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.document +4 -0
  3. data/.simplecov +9 -0
  4. data/ChangeLog +417 -0
  5. data/Events.md +20 -0
  6. data/History.md +4 -0
  7. data/LICENSE +29 -0
  8. data/Manifest.txt +72 -0
  9. data/Monitors.md +141 -0
  10. data/Nodes.md +0 -0
  11. data/Observers.md +72 -0
  12. data/Protocol.md +214 -0
  13. data/README.md +75 -0
  14. data/Rakefile +81 -0
  15. data/TODO.md +24 -0
  16. data/bin/amanagerd +10 -0
  17. data/bin/amonitord +12 -0
  18. data/bin/aobserverd +12 -0
  19. data/lib/arborist.rb +182 -0
  20. data/lib/arborist/client.rb +191 -0
  21. data/lib/arborist/event.rb +61 -0
  22. data/lib/arborist/event/node_acked.rb +18 -0
  23. data/lib/arborist/event/node_delta.rb +20 -0
  24. data/lib/arborist/event/node_matching.rb +34 -0
  25. data/lib/arborist/event/node_update.rb +19 -0
  26. data/lib/arborist/event/sys_reloaded.rb +15 -0
  27. data/lib/arborist/exceptions.rb +21 -0
  28. data/lib/arborist/manager.rb +508 -0
  29. data/lib/arborist/manager/event_publisher.rb +97 -0
  30. data/lib/arborist/manager/tree_api.rb +207 -0
  31. data/lib/arborist/mixins.rb +363 -0
  32. data/lib/arborist/monitor.rb +377 -0
  33. data/lib/arborist/monitor/socket.rb +163 -0
  34. data/lib/arborist/monitor_runner.rb +217 -0
  35. data/lib/arborist/node.rb +700 -0
  36. data/lib/arborist/node/host.rb +87 -0
  37. data/lib/arborist/node/root.rb +60 -0
  38. data/lib/arborist/node/service.rb +112 -0
  39. data/lib/arborist/observer.rb +176 -0
  40. data/lib/arborist/observer/action.rb +125 -0
  41. data/lib/arborist/observer/summarize.rb +105 -0
  42. data/lib/arborist/observer_runner.rb +181 -0
  43. data/lib/arborist/subscription.rb +82 -0
  44. data/spec/arborist/client_spec.rb +282 -0
  45. data/spec/arborist/event/node_update_spec.rb +71 -0
  46. data/spec/arborist/event_spec.rb +64 -0
  47. data/spec/arborist/manager/event_publisher_spec.rb +66 -0
  48. data/spec/arborist/manager/tree_api_spec.rb +458 -0
  49. data/spec/arborist/manager_spec.rb +442 -0
  50. data/spec/arborist/mixins_spec.rb +195 -0
  51. data/spec/arborist/monitor/socket_spec.rb +195 -0
  52. data/spec/arborist/monitor_runner_spec.rb +152 -0
  53. data/spec/arborist/monitor_spec.rb +251 -0
  54. data/spec/arborist/node/host_spec.rb +104 -0
  55. data/spec/arborist/node/root_spec.rb +29 -0
  56. data/spec/arborist/node/service_spec.rb +98 -0
  57. data/spec/arborist/node_spec.rb +552 -0
  58. data/spec/arborist/observer/action_spec.rb +205 -0
  59. data/spec/arborist/observer/summarize_spec.rb +294 -0
  60. data/spec/arborist/observer_spec.rb +146 -0
  61. data/spec/arborist/subscription_spec.rb +71 -0
  62. data/spec/arborist_spec.rb +146 -0
  63. data/spec/data/monitors/pings.rb +80 -0
  64. data/spec/data/monitors/port_checks.rb +27 -0
  65. data/spec/data/monitors/system_resources.rb +30 -0
  66. data/spec/data/monitors/web_services.rb +17 -0
  67. data/spec/data/nodes/duir.rb +20 -0
  68. data/spec/data/nodes/localhost.rb +15 -0
  69. data/spec/data/nodes/sidonie.rb +29 -0
  70. data/spec/data/nodes/yevaud.rb +26 -0
  71. data/spec/data/observers/auditor.rb +23 -0
  72. data/spec/data/observers/webservices.rb +18 -0
  73. data/spec/spec_helper.rb +117 -0
  74. metadata +368 -0
@@ -0,0 +1,442 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'timecop'
6
+ require 'arborist/manager'
7
+
8
+
9
+ describe Arborist::Manager do
10
+
11
+ after( :each ) do
12
+ Arborist::Node::Root.reset
13
+ end
14
+
15
+
16
+ let( :manager ) { described_class.new }
17
+
18
+
19
+
20
+ #
21
+ # Examples
22
+ #
23
+
24
+ it "starts with a root node" do
25
+ expect( described_class.new.root ).to be_a( Arborist::Node )
26
+ end
27
+
28
+
29
+ it "starts with a node registry with the root node and itself" do
30
+ result = manager.nodes
31
+ expect( result ).to include( '_' )
32
+ expect( result['_'] ).to be( manager.root )
33
+ end
34
+
35
+
36
+ it "knows how long it has been running" do
37
+ Timecop.freeze do
38
+ manager.start_time = Time.now
39
+
40
+ Timecop.travel( 10 ) do
41
+ expect( manager.uptime ).to be_within( 1 ).of( 10 )
42
+ end
43
+ end
44
+ end
45
+
46
+
47
+ it "has an uptime of 0 if it hasn't yet been started" do
48
+ expect( manager.uptime ).to eq( 0 )
49
+ end
50
+
51
+
52
+ context "a new empty manager" do
53
+
54
+ let( :node ) do
55
+ testing_node 'italian_lessons'
56
+ end
57
+ let( :node2 ) do
58
+ testing_node 'french_laundry'
59
+ end
60
+ let( :node3 ) do
61
+ testing_node 'german_oak_cats'
62
+ end
63
+
64
+
65
+ it "has a nodecount of 1" do
66
+ expect( manager.nodecount ).to eq( 1 )
67
+ end
68
+
69
+
70
+ it "can have a node added to it" do
71
+ manager.add_node( node )
72
+ expect( manager.nodes ).to include( 'italian_lessons' )
73
+ expect( manager.nodes['italian_lessons'] ).to be( node )
74
+ expect( manager.nodecount ).to eq( 2 )
75
+ expect( manager.nodelist ).to include( '_', 'italian_lessons' )
76
+ end
77
+
78
+
79
+ it "can load its tree from an Enumerator that yields nodes" do
80
+ manager.load_tree([ node, node2, node3 ])
81
+ expect( manager.nodes ).to include( 'italian_lessons', 'french_laundry', 'german_oak_cats' )
82
+ expect( manager.nodes['italian_lessons'] ).to be( node )
83
+ expect( manager.nodes['french_laundry'] ).to be( node2 )
84
+ expect( manager.nodes['german_oak_cats'] ).to be( node3 )
85
+ expect( manager.nodecount ).to eq( 4 )
86
+ expect( manager.nodelist ).to include(
87
+ '_', 'italian_lessons', 'french_laundry', 'german_oak_cats'
88
+ )
89
+ end
90
+
91
+
92
+ it "can replace an existing node" do
93
+ manager.add_node( node )
94
+ another_node = testing_node( 'italian_lessons' )
95
+ manager.add_node( another_node )
96
+
97
+ expect( manager.nodes ).to include( 'italian_lessons' )
98
+ expect( manager.nodes['italian_lessons'] ).to_not be( node )
99
+ expect( manager.nodes['italian_lessons'] ).to be( another_node )
100
+
101
+ expect( manager.nodecount ).to eq( 2 )
102
+ expect( manager.nodelist ).to include( '_', 'italian_lessons' )
103
+ end
104
+
105
+
106
+ it "can have a node removed from it" do
107
+ manager.add_node( node )
108
+ deleted_node = manager.remove_node( 'italian_lessons' )
109
+
110
+ expect( deleted_node ).to be( node )
111
+ expect( manager.nodes ).to_not include( 'italian_lessons' )
112
+
113
+ expect( manager.nodecount ).to eq( 1 )
114
+ expect( manager.nodelist ).to include( '_' )
115
+ end
116
+
117
+
118
+ it "disallows removal of operational nodes" do
119
+ expect {
120
+ manager.remove_node('_')
121
+ }.to raise_error( /can't remove an operational node/i )
122
+ end
123
+
124
+ end
125
+
126
+
127
+ context "a manager with some loaded nodes" do
128
+
129
+ let( :trunk_node ) do
130
+ testing_node( 'trunk' )
131
+ end
132
+ let( :branch_node ) do
133
+ testing_node( 'branch', 'trunk' )
134
+ end
135
+ let( :leaf_node ) do
136
+ testing_node( 'leaf', 'branch' )
137
+ end
138
+
139
+ let( :manager ) do
140
+ instance = described_class.new
141
+ instance.load_tree([ branch_node, leaf_node, trunk_node ])
142
+ instance
143
+ end
144
+
145
+
146
+ it "has a tree built out of its nodes" do
147
+ expect( manager.root ).to have_children
148
+ end
149
+
150
+
151
+ it "knows what nodes have been loaded" do
152
+ expect( manager.nodelist ).to include( 'trunk', 'branch', 'leaf' )
153
+ end
154
+
155
+
156
+ it "errors if any of its nodes are missing their parent" do
157
+ manager = described_class.new
158
+ orphan = testing_node( 'orphan' ) do
159
+ parent 'daddy_warbucks'
160
+ end
161
+
162
+ expect {
163
+ manager.load_tree([ orphan ])
164
+ }.to raise_error( /no parent 'daddy_warbucks' node loaded for/i )
165
+ end
166
+
167
+
168
+ it "grafts a node into the tree when one with a previously unknown identifier is added" do
169
+ new_node = testing_node( 'new' ) do
170
+ parent 'branch'
171
+ end
172
+
173
+ manager.add_node( new_node )
174
+ expect( manager.nodes['branch'].children ).to include( 'new' )
175
+ end
176
+
177
+
178
+ it "replaces a node in the tree when a node with an existing identifier is added" do
179
+ updated_node = testing_node( 'leaf' ) do
180
+ parent 'trunk'
181
+ end
182
+
183
+ manager.add_node( updated_node )
184
+ expect( manager.nodes['branch'].children ).to_not include( 'leaf' => leaf_node )
185
+ expect( manager.nodes['trunk'].children ).to include( 'leaf' => updated_node )
186
+ end
187
+
188
+
189
+ it "rebuilds the tree when a node is removed from it" do
190
+ manager.remove_node( 'branch' )
191
+
192
+ expect( manager.nodes['trunk'].children ).to_not include( 'branch' )
193
+ expect( manager.nodes ).to_not include( 'branch' )
194
+ expect( manager.nodes ).to_not include( 'leaf' )
195
+ end
196
+
197
+ end
198
+
199
+
200
+ describe "tree traversal" do
201
+
202
+ let( :tree ) do
203
+ # router
204
+ # host_a host_b host_c
205
+ # www smtp imap www nfs ssh www
206
+
207
+ [
208
+ testing_node( 'router' ),
209
+ testing_node( 'host_a', 'router' ),
210
+ testing_node( 'host_a_www', 'host_a' ),
211
+ testing_node( 'host_a_smtp', 'host_a' ),
212
+ testing_node( 'host_a_imap', 'host_a' ),
213
+ testing_node( 'host_b', 'router' ),
214
+ testing_node( 'host_b_www', 'host_b' ),
215
+ testing_node( 'host_b_nfs', 'host_b' ),
216
+ testing_node( 'host_b_ssh', 'host_b' ),
217
+ testing_node( 'host_c', 'router' ),
218
+ testing_node( 'host_c_www', 'host_c' ),
219
+ ]
220
+ end
221
+
222
+ let( :manager ) do
223
+ instance = described_class.new
224
+ instance.load_tree( tree )
225
+ instance
226
+ end
227
+
228
+
229
+ it "can traverse all nodes in its node tree" do
230
+ iter = manager.all_nodes
231
+ expect( iter ).to be_a( Enumerator )
232
+ expect( iter.to_a ).to eq( [manager.root] + tree )
233
+ end
234
+
235
+
236
+ it "can traverse all nodes whose status is 'up'" do
237
+ manager.nodes.each {|_, node| node.status = :up }
238
+ manager.nodes[ 'host_a' ].update( error: "ping failed" )
239
+ expect( manager.nodes[ 'host_a' ] ).to be_down
240
+ manager.nodes[ 'host_c' ].update( error: "gamma rays" )
241
+ expect( manager.nodes[ 'host_c' ] ).to be_down
242
+
243
+ iter = manager.reachable_nodes
244
+
245
+ expect( iter ).to be_a( Enumerator )
246
+
247
+ nodes = iter.to_a
248
+ expect( nodes.size ).to eq( 6 )
249
+ expect( nodes.map(&:identifier) ).to include(
250
+ "_",
251
+ "router",
252
+ "host_b",
253
+ "host_b_www",
254
+ "host_b_nfs",
255
+ "host_b_ssh"
256
+ )
257
+ end
258
+
259
+
260
+ it "can create an Enumerator for all of a node's parents from leaf to root"
261
+
262
+ end
263
+
264
+
265
+ describe "node updates and events" do
266
+
267
+ let( :tree ) do
268
+ # router
269
+ # host_a host_b host_c
270
+ # www smtp imap www nfs ssh www
271
+
272
+ [
273
+ testing_node( 'router' ),
274
+ testing_node( 'host_a', 'router' ),
275
+ testing_node( 'host_a_www', 'host_a' ),
276
+ testing_node( 'host_a_smtp', 'host_a' ),
277
+ testing_node( 'host_a_imap', 'host_a' ),
278
+ testing_node( 'host_b', 'router' ),
279
+ testing_node( 'host_b_www', 'host_b' ),
280
+ testing_node( 'host_b_nfs', 'host_b' ),
281
+ testing_node( 'host_b_ssh', 'host_b' ),
282
+ testing_node( 'host_c', 'router' ),
283
+ testing_node( 'host_c_www', 'host_c' ),
284
+ ]
285
+ end
286
+
287
+ let( :manager ) do
288
+ instance = described_class.new
289
+ instance.load_tree( tree )
290
+ instance
291
+ end
292
+
293
+
294
+ it "can fetch a Hash of node states" do
295
+ states = manager.fetch_matching_node_states( {}, [] )
296
+ expect( states.size ).to eq( manager.nodes.size )
297
+ expect( states ).to include( 'host_b_nfs', 'host_c', 'router' )
298
+ expect( states['host_b_nfs'] ).to be_a( Hash )
299
+ expect( states['host_c'] ).to be_a( Hash )
300
+ expect( states['router'] ).to be_a( Hash )
301
+ end
302
+
303
+
304
+ it "can update an event by identifier" do
305
+ manager.update_node( 'host_b_www', http: { status: 200 } )
306
+ expect(
307
+ manager.nodes['host_b_www'].properties
308
+ ).to include( 'http' => { 'status' => 200 } )
309
+ end
310
+
311
+
312
+ it "ignores updates to an identifier that is not (any longer) in the tree" do
313
+ expect {
314
+ manager.update_node( 'host_y', asset_tag: '2by-n86y7t' )
315
+ }.to_not raise_error
316
+ end
317
+
318
+
319
+ it "propagates events from an update up the node tree" do
320
+ expect( manager.root ).to receive( :publish_events ).
321
+ at_least( :once ).
322
+ and_call_original
323
+ expect( manager.nodes['host_c'] ).to receive( :publish_events ).
324
+ at_least( :once ).
325
+ and_call_original
326
+ manager.update_node( 'host_c_www', response_status: 504, error: 'Timeout talking to web service.' )
327
+ end
328
+
329
+
330
+ it "only propagates events to a node's ancestors" do
331
+ expect( manager.root ).to receive( :publish_events ).
332
+ at_least( :once ).
333
+ and_call_original
334
+ expect( manager.nodes['host_c'] ).to_not receive( :publish_events )
335
+
336
+ manager.update_node( 'host_b_www', response_status: 504, error: 'Timeout talking to web service.' )
337
+ end
338
+
339
+ end
340
+
341
+
342
+ describe "subscriptions" do
343
+
344
+ let( :tree ) {[ testing_node('host_c') ]}
345
+ let( :manager ) do
346
+ instance = described_class.new
347
+ instance.load_tree( tree )
348
+ instance
349
+ end
350
+
351
+
352
+ it "can attach subscriptions to a node by its identifier" do
353
+ sub = subid = nil
354
+ expect {
355
+ sub = manager.create_subscription( 'host_c', 'node.update', type: 'host' )
356
+ }.to change { manager.subscriptions.size }.by( 1 )
357
+
358
+ node = manager.subscriptions[ sub.id ]
359
+
360
+ expect( sub ).to be_a( Arborist::Subscription )
361
+ expect( node ).to be( manager.nodes['host_c'] )
362
+ end
363
+
364
+
365
+ it "can detach subscriptions from a node given the subscription ID" do
366
+ sub = manager.create_subscription( 'host_c', 'node.ack', type: 'service' )
367
+ rval = nil
368
+
369
+ expect {
370
+ rval = manager.remove_subscription( sub.id )
371
+ }.to change { manager.subscriptions.size }.by( -1 ).and(
372
+ change { manager.nodes['host_c'].subscriptions.size }.by( -1 )
373
+ )
374
+
375
+ expect( rval ).to be( sub )
376
+ end
377
+
378
+ end
379
+
380
+
381
+ describe "sockets" do
382
+
383
+ let( :zmq_context ) { Arborist.zmq_context }
384
+ let( :zmq_loop ) { instance_double(ZMQ::Loop) }
385
+ let( :tree_sock ) { instance_double(ZMQ::Socket::Rep, "tree API socket") }
386
+ let( :event_sock ) { instance_double(ZMQ::Socket::Pub, "event socket") }
387
+ let( :tree_pollitem ) { instance_double(ZMQ::Pollitem, "tree API pollitem") }
388
+ let( :event_pollitem ) { instance_double(ZMQ::Pollitem, "event API pollitem") }
389
+ let( :signal_timer ) { instance_double(ZMQ::Timer, "signal timer") }
390
+
391
+ before( :each ) do
392
+ allow( ZMQ::Loop ).to receive( :new ).and_return( zmq_loop )
393
+
394
+ allow( zmq_context ).to receive( :socket ).with( :REP ).and_return( tree_sock )
395
+ allow( zmq_context ).to receive( :socket ).with( :PUB ).and_return( event_sock )
396
+
397
+ allow( zmq_loop ).to receive( :remove ).with( tree_pollitem )
398
+ allow( zmq_loop ).to receive( :remove ).with( event_pollitem )
399
+
400
+ allow( tree_pollitem ).to receive( :pollable ).and_return( tree_sock )
401
+ allow( tree_sock ).to receive( :close )
402
+ allow( event_pollitem ).to receive( :pollable ).and_return( event_sock )
403
+ allow( event_sock ).to receive( :close )
404
+ end
405
+
406
+
407
+
408
+ it "sets up its sockets with handlers and starts the ZMQ loop when started" do
409
+ expect( tree_sock ).to receive( :bind ).with( Arborist.tree_api_url )
410
+ expect( tree_sock ).to receive( :linger= ).with( 0 )
411
+
412
+ expect( event_sock ).to receive( :bind ).with( Arborist.event_api_url )
413
+ expect( event_sock ).to receive( :linger= ).with( 0 )
414
+
415
+ expect( ZMQ::Pollitem ).to receive( :new ).with( tree_sock, ZMQ::POLLIN|ZMQ::POLLOUT ).
416
+ and_return( tree_pollitem )
417
+ expect( ZMQ::Pollitem ).to receive( :new ).with( event_sock, ZMQ::POLLOUT ).
418
+ and_return( event_pollitem )
419
+
420
+ expect( tree_pollitem ).to receive( :handler= ).
421
+ with( an_instance_of(Arborist::Manager::TreeAPI) )
422
+ expect( zmq_loop ).to receive( :register ).with( tree_pollitem )
423
+ expect( event_pollitem ).to receive( :handler= ).
424
+ with( an_instance_of(Arborist::Manager::EventPublisher) )
425
+ expect( zmq_loop ).to receive( :register ).with( event_pollitem )
426
+
427
+ expect( ZMQ::Timer ).to receive( :new ).
428
+ with( described_class::SIGNAL_INTERVAL, 0, manager.method(:process_signal_queue) ).
429
+ and_return( signal_timer )
430
+ expect( zmq_loop ).to receive( :register_timer ).with( signal_timer )
431
+ expect( zmq_loop ).to receive( :start )
432
+
433
+ expect( zmq_loop ).to receive( :remove ).with( tree_pollitem )
434
+ expect( zmq_loop ).to receive( :remove ).with( event_pollitem )
435
+
436
+ manager.run
437
+ end
438
+ end
439
+
440
+
441
+ end
442
+
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'timecop'
6
+
7
+ require 'arborist/mixins'
8
+
9
+
10
+ describe Arborist, "mixins" do
11
+
12
+ describe Arborist::MethodUtilities, 'used to extend a class' do
13
+
14
+ let!( :extended_class ) do
15
+ klass = Class.new
16
+ klass.extend( Arborist::MethodUtilities )
17
+ klass
18
+ end
19
+
20
+ it "can declare a class-level attribute reader" do
21
+ extended_class.singleton_attr_reader :foo
22
+ expect( extended_class ).to respond_to( :foo )
23
+ expect( extended_class ).to_not respond_to( :foo= )
24
+ expect( extended_class ).to_not respond_to( :foo? )
25
+ end
26
+
27
+ it "can declare a class-level attribute writer" do
28
+ extended_class.singleton_attr_writer :foo
29
+ expect( extended_class ).to_not respond_to( :foo )
30
+ expect( extended_class ).to respond_to( :foo= )
31
+ expect( extended_class ).to_not respond_to( :foo? )
32
+ end
33
+
34
+ it "can declare a class-level attribute reader and writer" do
35
+ extended_class.singleton_attr_accessor :foo
36
+ expect( extended_class ).to respond_to( :foo )
37
+ expect( extended_class ).to respond_to( :foo= )
38
+ expect( extended_class ).to_not respond_to( :foo? )
39
+ end
40
+
41
+ it "can declare a class-level alias" do
42
+ def extended_class.foo
43
+ return "foo"
44
+ end
45
+ extended_class.singleton_method_alias( :bar, :foo )
46
+
47
+ expect( extended_class.bar ).to eq( 'foo' )
48
+ end
49
+
50
+ it "can declare an instance attribute predicate method" do
51
+ extended_class.attr_predicate :foo
52
+ instance = extended_class.new
53
+
54
+ expect( instance ).to_not respond_to( :foo )
55
+ expect( instance ).to_not respond_to( :foo= )
56
+ expect( instance ).to respond_to( :foo? )
57
+
58
+ expect( instance.foo? ).to be_falsey
59
+
60
+ instance.instance_variable_set( :@foo, 1 )
61
+ expect( instance.foo? ).to be_truthy
62
+ end
63
+
64
+ it "can declare an instance attribute predicate and writer" do
65
+ extended_class.attr_predicate_accessor :foo
66
+ instance = extended_class.new
67
+
68
+ expect( instance ).to_not respond_to( :foo )
69
+ expect( instance ).to respond_to( :foo= )
70
+ expect( instance ).to respond_to( :foo? )
71
+
72
+ expect( instance.foo? ).to be_falsey
73
+
74
+ instance.foo = 1
75
+ expect( instance.foo? ).to be_truthy
76
+ end
77
+
78
+ it "can declare a class-level attribute predicate and writer" do
79
+ extended_class.singleton_predicate_accessor :foo
80
+ expect( extended_class ).to_not respond_to( :foo )
81
+ expect( extended_class ).to respond_to( :foo= )
82
+ expect( extended_class ).to respond_to( :foo? )
83
+ end
84
+
85
+ it "can declare a class-level predicate method" do
86
+ extended_class.singleton_predicate_reader :foo
87
+ expect( extended_class ).to_not respond_to( :foo )
88
+ expect( extended_class ).to_not respond_to( :foo= )
89
+ expect( extended_class ).to respond_to( :foo? )
90
+ end
91
+
92
+ end
93
+
94
+
95
+ describe Arborist::TimeRefinements do
96
+
97
+ using( described_class )
98
+
99
+ context "used to extend Time objects" do
100
+
101
+ it "makes them aware of whether they're in the future or not" do
102
+ Timecop.freeze do
103
+ time = Time.now
104
+ expect( time.future? ).to be_falsey
105
+
106
+ future_time = time + 1
107
+ expect( future_time.future? ).to be_truthy
108
+
109
+ past_time = time - 1
110
+ expect( past_time.future? ).to be_falsey
111
+ end
112
+ end
113
+
114
+
115
+ it "makes them aware of whether they're in the past or not" do
116
+ Timecop.freeze do
117
+ time = Time.now
118
+ expect( time.past? ).to be_falsey
119
+
120
+ future_time = time + 1
121
+ expect( future_time.past? ).to be_falsey
122
+
123
+ past_time = time - 1
124
+ expect( past_time.past? ).to be_truthy
125
+ end
126
+ end
127
+
128
+
129
+ it "adds the ability to express themselves as an offset in English" do
130
+ Timecop.freeze do
131
+ expect( 1.second.ago.as_delta ).to eq( 'less than a minute ago' )
132
+ expect( 1.second.from_now.as_delta ).to eq( 'less than a minute from now' )
133
+
134
+ expect( 1.minute.ago.as_delta ).to eq( 'a minute ago' )
135
+ expect( 1.minute.from_now.as_delta ).to eq( 'a minute from now' )
136
+ expect( 68.seconds.ago.as_delta ).to eq( 'a minute ago' )
137
+ expect( 68.seconds.from_now.as_delta ).to eq( 'a minute from now' )
138
+ expect( 2.minutes.ago.as_delta ).to eq( '2 minutes ago' )
139
+ expect( 2.minutes.from_now.as_delta ).to eq( '2 minutes from now' )
140
+ expect( 38.minutes.ago.as_delta ).to eq( '38 minutes ago' )
141
+ expect( 38.minutes.from_now.as_delta ).to eq( '38 minutes from now' )
142
+
143
+ expect( 1.hour.ago.as_delta ).to eq( 'about an hour ago' )
144
+ expect( 1.hour.from_now.as_delta ).to eq( 'about an hour from now' )
145
+ expect( 75.minutes.ago.as_delta ).to eq( 'about an hour ago' )
146
+ expect( 75.minutes.from_now.as_delta ).to eq( 'about an hour from now' )
147
+
148
+ expect( 2.hours.ago.as_delta ).to eq( '2 hours ago' )
149
+ expect( 2.hours.from_now.as_delta ).to eq( '2 hours from now' )
150
+ expect( 14.hours.ago.as_delta ).to eq( '14 hours ago' )
151
+ expect( 14.hours.from_now.as_delta ).to eq( '14 hours from now' )
152
+
153
+ expect( 22.hours.ago.as_delta ).to eq( 'about a day ago' )
154
+ expect( 22.hours.from_now.as_delta ).to eq( 'about a day from now' )
155
+ expect( 28.hours.ago.as_delta ).to eq( 'about a day ago' )
156
+ expect( 28.hours.from_now.as_delta ).to eq( 'about a day from now' )
157
+
158
+ expect( 36.hours.ago.as_delta ).to eq( '2 days ago' )
159
+ expect( 36.hours.from_now.as_delta ).to eq( '2 days from now' )
160
+ expect( 4.days.ago.as_delta ).to eq( '4 days ago' )
161
+ expect( 4.days.from_now.as_delta ).to eq( '4 days from now' )
162
+
163
+ expect( 1.week.ago.as_delta ).to eq( 'about a week ago' )
164
+ expect( 1.week.from_now.as_delta ).to eq( 'about a week from now' )
165
+ expect( 8.days.ago.as_delta ).to eq( 'about a week ago' )
166
+ expect( 8.days.from_now.as_delta ).to eq( 'about a week from now' )
167
+
168
+ expect( 15.days.ago.as_delta ).to eq( '2 weeks ago' )
169
+ expect( 15.days.from_now.as_delta ).to eq( '2 weeks from now' )
170
+ expect( 3.weeks.ago.as_delta ).to eq( '3 weeks ago' )
171
+ expect( 3.weeks.from_now.as_delta ).to eq( '3 weeks from now' )
172
+
173
+ expect( 1.month.ago.as_delta ).to eq( '4 weeks ago' )
174
+ expect( 1.month.from_now.as_delta ).to eq( '4 weeks from now' )
175
+ expect( 36.days.ago.as_delta ).to eq( '5 weeks ago' )
176
+ expect( 36.days.from_now.as_delta ).to eq( '5 weeks from now' )
177
+
178
+ expect( 6.months.ago.as_delta ).to eq( '6 months ago' )
179
+ expect( 6.months.from_now.as_delta ).to eq( '6 months from now' )
180
+ expect( 14.months.ago.as_delta ).to eq( '14 months ago' )
181
+ expect( 14.months.from_now.as_delta ).to eq( '14 months from now' )
182
+
183
+ expect( 6.year.ago.as_delta ).to eq( '6 years ago' )
184
+ expect( 6.year.from_now.as_delta ).to eq( '6 years from now' )
185
+ expect( 14.years.ago.as_delta ).to eq( '14 years ago' )
186
+ expect( 14.years.from_now.as_delta ).to eq( '14 years from now' )
187
+ end
188
+ end
189
+
190
+ end
191
+
192
+ end
193
+
194
+ end
195
+