From 02bbb1aeef2dcf720967ac476002877344e88f80 Mon Sep 17 00:00:00 2001 From: Lamont Granquist Date: Fri, 8 Jan 2016 12:14:20 -0800 Subject: chocolatey multipackage provider --- lib/chef/provider/package/chocolatey.rb | 230 ++++++++++++++ lib/chef/providers.rb | 3 +- lib/chef/resource/chocolatey_package.rb | 37 +++ lib/chef/resources.rb | 3 +- spec/unit/provider/package/chocolatey_spec.rb | 422 ++++++++++++++++++++++++++ spec/unit/provider/package/zypper_spec.rb | 2 +- spec/unit/resource/chocolatey_package_spec.rb | 67 ++++ 7 files changed, 761 insertions(+), 3 deletions(-) create mode 100644 lib/chef/provider/package/chocolatey.rb create mode 100644 lib/chef/resource/chocolatey_package.rb create mode 100644 spec/unit/provider/package/chocolatey_spec.rb create mode 100644 spec/unit/resource/chocolatey_package_spec.rb diff --git a/lib/chef/provider/package/chocolatey.rb b/lib/chef/provider/package/chocolatey.rb new file mode 100644 index 0000000000..3f52370939 --- /dev/null +++ b/lib/chef/provider/package/chocolatey.rb @@ -0,0 +1,230 @@ +# +# Copyright:: Copyright (c) 2015 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 'chef/provider/package' +require 'chef/resource/chocolatey_package' +require 'chef/mixin/powershell_out' + +class Chef + class Provider + class Package + class Chocolatey < Chef::Provider::Package + include Chef::Mixin::PowershellOut + + provides :chocolatey_package, os: "windows" + + # Declare that our arguments should be arrays + use_multipackage_api + + # Responsible for building the current_resource. + # + # @return [Chef::Resource::ChocolateyPackage] the current_resource + def load_current_resource + @current_resource = Chef::Resource::ChocolateyPackage.new(new_resource.name) + current_resource.package_name(new_resource.package_name) + current_resource.version(build_current_versions) + current_resource + end + + # Lazy initializer for candidate_version. A nil value means that there is no candidate + # version and the package is not installable (generally an error). + # + # @return [Array] list of candidate_versions indexed same as new_resource.package_name/version + def candidate_version + @candidate_version ||= build_candidate_versions + end + + # Install multiple packages via choco.exe + # + # @param names [Array] array of package names to install + # @param versions [Array] array of versions to install + def install_package(names, versions) + name_versions_to_install = desired_name_versions.select { |n, v| names.include?(n) } + + name_nil_versions = name_versions_to_install.select { |n,v| v.nil? } + name_has_versions = name_versions_to_install.reject { |n,v| v.nil? } + + # choco does not support installing multiple packages with version pins + name_has_versions.each do |name, version| + choco_command("install -y -version", version, cmd_args, name) + end + + # but we can do all the ones without version pins at once + unless name_nil_versions.empty? + cmd_names = name_nil_versions.keys + choco_command("install -y", cmd_args, *cmd_names) + end + end + + # Upgrade multiple packages via choco.exe + # + # @param names [Array] array of package names to install + # @param versions [Array] array of versions to install + def upgrade_package(names, versions) + name_versions_to_install = desired_name_versions.select { |n, v| names.include?(n) } + + name_nil_versions = name_versions_to_install.select { |n,v| v.nil? } + name_has_versions = name_versions_to_install.reject { |n,v| v.nil? } + + # choco does not support installing multiple packages with version pins + name_has_versions.each do |name, version| + choco_command("upgrade -y -version", version, cmd_args, name) + end + + # but we can do all the ones without version pins at once + unless name_nil_versions.empty? + cmd_names = name_nil_versions.keys + choco_command("upgrade -y", cmd_args, *cmd_names) + end + end + + # Remove multiple packages via choco.exe + # + # @param names [Array] array of package names to install + # @param versions [Array] array of versions to install + def remove_package(names, versions) + choco_command("uninstall -y", cmd_args, *names) + end + + # Support :uninstall as an action in order for users to easily convert + # from the `chocolatey` provider in the cookbook. It is, however, + # already deprecated. + def action_uninstall + Chef::Log.deprecation "The use of action :uninstall on the chocolatey_package provider is deprecated, please use :remove" + action_remove + end + + # Choco does not have dpkg's distinction between purge and remove + alias_method :purge_package, :remove_package + + # Override the superclass check. The semantics for our new_resource.source is not files to + # install from, but like the rubygem provider's sources which are more like repos. + def check_resource_semantics! + end + + private + + # Magic to find where chocolatey is installed in the system, and to + # return the full path of choco.exe + # + # @return [String] full path of choco.exe + def choco_exe + @choco_exe ||= + ::File.join( + powershell_out!( + "[System.Environment]::GetEnvironmentVariable('ChocolateyInstall', 'MACHINE')" + ).stdout.chomp, + 'bin', + 'choco.exe' + ) + end + + # Helper to dispatch a choco command through shell_out using the timeout + # set on the new resource, with nice command formatting. + # + # @param args [String] variable number of string arguments + # @return [Mixlib::ShellOut] object returned from shell_out! + def choco_command(*args) + shell_out_with_timeout!(args_to_string(choco_exe, *args)) + end + + # Use the available_packages Hash helper to create an array suitable for + # using in candidate_version + # + # @return [Array] list of candidate_version, same index as new_resource.package_name/version + def build_candidate_versions + new_resource.package_name.map do |package_name| + available_packages[package_name.downcase] + end + end + + # Use the installed_packages Hash helper to create an array suitable for + # using in current_resource.version + # + # @return [Array] list of candidate_version, same index as new_resource.package_name/version + def build_current_versions + new_resource.package_name.map do |package_name| + installed_packages[package_name.downcase] + end + end + + # Helper to construct Hash of names-to-versions, requested on the new_resource. + # If new_resource.version is nil, then all values will be nil. + # + # @return [Hash] Mapping of requested names to versions + def desired_name_versions + desired_versions = new_resource.version || new_resource.package_name.map { nil } + Hash[*new_resource.package_name.zip(desired_versions).flatten] + end + + # Helper to construct optional args out of new_resource + # + # @return [String] options from new_resource or empty string + def cmd_args + cmd_args = [ new_resource.options ] + cmd_args.push( "-source #{new_resource.source}" ) if new_resource.source + args_to_string(*cmd_args) + end + + # Helper to nicely convert variable string args into a single command line. It + # will compact nulls or empty strings and join arguments with single spaces, without + # introducing any double-spaces for missing args. + # + # @param args [String] variable number of string arguments + # @return [String] nicely concatenated string or empty string + def args_to_string(*args) + args.reject {|i| i.nil? || i == "" }.join(" ") + end + + # Available packages in chocolatey as a Hash of names mapped to versions + # (names are downcased for case-insensitive matching) + # + # @return [Hash] name-to-version mapping of available packages + def available_packages + @available_packages ||= + begin + cmd = [ "list -r #{package_name_array.join ' '}" ] + cmd.push( "-source #{new_resource.source}" ) if new_resource.source + parse_list_output(*cmd) + end + end + + # Installed packages in chocolatey as a Hash of names mapped to versions + # (names are downcased for case-insensitive matching) + # + # @return [Hash] name-to-version mapping of installed packages + def installed_packages + @installed_packages ||= parse_list_output("list -l -r") + end + + # Helper to convert choco.exe list output to a Hash + # (names are downcased for case-insenstive matching) + # + # @param cmd [String] command to run + # @return [String] list output converted to ruby Hash + def parse_list_output(*args) + hash = {} + choco_command(*args).stdout.each_line do |line| + name, version = line.split('|') + hash[name.downcase] = version.chomp + end + hash + end + end + end + end +end diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index f5e7a0f989..b4eeabd48d 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -1,6 +1,6 @@ # # Author:: Daniel DeLeo () -# Copyright:: Copyright (c) 2010 Opscode, Inc. +# Copyright:: Copyright (c) 2010-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -57,6 +57,7 @@ require 'chef/provider/whyrun_safe_ruby_block' require 'chef/provider/env/windows' require 'chef/provider/package/apt' +require 'chef/provider/package/chocolatey' require 'chef/provider/package/dpkg' require 'chef/provider/package/easy_install' require 'chef/provider/package/freebsd/port' diff --git a/lib/chef/resource/chocolatey_package.rb b/lib/chef/resource/chocolatey_package.rb new file mode 100644 index 0000000000..2e29c1722e --- /dev/null +++ b/lib/chef/resource/chocolatey_package.rb @@ -0,0 +1,37 @@ +# +# Author:: Adam Jacob () +# Copyright:: Copyright (c) 2008-2015 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 'chef/resource/package' + +class Chef + class Resource + class ChocolateyPackage < Chef::Resource::Package + + provides :chocolatey_package, os: "windows" + + def initialize(name, run_context=nil) + super + @resource_name = :chocolatey_package + end + + property :package_name, [String, Array], coerce: proc { |x| [x].flatten } + + property :version, [String, Array], coerce: proc { |x| [x].flatten } + end + end +end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index f699d95ace..cc5b9be8c6 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -1,6 +1,6 @@ # # Author:: Daniel DeLeo () -# Copyright:: Copyright (c) 2010 Opscode, Inc. +# Copyright:: Copyright (c) 2010-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,7 @@ require 'chef/resource/batch' require 'chef/resource/breakpoint' require 'chef/resource/cookbook_file' require 'chef/resource/chef_gem' +require 'chef/resource/chocolatey_package' require 'chef/resource/cron' require 'chef/resource/csh' require 'chef/resource/deploy' diff --git a/spec/unit/provider/package/chocolatey_spec.rb b/spec/unit/provider/package/chocolatey_spec.rb new file mode 100644 index 0000000000..d2d6fabb19 --- /dev/null +++ b/spec/unit/provider/package/chocolatey_spec.rb @@ -0,0 +1,422 @@ +# +# Author:: Adam Jacob () +# Copyright:: Copyright (c) 2008-2015 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::Provider::Package::Chocolatey do + let(:timeout) { 900 } + + let(:new_resource) { Chef::Resource::ChocolateyPackage.new("git") } + + let(:provider) do + node = Chef::Node.new + events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, events) + Chef::Provider::Package::Chocolatey.new(new_resource, run_context) + end + + let(:choco_exe) { 'C:\ProgramData\chocolatey\bin\choco.exe' } + + # installed packages (ConEmu is upgradable) + let(:local_list_stdout) do + <<-EOF +chocolatey|0.9.9.11 +ConEmu|15.10.25.0 + EOF + end + + before do + allow(provider).to receive(:choco_exe).and_return(choco_exe) + local_list_obj = double(:stdout => local_list_stdout) + allow(provider).to receive(:shell_out!).with("#{choco_exe} list -l -r", {:timeout => timeout}).and_return(local_list_obj) + end + + def allow_remote_list(package_names, args = nil) + remote_list_stdout = <<-EOF +chocolatey|0.9.9.11 +ConEmu|15.10.25.1 +git|2.6.2 +munin-node|1.6.1.20130823 + EOF + remote_list_obj = double(stdout: remote_list_stdout) + allow(provider).to receive(:shell_out!).with("#{choco_exe} list -r #{package_names.join ' '}#{args}", {timeout: timeout}).and_return(remote_list_obj) + end + + describe "#initialize" do + it "should return the correct class" do + expect(provider).to be_kind_of(Chef::Provider::Package::Chocolatey) + end + + it "should support arrays" do + expect(provider.use_multipackage_api?).to be true + end + end + + describe "#candidate_version" do + it "should set the candidate_version correctly" do + allow_remote_list(["git"]) + expect(provider.candidate_version).to eql(["2.6.2"]) + end + + it "should set the candidate_version to nil if there is no candidate" do + allow_remote_list(["vim"]) + new_resource.package_name("vim") + expect(provider.candidate_version).to eql([nil]) + end + + it "should set the candidate_version correctly when there are two packages to install" do + allow_remote_list(["ConEmu", "chocolatey"]) + new_resource.package_name(["ConEmu", "chocolatey"]) + expect(provider.candidate_version).to eql(["15.10.25.1", "0.9.9.11"]) + end + + it "should set the candidate_version correctly when only the first is installable" do + allow_remote_list(["ConEmu", "vim"]) + new_resource.package_name(["ConEmu", "vim"]) + expect(provider.candidate_version).to eql(["15.10.25.1", nil]) + end + + it "should set the candidate_version correctly when only the last is installable" do + allow_remote_list(["vim", "chocolatey"]) + new_resource.package_name(["vim", "chocolatey"]) + expect(provider.candidate_version).to eql([nil, "0.9.9.11"]) + end + + it "should set the candidate_version correctly when neither are is installable" do + allow_remote_list(["vim", "ruby"]) + new_resource.package_name(["vim", "ruby"]) + expect(provider.candidate_version).to eql([nil, nil]) + end + end + + describe "#load_current_resource" do + it "should return a current_resource" do + expect(provider.load_current_resource).to be_kind_of(Chef::Resource::ChocolateyPackage) + end + + it "should set the current_resource#package_name" do + provider.load_current_resource + expect(provider.current_resource.package_name).to eql(["git"]) + end + + it "should load and downcase names in the installed_packages hash" do + provider.load_current_resource + expect(provider.send(:installed_packages)).to eql( + {"chocolatey"=>"0.9.9.11", "conemu"=>"15.10.25.0"} + ) + end + + it "should load and downcase names in the available_packages hash" do + allow_remote_list(["git"]) + provider.load_current_resource + expect(provider.send(:available_packages)).to eql( + {"chocolatey"=>"0.9.9.11", "conemu"=>"15.10.25.1", "git"=>"2.6.2", "munin-node" => "1.6.1.20130823"} + ) + end + + it "should set the current_resource.version to nil when the package is not installed" do + provider.load_current_resource + expect(provider.current_resource.version).to eql([nil]) + end + + it "should set the current_resource.version to the installed version when the package is installed" do + new_resource.package_name("ConEmu") + provider.load_current_resource + expect(provider.current_resource.version).to eql(["15.10.25.0"]) + end + + it "should set the current_resource.version when there are two packages that are installed" do + new_resource.package_name(["ConEmu", "chocolatey"]) + provider.load_current_resource + expect(provider.current_resource.version).to eql(["15.10.25.0", "0.9.9.11"]) + end + + it "should set the current_resource.version correctly when only the first is installed" do + new_resource.package_name(["ConEmu", "git"]) + provider.load_current_resource + expect(provider.current_resource.version).to eql(["15.10.25.0", nil]) + end + + it "should set the current_resource.version correctly when only the last is installed" do + new_resource.package_name(["git", "chocolatey"]) + provider.load_current_resource + expect(provider.current_resource.version).to eql([nil, "0.9.9.11"]) + end + + it "should set the current_resource.version correctly when none are installed" do + new_resource.package_name(["git", "vim"]) + provider.load_current_resource + expect(provider.current_resource.version).to eql([nil, nil]) + end + end + + describe "#action_install" do + it "should install a single package" do + allow_remote_list(["git"]) + provider.load_current_resource + expect(provider).to receive(:shell_out!).with("#{choco_exe} install -y git", {:timeout=>timeout}).and_return(double) + provider.run_action(:install) + expect(new_resource).to be_updated_by_last_action + end + + context "when changing the timeout to 3600" do + let(:timeout) { 3600 } + it "sets the timeout on shell_out commands" do + allow_remote_list(["git"]) + new_resource.timeout(timeout) + provider.load_current_resource + expect(provider).to receive(:shell_out!).with("#{choco_exe} install -y git", {:timeout=>timeout}).and_return(double) + provider.run_action(:install) + expect(new_resource).to be_updated_by_last_action + end + end + + it "should not install packages that are up-to-date" do + allow_remote_list(["chocolatey"]) + new_resource.package_name("chocolatey") + provider.load_current_resource + expect(provider).not_to receive(:install_package) + provider.run_action(:install) + expect(new_resource).not_to be_updated_by_last_action + end + + it "should not upgrade packages" do + allow_remote_list(["ConEmu"]) + new_resource.package_name("ConEmu") + provider.load_current_resource + expect(provider).not_to receive(:install_package) + provider.run_action(:install) + expect(new_resource).not_to be_updated_by_last_action + end + + it "should upgrade packages when given a version pin" do + allow_remote_list(["ConEmu"]) + new_resource.package_name("ConEmu") + new_resource.version("15.10.25.1") + provider.load_current_resource + expect(provider).to receive(:shell_out!).with("#{choco_exe} install -y -version 15.10.25.1 ConEmu", {:timeout=>timeout}).and_return(double) + provider.run_action(:install) + expect(new_resource).to be_updated_by_last_action + end + + it "should handle complicated cases when the name/version array is pruned" do + # chocolatey will be pruned by the superclass out of the args to install_package and we + # implicitly test that we correctly pick up new_resource.version[1] instead of + # new_version.resource[0] + allow_remote_list(["chocolatey", "ConEmu"]) + new_resource.package_name(["chocolatey", "ConEmu"]) + new_resource.version([nil, "15.10.25.1"]) + provider.load_current_resource + expect(provider).to receive(:shell_out!).with("#{choco_exe} install -y -version 15.10.25.1 ConEmu", {:timeout=>timeout}).and_return(double) + provider.run_action(:install) + expect(new_resource).to be_updated_by_last_action + end + + it "should be case-insensitive" do + allow_remote_list(["conemu"]) + new_resource.package_name("conemu") + new_resource.version("15.10.25.1") + provider.load_current_resource + expect(provider).to receive(:shell_out!).with("#{choco_exe} install -y -version 15.10.25.1 conemu", {:timeout=>timeout}).and_return(double) + provider.run_action(:install) + expect(new_resource).to be_updated_by_last_action + end + + it "should split up commands when given two packages, one with a version pin" do + allow_remote_list(["ConEmu", "git"]) + new_resource.package_name(["ConEmu", "git"]) + new_resource.version(["15.10.25.1", nil]) + provider.load_current_resource + expect(provider).to receive(:shell_out!).with("#{choco_exe} install -y -version 15.10.25.1 ConEmu", {:timeout=>timeout}).and_return(double) + expect(provider).to receive(:shell_out!).with("#{choco_exe} install -y git", {:timeout=>timeout}).and_return(double) + provider.run_action(:install) + expect(new_resource).to be_updated_by_last_action + end + + it "should do multipackage installs when given two packages without constraints" do + allow_remote_list(["git", "munin-node"]) + new_resource.package_name(["git", "munin-node"]) + provider.load_current_resource + expect(provider).to receive(:shell_out!).with("#{choco_exe} install -y git munin-node", {:timeout=>timeout}).and_return(double) + provider.run_action(:install) + expect(new_resource).to be_updated_by_last_action + end + + context "when passing a source argument" do + it "should pass options into the install command" do + allow_remote_list(["git"], " -source localpackages") + new_resource.source("localpackages") + provider.load_current_resource + expect(provider).to receive(:shell_out!).with("#{choco_exe} install -y -source localpackages git", {:timeout=>timeout}).and_return(double) + provider.run_action(:install) + expect(new_resource).to be_updated_by_last_action + end + end + + it "should pass options into the install command" do + allow_remote_list(["git"]) + new_resource.options("-force") + provider.load_current_resource + expect(provider).to receive(:shell_out!).with("#{choco_exe} install -y -force git", {:timeout=>timeout}).and_return(double) + provider.run_action(:install) + expect(new_resource).to be_updated_by_last_action + end + + it "installing a package that does not exist throws an error" do + allow_remote_list(["package-does-not-exist"]) + new_resource.package_name("package-does-not-exist") + provider.load_current_resource + expect { provider.run_action(:install) }.to raise_error(Chef::Exceptions::Package) + end + + it "installing multiple packages with a package that does not exist throws an error" do + allow_remote_list(["git", "package-does-not-exist"]) + new_resource.package_name(["git", "package-does-not-exist"]) + provider.load_current_resource + expect { provider.run_action(:install) }.to raise_error(Chef::Exceptions::Package) + end + end + + describe "#action_upgrade" do + it "should install a package that is not installed" do + allow_remote_list(["git"]) + provider.load_current_resource + expect(provider).to receive(:shell_out!).with("#{choco_exe} upgrade -y git", {:timeout=>timeout}).and_return(double) + provider.run_action(:upgrade) + expect(new_resource).to be_updated_by_last_action + end + + it "should upgrade a package that is installed but upgradable" do + allow_remote_list(["ConEmu"]) + new_resource.package_name("ConEmu") + provider.load_current_resource + expect(provider).to receive(:shell_out!).with("#{choco_exe} upgrade -y ConEmu", {:timeout=>timeout}).and_return(double) + provider.run_action(:upgrade) + expect(new_resource).to be_updated_by_last_action + end + + it "should be case insensitive" do + allow_remote_list(["conemu"]) + new_resource.package_name("conemu") + provider.load_current_resource + expect(provider).to receive(:shell_out!).with("#{choco_exe} upgrade -y conemu", {:timeout=>timeout}).and_return(double) + provider.run_action(:upgrade) + expect(new_resource).to be_updated_by_last_action + end + + it "should not install a package that is up-to-date" do + allow_remote_list(["chocolatey"]) + new_resource.package_name("chocolatey") + provider.load_current_resource + expect(provider).not_to receive(:shell_out!).with("#{choco_exe} upgrade -y chocolatey", {:timeout=>timeout}) + provider.run_action(:upgrade) + expect(new_resource).not_to be_updated_by_last_action + end + + it "version pins work as well" do + allow_remote_list(["git"]) + new_resource.version("99.99.99.99") + provider.load_current_resource + expect(provider).to receive(:shell_out!).with("#{choco_exe} upgrade -y -version 99.99.99.99 git", {:timeout=>timeout}) + provider.run_action(:upgrade) + expect(new_resource).to be_updated_by_last_action + end + + it "upgrading multiple packages uses a single command" do + allow_remote_list(["conemu", "git"]) + new_resource.package_name(["conemu", "git"]) + expect(provider).to receive(:shell_out!).with("#{choco_exe} upgrade -y conemu git", {:timeout=>timeout}).and_return(double) + provider.run_action(:upgrade) + expect(new_resource).to be_updated_by_last_action + end + + it "upgrading a package that does not exist throws an error" do + allow_remote_list(["package-does-not-exist"]) + new_resource.package_name("package-does-not-exist") + provider.load_current_resource + expect { provider.run_action(:upgrade) }.to raise_error(Chef::Exceptions::Package) + end + + it "upgrading multiple packages with a package that does not exist throws an error" do + allow_remote_list(["git", "package-does-not-exist"]) + new_resource.package_name(["git", "package-does-not-exist"]) + provider.load_current_resource + expect { provider.run_action(:upgrade) }.to raise_error(Chef::Exceptions::Package) + end + end + + describe "#action_remove" do + it "does nothing when the package is already removed" do + allow_remote_list(["git"]) + provider.load_current_resource + expect(provider).not_to receive(:remove_package) + provider.run_action(:remove) + expect(new_resource).not_to be_updated_by_last_action + end + + it "does nothing when all the packages are already removed" do + allow_remote_list(["git", "package-does-not-exist"]) + new_resource.package_name(["git", "package-does-not-exist"]) + provider.load_current_resource + expect(provider).not_to receive(:remove_package) + provider.run_action(:remove) + expect(new_resource).not_to be_updated_by_last_action + end + + it "removes a package" do + allow_remote_list(["ConEmu"]) + new_resource.package_name("ConEmu") + provider.load_current_resource + expect(provider).to receive(:shell_out!).with("#{choco_exe} uninstall -y ConEmu", {:timeout=>timeout}).and_return(double) + provider.run_action(:remove) + expect(new_resource).to be_updated_by_last_action + end + + it "is case-insensitive" do + allow_remote_list(["conemu"]) + new_resource.package_name("conemu") + provider.load_current_resource + expect(provider).to receive(:shell_out!).with("#{choco_exe} uninstall -y conemu", {:timeout=>timeout}).and_return(double) + provider.run_action(:remove) + expect(new_resource).to be_updated_by_last_action + end + + it "removes a single package when its the only one installed" do + pending "this is a bug in the superclass" + allow_remote_list(["git", "conemu"]) + new_resource.package_name(["git", "conemu"]) + provider.load_current_resource + expect(provider).to receive(:shell_out!).with("#{choco_exe} uninstall -y conemu", {:timeout=>timeout}).and_return(double) + provider.run_action(:remove) + expect(new_resource).to be_updated_by_last_action + end + end + + describe "#action_uninstall" do + it "should call :remove with a deprecation warning" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + expect(Chef::Log).to receive(:deprecation).with(/please use :remove/) + allow_remote_list(["ConEmu"]) + new_resource.package_name("ConEmu") + provider.load_current_resource + expect(provider).to receive(:remove_package) + provider.run_action(:uninstall) + expect(new_resource).to be_updated_by_last_action + end + end +end diff --git a/spec/unit/provider/package/zypper_spec.rb b/spec/unit/provider/package/zypper_spec.rb index 661bca4c1b..5fb3abe875 100644 --- a/spec/unit/provider/package/zypper_spec.rb +++ b/spec/unit/provider/package/zypper_spec.rb @@ -1,6 +1,6 @@ # # Author:: Adam Jacob () -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/spec/unit/resource/chocolatey_package_spec.rb b/spec/unit/resource/chocolatey_package_spec.rb new file mode 100644 index 0000000000..38f1d2905c --- /dev/null +++ b/spec/unit/resource/chocolatey_package_spec.rb @@ -0,0 +1,67 @@ +# +# Author:: Adam Jacob () +# Copyright:: Copyright (c) 2008-2015 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::Resource::ChocolateyPackage do + + let(:resource) { Chef::Resource::ChocolateyPackage.new("fakey_fakerton") } + + it "should create a new Chef::Resource::ChocolateyPackage" do + expect(resource).to be_a_kind_of(Chef::Resource) + expect(resource).to be_a_kind_of(Chef::Resource::Package) + expect(resource).to be_a_instance_of(Chef::Resource::ChocolateyPackage) + end + + it "should have a resource name of :python" do + expect(resource.resource_name).to eql(:chocolatey_package) + end + + it "should coerce its name to a package_name array" do + expect(resource.package_name).to eql(["fakey_fakerton"]) + end + + it "the package_name setter should coerce to arrays" do + resource.package_name("git") + expect(resource.package_name).to eql(["git"]) + end + + it "the package_name setter should accept arrays" do + resource.package_name(["git", "unzip"]) + expect(resource.package_name).to eql(["git", "unzip"]) + end + + it "the name should accept arrays" do + resource = Chef::Resource::ChocolateyPackage.new(["git", "unzip"]) + expect(resource.package_name).to eql(["git", "unzip"]) + end + + it "the default version should be nil" do + expect(resource.version).to eql(nil) + end + + it "the version setter should coerce to arrays" do + resource.version("1.2.3") + expect(resource.version).to eql(["1.2.3"]) + end + + it "the version setter should accept arrays" do + resource.version(["1.2.3", "4.5.6"]) + expect(resource.version).to eql(["1.2.3", "4.5.6"]) + end +end -- cgit v1.2.1