diff options
Diffstat (limited to 'chef-expander/spec/unit')
-rw-r--r-- | chef-expander/spec/unit/configuration_spec.rb | 100 | ||||
-rw-r--r-- | chef-expander/spec/unit/control_spec.rb | 27 | ||||
-rw-r--r-- | chef-expander/spec/unit/node_spec.rb | 185 | ||||
-rw-r--r-- | chef-expander/spec/unit/solrizer_spec.rb | 260 | ||||
-rw-r--r-- | chef-expander/spec/unit/vnode_spec.rb | 79 | ||||
-rw-r--r-- | chef-expander/spec/unit/vnode_supervisor_spec.rb | 152 | ||||
-rw-r--r-- | chef-expander/spec/unit/vnode_table_spec.rb | 114 |
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&w#{SEP}<rootbeer/>") + 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&w>") + 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 |