# -*- encoding: utf-8 -*- # # Author:: Fletcher Nichol () # # Copyright (C) 2013, Fletcher Nichol # # 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_relative '../../spec_helper' require 'kitchen/errors' require 'kitchen/util' require 'kitchen/loader/yaml' class Yamled attr_accessor :foo end describe Kitchen::Loader::YAML do let(:loader) do Kitchen::Loader::YAML.new(:project_config => "/tmp/.kitchen.yml") end before do FakeFS.activate! FileUtils.mkdir_p("/tmp") end after do FakeFS.deactivate! FakeFS::FileSystem.clear end describe ".initialize" do it "sets project_config based on Dir.pwd by default" do stub_file(File.join(Dir.pwd, '.kitchen.yml'), Hash.new) loader = Kitchen::Loader::YAML.new loader.diagnose[:project_config][:filename]. must_equal File.expand_path(File.join(Dir.pwd, '.kitchen.yml')) end it "sets project_config from parameter, if given" do stub_file('/tmp/crazyfunkytown.file', Hash.new) loader = Kitchen::Loader::YAML.new( :project_config => '/tmp/crazyfunkytown.file') loader.diagnose[:project_config][:filename]. must_match %r{/tmp/crazyfunkytown.file$} end it "sets local_config based on Dir.pwd by default" do stub_file(File.join(Dir.pwd, '.kitchen.local.yml'), Hash.new) loader = Kitchen::Loader::YAML.new loader.diagnose[:local_config][:filename]. must_equal File.expand_path(File.join(Dir.pwd, '.kitchen.local.yml')) end it "sets local_config based on location of project_config by default" do stub_file('/tmp/.kitchen.local.yml', Hash.new) loader = Kitchen::Loader::YAML.new( :project_config => '/tmp/.kitchen.yml') loader.diagnose[:local_config][:filename]. must_match %r{/tmp/.kitchen.local.yml$} end it "sets local_config from parameter, if given" do stub_file('/tmp/crazyfunkytown.file', Hash.new) loader = Kitchen::Loader::YAML.new( :local_config => '/tmp/crazyfunkytown.file') loader.diagnose[:local_config][:filename]. must_match %r{/tmp/crazyfunkytown.file$} end it "sets global_config based on ENV['HOME'] by default" do stub_file(File.join(ENV['HOME'], '.kitchen/config.yml'), Hash.new) loader = Kitchen::Loader::YAML.new loader.diagnose[:global_config][:filename].must_equal File.expand_path( File.join(ENV['HOME'], '.kitchen/config.yml')) end it "sets global_config from parameter, if given" do stub_file('/tmp/crazyfunkytown.file', Hash.new) loader = Kitchen::Loader::YAML.new( :global_config => '/tmp/crazyfunkytown.file') loader.diagnose[:global_config][:filename]. must_match %r{/tmp/crazyfunkytown.file$} end end describe "#read" do it "returns a hash of kitchen.yml with symbolized keys" do stub_yaml!({ 'foo' => 'bar' }) loader.read.must_equal({ :foo => 'bar' }) end it "deep merges in kitchen.local.yml configuration with kitchen.yml" do stub_yaml!(".kitchen.yml", { 'common' => { 'xx' => 1 }, 'a' => 'b' }) stub_yaml!(".kitchen.local.yml", { 'common' => { 'yy' => 2 }, 'c' => 'd' }) loader.read.must_equal({ :a => 'b', :c => 'd', :common => { :xx => 1, :yy => 2 } }) end it "deep merges in a global config file with all other configs" do stub_yaml!(".kitchen.yml", { 'common' => { 'xx' => 1 }, 'a' => 'b' }) stub_yaml!(".kitchen.local.yml", { 'common' => { 'yy' => 2 }, 'c' => 'd' }) stub_global!({ 'common' => { 'zz' => 3 }, 'e' => 'f' }) loader.read.must_equal({ :a => 'b', :c => 'd', :e => 'f', :common => { :xx => 1, :yy => 2, :zz => 3 } }) end it "merges kitchen.local.yml over configuration in kitchen.yml" do stub_yaml!(".kitchen.yml", { 'common' => { 'thekey' => 'nope' } }) stub_yaml!(".kitchen.local.yml", { 'common' => { 'thekey' => 'yep' } }) loader.read.must_equal({ :common => { :thekey => 'yep' } }) end it "merges global config over both kitchen.local.yml and kitchen.yml" do stub_yaml!(".kitchen.yml", { 'common' => { 'thekey' => 'nope' } }) stub_yaml!(".kitchen.local.yml", { 'common' => { 'thekey' => 'yep' } }) stub_global!({ 'common' => { 'thekey' => 'kinda' } }) loader.read.must_equal({ :common => { :thekey => 'kinda' } }) end NORMALIZED_KEYS = { "driver" => "name", "provisioner" => "name", "busser" => "version" } NORMALIZED_KEYS.each do |key, default_key| describe "normalizing #{key} config hashes" do it "merges local with #{key} string value over yaml with hash value" do stub_yaml!(".kitchen.yml", { key => { 'dakey' => 'ya' } }) stub_yaml!(".kitchen.local.yml", { key => 'namey' }) loader.read.must_equal({ key.to_sym => { default_key.to_sym => "namey", :dakey => 'ya' } }) end it "merges local with #{key} hash value over yaml with string value" do stub_yaml!(".kitchen.yml", { key => 'namey' }) stub_yaml!(".kitchen.local.yml", { key => { 'dakey' => 'ya' } }) loader.read.must_equal({ key.to_sym => { default_key.to_sym => "namey", :dakey => 'ya' } }) end it "merges local with #{key} nil value over yaml with hash value" do stub_yaml!(".kitchen.yml", { key => { 'dakey' => 'ya' } }) stub_yaml!(".kitchen.local.yml", { key => nil }) loader.read.must_equal({ key.to_sym => { :dakey => 'ya' } }) end it "merges local with #{key} hash value over yaml with nil value" do stub_yaml!(".kitchen.yml", { key => 'namey' }) stub_yaml!(".kitchen.local.yml", { key => nil }) loader.read.must_equal({ key.to_sym => { default_key.to_sym => "namey" } }) end it "merges global with #{key} string value over yaml with hash value" do stub_yaml!(".kitchen.yml", { key => { 'dakey' => 'ya' } }) stub_global!({ key => 'namey' }) loader.read.must_equal({ key.to_sym => { default_key.to_sym => "namey", :dakey => 'ya' } }) end it "merges global with #{key} hash value over yaml with string value" do stub_yaml!(".kitchen.yml", { key => 'namey' }) stub_global!({ key => { 'dakey' => 'ya' } }) loader.read.must_equal({ key.to_sym => { default_key.to_sym => "namey", :dakey => 'ya' } }) end it "merges global with #{key} nil value over yaml with hash value" do stub_yaml!(".kitchen.yml", { key => { 'dakey' => 'ya' } }) stub_global!({ key => nil }) loader.read.must_equal({ key.to_sym => { :dakey => 'ya' } }) end it "merges global with #{key} hash value over yaml with nil value" do stub_yaml!(".kitchen.yml", { key => nil }) stub_global!({ key => { 'dakey' => 'ya' } }) loader.read.must_equal({ key.to_sym => { :dakey => 'ya' } }) end it "merges global, local, over yaml with mixed hash, string, nil values" do stub_yaml!(".kitchen.yml", { key => nil }) stub_yaml!(".kitchen.local.yml", { key => "namey" }) stub_global!({ key => { 'dakey' => 'ya' } }) loader.read.must_equal({ key.to_sym => { default_key.to_sym => "namey", :dakey => 'ya' } }) end end end it "handles a kitchen.local.yml with no yaml elements" do stub_yaml!(".kitchen.yml", { 'a' => 'b' }) stub_yaml!(".kitchen.local.yml", Hash.new) loader.read.must_equal({ :a => 'b' }) end it "handles a kitchen.yml with no yaml elements" do stub_yaml!(".kitchen.yml", Hash.new) stub_yaml!(".kitchen.local.yml", { 'a' => 'b' }) loader.read.must_equal({ :a => 'b' }) end it "handles a kitchen.yml with yaml elements that parse as nil" do stub_yaml!(".kitchen.yml", nil) stub_yaml!(".kitchen.local.yml", { 'a' => 'b' }) loader.read.must_equal({ :a => 'b' }) end it "raises an UserError if the config_file does not exist" do proc { loader.read }.must_raise Kitchen::UserError end it "arbitrary objects aren't deserialized in kitchen.yml" do FileUtils.mkdir_p "/tmp" File.open("/tmp/.kitchen.yml", "wb") do |f| f.write <<-YAML.gsub(/^ {10}/, '') --- !ruby/object:Yamled foo: bar YAML end loader.read.class.wont_equal Yamled loader.read.class.must_equal Hash loader.read.must_equal({ :foo => 'bar' }) end it "arbitrary objects aren't deserialized in kitchen.local.yml" do FileUtils.mkdir_p "/tmp" File.open("/tmp/.kitchen.local.yml", "wb") do |f| f.write <<-YAML.gsub(/^ {10}/, '') --- !ruby/object:Yamled wakka: boop YAML end stub_yaml!(".kitchen.yml", Hash.new) loader.read.class.wont_equal Yamled loader.read.class.must_equal Hash loader.read.must_equal({ :wakka => 'boop' }) end it "raises a UserError if kitchen.yml cannot be parsed" do FileUtils.mkdir_p "/tmp" File.open("/tmp/.kitchen.yml", "wb") { |f| f.write '&*%^*' } err = proc { loader.read }.must_raise Kitchen::UserError err.message.must_match Regexp.new( "Error parsing ([a-zA-Z]:)?\/tmp\/\.kitchen\.yml") end it "raises a UserError if kitchen.yml cannot be parsed" do FileUtils.mkdir_p "/tmp" File.open("/tmp/.kitchen.yml", "wb") { |f| f.write 'uhoh' } err = proc { loader.read }.must_raise Kitchen::UserError err.message.must_match Regexp.new( "Error parsing ([a-zA-Z]:)?\/tmp\/\.kitchen\.yml") end it "handles a kitchen.yml if it is a commented out YAML document" do FileUtils.mkdir_p "/tmp" File.open("/tmp/.kitchen.yml", "wb") { |f| f.write '#---\n' } loader.read.must_equal(Hash.new) end it "raises a UserError if kitchen.local.yml cannot be parsed" do FileUtils.mkdir_p "/tmp" File.open("/tmp/.kitchen.local.yml", "wb") { |f| f.write '&*%^*' } stub_yaml!(".kitchen.yml", Hash.new) proc { loader.read }.must_raise Kitchen::UserError end it "evaluates kitchen.yml through erb before loading by default" do FileUtils.mkdir_p "/tmp" File.open("/tmp/.kitchen.yml", "wb") do |f| f.write <<-'YAML'.gsub(/^ {10}/, '') --- name: <%= "AHH".downcase + "choo" %> YAML end loader.read.must_equal({ :name => "ahhchoo" }) end it "raises a UserError if there is an ERB processing error" do FileUtils.mkdir_p "/tmp" File.open("/tmp/.kitchen.yml", "wb") do |f| f.write <<-'YAML'.gsub(/^ {10}/, '') --- <%= poop %>: yep YAML end err = proc { loader.read }.must_raise Kitchen::UserError err.message.must_match Regexp.new( "Error parsing ERB content in ([a-zA-Z]:)?\/tmp\/\.kitchen\.yml") end it "evaluates kitchen.local.yml through erb before loading by default" do FileUtils.mkdir_p "/tmp" File.open("/tmp/.kitchen.local.yml", "wb") do |f| f.write <<-'YAML'.gsub(/^ {10}/, '') --- <% %w{noodle mushroom}.each do |kind| %> <%= kind %>: soup <% end %> YAML end stub_yaml!(".kitchen.yml", { 'spinach' => 'salad' }) loader.read.must_equal({ :spinach => 'salad', :noodle => 'soup', :mushroom => 'soup' }) end it "skips evaluating kitchen.yml through erb if disabled" do loader = Kitchen::Loader::YAML.new( :project_config => '/tmp/.kitchen.yml', :process_erb => false) FileUtils.mkdir_p "/tmp" File.open("/tmp/.kitchen.yml", "wb") do |f| f.write <<-'YAML'.gsub(/^ {10}/, '') --- name: <%= "AHH".downcase %> YAML end loader.read.must_equal({ :name => '<%= "AHH".downcase %>' }) end it "skips evaluating kitchen.local.yml through erb if disabled" do loader = Kitchen::Loader::YAML.new( :project_config => '/tmp/.kitchen.yml', :process_erb => false) FileUtils.mkdir_p "/tmp" File.open("/tmp/.kitchen.local.yml", "wb") do |f| f.write <<-'YAML'.gsub(/^ {10}/, '') --- name: <%= "AHH".downcase %> YAML end stub_yaml!(".kitchen.yml", Hash.new) loader.read.must_equal({ :name => '<%= "AHH".downcase %>' }) end it "skips kitchen.local.yml if disabled" do loader = Kitchen::Loader::YAML.new( :project_config => '/tmp/.kitchen.yml', :process_local => false) stub_yaml!(".kitchen.yml", { 'a' => 'b' }) stub_yaml!(".kitchen.local.yml", { 'superawesomesauceadditions' => 'enabled, yo' }) loader.read.must_equal({ :a => 'b' }) end it "skips the global config if disabled" do loader = Kitchen::Loader::YAML.new( :project_config => '/tmp/.kitchen.yml', :process_global => false) stub_yaml!(".kitchen.yml", { 'a' => 'b' }) stub_global!({ 'superawesomesauceadditions' => 'enabled, yo' }) loader.read.must_equal({ :a => 'b' }) end end describe "#diagnose" do it "returns a Hash" do stub_yaml!(Hash.new) loader.diagnose.must_be_kind_of(Hash) end it "contains erb processing information when true" do stub_yaml!(Hash.new) loader.diagnose[:process_erb].must_equal true end it "contains erb processing information when false" do stub_yaml!(Hash.new) loader = Kitchen::Loader::YAML.new( :project_config => '/tmp/.kitchen.yml', :process_erb => false) loader.diagnose[:process_erb].must_equal false end it "contains local processing information when true" do stub_yaml!(Hash.new) loader.diagnose[:process_local].must_equal true end it "contains local processing information when false" do stub_yaml!(Hash.new) loader = Kitchen::Loader::YAML.new( :project_config => '/tmp/.kitchen.yml', :process_local => false) loader.diagnose[:process_local].must_equal false end it "contains global processing information when true" do stub_yaml!(Hash.new) loader.diagnose[:process_global].must_equal true end it "contains global processing information when false" do stub_yaml!(Hash.new) loader = Kitchen::Loader::YAML.new( :project_config => '/tmp/.kitchen.yml', :process_global => false) loader.diagnose[:process_global].must_equal false end describe "for yaml files" do before do stub_yaml!(".kitchen.yml", { "from_project" => "project", "common" => { "p" => "pretty" } }) stub_yaml!(".kitchen.local.yml", { "from_local" => "local", "common" => { "l" => "looky" } }) stub_global!({ "from_global" => "global", "common" => { "g" => "goody" } }) end it "global config contains a filename" do loader.diagnose[:global_config][:filename]. must_equal File.join(ENV["HOME"], ".kitchen/config.yml") end it "global config contains raw data" do loader.diagnose[:global_config][:raw_data].must_equal({ "from_global" => "global", "common" => { "g" => "goody" } }) end it "project config contains a filename" do loader.diagnose[:project_config][:filename]. must_match %r{/tmp/.kitchen.yml$} end it "project config contains raw data" do loader.diagnose[:project_config][:raw_data].must_equal({ "from_project" => "project", "common" => { "p" => "pretty" } }) end it "local config contains a filename" do loader.diagnose[:local_config][:filename]. must_match %r{/tmp/.kitchen.local.yml$} end it "local config contains raw data" do loader.diagnose[:local_config][:raw_data].must_equal({ "from_local" => "local", "common" => { "l" => "looky" } }) end it "combined config contains a nil filename" do loader.diagnose[:combined_config][:filename]. must_equal nil end it "combined config contains raw data" do loader.diagnose[:combined_config][:raw_data].must_equal({ "from_global" => "global", "from_project" => "project", "from_local" => "local", "common" => { "g" => "goody", "p" => "pretty", "l" => "looky" } }) end describe "for global on error" do before do FileUtils.mkdir_p(File.join(ENV["HOME"], ".kitchen")) File.open(File.join(ENV["HOME"], ".kitchen/config.yml"), "wb") do |f| f.write '&*%^*' end end it "uses an error hash with the raw file contents" do loader.diagnose[:global_config][:raw_data][:error][:raw_file]. must_equal "&*%^*" end it "uses an error hash with the exception" do loader.diagnose[:global_config][:raw_data][:error][:exception]. must_match %r{Kitchen::UserError} end it "uses an error hash with the exception message" do loader.diagnose[:global_config][:raw_data][:error][:message]. must_match %r{Error parsing} end it "uses an error hash with the exception backtrace" do loader.diagnose[:global_config][:raw_data][:error][:backtrace]. must_be_kind_of Array end end describe "for project on error" do before do File.open("/tmp/.kitchen.yml", "wb") do |f| f.write '&*%^*' end end it "uses an error hash with the raw file contents" do loader.diagnose[:project_config][:raw_data][:error][:raw_file]. must_equal "&*%^*" end it "uses an error hash with the exception" do loader.diagnose[:project_config][:raw_data][:error][:exception]. must_match %r{Kitchen::UserError} end it "uses an error hash with the exception message" do loader.diagnose[:project_config][:raw_data][:error][:message]. must_match %r{Error parsing} end it "uses an error hash with the exception backtrace" do loader.diagnose[:project_config][:raw_data][:error][:backtrace]. must_be_kind_of Array end end describe "for local on error" do before do File.open("/tmp/.kitchen.local.yml", "wb") do |f| f.write '&*%^*' end end it "uses an error hash with the raw file contents" do loader.diagnose[:local_config][:raw_data][:error][:raw_file]. must_equal "&*%^*" end it "uses an error hash with the exception" do loader.diagnose[:local_config][:raw_data][:error][:exception]. must_match %r{Kitchen::UserError} end it "uses an error hash with the exception message" do loader.diagnose[:local_config][:raw_data][:error][:message]. must_match %r{Error parsing} end it "uses an error hash with the exception backtrace" do loader.diagnose[:local_config][:raw_data][:error][:backtrace]. must_be_kind_of Array end end describe "for combined on error" do before do File.open("/tmp/.kitchen.yml", "wb") do |f| f.write '&*%^*' end end it "uses an error hash with nil raw file contents" do loader.diagnose[:combined_config][:raw_data][:error][:raw_file]. must_equal nil end it "uses an error hash with the exception" do loader.diagnose[:combined_config][:raw_data][:error][:exception]. must_match %r{Kitchen::UserError} end it "uses an error hash with the exception message" do loader.diagnose[:combined_config][:raw_data][:error][:message]. must_match %r{Error parsing} end it "uses an error hash with the exception backtrace" do loader.diagnose[:combined_config][:raw_data][:error][:backtrace]. must_be_kind_of Array end end end end private def stub_file(path, hash) FileUtils.mkdir_p(File.dirname(path)) File.open(path, "wb") { |f| f.write(hash.to_yaml) } end def stub_yaml!(name = ".kitchen.yml", hash) stub_file(File.join("/tmp", name), hash) end def stub_global!(hash) stub_file(File.join(File.expand_path(ENV["HOME"]), ".kitchen", "config.yml"), hash) end end