diff options
author | danielsdeleo <dan@getchef.com> | 2014-08-12 11:53:46 -0700 |
---|---|---|
committer | danielsdeleo <dan@getchef.com> | 2014-08-12 11:53:46 -0700 |
commit | e07deb9e9f5e89bb3cc144b8629fece223a70943 (patch) | |
tree | 10c49903939af55bab148b54b4e408316557ed6c | |
parent | d188093dccf97428dcf625c57a996faada88a31b (diff) | |
parent | a0777738b985314d1f4abadde6f7c9687178d61f (diff) | |
download | chef-e07deb9e9f5e89bb3cc144b8629fece223a70943.tar.gz |
Merge branch 'respect-metadata-name'
Closes CHEF-3307, CHEF-3490.
28 files changed, 863 insertions, 299 deletions
diff --git a/lib/chef/cookbook/cookbook_version_loader.rb b/lib/chef/cookbook/cookbook_version_loader.rb index fac8c80993..ddbcb785dc 100644 --- a/lib/chef/cookbook/cookbook_version_loader.rb +++ b/lib/chef/cookbook/cookbook_version_loader.rb @@ -18,21 +18,29 @@ class Chef UPLOADED_COOKBOOK_VERSION_FILE = ".uploaded-cookbook-version.json".freeze - attr_reader :cookbook_name attr_reader :cookbook_settings attr_reader :cookbook_paths attr_reader :metadata_filenames attr_reader :frozen attr_reader :uploaded_cookbook_version_file + attr_reader :cookbook_path + + # The cookbook's name as inferred from its directory. + attr_reader :inferred_cookbook_name + + attr_reader :metadata_error + def initialize(path, chefignore=nil) @cookbook_path = File.expand_path( path ) # cookbook_path from which this was loaded # We keep a list of all cookbook paths that have been merged in - @cookbook_paths = [ @cookbook_path ] - @cookbook_name = File.basename( path ) + @cookbook_paths = [ cookbook_path ] + + @inferred_cookbook_name = File.basename( path ) @chefignore = chefignore - @metadata = Hash.new + @metadata = nil @relative_path = /#{Regexp.escape(@cookbook_path)}\/(.+)$/ + @metadata_loaded = false @cookbook_settings = { :attribute_filenames => {}, :definition_filenames => {}, @@ -46,9 +54,29 @@ class Chef } @metadata_filenames = [] + @metadata_error = nil + end + + # Load the cookbook. Raises an error if the cookbook_path given to the + # constructor doesn't point to a valid cookbook. + def load! + file_paths_map = load + + if empty? + raise Exceptions::CookbookNotFoundInRepo, "The directory #{cookbook_path} does not contain a cookbook" + end + file_paths_map end - def load_cookbooks + # Load the cookbook. Does not raise an error if given a non-cookbook + # directory as the cookbook_path. This behavior is provided for + # compatibility, it is recommended to use #load! instead. + def load + metadata # force lazy evaluation to occur + + # re-raise any exception that occurred when reading the metadata + raise_metadata_error! + load_as(:attribute_filenames, 'attributes', '*.rb') load_as(:definition_filenames, 'definitions', '*.rb') load_as(:recipe_filenames, 'recipes', '*.rb') @@ -61,31 +89,37 @@ class Chef remove_ignored_files - if File.exists?(File.join(@cookbook_path, UPLOADED_COOKBOOK_VERSION_FILE)) - @uploaded_cookbook_version_file = File.join(@cookbook_path, UPLOADED_COOKBOOK_VERSION_FILE) + if empty? + Chef::Log.warn "found a directory #{cookbook_name} in the cookbook path, but it contains no cookbook files. skipping." + end + @cookbook_settings + end + + alias :load_cookbooks :load + + def metadata_filenames + return @metadata_filenames unless @metadata_filenames.empty? + if File.exists?(File.join(cookbook_path, UPLOADED_COOKBOOK_VERSION_FILE)) + @uploaded_cookbook_version_file = File.join(cookbook_path, UPLOADED_COOKBOOK_VERSION_FILE) end - if File.exists?(File.join(@cookbook_path, "metadata.rb")) - @metadata_filenames << File.join(@cookbook_path, "metadata.rb") - elsif File.exists?(File.join(@cookbook_path, "metadata.json")) - @metadata_filenames << File.join(@cookbook_path, "metadata.json") + if File.exists?(File.join(cookbook_path, "metadata.rb")) + @metadata_filenames << File.join(cookbook_path, "metadata.rb") + elsif File.exists?(File.join(cookbook_path, "metadata.json")) + @metadata_filenames << File.join(cookbook_path, "metadata.json") elsif @uploaded_cookbook_version_file @metadata_filenames << @uploaded_cookbook_version_file end # Set frozen based on .uploaded-cookbook-version.json set_frozen - - if empty? - Chef::Log.warn "found a directory #{cookbook_name} in the cookbook path, but it contains no cookbook files. skipping." - end - @cookbook_settings + @metadata_filenames end def cookbook_version return nil if empty? - Chef::CookbookVersion.new(@cookbook_name.to_sym, *@cookbook_paths).tap do |c| + Chef::CookbookVersion.new(cookbook_name, *cookbook_paths).tap do |c| c.attribute_filenames = cookbook_settings[:attribute_filenames].values c.definition_filenames = cookbook_settings[:definition_filenames].values c.recipe_filenames = cookbook_settings[:recipe_filenames].values @@ -95,16 +129,32 @@ class Chef c.resource_filenames = cookbook_settings[:resource_filenames].values c.provider_filenames = cookbook_settings[:provider_filenames].values c.root_filenames = cookbook_settings[:root_filenames].values - c.metadata_filenames = @metadata_filenames - c.metadata = metadata(c) + c.metadata_filenames = metadata_filenames + c.metadata = metadata + c.freeze_version if @frozen end end + def cookbook_name + # The `name` attribute is now required in metadata, so + # inferred_cookbook_name generally should not be used. Per CHEF-2923, + # we have to not raise errors in cookbook metadata immediately, so that + # users can still `knife cookbook upload some-cookbook` when an + # unrelated cookbook has an error in its metadata. This situation + # could prevent us from reading the `name` attribute from the metadata + # entirely, but the name is used as a hash key in CookbookLoader, so we + # fall back to the inferred name here. + (metadata.name || @inferred_cookbook_name).to_sym + end + # Generates the Cookbook::Metadata object - def metadata(cookbook_version) - @metadata = Chef::Cookbook::Metadata.new(cookbook_version) - @metadata_filenames.each do |metadata_file| + def metadata + return @metadata unless @metadata.nil? + + @metadata = Chef::Cookbook::Metadata.new + + metadata_filenames.each do |metadata_file| case metadata_file when /\.rb$/ apply_ruby_metadata(metadata_file) @@ -116,51 +166,75 @@ class Chef raise RuntimeError, "Invalid metadata file: #{metadata_file} for cookbook: #{cookbook_version}" end end + + @metadata + + # Rescue errors so that users can upload cookbooks via `knife cookbook + # upload` even if some cookbooks in their chef-repo have errors in + # their metadata. We only rescue StandardError because you have to be + # doing something *really* terrible to raise an exception that inherits + # directly from Exception in your metadata.rb file. + rescue StandardError => e + @metadata_error = e @metadata end + def raise_metadata_error! + raise @metadata_error unless @metadata_error.nil? + # Metadata won't be valid if the cookbook is empty. If the cookbook is + # actually empty, a metadata error here would be misleading, so don't + # raise it (if called by #load!, a different error is raised). + if !empty? && !metadata.valid? + message = "Cookbook loaded at path(s) [#{@cookbook_paths.join(', ')}] has invalid metadata: #{metadata.errors.join('; ')}" + raise Exceptions::MetadataNotValid, message + end + false + end + def empty? - @cookbook_settings.values.all? { |files_hash| files_hash.empty? } && @metadata_filenames.size == 0 + cookbook_settings.values.all? { |files_hash| files_hash.empty? } && metadata_filenames.size == 0 end def merge!(other_cookbook_loader) other_cookbook_settings = other_cookbook_loader.cookbook_settings - @cookbook_settings.each do |file_type, file_list| + cookbook_settings.each do |file_type, file_list| file_list.merge!(other_cookbook_settings[file_type]) end - @metadata_filenames.concat(other_cookbook_loader.metadata_filenames) + metadata_filenames.concat(other_cookbook_loader.metadata_filenames) @cookbook_paths += other_cookbook_loader.cookbook_paths @frozen = true if other_cookbook_loader.frozen + @metadata = nil # reset metadata so it gets reloaded and all metadata files applied. + self end def chefignore - @chefignore ||= Chefignore.new(File.basename(@cookbook_path)) + @chefignore ||= Chefignore.new(File.basename(cookbook_path)) end def load_root_files - Dir.glob(File.join(@cookbook_path, '*'), File::FNM_DOTMATCH).each do |file| + Dir.glob(File.join(cookbook_path, '*'), File::FNM_DOTMATCH).each do |file| next if File.directory?(file) next if File.basename(file) == UPLOADED_COOKBOOK_VERSION_FILE - @cookbook_settings[:root_filenames][file[@relative_path, 1]] = file + cookbook_settings[:root_filenames][file[@relative_path, 1]] = file end end def load_recursively_as(category, category_dir, glob) - file_spec = File.join(@cookbook_path, category_dir, '**', glob) + file_spec = File.join(cookbook_path, category_dir, '**', glob) Dir.glob(file_spec, File::FNM_DOTMATCH).each do |file| next if File.directory?(file) - @cookbook_settings[category][file[@relative_path, 1]] = file + cookbook_settings[category][file[@relative_path, 1]] = file end end def load_as(category, *path_glob) - Dir[File.join(@cookbook_path, *path_glob)].each do |file| - @cookbook_settings[category][file[@relative_path, 1]] = file + Dir[File.join(cookbook_path, *path_glob)].each do |file| + cookbook_settings[category][file[@relative_path, 1]] = file end end def remove_ignored_files - @cookbook_settings.each_value do |file_list| + cookbook_settings.each_value do |file_list| file_list.reject! do |relative_path, full_path| chefignore.ignored?(relative_path) end @@ -171,7 +245,7 @@ class Chef begin @metadata.from_file(file) rescue Chef::Exceptions::JSON::ParseError - Chef::Log.error("Error evaluating metadata.rb for #@cookbook_name in " + file) + Chef::Log.error("Error evaluating metadata.rb for #@inferred_cookbook_name in " + file) raise end end @@ -180,7 +254,7 @@ class Chef begin @metadata.from_json(IO.read(file)) rescue Chef::Exceptions::JSON::ParseError - Chef::Log.error("Couldn't parse cookbook metadata JSON for #@cookbook_name in " + file) + Chef::Log.error("Couldn't parse cookbook metadata JSON for #@inferred_cookbook_name in " + file) raise end end @@ -189,8 +263,17 @@ class Chef begin data = Chef::JSONCompat.from_json(IO.read(file), :create_additions => false) @metadata.from_hash(data['metadata']) + # the JSON cookbok metadata file is only used by chef-zero. + # The Chef Server API currently does not enforce that the metadata + # have a `name` field, but that will cause an error when attempting + # to load the cookbook. To keep compatibility, we fake it by setting + # the metadata name from the cookbook version object's name. + # + # This behavior can be removed if/when Chef Server enforces that the + # metadata contains a name key. + @metadata.name(data['cookbook_name']) unless data['metadata'].key?('name') rescue Chef::Exceptions::JSON::ParseError - Chef::Log.error("Couldn't parse cookbook metadata JSON for #@cookbook_name in " + file) + Chef::Log.error("Couldn't parse cookbook metadata JSON for #@inferred_cookbook_name in " + file) raise end end @@ -201,7 +284,7 @@ class Chef data = Chef::JSONCompat.from_json(IO.read(uploaded_cookbook_version_file), :create_additions => false) @frozen = data['frozen?'] rescue Chef::Exceptions::JSON::ParseError - Chef::Log.error("Couldn't parse cookbook metadata JSON for #@cookbook_name in #{uploaded_cookbook_version_file}") + Chef::Log.error("Couldn't parse cookbook metadata JSON for #@inferred_cookbook_name in #{uploaded_cookbook_version_file}") raise end end diff --git a/lib/chef/cookbook/metadata.rb b/lib/chef/cookbook/metadata.rb index 8d3f8b84aa..3964354d50 100644 --- a/lib/chef/cookbook/metadata.rb +++ b/lib/chef/cookbook/metadata.rb @@ -18,6 +18,7 @@ # limitations under the License. # +require 'chef/exceptions' require 'chef/mash' require 'chef/mixin/from_file' require 'chef/mixin/params_validate' @@ -67,18 +68,17 @@ class Chef include Chef::Mixin::ParamsValidate include Chef::Mixin::FromFile - attr_reader :cookbook, - :platforms, - :dependencies, - :recommendations, - :suggestions, - :conflicting, - :providing, - :replacing, - :attributes, - :groupings, - :recipes, - :version + attr_reader :platforms + attr_reader :dependencies + attr_reader :recommendations + attr_reader :suggestions + attr_reader :conflicting + attr_reader :providing + attr_reader :replacing + attr_reader :attributes + attr_reader :groupings + attr_reader :recipes + attr_reader :version # Builds a new Chef::Cookbook::Metadata object. # @@ -90,14 +90,16 @@ class Chef # # === Returns # metadata<Chef::Cookbook::Metadata> - def initialize(cookbook=nil, maintainer='YOUR_COMPANY_NAME', maintainer_email='YOUR_EMAIL', license='none') - @cookbook = cookbook - @name = cookbook ? cookbook.name : "" - @long_description = "" - self.maintainer(maintainer) - self.maintainer_email(maintainer_email) - self.license(license) - self.description('A fabulous new cookbook') + def initialize + @name = nil + + @description = '' + @long_description = '' + @license = 'All rights reserved' + + @maintainer = nil + @maintainer_email = nil + @platforms = Mash.new @dependencies = Mash.new @recommendations = Mash.new @@ -108,15 +110,9 @@ class Chef @attributes = Mash.new @groupings = Mash.new @recipes = Mash.new - @version = Version.new "0.0.0" - if cookbook - @recipes = cookbook.fully_qualified_recipe_names.inject({}) do |r, e| - e = self.name.to_s if e =~ /::default$/ - r[e] ||= "" - self.provides e - r - end - end + @version = Version.new("0.0.0") + + @errors = [] end def ==(other) @@ -125,6 +121,32 @@ class Chef end end + # Whether this metadata is valid. In order to be valid, all required + # fields must be set. Chef's validation implementation checks the content + # of a given field when setting (and raises an error if the content does + # not meet the criteria), so the content of the fields is not considered + # when checking validity. + # + # === Returns + # valid<Boolean>:: Whether this metadata object is valid + def valid? + run_validation + @errors.empty? + end + + # A list of validation errors for this metadata object. See #valid? for + # comments about the validation criteria. + # + # If there are any validation errors, one or more error strings will be + # returned. Otherwise an empty array is returned. + # + # === Returns + # error messages<Array>:: Whether this metadata object is valid + def errors + run_validation + @errors + end + # Sets the cookbooks maintainer, or returns it. # # === Parameters @@ -365,6 +387,32 @@ class Chef @recipes[name] = description end + # Sets the cookbook's recipes to the list of recipes in the given + # +cookbook+. Any recipe that already has a description (if set by the + # #recipe method) will not be updated. + # + # === Parameters + # cookbook<CookbookVersion>:: CookbookVersion object representing the cookbook + # description<String>:: The description of the recipe + # + # === Returns + # recipe_unqualified_names<Array>:: An array of the recipe names given by the cookbook + def recipes_from_cookbook_version(cookbook) + cookbook.fully_qualified_recipe_names.map do |recipe_name| + unqualified_name = + if recipe_name =~ /::default$/ + self.name.to_s + else + recipe_name + end + + @recipes[unqualified_name] ||= "" + provides(unqualified_name) + + unqualified_name + end + end + # Adds an attribute )hat a user needs to configure for this cookbook. Takes # a name (with the / notation for a nested attribute), followed by any of # these options @@ -480,9 +528,9 @@ class Chef def self.validate_json(json_str) o = Chef::JSONCompat.from_json(json_str) metadata = new() - VERSION_CONSTRAINTS.each do |method_name, hash_key| - if constraints = o[hash_key] - constraints.each do |cb_name, constraints| + VERSION_CONSTRAINTS.each do |dependency_type, hash_key| + if dependency_group = o[hash_key] + dependency_group.each do |cb_name, constraints| metadata.send(method_name, cb_name, *Array(constraints)) end end @@ -497,6 +545,12 @@ class Chef private + def run_validation + if name.nil? + @errors = ["The `name' attribute is required in cookbook metadata"] + end + end + def new_args_format(caller_name, dep_name, version_constraints) if version_constraints.empty? ">= 0.0.0" diff --git a/lib/chef/cookbook_loader.rb b/lib/chef/cookbook_loader.rb index 27cf978acb..d569cdd008 100644 --- a/lib/chef/cookbook_loader.rb +++ b/lib/chef/cookbook_loader.rb @@ -50,6 +50,9 @@ class Chef repo_path = File.expand_path(repo_path) end + @preloaded_cookbooks = false + @loaders_by_name = {} + # Used to track which cookbooks appear in multiple places in the cookbook repos # and are merged in to a single cookbook by file shadowing. This behavior is # deprecated, so users of this class may issue warnings to the user by checking @@ -64,25 +67,25 @@ class Chef end def load_cookbooks - @repo_paths.each do |repo_path| - Dir[File.join(repo_path, "*")].each do |cookbook_path| - load_cookbook(File.basename(cookbook_path), [repo_path]) - end + preload_cookbooks + @loaders_by_name.each do |cookbook_name, _loaders| + load_cookbook(cookbook_name) end @cookbooks_by_name end - def load_cookbook(cookbook_name, repo_paths=nil) - repo_paths ||= @repo_paths - repo_paths.each do |repo_path| - @chefignores[repo_path] ||= Cookbook::Chefignore.new(repo_path) - cookbook_path = File.join(repo_path, cookbook_name.to_s) - next unless File.directory?(cookbook_path) and Dir[File.join(repo_path, "*")].include?(cookbook_path) - loader = Cookbook::CookbookVersionLoader.new(cookbook_path, @chefignores[repo_path]) - loader.load_cookbooks + def load_cookbook(cookbook_name) + preload_cookbooks + + return nil unless @loaders_by_name.key?(cookbook_name.to_s) + + cookbook_loaders_for(cookbook_name).each do |loader| + loader.load + next if loader.empty? - cookbook_name = loader.cookbook_name - @cookbooks_paths[cookbook_name] << cookbook_path # for deprecation warnings + + @cookbooks_paths[cookbook_name] << loader.cookbook_path # for deprecation warnings + if @loaded_cookbooks.key?(cookbook_name) @merged_cookbooks << cookbook_name # for deprecation warnings @loaded_cookbooks[cookbook_name].merge!(loader) @@ -130,5 +133,50 @@ class Chef end alias :cookbooks :values + private + + def preload_cookbooks + return false if @preloaded_cookbooks + + all_directories_in_repo_paths.each do |cookbook_path| + preload_cookbook(cookbook_path) + end + @preloaded_cookbooks = true + true + end + + def preload_cookbook(cookbook_path) + repo_path = File.dirname(cookbook_path) + @chefignores[repo_path] ||= Cookbook::Chefignore.new(repo_path) + loader = Cookbook::CookbookVersionLoader.new(cookbook_path, @chefignores[repo_path]) + add_cookbook_loader(loader) + end + + def all_directories_in_repo_paths + @all_directories_in_repo_paths ||= + all_files_in_repo_paths.select { |path| File.directory?(path) } + end + + def all_files_in_repo_paths + @all_files_in_repo_paths ||= + begin + @repo_paths.inject([]) do |all_children, repo_path| + all_children += Dir[File.join(repo_path, "*")] + end + end + end + + def add_cookbook_loader(loader) + cookbook_name = loader.cookbook_name + + @loaders_by_name[cookbook_name.to_s] ||= [] + @loaders_by_name[cookbook_name.to_s] << loader + loader + end + + def cookbook_loaders_for(cookbook_name) + @loaders_by_name[cookbook_name.to_s] + end + end end diff --git a/lib/chef/cookbook_version.rb b/lib/chef/cookbook_version.rb index 4482f778c1..76e6d152b2 100644 --- a/lib/chef/cookbook_version.rb +++ b/lib/chef/cookbook_version.rb @@ -51,10 +51,14 @@ class Chef attr_accessor :provider_filenames attr_accessor :root_filenames attr_accessor :name - attr_accessor :metadata attr_accessor :metadata_filenames attr_accessor :status + # A Chef::Cookbook::Metadata object. It has a setter that fixes up the + # metadata to add descriptions of the recipes contained in this + # CookbookVersion. + attr_reader :metadata + # attribute_filenames also has a setter that has non-default # functionality. attr_reader :attribute_filenames @@ -195,6 +199,12 @@ class Chef attribute_filenames end + def metadata=(metadata) + @metadata = metadata + @metadata.recipes_from_cookbook_version(self) + @metadata + end + ## BACKCOMPAT/DEPRECATED - Remove these and fix breakage before release [DAN - 5/20/2010]## alias :attribute_files :attribute_filenames alias :attribute_files= :attribute_filenames= diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index bd6d887884..194c758f37 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -132,6 +132,8 @@ class Chef # Version constraints are not allowed in chef-solo class IllegalVersionConstraint < NotImplementedError; end + class MetadataNotValid < StandardError; end + # File operation attempted but no permissions to perform it class InsufficientPermissions < RuntimeError; end diff --git a/pedant.gemfile b/pedant.gemfile index 2ce3ed27fb..7b135bdef4 100644 --- a/pedant.gemfile +++ b/pedant.gemfile @@ -2,7 +2,7 @@ source "https://rubygems.org" gemspec :name => "chef" gem 'rest-client', :github => 'opscode/rest-client', :branch => 'lcg/1.6.7-version-lying' -gem 'chef-pedant', :github => 'opscode/chef-pedant', :ref => 'd12864964a18994b467908daa91811eee1f1bf68' +gem 'chef-pedant', :github => 'opscode/chef-pedant', :branch => "metadata-name-fix" # TODO figure out how to grab this stuff from the main Gemfile gem "activesupport", "< 4.0.0", :group => :compat_testing, :platform => "ruby" diff --git a/spec/data/cookbooks/angrybash/metadata.rb b/spec/data/cookbooks/angrybash/metadata.rb new file mode 100644 index 0000000000..720b8085be --- /dev/null +++ b/spec/data/cookbooks/angrybash/metadata.rb @@ -0,0 +1,2 @@ +name "angrybash" +version "1.0.0" diff --git a/spec/data/cookbooks/apache2/metadata.rb b/spec/data/cookbooks/apache2/metadata.rb new file mode 100644 index 0000000000..1273d20d79 --- /dev/null +++ b/spec/data/cookbooks/apache2/metadata.rb @@ -0,0 +1,2 @@ +name "apache2" +version "0.0.1" diff --git a/spec/data/cookbooks/borken/metadata.rb b/spec/data/cookbooks/borken/metadata.rb new file mode 100644 index 0000000000..58700b2d8e --- /dev/null +++ b/spec/data/cookbooks/borken/metadata.rb @@ -0,0 +1,2 @@ +name "borken" +version "1.0.0" diff --git a/spec/data/cookbooks/ignorken/metadata.rb b/spec/data/cookbooks/ignorken/metadata.rb new file mode 100644 index 0000000000..f91d92de32 --- /dev/null +++ b/spec/data/cookbooks/ignorken/metadata.rb @@ -0,0 +1,2 @@ +name "ignorken" +version "1.0.0" diff --git a/spec/data/cookbooks/java/metadata.rb b/spec/data/cookbooks/java/metadata.rb new file mode 100644 index 0000000000..7c5585304c --- /dev/null +++ b/spec/data/cookbooks/java/metadata.rb @@ -0,0 +1,2 @@ +name "java" +version "0.0.1" diff --git a/spec/data/cookbooks/name-mismatch-versionnumber/README.md b/spec/data/cookbooks/name-mismatch-versionnumber/README.md new file mode 100644 index 0000000000..a61dc9a390 --- /dev/null +++ b/spec/data/cookbooks/name-mismatch-versionnumber/README.md @@ -0,0 +1,4 @@ +# name-mismatch + +TODO: Enter the cookbook description here. + diff --git a/spec/data/cookbooks/name-mismatch-versionnumber/metadata.rb b/spec/data/cookbooks/name-mismatch-versionnumber/metadata.rb new file mode 100644 index 0000000000..81775bdcc8 --- /dev/null +++ b/spec/data/cookbooks/name-mismatch-versionnumber/metadata.rb @@ -0,0 +1,8 @@ +name 'name-mismatch' +maintainer '' +maintainer_email '' +license '' +description 'Installs/Configures name-mismatch' +long_description 'Installs/Configures name-mismatch' +version '0.1.0' + diff --git a/spec/data/cookbooks/name-mismatch-versionnumber/recipes/default.rb b/spec/data/cookbooks/name-mismatch-versionnumber/recipes/default.rb new file mode 100644 index 0000000000..01d302f043 --- /dev/null +++ b/spec/data/cookbooks/name-mismatch-versionnumber/recipes/default.rb @@ -0,0 +1,8 @@ +# +# Cookbook Name:: name-mismatch +# Recipe:: default +# +# Copyright (C) 2014 +# +# +# diff --git a/spec/data/cookbooks/preseed/metadata.rb b/spec/data/cookbooks/preseed/metadata.rb new file mode 100644 index 0000000000..a48deec08b --- /dev/null +++ b/spec/data/cookbooks/preseed/metadata.rb @@ -0,0 +1,2 @@ +name "preseed" +version "1.0.0" diff --git a/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/README.md b/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/README.md new file mode 100644 index 0000000000..3ac2a4f90c --- /dev/null +++ b/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/README.md @@ -0,0 +1,4 @@ +# incomplete-metadata + +TODO: Enter the cookbook description here. + diff --git a/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/metadata.rb b/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/metadata.rb new file mode 100644 index 0000000000..50284be3dd --- /dev/null +++ b/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/metadata.rb @@ -0,0 +1,13 @@ +# ## INCOMPLETE METADATA ## +# This cookbook is invalid b/c it does not set the `name' of the cookbook. + +# Commented out for illustrative purposes +# name 'incomplete-metadata' + +maintainer '' +maintainer_email '' +license '' +description 'Installs/Configures incomplete-metadata' +long_description 'Installs/Configures incomplete-metadata' +version '0.1.0' + diff --git a/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/recipes/default.rb b/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/recipes/default.rb new file mode 100644 index 0000000000..65ae049ce8 --- /dev/null +++ b/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/recipes/default.rb @@ -0,0 +1,8 @@ +# +# Cookbook Name:: incomplete-metadata +# Recipe:: default +# +# Copyright (C) 2014 +# +# +# diff --git a/spec/data/invalid-metadata-chef-repo/invalid-metadata/README.md b/spec/data/invalid-metadata-chef-repo/invalid-metadata/README.md new file mode 100644 index 0000000000..a41867f17c --- /dev/null +++ b/spec/data/invalid-metadata-chef-repo/invalid-metadata/README.md @@ -0,0 +1,4 @@ +# invalid-metadata + +TODO: Enter the cookbook description here. + diff --git a/spec/data/invalid-metadata-chef-repo/invalid-metadata/metadata.rb b/spec/data/invalid-metadata-chef-repo/invalid-metadata/metadata.rb new file mode 100644 index 0000000000..f548ad1dd8 --- /dev/null +++ b/spec/data/invalid-metadata-chef-repo/invalid-metadata/metadata.rb @@ -0,0 +1,10 @@ +raise "THIS METADATA HAS A BUG" + +name 'invalid-metadata' +maintainer '' +maintainer_email '' +license '' +description 'Installs/Configures invalid-metadata' +long_description 'Installs/Configures invalid-metadata' +version '0.1.0' + diff --git a/spec/data/invalid-metadata-chef-repo/invalid-metadata/recipes/default.rb b/spec/data/invalid-metadata-chef-repo/invalid-metadata/recipes/default.rb new file mode 100644 index 0000000000..1411b9e39f --- /dev/null +++ b/spec/data/invalid-metadata-chef-repo/invalid-metadata/recipes/default.rb @@ -0,0 +1,8 @@ +# +# Cookbook Name:: invalid-metadata +# Recipe:: default +# +# Copyright (C) 2014 +# +# +# diff --git a/spec/integration/knife/chef_fs_data_store_spec.rb b/spec/integration/knife/chef_fs_data_store_spec.rb index da1c72bd0c..c6737e08cb 100644 --- a/spec/integration/knife/chef_fs_data_store_spec.rb +++ b/spec/integration/knife/chef_fs_data_store_spec.rb @@ -26,10 +26,13 @@ describe 'ChefFSDataStore tests' do include IntegrationSupport include KnifeSupport + let(:cookbook_x_100_metadata_rb) { cb_metadata("x", "1.0.0") } + let(:cookbook_z_100_metadata_rb) { cb_metadata("z", "1.0.0") } + when_the_repository "has one of each thing" do before do file 'clients/x.json', {} - file 'cookbooks/x/metadata.rb', 'version "1.0.0"' + file 'cookbooks/x/metadata.rb', cookbook_x_100_metadata_rb file 'data_bags/x/y.json', {} file 'environments/x.json', {} file 'nodes/x.json', {} @@ -108,7 +111,7 @@ EOM end it 'knife show -z /cookbooks/x/metadata.rb works' do - knife('show -z /cookbooks/x/metadata.rb').should_succeed "/cookbooks/x/metadata.rb:\nversion \"1.0.0\"\n" + knife('show -z /cookbooks/x/metadata.rb').should_succeed "/cookbooks/x/metadata.rb:\n#{cookbook_x_100_metadata_rb}\n" end it 'knife show -z /data_bags/x/y.json works' do @@ -136,7 +139,7 @@ EOM before do file 'empty.json', {} file 'rolestuff.json', '{"description":"hi there","name":"x"}' - file 'cookbooks_to_upload/x/metadata.rb', "version '1.0.0'\n\n" + file 'cookbooks_to_upload/x/metadata.rb', cookbook_x_100_metadata_rb end it 'knife raw -z -i empty.json -m PUT /clients/x' do @@ -196,7 +199,7 @@ EOM file 'empty_x.json', { 'name' => 'x' } file 'empty_id.json', { 'id' => 'z' } file 'rolestuff.json', '{"description":"hi there","name":"x"}' - file 'cookbooks_to_upload/z/metadata.rb', "version '1.0.0'" + file 'cookbooks_to_upload/z/metadata.rb', cookbook_z_100_metadata_rb end it 'knife raw -z -i empty.json -m POST /clients' do diff --git a/spec/integration/knife/cookbook_api_ipv6_spec.rb b/spec/integration/knife/cookbook_api_ipv6_spec.rb index ac2538dc38..b3efae6cd3 100644 --- a/spec/integration/knife/cookbook_api_ipv6_spec.rb +++ b/spec/integration/knife/cookbook_api_ipv6_spec.rb @@ -101,7 +101,7 @@ END_CLIENT_RB it "downloads the cookbook" do shell_out!("knife cookbook download apache2 #{knife_config_flag} -d #{cache_path}", :cwd => chef_dir) - Dir["#{cache_path}/*"].map {|entry| File.basename(entry)}.should include("apache2-0.0.0") + Dir["#{cache_path}/*"].map {|entry| File.basename(entry)}.should include("apache2-0.0.1") end end diff --git a/spec/integration/solo/solo_spec.rb b/spec/integration/solo/solo_spec.rb index b10ff379e2..793789b754 100644 --- a/spec/integration/solo/solo_spec.rb +++ b/spec/integration/solo/solo_spec.rb @@ -11,9 +11,13 @@ describe "chef-solo" do let(:chef_dir) { File.join(File.dirname(__FILE__), "..", "..", "..") } + let(:cookbook_x_100_metadata_rb) { cb_metadata("x", "1.0.0") } + + let(:cookbook_ancient_100_metadata_rb) { cb_metadata("ancient", "1.0.0") } + when_the_repository "has a cookbook with a basic recipe" do before do - file 'cookbooks/x/metadata.rb', 'version "1.0.0"' + file 'cookbooks/x/metadata.rb', cookbook_x_100_metadata_rb file 'cookbooks/x/recipes/default.rb', 'puts "ITWORKS"' end @@ -46,10 +50,10 @@ E when_the_repository "has a cookbook with an undeclared dependency" do before do - file 'cookbooks/x/metadata.rb', 'version "1.0.0"' + file 'cookbooks/x/metadata.rb', cookbook_x_100_metadata_rb file 'cookbooks/x/recipes/default.rb', 'include_recipe "ancient::aliens"' - file 'cookbooks/ancient/metadata.rb', 'version "1.0.0"' + file 'cookbooks/ancient/metadata.rb', cookbook_ancient_100_metadata_rb file 'cookbooks/ancient/recipes/aliens.rb', 'print "it was aliens"' end @@ -69,7 +73,7 @@ EOM before do directory 'logs' file 'logs/runs.log', '' - file 'cookbooks/x/metadata.rb', 'version "1.0.0"' + file 'cookbooks/x/metadata.rb', cookbook_x_100_metadata_rb file 'cookbooks/x/recipes/default.rb', <<EOM ruby_block "sleeping" do block do diff --git a/spec/unit/cookbook/cookbook_version_loader_spec.rb b/spec/unit/cookbook/cookbook_version_loader_spec.rb new file mode 100644 index 0000000000..ad10f24573 --- /dev/null +++ b/spec/unit/cookbook/cookbook_version_loader_spec.rb @@ -0,0 +1,179 @@ +# +# Author:: Daniel DeLeo (<dan@getchef.com>) +# Copyright:: Copyright (c) 2014 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' + +describe Chef::Cookbook::CookbookVersionLoader do + + describe "loading a cookbook" do + + let(:chefignore) { nil } + + let(:cookbook_path) { File.join(CHEF_SPEC_DATA, "cookbooks/openldap") } + + let(:cookbook_loader) { Chef::Cookbook::CookbookVersionLoader.new(cookbook_path, chefignore) } + + let(:loaded_cookbook) do + cookbook_loader.load! + cookbook_loader.cookbook_version + end + + def full_path(cookbook_relative_path) + File.join(cookbook_path, cookbook_relative_path) + end + + it "loads attribute files of the cookbook" do + expect(loaded_cookbook.attribute_filenames).to include(full_path("/attributes/default.rb")) + expect(loaded_cookbook.attribute_filenames).to include(full_path("/attributes/smokey.rb")) + end + + it "loads definition files" do + expect(loaded_cookbook.definition_filenames).to include(full_path("/definitions/client.rb")) + expect(loaded_cookbook.definition_filenames).to include(full_path("/definitions/server.rb")) + end + + it "loads recipes" do + expect(loaded_cookbook.recipe_filenames).to include(full_path("/recipes/default.rb")) + expect(loaded_cookbook.recipe_filenames).to include(full_path("/recipes/gigantor.rb")) + expect(loaded_cookbook.recipe_filenames).to include(full_path("/recipes/one.rb")) + expect(loaded_cookbook.recipe_filenames).to include(full_path("/recipes/return.rb")) + end + + it "loads static files in the files/ dir" do + expect(loaded_cookbook.file_filenames).to include(full_path("/files/default/remotedir/remotesubdir/remote_subdir_file1.txt")) + expect(loaded_cookbook.file_filenames).to include(full_path("/files/default/remotedir/remotesubdir/remote_subdir_file2.txt")) + end + + it "loads files that start with a ." do + expect(loaded_cookbook.file_filenames).to include(full_path("/files/default/.dotfile")) + expect(loaded_cookbook.file_filenames).to include(full_path("/files/default/.ssh/id_rsa")) + expect(loaded_cookbook.file_filenames).to include(full_path("/files/default/remotedir/.a_dotdir/.a_dotfile_in_a_dotdir")) + end + + it "should load the metadata for the cookbook" do + loaded_cookbook.metadata.name.to_s.should == "openldap" + loaded_cookbook.metadata.should be_a_kind_of(Chef::Cookbook::Metadata) + end + + context "when a cookbook has ignored files" do + + let(:chefignore) { Chef::Cookbook::Chefignore.new(File.join(CHEF_SPEC_DATA, "cookbooks")) } + + let(:cookbook_path) { File.join(CHEF_SPEC_DATA, "kitchen/openldap") } + + it "skips ignored files" do + expect(loaded_cookbook.recipe_filenames).to include(full_path("recipes/gigantor.rb")) + expect(loaded_cookbook.recipe_filenames).to include(full_path("recipes/woot.rb")) + expect(loaded_cookbook.recipe_filenames).to_not include(full_path("recipes/ignoreme.rb")) + end + + end + + context "when the given path is not actually a cookbook" do + + let(:cookbook_path) { File.join(CHEF_SPEC_DATA, "cookbooks/NOTHING_HERE_FOLKS") } + + it "raises an error when loading with #load!" do + expect { cookbook_loader.load! }.to raise_error(Chef::Exceptions::CookbookNotFoundInRepo) + end + + it "skips the cookbook when called with #load" do + expect { cookbook_loader.load }.to_not raise_error + end + + end + + context "when a cookbook has a metadata name different than directory basename" do + + let(:cookbook_path) { File.join(CHEF_SPEC_DATA, "cookbooks/name-mismatch-versionnumber") } + + it "prefers the metadata name to the directory basename" do + expect(loaded_cookbook.name).to eq(:"name-mismatch") + end + + end + + context "when a cookbook has a metadata file with a ruby error [CHEF-2923]" do + + let(:cookbook_path) { File.join(CHEF_SPEC_DATA, "invalid-metadata-chef-repo/invalid-metadata") } + + it "raises an error when loading with #load!" do + expect { cookbook_loader.load! }.to raise_error("THIS METADATA HAS A BUG") + end + + it "raises an error when called with #load" do + expect { cookbook_loader.load }.to raise_error("THIS METADATA HAS A BUG") + end + + it "doesn't raise an error when determining the cookbook name" do + expect { cookbook_loader.cookbook_name }.to_not raise_error + end + + it "doesn't raise an error when metadata is first generated" do + expect { cookbook_loader.metadata }.to_not raise_error + end + + it "sets an error flag containing error information" do + cookbook_loader.metadata + expect(cookbook_loader.metadata_error).to be_a(StandardError) + expect(cookbook_loader.metadata_error.message).to eq("THIS METADATA HAS A BUG") + end + + end + + context "when a cookbook has a metadata file with invalid metadata" do + + let(:cookbook_path) { File.join(CHEF_SPEC_DATA, "incomplete-metadata-chef-repo/incomplete-metadata") } + + let(:error_message) do + "Cookbook loaded at path(s) [#{cookbook_path}] has invalid metadata: The `name' attribute is required in cookbook metadata" + end + + it "raises an error when loading with #load!" do + expect { cookbook_loader.load! }.to raise_error(Chef::Exceptions::MetadataNotValid, error_message) + end + + it "raises an error when called with #load" do + expect { cookbook_loader.load }.to raise_error(Chef::Exceptions::MetadataNotValid, error_message) + end + + it "uses the inferred cookbook name [CHEF-2923]" do + # This behavior is intended to support the CHEF-2923 feature where + # invalid metadata doesn't prevent you from uploading other cookbooks. + # + # The metadata is the definitive source of the cookbook name, but if + # the metadata is incomplete/invalid, we can't read the name from it. + # + # The CookbookLoader stores CookbookVersionLoaders in a Hash with + # cookbook names as the keys, and finds the loader in this Hash to call + # #load on it when the user runs a command like `knife cookbook upload specific-cookbook` + # + # Most of the time this will work, but if the user tries to upload a + # specific cookbook by name, has customized that cookbook's name (so it + # doesn't match the inferred name), and that metadata file has a syntax + # error, we might report a "cookbook not found" error instead of the + # metadata syntax error that is the actual cause. + expect(cookbook_loader.cookbook_name).to eq(:"incomplete-metadata") + end + + end + + end + +end + diff --git a/spec/unit/cookbook/metadata_spec.rb b/spec/unit/cookbook/metadata_spec.rb index 8e98610183..e61c85b42b 100644 --- a/spec/unit/cookbook/metadata_spec.rb +++ b/spec/unit/cookbook/metadata_spec.rb @@ -21,10 +21,8 @@ require 'spec_helper' require 'chef/cookbook/metadata' describe Chef::Cookbook::Metadata do - before(:each) do - @cookbook = Chef::CookbookVersion.new('test_cookbook') - @meta = Chef::Cookbook::Metadata.new(@cookbook) - end + + let(:metadata) { Chef::Cookbook::Metadata.new } describe "when comparing for equality" do before do @@ -35,7 +33,7 @@ describe Chef::Cookbook::Metadata do end it "does not depend on object identity for equality" do - @meta.should == @meta.dup + metadata.should == metadata.dup end it "is not equal to another object if it isn't have all of the metadata fields" do @@ -45,7 +43,7 @@ describe Chef::Cookbook::Metadata do almost_duck_type = Struct.new(*fields_to_include).new @fields.each do |field| setter = "#{field}=" - metadata_value = @meta.send(field) + metadata_value = metadata.send(field) almost_duck_type.send(setter, metadata_value) if almost_duck_type.respond_to?(setter) @mets.should_not == almost_duck_type end @@ -56,10 +54,10 @@ describe Chef::Cookbook::Metadata do duck_type = Struct.new(*@fields).new @fields.each do |field| setter = "#{field}=" - metadata_value = @meta.send(field) + metadata_value = metadata.send(field) duck_type.send(setter, metadata_value) end - @meta.should == duck_type + metadata.should == duck_type end it "is not equal if any values are different" do @@ -69,78 +67,117 @@ describe Chef::Cookbook::Metadata do @fields.each do |field| setter = "#{field}=" - metadata_value = @meta.send(field) + metadata_value = metadata.send(field) duck_type.send(setter, metadata_value) end - field_to_change - duck_type.send("#{field_to_change}=".to_sym, :epic_fail) - @meta.should_not == duck_type + metadata.should_not == duck_type end end end describe "when first created" do - it "should return a Chef::Cookbook::Metadata object" do - @meta.should be_a_kind_of(Chef::Cookbook::Metadata) + + it "has no name" do + metadata.name.should eq(nil) end - it "should allow a cookbook as the first argument" do - lambda { Chef::Cookbook::Metadata.new(@cookbook) }.should_not raise_error + it "has an empty description" do + metadata.description.should eq("") end - it "should allow an maintainer name for the second argument" do - lambda { Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown') }.should_not raise_error + it "has an empty long description" do + metadata.long_description.should eq("") end - it "should set the maintainer name from the second argument" do - md = Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown') - md.maintainer.should == 'Bobo T. Clown' + it "defaults to 'all rights reserved' license" do + metadata.license.should eq("All rights reserved") end - it "should allow an maintainer email for the third argument" do - lambda { Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown', 'bobo@clown.co') }.should_not raise_error + it "has an empty maintainer field" do + metadata.maintainer.should eq(nil) end - it "should set the maintainer email from the third argument" do - md = Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown', 'bobo@clown.co') - md.maintainer_email.should == 'bobo@clown.co' + it "has an empty maintainer_email field" do + metadata.maintainer.should eq(nil) end - it "should allow a license for the fourth argument" do - lambda { Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown', 'bobo@clown.co', 'Clown License v1') }.should_not raise_error + it "has an empty platforms list" do + metadata.platforms.should eq(Mash.new) end - it "should set the license from the fourth argument" do - md = Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown', 'bobo@clown.co', 'Clown License v1') - md.license.should == 'Clown License v1' + it "has an empty dependencies list" do + metadata.dependencies.should eq(Mash.new) end - end - describe "cookbook" do - it "should return the cookbook we were initialized with" do - @meta.cookbook.should eql(@cookbook) + it "has an empty recommends list" do + metadata.recommendations.should eq(Mash.new) end - end - describe "name" do - it "should return the name of the cookbook" do - @meta.name.should eql(@cookbook.name) + it "has an empty suggestions list" do + metadata.suggestions.should eq(Mash.new) end + + it "has an empty conflicts list" do + metadata.conflicting.should eq(Mash.new) + end + + it "has an empty replaces list" do + metadata.replacing.should eq(Mash.new) + end + + it "has an empty attributes list" do + metadata.attributes.should eq(Mash.new) + end + + it "has an empty groupings list" do + metadata.groupings.should eq(Mash.new) + end + + it "has an empty recipes list" do + metadata.recipes.should eq(Mash.new) + end + end - describe "platforms" do - it "should return the current platform hash" do - @meta.platforms.should be_a_kind_of(Hash) + describe "validation" do + + context "when no required fields are set" do + + it "is not valid" do + metadata.should_not be_valid + end + + it "has a list of validation errors" do + expected_errors = ["The `name' attribute is required in cookbook metadata"] + metadata.errors.should eq(expected_errors) + end + end + + context "when all required fields are set" do + before do + metadata.name "a-valid-name" + end + + it "is valid" do + metadata.should be_valid + end + + it "has no validation errors" do + metadata.errors.should be_empty + end + + end + end describe "adding a supported platform" do it "should support adding a supported platform with a single expression" do - @meta.supports("ubuntu", ">= 8.04") - @meta.platforms["ubuntu"].should == '>= 8.04' + metadata.supports("ubuntu", ">= 8.04") + metadata.platforms["ubuntu"].should == '>= 8.04' end end @@ -156,23 +193,23 @@ describe Chef::Cookbook::Metadata do params.sort { |a,b| a.to_s <=> b.to_s }.each do |field, field_value| describe field do it "should be set-able via #{field}" do - @meta.send(field, field_value).should eql(field_value) + metadata.send(field, field_value).should eql(field_value) end it "should be get-able via #{field}" do - @meta.send(field, field_value) - @meta.send(field).should eql(field_value) + metadata.send(field, field_value) + metadata.send(field).should eql(field_value) end end end describe "version transformation" do it "should transform an '0.6' version to '0.6.0'" do - @meta.send(:version, "0.6").should eql("0.6.0") + metadata.send(:version, "0.6").should eql("0.6.0") end it "should spit out '0.6.0' after transforming '0.6'" do - @meta.send(:version, "0.6") - @meta.send(:version).should eql("0.6.0") + metadata.send(:version, "0.6") + metadata.send(:version).should eql("0.6.0") end end end @@ -191,11 +228,11 @@ describe Chef::Cookbook::Metadata do check_with = dep_args.shift describe dep do it "should be set-able via #{dep}" do - @meta.send(dep, *dep_args).should == dep_args[1] + metadata.send(dep, *dep_args).should == dep_args[1] end it "should be get-able via #{check_with}" do - @meta.send(dep, *dep_args) - @meta.send(check_with).should == { dep_args[0] => dep_args[1] } + metadata.send(dep, *dep_args) + metadata.send(check_with).should == { dep_args[0] => dep_args[1] } end end end @@ -213,11 +250,11 @@ describe Chef::Cookbook::Metadata do normalized_version = dep_args.pop describe dep do it "should be set-able and normalized via #{dep}" do - @meta.send(dep, *dep_args).should == normalized_version + metadata.send(dep, *dep_args).should == normalized_version end it "should be get-able and normalized via #{check_with}" do - @meta.send(dep, *dep_args) - @meta.send(check_with).should == { dep_args[0] => normalized_version } + metadata.send(dep, *dep_args) + metadata.send(check_with).should == { dep_args[0] => normalized_version } end end end @@ -235,7 +272,7 @@ describe Chef::Cookbook::Metadata do dep_types.each do |dep, dep_args| it "for #{dep} raises an informative error instead of vomiting on your shoes" do - lambda {@meta.send(dep, *dep_args)}.should raise_error(Chef::Exceptions::ObsoleteDependencySyntax) + lambda {metadata.send(dep, *dep_args)}.should raise_error(Chef::Exceptions::ObsoleteDependencySyntax) end end end @@ -253,7 +290,7 @@ describe Chef::Cookbook::Metadata do dep_types.each do |dep, dep_args| it "for #{dep} raises an informative error instead of vomiting on your shoes" do - lambda {@meta.send(dep, *dep_args)}.should raise_error(Chef::Exceptions::InvalidVersionConstraint) + lambda {metadata.send(dep, *dep_args)}.should raise_error(Chef::Exceptions::InvalidVersionConstraint) end end end @@ -265,23 +302,23 @@ describe Chef::Cookbook::Metadata do "title" => "MySQL Tuning", "description" => "Setting from the my.cnf file that allow you to tune your mysql server" } - @meta.grouping("/db/mysql/databases/tuning", group).should == group + metadata.grouping("/db/mysql/databases/tuning", group).should == group end it "should not accept anything but a string for display_name" do lambda { - @meta.grouping("db/mysql/databases", :title => "foo") + metadata.grouping("db/mysql/databases", :title => "foo") }.should_not raise_error lambda { - @meta.grouping("db/mysql/databases", :title => Hash.new) + metadata.grouping("db/mysql/databases", :title => Hash.new) }.should raise_error(ArgumentError) end it "should not accept anything but a string for the description" do lambda { - @meta.grouping("db/mysql/databases", :description => "foo") + metadata.grouping("db/mysql/databases", :description => "foo") }.should_not raise_error lambda { - @meta.grouping("db/mysql/databases", :description => Hash.new) + metadata.grouping("db/mysql/databases", :description => Hash.new) }.should raise_error(ArgumentError) end end @@ -298,151 +335,151 @@ describe Chef::Cookbook::Metadata do "recipes" => [ "mysql::server", "mysql::master" ], "default" => [ ] } - @meta.attribute("/db/mysql/databases", attrs).should == attrs + metadata.attribute("/db/mysql/databases", attrs).should == attrs end it "should not accept anything but a string for display_name" do lambda { - @meta.attribute("db/mysql/databases", :display_name => "foo") + metadata.attribute("db/mysql/databases", :display_name => "foo") }.should_not raise_error lambda { - @meta.attribute("db/mysql/databases", :display_name => Hash.new) + metadata.attribute("db/mysql/databases", :display_name => Hash.new) }.should raise_error(ArgumentError) end it "should not accept anything but a string for the description" do lambda { - @meta.attribute("db/mysql/databases", :description => "foo") + metadata.attribute("db/mysql/databases", :description => "foo") }.should_not raise_error lambda { - @meta.attribute("db/mysql/databases", :description => Hash.new) + metadata.attribute("db/mysql/databases", :description => Hash.new) }.should raise_error(ArgumentError) end it "should not accept anything but an array of strings for choice" do lambda { - @meta.attribute("db/mysql/databases", :choice => ['dedicated', 'shared']) + metadata.attribute("db/mysql/databases", :choice => ['dedicated', 'shared']) }.should_not raise_error lambda { - @meta.attribute("db/mysql/databases", :choice => [10, 'shared']) + metadata.attribute("db/mysql/databases", :choice => [10, 'shared']) }.should raise_error(ArgumentError) lambda { - @meta.attribute("db/mysql/databases", :choice => Hash.new) + metadata.attribute("db/mysql/databases", :choice => Hash.new) }.should raise_error(ArgumentError) end it "should set choice to empty array by default" do - @meta.attribute("db/mysql/databases", {}) - @meta.attributes["db/mysql/databases"][:choice].should == [] + metadata.attribute("db/mysql/databases", {}) + metadata.attributes["db/mysql/databases"][:choice].should == [] end it "should let calculated be true or false" do lambda { - @meta.attribute("db/mysql/databases", :calculated => true) + metadata.attribute("db/mysql/databases", :calculated => true) }.should_not raise_error lambda { - @meta.attribute("db/mysql/databases", :calculated => false) + metadata.attribute("db/mysql/databases", :calculated => false) }.should_not raise_error lambda { - @meta.attribute("db/mysql/databases", :calculated => Hash.new) + metadata.attribute("db/mysql/databases", :calculated => Hash.new) }.should raise_error(ArgumentError) end it "should set calculated to false by default" do - @meta.attribute("db/mysql/databases", {}) - @meta.attributes["db/mysql/databases"][:calculated].should == false + metadata.attribute("db/mysql/databases", {}) + metadata.attributes["db/mysql/databases"][:calculated].should == false end it "accepts String for the attribute type" do lambda { - @meta.attribute("db/mysql/databases", :type => "string") + metadata.attribute("db/mysql/databases", :type => "string") }.should_not raise_error end it "accepts Array for the attribute type" do lambda { - @meta.attribute("db/mysql/databases", :type => "array") + metadata.attribute("db/mysql/databases", :type => "array") }.should_not raise_error lambda { - @meta.attribute("db/mysql/databases", :type => Array.new) + metadata.attribute("db/mysql/databases", :type => Array.new) }.should raise_error(ArgumentError) end it "accepts symbol for the attribute type" do lambda { - @meta.attribute("db/mysql/databases", :type => "symbol") + metadata.attribute("db/mysql/databases", :type => "symbol") }.should_not raise_error end it "should let type be hash (backwards compatability only)" do lambda { - @meta.attribute("db/mysql/databases", :type => "hash") + metadata.attribute("db/mysql/databases", :type => "hash") }.should_not raise_error end it "should let required be required, recommended or optional" do lambda { - @meta.attribute("db/mysql/databases", :required => 'required') + metadata.attribute("db/mysql/databases", :required => 'required') }.should_not raise_error lambda { - @meta.attribute("db/mysql/databases", :required => 'recommended') + metadata.attribute("db/mysql/databases", :required => 'recommended') }.should_not raise_error lambda { - @meta.attribute("db/mysql/databases", :required => 'optional') + metadata.attribute("db/mysql/databases", :required => 'optional') }.should_not raise_error end it "should convert required true to required" do lambda { - @meta.attribute("db/mysql/databases", :required => true) + metadata.attribute("db/mysql/databases", :required => true) }.should_not raise_error - #attrib = @meta.attributes["db/mysql/databases"][:required].should == "required" + #attrib = metadata.attributes["db/mysql/databases"][:required].should == "required" end it "should convert required false to optional" do lambda { - @meta.attribute("db/mysql/databases", :required => false) + metadata.attribute("db/mysql/databases", :required => false) }.should_not raise_error - #attrib = @meta.attributes["db/mysql/databases"][:required].should == "optional" + #attrib = metadata.attributes["db/mysql/databases"][:required].should == "optional" end it "should set required to 'optional' by default" do - @meta.attribute("db/mysql/databases", {}) - @meta.attributes["db/mysql/databases"][:required].should == 'optional' + metadata.attribute("db/mysql/databases", {}) + metadata.attributes["db/mysql/databases"][:required].should == 'optional' end it "should make sure recipes is an array" do lambda { - @meta.attribute("db/mysql/databases", :recipes => []) + metadata.attribute("db/mysql/databases", :recipes => []) }.should_not raise_error lambda { - @meta.attribute("db/mysql/databases", :required => Hash.new) + metadata.attribute("db/mysql/databases", :required => Hash.new) }.should raise_error(ArgumentError) end it "should set recipes to an empty array by default" do - @meta.attribute("db/mysql/databases", {}) - @meta.attributes["db/mysql/databases"][:recipes].should == [] + metadata.attribute("db/mysql/databases", {}) + metadata.attributes["db/mysql/databases"][:recipes].should == [] end it "should allow the default value to be a string, array, hash, boolean or numeric" do lambda { - @meta.attribute("db/mysql/databases", :default => []) + metadata.attribute("db/mysql/databases", :default => []) }.should_not raise_error lambda { - @meta.attribute("db/mysql/databases", :default => {}) + metadata.attribute("db/mysql/databases", :default => {}) }.should_not raise_error lambda { - @meta.attribute("db/mysql/databases", :default => "alice in chains") + metadata.attribute("db/mysql/databases", :default => "alice in chains") }.should_not raise_error lambda { - @meta.attribute("db/mysql/databases", :default => 1337) + metadata.attribute("db/mysql/databases", :default => 1337) }.should_not raise_error lambda { - @meta.attribute("db/mysql/databases", :default => true) + metadata.attribute("db/mysql/databases", :default => true) }.should_not raise_error lambda { - @meta.attribute("db/mysql/databases", :required => :not_gonna_do_it) + metadata.attribute("db/mysql/databases", :required => :not_gonna_do_it) }.should raise_error(ArgumentError) end @@ -453,16 +490,16 @@ describe Chef::Cookbook::Metadata do :default => "test1" } lambda { - @meta.attribute("test_cookbook/test", options) + metadata.attribute("test_cookbook/test", options) }.should_not raise_error - + options = { :type => "boolean", :choice => [ true, false ], :default => true } lambda { - @meta.attribute("test_cookbook/test", options) + metadata.attribute("test_cookbook/test", options) }.should_not raise_error options = { @@ -471,7 +508,7 @@ describe Chef::Cookbook::Metadata do :default => 1337 } lambda { - @meta.attribute("test_cookbook/test", options) + metadata.attribute("test_cookbook/test", options) }.should_not raise_error options = { @@ -480,7 +517,7 @@ describe Chef::Cookbook::Metadata do :default => false } lambda { - @meta.attribute("test_cookbook/test", options) + metadata.attribute("test_cookbook/test", options) }.should raise_error end @@ -490,14 +527,14 @@ describe Chef::Cookbook::Metadata do :calculated => true, :default => [ "I thought you said calculated" ] } - @meta.attribute("db/mysql/databases", attrs) + metadata.attribute("db/mysql/databases", attrs) }.should raise_error(ArgumentError) lambda { attrs = { :calculated => true, :default => "I thought you said calculated" } - @meta.attribute("db/mysql/databases", attrs) + metadata.attribute("db/mysql/databases", attrs) }.should raise_error(ArgumentError) end @@ -507,14 +544,14 @@ describe Chef::Cookbook::Metadata do :choice => [ "a", "b", "c"], :default => "b" } - @meta.attribute("db/mysql/databases", attrs) + metadata.attribute("db/mysql/databases", attrs) }.should_not raise_error lambda { attrs = { :choice => [ "a", "b", "c", "d", "e"], :default => ["b", "d"] } - @meta.attribute("db/mysql/databases", attrs) + metadata.attribute("db/mysql/databases", attrs) }.should_not raise_error end @@ -524,71 +561,74 @@ describe Chef::Cookbook::Metadata do :choice => [ "a", "b", "c"], :default => "d" } - @meta.attribute("db/mysql/databases", attrs) + metadata.attribute("db/mysql/databases", attrs) }.should raise_error(ArgumentError) lambda { attrs = { :choice => [ "a", "b", "c", "d", "e"], :default => ["b", "z"] } - @meta.attribute("db/mysql/databases", attrs) + metadata.attribute("db/mysql/databases", attrs) }.should raise_error(ArgumentError) end end describe "recipes" do + let(:cookbook) do + c = Chef::CookbookVersion.new('test_cookbook') + c.recipe_files = [ "default.rb", "enlighten.rb" ] + c + end + before(:each) do - @cookbook.recipe_files = [ "default.rb", "enlighten.rb" ] - @meta = Chef::Cookbook::Metadata.new(@cookbook) + metadata.name("test_cookbook") + metadata.recipes_from_cookbook_version(cookbook) end it "should have the names of the recipes" do - @meta.recipes["test_cookbook"].should == "" - @meta.recipes["test_cookbook::enlighten"].should == "" + metadata.recipes["test_cookbook"].should == "" + metadata.recipes["test_cookbook::enlighten"].should == "" end it "should let you set the description for a recipe" do - @meta.recipe "test_cookbook", "It, um... tests stuff?" - @meta.recipes["test_cookbook"].should == "It, um... tests stuff?" + metadata.recipe "test_cookbook", "It, um... tests stuff?" + metadata.recipes["test_cookbook"].should == "It, um... tests stuff?" end it "should automatically provide each recipe" do - @meta.providing.has_key?("test_cookbook").should == true - @meta.providing.has_key?("test_cookbook::enlighten").should == true + metadata.providing.has_key?("test_cookbook").should == true + metadata.providing.has_key?("test_cookbook::enlighten").should == true end end describe "json" do before(:each) do - @cookbook.recipe_files = [ "default.rb", "enlighten.rb" ] - @meta = Chef::Cookbook::Metadata.new(@cookbook) - @meta.version "1.0" - @meta.maintainer "Bobo T. Clown" - @meta.maintainer_email "bobo@example.com" - @meta.long_description "I have a long arm!" - @meta.supports :ubuntu, "> 8.04" - @meta.depends "bobo", "= 1.0" - @meta.depends "bubu", "=1.0" - @meta.depends "bobotclown", "= 1.1" - @meta.recommends "snark", "< 3.0" - @meta.suggests "kindness", "> 2.0" - @meta.conflicts "hatred" - @meta.provides "foo(:bar, :baz)" - @meta.replaces "snarkitron" - @meta.recipe "test_cookbook::enlighten", "is your buddy" - @meta.attribute "bizspark/has_login", + metadata.version "1.0" + metadata.maintainer "Bobo T. Clown" + metadata.maintainer_email "bobo@example.com" + metadata.long_description "I have a long arm!" + metadata.supports :ubuntu, "> 8.04" + metadata.depends "bobo", "= 1.0" + metadata.depends "bubu", "=1.0" + metadata.depends "bobotclown", "= 1.1" + metadata.recommends "snark", "< 3.0" + metadata.suggests "kindness", "> 2.0" + metadata.conflicts "hatred" + metadata.provides "foo(:bar, :baz)" + metadata.replaces "snarkitron" + metadata.recipe "test_cookbook::enlighten", "is your buddy" + metadata.attribute "bizspark/has_login", :display_name => "You have nothing" - @meta.version "1.2.3" + metadata.version "1.2.3" end describe "serialize" do - before(:each) do - @serial = Chef::JSONCompat.from_json(@meta.to_json) - end + + let(:deserialized_metadata) { Chef::JSONCompat.from_json(metadata.to_json) } it "should serialize to a json hash" do - Chef::JSONCompat.from_json(@meta.to_json).should be_a_kind_of(Hash) + deserialized_metadata.should be_a_kind_of(Hash) end %w{ @@ -610,18 +650,18 @@ describe Chef::Cookbook::Metadata do version }.each do |t| it "should include '#{t}'" do - @serial[t].should == @meta.send(t.to_sym) + deserialized_metadata[t].should == metadata.send(t.to_sym) end end end describe "deserialize" do - before(:each) do - @deserial = Chef::Cookbook::Metadata.from_json(@meta.to_json) - end + + let(:deserialized_metadata) { Chef::Cookbook::Metadata.from_json(metadata.to_json) } + it "should deserialize to a Chef::Cookbook::Metadata object" do - @deserial.should be_a_kind_of(Chef::Cookbook::Metadata) + deserialized_metadata.should be_a_kind_of(Chef::Cookbook::Metadata) end %w{ @@ -643,14 +683,14 @@ describe Chef::Cookbook::Metadata do version }.each do |t| it "should match '#{t}'" do - @deserial.send(t.to_sym).should == @meta.send(t.to_sym) + deserialized_metadata.send(t.to_sym).should == metadata.send(t.to_sym) end end end describe "from_hash" do before(:each) do - @hash = @meta.to_hash + @hash = metadata.to_hash end [:dependencies, diff --git a/spec/unit/cookbook/syntax_check_spec.rb b/spec/unit/cookbook/syntax_check_spec.rb index 7f7a914115..b83bffe1c9 100644 --- a/spec/unit/cookbook/syntax_check_spec.rb +++ b/spec/unit/cookbook/syntax_check_spec.rb @@ -147,7 +147,8 @@ describe Chef::Cookbook::SyntaxCheck do it "does not remove the invalid file from the list of untested files" do syntax_check.untested_ruby_files.should include(File.join(cookbook_path, 'recipes', 'default.rb')) - lambda { syntax_check.validate_ruby_files }.should_not change(syntax_check, :untested_ruby_files) + syntax_check.validate_ruby_files + syntax_check.untested_ruby_files.should include(File.join(cookbook_path, 'recipes', 'default.rb')) end it "indicates that a template file has a syntax error" do @@ -166,7 +167,7 @@ describe Chef::Cookbook::SyntaxCheck do cookbook_path = File.join(CHEF_SPEC_DATA, 'cookbooks', 'ignorken') Chef::Config[:cookbook_path] = File.dirname(cookbook_path) syntax_check.cookbook_path.replace(cookbook_path) - @ruby_files = [File.join(cookbook_path, 'recipes/default.rb')] + @ruby_files = [File.join(cookbook_path, 'metadata.rb'), File.join(cookbook_path, 'recipes/default.rb')] end it "shows that ignored ruby files do not require a syntax check" do diff --git a/spec/unit/cookbook_loader_spec.rb b/spec/unit/cookbook_loader_spec.rb index 4c21c124e0..d5d585b8e1 100644 --- a/spec/unit/cookbook_loader_spec.rb +++ b/spec/unit/cookbook_loader_spec.rb @@ -19,39 +19,61 @@ require 'spec_helper' describe Chef::CookbookLoader do - before(:each) do - @repo_paths = [ File.expand_path(File.join(CHEF_SPEC_DATA, "kitchen")), - File.expand_path(File.join(CHEF_SPEC_DATA, "cookbooks")) ] - @cookbook_loader = Chef::CookbookLoader.new(@repo_paths) + + let(:repo_paths) do + [ + File.expand_path(File.join(CHEF_SPEC_DATA, "kitchen")), + File.expand_path(File.join(CHEF_SPEC_DATA, "cookbooks")) + ] end - describe "loading all cookbooks" do + let(:cookbook_loader) { Chef::CookbookLoader.new(repo_paths) } + + it "checks each directory only once when loading (CHEF-3487)" do + cookbook_paths = [] + repo_paths.each do |repo_path| + cookbook_paths |= Dir[File.join(repo_path, "*")] + end + + cookbook_paths.delete_if { |path| File.basename(path) == "chefignore" } + + cookbook_paths.each do |cookbook_path| + Chef::Cookbook::CookbookVersionLoader.should_receive(:new). + with(cookbook_path, anything). + once. + and_call_original + end + cookbook_loader.load_cookbooks + end + + + context "after loading all cookbooks" do before(:each) do - @cookbook_loader.load_cookbooks + cookbook_loader.load_cookbooks end describe "[]" do it "should return cookbook objects with []" do - @cookbook_loader[:openldap].should be_a_kind_of(Chef::CookbookVersion) + cookbook_loader[:openldap].should be_a_kind_of(Chef::CookbookVersion) end it "should raise an exception if it cannot find a cookbook with []" do - lambda { @cookbook_loader[:monkeypoop] }.should raise_error(Chef::Exceptions::CookbookNotFoundInRepo) + lambda { cookbook_loader[:monkeypoop] }.should raise_error(Chef::Exceptions::CookbookNotFoundInRepo) end it "should allow you to look up available cookbooks with [] and a symbol" do - @cookbook_loader[:openldap].name.should eql(:openldap) + cookbook_loader[:openldap].name.should eql(:openldap) end it "should allow you to look up available cookbooks with [] and a string" do - @cookbook_loader["openldap"].name.should eql(:openldap) + cookbook_loader["openldap"].name.should eql(:openldap) end end describe "each" do it "should allow you to iterate over cookbooks with each" do seen = Hash.new - @cookbook_loader.each do |cookbook_name, cookbook| + cookbook_loader.each do |cookbook_name, cookbook| seen[cookbook_name] = true end seen.should have_key("openldap") @@ -60,7 +82,7 @@ describe Chef::CookbookLoader do it "should iterate in alphabetical order" do seen = Array.new - @cookbook_loader.each do |cookbook_name, cookbook| + cookbook_loader.each do |cookbook_name, cookbook| seen << cookbook_name end seen[0].should == "angrybash" @@ -68,106 +90,111 @@ describe Chef::CookbookLoader do seen[2].should == "borken" seen[3].should == "ignorken" seen[4].should == "java" - seen[5].should == "openldap" + seen[5].should == "name-mismatch" + seen[6].should == "openldap" end end - describe "load_cookbooks" do + describe "referencing cookbook files" do it "should find all the cookbooks in the cookbook path" do - Chef::Config.cookbook_path << File.expand_path(File.join(CHEF_SPEC_DATA, "hidden-cookbooks")) - @cookbook_loader.load_cookbooks - @cookbook_loader.should have_key(:openldap) - @cookbook_loader.should have_key(:apache2) + cookbook_loader.load_cookbooks + cookbook_loader.should have_key(:openldap) + cookbook_loader.should have_key(:apache2) end it "should allow you to override an attribute file via cookbook_path" do - @cookbook_loader[:openldap].attribute_filenames.detect { |f| + cookbook_loader[:openldap].attribute_filenames.detect { |f| f =~ /cookbooks\/openldap\/attributes\/default.rb/ }.should_not eql(nil) - @cookbook_loader[:openldap].attribute_filenames.detect { |f| + cookbook_loader[:openldap].attribute_filenames.detect { |f| f =~ /kitchen\/openldap\/attributes\/default.rb/ }.should eql(nil) end it "should load different attribute files from deeper paths" do - @cookbook_loader[:openldap].attribute_filenames.detect { |f| + cookbook_loader[:openldap].attribute_filenames.detect { |f| f =~ /kitchen\/openldap\/attributes\/robinson.rb/ }.should_not eql(nil) end it "should allow you to override a definition file via cookbook_path" do - @cookbook_loader[:openldap].definition_filenames.detect { |f| + cookbook_loader[:openldap].definition_filenames.detect { |f| f =~ /cookbooks\/openldap\/definitions\/client.rb/ }.should_not eql(nil) - @cookbook_loader[:openldap].definition_filenames.detect { |f| + cookbook_loader[:openldap].definition_filenames.detect { |f| f =~ /kitchen\/openldap\/definitions\/client.rb/ }.should eql(nil) end it "should load definition files from deeper paths" do - @cookbook_loader[:openldap].definition_filenames.detect { |f| + cookbook_loader[:openldap].definition_filenames.detect { |f| f =~ /kitchen\/openldap\/definitions\/drewbarrymore.rb/ }.should_not eql(nil) end it "should allow you to override a recipe file via cookbook_path" do - @cookbook_loader[:openldap].recipe_filenames.detect { |f| + cookbook_loader[:openldap].recipe_filenames.detect { |f| f =~ /cookbooks\/openldap\/recipes\/gigantor.rb/ }.should_not eql(nil) - @cookbook_loader[:openldap].recipe_filenames.detect { |f| + cookbook_loader[:openldap].recipe_filenames.detect { |f| f =~ /kitchen\/openldap\/recipes\/gigantor.rb/ }.should eql(nil) end it "should load recipe files from deeper paths" do - @cookbook_loader[:openldap].recipe_filenames.detect { |f| + cookbook_loader[:openldap].recipe_filenames.detect { |f| f =~ /kitchen\/openldap\/recipes\/woot.rb/ }.should_not eql(nil) end it "should allow you to have an 'ignore' file, which skips loading files in later cookbooks" do - @cookbook_loader[:openldap].recipe_filenames.detect { |f| + cookbook_loader[:openldap].recipe_filenames.detect { |f| f =~ /kitchen\/openldap\/recipes\/ignoreme.rb/ }.should eql(nil) end it "should find files that start with a ." do - @cookbook_loader[:openldap].file_filenames.detect { |f| + cookbook_loader[:openldap].file_filenames.detect { |f| f =~ /\.dotfile$/ }.should =~ /\.dotfile$/ - @cookbook_loader[:openldap].file_filenames.detect { |f| + cookbook_loader[:openldap].file_filenames.detect { |f| f =~ /\.ssh\/id_rsa$/ }.should =~ /\.ssh\/id_rsa$/ end it "should load the metadata for the cookbook" do - @cookbook_loader.metadata[:openldap].name.to_s.should == "openldap" - @cookbook_loader.metadata[:openldap].should be_a_kind_of(Chef::Cookbook::Metadata) + cookbook_loader.metadata[:openldap].name.to_s.should == "openldap" + cookbook_loader.metadata[:openldap].should be_a_kind_of(Chef::Cookbook::Metadata) end - it "should check each cookbook directory only once (CHEF-3487)" do - cookbooks = [] - @repo_paths.each do |repo_path| - cookbooks |= Dir[File.join(repo_path, "*")] - end - cookbooks.each do |cookbook| - File.should_receive(:directory?).with(cookbook).once; - end - @cookbook_loader.load_cookbooks - end - end # load_cookbooks + end # referencing cookbook files end # loading all cookbooks + context "loading all cookbooks when one has invalid metadata" do + + let(:repo_paths) do + [ + File.join(CHEF_SPEC_DATA, "kitchen"), + File.join(CHEF_SPEC_DATA, "cookbooks"), + File.join(CHEF_SPEC_DATA, "invalid-metadata-chef-repo") + ] + end + + it "does not squelch the exception" do + expect { cookbook_loader.load_cookbooks }.to raise_error("THIS METADATA HAS A BUG") + end + + end + describe "loading only one cookbook" do before(:each) do - @cookbook_loader = Chef::CookbookLoader.new(@repo_paths) - @cookbook_loader.load_cookbook("openldap") + cookbook_loader.load_cookbook("openldap") end it "should have loaded the correct cookbook" do seen = Hash.new - @cookbook_loader.each do |cookbook_name, cookbook| + cookbook_loader.each do |cookbook_name, cookbook| seen[cookbook_name] = true end seen.should have_key("openldap") @@ -176,7 +203,7 @@ describe Chef::CookbookLoader do it "should not duplicate keys when serialized to JSON" do # Chef JSON serialization will generate duplicate keys if given # a Hash containing matching string and symbol keys. See CHEF-4571. - aa = @cookbook_loader["openldap"] + aa = cookbook_loader["openldap"] aa.to_hash["metadata"].recipes.keys.should_not include(:openldap) aa.to_hash["metadata"].recipes.keys.should include("openldap") expected_desc = "Main Open LDAP configuration" @@ -190,30 +217,50 @@ describe Chef::CookbookLoader do end it "should not load the cookbook again when accessed" do - @cookbook_loader.should_not_receive('load_cookbook') - @cookbook_loader["openldap"] + cookbook_loader.should_not_receive('load_cookbook') + cookbook_loader["openldap"] end it "should not load the other cookbooks" do seen = Hash.new - @cookbook_loader.each do |cookbook_name, cookbook| + cookbook_loader.each do |cookbook_name, cookbook| seen[cookbook_name] = true end seen.should_not have_key("apache2") end it "should load another cookbook lazily with []" do - @cookbook_loader["apache2"].should be_a_kind_of(Chef::CookbookVersion) + cookbook_loader["apache2"].should be_a_kind_of(Chef::CookbookVersion) + end + + context "when an unrelated cookbook has invalid metadata" do + + let(:repo_paths) do + [ + File.join(CHEF_SPEC_DATA, "kitchen"), + File.join(CHEF_SPEC_DATA, "cookbooks"), + File.join(CHEF_SPEC_DATA, "invalid-metadata-chef-repo") + ] + end + + it "ignores the invalid cookbook" do + expect { cookbook_loader["openldap"] }.to_not raise_error + end + + it "surfaces the exception if the cookbook is loaded later" do + expect { cookbook_loader["invalid-metadata"] }.to raise_error("THIS METADATA HAS A BUG") + end + end describe "loading all cookbooks after loading only one cookbook" do before(:each) do - @cookbook_loader.load_cookbooks + cookbook_loader.load_cookbooks end it "should load all cookbooks" do seen = Hash.new - @cookbook_loader.each do |cookbook_name, cookbook| + cookbook_loader.each do |cookbook_name, cookbook| seen[cookbook_name] = true end seen.should have_key("openldap") @@ -221,4 +268,18 @@ describe Chef::CookbookLoader do end end end # loading only one cookbook + + describe "loading a single cookbook with a different name than basename" do + + before(:each) do + cookbook_loader.load_cookbook("name-mismatch") + end + + it "loads the correct cookbook" do + cookbook_version = cookbook_loader["name-mismatch"] + cookbook_version.should be_a_kind_of(Chef::CookbookVersion) + cookbook_version.name.should == :"name-mismatch" + end + + end end |