summaryrefslogtreecommitdiff
path: root/chef-expander/spec/unit
diff options
context:
space:
mode:
Diffstat (limited to 'chef-expander/spec/unit')
-rw-r--r--chef-expander/spec/unit/configuration_spec.rb100
-rw-r--r--chef-expander/spec/unit/control_spec.rb27
-rw-r--r--chef-expander/spec/unit/node_spec.rb185
-rw-r--r--chef-expander/spec/unit/solrizer_spec.rb260
-rw-r--r--chef-expander/spec/unit/vnode_spec.rb79
-rw-r--r--chef-expander/spec/unit/vnode_supervisor_spec.rb152
-rw-r--r--chef-expander/spec/unit/vnode_table_spec.rb114
7 files changed, 917 insertions, 0 deletions
diff --git a/chef-expander/spec/unit/configuration_spec.rb b/chef-expander/spec/unit/configuration_spec.rb
new file mode 100644
index 0000000000..2dddf686aa
--- /dev/null
+++ b/chef-expander/spec/unit/configuration_spec.rb
@@ -0,0 +1,100 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+require 'stringio'
+require 'chef/expander/configuration'
+
+describe Expander::Configuration do
+ before do
+ @config = Expander::Configuration::Base.new
+ @config.reset!
+ @config.apply_defaults
+ @config.index = 1
+ @config.node_count = 5
+ end
+
+ it "stores the number of nodes" do
+ @config.node_count.should == 5
+ end
+
+ it "stores the position in the node ring" do
+ @config.index.should == 1
+ end
+
+ it "computes the vnodes the node should claim" do
+ @config.vnode_numbers.should == (0..203).to_a
+ end
+
+ it "assigns any remainder to the last node in the ring" do
+ @config.index = 5
+ @config.vnode_numbers.should == (816..1023).to_a
+ end
+
+ it "raises an invalid config error when then node index is not set" do
+ @config.index = nil
+ lambda { @config.validate! }.should raise_error(Expander::Configuration::InvalidConfiguration)
+ end
+
+ it "raises an invalid config error when the node count is not set" do
+ @config.node_count = nil
+ lambda { @config.validate! }.should raise_error(Expander::Configuration::InvalidConfiguration)
+ end
+
+ it "raises an invalid config error when the index is greater than the node count" do
+ @config.node_count = 5
+ @config.index = 10
+ lambda { @config.validate! }.should raise_error(Expander::Configuration::InvalidConfiguration)
+ end
+
+ it "exits when the config is invalid" do
+ stdout = StringIO.new
+ @config.reset!(stdout)
+ @config.node_count = nil
+ lambda {@config.fail_if_invalid}.should raise_error(SystemExit)
+ stdout.string.should match(/You must specify this node's position in the ring as an integer/)
+ end
+
+ it "has a setting for solr url defaulting to localhost:8983" do
+ @config.solr_url.should == "http://localhost:8983"
+ end
+
+ it "has a setting for the amqp host to connect to, defaulting to 0.0.0.0" do
+ @config.amqp_host.should == '0.0.0.0'
+ end
+
+ it "has a setting for the amqp port to use, defaulting to 5672" do
+ @config.amqp_port.should == '5672'
+ end
+
+ it "has a setting for the amqp_user, defaulting to 'chef'" do
+ @config.amqp_user.should == 'chef'
+ end
+
+ it "has a setting for the amqp password, defaulting to 'testing'" do
+ @config.amqp_pass.should == 'testing'
+ end
+
+ it "has a setting for the amqp vhost, defaulting to /chef" do
+ @config.amqp_vhost.should == '/chef'
+ end
+
+ it "generates an AMQP configuration hash suitable for passing to Bunny.new or AMQP.start" do
+ @config.amqp_config.should == {:host => '0.0.0.0', :port => '5672', :user => 'chef', :pass => 'testing', :vhost => '/chef'}
+ end
+
+ it "merges another config on top of itself" do
+ other = Expander::Configuration::Base.new
+ other.solr_url = "somewhere with non-pitiful disk io"
+ @config.merge_config(other)
+ @config.solr_url.should == "somewhere with non-pitiful disk io" #if only it was that easy
+ end
+
+ it "merges config settings so that defaults < config_file < command line " do
+ config_file = File.dirname(__FILE__) + '/../fixtures/chef-expander.rb'
+ argv = ["-c", config_file, '-n', '23']
+ Expander.config.reset!
+ Expander.init_config(argv)
+ Expander.config.amqp_pass.should == 'config-file'
+ Expander.config.node_count.should == 23
+ end
+
+end \ No newline at end of file
diff --git a/chef-expander/spec/unit/control_spec.rb b/chef-expander/spec/unit/control_spec.rb
new file mode 100644
index 0000000000..7c456ec5e8
--- /dev/null
+++ b/chef-expander/spec/unit/control_spec.rb
@@ -0,0 +1,27 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Author:: Chris Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 2010-2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+require 'chef/expander/control'
+
+describe Expander::Control do
+
+end
diff --git a/chef-expander/spec/unit/node_spec.rb b/chef-expander/spec/unit/node_spec.rb
new file mode 100644
index 0000000000..2d9c1b207d
--- /dev/null
+++ b/chef-expander/spec/unit/node_spec.rb
@@ -0,0 +1,185 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Author:: Chris Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 2010-2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+require 'ostruct'
+require 'chef/expander/node'
+
+describe Expander::Node do
+
+ it "can be created from a hash" do
+ node_info = { :foo => :blargh,
+ :guid => "93226974-6d0b-4ca6-8d42-124dd55e0076",
+ :hostname_f => "fermi.local", :pid => 12345}
+ unmodified_hash = node_info.dup
+ node_from_hash = Expander::Node.from_hash(node_info)
+ node_info.should == unmodified_hash
+ node_from_hash.guid.should == "93226974-6d0b-4ca6-8d42-124dd55e0076"
+ node_from_hash.hostname_f.should == "fermi.local"
+ node_from_hash.pid.should == 12345
+ end
+
+ describe "when first created" do
+ before do
+ @guid = "93226974-6d0b-4ca6-8d42-124dd55e0076"
+ @hostname_f = "fermi.local"
+ @pid = 12345
+ @node = Expander::Node.new(@guid, @hostname_f, @pid)
+ end
+
+ it "has the guid it was created with" do
+ @node.guid.should == @guid
+ end
+
+ it "has the hostname it was created with" do
+ @node.hostname_f.should == @hostname_f
+ end
+
+ it "has the pid it was created with" do
+ @node.pid.should == @pid
+ end
+
+ it "names its shared control queue using a constant/consistent name" do
+ @node.shared_control_queue_name.should == "chef-search-control--shared"
+ end
+
+ it "names its exclusive control queue after its hostname, pid, and guid" do
+ @node.exclusive_control_queue_name.should == "fermi.local--12345--93226974-6d0b-4ca6-8d42-124dd55e0076--exclusive-control"
+ end
+
+ it "names its broadcast control queue after its hostname, pid, and guid" do
+ @node.broadcast_control_queue_name.should == "fermi.local--12345--93226974-6d0b-4ca6-8d42-124dd55e0076--broadcast"
+ end
+
+ it "names the broadcast control exchange using a consistent name" do
+ @node.broadcast_control_exchange_name.should == 'chef-search-control--broadcast'
+ end
+
+ it "generates its hash from a string concatenting the hostname, pid and guid" do
+ concat_string = "fermi.local--12345--93226974-6d0b-4ca6-8d42-124dd55e0076"
+ @node.hash.should == concat_string.hash
+ end
+
+ it "is eql to another Node if it has the same guid, hostname, and pid" do
+ other = Expander::Node.new(@guid.dup, @hostname_f.dup, @pid)
+ @node.should eql(other)
+ end
+
+ it "is == to another object if it has the same guid, hostname, and pid" do
+ other = Class.new(Expander::Node).new(@guid.dup, @hostname_f.dup, @pid)
+ other.should == @node
+ end
+
+ it "converts to a hash" do
+ @node.to_hash.should == {:guid => @guid, :hostname_f => @hostname_f, :pid => @pid}
+ end
+
+ end
+
+ describe "when describing the node it's running on" do
+ before do
+ hostname_f = OpenStruct.new(:stdout => "fermi.local\n")
+ Expander::Node.stub!(:shell_out!).and_return(hostname_f)
+ @node = Expander::Node.local_node
+ end
+
+ it "uses the current machine's hostname -f for the hostname" do
+ @node.hostname_f.should == %x(hostname -f).strip
+ end
+
+ it "uses the current process id for the pid" do
+ @node.pid.should == Process.pid
+ end
+
+ it "generates a guid for the guid" do
+ @node.guid.should match /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
+ end
+ end
+
+ describe "when sending and receiving messages" do
+
+ before do
+ @guid = "93226974-6d0b-4ca6-8d42-124dd55e0076"
+ @hostname_f = "fermi.local"
+ @pid = rand(10000)
+ @node = Expander::Node.new(@guid, @hostname_f, @pid)
+ @log_stream = StringIO.new
+ @node.log.init(@log_stream)
+ end
+
+ after do
+ b = Bunny.new(OPSCODE_EXPANDER_MQ_CONFIG)
+ b.start
+ b.exchange(@node.broadcast_control_exchange_name, :type => :fanout).delete
+ b.stop
+ end
+
+ it "receives messages on the broadcast exchange" do
+ messages = []
+
+ AMQP.start(OPSCODE_EXPANDER_MQ_CONFIG) do
+ @node.start do |message|
+ messages << message
+ end
+
+ @node.broadcast_message("hello everybody")
+
+ EM.add_timer(0.1) {AMQP.hard_reset!}
+ end
+
+ messages.should == ["hello everybody"]
+ end
+
+ it "receives messages on its exclusive queue" do
+ messages = []
+
+ AMQP.start(OPSCODE_EXPANDER_MQ_CONFIG) do
+ @node.start do |message|
+ messages << message
+ end
+
+ @node.direct_message("hello node")
+
+ EM.add_timer(0.1) {AMQP.hard_reset!}
+ end
+
+ messages.should == ["hello node"]
+ end
+
+ it "receives messages on the shared queue" do
+ messages = []
+
+ AMQP.start(OPSCODE_EXPANDER_MQ_CONFIG) do
+ @node.start do |message|
+ messages << message
+ end
+
+ @node.shared_message("hello one of N")
+
+ EM.add_timer(0.1) {AMQP.hard_reset!}
+ end
+
+ messages.should == ["hello one of N"]
+ end
+
+ end
+
+end
diff --git a/chef-expander/spec/unit/solrizer_spec.rb b/chef-expander/spec/unit/solrizer_spec.rb
new file mode 100644
index 0000000000..443e93a3fa
--- /dev/null
+++ b/chef-expander/spec/unit/solrizer_spec.rb
@@ -0,0 +1,260 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Author:: Chris Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 2010-2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+require 'stringio'
+require 'chef/expander/solrizer'
+require 'yajl'
+require 'rexml/document'
+
+describe Expander::Solrizer do
+ SEP = "__=__"
+
+ describe "when created with an add request" do
+ before do
+ @now = Time.now.utc.to_i
+ @indexer_payload = {:item => {:foo => {:bar => :baz}},
+ :type => :node,
+ :database => :testdb,
+ :id => "2342",
+ :enqueued_at => @now}
+
+ @update_object = {:action => "add", :payload => @indexer_payload}
+ @update_json = Yajl::Encoder.encode(@update_object)
+ @solrizer = Expander::Solrizer.new(@update_json) { :no_op }
+
+ @log_stream = StringIO.new
+ @solrizer.log.init(@log_stream)
+ @expected_fields = %w(X_CHEF_id_CHEF_X X_CHEF_database_CHEF_X X_CHEF_type_CHEF_X)
+ end
+
+ it "extracts the indexing-specific payload from the update message" do
+ @solrizer.indexer_payload.should == { 'item' => {'foo' => {'bar' => "baz"}},
+ 'type' => 'node',
+ 'database' => 'testdb', 'id' => "2342",
+ "enqueued_at"=>@now}
+ end
+
+ it "extracts the action from the update message" do
+ @solrizer.action.should == "add"
+ end
+
+ it "extracts the item to update from the update message" do
+ @solrizer.chef_object.should == {"foo" => {"bar" => "baz"}}
+ end
+
+ it "extracts the database name from the update message" do
+ @solrizer.database.should == "testdb"
+ end
+
+ it "extracts the object id from the update message" do
+ @solrizer.obj_id.should == "2342"
+ end
+
+ it "extracts the object type from the update message" do
+ @solrizer.obj_type.should == "node"
+ end
+
+ it "extracts the time the object was enqueued from the message" do
+ @solrizer.enqueued_at.should == @now
+ end
+
+ it "is eql to another Solrizer object that has the same object type, id, database, action, and enqueued_at time" do
+ eql_solrizer = Expander::Solrizer.new(@update_json)
+ @solrizer.should eql eql_solrizer
+ end
+
+ it "is not eql to another Solrizer if the enqueued_at time is different" do
+ update_hash = @update_object.dup
+ update_hash[:payload] = update_hash[:payload].merge({:enqueued_at => (Time.now.utc.to_i + 10000)})
+ update_json = Yajl::Encoder.encode(update_hash)
+ uneql_solrizer = Expander::Solrizer.new(update_json)
+ @solrizer.should_not eql(uneql_solrizer)
+ end
+
+ it "is not eql to another Solrizer if the object id is different" do
+ update_hash = @update_object.dup
+ update_hash[:payload] = update_hash[:payload].merge({:id => 12345})
+ update_json = Yajl::Encoder.encode(update_hash)
+ uneql_solrizer = Expander::Solrizer.new(update_json)
+ @solrizer.should_not eql(uneql_solrizer)
+ end
+
+ it "is not eql to another Solrizer if the database is different" do
+ update_hash = @update_object.dup
+ update_hash[:payload] = update_hash[:payload].merge({:database => "nononono"})
+ update_json = Yajl::Encoder.encode(update_hash)
+ uneql_solrizer = Expander::Solrizer.new(update_json)
+ @solrizer.should_not eql(uneql_solrizer)
+ end
+
+ it "is not eql to another Solrizer if the action is different" do
+ update_hash = @update_object.dup
+ update_hash[:action] = :delete
+ update_json = Yajl::Encoder.encode(update_hash)
+ uneql_solrizer = Expander::Solrizer.new(update_json)
+ @solrizer.should_not eql(uneql_solrizer)
+ end
+
+ describe "when flattening to XML" do
+ before do
+ @expected_object = {"foo" => ["bar"],
+ "foo_bar" => ["baz"],
+ "bar" => ["baz"],
+ "X_CHEF_id_CHEF_X" => ["2342"],
+ "X_CHEF_database_CHEF_X" => ["testdb"],
+ "X_CHEF_type_CHEF_X" => ["node"]}
+ @expected_fields = %w(X_CHEF_id_CHEF_X X_CHEF_database_CHEF_X X_CHEF_type_CHEF_X)
+ end
+
+ it "generates the flattened and expanded representation of the object" do
+ @solrizer.flattened_object.should == @expected_object
+ end
+
+ it "has the expected fields in the document" do
+ doc = REXML::Document.new(@solrizer.pointyize_add)
+ flds = doc.elements.to_a("add/doc/field").map {|f| f.attributes["name"] }
+ @expected_fields.each do |field|
+ flds.should include(field)
+ end
+ end
+
+ it "the content field contains key value pairs delimited with the right separator" do
+ doc = REXML::Document.new(@solrizer.pointyize_add)
+ doc.elements.each("add/doc/field[@name='content']") do |content|
+ raw = content.text
+ @expected_object.each do |k, v|
+ s = "#{k}#{SEP}#{v.first}"
+ raw.index(s).should_not be_nil
+ end
+ end
+ end
+ end
+
+ describe "when flattening data to XML that needs XML escaping" do
+ before do
+ @indexer_payload[:type] = :role
+ @indexer_payload[:item] = { "a&w" => "<rootbeer/>" }
+ update_object = {:action => "add", :payload => @indexer_payload}
+ update_json = Yajl::Encoder.encode(update_object)
+ @solrizer = Expander::Solrizer.new(update_json) { :no_op }
+ @solrizer.log.init(@log_stream)
+ end
+
+ it "the content field contains escaped keys and values" do
+ raw = @solrizer.pointyize_add
+ raw.should match("a&amp;w#{SEP}&lt;rootbeer/&gt;")
+ end
+ end
+
+ describe "when flattening data bag XML" do
+ before do
+ @indexer_payload[:type] = :data_bag_item
+ @indexer_payload[:item] = {:k1 => "v1", "data_bag" => "stuff"}
+ update_object = {:action => "add", :payload => @indexer_payload}
+ update_json = Yajl::Encoder.encode(update_object)
+ @solrizer = Expander::Solrizer.new(update_json) { :no_op }
+ @solrizer.log.init(@log_stream)
+ @expected_fields << "data_bag"
+ end
+
+ it "contains a data_bag field with the right name" do
+ doc = REXML::Document.new(@solrizer.pointyize_add)
+ flds = doc.elements.to_a("add/doc/field[@name='data_bag']")
+ flds.size.should == 1
+ flds.first.text.should == "stuff"
+ end
+
+ it "has the expected fields in the document" do
+ doc = REXML::Document.new(@solrizer.pointyize_add)
+ flds = doc.elements.to_a("add/doc/field").map {|f| f.attributes["name"] }
+ @expected_fields.each do |field|
+ flds.should include(field)
+ end
+ end
+ describe "and data bag name needs escaping" do
+ before do
+ @indexer_payload[:item] = {:k1 => "v1", "data_bag" => "a&w>"}
+ update_object = {:action => "add", :payload => @indexer_payload}
+ update_json = Yajl::Encoder.encode(update_object)
+ @solrizer = Expander::Solrizer.new(update_json) { :no_op }
+ @solrizer.log.init(@log_stream)
+ end
+
+ it "contains a data_bag field with an escaped name" do
+ raw = @solrizer.pointyize_add
+ raw.should match("data_bag#{SEP}a&amp;w&gt;")
+ end
+ end
+ end
+
+ describe "when no HTTP request is in progress" do
+
+ it "does not report that an HTTP request is in progress" do
+ Expander::Solrizer.http_requests_active?.should be_false
+ end
+
+ end
+
+ describe "when an HTTP request is in progress" do
+ before do
+ Expander::Solrizer.clear_http_requests
+ @solrizer.http_request_started
+ end
+
+ it "registers the in-progress HTTP request" do
+ Expander::Solrizer.http_requests_active?.should be_true
+ end
+
+ it "removes itself from the list of active http requests when the request completes" do
+ @solrizer.completed
+ Expander::Solrizer.http_requests_active?.should be_false
+ end
+
+ end
+
+
+ end
+
+ describe "when created with a delete request" do
+ before do
+ @indexer_payload = {:id => "2342"}
+ @update_object = {:action => "add", :payload => @indexer_payload}
+ @update_json = Yajl::Encoder.encode(@update_object)
+ @solrizer = Expander::Solrizer.new(@update_json)
+ end
+
+ it "extracts the indexer payload" do
+ @solrizer.indexer_payload.should == {"id" => "2342"}
+ end
+
+ it "extracts the object id" do
+ @solrizer.obj_id.should == "2342"
+ end
+
+ it "converts the delete request to XML" do
+ expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<delete><id>2342</id></delete>\n"
+ @solrizer.pointyize_delete.should == expected
+ end
+
+ end
+
+end
diff --git a/chef-expander/spec/unit/vnode_spec.rb b/chef-expander/spec/unit/vnode_spec.rb
new file mode 100644
index 0000000000..9321c03ef3
--- /dev/null
+++ b/chef-expander/spec/unit/vnode_spec.rb
@@ -0,0 +1,79 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+require 'chef/expander/vnode_supervisor'
+require 'chef/expander/vnode'
+
+describe Expander::VNode do
+ before do
+ @supervisor = Expander::VNodeSupervisor.new
+ @vnode = Expander::VNode.new("2342", @supervisor, :supervise_interval => 0.1)
+ @log_stream = StringIO.new
+ @vnode.log.init(@log_stream)
+ end
+
+ it "has the vnode number it was created with" do
+ @vnode.vnode_number.should == 2342
+ end
+
+ it "has a queue named after its vnode number" do
+ @vnode.queue_name.should == "vnode-2342"
+ end
+
+ it "has a control queue name" do
+ @vnode.control_queue_name.should == "vnode-2342-control"
+ end
+
+ describe "when connecting to rabbitmq" do
+ it "disconnects if there is another subscriber" do
+ begin
+ q = nil
+ b = Bunny.new(OPSCODE_EXPANDER_MQ_CONFIG)
+ b.start
+ q = b.queue(@vnode.queue_name, :passive => false, :durable => true, :exclusive => false, :auto_delete => false)
+ t = Thread.new { q.subscribe { |message| nil }}
+
+ AMQP.start(OPSCODE_EXPANDER_MQ_CONFIG) do
+ EM.add_timer(0.5) do
+ AMQP.stop
+ EM.stop
+ end
+ @vnode.start
+ end
+ t.kill
+
+ @vnode.should be_stopped
+ @log_stream.string.should match(/Detected extra consumers/)
+ ensure
+ q && q.delete
+ b.stop
+ end
+ end
+
+ it "calls back to the supervisor when it subscribes to the queue" do
+ AMQP.start(OPSCODE_EXPANDER_MQ_CONFIG) do
+ MQ.topic('foo')
+ EM.add_timer(0.1) do
+ AMQP.stop
+ EM.stop
+ end
+ @vnode.start
+ end
+ @supervisor.vnodes.should == [2342]
+ end
+
+ it "calls back to the supervisor when it stops subscribing" do
+ @supervisor.vnode_added(@vnode)
+ AMQP.start(OPSCODE_EXPANDER_MQ_CONFIG) do
+ MQ.topic('foo')
+ EM.add_timer(0.1) do
+ @vnode.stop
+ AMQP.stop
+ EM.stop
+ end
+ end
+ @supervisor.vnodes.should be_empty
+ end
+
+ end
+
+end
diff --git a/chef-expander/spec/unit/vnode_supervisor_spec.rb b/chef-expander/spec/unit/vnode_supervisor_spec.rb
new file mode 100644
index 0000000000..59622675e8
--- /dev/null
+++ b/chef-expander/spec/unit/vnode_supervisor_spec.rb
@@ -0,0 +1,152 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Author:: Chris Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 2010-2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+require 'chef/expander/vnode_supervisor'
+
+describe Expander::VNodeSupervisor do
+ before do
+ @log_stream = StringIO.new
+ @local_node = Expander::Node.new("1101d02d-1547-45ab-b2f6-f0153d0abb34", "fermi.local", 12342)
+ @vnode_supervisor = Expander::VNodeSupervisor.new
+ @vnode_supervisor.instance_variable_set(:@local_node, @local_node)
+ @vnode_supervisor.log.init(@log_stream)
+ @vnode = Expander::VNode.new("42", @vnode_supervisor)
+ end
+
+ after do
+ b = Bunny.new(OPSCODE_EXPANDER_MQ_CONFIG)
+ b.start
+ b.exchange(@vnode_supervisor.local_node.broadcast_control_exchange_name, :type => :fanout).delete
+ b.queue(@vnode_supervisor.local_node.broadcast_control_queue_name).purge
+ b.stop
+ end
+
+ it "keeps a list of vnodes" do
+ @vnode_supervisor.vnodes.should be_empty
+ @vnode_supervisor.vnode_added(@vnode)
+ @vnode_supervisor.vnodes.should == [42]
+ end
+
+ it "has a callback for vnode removal" do
+ @vnode_supervisor.vnode_added(@vnode)
+ @vnode_supervisor.vnodes.should == [42]
+ @vnode_supervisor.vnode_removed(@vnode)
+ @vnode_supervisor.vnodes.should be_empty
+ end
+
+ it "spawns a vnode" do
+ AMQP.start(OPSCODE_EXPANDER_MQ_CONFIG) do
+ @vnode_supervisor.spawn_vnode(42)
+ MQ.topic('foo')
+ EM.add_timer(0.1) do
+ AMQP.hard_reset!
+ end
+ end
+ @vnode_supervisor.vnodes.should == [42]
+ end
+
+ it "subscribes to the control queue" do
+ AMQP.start(OPSCODE_EXPANDER_MQ_CONFIG) do
+ @vnode_supervisor.start([])
+ @vnode_supervisor.should_receive(:process_control_message).with("hello_robot_overlord")
+ Expander::Node.local_node.broadcast_message("hello_robot_overlord")
+ EM.add_timer(0.1) do
+ AMQP.hard_reset!
+ end
+ end
+ end
+
+ it "periodically publishes its list of vnodes to the gossip queue" do
+ pending("disabled until cluster healing is implemented")
+ end
+
+ describe "when responding to control messages" do
+ it "passes vnode table updates to its vnode table" do
+ vnode_table_update = Expander::Node.local_node.to_hash
+ vnode_table_update[:vnodes] = (0...16).to_a
+ vnode_table_update[:update] = :add
+ update_message = Yajl::Encoder.encode({:action => :update_vnode_table, :data => vnode_table_update})
+ @vnode_supervisor.process_control_message(update_message)
+ @vnode_supervisor.vnode_table.vnodes_by_node[Expander::Node.local_node].should == (0...16).to_a
+ end
+
+ it "publishes the vnode table when it receives a :vnode_table_publish message" do
+ pending "disabled until cluster healing is implemented"
+ update_message = Yajl::Encoder.encode({:action => :vnode_table_publish})
+ @vnode_supervisor.process_control_message(update_message)
+ end
+
+ describe "and it is the leader" do
+ before do
+ vnode_table_update = Expander::Node.local_node.to_hash
+ vnode_table_update[:vnodes] = (0...16).to_a
+ vnode_table_update[:update] = :add
+ update_message = Yajl::Encoder.encode({:action => :update_vnode_table, :data => vnode_table_update})
+ @vnode_supervisor.process_control_message(update_message)
+ end
+
+ it "distributes the vnode when it receives a recover_vnode message and it is the leader" do
+ control_msg = {:action => :recover_vnode, :vnode_id => 23}
+
+ @vnode_supervisor.local_node.should_receive(:shared_message)
+ @vnode_supervisor.process_control_message(Yajl::Encoder.encode(control_msg))
+ end
+
+ it "waits before re-advertising a vnode as available" do
+ pending("not yet implemented")
+ vnode_table_update = Expander::Node.local_node.to_hash
+ vnode_table_update[:vnodes] = (0...16).to_a
+ vnode_table_update[:update] = :add
+ update_message = Yajl::Encoder.encode({:action => :update_vnode_table, :data => vnode_table_update})
+ @vnode_supervisor.process_control_message(update_message)
+
+ control_msg = {:action => :recover_vnode, :vnode_id => 23}
+
+ @vnode_supervisor.local_node.should_receive(:shared_message).once
+ @vnode_supervisor.process_control_message(Yajl::Encoder.encode(control_msg))
+ @vnode_supervisor.process_control_message(Yajl::Encoder.encode(control_msg))
+ end
+ end
+
+
+ it "doesn't distribute a vnode when it is not the leader" do
+ vnode_table_update = Expander::Node.local_node.to_hash
+ vnode_table_update[:vnodes] = (16...32).to_a
+ vnode_table_update[:update] = :add
+ update_message = Yajl::Encoder.encode({:action => :update_vnode_table, :data => vnode_table_update})
+ @vnode_supervisor.process_control_message(update_message)
+
+ vnode_table_update = Expander::Node.new("1c53daf0-34a1-4e4f-8069-332665453b44", 'fermi.local', 2342).to_hash
+ vnode_table_update[:vnodes] = (0...16).to_a
+ vnode_table_update[:update] = :add
+ update_message = Yajl::Encoder.encode({:action => :update_vnode_table, :data => vnode_table_update})
+ @vnode_supervisor.process_control_message(update_message)
+
+ control_msg = {:action => :recover_vnode, :vnode_id => 42}
+
+ @vnode_supervisor.local_node.should_not_receive(:shared_message)
+ @vnode_supervisor.process_control_message(Yajl::Encoder.encode(control_msg))
+ end
+
+ end
+
+end
diff --git a/chef-expander/spec/unit/vnode_table_spec.rb b/chef-expander/spec/unit/vnode_table_spec.rb
new file mode 100644
index 0000000000..faa76b84c1
--- /dev/null
+++ b/chef-expander/spec/unit/vnode_table_spec.rb
@@ -0,0 +1,114 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Author:: Chris Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 2010-2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+require 'chef/expander/vnode_table'
+require 'chef/expander/vnode_supervisor'
+
+describe Expander::VNodeTable do
+ before do
+ @vnode_supervisor = Expander::VNodeSupervisor.new
+ @vnode_table = Expander::VNodeTable.new(@vnode_supervisor)
+
+ @log_stream = StringIO.new
+ @vnode_table.log.init(@log_stream)
+ end
+
+ describe "when first created" do
+ it "has no nodes" do
+ @vnode_table.nodes.should be_empty
+ end
+ end
+
+ describe "when one node's vnode info has been added" do
+ before do
+ @guid = "93226974-6d0b-4ca6-8d42-124dd55e0076"
+ @hostname_f = "fermi.localhost"
+ @pid = 12345
+ @vnodes = (0..511).to_a
+ @update = {:guid => @guid, :hostname_f => @hostname_f, :pid => @pid, :vnodes => @vnodes, :update => 'update'}
+ @vnode_table.update_table(@update)
+ end
+
+ it "has one vnode" do
+ @vnode_table.should have(1).nodes
+ @vnode_table.nodes.first.should == Expander::Node.from_hash(@update)
+ end
+
+ it "removes the node from the table when it exits the cluster" do
+ update = @update
+ update[:update] = 'remove'
+ @vnode_table.update_table(update)
+ @vnode_table.should have(0).nodes
+ end
+
+ end
+
+ describe "when several nodes are in the table" do
+ before do
+ @node_1 = Expander::Node.new("93226974-6d0b-4ca6-8d42-124dd55e0076", "fermi.local", 12345)
+ @node_1_hash = @node_1.to_hash
+ @node_1_hash[:vnodes] = (0..511).to_a
+ @node_1_hash[:update] = "update"
+ @node_2 = Expander::Node.new("ad265988-f650-4a31-a97b-5dbf4db8e1b0", "fermi.local", 23425)
+ @node_2_hash = @node_2.to_hash
+ @node_2_hash[:vnodes] = (512..767).to_a
+ @node_2_hash[:update] = "update"
+ @vnode_table.update_table(@node_1_hash)
+ @vnode_table.update_table(@node_2_hash)
+ end
+
+ it "determines the node with the lowest numbered vnode is the leader node" do
+ @vnode_table.leader_node.should == @node_1
+ end
+
+ it "determines the local node is the leader when the local node matches the leader node" do
+ Expander::Node.stub!(:local_node).and_return(@node_1)
+ @vnode_table.local_node_is_leader?.should be_true
+ end
+
+ it "determines the local node is not the leader when the local node doesn't match the leader node" do
+ Expander::Node.stub!(:local_node).and_return(@node_2)
+ @vnode_table.local_node_is_leader?.should be_false
+ end
+ end
+
+ describe "when only one node has claimed any vnodes" do
+ before do
+ @node_1 = Expander::Node.new("93226974-6d0b-4ca6-8d42-124dd55e0076", "fermi.local", 12345)
+ @node_1_hash = @node_1.to_hash
+ @node_1_hash[:vnodes] = (0..511).to_a
+ @node_1_hash[:update] = "update"
+ @node_2 = Expander::Node.new("ad265988-f650-4a31-a97b-5dbf4db8e1b0", "fermi.local", 23425)
+ @node_2_hash = @node_2.to_hash
+ @node_2_hash[:vnodes] = []
+ @node_2_hash[:update] = "update"
+ @vnode_table.update_table(@node_1_hash)
+ @vnode_table.update_table(@node_2_hash)
+ end
+
+ it "still reliably determines who the leader is" do
+ pending
+ end
+
+ end
+
+end