summaryrefslogtreecommitdiff
path: root/spec/unit/cookbook
diff options
context:
space:
mode:
Diffstat (limited to 'spec/unit/cookbook')
-rw-r--r--spec/unit/cookbook/chefignore_spec.rb38
-rw-r--r--spec/unit/cookbook/metadata_spec.rb627
-rw-r--r--spec/unit/cookbook/synchronizer_spec.rb258
-rw-r--r--spec/unit/cookbook/syntax_check_spec.rb211
4 files changed, 1134 insertions, 0 deletions
diff --git a/spec/unit/cookbook/chefignore_spec.rb b/spec/unit/cookbook/chefignore_spec.rb
new file mode 100644
index 0000000000..30b97e865d
--- /dev/null
+++ b/spec/unit/cookbook/chefignore_spec.rb
@@ -0,0 +1,38 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+require 'spec_helper'
+
+describe Chef::Cookbook::Chefignore do
+ before do
+ @chefignore = Chef::Cookbook::Chefignore.new(File.join(CHEF_SPEC_DATA, 'cookbooks'))
+ end
+
+ it "loads the globs in the chefignore file" do
+ @chefignore.ignores.should =~ %w[recipes/ignoreme.rb]
+ end
+
+ it "removes items from an array that match the ignores" do
+ file_list = %w[ recipes/ignoreme.rb recipes/dontignoreme.rb ]
+ @chefignore.remove_ignores_from(file_list).should == %w[recipes/dontignoreme.rb]
+ end
+
+ it "determines if a file is ignored" do
+ @chefignore.ignored?('recipes/ignoreme.rb').should be_true
+ @chefignore.ignored?('recipes/dontignoreme.rb').should be_false
+ end
+end
diff --git a/spec/unit/cookbook/metadata_spec.rb b/spec/unit/cookbook/metadata_spec.rb
new file mode 100644
index 0000000000..2757f92506
--- /dev/null
+++ b/spec/unit/cookbook/metadata_spec.rb
@@ -0,0 +1,627 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Copyright:: Copyright 2008-2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require '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
+
+ describe "when comparing for equality" do
+ before do
+ @fields = [ :name, :description, :long_description, :maintainer,
+ :maintainer_email, :license, :platforms, :dependencies,
+ :recommendations, :suggestions, :conflicting, :providing,
+ :replacing, :attributes, :groupings, :recipes, :version]
+ end
+
+ it "does not depend on object identity for equality" do
+ @meta.should == @meta.dup
+ end
+
+ it "is not equal to another object if it isn't have all of the metadata fields" do
+ @fields.each_index do |field_to_remove|
+ fields_to_include = @fields.dup
+ fields_to_include.delete_at(field_to_remove)
+ almost_duck_type = Struct.new(*fields_to_include).new
+ @fields.each do |field|
+ setter = "#{field}="
+ metadata_value = @meta.send(field)
+ almost_duck_type.send(setter, metadata_value) if almost_duck_type.respond_to?(setter)
+ @mets.should_not == almost_duck_type
+ end
+ end
+ end
+
+ it "is equal to another object if it has equal values for all metadata fields" do
+ duck_type = Struct.new(*@fields).new
+ @fields.each do |field|
+ setter = "#{field}="
+ metadata_value = @meta.send(field)
+ duck_type.send(setter, metadata_value)
+ end
+ @meta.should == duck_type
+ end
+
+ it "is not equal if any values are different" do
+ duck_type_class = Struct.new(*@fields)
+ @fields.each do |field_to_change|
+ duck_type = duck_type_class.new
+
+ @fields.each do |field|
+ setter = "#{field}="
+ metadata_value = @meta.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
+ 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)
+ end
+
+ it "should allow a cookbook as the first argument" do
+ lambda { Chef::Cookbook::Metadata.new(@cookbook) }.should_not raise_error
+ 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
+ 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'
+ 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
+ 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'
+ 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
+ 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'
+ end
+ end
+
+ describe "cookbook" do
+ it "should return the cookbook we were initialized with" do
+ @meta.cookbook.should eql(@cookbook)
+ end
+ end
+
+ describe "name" do
+ it "should return the name of the cookbook" do
+ @meta.name.should eql(@cookbook.name)
+ end
+ end
+
+ describe "platforms" do
+ it "should return the current platform hash" do
+ @meta.platforms.should be_a_kind_of(Hash)
+ 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'
+ end
+ end
+
+ describe "meta-data attributes" do
+ params = {
+ :maintainer => "Adam Jacob",
+ :maintainer_email => "adam@opscode.com",
+ :license => "Apache v2.0",
+ :description => "Foobar!",
+ :long_description => "Much Longer\nSeriously",
+ :version => "0.6.0"
+ }
+ 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)
+ end
+ it "should be get-able via #{field}" do
+ @meta.send(field, field_value)
+ @meta.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")
+ 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")
+ end
+ end
+ end
+
+ describe "describing dependencies" do
+ dep_types = {
+ :depends => [ :dependencies, "foo::bar", "> 0.2" ],
+ :recommends => [ :recommendations, "foo::bar", ">= 0.2" ],
+ :suggests => [ :suggestions, "foo::bar", "> 0.2" ],
+ :conflicts => [ :conflicting, "foo::bar", "~> 0.2" ],
+ :provides => [ :providing, "foo::bar", "<= 0.2" ],
+ :replaces => [ :replacing, "foo::bar", "= 0.2.1" ],
+ }
+ dep_types.sort { |a,b| a.to_s <=> b.to_s }.each do |dep, dep_args|
+ 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]
+ 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] }
+ end
+ end
+ end
+
+
+ describe "in the obsoleted format" do
+ dep_types = {
+ :depends => [ "foo::bar", "> 0.2", "< 1.0" ],
+ :recommends => [ "foo::bar", ">= 0.2", "< 1.0" ],
+ :suggests => [ "foo::bar", "> 0.2", "< 1.0" ],
+ :conflicts => [ "foo::bar", "> 0.2", "< 1.0" ],
+ :provides => [ "foo::bar", "> 0.2", "< 1.0" ],
+ :replaces => [ "foo::bar", "> 0.2.1", "< 1.0" ],
+ }
+
+ 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)
+ end
+ end
+ end
+
+
+ describe "with obsolete operators" do
+ dep_types = {
+ :depends => [ "foo::bar", ">> 0.2"],
+ :recommends => [ "foo::bar", ">> 0.2"],
+ :suggests => [ "foo::bar", ">> 0.2"],
+ :conflicts => [ "foo::bar", ">> 0.2"],
+ :provides => [ "foo::bar", ">> 0.2"],
+ :replaces => [ "foo::bar", ">> 0.2.1"],
+ }
+
+ 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)
+ end
+ end
+ end
+ end
+
+ describe "attribute groupings" do
+ it "should allow you set a grouping" do
+ group = {
+ "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
+ end
+ it "should not accept anything but a string for display_name" do
+ lambda {
+ @meta.grouping("db/mysql/databases", :title => "foo")
+ }.should_not raise_error(ArgumentError)
+ lambda {
+ @meta.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")
+ }.should_not raise_error(ArgumentError)
+ lambda {
+ @meta.grouping("db/mysql/databases", :description => Hash.new)
+ }.should raise_error(ArgumentError)
+ end
+ end
+
+ describe "cookbook attributes" do
+ it "should allow you set an attributes metadata" do
+ attrs = {
+ "display_name" => "MySQL Databases",
+ "description" => "Description of MySQL",
+ "choice" => ['dedicated', 'shared'],
+ "calculated" => false,
+ "type" => 'string',
+ "required" => 'recommended',
+ "recipes" => [ "mysql::server", "mysql::master" ],
+ "default" => [ ]
+ }
+ @meta.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")
+ }.should_not raise_error(ArgumentError)
+ lambda {
+ @meta.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")
+ }.should_not raise_error(ArgumentError)
+ lambda {
+ @meta.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'])
+ }.should_not raise_error(ArgumentError)
+ lambda {
+ @meta.attribute("db/mysql/databases", :choice => [10, 'shared'])
+ }.should raise_error(ArgumentError)
+ lambda {
+ @meta.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 == []
+ end
+
+ it "should let calculated be true or false" do
+ lambda {
+ @meta.attribute("db/mysql/databases", :calculated => true)
+ }.should_not raise_error(ArgumentError)
+ lambda {
+ @meta.attribute("db/mysql/databases", :calculated => false)
+ }.should_not raise_error(ArgumentError)
+ lambda {
+ @meta.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
+ end
+
+ it "accepts String for the attribute type" do
+ lambda {
+ @meta.attribute("db/mysql/databases", :type => "string")
+ }.should_not raise_error(ArgumentError)
+ end
+
+ it "accepts Array for the attribute type" do
+ lambda {
+ @meta.attribute("db/mysql/databases", :type => "array")
+ }.should_not raise_error(ArgumentError)
+ lambda {
+ @meta.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")
+ }.should_not raise_error(ArgumentError)
+ end
+
+ it "should let type be hash (backwards compatability only)" do
+ lambda {
+ @meta.attribute("db/mysql/databases", :type => "hash")
+ }.should_not raise_error(ArgumentError)
+ end
+
+ it "should let required be required, recommended or optional" do
+ lambda {
+ @meta.attribute("db/mysql/databases", :required => 'required')
+ }.should_not raise_error(ArgumentError)
+ lambda {
+ @meta.attribute("db/mysql/databases", :required => 'recommended')
+ }.should_not raise_error(ArgumentError)
+ lambda {
+ @meta.attribute("db/mysql/databases", :required => 'optional')
+ }.should_not raise_error(ArgumentError)
+ end
+
+ it "should convert required true to required" do
+ lambda {
+ @meta.attribute("db/mysql/databases", :required => true)
+ }.should_not raise_error(ArgumentError)
+ #attrib = @meta.attributes["db/mysql/databases"][:required].should == "required"
+ end
+
+ it "should convert required false to optional" do
+ lambda {
+ @meta.attribute("db/mysql/databases", :required => false)
+ }.should_not raise_error(ArgumentError)
+ #attrib = @meta.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'
+ end
+
+ it "should make sure recipes is an array" do
+ lambda {
+ @meta.attribute("db/mysql/databases", :recipes => [])
+ }.should_not raise_error(ArgumentError)
+ lambda {
+ @meta.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 == []
+ end
+
+ it "should allow the default value to be a string, array, or hash" do
+ lambda {
+ @meta.attribute("db/mysql/databases", :default => [])
+ }.should_not raise_error(ArgumentError)
+ lambda {
+ @meta.attribute("db/mysql/databases", :default => {})
+ }.should_not raise_error(ArgumentError)
+ lambda {
+ @meta.attribute("db/mysql/databases", :default => "alice in chains")
+ }.should_not raise_error(ArgumentError)
+ lambda {
+ @meta.attribute("db/mysql/databases", :required => :not_gonna_do_it)
+ }.should raise_error(ArgumentError)
+ end
+
+ it "should error if default used with calculated" do
+ lambda {
+ attrs = {
+ :calculated => true,
+ :default => [ "I thought you said calculated" ]
+ }
+ @meta.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)
+ }.should raise_error(ArgumentError)
+ end
+
+ it "should allow a default that is a choice" do
+ lambda {
+ attrs = {
+ :choice => [ "a", "b", "c"],
+ :default => "b"
+ }
+ @meta.attribute("db/mysql/databases", attrs)
+ }.should_not raise_error(ArgumentError)
+ lambda {
+ attrs = {
+ :choice => [ "a", "b", "c", "d", "e"],
+ :default => ["b", "d"]
+ }
+ @meta.attribute("db/mysql/databases", attrs)
+ }.should_not raise_error(ArgumentError)
+ end
+
+ it "should error if default is not a choice" do
+ lambda {
+ attrs = {
+ :choice => [ "a", "b", "c"],
+ :default => "d"
+ }
+ @meta.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)
+ }.should raise_error(ArgumentError)
+ end
+ end
+
+ describe "recipes" do
+ before(:each) do
+ @cookbook.recipe_files = [ "default.rb", "enlighten.rb" ]
+ @meta = Chef::Cookbook::Metadata.new(@cookbook)
+ end
+
+ it "should have the names of the recipes" do
+ @meta.recipes["test_cookbook"].should == ""
+ @meta.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?"
+ 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
+ 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 "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",
+ :display_name => "You have nothing"
+ @meta.version "1.2.3"
+ end
+
+ describe "serialize" do
+ before(:each) do
+ @serial = Chef::JSONCompat.from_json(@meta.to_json)
+ end
+
+ it "should serialize to a json hash" do
+ Chef::JSONCompat.from_json(@meta.to_json).should be_a_kind_of(Hash)
+ end
+
+ %w{
+ name
+ description
+ long_description
+ maintainer
+ maintainer_email
+ license
+ platforms
+ dependencies
+ suggestions
+ recommendations
+ conflicting
+ providing
+ replacing
+ attributes
+ recipes
+ version
+ }.each do |t|
+ it "should include '#{t}'" do
+ @serial[t].should == @meta.send(t.to_sym)
+ end
+ end
+ end
+
+ describe "deserialize" do
+ before(:each) do
+ @deserial = Chef::Cookbook::Metadata.from_json(@meta.to_json)
+ end
+
+ it "should deserialize to a Chef::Cookbook::Metadata object" do
+ @deserial.should be_a_kind_of(Chef::Cookbook::Metadata)
+ end
+
+ %w{
+ name
+ description
+ long_description
+ maintainer
+ maintainer_email
+ license
+ platforms
+ dependencies
+ suggestions
+ recommendations
+ conflicting
+ providing
+ replacing
+ attributes
+ recipes
+ version
+ }.each do |t|
+ it "should match '#{t}'" do
+ @deserial.send(t.to_sym).should == @meta.send(t.to_sym)
+ end
+ end
+ end
+
+ describe "from_hash" do
+ before(:each) do
+ @hash = @meta.to_hash
+ end
+
+ [:dependencies,
+ :recommendations,
+ :suggestions,
+ :conflicting,
+ :replacing].each do |to_check|
+ it "should transform deprecated greater than syntax for :#{to_check.to_s}" do
+ @hash[to_check.to_s]["foo::bar"] = ">> 0.2"
+ deserial = Chef::Cookbook::Metadata.from_hash(@hash)
+ deserial.send(to_check)["foo::bar"].should == '> 0.2'
+ end
+
+ it "should transform deprecated less than syntax for :#{to_check.to_s}" do
+ @hash[to_check.to_s]["foo::bar"] = "<< 0.2"
+ deserial = Chef::Cookbook::Metadata.from_hash(@hash)
+ deserial.send(to_check)["foo::bar"].should == '< 0.2'
+ end
+
+ it "should ignore multiple dependency constraints for :#{to_check.to_s}" do
+ @hash[to_check.to_s]["foo::bar"] = [ ">= 1.0", "<= 5.2" ]
+ deserial = Chef::Cookbook::Metadata.from_hash(@hash)
+ deserial.send(to_check)["foo::bar"].should == []
+ end
+
+ it "should accept an empty array of dependency constraints for :#{to_check.to_s}" do
+ @hash[to_check.to_s]["foo::bar"] = []
+ deserial = Chef::Cookbook::Metadata.from_hash(@hash)
+ deserial.send(to_check)["foo::bar"].should == []
+ end
+
+ it "should accept single-element arrays of dependency constraints for :#{to_check.to_s}" do
+ @hash[to_check.to_s]["foo::bar"] = [ ">= 2.0" ]
+ deserial = Chef::Cookbook::Metadata.from_hash(@hash)
+ deserial.send(to_check)["foo::bar"].should == ">= 2.0"
+ end
+ end
+ end
+
+ end
+
+end
diff --git a/spec/unit/cookbook/synchronizer_spec.rb b/spec/unit/cookbook/synchronizer_spec.rb
new file mode 100644
index 0000000000..e84fd3cfc5
--- /dev/null
+++ b/spec/unit/cookbook/synchronizer_spec.rb
@@ -0,0 +1,258 @@
+require 'spec_helper'
+require 'chef/cookbook/synchronizer'
+require 'chef/cookbook_version'
+
+describe Chef::CookbookCacheCleaner do
+ describe "when cleaning up unused cookbook components" do
+
+ before do
+ @cleaner = Chef::CookbookCacheCleaner.instance
+ @cleaner.reset!
+ end
+
+ it "removes all files that belong to unused cookbooks" do
+ end
+
+ it "removes all files not validated during the chef run" do
+ file_cache = mock("Chef::FileCache with files from unused cookbooks")
+ unused_template_files = %w{cookbooks/unused/templates/default/foo.conf.erb cookbooks/unused/tempaltes/default/bar.conf.erb}
+ valid_cached_cb_files = %w{cookbooks/valid1/recipes/default.rb cookbooks/valid2/recipes/default.rb}
+ @cleaner.mark_file_as_valid('cookbooks/valid1/recipes/default.rb')
+ @cleaner.mark_file_as_valid('cookbooks/valid2/recipes/default.rb')
+ file_cache.should_receive(:find).with(File.join(%w{cookbooks ** *})).and_return(valid_cached_cb_files + unused_template_files)
+ file_cache.should_receive(:delete).with('cookbooks/unused/templates/default/foo.conf.erb')
+ file_cache.should_receive(:delete).with('cookbooks/unused/tempaltes/default/bar.conf.erb')
+ cookbook_hash = {"valid1"=> {}, "valid2" => {}}
+ @cleaner.stub!(:cache).and_return(file_cache)
+ @cleaner.cleanup_file_cache
+ end
+
+ describe "on chef-solo" do
+ before do
+ Chef::Config[:solo] = true
+ end
+
+ after do
+ Chef::Config[:solo] = false
+ end
+
+ it "does not remove anything" do
+ @cleaner.cache.stub!(:find).and_return(%w{cookbooks/valid1/recipes/default.rb cookbooks/valid2/recipes/default.rb})
+ @cleaner.cache.should_not_receive(:delete)
+ @cleaner.cleanup_file_cache
+ end
+
+ end
+
+ end
+end
+
+describe Chef::CookbookSynchronizer do
+ before do
+ segments = [ :resources, :providers, :recipes, :definitions, :libraries, :attributes, :files, :templates, :root_files ]
+ @cookbook_manifest = {}
+ @cookbook_a = Chef::CookbookVersion.new("cookbook_a")
+ @cookbook_a_manifest = segments.inject({}) {|h, segment| h[segment.to_s] = []; h}
+ @cookbook_a_default_recipe = { "path" => "recipes/default.rb",
+ "url" => "http://chef.example.com/abc123",
+ "checksum" => "abc123" }
+ @cookbook_a_manifest["recipes"] = [ @cookbook_a_default_recipe ]
+
+ @cookbook_a_default_attrs = { "path" => "attributes/default.rb",
+ "url" => "http://chef.example.com/abc456",
+ "checksum" => "abc456" }
+ @cookbook_a_manifest["attributes"] = [ @cookbook_a_default_attrs ]
+ @cookbook_a_manifest["templates"] = [{"path" => "templates/default/apache2.conf.erb", "url" => "http://chef.example.com/ffffff"}]
+ @cookbook_a.manifest = @cookbook_a_manifest
+ @cookbook_manifest["cookbook_a"] = @cookbook_a
+
+ @events = Chef::EventDispatch::Dispatcher.new
+ @synchronizer = Chef::CookbookSynchronizer.new(@cookbook_manifest, @events)
+ end
+
+ it "lists the cookbook names" do
+ @synchronizer.cookbook_names.should == %w[cookbook_a]
+ end
+
+ it "lists the cookbook manifests" do
+ @synchronizer.cookbooks.should == [@cookbook_a]
+ end
+
+ context "when the cache contains unneeded cookbooks" do
+ before do
+ @file_cache = mock("Chef::FileCache with files from unused cookbooks")
+ @valid_cached_cb_files = %w{cookbooks/valid1/recipes/default.rb cookbooks/valid2/recipes/default.rb}
+ @obsolete_cb_files = %w{cookbooks/old1/recipes/default.rb cookbooks/old2/recipes/default.rb}
+
+ @cookbook_hash = {"valid1"=> {}, "valid2" => {}}
+
+ @synchronizer = Chef::CookbookSynchronizer.new(@cookbook_hash, @events)
+ end
+
+ it "removes unneeded cookbooks" do
+ @file_cache.should_receive(:find).with(File.join(%w{cookbooks ** *})).and_return(@valid_cached_cb_files + @obsolete_cb_files)
+ @file_cache.should_receive(:delete).with('cookbooks/old1/recipes/default.rb')
+ @file_cache.should_receive(:delete).with('cookbooks/old2/recipes/default.rb')
+ @synchronizer.stub!(:cache).and_return(@file_cache)
+ @synchronizer.clear_obsoleted_cookbooks
+ end
+ end
+
+ describe "when syncing cookbooks with the server" do
+ before do
+ # Would rather not stub out methods on the test subject, but setting up
+ # the state is a PITA and tests for this behavior are above.
+ @synchronizer.should_receive(:clear_obsoleted_cookbooks)
+
+ @server_api = mock("Chef::REST (mock)")
+ @file_cache = mock("Chef::FileCache (mock)")
+ @synchronizer.stub!(:server_api).and_return(@server_api)
+ @synchronizer.stub!(:cache).and_return(@file_cache)
+
+
+ @cookbook_a_default_recipe_tempfile = mock("Tempfile for cookbook_a default.rb recipe",
+ :path => "/tmp/cookbook_a_recipes_default_rb")
+
+ @cookbook_a_default_attribute_tempfile = mock("Tempfile for cookbook_a default.rb attr file",
+ :path => "/tmp/cookbook_a_attributes_default_rb")
+
+ end
+
+ context "when the cache does not contain the desired files" do
+ before do
+
+ # Files are not in the cache:
+ @file_cache.should_receive(:has_key?).
+ with("cookbooks/cookbook_a/recipes/default.rb").
+ and_return(false)
+ @file_cache.should_receive(:has_key?).
+ with("cookbooks/cookbook_a/attributes/default.rb").
+ and_return(false)
+
+ # Fetch and copy default.rb recipe
+ @server_api.should_receive(:get_rest).
+ with('http://chef.example.com/abc123', true).
+ and_return(@cookbook_a_default_recipe_tempfile)
+ @file_cache.should_receive(:move_to).
+ with("/tmp/cookbook_a_recipes_default_rb", "cookbooks/cookbook_a/recipes/default.rb")
+ @file_cache.should_receive(:load).
+ with("cookbooks/cookbook_a/recipes/default.rb", false).
+ and_return("/file-cache/cookbooks/cookbook_a/recipes/default.rb")
+
+ # Fetch and copy default.rb attribute file
+ @server_api.should_receive(:get_rest).
+ with('http://chef.example.com/abc456', true).
+ and_return(@cookbook_a_default_attribute_tempfile)
+ @file_cache.should_receive(:move_to).
+ with("/tmp/cookbook_a_attributes_default_rb", "cookbooks/cookbook_a/attributes/default.rb")
+ @file_cache.should_receive(:load).
+ with("cookbooks/cookbook_a/attributes/default.rb", false).
+ and_return("/file-cache/cookbooks/cookbook_a/attributes/default.rb")
+ end
+
+ it "fetches eagerly loaded files" do
+ @synchronizer.sync_cookbooks
+ end
+
+ it "does not fetch templates or cookbook files" do
+ # Implicitly tested in previous test; this test is just for behavior specification.
+ @server_api.should_not_receive(:get_rest).
+ with('http://chef.example.com/ffffff', true)
+
+ @synchronizer.sync_cookbooks
+ end
+
+ end
+
+ context "when the cache contains outdated files" do
+ before do
+ # Files are in the cache:
+ @file_cache.should_receive(:has_key?).
+ with("cookbooks/cookbook_a/recipes/default.rb").
+ and_return(true)
+ @file_cache.should_receive(:has_key?).
+ with("cookbooks/cookbook_a/attributes/default.rb").
+ and_return(true)
+
+
+ # Fetch and copy default.rb recipe
+ @server_api.should_receive(:get_rest).
+ with('http://chef.example.com/abc123', true).
+ and_return(@cookbook_a_default_recipe_tempfile)
+ @file_cache.should_receive(:move_to).
+ with("/tmp/cookbook_a_recipes_default_rb", "cookbooks/cookbook_a/recipes/default.rb")
+ @file_cache.should_receive(:load).
+ with("cookbooks/cookbook_a/recipes/default.rb", false).
+ twice.
+ and_return("/file-cache/cookbooks/cookbook_a/recipes/default.rb")
+
+ # Current file has fff000, want abc123
+ Chef::CookbookVersion.should_receive(:checksum_cookbook_file).
+ with("/file-cache/cookbooks/cookbook_a/recipes/default.rb").
+ and_return("fff000")
+
+ # Fetch and copy default.rb attribute file
+ @server_api.should_receive(:get_rest).
+ with('http://chef.example.com/abc456', true).
+ and_return(@cookbook_a_default_attribute_tempfile)
+ @file_cache.should_receive(:move_to).
+ with("/tmp/cookbook_a_attributes_default_rb", "cookbooks/cookbook_a/attributes/default.rb")
+ @file_cache.should_receive(:load).
+ with("cookbooks/cookbook_a/attributes/default.rb", false).
+ twice.
+ and_return("/file-cache/cookbooks/cookbook_a/attributes/default.rb")
+
+ # Current file has fff000, want abc456
+ Chef::CookbookVersion.should_receive(:checksum_cookbook_file).
+ with("/file-cache/cookbooks/cookbook_a/attributes/default.rb").
+ and_return("fff000")
+ end
+
+ it "updates the outdated files" do
+ @synchronizer.sync_cookbooks
+ end
+ end
+
+ context "when the cache is up to date" do
+ before do
+ # Files are in the cache:
+ @file_cache.should_receive(:has_key?).
+ with("cookbooks/cookbook_a/recipes/default.rb").
+ and_return(true)
+ @file_cache.should_receive(:has_key?).
+ with("cookbooks/cookbook_a/attributes/default.rb").
+ and_return(true)
+
+ # Current file has abc123, want abc123
+ Chef::CookbookVersion.should_receive(:checksum_cookbook_file).
+ with("/file-cache/cookbooks/cookbook_a/recipes/default.rb").
+ and_return("abc123")
+
+ # Current file has abc456, want abc456
+ Chef::CookbookVersion.should_receive(:checksum_cookbook_file).
+ with("/file-cache/cookbooks/cookbook_a/attributes/default.rb").
+ and_return("abc456")
+
+ @file_cache.should_receive(:load).
+ with("cookbooks/cookbook_a/recipes/default.rb", false).
+ twice.
+ and_return("/file-cache/cookbooks/cookbook_a/recipes/default.rb")
+
+ @file_cache.should_receive(:load).
+ with("cookbooks/cookbook_a/attributes/default.rb", false).
+ twice.
+ and_return("/file-cache/cookbooks/cookbook_a/attributes/default.rb")
+ end
+
+ it "does not update files" do
+ @file_cache.should_not_receive(:move_to)
+ @server_api.should_not_receive(:get_rest)
+ @synchronizer.sync_cookbooks
+ end
+
+ end
+
+ end
+
+end
+
diff --git a/spec/unit/cookbook/syntax_check_spec.rb b/spec/unit/cookbook/syntax_check_spec.rb
new file mode 100644
index 0000000000..b41c2ddf0a
--- /dev/null
+++ b/spec/unit/cookbook/syntax_check_spec.rb
@@ -0,0 +1,211 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+###################################################
+# OLD:
+###################################################
+# def test_ruby(cookbook_dir)
+# cache = Chef::ChecksumCache.instance
+# Dir[File.join(cookbook_dir, '**', '*.rb')].each do |ruby_file|
+# key = cache.generate_key(ruby_file, "chef-test")
+# fstat = File.stat(ruby_file)
+#
+# if cache.lookup_checksum(key, fstat)
+# Chef::Log.info("No change in checksum of #{ruby_file}")
+# else
+# Chef::Log.info("Testing #{ruby_file} for syntax errors...")
+# Chef::Mixin::Command.run_command(:command => "ruby -c #{ruby_file}", :output_on_failure => true)
+# cache.generate_checksum(key, ruby_file, fstat)
+# end
+# end
+# end
+#
+#def test_templates(cookbook_dir)
+# cache = Chef::ChecksumCache.instance
+# Dir[File.join(cookbook_dir, '**', '*.erb')].each do |erb_file|
+# key = cache.generate_key(erb_file, "chef-test")
+# fstat = File.stat(erb_file)
+#
+# if cache.lookup_checksum(key, fstat)
+# Chef::Log.info("No change in checksum of #{erb_file}")
+# else
+# Chef::Log.info("Testing template #{erb_file} for syntax errors...")
+# Chef::Mixin::Command.run_command(:command => "sh -c 'erubis -x #{erb_file} | ruby -c'", :output_on_failure => true)
+# cache.generate_checksum(key, erb_file, fstat)
+# end
+# end
+#end
+#
+
+###################################################
+# NEW:
+###################################################
+# def test_template_file(cookbook_dir, erb_file)
+# Chef::Log.debug("Testing template #{erb_file} for syntax errors...")
+# result = shell_out("sh -c 'erubis -x #{erb_file} | ruby -c'")
+# result.error!
+# rescue Mixlib::ShellOut::ShellCommandFailed
+# file_relative_path = erb_file[/^#{Regexp.escape(cookbook_dir+File::Separator)}(.*)/, 1]
+# Chef::Log.fatal("Erb template #{file_relative_path} has a syntax error:")
+# result.stderr.each_line { |l| Chef::Log.fatal(l.chomp) }
+# exit(1)
+# end
+#
+# def test_ruby_file(cookbook_dir, ruby_file)
+# Chef::Log.debug("Testing #{ruby_file} for syntax errors...")
+# result = shell_out("ruby -c #{ruby_file}")
+# result.error!
+# rescue Mixlib::ShellOut::ShellCommandFailed
+# file_relative_path = ruby_file[/^#{Regexp.escape(cookbook_dir+File::Separator)}(.*)/, 1]
+# Chef::Log.fatal("Cookbook file #{file_relative_path} has a syntax error:")
+# result.stderr.each_line { |l| Chef::Log.fatal(l.chomp) }
+# exit(1)
+# end
+#
+
+require 'spec_helper'
+require "chef/cookbook/syntax_check"
+
+describe Chef::Cookbook::SyntaxCheck do
+ before do
+ Chef::Log.logger = Logger.new(StringIO.new)
+
+ @cookbook_path = File.join(CHEF_SPEC_DATA, 'cookbooks', 'openldap')
+
+ @attr_files = %w{default.rb smokey.rb}.map { |f| File.join(@cookbook_path, 'attributes', f) }
+ @defn_files = %w{client.rb server.rb}.map { |f| File.join(@cookbook_path, 'definitions', f)}
+ @recipes = %w{default.rb gigantor.rb one.rb}.map { |f| File.join(@cookbook_path, 'recipes', f) }
+ @ruby_files = @attr_files + @defn_files + @recipes
+
+ @template_files = %w{openldap_stuff.conf.erb openldap_variable_stuff.conf.erb test.erb}.map { |f| File.join(@cookbook_path, 'templates', 'default', f)}
+
+ @syntax_check = Chef::Cookbook::SyntaxCheck.new(@cookbook_path)
+ end
+
+ it "creates a syntax checker given the cookbook name when Chef::Config.cookbook_path is set" do
+ Chef::Config[:cookbook_path] = File.dirname(@cookbook_path)
+ syntax_check = Chef::Cookbook::SyntaxCheck.for_cookbook(:openldap)
+ syntax_check.cookbook_path.should == @cookbook_path
+ end
+
+ describe "when first created" do
+ it "has the path to the cookbook to syntax check" do
+ @syntax_check.cookbook_path.should == @cookbook_path
+ end
+
+ it "has access to the checksum cache" do
+ @syntax_check.cache.should equal(Chef::ChecksumCache.instance)
+ end
+
+ it "lists the ruby files in the cookbook" do
+ @syntax_check.ruby_files.sort.should == @ruby_files.sort
+ end
+
+ it "lists the erb templates in the cookbook" do
+ @syntax_check.template_files.sort.should == @template_files.sort
+ end
+
+ end
+
+ describe "when validating cookbooks" do
+ before do
+ Chef::Config[:cache_type] = 'Memory'
+ @checksum_cache_klass = Class.new(Chef::ChecksumCache)
+ @checksum_cache = @checksum_cache_klass.instance
+ @checksum_cache.reset!('Memory')
+ @syntax_check.stub!(:cache).and_return(@checksum_cache)
+ $stdout.stub!(:write)
+ end
+
+ describe "and the files have not been syntax checked previously" do
+ it "shows that all ruby files require a syntax check" do
+ @syntax_check.untested_ruby_files.sort.should == @ruby_files.sort
+ end
+
+ it "shows that all template files require a syntax check" do
+ @syntax_check.untested_template_files.sort.should == @template_files.sort
+ end
+
+ it "removes a ruby file from the list of untested files after it is marked as validated" do
+ recipe = File.join(@cookbook_path, 'recipes', 'default.rb')
+ @syntax_check.validated(recipe)
+ @syntax_check.untested_ruby_files.should_not include(recipe)
+ end
+
+ it "removes a template file from the list of untested files after it is marked as validated" do
+ template = File.join(@cookbook_path, 'templates', 'default', 'test.erb')
+ @syntax_check.validated(template)
+ @syntax_check.untested_template_files.should_not include(template)
+ end
+
+ it "validates all ruby files" do
+ @syntax_check.validate_ruby_files.should be_true
+ @syntax_check.untested_ruby_files.should be_empty
+ end
+
+ it "validates all templates" do
+ @syntax_check.validate_templates.should be_true
+ @syntax_check.untested_template_files.should be_empty
+ end
+
+ describe "and a file has a syntax error" do
+ before do
+ @cookbook_path = File.join(CHEF_SPEC_DATA, 'cookbooks', 'borken')
+ @syntax_check.cookbook_path.replace(@cookbook_path)
+ end
+
+ it "it indicates that a ruby file has a syntax error" do
+ @syntax_check.validate_ruby_files.should be_false
+ end
+
+ 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)
+ end
+
+ it "indicates that a template file has a syntax error" do
+ @syntax_check.validate_templates.should be_false
+ end
+
+ it "does not remove the invalid template from the list of untested templates" do
+ @syntax_check.untested_template_files.should include(File.join(@cookbook_path, 'templates', 'default', 'borken.erb'))
+ lambda {@syntax_check.validate_templates}.should_not change(@syntax_check, :untested_template_files)
+ end
+
+ end
+
+ end
+
+ describe "and the files have been syntax checked previously" do
+ before do
+ @syntax_check.untested_ruby_files.each { |f| @syntax_check.validated(f) }
+ @syntax_check.untested_template_files.each { |f| @syntax_check.validated(f) }
+ end
+
+ it "does not syntax check ruby files" do
+ @syntax_check.should_not_receive(:shell_out)
+ @syntax_check.validate_ruby_files.should be_true
+ end
+
+ it "does not syntax check templates" do
+ @syntax_check.should_not_receive(:shell_out)
+ @syntax_check.validate_templates.should be_true
+ end
+ end
+ end
+end