spec/arborist/node_spec.rb in arborist-0.2.0.pre20170519125456 vs spec/arborist/node_spec.rb in arborist-0.2.0
- old
+ new
@@ -9,19 +9,46 @@
describe Arborist::Node do
before( :all ) do
Arborist::Event.load_all
end
+ before( :each ) do
+ @real_derivatives = described_class.derivatives.dup
+ end
+ after( :each ) do
+ described_class.derivatives.replace( @real_derivatives )
+ end
- let( :concrete_class ) { TestNode }
- let( :subnode_class ) { TestSubNode }
+ let( :concrete_class ) do
+ Class.new( described_class )
+ end
let( :identifier ) { 'the_identifier' }
let( :identifier2 ) { 'the_other_identifier' }
+ shared_examples_for "a reachable node" do
+
+ it "is still 'reachable'" do
+ expect( node ).to be_reachable
+ expect( node ).to_not be_unreachable
+ end
+
+ end
+
+
+ shared_examples_for "an unreachable node" do
+
+ it "is not 'reachable'" do
+ expect( node ).to_not be_reachable
+ expect( node ).to be_unreachable
+ end
+
+ end
+
+
it "can be loaded from a file" do
concrete_instance = nil
expect( Kernel ).to receive( :load ).with( "a/path/to/a/node.rb" ) do
concrete_instance = concrete_class.new( identifier )
end
@@ -79,210 +106,516 @@
context "subnode classes" do
it "can declare the type of node they live under" do
- expect( subnode_class.parent_types ).to include( described_class.get_subclass(:test) )
+ subnode_class = Class.new( described_class )
+ subnode_class.parent_type( concrete_class )
+
+ expect( subnode_class.parent_types ).to include( concrete_class )
end
it "can be constructed via a factory method on instances of their parent type" do
+ subnode_class = Class.new( described_class ) do
+ def self::name; "TestSubNode"; end
+ def self::plugin_name; "testsub"; end
+ end
+ described_class.derivatives['testsub'] = subnode_class
+
+ subnode_class.parent_type( concrete_class )
parent = concrete_class.new( 'branch' )
node = parent.testsub( 'leaf' )
+
expect( node ).to be_an_instance_of( subnode_class )
+ expect( node.identifier ).to eq( 'leaf' )
+ expect( node.parent ).to eq( 'branch' )
+ end
+
+
+ it "can pre-process the factory method arguments" do
+ subnode_class = Class.new( described_class ) do
+ def self::name; "TestSubNode"; end
+ def self::plugin_name; "testsub"; end
+ def args( new_args=nil )
+ @args = new_args if new_args
+ return @args
+ end
+ def modify( attributes )
+ attributes = stringify_keys( attributes )
+ super
+ self.args( attributes['args'] )
+ end
+ end
+ described_class.derivatives['testsub'] = subnode_class
+
+ subnode_class.parent_type( concrete_class ) do |arg1, id, *args|
+ [ id, {args: [arg1] + args} ]
+ end
+
+ parent = concrete_class.new( 'branch' )
+ node = parent.testsub( :arg1, 'leaf', :arg2, :arg3 )
+
+ expect( node ).to be_an_instance_of( subnode_class )
expect( node.parent ).to eq( parent.identifier )
+ expect( node.args ).to eq([ :arg1, :arg2, :arg3 ])
end
end
context "an instance of a concrete subclass" do
- let( :node ) { concrete_class.new(identifier) }
- let( :child_node ) do
- concrete_class.new(identifier2) do
+ let( :parent_node ) { concrete_class.new(identifier) }
+ let( :sibling_node ) do
+ concrete_class.new( 'sibling' ) do
parent 'the_identifier'
end
end
+ let( :node ) do
+ concrete_class.new( identifier2 ) do
+ parent 'the_identifier'
+ end
+ end
it "can declare what its parent is by identifier" do
- expect( child_node.parent ).to eq( identifier )
+ expect( node.parent ).to eq( identifier )
end
it "can have child nodes added to it" do
- node.add_child( child_node )
- expect( node.children ).to include( child_node.identifier )
+ parent_node.add_child( node )
+ expect( parent_node.children ).to include( node.identifier )
end
it "can have child nodes appended to it" do
- node << child_node
- expect( node.children ).to include( child_node.identifier )
+ parent_node << node
+ expect( parent_node.children ).to include( node.identifier )
end
it "raises an error if a node which specifies a different parent is added to it" do
- not_child_node = concrete_class.new(identifier2) do
+ stranger_node = concrete_class.new( identifier2 ) do
parent 'youre_not_my_mother'
end
expect {
- node.add_child( not_child_node )
+ parent_node.add_child( stranger_node )
}.to raise_error( /not a child of/i )
end
it "doesn't add the same child more than once" do
- node.add_child( child_node )
- node.add_child( child_node )
- expect( node.children.size ).to eq( 1 )
+ parent_node.add_child( node )
+ parent_node.add_child( node )
+ expect( parent_node.children.size ).to eq( 1 )
end
it "knows it doesn't have any children if it's empty" do
- expect( node ).to_not have_children
+ expect( parent_node ).to_not have_children
end
it "knows it has children if subnodes have been added" do
- node.add_child( child_node )
- expect( node ).to have_children
+ parent_node.add_child( node )
+ expect( parent_node ).to have_children
end
it "knows how to remove one of its children" do
- node.add_child( child_node )
- node.remove_child( child_node )
- expect( node ).to_not have_children
+ parent_node.add_child( node )
+ parent_node.remove_child( node )
+ expect( parent_node ).to_not have_children
end
- describe "status" do
- it "starts out in `unknown` status" do
- expect( node ).to be_unknown
+ it "starts out in `unknown` status" do
+ expect( parent_node ).to be_unknown
+ end
+
+
+ it "remembers status time changes" do
+ expect( node.status_changed ).to eq( Time.at(0) )
+
+ time = Time.at( 1523900910 )
+ allow( Time ).to receive( :now ).and_return( time )
+
+ node.update( { error: 'boom' } )
+ expect( node ).to be_down
+ expect( node.status_changed ).to eq( time )
+ expect( node.status_last_changed ).to eq( Time.at(0) )
+
+
+ node.update( {} )
+ expect( node ).to be_up
+ expect( node.status_last_changed ).to eq( time )
+ end
+
+
+ it "groups errors from separate monitors by their key" do
+ expect( node ).to be_unknown
+
+ node.update( {error: 'ded'}, 'MonitorTron2000' )
+ node.update( {error: 'moar ded'}, 'MonitorTron5000' )
+ expect( node ).to be_down
+
+ expect( node.errors.length ).to eq( 2 )
+ node.update( {}, 'MonitorTron5000' )
+
+ expect( node ).to be_down
+ expect( node.errors.length ).to eq( 1 )
+
+ node.update( {}, 'MonitorTron2000' )
+ expect( node ).to be_up
+ end
+
+
+ it "sets a default monitor key" do
+ node.update( error: 'ded' )
+ expect( node ).to be_down
+ expect( node.errors ).to eq({ '_' => 'ded' })
+ end
+
+
+ describe "in `unknown` status" do
+
+ let( :node ) do
+ obj = super()
+ obj.status = 'unknown'
+ obj
end
- it "transitions to `up` status if its state is updated with no `error` property" do
- node.update( tested: true )
- expect( node ).to be_up
+ it_behaves_like "a reachable node"
+
+
+ it "transitions to `up` status if doesn't have any errors after an update" do
+ expect {
+ node.update( tested: true )
+ }.to change { node.status }.from( 'unknown' ).to( 'up' )
end
it "transitions to `down` status if its state is updated with an `error` property" do
- node.update( error: "Couldn't talk to it!" )
- expect( node ).to be_down
+ expect {
+ node.update( error: "Couldn't talk to it!" )
+ }.to change { node.status }.from( 'unknown' ).to( 'down' )
end
- it "transitions from `down` to `acked` status if it's updated with an `ack` property" do
- node.status = 'down'
- node.errors = 'Something is wrong | he falls | betraying the trust | "\
+
+ it "transitions to `warn` status if its state is updated with a `warning` property" do
+ expect {
+ node.update( warning: "Things are starting to look bad!" )
+ }.to change { node.status }.from( 'unknown' ).to( 'warn' )
+ end
+
+
+ it "transitions to `disabled` if it's acknowledged" do
+ expect {
+ node.acknowledge( message: "Maintenance", sender: 'mahlon' )
+ }.to change { node.status }.from( 'unknown' ).to( 'disabled' )
+ end
+
+ end
+
+
+ describe "in `up` status" do
+
+ let( :node ) do
+ obj = super()
+ obj.status = 'up'
+ obj
+ end
+
+
+ it_behaves_like "a reachable node"
+
+
+ it "stays in `up` status if doesn't have any errors after an update" do
+ expect {
+ node.update( tested: true )
+ }.to_not change { node.status }.from( 'up' )
+ end
+
+
+ it "transitions to `down` status if its state is updated with an `error` property" do
+ expect {
+ node.update( error: "Couldn't talk to it!" )
+ }.to change { node.status }.from( 'up' ).to( 'down' )
+ end
+
+
+ it "transitions to `down` status if it's updated with both an `error` and `warning` property" do
+ expect {
+ node.update( error: "Couldn't talk to it!", warning: "Above configured levels!" )
+ }.to change { node.status }.from( 'up' ).to( 'down' )
+ end
+
+
+ it "transitions to `warn` status if its state is updated with a `warning` property" do
+ expect {
+ node.update( warning: "Things are starting to look bad!" )
+ }.to change { node.status }.from( 'up' ).to( 'warn' )
+ end
+
+
+ it "transitions to `disabled` if it's acknowledged" do
+ expect {
+ node.acknowledge( message: "Maintenance", sender: 'mahlon' )
+ }.to change { node.status }.from( 'up' ).to( 'disabled' )
+ end
+
+
+ it "transitions to `quieted` if it's notified that its parent has gone down" do
+ down_event = Arborist::Event.create( :node_down, parent_node )
+ expect {
+ node.handle_event( down_event )
+ }.to change { node.status }.from( 'up' ).to( 'quieted' )
+ end
+
+ end
+
+
+ describe "in `down` status" do
+
+ let( :node ) do
+ obj = super()
+ obj.status = 'down'
+ obj.errors['moldovia'] = 'Something is wrong | he falls | betraying the trust | "\
"there is a disaster in his life.'
- node.update( ack: {message: "Leitmotiv", sender: 'ged'} )
- expect( node ).to be_acked
+ obj
end
- it "transitions from `acked` to `up` status if its error is cleared" do
- node.status = 'down'
- node.errors = { '_' => 'Something is wrong | he falls | betraying the trust | "\
- "there is a disaster in his life.' }
- node.update( ack: {message: "Leitmotiv", sender: 'ged'} )
- node.update( error: nil )
- expect( node ).to be_up
+ it_behaves_like "an unreachable node"
+
+
+ it "transitions to `acked` status if it's acknowledged" do
+ expect {
+ node.acknowledge( message: "Leitmotiv", sender: 'ged' )
+ }.to change { node.status }.from( 'down' ).to( 'acked' )
end
- it "stays `up` if its error is cleared and stays cleared" do
- node.status = 'down'
- node.errors = { '_' => 'stay up damn you!' }
- node.update( ack: {message: "Leitmotiv", sender: 'ged'} )
- node.update( error: nil )
- node.update( error: nil )
- expect( node ).to be_up
+ it "transitions to `up` status if all of its errors are cleared" do
+ expect {
+ node.update( {error: nil}, 'moldovia' )
+ }.to change { node.status }.from( 'down' ).to( 'up' )
end
- it "transitions to `disabled` from `up` status if it's updated with an `ack` property" do
- node.status = 'up'
- node.update( ack: {message: "Maintenance", sender: 'mahlon'} )
- expect( node ).to be_disabled
+ it "transitions to `warn` status if errors are cleared but warnings remain" do
+ expect {
+ node.update( {error: nil, warning: 'squirt!'}, 'moldovia' )
+ }.to change { node.status }.from( 'down' ).to( 'warn' )
end
- it "transitions to `disabled` from `unknown` status if it's updated with an `ack` property" do
- node.status = 'unknown'
- node.update( ack: {message: "Maintenance", sender: 'mahlon'} )
+ end
- expect( node ).to be_disabled
+
+ describe "in `warn` status" do
+
+ let( :node ) do
+ obj = super()
+ obj.status = 'warn'
+ obj.warnings = { 'beach' => 'Sweaty but functional servers.' }
+ obj
end
+
+ it_behaves_like "a reachable node"
+
+
+ it "transitions to `up` if its warnings are cleared" do
+ expect {
+ node.update( {warning: nil}, 'beach' )
+ }.to change { node.status }.from( 'warn' ).to( 'up' )
+ end
+
+
+ it "transitions to `down` if has an error set" do
+ expect {
+ node.update( {error: "Shark warning."}, 'beach' )
+ }.to change { node.status }.from( 'warn' ).to( 'down' )
+ end
+
+
+ it "transitions to `disabled` if it's acknowledged" do
+ expect {
+ node.acknowledge( message: "Chill", sender: 'ged' )
+ }.to change { node.status }.from( 'warn' ).to( 'disabled' )
+ end
+
+ end
+
+
+ describe "in `acked` status" do
+
+ let( :node ) do
+ obj = super()
+ obj.status = 'acked'
+ obj.errors['moldovia'] = 'Something is wrong | he falls | betraying the trust | "\
+ "there is a disaster in his life.'
+ obj.acknowledge( message: "Leitmotiv", sender: 'ged' )
+ obj
+ end
+
+
+ it_behaves_like "a reachable node"
+
+
+ it "transitions to `up` status if its error is cleared" do
+ expect {
+ node.update( {error: nil}, 'moldovia' )
+ }.to change { node.status }.from( 'acked' ).to( 'up' )
+ end
+
+
+ it "stays `up` if it is updated twice with an error key" do
+ node.update( {error: nil}, 'moldovia' )
+
+ expect {
+ node.update( {error: nil}, 'moldovia' ) # make sure it stays cleared
+ }.to_not change { node.status }.from( 'up' )
+ end
+
+ end
+
+
+ describe "in `disabled` status" do
+
+ let( :node ) do
+ obj = super()
+ obj.acknowledge( message: "Bikini models", sender: 'ged' )
+ obj
+ end
+
+
+ it_behaves_like "an unreachable node"
+
+
it "stays `disabled` if it gets an error" do
- node.status = 'up'
- node.update( ack: {message: "Maintenance", sender: 'mahlon'} )
- node.update( error: "take me to the virus hospital" )
+ expect {
+ node.update( error: "take me to the virus hospital" )
+ }.to_not change { node.status }.from( 'disabled' )
- expect( node ).to be_disabled
expect( node.ack ).to_not be_nil
end
+
+ it "stays `disabled` if it gets a warning" do
+ expect {
+ node.update( warning: "heartbone" )
+ }.to_not change { node.status }.from( 'disabled' )
+
+ expect( node.ack ).to_not be_nil
+ end
+
+
it "stays `disabled` if it gets a successful update" do
- node.status = 'up'
- node.update( ack: {message: "Maintenance", sender: 'mahlon'} )
- node.update( ping: {time: 0.02} )
+ expect {
+ node.update( ping: {time: 0.02} )
+ }.to_not change { node.status }.from( 'disabled' )
- expect( node ).to be_disabled
expect( node.ack ).to_not be_nil
end
- it "transitions to `unknown` from `disabled` status if its ack is cleared" do
- node.status = 'up'
- node.update( ack: {message: "Maintenance", sender: 'mahlon'} )
- node.update( ack: nil )
- expect( node ).to_not be_disabled
- expect( node ).to be_unknown
+ it "transitions to `unknown` if its acknowledgment is cleared" do
+ expect {
+ node.unacknowledge
+ }.to change { node.status }.from( 'disabled' ).to( 'unknown' )
+
expect( node.ack ).to be_nil
end
- it "knows if it's status deems it 'reachable'" do
- node.update( error: nil )
- expect( node ).to be_reachable
- expect( node ).to_not be_unreachable
+ end
+
+
+ describe "in `quieted` status because its parent is down" do
+
+ let( :down_event ) { Arborist::Event.create(:node_down, parent_node) }
+ let( :up_event ) { Arborist::Event.create(:node_up, parent_node) }
+
+ let( :node ) do
+ obj = super()
+ obj.handle_event( down_event )
+ obj
end
- it "knows if it's status deems it 'unreachable'" do
- node.update( error: 'ded' )
- expect( node ).to be_unreachable
- expect( node ).to_not be_reachable
+
+ it_behaves_like "an unreachable node"
+
+
+ it "remains `quieted` even if updated with an error" do
+ expect {
+ node.update( {error: "Internal error"}, 'webservice' )
+ }.to_not change { node.status }.from( 'quieted' )
end
- it "groups errors from separate monitor by their key" do
- expect( node ).to be_unknown
- node.update( _monitor_key: 'MonitorTron2000', error: 'ded' )
- node.update( _monitor_key: 'MonitorTron5000', error: 'moar ded' )
- expect( node ).to be_down
+ it "transitions to `unknown` if its parent transitions to up" do
+ up_event = Arborist::Event.create( :node_up, parent_node )
- expect( node.errors.length ).to eq( 2 )
- node.update( _monitor_key: 'MonitorTron5000' )
+ expect {
+ node.handle_event( up_event )
+ }.to change { node.status }.from( 'quieted' ).to( 'unknown' )
+ end
- expect( node ).to be_down
- expect( node.errors.length ).to eq( 1 )
- node.update( _monitor_key: 'MonitorTron2000' )
- expect( node ).to be_up
+ it "transitions to `unknown` if its parent transitions to warn" do
+ warn_event = Arborist::Event.create( :node_warn, parent_node )
+
+ expect {
+ node.handle_event( warn_event )
+ }.to change { node.status }.from( 'quieted' ).to( 'unknown' )
end
- it "sets a default monitor key" do
- node.update( error: 'ded' )
- expect( node ).to be_down
- expect( node.errors ).to eq({ '_' => 'ded' })
+
+ it "transitions to `disabled` if it's acknowledged" do
+ expect {
+ node.acknowledge( message: 'Turning this off for now.', sender: 'ged' )
+ }.to change { node.status }.from( 'quieted' ).to( 'disabled' )
end
+
end
+ describe "in `quieted` status because one of its dependencies is down" do
+
+ let( :down_event ) { Arborist::Event.create(:node_down, sibling_node) }
+ let( :up_event ) { Arborist::Event.create(:node_up, sibling_node) }
+
+ let( :node ) do
+ obj = super()
+ obj.depends_on( 'sibling' )
+ obj.handle_event( down_event )
+ obj
+ end
+
+
+ it_behaves_like "an unreachable node"
+
+
+ it "transitions to `unknown` if its reasons for being quieted are cleared" do
+ expect {
+ node.handle_event( up_event )
+ }.to change { node.status }.from( 'quieted' ).to( 'unknown' )
+ end
+
+
+ it "transitions to `disabled` if it's acknowledged" do
+ expect {
+ node.acknowledge( message: 'Turning this off for now.', sender: 'ged' )
+ }.to change { node.status }.from( 'quieted' ).to( 'disabled' )
+ end
+
+ end
+
+
describe "Properties API" do
it "is initialized with an empty set" do
expect( node.properties ).to be_empty
end
@@ -374,24 +707,26 @@
describe "Enumeration" do
it "iterates over its children for #each" do
- parent = node
+ parent = parent_node
parent <<
concrete_class.new('child1') { parent 'the_identifier' } <<
concrete_class.new('child2') { parent 'the_identifier' } <<
concrete_class.new('child3') { parent 'the_identifier' }
- expect( parent.map(&:identifier) ).to eq([ 'child1', 'child2', 'child3' ])
+ expect( parent_node.map(&:identifier) ).to eq([ 'child1', 'child2', 'child3' ])
end
end
describe "Serialization" do
+ # From spec_helper.rb
+ let( :concrete_class ) { TestNode }
let( :node ) do
concrete_class.new( 'foo' ) do
parent 'bar'
description "The prototypical node"
tags :chunker, :hunky, :flippin, :hippo
@@ -405,16 +740,41 @@
update( 'song' => 'Around the World', 'artist' => 'Daft Punk', 'length' => '7:09' )
end
end
+ let( :tree ) do
+ node_hierarchy( node,
+ node_hierarchy( 'host-a',
+ testing_node( 'host-a-www' ),
+ testing_node( 'host-a-smtp' ),
+ testing_node( 'host-a-imap' )
+ ),
+ node_hierarchy( 'host-b',
+ testing_node( 'host-b-www' ),
+ testing_node( 'host-b-nfs' ),
+ testing_node( 'host-b-ssh' )
+ ),
+ node_hierarchy( 'host-c',
+ testing_node( 'host-c-www' )
+ ),
+ node_hierarchy( 'host-d',
+ testing_node( 'host-d-ssh' ),
+ testing_node( 'host-d-amqp' ),
+ testing_node( 'host-d-database' ),
+ testing_node( 'host-d-memcached' )
+ )
+ )
+ end
+
it "can restore saved state from an older copy of the node" do
old_node = Marshal.load( Marshal.dump(node) )
old_node.status = 'down'
old_node.status_changed = Time.now - 400
+ old_node.status_last_changed = Time.now - 800
old_node.errors = "Host unreachable"
old_node.update(
ack: {
'time' => Time.now - 200,
'message' => "Technician dispatched.",
@@ -431,10 +791,11 @@
node.restore( old_node )
expect( node.status ).to eq( old_node.status )
expect( node.status_changed ).to eq( old_node.status_changed )
+ expect( node.status_last_changed ).to eq( old_node.status_last_changed )
expect( node.errors ).to eq( old_node.errors )
expect( node.ack ).to eq( old_node.ack )
expect( node.properties ).to include( old_node.properties )
expect( node.last_contacted ).to eq( old_node.last_contacted )
expect( node.dependencies ).to eql( old_node.dependencies )
@@ -480,35 +841,67 @@
expect( node.dependencies.down_subdeps.length ).to eq( 1 )
end
it "can return a Hash of serializable node data" do
- result = node.to_h
+ result = tree.to_h
expect( result ).to be_a( Hash )
expect( result ).to include(
:identifier,
:parent, :description, :tags, :properties, :ack, :status,
:last_contacted, :status_changed, :errors, :quieted_reasons,
- :dependencies
+ :dependencies, :status_last_changed
)
expect( result[:identifier] ).to eq( 'foo' )
expect( result[:type] ).to eq( 'testnode' )
expect( result[:parent] ).to eq( 'bar' )
expect( result[:description] ).to eq( node.description )
expect( result[:tags] ).to eq( node.tags )
expect( result[:properties] ).to eq( node.properties )
expect( result[:ack] ).to be_nil
expect( result[:last_contacted] ).to eq( node.last_contacted.iso8601 )
expect( result[:status_changed] ).to eq( node.status_changed.iso8601 )
+ expect( result[:status_last_changed] ).to eq( node.status_last_changed.iso8601 )
expect( result[:errors] ).to be_a( Hash )
expect( result[:errors] ).to be_empty
expect( result[:dependencies] ).to be_a( Hash )
expect( result[:quieted_reasons] ).to be_a( Hash )
+
+ expect( result[:children] ).to be_empty
end
+ it "can include all of its serialized children" do
+ result = tree.to_h( depth: -1 )
+
+ expect( result ).to be_a( Hash )
+ expect( result ).to include(
+ :identifier,
+ :parent, :description, :tags, :properties, :ack, :status,
+ :last_contacted, :status_changed, :errors, :quieted_reasons,
+ :dependencies
+ )
+
+ expect( result[:children] ).to be_a( Hash )
+ expect( result[:children].length ).to eq( 4 )
+
+ host_a = result[:children]['host-a']
+ expect( host_a ).to be_a( Hash )
+ expect( host_a ).to include(
+ :identifier,
+ :parent, :description, :tags, :properties, :ack, :status,
+ :last_contacted, :status_changed, :errors, :quieted_reasons,
+ :dependencies
+ )
+ expect( host_a[:children].length ).to eq( 3 )
+ end
+
+
+ it "can include a specific depth of its children"
+
+
it "can be reconstituted from a serialized Hash of node data" do
hash = node.to_h
cloned_node = concrete_class.from_hash( hash )
expect( cloned_node ).to eq( node )
@@ -523,14 +916,14 @@
end
it "an ACKed node stays ACKed when serialized and restored" do
node.update( error: "there's a fire" )
- node.update( ack: {
+ node.acknowledge(
message: 'We know about the fire. It rages on.',
sender: '1986 Labyrinth David Bowie'
- })
+ )
expect( node ).to be_acked
restored_node = Marshal.load( Marshal.dump(node) )
expect( restored_node ).to be_acked
@@ -618,24 +1011,82 @@
expect( delta_event.payload ).to include( 'status' => ['up', 'down'] )
end
- it "generates a node.acked event when a node is acked" do
- node.update( error: 'ping failed ')
- events = node.update(ack: {
+ it "includes the original ack in delta events" do
+ events = node.acknowledge(
message: "I have a poisonous friend. She's living in the house.",
sender: 'Seabound'
- })
+ )
+ delta_event = events.find {|ev| ev.type == 'node.delta' }
+ expect( delta_event.payload ).to include( 'status' => ['up', 'disabled'] )
+ expect( delta_event.payload ).to include( 'ack' => [ nil, a_hash_including(sender: 'Seabound') ] )
- expect( events.size ).to eq( 3 )
- ack_event = events.find {|ev| ev.type == 'node.acked' }
+ events = node.unacknowledge
+ delta_event = events.find {|ev| ev.type == 'node.delta' }
- expect( ack_event ).to be_a( Arborist::Event )
- expect( ack_event.payload ).to include( ack: a_hash_including(sender: 'Seabound') )
+ expect( delta_event.payload ).to include( 'status' => ['disabled', 'unknown'] )
+ expect( delta_event.payload ).to include( 'ack' => [ a_hash_including(sender: 'Seabound'), nil ] )
end
+
+ it "generates a node.delta event when a node ack is updated" do
+ node.update( error: 'ping failed ')
+ node.acknowledge(
+ message: "The last one was dead. This one is on her way.",
+ sender: 'Average Trigram'
+ )
+
+ events = node.acknowledge(
+ message: "000100101011111",
+ sender: 'Robots'
+ )
+ expect( events.size ).to eq( 2 )
+
+ delta = events.last
+ expect( delta ).to be_a( Arborist::Event::NodeDelta )
+
+ expect( delta.payload ).
+ to include( 'ack' => [
+ a_hash_including(sender: 'Average Trigram'), a_hash_including(sender: 'Robots')
+ ]
+ )
+ end
+
+
+ it "generates a node.acked and node.delta event when a node is acked" do
+ node.update( error: 'ping failed ')
+ events = node.acknowledge(
+ message: "The last one was dead. This one is on her way.",
+ sender: 'Average Trigram'
+ )
+
+ expect( events.size ).to eq( 2 )
+
+ expect( events.first ).to be_a( Arborist::Event::NodeAcked )
+ expect( events.last ).to be_a( Arborist::Event::NodeDelta )
+ expect( events.first.payload ).
+ to include( ack: a_hash_including(sender: 'Average Trigram') )
+ expect( events.last.payload ).
+ to include( 'ack' => [ nil, a_hash_including(sender: 'Average Trigram') ])
+ expect( events.last.payload ).to include( 'status' => ['down', 'acked'] )
+ end
+
+
+ it "generates a node.down and node.delta event when a node is unacked" do
+ node.update( error: 'ping failed ')
+ node.acknowledge(
+ message: "The humans are dead. I poked one. It's dead.",
+ sender: 'Jermaine and Brit'
+ )
+
+ events = node.unacknowledge
+ expect( events.last.payload ).
+ to include( 'ack' => [ a_hash_including(sender: 'Jermaine and Brit'), nil ])
+ expect( events.last.payload ).to include( 'status' => ['acked', 'down'] )
+ end
end
describe "subscriptions" do
@@ -646,18 +1097,18 @@
tags :chunker, :hunky, :flippin, :hippo
end
end
+
it "allows the addition of a Subscription" do
sub = Arborist::Subscription.new {}
node.add_subscription( sub )
expect( node.subscriptions ).to include( sub.id )
expect( node.subscriptions[sub.id] ).to be( sub )
end
-
it "allows the removal of a Subscription" do
sub = Arborist::Subscription.new {}
node.add_subscription( sub )
node.remove_subscription( sub.id )
expect( node.subscriptions ).to_not include( sub )
@@ -676,15 +1127,27 @@
expect( results.size ).to eq( 1 )
expect( results ).to all( be_a(Arborist::Subscription) )
expect( results.first ).to be( sub )
end
+
+ it "can return the identifiers of all other nodes that subscribe to it" do
+
+ end
+
end
describe "matching" do
+ let( :concrete_class ) do
+ cls = Class.new( described_class ) do
+ def self::name; "TestNode"; end
+ end
+ end
+
+
let( :node ) do
concrete_class.new( 'foo' ) do
parent 'bar'
description "The prototypical node"
tags :chunker, :hunky, :flippin, :hippo
@@ -715,20 +1178,28 @@
it "can be matched with its status" do
expect( node ).to match_criteria( status: 'up' )
expect( node ).to_not match_criteria( status: 'down' )
end
+ it "can be matched with multiple statuses" do
+ expect( node ).to match_criteria( status: ['up','warn'] )
+ expect( node ).to_not match_criteria( status: 'down' )
+ expect( node ).to match_criteria( status: 'up' )
+ end
+
it "can be matched with its type" do
expect( node ).to match_criteria( type: 'testnode' )
expect( node ).to_not match_criteria( type: 'service' )
end
it "can be matched with its parent" do
expect( node ).to match_criteria( parent: 'bar' )
+ expect( node ).to match_criteria( parent: [ 'bar', 'hooowat' ] )
expect( node ).to_not match_criteria( parent: 'hooowat' )
+ expect( node ).to_not match_criteria( parent: [ 'hooowat', 'wathoooo' ] )
end
it "can be matched with a single tag" do
expect( node ).to match_criteria( tag: 'hunky' )
@@ -900,25 +1371,25 @@
mgr = Arborist::Manager.new
mgr.load_tree([ vmhost01, vm01, memcache ])
events = vmhost01.
- update( ack: {message: "Imma gonna f up yo' sash", sender: "GOD"} )
+ acknowledge( message: "Imma gonna f up yo' sash", sender: "GOD" )
vmhost01.publish_events( *events )
expect( memcache ).to be_quieted
end
end
describe "operational attribute modification" do
-
let( :node ) do
concrete_class.new( 'foo' ) do
parent 'bar'
+ config boop: false
description "The prototypical node"
tags :chunker, :hunky, :flippin, :hippo
end
end
@@ -933,18 +1404,109 @@
node.modify( description: 'A different node' )
expect( node.description ).to eq( 'A different node' )
end
+ it "can change any custom configuration values" do
+ node.modify( config: { boop: true } )
+ expect( node.config ).to eq({ 'boop' => true })
+ end
+
+
it "can change its tags" do
node.modify( tags: %w[dew dairy daisy dilettante] )
expect( node.tags ).to eq( %w[dew dairy daisy dilettante] )
end
it "arrayifies tags modifications" do
node.modify( tags: 'single' )
expect( node.tags ).to eq( %w[single] )
+ end
+ end
+
+
+ describe "reparenting" do
+
+ before( :each ) do
+ @old_parent = concrete_class.new( 'router1' ) do
+ description "The first router"
+ end
+ @new_parent = concrete_class.new( 'router2' ) do
+ description "The second router"
+ end
+ @node = concrete_class.new( 'foo' ) do
+ parent 'router1'
+ description "The prototypical node"
+ end
+
+ @old_parent.add_child( @node )
+ end
+
+ let( :node ) { @node }
+ let( :old_parent ) { @old_parent }
+ let( :new_parent ) { @new_parent }
+
+
+ it "moves itself to the new node and removes itself from its old parent" do
+ expect( old_parent.children ).to include( node.identifier )
+ expect( new_parent.children ).to_not include( node.identifier )
+
+ node.reparent( old_parent, new_parent )
+
+ expect( old_parent.children ).to_not include( node.identifier )
+ expect( new_parent.children ).to include( node.identifier )
+ end
+
+
+ it "sets its state to unknown if it was down prior to the move" do
+ node.update( error: 'Rock and Roll McDonalds' )
+
+ node.reparent( old_parent, new_parent )
+
+ expect( node ).to be_unknown
+ end
+
+
+ it "sets its state to unknown if it was quieted by its parent prior to the move" do
+ node.quieted_reasons[ :primary ] = "Timex takes a licking and... well, broke, it looks like."
+ node.status = 'quieted'
+
+ node.reparent( old_parent, new_parent )
+
+ expect( node ).to be_unknown
+ end
+
+
+ it "keeps its quieted state if it was quieted by secondary dependency prior to the move" do
+ node.quieted_reasons[ :primary ] = "Timex takes a licking and... well, broke, it looks like."
+ node.quieted_reasons[ :secondary ] = "Western Union: The fastest way to send money"
+ node.status = 'quieted'
+
+ node.reparent( old_parent, new_parent )
+
+ expect( node ).to be_quieted
+ end
+
+
+ it "keeps its disabled state" do
+ node.acknowledge( message: 'Moving the machine', sender: 'Me' )
+ expect( node ).to be_disabled
+
+ node.reparent( old_parent, new_parent )
+
+ expect( node ).to be_disabled
+ end
+
+
+ it "keeps its acked state" do
+ node.update( {error: 'Batman whooped my ass.'}, 'gotham' )
+ node.acknowledge( message: 'Moving the machine', sender: 'Me' )
+ expect( node ).to be_acked
+
+ node.reparent( old_parent, new_parent )
+
+ expect( node ).to be_acked
end
end
end