diff options
-rw-r--r-- | chef/lib/chef/provider/package/rubygems.rb | 140 | ||||
-rw-r--r-- | chef/spec/unit/provider/package/rubygems_spec.rb | 145 |
2 files changed, 203 insertions, 82 deletions
diff --git a/chef/lib/chef/provider/package/rubygems.rb b/chef/lib/chef/provider/package/rubygems.rb index d9c80f2e33..e9345bf795 100644 --- a/chef/lib/chef/provider/package/rubygems.rb +++ b/chef/lib/chef/provider/package/rubygems.rb @@ -6,9 +6,9 @@ # 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. @@ -20,11 +20,68 @@ require 'chef/provider/package' require 'chef/mixin/command' require 'chef/resource/package' +require 'rubygems/specification' + class Chef class Provider class Package - class Rubygems < Chef::Provider::Package - + class Rubygems < Chef::Provider::Package + class CurrentGemEnvironment + + def gem_paths + Gem.path + end + + def installed_versions(gem_name, version_spec=nil) + gem_source_index.search(Gem::Dependency.new(gem_name, version_spec)).map { |g| g.version } + end + + def gem_source_index + Gem.source_index + end + + end + + class AlternateGemEnvironment + include Chef::Mixin::ShellOut + + attr_reader :gem_binary_location + + def initialize(gem_binary_location) + @gem_binary_location = gem_binary_location + end + + def gem_paths + @gempaths ||= begin + # shellout! is a fork/exec which won't work on windows + shell_style_paths = shell_out!("#{@gem_binary_location} env gempath").stdout + # on windows, the path separator is (usually? always?) semicolon + shell_style_paths.split(::File::PATH_SEPARATOR).map { |path| path.strip } + end + end + + # TODO: de-duplicate + def installed_versions(gem_name, version_spec=nil) + gem_source_index.search(Gem::Dependency.new(gem_name, version_spec)).map { |g| g.version } + end + + def gem_source_index + @source_index ||= Gem::SourceIndex.from_gems_in(*gem_paths.map { |p| p + '/specifications' }) + end + + end + + attr_reader :gem_implementation + + def initialize(new_resource, run_context=nil) + super + if new_resource.gem_binary + @gem_implementation = AlternateGemEnvironment.new(new_resource.gem_binary) + else + @gem_implementation = CurrentGemEnvironment.new + end + end + def gem_list_parse(line) installed_versions = Array.new if md = line.match(/^#{@new_resource.package_name} \((.+?)(?: [^\)\.]+)?\)$/) @@ -34,38 +91,41 @@ class Chef end end - def gem_binary_path - path = @new_resource.gem_binary - path ? path : 'gem' - end - + # def gem_binary_path + # path = @new_resource.gem_binary + # path ? path : 'gem' + # end + def load_current_resource @current_resource = Chef::Resource::Package.new(@new_resource.name) @current_resource.package_name(@new_resource.package_name) @current_resource.version(nil) - - # First, we need to look up whether we have the local gem installed or not - status = popen4("#{gem_binary_path} list --local #{@new_resource.package_name}") do |pid, stdin, stdout, stderr| - stdout.each_line do |line| - next unless installed_versions = gem_list_parse(line) - # If the version we are asking for is installed, make that our current - # version. Otherwise, go ahead and use the highest one, which - # happens to come first in the array. - if installed_versions.detect { |v| v == @new_resource.version } - Chef::Log.debug("#{@new_resource.package_name} at version #{@new_resource.version}") - @current_resource.version(@new_resource.version) - else - iv = installed_versions.first - Chef::Log.debug("#{@new_resource.package_name} at version #{iv}") - @current_resource.version(iv) - end - end - end - - unless status.exitstatus == 0 - raise Chef::Exceptions::Package, "#{gem_binary_path} list --local failed - #{status.inspect}!" - end + + ## Find out if the gem is installed and what the version is + pending + # First, we need to look up whether we have the local gem installed or not + # status = popen4("#{gem_binary_path} list --local #{@new_resource.package_name}") do |pid, stdin, stdout, stderr| + # stdout.each_line do |line| + # next unless installed_versions = gem_list_parse(line) + # # If the version we are asking for is installed, make that our current + # # version. Otherwise, go ahead and use the highest one, which + # # happens to come first in the array. + # if installed_versions.detect { |v| v == @new_resource.version } + # Chef::Log.debug("#{@new_resource.package_name} at version #{@new_resource.version}") + # @current_resource.version(@new_resource.version) + # else + # iv = installed_versions.first + # Chef::Log.debug("#{@new_resource.package_name} at version #{iv}") + # @current_resource.version(iv) + # end + # end + # end + + # unless status.exitstatus == 0 + # raise Chef::Exceptions::Package, "#{gem_binary_path} list --local failed - #{status.inspect}!" + # end + @current_resource end @@ -76,7 +136,7 @@ class Chef stdout.each_line do |line| next unless installed_versions = gem_list_parse(line) Chef::Log.debug("candidate_version: remote rubygem(s) available: #{installed_versions.inspect}") - + unless installed_versions.empty? Chef::Log.debug("candidate_version: setting install candidate version to #{installed_versions.first}") @candidate_version = installed_versions.first @@ -90,21 +150,21 @@ class Chef end @candidate_version end - + def install_package(name, version) src = nil if @new_resource.source src = " --source=#{@new_resource.source} --source=http://rubygems.org" - end + end run_command_with_systems_locale( :command => "#{gem_binary_path} install #{name} -q --no-rdoc --no-ri -v \"#{version}\"#{src}#{opts}" ) end - + def upgrade_package(name, version) install_package(name, version) end - + def remove_package(name, version) if version run_command_with_systems_locale( @@ -116,17 +176,17 @@ class Chef ) end end - + def purge_package(name, version) remove_package(name, version) end - + private - + def opts expand_options(@new_resource.options) end - + end end end diff --git a/chef/spec/unit/provider/package/rubygems_spec.rb b/chef/spec/unit/provider/package/rubygems_spec.rb index 0400615323..31681a820a 100644 --- a/chef/spec/unit/provider/package/rubygems_spec.rb +++ b/chef/spec/unit/provider/package/rubygems_spec.rb @@ -16,62 +16,123 @@ # See the License for the specific language governing permissions and # limitations under the License. # +require 'pp' require File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "spec_helper")) +require 'ostruct' -describe Chef::Provider::Package::Rubygems do - before(:each) do - @node = Chef::Node.new - @run_context = Chef::RunContext.new(@node, {}) - @new_resource = Chef::Resource::GemPackage.new("nokogiri") - @new_resource.version "1.4.1" - @provider = Chef::Provider::Package::Rubygems.new(@new_resource, @run_context) +class String + undef_method :version +end + +describe Chef::Provider::Package::Rubygems::CurrentGemEnvironment do + before do + @gem_env = Chef::Provider::Package::Rubygems::CurrentGemEnvironment.new end - describe "when selecting the gem binary to use" do - it "should return a relative path to gem if no gem_binary is given" do - @provider.gem_binary_path.should == "gem" - end + it "determines the gem paths from the in memory rubygems" do + @gem_env.gem_paths.should == Gem.path + end - it "should return a specific path to gem if a gem_binary is given" do - @new_resource.gem_binary "/opt/local/bin/custom/ruby" - @provider.gem_binary_path.should == "/opt/local/bin/custom/ruby" - end + it "determines the installed versions of gems from Gem.source_index" do + gems = [Gem::Specification.new('rspec', Gem::Version.new('1.2.9')), Gem::Specification.new('rspec', Gem::Version.new('1.3.0'))] + Gem.source_index.should_receive(:search).with(Gem::Dependency.new('rspec', nil)).and_return(gems) + @gem_env.installed_versions('rspec').should == [Gem::Version.new('1.2.9'), Gem::Version.new('1.3.0')] end - describe "loading the current state" do - it "determines the installed versions of gems" do - gem_list = "nokogiri (2.3.5, 2.2.2, 1.2.6)" - @provider.gem_list_parse(gem_list).should == %w{2.3.5 2.2.2 1.2.6} - end + it "determines the installed versions of gems from the source index (part2: the unmockening)" do + @gem_env.installed_versions('rspec').should include(Gem::Version.new(Spec::VERSION::STRING)) end - describe "determining the candidate version" do - it "parses the available versions as reported by rubygems 1.3.6 and lower" do - gem_list = "nokogiri (1.4.1)\nnokogiri-happymapper (0.3.3)" - @provider.gem_list_parse(gem_list).should == ['1.4.1'] - end +end - it "parses the available versions as reported by rubygems 1.3.7 and newer" do - gem_list = "nokogiri (1.4.1 ruby java x86-mingw32 x86-mswin32)\nnokogiri-happymapper (0.3.3)\n" - @provider.gem_list_parse(gem_list).should == ['1.4.1'] - end +describe Chef::Provider::Package::Rubygems::AlternateGemEnvironment do + before do + @gem_env = Chef::Provider::Package::Rubygems::AlternateGemEnvironment.new('/usr/weird/bin/gem') + end + it "determines the gem paths from shelling out to gem env" do + gem_env_output = ['/path/to/gems', '/another/path/to/gems'].join(File::PATH_SEPARATOR) + shell_out_result = OpenStruct.new(:stdout => gem_env_output) + @gem_env.should_receive(:shell_out!).with('/usr/weird/bin/gem env gempath').and_return(shell_out_result) + @gem_env.gem_paths.should == ['/path/to/gems', '/another/path/to/gems'] + end + + it "builds the gems source index from the gem paths" do + Gem::SourceIndex.should_receive(:from_gems_in).with('/path/to/gems/specifications', '/another/path/to/gems/specifications') + @gem_env.stub!(:gem_paths).and_return(['/path/to/gems', '/another/path/to/gems']) + @gem_env.gem_source_index + end + + it "determines the installed versions of gems from the source index" do + gems = [Gem::Specification.new('rspec', Gem::Version.new('1.2.9')), Gem::Specification.new('rspec', Gem::Version.new('1.3.0'))] + @gem_env.stub!(:gem_source_index).and_return(Gem.source_index) + @gem_env.gem_source_index.should_receive(:search).with(Gem::Dependency.new('rspec', nil)).and_return(gems) + @gem_env.installed_versions('rspec').should == [Gem::Version.new('1.2.9'), Gem::Version.new('1.3.0')] end - describe "when installing a gem" do - it "should run gem install with the package name and version" do - @provider.should_receive(:run_command).with( - :command => "gem install rspec -q --no-rdoc --no-ri -v \"1.2.2\"", - :environment => {"LC_ALL" => nil}) - @provider.install_package("rspec", "1.2.2") - end + it "determines the installed versions of gems from the source index (part2: the unmockening)" do + path_to_gem = `which gem`.strip + pending("cant find your gem executable") if path_to_gem.empty? + gem_env = Chef::Provider::Package::Rubygems::AlternateGemEnvironment.new(path_to_gem) + gem_env.installed_versions('rspec').should include(Gem::Version.new(Spec::VERSION::STRING)) + end - it "installs gems with arbitrary options set by resource's options" do - @new_resource.options "-i /arbitrary/install/dir" - @provider.should_receive(:run_command_with_systems_locale). - with(:command => "gem install rspec -q --no-rdoc --no-ri -v \"1.2.2\" -i /arbitrary/install/dir") - @provider.install_package("rspec", "1.2.2") - end +end + +describe Chef::Provider::Package::Rubygems do + before(:each) do + @node = Chef::Node.new + @new_resource = Chef::Resource::GemPackage.new("nokogiri") + @new_resource.version "1.4.1" + @run_context = Chef::RunContext.new(@node, {}) + + @provider = Chef::Provider::Package::Rubygems.new(@new_resource, @run_context) + end + + it "uses the CurrentGemEnvironment implementation when no gem_binary_path is provided" do + @provider.gem_implementation.should be_a_kind_of(Chef::Provider::Package::Rubygems::CurrentGemEnvironment) end + + it "uses the AlternateGemEnvironment implementation when a gem_binary_path is provided" do + @new_resource.gem_binary('/usr/weird/bin/gem') + provider = Chef::Provider::Package::Rubygems.new(@new_resource, @run_context) + provider.gem_implementation.gem_binary_location.should == '/usr/weird/bin/gem' + end + + # describe "loading the current state" do + # it "determines the installed versions of gems" do + # gem_list = "nokogiri (2.3.5, 2.2.2, 1.2.6)" + # @provider.gem_list_parse(gem_list).should == %w{2.3.5 2.2.2 1.2.6} + # end + # end + # + # describe "determining the candidate version" do + # it "parses the available versions as reported by rubygems 1.3.6 and lower" do + # gem_list = "nokogiri (1.4.1)\nnokogiri-happymapper (0.3.3)" + # @provider.gem_list_parse(gem_list).should == ['1.4.1'] + # end + # + # it "parses the available versions as reported by rubygems 1.3.7 and newer" do + # gem_list = "nokogiri (1.4.1 ruby java x86-mingw32 x86-mswin32)\nnokogiri-happymapper (0.3.3)\n" + # @provider.gem_list_parse(gem_list).should == ['1.4.1'] + # end + # + # end + # + # describe "when installing a gem" do + # it "should run gem install with the package name and version" do + # @provider.should_receive(:run_command).with( + # :command => "gem install rspec -q --no-rdoc --no-ri -v \"1.2.2\"", + # :environment => {"LC_ALL" => nil}) + # @provider.install_package("rspec", "1.2.2") + # end + # + # it "installs gems with arbitrary options set by resource's options" do + # @new_resource.options "-i /arbitrary/install/dir" + # @provider.should_receive(:run_command_with_systems_locale). + # with(:command => "gem install rspec -q --no-rdoc --no-ri -v \"1.2.2\" -i /arbitrary/install/dir") + # @provider.install_package("rspec", "1.2.2") + # end + # end end |