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.
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,195 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../../spec_helper'
4
+
5
+ require 'arborist'
6
+ require 'arborist/node/host'
7
+ require 'arborist/node/service'
8
+ require 'arborist/monitor/socket'
9
+
10
+
11
+ describe Arborist::Monitor::Socket do
12
+
13
+ describe 'TCP' do
14
+
15
+ let( :described_class ) { Arborist::Monitor::Socket::TCP }
16
+
17
+ let( :host_node ) do
18
+ Arborist::Node.create( 'host', 'test' ) do
19
+ description "Test host node with a few TCP services"
20
+ address '192.168.26.1'
21
+
22
+ tags :testing
23
+ end
24
+ end
25
+
26
+ let( :default_timeout ) { described_class::DEFAULT_OPTIONS[:timeout] }
27
+
28
+ let( :www_service_node ) { host_node.service('www') }
29
+ let( :ssh_service_node ) { host_node.service('ssh') }
30
+ let( :nat_pmp_service_node ) { host_node.service('nat-pmp', port: 5351) }
31
+
32
+ let( :service_nodes ) {[ www_service_node, ssh_service_node, nat_pmp_service_node ]}
33
+ let( :service_nodes_hash ) do
34
+ service_nodes.each_with_object({}) do |node, accum|
35
+ accum[ node.identifier ] = node.fetch_values
36
+ end
37
+ end
38
+
39
+
40
+ # it_behaves_like "an Arborist Monitor"
41
+
42
+
43
+ def sockaddr_for( node )
44
+ return Socket.sockaddr_in( node.port, node.addresses.first.to_s )
45
+ end
46
+
47
+
48
+ def make_successful_mock_socket( node )
49
+ address = Addrinfo.tcp( node.addresses.first.to_s, node.port )
50
+ socket = instance_double( Socket, "#{node.identifier} socket", remote_address: address )
51
+ errors = [ IO::EINPROGRESSWaitWritable, Errno::EISCONN ]
52
+
53
+ expect( socket ).to receive( :connect_nonblock ) do |addr|
54
+ expect( addr ).to eq( sockaddr_for(node) )
55
+ raise errors.shift
56
+ end.at_least( :once )
57
+
58
+ return socket
59
+ end
60
+
61
+
62
+ def make_initial_error_mock_socket( node, error_class, message )
63
+ address = Addrinfo.tcp( node.addresses.first.to_s, node.port )
64
+ socket = instance_double( Socket, "#{node.identifier} socket", remote_address: address )
65
+
66
+ expect( socket ).to receive( :connect_nonblock ).
67
+ with( sockaddr_for(node) ).
68
+ and_raise( error_class.new(message) )
69
+
70
+ return socket
71
+ end
72
+
73
+
74
+ def make_wait_error_mock_socket( node, error_class, message )
75
+ address = Addrinfo.tcp( node.addresses.first.to_s, node.port )
76
+ socket = instance_double( Socket, "#{node.identifier} socket", remote_address: address )
77
+ errors = [ IO::EINPROGRESSWaitWritable, error_class.new(message) ]
78
+
79
+ expect( socket ).to receive( :connect_nonblock ) do |addr|
80
+ expect( addr ).to eq( sockaddr_for(node) )
81
+ raise errors.shift
82
+ end.at_least( :once )
83
+
84
+ return socket
85
+ end
86
+
87
+
88
+ it "opens TCP connections to the ports of the nodes" do
89
+ fake_sockets = service_nodes.map do |node|
90
+ make_successful_mock_socket( node )
91
+ end
92
+
93
+ expect( Socket ).to receive( :new ).and_return( *fake_sockets )
94
+ expect( IO ).to receive( :select ).
95
+ with( nil, fake_sockets, nil, kind_of(Numeric) ).
96
+ and_return( [nil, fake_sockets, nil] )
97
+
98
+ expect( fake_sockets ).to all( receive( :close ) )
99
+
100
+ result = described_class.run( service_nodes_hash )
101
+
102
+ expect( result ).to be_a( Hash )
103
+ expect( result ).to include( *service_nodes.map(&:identifier) )
104
+ expect( result.values ).to all( include(
105
+ tcp_socket_connect: a_hash_including(:time, :duration)
106
+ ) )
107
+ expect( result.map {|_, res| res[:tcp_socket_connect][:time]} ).to all( be_a(String) )
108
+ end
109
+
110
+
111
+ it "updates nodes with an error on a SocketError" do
112
+ socket = make_initial_error_mock_socket( www_service_node, SocketError,
113
+ "getaddrinfo: nodename nor servname provided, or not known" )
114
+ allow( Socket ).to receive( :new ).and_return( socket )
115
+
116
+ result = described_class.run( 'test-www' => www_service_node.fetch_values )
117
+
118
+ expect( result ).to be_a( Hash )
119
+ expect( result ).to include( 'test-www' )
120
+ expect(
121
+ result['test-www']
122
+ ).to include( error: 'getaddrinfo: nodename nor servname provided, or not known' )
123
+ end
124
+
125
+
126
+ it "updates nodes with an error if the connection times out" do
127
+ socket = make_successful_mock_socket( www_service_node )
128
+ allow( Socket ).to receive( :new ).and_return( socket )
129
+ allow( socket ).to receive( :close )
130
+ allow( IO ).to receive( :select ) do
131
+ sleep 0.2
132
+ [nil, nil, nil]
133
+ end
134
+
135
+ result = described_class.new( timeout: 0.1 ).
136
+ run( 'test-www' => www_service_node.fetch_values )
137
+
138
+ expect( result ).to be_a( Hash )
139
+ expect( result ).to include( 'test-www' )
140
+ expect( result['test-www'] ).to include( error: 'Timeout after 0.100s' )
141
+ end
142
+
143
+
144
+ it "updates nodes with an error on a 'connection refused' error" do
145
+ socket = make_initial_error_mock_socket( www_service_node, Errno::ECONNREFUSED,
146
+ "the message" )
147
+ allow( Socket ).to receive( :new ).and_return( socket )
148
+
149
+ result = described_class.run( 'test-www' => www_service_node.fetch_values )
150
+
151
+ expect( result ).to be_a( Hash )
152
+ expect( result ).to include( 'test-www' )
153
+ expect( result['test-www'] ).to include( error: 'Connection refused - the message' )
154
+ end
155
+
156
+
157
+ it "updates nodes with an error on a 'host unreachable' error" do
158
+ socket = make_initial_error_mock_socket( www_service_node, Errno::EHOSTUNREACH,
159
+ "the message" )
160
+ allow( Socket ).to receive( :new ).and_return( socket )
161
+
162
+ result = described_class.run( 'test-www' => www_service_node.fetch_values )
163
+
164
+ expect( result ).to be_a( Hash )
165
+ expect( result ).to include( 'test-www' )
166
+ expect( result['test-www'] ).to include( error: 'No route to host - the message' )
167
+ end
168
+
169
+
170
+ it "updates nodes with an error on a 'getpeername' error" do
171
+ socket = make_wait_error_mock_socket( www_service_node, Errno::EINVAL, "getpeername(2)" )
172
+ allow( Socket ).to receive( :new ).and_return( socket )
173
+ allow( IO ).to receive( :select ).
174
+ with( nil, [socket], nil, kind_of(Numeric) ).
175
+ and_return( [nil, [socket], nil] )
176
+ allow( socket ).to receive( :close )
177
+
178
+ result = described_class.run( 'test-www' => www_service_node.fetch_values )
179
+
180
+ expect( result ).to be_a( Hash )
181
+ expect( result ).to include( 'test-www' )
182
+ expect( result['test-www'] ).to include( error: 'Invalid argument - getpeername(2)' )
183
+ end
184
+
185
+
186
+ it "can be instantiated to run with a different timeout" do
187
+ mon = described_class.new.with_timeout( 30 )
188
+ expect( mon.timeout ).to eq( 30 )
189
+ end
190
+
191
+ end
192
+
193
+ end
194
+
195
+
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'arborist/monitor_runner'
6
+
7
+
8
+ describe Arborist::MonitorRunner do
9
+
10
+ let( :zmq_loop ) { instance_double(ZMQ::Loop) }
11
+ let( :req_socket ) { instance_double(ZMQ::Socket::Req) }
12
+ let( :pollitem ) { instance_double(ZMQ::Pollitem) }
13
+
14
+ let( :runner ) do
15
+ obj = described_class.new
16
+ obj.reactor = zmq_loop
17
+ obj
18
+ end
19
+
20
+ let( :monitor_class ) { Class.new(Arborist::Monitor) }
21
+
22
+ let( :mon1 ) { monitor_class.new("testing monitor1") }
23
+ let( :mon2 ) { monitor_class.new("testing monitor2") { splay 10 } }
24
+ let( :mon3 ) { monitor_class.new("testing monitor3") }
25
+ let( :monitors ) {[ mon1, mon2, mon3 ]}
26
+
27
+
28
+ it "can load monitors from an enumerator that yields Arborist::Monitors" do
29
+ runner.load_monitors([ mon1, mon2, mon3 ])
30
+ expect( runner.monitors ).to include( mon1, mon2, mon3 )
31
+ end
32
+
33
+
34
+ describe "a runner with loaded monitors" do
35
+
36
+ before( :each ) do
37
+ allow( zmq_loop ).to receive( :register ).with( an_instance_of(ZMQ::Pollitem) )
38
+ end
39
+
40
+
41
+ it "registers its monitors to run on an interval and starts the ZMQ loop when run" do
42
+ runner.monitors.replace([ mon1 ])
43
+
44
+ interval_timer = instance_double( ZMQ::Timer )
45
+ expect( ZMQ::Timer ).to receive( :new ) do |i_delay, i_repeat, &i_block|
46
+ expect( i_delay ).to eq( mon1.interval )
47
+ expect( i_repeat ).to eq( 0 )
48
+
49
+ expect( runner.handler ).to receive( :run_monitor ).with( mon1 )
50
+
51
+ i_block.call
52
+ interval_timer
53
+ end
54
+
55
+ expect( zmq_loop ).to receive( :register_timer ).with( interval_timer )
56
+ expect( zmq_loop ).to receive( :start )
57
+
58
+ runner.run
59
+ end
60
+
61
+
62
+ it "delays registration of its interval timer if a monitor has a splay" do
63
+ runner.monitors.replace([ mon2 ])
64
+
65
+ interval_timer = instance_double( ZMQ::Timer )
66
+ expect( ZMQ::Timer ).to receive( :new ).with( mon2.interval, 0 ).
67
+ and_return( interval_timer )
68
+
69
+ timer = instance_double( ZMQ::Timer )
70
+ expect( ZMQ::Timer ).to receive( :new ) do |delay, repeat, &block|
71
+ expect( delay ).to be >= 0
72
+ expect( delay ).to be <= mon2.splay
73
+ expect( repeat ).to eq( 1 )
74
+
75
+ block.call
76
+ timer
77
+ end
78
+
79
+ expect( zmq_loop ).to receive( :register_timer ).with( interval_timer )
80
+ expect( zmq_loop ).to receive( :register_timer ).with( timer )
81
+ expect( zmq_loop ).to receive( :start )
82
+
83
+ runner.run
84
+ end
85
+
86
+ end
87
+
88
+
89
+ describe Arborist::MonitorRunner::Handler do
90
+
91
+ let( :tree_api_handler ) { Arborist::Manager::TreeAPI.new(:pollable, :manager) }
92
+
93
+ let( :handler ) { described_class.new( zmq_loop ) }
94
+
95
+ let( :node_tree ) {{
96
+ 'router' => {
97
+ 'addresses' => ['10.2.1.2', '1.2.3.4']
98
+ },
99
+ 'server' => {
100
+ 'addresses' => ['10.2.1.118']
101
+ }
102
+ }}
103
+ let( :ping_monitor_data ) {{
104
+ 'router' => {'ping' => { 'rtt' => 22 }},
105
+ 'server' => {'ping' => { 'rtt' => 8 }},
106
+ }}
107
+
108
+
109
+ it "can run a monitor using async ZMQ IO" do
110
+ expect( zmq_loop ).to receive( :register ).with( handler.pollitem )
111
+
112
+ # Queue up the monitor requests and register the socket as wanting to write
113
+ mon1.exec do |nodes|
114
+ ping_monitor_data
115
+ end
116
+ expect {
117
+ handler.run_monitor( mon1 )
118
+ }.to change { handler.registered? }.from( false ).to( true )
119
+
120
+ # Fetch
121
+ request = handler.client.make_fetch_request(
122
+ mon1.positive_criteria,
123
+ include_down: false,
124
+ properties: mon1.node_properties
125
+ )
126
+ response = tree_api_handler.successful_response( node_tree )
127
+
128
+ expect( handler.client.tree_api ).to receive( :send ).with( request )
129
+ expect( handler.client.tree_api ).to receive( :recv ).and_return( response )
130
+
131
+ expect {
132
+ handler.on_writable
133
+ }.to_not change { handler.registered? }
134
+
135
+ # Update
136
+ request = handler.client.make_update_request( ping_monitor_data )
137
+ response = tree_api_handler.successful_response( nil )
138
+ expect( handler.client.tree_api ).to receive( :send ).with( request )
139
+ expect( handler.client.tree_api ).to receive( :recv ).and_return( response )
140
+
141
+ # Unregister
142
+ expect( zmq_loop ).to receive( :remove ).with( handler.pollitem )
143
+ expect {
144
+ handler.on_writable
145
+ }.to change { handler.registered? }.from( true ).to( false )
146
+
147
+ end
148
+
149
+ end
150
+
151
+ end
152
+
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'arborist/monitor'
6
+
7
+
8
+ describe Arborist::Monitor do
9
+
10
+
11
+ let( :trunk_node ) do
12
+ testing_node( 'trunk' ) do
13
+ properties['pork'] = 'nope'
14
+ end
15
+ end
16
+ let( :branch_node ) do
17
+ testing_node( 'branch', 'trunk' ) do
18
+ properties['pork'] = 'yes'
19
+ end
20
+ end
21
+ let( :leaf_node ) do
22
+ testing_node( 'leaf', 'branch' ) do
23
+ properties['pork'] = 'twice'
24
+ end
25
+ end
26
+
27
+
28
+ let( :testing_nodes ) {[ trunk_node, branch_node, leaf_node ]}
29
+
30
+
31
+ it "can be created with just a description" do
32
+ mon = described_class.new( "the description" )
33
+ expect( mon ).to be_a( described_class )
34
+ expect( mon.description ).to eq( "the description" )
35
+ expect( mon.include_down? ).to be_falsey
36
+ expect( mon.interval ).to eq( Arborist::Monitor::DEFAULT_INTERVAL )
37
+ expect( mon.splay ).to eq( 0 )
38
+ expect( mon.positive_criteria ).to be_empty
39
+ expect( mon.negative_criteria ).to be_empty
40
+ expect( mon.node_properties ).to be_empty
41
+ end
42
+
43
+
44
+ it "yields itself to the provided block for the DSL" do
45
+ block_self = nil
46
+ mon = described_class.new( "testing monitor" ) do
47
+ block_self = self
48
+ end
49
+
50
+ expect( block_self ).to be( mon )
51
+ end
52
+
53
+
54
+ it "can specify an interval" do
55
+ mon = described_class.new( "testing monitor" ) do
56
+ every 30
57
+ end
58
+
59
+ expect( mon.interval ).to eq( 30 )
60
+ end
61
+
62
+
63
+ it "can specify a splay" do
64
+ mon = described_class.new( "testing monitor" ) do
65
+ splay 15
66
+ end
67
+
68
+ expect( mon.splay ).to eq( 15 )
69
+ end
70
+
71
+
72
+ it "can specify criteria for matching nodes to monitor" do
73
+ mon = described_class.new( "testing monitor" ) do
74
+ match type: 'host'
75
+ end
76
+
77
+ expect( mon.positive_criteria ).to include( type: 'host' )
78
+ end
79
+
80
+
81
+ it "can specify criteria for matching nodes not to monitor" do
82
+ mon = described_class.new( "testing monitor" ) do
83
+ exclude tag: 'laptop'
84
+ end
85
+
86
+ expect( mon.negative_criteria ).to include( tag: 'laptop' )
87
+ end
88
+
89
+
90
+ it "can specify that it will include hosts marked as 'down'" do
91
+ mon = described_class.new( "testing monitor" ) do
92
+ include_down true
93
+ end
94
+
95
+ expect( mon.include_down? ).to be_truthy
96
+ end
97
+
98
+
99
+ it "can specify one or more properties to include in the input to the monitor" do
100
+ mon = described_class.new( "testing monitor" ) do
101
+ use :address, :tags
102
+ end
103
+
104
+ expect( mon.node_properties ).to include( :address, :tags )
105
+ end
106
+
107
+
108
+ it "can specify a command to exec to do the monitor's work" do
109
+ mon = described_class.new( "the description" ) do
110
+ exec 'cat'
111
+ end
112
+
113
+ output = mon.run( testing_nodes )
114
+ expect( output ).to be_a( Hash )
115
+ expect( output ).to include( *(testing_nodes.map(&:identifier)) )
116
+ end
117
+
118
+
119
+ it "can specify a block to call to do the monitor's work" do
120
+ block_was_run = false
121
+
122
+ mon = described_class.new( "the description" )
123
+ mon.exec do |nodes|
124
+ block_was_run = true
125
+ end
126
+
127
+ mon.run( testing_nodes )
128
+
129
+ expect( block_was_run ).to be_truthy
130
+ end
131
+
132
+
133
+ it "can specify a runnable object to do the monitor's work" do
134
+ mod = Module.new do
135
+ class << self; attr_accessor :was_run ; end
136
+ @was_run = false
137
+
138
+ def self::run( nodes )
139
+ self.was_run = true
140
+ end
141
+ end
142
+
143
+
144
+ mon = described_class.new( "the description" )
145
+ mon.exec( mod )
146
+
147
+ mon.run( testing_nodes )
148
+
149
+ expect( mod.was_run ).to be_truthy
150
+ end
151
+
152
+
153
+ it "can provide a function for building arguments for its command" do
154
+ mon = described_class.new( "the description" ) do
155
+
156
+ exec 'the_command'
157
+
158
+ handle_results {|*| }
159
+ exec_input {|*| }
160
+ exec_arguments do |nodes|
161
+ Loggability[ Arborist ].debug "In the argument-builder."
162
+ nodes.map {|n| n.identifier }
163
+ end
164
+ end
165
+
166
+ expect( Process ).to receive( :spawn ) do |*args|
167
+ options = args.pop
168
+
169
+ expect( args ).to eq([ 'the_command', 'trunk', 'branch', 'leaf' ])
170
+ expect( options ).to be_a( Hash )
171
+ expect( options ).to include( :in, :out, :err )
172
+
173
+ nil
174
+ end
175
+
176
+ mon.run( testing_nodes )
177
+ end
178
+
179
+
180
+ it "can provide a function for providing input to its command" do
181
+ mon = described_class.new( "the description" ) do
182
+
183
+ exec 'cat'
184
+
185
+ exec_input do |nodes, writer|
186
+ writer.puts( nodes.map(&:identifier) )
187
+ end
188
+ handle_results do |pid, out, err|
189
+ return out.readlines.map( &:chomp )
190
+ end
191
+ end
192
+
193
+ results = mon.run( testing_nodes )
194
+
195
+ expect( results ).to eq( testing_nodes.map(&:identifier) )
196
+ end
197
+
198
+
199
+ it "can provide a function for parsing its command's output" do
200
+ mon = described_class.new( "the description" ) do
201
+
202
+ exec 'cat'
203
+
204
+ exec_arguments {|*| }
205
+ exec_input do |nodes, writer|
206
+ writer.puts( nodes.map(&:identifier) )
207
+ end
208
+ handle_results do |pid, out, err|
209
+ out.readlines.map( &:chomp ).map( &:upcase )
210
+ end
211
+ end
212
+
213
+ results = mon.run( testing_nodes )
214
+
215
+ expect( results ).to eq( testing_nodes.map(&:identifier).map(&:upcase) )
216
+ end
217
+
218
+
219
+ it "can provide a Module that implements its exec callbacks" do
220
+ the_module = Module.new do
221
+
222
+ def exec_input( nodes, writer )
223
+ writer.puts( nodes.map {|n| n.identifier } )
224
+ end
225
+
226
+ def handle_results( pid, out, err )
227
+ err.flush
228
+ return out.each_line.with_object({}) do |line, accum|
229
+ accum[ line.chomp ] = { echoed: 'yep' }
230
+ end
231
+ end
232
+
233
+ end
234
+
235
+ mon = described_class.new( "the description" ) do
236
+ exec 'cat'
237
+ exec_callbacks( the_module )
238
+ end
239
+
240
+ results = mon.run( testing_nodes )
241
+
242
+ expect( results ).to be_a( Hash )
243
+ expect( results.size ).to eq( 3 )
244
+ expect( results ).to include( *testing_nodes.map(&:identifier) )
245
+ expect( results['trunk'] ).to eq({ echoed: 'yep' })
246
+ expect( results['branch'] ).to eq({ echoed: 'yep' })
247
+ expect( results['leaf'] ).to eq({ echoed: 'yep' })
248
+ end
249
+
250
+ end
251
+