File Manager
Viewing File: attribute_spec.rb
#
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: AJ Christensen (<aj@chef.io>)
# Copyright:: Copyright 2008-2016, Chef Software 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 "spec_helper"
require "chef/node/attribute"
describe Chef::Node::Attribute do
let(:events) { instance_double(Chef::EventDispatch::Dispatcher) }
let(:run_context) { instance_double(Chef::RunContext, :events => events) }
let(:node) { instance_double(Chef::Node, :run_context => run_context) }
before(:each) do
allow(events).to receive(:attribute_changed)
@attribute_hash =
{ "dmi" => {},
"command" => { "ps" => "ps -ef" },
"platform_version" => "10.5.7",
"platform" => "mac_os_x",
"ipaddress" => "192.168.0.117",
"network" => { "default_interface" => "en1",
"interfaces" => { "vmnet1" => { "flags" => %w{UP BROADCAST SMART RUNNING SIMPLEX MULTICAST},
"number" => "1",
"addresses" => { "00:50:56:c0:00:01" => { "family" => "lladdr" },
"192.168.110.1" => { "broadcast" => "192.168.110.255",
"netmask" => "255.255.255.0",
"family" => "inet" } },
"mtu" => "1500",
"type" => "vmnet",
"arp" => { "192.168.110.255" => "ff:ff:ff:ff:ff:ff" },
"encapsulation" => "Ethernet" },
"stf0" => { "flags" => [],
"number" => "0",
"addresses" => {},
"mtu" => "1280",
"type" => "stf",
"encapsulation" => "6to4" },
"lo0" => { "flags" => %w{UP LOOPBACK RUNNING MULTICAST},
"number" => "0",
"addresses" => { "::1" => { "scope" => "Node", "prefixlen" => "128", "family" => "inet6" },
"127.0.0.1" => { "netmask" => "255.0.0.0", "family" => "inet" },
"fe80::1" => { "scope" => "Link", "prefixlen" => "64", "family" => "inet6" } },
"mtu" => "16384",
"type" => "lo",
"encapsulation" => "Loopback" },
"gif0" => { "flags" => %w{POINTOPOINT MULTICAST},
"number" => "0",
"addresses" => {},
"mtu" => "1280",
"type" => "gif",
"encapsulation" => "IPIP" },
"vmnet8" => { "flags" => %w{UP BROADCAST SMART RUNNING SIMPLEX MULTICAST},
"number" => "8",
"addresses" => { "192.168.4.1" => { "broadcast" => "192.168.4.255",
"netmask" => "255.255.255.0",
"family" => "inet" },
"00:50:56:c0:00:08" => { "family" => "lladdr" } },
"mtu" => "1500",
"type" => "vmnet",
"arp" => { "192.168.4.255" => "ff:ff:ff:ff:ff:ff" },
"encapsulation" => "Ethernet" },
"en0" => { "status" => "inactive",
"flags" => %w{UP BROADCAST SMART RUNNING SIMPLEX MULTICAST},
"number" => "0",
"addresses" => { "00:23:32:b0:32:f2" => { "family" => "lladdr" } },
"mtu" => "1500",
"media" => { "supported" => { "autoselect" => { "options" => [] },
"none" => { "options" => [] },
"1000baseT" => { "options" => ["full-duplex", "flow-control", "hw-loopback"] },
"10baseT/UTP" => { "options" => ["half-duplex", "full-duplex", "flow-control", "hw-loopback"] },
"100baseTX" => { "options" => ["half-duplex", "full-duplex", "flow-control", "hw-loopback"] } },
"selected" => { "autoselect" => { "options" => [] } } },
"type" => "en",
"encapsulation" => "Ethernet" },
"en1" => { "status" => "active",
"flags" => %w{UP BROADCAST SMART RUNNING SIMPLEX MULTICAST},
"number" => "1",
"addresses" => { "fe80::223:6cff:fe7f:676c" => { "scope" => "Link", "prefixlen" => "64", "family" => "inet6" },
"00:23:6c:7f:67:6c" => { "family" => "lladdr" },
"192.168.0.117" => { "broadcast" => "192.168.0.255",
"netmask" => "255.255.255.0",
"family" => "inet" } },
"mtu" => "1500",
"media" => { "supported" => { "autoselect" => { "options" => [] } },
"selected" => { "autoselect" => { "options" => [] } } },
"type" => "en",
"arp" => { "192.168.0.72" => "0:f:ea:39:fa:d5",
"192.168.0.1" => "0:1c:fb:fc:6f:20",
"192.168.0.255" => "ff:ff:ff:ff:ff:ff",
"192.168.0.3" => "0:1f:33:ea:26:9b",
"192.168.0.77" => "0:23:12:70:f8:cf",
"192.168.0.152" => "0:26:8:7d:2:4c" },
"encapsulation" => "Ethernet" },
"en2" => { "status" => "active",
"flags" => %w{UP BROADCAST SMART RUNNING SIMPLEX MULTICAST},
"number" => "2",
"addresses" => { "169.254.206.152" => { "broadcast" => "169.254.255.255",
"netmask" => "255.255.0.0",
"family" => "inet" },
"00:1c:42:00:00:01" => { "family" => "lladdr" },
"fe80::21c:42ff:fe00:1" => { "scope" => "Link", "prefixlen" => "64", "family" => "inet6" } },
"mtu" => "1500",
"media" => { "supported" => { "autoselect" => { "options" => [] } },
"selected" => { "autoselect" => { "options" => [] } } },
"type" => "en",
"encapsulation" => "Ethernet" },
"fw0" => { "status" => "inactive",
"flags" => %w{BROADCAST SIMPLEX MULTICAST},
"number" => "0",
"addresses" => { "00:23:32:ff:fe:b0:32:f2" => { "family" => "lladdr" } },
"mtu" => "4078",
"media" => { "supported" => { "autoselect" => { "options" => ["full-duplex"] } },
"selected" => { "autoselect" => { "options" => ["full-duplex"] } } },
"type" => "fw",
"encapsulation" => "1394" },
"en3" => { "status" => "active",
"flags" => %w{UP BROADCAST SMART RUNNING SIMPLEX MULTICAST},
"number" => "3",
"addresses" => { "169.254.206.152" => { "broadcast" => "169.254.255.255",
"netmask" => "255.255.0.0",
"family" => "inet" },
"00:1c:42:00:00:00" => { "family" => "lladdr" },
"fe80::21c:42ff:fe00:0" => { "scope" => "Link", "prefixlen" => "64", "family" => "inet6" } },
"mtu" => "1500",
"media" => { "supported" => { "autoselect" => { "options" => [] } },
"selected" => { "autoselect" => { "options" => [] } } },
"type" => "en",
"encapsulation" => "Ethernet" } } },
"fqdn" => "latte.local",
"ohai_time" => 1249065590.90391,
"domain" => "local",
"os" => "darwin",
"platform_build" => "9J61",
"os_version" => "9.7.0",
"hostname" => "latte",
"macaddress" => "00:23:6c:7f:67:6c",
"music" => { "jimmy_eat_world" => "nice", "apophis" => false },
}
@default_hash = {
"domain" => "opscode.com",
"hot" => { "day" => "saturday" },
"music" => {
"jimmy_eat_world" => "is fun!",
"mastodon" => "rocks",
"mars_volta" => "is loud and nutty",
"deeper" => { "gates_of_ishtar" => nil },
"this" => { "apparatus" => { "must" => "be unearthed" } },
},
}
@override_hash = {
"macaddress" => "00:00:00:00:00:00",
"hot" => { "day" => "sunday" },
"fire" => "still burn",
"music" => {
"mars_volta" => "cicatriz",
},
}
@automatic_hash = { "week" => "friday" }
@attributes = Chef::Node::Attribute.new(@attribute_hash, @default_hash, @override_hash, @automatic_hash, node)
end
describe "initialize" do
it "should return a Chef::Node::Attribute" do
expect(@attributes).to be_a_kind_of(Chef::Node::Attribute)
end
it "should take an Automatioc, Normal, Default and Override hash" do
expect { Chef::Node::Attribute.new({}, {}, {}, {}) }.not_to raise_error
end
[ :normal, :default, :override, :automatic ].each do |accessor|
it "should set #{accessor}" do
na = Chef::Node::Attribute.new({ :normal => true }, { :default => true }, { :override => true }, { :automatic => true })
expect(na.send(accessor)).to eq({ accessor.to_s => true })
end
end
it "should be enumerable" do
expect(@attributes).to be_is_a(Enumerable)
end
end
describe "when printing attribute components" do
it "does not cause a type error" do
# See CHEF-3799; IO#puts implicitly calls #to_ary on its argument. This
# is expected to raise a NoMethodError or return an Array. `to_ary` is
# the "strict" conversion method that should only be implemented by
# things that are truly Array-like, so NoMethodError is the right choice.
# (cf. there is no Hash#to_ary).
expect { @attributes.default.to_ary }.to raise_error(NoMethodError)
end
end
describe "when debugging attributes" do
before do
@attributes.default[:foo][:bar] = "default"
@attributes.env_default[:foo][:bar] = "env_default"
@attributes.role_default[:foo][:bar] = "role_default"
@attributes.force_default[:foo][:bar] = "force_default"
@attributes.normal[:foo][:bar] = "normal"
@attributes.override[:foo][:bar] = "override"
@attributes.role_override[:foo][:bar] = "role_override"
@attributes.env_override[:foo][:bar] = "env_override"
@attributes.force_override[:foo][:bar] = "force_override"
@attributes.automatic[:foo][:bar] = "automatic"
end
it "gives the value at each level of precedence for a path spec" do
expected = [
%w{default default},
%w{env_default env_default},
%w{role_default role_default},
%w{force_default force_default},
%w{normal normal},
%w{override override},
%w{role_override role_override},
%w{env_override env_override},
%w{force_override force_override},
%w{automatic automatic},
]
expect(@attributes.debug_value(:foo, :bar)).to eq(expected)
end
end
describe "when fetching values based on precedence" do
before do
@attributes.default["default"] = "cookbook default"
@attributes.override["override"] = "cookbook override"
end
it "prefers 'forced default' over any other default" do
@attributes.force_default["default"] = "force default"
@attributes.role_default["default"] = "role default"
@attributes.env_default["default"] = "environment default"
expect(@attributes["default"]).to eq("force default")
end
it "prefers role_default over environment or cookbook default" do
@attributes.role_default["default"] = "role default"
@attributes.env_default["default"] = "environment default"
expect(@attributes["default"]).to eq("role default")
end
it "prefers environment default over cookbook default" do
@attributes.env_default["default"] = "environment default"
expect(@attributes["default"]).to eq("environment default")
end
it "returns the cookbook default when no other default values are present" do
expect(@attributes["default"]).to eq("cookbook default")
end
it "prefers 'forced overrides' over role or cookbook overrides" do
@attributes.force_override["override"] = "force override"
@attributes.env_override["override"] = "environment override"
@attributes.role_override["override"] = "role override"
expect(@attributes["override"]).to eq("force override")
end
it "prefers environment overrides over role or cookbook overrides" do
@attributes.env_override["override"] = "environment override"
@attributes.role_override["override"] = "role override"
expect(@attributes["override"]).to eq("environment override")
end
it "prefers role overrides over cookbook overrides" do
@attributes.role_override["override"] = "role override"
expect(@attributes["override"]).to eq("role override")
end
it "returns cookbook overrides when no other overrides are present" do
expect(@attributes["override"]).to eq("cookbook override")
end
it "merges arrays within the default precedence" do
@attributes.role_default["array"] = %w{role}
@attributes.env_default["array"] = %w{env}
expect(@attributes["array"]).to eq(%w{env role})
end
it "merges arrays within the override precedence" do
@attributes.role_override["array"] = %w{role}
@attributes.env_override["array"] = %w{env}
expect(@attributes["array"]).to eq(%w{role env})
end
it "does not merge arrays between default and normal" do
@attributes.role_default["array"] = %w{role}
@attributes.normal["array"] = %w{normal}
expect(@attributes["array"]).to eq(%w{normal})
end
it "does not merge arrays between normal and override" do
@attributes.normal["array"] = %w{normal}
@attributes.role_override["array"] = %w{role}
expect(@attributes["array"]).to eq(%w{role})
end
it "merges nested hashes between precedence levels" do
@attributes = Chef::Node::Attribute.new({}, {}, {}, {})
@attributes.env_default = { "a" => { "b" => { "default" => "default" } } }
@attributes.normal = { "a" => { "b" => { "normal" => "normal" } } }
@attributes.override = { "a" => { "override" => "role" } }
@attributes.automatic = { "a" => { "automatic" => "auto" } }
expect(@attributes["a"]).to eq({ "b" => { "default" => "default", "normal" => "normal" },
"override" => "role",
"automatic" => "auto" })
end
end
describe "when reading combined default or override values" do
before do
@attributes.default["cd"] = "cookbook default"
@attributes.role_default["rd"] = "role default"
@attributes.env_default["ed"] = "env default"
@attributes.default!["fd"] = "force default"
@attributes.override["co"] = "cookbook override"
@attributes.role_override["ro"] = "role override"
@attributes.env_override["eo"] = "env override"
@attributes.override!["fo"] = "force override"
end
it "merges all types of overrides into a combined override" do
expect(@attributes.combined_override["co"]).to eq("cookbook override")
expect(@attributes.combined_override["ro"]).to eq("role override")
expect(@attributes.combined_override["eo"]).to eq("env override")
expect(@attributes.combined_override["fo"]).to eq("force override")
end
it "merges all types of defaults into a combined default" do
expect(@attributes.combined_default["cd"]).to eq("cookbook default")
expect(@attributes.combined_default["rd"]).to eq("role default")
expect(@attributes.combined_default["ed"]).to eq("env default")
expect(@attributes.combined_default["fd"]).to eq("force default")
end
end
describe "[]" do
it "should return override data if it exists" do
expect(@attributes["macaddress"]).to eq("00:00:00:00:00:00")
end
it "should return attribute data if it is not overridden" do
expect(@attributes["platform"]).to eq("mac_os_x")
end
it "should return data that doesn't have corresponding keys in every hash" do
expect(@attributes["command"]["ps"]).to eq("ps -ef")
end
it "should return default data if it is not overriden or in attribute data" do
expect(@attributes["music"]["mastodon"]).to eq("rocks")
end
it "should prefer the override data over an available default" do
expect(@attributes["music"]["mars_volta"]).to eq("cicatriz")
end
it "should prefer the attribute data over an available default" do
expect(@attributes["music"]["jimmy_eat_world"]).to eq("nice")
end
it "should prefer override data over default data if there is no attribute data" do
expect(@attributes["hot"]["day"]).to eq("sunday")
end
it "should return the merged hash if all three have values" do
result = @attributes["music"]
expect(result["mars_volta"]).to eq("cicatriz")
expect(result["jimmy_eat_world"]).to eq("nice")
expect(result["mastodon"]).to eq("rocks")
end
end
describe "[]=" do
it "should error out when the type of attribute to set has not been specified" do
@attributes.normal["the_ghost"] = {}
expect { @attributes["the_ghost"]["exterminate"] = false }.to raise_error(Chef::Exceptions::ImmutableAttributeModification)
end
it "should let you set an attribute value when another hash has an intermediate value" do
@attributes.normal["the_ghost"] = { "exterminate" => "the future" }
expect { @attributes.normal["the_ghost"]["eviscerate"]["tomorrow"] = false }.not_to raise_error
end
it "should set the attribute value" do
@attributes.normal["longboard"] = "surfing"
expect(@attributes.normal["longboard"]).to eq("surfing")
expect(@attributes.normal["longboard"]).to eq("surfing")
end
it "should set deeply nested attribute values when a precedence level is specified" do
@attributes.normal["deftones"]["hunters"]["nap"] = "surfing"
expect(@attributes.normal["deftones"]["hunters"]["nap"]).to eq("surfing")
end
it "should die if you try and do nested attributes that do not exist without read vivification" do
expect { @attributes["foo"]["bar"] = :baz }.to raise_error(NoMethodError)
end
it "should let you set attributes manually without vivification" do
@attributes.normal["foo"] = Mash.new
@attributes.normal["foo"]["bar"] = :baz
expect(@attributes.normal["foo"]["bar"]).to eq(:baz)
end
it "does not support ||= when setting" do
# This is a limitation of auto-vivification.
# Users who need this behavior can use set_unless and friends
@attributes.normal["foo"] = Mash.new
@attributes.normal["foo"]["bar"] ||= "stop the world"
expect(@attributes.normal["foo"]["bar"]).to eq({})
end
end
describe "to_hash" do
it "should convert to a hash" do
expect(@attributes.to_hash.class).to eq(Hash)
end
it "should convert to a hash based on current state" do
hash = @attributes["hot"].to_hash
expect(hash.class).to eq(Hash)
expect(hash["day"]).to eq("sunday")
end
it "should create a deep copy of the node attribute" do
@attributes.default["foo"]["bar"]["baz"] = "fizz"
hash = @attributes["foo"].to_hash
expect(hash).to eql({ "bar" => { "baz" => "fizz" } })
hash["bar"]["baz"] = "buzz"
expect(hash).to eql({ "bar" => { "baz" => "buzz" } })
expect(@attributes.default["foo"]).to eql({ "bar" => { "baz" => "fizz" } })
end
it "should create a deep copy of arrays in the node attribute" do
@attributes.default["foo"]["bar"] = ["fizz"]
hash = @attributes["foo"].to_hash
expect(hash).to eql({ "bar" => [ "fizz" ] })
hash["bar"].push("buzz")
expect(hash).to eql({ "bar" => %w{fizz buzz} })
expect(@attributes.default["foo"]).to eql({ "bar" => [ "fizz" ] })
end
it "mutating strings should not mutate the attributes" do
pending "this is a bug that should be fixed"
@attributes.default["foo"]["bar"]["baz"] = "fizz"
hash = @attributes["foo"].to_hash
expect(hash).to eql({ "bar" => { "baz" => "fizz" } })
hash["bar"]["baz"] << "buzz"
expect(hash).to eql({ "bar" => { "baz" => "fizzbuzz" } })
expect(@attributes.default["foo"]).to eql({ "bar" => { "baz" => "fizz" } })
end
end
describe "dup" do
it "array can be duped even if some elements can't" do
@attributes.default[:foo] = %w{foo bar baz} + Array(1..3) + [nil, true, false, [ "el", 0, nil ] ]
@attributes.default[:foo].dup
end
end
describe "has_key?" do
it "should return true if an attribute exists" do
expect(@attributes.has_key?("music")).to eq(true)
end
it "should return false if an attribute does not exist" do
expect(@attributes.has_key?("ninja")).to eq(false)
end
it "should return false if an attribute does not exist using dot notation" do
expect(@attributes.has_key?("does_not_exist_at_all")).to eq(false)
end
it "should return true if an attribute exists but is set to nil using dot notation" do
Chef::Config[:treat_deprecation_warnings_as_errors] = false
expect(@attributes.music.deeper.has_key?("gates_of_ishtar")).to eq(true)
end
it "should return true if an attribute exists but is set to false" do
@attributes.has_key?("music")
expect(@attributes["music"].has_key?("apophis")).to eq(true)
end
it "does not find keys above the current nesting level" do
expect(@attributes["music"]["this"]["apparatus"]).not_to have_key("this")
end
it "does not find keys below the current nesting level" do
expect(@attributes["music"]["this"]).not_to have_key("must")
end
[:include?, :key?, :member?].each do |method|
it "should alias the method #{method} to itself" do
expect(@attributes).to respond_to(method)
end
it "#{method} should behave like has_key?" do
expect(@attributes.send(method, "music")).to eq(true)
end
end
end
describe "attribute?" do
it "should return true if an attribute exists" do
expect(@attributes.attribute?("music")).to eq(true)
end
it "should return false if an attribute does not exist" do
expect(@attributes.attribute?("ninja")).to eq(false)
end
end
describe "method_missing" do
it "should behave like a [] lookup" do
Chef::Config[:treat_deprecation_warnings_as_errors] = false
expect(@attributes.music.mastodon).to eq("rocks")
end
it "should allow the last method to set a value if it has an = sign on the end" do
Chef::Config[:treat_deprecation_warnings_as_errors] = false
@attributes.normal.music.mastodon = %w{dream still shining}
expect(@attributes.normal.music.mastodon).to eq(%w{dream still shining})
end
end
describe "keys" do
before(:each) do
@attributes = Chef::Node::Attribute.new(
{
"one" => { "two" => "three" },
"hut" => { "two" => "three" },
"place" => {},
},
{
"one" => { "four" => "five" },
"snakes" => "on a plane",
},
{
"one" => { "six" => "seven" },
"snack" => "cookies",
},
{}
)
end
it "should yield each top level key" do
collect = Array.new
@attributes.keys.each do |k|
collect << k
end
expect(collect.include?("one")).to eq(true)
expect(collect.include?("hut")).to eq(true)
expect(collect.include?("snakes")).to eq(true)
expect(collect.include?("snack")).to eq(true)
expect(collect.include?("place")).to eq(true)
expect(collect.length).to eq(5)
end
it "should yield lower if we go deeper" do
collect = Array.new
@attributes["one"].keys.each do |k|
collect << k
end
expect(collect.include?("two")).to eq(true)
expect(collect.include?("four")).to eq(true)
expect(collect.include?("six")).to eq(true)
expect(collect.length).to eq(3)
end
it "should not raise an exception if one of the hashes has a nil value on a deep lookup" do
expect { @attributes["place"].keys { |k| } }.not_to raise_error
end
end
describe "each" do
before(:each) do
@attributes = Chef::Node::Attribute.new(
{
"one" => "two",
"hut" => "three",
},
{
"one" => "four",
"snakes" => "on a plane",
},
{
"one" => "six",
"snack" => "cookies",
},
{}
)
end
it "should yield each top level key and value, post merge rules" do
collect = Hash.new
@attributes.each do |k, v|
collect[k] = v
end
expect(collect["one"]).to eq("six")
expect(collect["hut"]).to eq("three")
expect(collect["snakes"]).to eq("on a plane")
expect(collect["snack"]).to eq("cookies")
end
it "should yield as a two-element array" do
@attributes.each do |a|
expect(a).to be_an_instance_of(Array)
end
end
end
describe "each_key" do
before do
@attributes = Chef::Node::Attribute.new(
{
"one" => "two",
"hut" => "three",
},
{
"one" => "four",
"snakes" => "on a plane",
},
{
"one" => "six",
"snack" => "cookies",
},
{}
)
end
it "should respond to each_key" do
expect(@attributes).to respond_to(:each_key)
end
it "should yield each top level key, post merge rules" do
collect = Array.new
@attributes.each_key do |k|
collect << k
end
expect(collect).to include("one")
expect(collect).to include("snack")
expect(collect).to include("hut")
expect(collect).to include("snakes")
end
end
describe "each_pair" do
before do
@attributes = Chef::Node::Attribute.new(
{
"one" => "two",
"hut" => "three",
},
{
"one" => "four",
"snakes" => "on a plane",
},
{
"one" => "six",
"snack" => "cookies",
},
{}
)
end
it "should respond to each_pair" do
expect(@attributes).to respond_to(:each_pair)
end
it "should yield each top level key and value pair, post merge rules" do
collect = Hash.new
@attributes.each_pair do |k, v|
collect[k] = v
end
expect(collect["one"]).to eq("six")
expect(collect["hut"]).to eq("three")
expect(collect["snakes"]).to eq("on a plane")
expect(collect["snack"]).to eq("cookies")
end
end
describe "each_value" do
before do
@attributes = Chef::Node::Attribute.new(
{
"one" => "two",
"hut" => "three",
},
{
"one" => "four",
"snakes" => "on a plane",
},
{
"one" => "six",
"snack" => "cookies",
},
{}
)
end
it "should respond to each_value" do
expect(@attributes).to respond_to(:each_value)
end
it "should yield each value, post merge rules" do
collect = Array.new
@attributes.each_value do |v|
collect << v
end
expect(collect).to include("cookies")
expect(collect).to include("three")
expect(collect).to include("on a plane")
end
it "should yield four elements" do
collect = Array.new
@attributes.each_value do |v|
collect << v
end
expect(collect.length).to eq(4)
end
end
describe "empty?" do
before do
@attributes = Chef::Node::Attribute.new(
{
"one" => "two",
"hut" => "three",
},
{
"one" => "four",
"snakes" => "on a plane",
},
{
"one" => "six",
"snack" => "cookies",
},
{}
)
@empty = Chef::Node::Attribute.new({}, {}, {}, {})
end
it "should respond to empty?" do
expect(@attributes).to respond_to(:empty?)
end
it "should return true when there are no keys" do
expect(@empty.empty?).to eq(true)
end
it "should return false when there are keys" do
expect(@attributes.empty?).to eq(false)
end
end
describe "fetch" do
before do
@attributes = Chef::Node::Attribute.new(
{
"one" => "two",
"hut" => "three",
},
{
"one" => "four",
"snakes" => "on a plane",
},
{
"one" => "six",
"snack" => "cookies",
},
{}
)
end
it "should respond to fetch" do
expect(@attributes).to respond_to(:fetch)
end
describe "when the key exists" do
it "should return the value of the key, post merge (same result as each)" do
{
"one" => "six",
"hut" => "three",
"snakes" => "on a plane",
"snack" => "cookies",
}.each do |k, v|
expect(@attributes.fetch(k)).to eq(v)
end
end
end
describe "when the key does not exist" do
describe "and no args are passed" do
it "should raise an indexerror" do
expect { @attributes.fetch("lololol") }.to raise_error(IndexError)
end
end
describe "and a default arg is passed" do
it "should return the value of the default arg" do
expect(@attributes.fetch("lol", "blah")).to eq("blah")
end
end
describe "and a block is passed" do
it "should run the block and return its value" do
expect(@attributes.fetch("lol") { |x| "#{x}, blah" }).to eq("lol, blah")
end
end
end
end
describe "has_value?" do
before do
@attributes = Chef::Node::Attribute.new(
{
"one" => "two",
"hut" => "three",
},
{
"one" => "four",
"snakes" => "on a plane",
},
{
"one" => "six",
"snack" => "cookies",
},
{}
)
end
it "should respond to has_value?" do
expect(@attributes).to respond_to(:has_value?)
end
it "should return true if any key has the value supplied" do
expect(@attributes.has_value?("cookies")).to eq(true)
end
it "should return false no key has the value supplied" do
expect(@attributes.has_value?("lololol")).to eq(false)
end
it "should alias value?" do
expect(@attributes).to respond_to(:value?)
end
end
describe "index" do
# Hash#index is deprecated and triggers warnings.
def silence
old_verbose = $VERBOSE
$VERBOSE = nil
yield
ensure
$VERBOSE = old_verbose
end
before do
@attributes = Chef::Node::Attribute.new(
{
"one" => "two",
"hut" => "three",
},
{
"one" => "four",
"snakes" => "on a plane",
},
{
"one" => "six",
"snack" => "cookies",
},
{}
)
end
it "should respond to index" do
expect(@attributes).to respond_to(:index)
end
describe "when the value is indexed" do
it "should return the index" do
silence do
expect(@attributes.index("six")).to eq("one")
end
end
end
describe "when the value is not indexed" do
it "should return nil" do
silence do
expect(@attributes.index("lolol")).to eq(nil)
end
end
end
end
describe "values" do
before do
@attributes = Chef::Node::Attribute.new(
{
"one" => "two",
"hut" => "three",
},
{
"one" => "four",
"snakes" => "on a plane",
},
{
"one" => "six",
"snack" => "cookies",
},
{}
)
end
it "should respond to values" do
expect(@attributes).to respond_to(:values)
end
it "should return an array of values" do
expect(@attributes.values.length).to eq(4)
end
it "should match the values output from each" do
expect(@attributes.values).to include("six")
expect(@attributes.values).to include("cookies")
expect(@attributes.values).to include("three")
expect(@attributes.values).to include("on a plane")
end
end
describe "select" do
before do
@attributes = Chef::Node::Attribute.new(
{
"one" => "two",
"hut" => "three",
},
{
"one" => "four",
"snakes" => "on a plane",
},
{
"one" => "six",
"snack" => "cookies",
},
{}
)
end
it "should respond to select" do
expect(@attributes).to respond_to(:select)
end
if RUBY_VERSION >= "1.8.7"
it "should not raise a LocalJumpError if no block is given" do
expect { @attributes.select }.not_to raise_error
end
else
it "should raise a LocalJumpError if no block is given" do
expect { @attributes.select }.to raise_error(LocalJumpError)
end
end
it "should return an empty hash/array (ruby-version-dependent) for a block containing nil" do
expect(@attributes.select { nil }).to eq({}.select { nil })
end
# sorted for spec clarity
it "should return a new array of k,v pairs for which the block returns true" do
expect(@attributes.select { true }.sort).to eq(
[
%w{hut three},
%w{one six},
%w{snack cookies},
["snakes", "on a plane"],
]
)
end
end
describe "size" do
before do
@attributes = Chef::Node::Attribute.new(
{
"one" => "two",
"hut" => "three",
},
{
"one" => "four",
"snakes" => "on a plane",
},
{
"one" => "six",
"snack" => "cookies",
},
{}
)
@empty = Chef::Node::Attribute.new({}, {}, {}, {})
end
it "should respond to size" do
expect(@attributes).to respond_to(:size)
end
it "should alias length to size" do
expect(@attributes).to respond_to(:length)
end
it "should return 0 for an empty attribute" do
expect(@empty.size).to eq(0)
end
it "should return the number of pairs" do
expect(@attributes.size).to eq(4)
end
end
describe "kind_of?" do
it "should falsely inform you that it is a Hash" do
expect(@attributes).to be_a_kind_of(Hash)
end
it "should falsely inform you that it is a Mash" do
expect(@attributes).to be_a_kind_of(Mash)
end
it "should inform you that it is a Chef::Node::Attribute" do
expect(@attributes).to be_a_kind_of(Chef::Node::Attribute)
end
it "should inform you that it is anything else" do
expect(@attributes).not_to be_a_kind_of(Chef::Node)
end
end
describe "to_s" do
it "should output simple attributes" do
attributes = Chef::Node::Attribute.new(nil, nil, nil, nil)
expect(attributes.to_s).to eq("{}")
end
it "should output merged attributes" do
default_hash = {
"a" => 1,
"b" => 2,
}
override_hash = {
"b" => 3,
"c" => 4,
}
attributes = Chef::Node::Attribute.new(nil, default_hash, override_hash, nil)
expect(attributes.to_s).to eq('{"a"=>1, "b"=>3, "c"=>4}')
end
end
describe "inspect" do
it "should be readable" do
# NOTE: previous implementation hid the values, showing @automatic={...}
# That is nice and compact, but hides a lot of info, which seems counter
# to the point of calling #inspect...
expect(@attributes.inspect).to match(/@automatic=\{.*\}/)
expect(@attributes.inspect).to match(/@normal=\{.*\}/)
end
end
describe "when not mutated" do
it "does not reset the cache when dup'd [CHEF-3680]" do
@attributes.default[:foo][:bar] = "set on original"
subtree = @attributes[:foo]
@attributes.default[:foo].dup[:bar] = "set on dup"
expect(subtree[:bar]).to eq("set on original")
end
end
describe "when setting a component attribute to a new value" do
it "converts the input in to a VividMash tree (default)" do
@attributes.default = {}
@attributes.default.foo = "bar"
expect(@attributes.merged_attributes[:foo]).to eq("bar")
end
it "converts the input in to a VividMash tree (normal)" do
@attributes.normal = {}
@attributes.normal.foo = "bar"
expect(@attributes.merged_attributes[:foo]).to eq("bar")
end
it "converts the input in to a VividMash tree (override)" do
@attributes.override = {}
@attributes.override.foo = "bar"
expect(@attributes.merged_attributes[:foo]).to eq("bar")
end
it "converts the input in to a VividMash tree (automatic)" do
@attributes.automatic = {}
@attributes.automatic.foo = "bar"
expect(@attributes.merged_attributes[:foo]).to eq("bar")
end
end
describe "when deep-merging between precedence levels" do
it "correctly deep merges hashes and preserves the original contents" do
@attributes.default = { "arglebargle" => { "foo" => "bar" } }
@attributes.override = { "arglebargle" => { "fizz" => "buzz" } }
expect(@attributes.merged_attributes[:arglebargle]).to eq({ "foo" => "bar", "fizz" => "buzz" })
expect(@attributes.default[:arglebargle]).to eq({ "foo" => "bar" })
expect(@attributes.override[:arglebargle]).to eq({ "fizz" => "buzz" })
end
it "does not deep merge arrays, and preserves the original contents" do
@attributes.default = { "arglebargle" => [ 1, 2, 3 ] }
@attributes.override = { "arglebargle" => [ 4, 5, 6 ] }
expect(@attributes.merged_attributes[:arglebargle]).to eq([ 4, 5, 6 ])
expect(@attributes.default[:arglebargle]).to eq([ 1, 2, 3 ])
expect(@attributes.override[:arglebargle]).to eq([ 4, 5, 6 ])
end
it "correctly deep merges hashes and preserves the original contents when merging default and role_default" do
@attributes.default = { "arglebargle" => { "foo" => "bar" } }
@attributes.role_default = { "arglebargle" => { "fizz" => "buzz" } }
expect(@attributes.merged_attributes[:arglebargle]).to eq({ "foo" => "bar", "fizz" => "buzz" })
expect(@attributes.default[:arglebargle]).to eq({ "foo" => "bar" })
expect(@attributes.role_default[:arglebargle]).to eq({ "fizz" => "buzz" })
end
it "correctly deep merges arrays, and preserves the original contents when merging default and role_default" do
@attributes.default = { "arglebargle" => [ 1, 2, 3 ] }
@attributes.role_default = { "arglebargle" => [ 4, 5, 6 ] }
expect(@attributes.merged_attributes[:arglebargle]).to eq([ 1, 2, 3, 4, 5, 6 ])
expect(@attributes.default[:arglebargle]).to eq([ 1, 2, 3 ])
expect(@attributes.role_default[:arglebargle]).to eq([ 4, 5, 6 ])
end
end
describe "when attemping to write without specifying precedence" do
it "raises an error when using []=" do
expect { @attributes[:new_key] = "new value" }.to raise_error(Chef::Exceptions::ImmutableAttributeModification)
end
it "raises an error when using `attr=value`" do
Chef::Config[:treat_deprecation_warnings_as_errors] = false
expect { @attributes.new_key = "new value" }.to raise_error(Chef::Exceptions::ImmutableAttributeModification)
end
end
describe "deeply converting values" do
it "converts values through an array" do
@attributes.default[:foo] = [ { bar: true } ]
expect(@attributes["foo"].class).to eql(Chef::Node::ImmutableArray)
expect(@attributes["foo"][0].class).to eql(Chef::Node::ImmutableMash)
expect(@attributes["foo"][0]["bar"]).to be true
end
it "converts values through nested arrays" do
@attributes.default[:foo] = [ [ { bar: true } ] ]
expect(@attributes["foo"].class).to eql(Chef::Node::ImmutableArray)
expect(@attributes["foo"][0].class).to eql(Chef::Node::ImmutableArray)
expect(@attributes["foo"][0][0].class).to eql(Chef::Node::ImmutableMash)
expect(@attributes["foo"][0][0]["bar"]).to be true
end
it "converts values through nested hashes" do
@attributes.default[:foo] = { baz: { bar: true } }
expect(@attributes["foo"].class).to eql(Chef::Node::ImmutableMash)
expect(@attributes["foo"]["baz"].class).to eql(Chef::Node::ImmutableMash)
expect(@attributes["foo"]["baz"]["bar"]).to be true
end
end
describe "node state" do
it "sets __root__ correctly" do
@attributes.default["foo"]["bar"]["baz"] = "quux"
expect(@attributes["foo"].__root__).to eql(@attributes)
expect(@attributes["foo"]["bar"].__root__).to eql(@attributes)
expect(@attributes.default["foo"].__root__).to eql(@attributes)
expect(@attributes.default["foo"]["bar"].__root__).to eql(@attributes)
end
it "sets __node__ correctly" do
@attributes.default["foo"]["bar"]["baz"] = "quux"
expect(@attributes["foo"].__node__).to eql(node)
expect(@attributes["foo"]["bar"].__node__).to eql(node)
expect(@attributes.default["foo"].__node__).to eql(node)
expect(@attributes.default["foo"]["bar"].__node__).to eql(node)
end
it "sets __path__ correctly" do
@attributes.default["foo"]["bar"]["baz"] = "quux"
expect(@attributes["foo"].__path__).to eql(["foo"])
expect(@attributes["foo"]["bar"].__path__).to eql(%w{foo bar})
expect(@attributes.default["foo"].__path__).to eql(["foo"])
expect(@attributes.default["foo"]["bar"].__path__).to eql(%w{foo bar})
end
it "sets __precedence__ correctly" do
@attributes.default["foo"]["bar"]["baz"] = "quux"
expect(@attributes["foo"].__precedence__).to eql(:merged)
expect(@attributes["foo"]["bar"].__precedence__).to eql(:merged)
expect(@attributes.default["foo"].__precedence__).to eql(:default)
expect(@attributes.default["foo"]["bar"].__precedence__).to eql(:default)
end
it "notifies on attribute changes" do
expect(events).to receive(:attribute_changed).with(:default, ["foo"], {})
expect(events).to receive(:attribute_changed).with(:default, %w{foo bar}, {})
expect(events).to receive(:attribute_changed).with(:default, %w{foo bar baz}, "quux")
@attributes.default["foo"]["bar"]["baz"] = "quux"
end
end
end