arborist 0.0.1.pre20160106113421
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 +7 -0
- data/.document +4 -0
- data/.simplecov +9 -0
- data/ChangeLog +417 -0
- data/Events.md +20 -0
- data/History.md +4 -0
- data/LICENSE +29 -0
- data/Manifest.txt +72 -0
- data/Monitors.md +141 -0
- data/Nodes.md +0 -0
- data/Observers.md +72 -0
- data/Protocol.md +214 -0
- data/README.md +75 -0
- data/Rakefile +81 -0
- data/TODO.md +24 -0
- data/bin/amanagerd +10 -0
- data/bin/amonitord +12 -0
- data/bin/aobserverd +12 -0
- data/lib/arborist.rb +182 -0
- data/lib/arborist/client.rb +191 -0
- data/lib/arborist/event.rb +61 -0
- data/lib/arborist/event/node_acked.rb +18 -0
- data/lib/arborist/event/node_delta.rb +20 -0
- data/lib/arborist/event/node_matching.rb +34 -0
- data/lib/arborist/event/node_update.rb +19 -0
- data/lib/arborist/event/sys_reloaded.rb +15 -0
- data/lib/arborist/exceptions.rb +21 -0
- data/lib/arborist/manager.rb +508 -0
- data/lib/arborist/manager/event_publisher.rb +97 -0
- data/lib/arborist/manager/tree_api.rb +207 -0
- data/lib/arborist/mixins.rb +363 -0
- data/lib/arborist/monitor.rb +377 -0
- data/lib/arborist/monitor/socket.rb +163 -0
- data/lib/arborist/monitor_runner.rb +217 -0
- data/lib/arborist/node.rb +700 -0
- data/lib/arborist/node/host.rb +87 -0
- data/lib/arborist/node/root.rb +60 -0
- data/lib/arborist/node/service.rb +112 -0
- data/lib/arborist/observer.rb +176 -0
- data/lib/arborist/observer/action.rb +125 -0
- data/lib/arborist/observer/summarize.rb +105 -0
- data/lib/arborist/observer_runner.rb +181 -0
- data/lib/arborist/subscription.rb +82 -0
- data/spec/arborist/client_spec.rb +282 -0
- data/spec/arborist/event/node_update_spec.rb +71 -0
- data/spec/arborist/event_spec.rb +64 -0
- data/spec/arborist/manager/event_publisher_spec.rb +66 -0
- data/spec/arborist/manager/tree_api_spec.rb +458 -0
- data/spec/arborist/manager_spec.rb +442 -0
- data/spec/arborist/mixins_spec.rb +195 -0
- data/spec/arborist/monitor/socket_spec.rb +195 -0
- data/spec/arborist/monitor_runner_spec.rb +152 -0
- data/spec/arborist/monitor_spec.rb +251 -0
- data/spec/arborist/node/host_spec.rb +104 -0
- data/spec/arborist/node/root_spec.rb +29 -0
- data/spec/arborist/node/service_spec.rb +98 -0
- data/spec/arborist/node_spec.rb +552 -0
- data/spec/arborist/observer/action_spec.rb +205 -0
- data/spec/arborist/observer/summarize_spec.rb +294 -0
- data/spec/arborist/observer_spec.rb +146 -0
- data/spec/arborist/subscription_spec.rb +71 -0
- data/spec/arborist_spec.rb +146 -0
- data/spec/data/monitors/pings.rb +80 -0
- data/spec/data/monitors/port_checks.rb +27 -0
- data/spec/data/monitors/system_resources.rb +30 -0
- data/spec/data/monitors/web_services.rb +17 -0
- data/spec/data/nodes/duir.rb +20 -0
- data/spec/data/nodes/localhost.rb +15 -0
- data/spec/data/nodes/sidonie.rb +29 -0
- data/spec/data/nodes/yevaud.rb +26 -0
- data/spec/data/observers/auditor.rb +23 -0
- data/spec/data/observers/webservices.rb +18 -0
- data/spec/spec_helper.rb +117 -0
- 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
|
+
|