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