spec/arborist/node_spec.rb in arborist-0.0.1.pre20160128152542 vs spec/arborist/node_spec.rb in arborist-0.0.1.pre20160606141735
- old
+ new
@@ -7,14 +7,16 @@
describe Arborist::Node do
let( :concrete_class ) { TestNode }
+ let( :subnode_class ) { TestSubNode }
let( :identifier ) { 'the_identifier' }
let( :identifier2 ) { 'the_other_identifier' }
+
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
@@ -24,10 +26,24 @@
expect( result.length ).to eq( 1 )
expect( result ).to include( concrete_instance )
end
+ it "can be constructed from a Hash" do
+ instance = concrete_class.new( identifier,
+ parent: 'branch',
+ description: 'A testing node',
+ tags: ['internal', 'testing']
+ )
+
+ expect( instance ).to be_a( described_class )
+ expect( instance.parent ).to eq( 'branch' )
+ expect( instance.description ).to eq( 'A testing node' )
+ expect( instance.tags ).to include( 'internal', 'testing' )
+ end
+
+
it "can load multiple nodes from a single file" do
concrete_instance1 = concrete_instance2 = nil
expect( Kernel ).to receive( :load ).with( "a/path/to/a/node.rb" ) do
concrete_instance1 = concrete_class.new( identifier )
concrete_instance2 = concrete_class.new( identifier2 )
@@ -55,10 +71,27 @@
described_class.new 'bad identifier'
}.to raise_error( RuntimeError, /identifier/i )
end
+ 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) )
+ end
+
+
+ it "can be constructed via a factory method on instances of their parent type" do
+ parent = concrete_class.new( 'branch' )
+ node = parent.testsub( 'leaf' )
+ expect( node ).to be_an_instance_of( subnode_class )
+ expect( node.parent ).to eq( parent.identifier )
+ 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
@@ -143,10 +176,30 @@
"there is a disaster in his life.'
node.update( ack: {message: "Leitmotiv", sender: 'ged'} )
expect( node ).to be_acked
end
+ it "transitions from `acked` to `up` status if its error is cleared" do
+ node.status = 'down'
+ node.error = '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
+ end
+
+ it "stays `up` if its error is cleared and stays cleared" do
+ node.status = 'down'
+ node.error = 'stay up damn you!'
+ node.update( ack: {message: "Leitmotiv", sender: 'ged'} )
+ node.update( error: nil )
+ node.update( error: nil )
+
+ expect( node ).to be_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
@@ -296,53 +349,134 @@
concrete_class.new( 'foo' ) do
parent 'bar'
description "The prototypical node"
tags :chunker, :hunky, :flippin, :hippo
+ depends_on(
+ all_of('postgres', 'rabbitmq', 'memcached', on: 'svchost'),
+ any_of('webproxy', on: ['fe-host1','fe-host2','fe-host3'])
+ )
+
update( 'song' => 'Around the World', 'artist' => 'Daft Punk', 'length' => '7:09' )
end
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.error = "Host unreachable"
+ old_node.update(
+ ack: {
+ 'time' => Time.now - 200,
+ 'message' => "Technician dispatched.",
+ 'sender' => 'darby@example.com'
+ }
+ )
+ old_node.properties.replace(
+ 'ping' => {
+ 'ttl' => 0.23
+ }
+ )
+ old_node.last_contacted = Time.now - 28
+ old_node.dependencies.mark_down( 'svchost-postgres' )
+
+ node.restore( old_node )
+
+ expect( node.status ).to eq( old_node.status )
+ expect( node.status_changed ).to eq( old_node.status_changed )
+ expect( node.error ).to eq( old_node.error )
+ 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 )
+ end
+
+
+ it "doesn't restore operational attributes from the node file on disk with those from saved state" do
+ old_node = Marshal.load( Marshal.dump(node) )
+ node_copy = Marshal.load( Marshal.dump(node) )
+
+ old_node.instance_variable_set( :@parent, 'foo' )
+ old_node.instance_variable_set( :@description, 'Some older description' )
+ old_node.tags( :bunker, :lucky, :tickle, :trucker )
+ old_node.source = '/somewhere/else'
+
+ node.restore( old_node )
+
+ expect( node.parent ).to eq( node_copy.parent )
+ expect( node.description ).to eq( node_copy.description )
+ expect( node.tags ).to eq( node_copy.tags )
+ expect( node.source ).to eq( node_copy.source )
+ expect( node.dependencies ).to eq( node_copy.dependencies )
+ end
+
+
+ it "doesn't replace dependencies if they've changed" do
+ old_node = Marshal.load( Marshal.dump(node) )
+ old_node.dependencies.mark_down( 'svchost-postgres' )
+ old_node.dependencies.mark_down( 'svchost-rabbitmq' )
+
+ # Drop 'svchost-rabbitmq'
+ node.depends_on(
+ node.all_of('postgres', 'memcached', on: 'svchost'),
+ node.any_of('webproxy', on: ['fe-host1','fe-host2','fe-host3'])
+ )
+
+ node.restore( old_node )
+
+ expect( node.dependencies ).to_not eql( old_node.dependencies )
+ expect( node.dependencies.all_identifiers ).to_not include( 'svchost-rabbitmq' )
+ expect( node.dependencies.down_subdeps.length ).to eq( 1 )
+ end
+
+
it "can return a Hash of serializable node data" do
- result = node.to_hash
+ result = node.to_h
expect( result ).to be_a( Hash )
expect( result ).to include(
:identifier,
- :parent, :description, :tags, :properties, :status, :ack,
- :last_contacted, :status_changed, :error
+ :parent, :description, :tags, :properties, :ack, :status,
+ :last_contacted, :status_changed, :error, :quieted_reasons,
+ :dependencies
)
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[:status] ).to eq( node.status )
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[:error] ).to be_nil
+ expect( result[:dependencies] ).to be_a( Hash )
+ expect( result[:quieted_reasons] ).to be_a( Hash )
end
it "can be reconstituted from a serialized Hash of node data" do
- hash = node.to_hash
+ hash = node.to_h
cloned_node = concrete_class.from_hash( hash )
expect( cloned_node ).to eq( node )
end
- it "an ACKed node stays ACKed when reconstituted" do
+ it "an ACKed node goes back to ACKed when re-added to the tree" do
+
node.update( error: "there's a fire" )
node.update( ack: {
message: 'We know about the fire. It rages on.',
sender: '1986 Labyrinth David Bowie'
})
- cloned_node = concrete_class.from_hash( node.to_hash )
+ cloned_node = concrete_class.from_hash( node.to_h )
+ node_added_event = Arborist::Event.create( :sys_node_added, cloned_node )
+ cloned_node.handle_event( node_added_event )
expect( cloned_node ).to be_acked
end
@@ -446,11 +580,11 @@
expect( events.size ).to eq( 3 )
ack_event = events.find {|ev| ev.type == 'node.acked' }
expect( ack_event ).to be_a( Arborist::Event )
- expect( ack_event.payload ).to include( sender: 'Seabound' )
+ expect( ack_event.payload ).to include( ack: a_hash_including(sender: 'Seabound') )
end
end
@@ -464,30 +598,30 @@
end
end
it "allows the addition of a Subscription" do
- sub = Arborist::Subscription.new( 'test', { type: 'host'} )
+ 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( 'test', { type: 'host'} )
+ sub = Arborist::Subscription.new {}
node.add_subscription( sub )
node.remove_subscription( sub.id )
expect( node.subscriptions ).to_not include( sub )
end
it "can find subscriptions that match a given event" do
events = node.update( 'song' => 'Fear', 'artist' => "Mind.in.a.Box" )
delta_event = events.find {|ev| ev.type == 'node.delta' }
- sub = Arborist::Subscription.new( 'node.delta' )
+ sub = Arborist::Subscription.new( 'node.delta' ) {}
node.add_subscription( sub )
results = node.find_matching_subscriptions( delta_event )
expect( results.size ).to eq( 1 )
@@ -573,9 +707,152 @@
)
expect( node ).to_not match_criteria( sausage: {size: 'lunch'} )
expect( node ).to_not match_criteria( other: 'key' )
expect( node ).to_not match_criteria( sausage: 'weißwürst' )
end
+
+ end
+
+
+ describe "secondary dependencies" do
+
+ let( :provider_node_parent ) do
+ concrete_class.new( 'san' )
+ end
+
+ let( :provider_node ) do
+ concrete_class.new( 'san-iscsi' ) do
+ parent 'san'
+ end
+ end
+
+ let( :node ) do
+ concrete_class.new( 'appserver' ) do
+ description "An appserver virtual machine"
+ end
+ end
+
+ let( :manager ) do
+ man = Arborist::Manager.new
+ man.load_tree([ node, provider_node, provider_node_parent ])
+ man
+ end
+
+
+ it "can be declared for a node" do
+ node.depends_on( 'san-iscsi' )
+ expect( node ).to have_dependencies
+ expect( node.dependencies ).to include( 'san-iscsi' )
+ end
+
+
+ it "can't be declared for the root node" do
+ expect {
+ node.depends_on( '_' )
+ }.to raise_exception( Arborist::ConfigError, /root node/i )
+ end
+
+
+ it "can't be declared for itself" do
+ expect {
+ node.depends_on( 'appserver' )
+ }.to raise_exception( Arborist::ConfigError, /itself/i )
+ end
+
+
+ it "can't be declared for any of its ancestors" do
+ provider_node.depends_on( 'san' )
+
+ expect {
+ provider_node.register_secondary_dependencies( manager )
+ }.to raise_exception( Arborist::ConfigError, /ancestor/i )
+ end
+
+
+ it "can't be declared for any of its decendants" do
+ provider_node_parent.depends_on( 'san-iscsi' )
+
+ expect {
+ provider_node_parent.register_secondary_dependencies( manager )
+ }.to raise_exception( Arborist::ConfigError, /descendant/i )
+ end
+
+
+ it "can be declared with a simple identifier" do
+ node.depends_on( 'san-iscsi' )
+
+ expect {
+ node.register_secondary_dependencies( manager )
+ }.to_not raise_exception
+ end
+
+
+ it "can be declared on a service on a host" do
+ node.depends_on( 'iscsi', on: 'san' )
+ expect( node ).to have_dependencies
+ expect( node.dependencies.behavior ).to eq( :all )
+ expect( node.dependencies.identifiers ).to include( 'san-iscsi' )
+ end
+
+
+ it "can be declared for unrelated identifiers"
+ it "can be declared for related identifiers"
+
+
+ it "can be declared for all of a group of identifiers"
+ it "can be declared for any of a group of identifiers"
+
+
+ it "cause the node to be quieted when the dependent node goes down" do
+ node.depends_on( provider_node.identifier )
+ node.register_secondary_dependencies( manager )
+
+ events = provider_node.update( error: "fatal disk error: offlined" )
+ provider_node.publish_events( *events )
+
+ expect( node ).to be_quieted
+ expect( node ).to have_downed_dependencies
+ # :TODO: Quieted description?
+ end
+
+ end
+
+
+ describe "operational attribute modification" do
+
+
+ let( :node ) do
+ concrete_class.new( 'foo' ) do
+ parent 'bar'
+ description "The prototypical node"
+ tags :chunker, :hunky, :flippin, :hippo
+ end
+ end
+
+
+ it "can change its parent" do
+ node.modify( parent: 'foo' )
+ expect( node.parent ).to eq( 'foo' )
+ end
+
+
+ it "can change its description" do
+ node.modify( description: 'A different node' )
+ expect( node.description ).to eq( 'A different node' )
+ 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
end