diff options
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | DOC_CHANGES.md | 29 | ||||
-rw-r--r-- | RELEASE_NOTES.md | 14 | ||||
-rw-r--r-- | lib/chef/provider/package/windows.rb | 80 | ||||
-rw-r--r-- | lib/chef/provider/package/windows/msi.rb | 69 | ||||
-rw-r--r-- | lib/chef/resource/windows_package.rb | 79 | ||||
-rw-r--r-- | lib/chef/resources.rb | 1 | ||||
-rw-r--r-- | lib/chef/win32/api/installer.rb | 166 | ||||
-rw-r--r-- | spec/unit/provider/package/windows/msi_spec.rb | 60 | ||||
-rw-r--r-- | spec/unit/provider/package/windows_spec.rb | 80 | ||||
-rw-r--r-- | spec/unit/resource/windows_package_spec.rb | 74 |
11 files changed, 653 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index a38ae9ffe5..27811f1a77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ which was accidentally removed in 11.0.0. * Don't save the run_list during `node.save` when running with override run list. (CHEF-4443) * Enable Content-Length validation for Chef::HTTP::Simple and fix issues around it. (CHEF-5041, CHEF-5100) +* Windows MSI Package Provider (CHEF-5087) ## Last Release: 11.10.0 (02/06/2014) diff --git a/DOC_CHANGES.md b/DOC_CHANGES.md index f29af23d30..e5f1c2a481 100644 --- a/DOC_CHANGES.md +++ b/DOC_CHANGES.md @@ -140,3 +140,32 @@ The default value of this setting is `false` feature. Enabling this on a client that connects to a 10.X API server will cause client registration to silently fail. Don't do it. +### Windows Installer (MSI) Package Provider + +The windows_package provider installs and removes Windows Installer (MSI) packages. +This provider utilizies the ProductCode extracted from the MSI package to determine +if the package is currently installed. + +You may use the ```package``` resource to use this provider, and you must use the +```package``` resource if you are also using the windows cookbook, which contains +the windows_package LWRP. + +#### Example + +``` +package "7zip" do + action :install + source 'C:\7z920.msi' +end +``` + +#### Actions +* :install +* :remove + +#### Attributes +* source - The location of the package to install. Default value: the ```name``` of the resource. +* options - Additional options that are passed to msiexec. +* installer_type - The type of package being installed. Can be auto-detected. Currently only :msi is supported. +* timeout - The time in seconds allowed for the package to successfully be installed. Defaults to 600 seconds. +* returns - Return codes that signal a successful installation. Defaults to 0. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 5d0f27b738..9856ddf660 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -89,6 +89,20 @@ recipe which is not a dependency of any cookbook specified in the run list will now log a warning with a message describing the problem and solution. In the future, this warning will become an error. +#### Windows MSI Package Provider + +The first windows package provider has been added to core Chef. It supports Windows Installer (MSI) files only, +and maintains idempotency by using the ProductCode from inside the MSI to determine if the products installation state. + +``` +package "install 7zip" do + action :install + source 'c:\downloads\7zip.msi' +end +``` + +You can continue to use the windows_package LWRP from the windows cookbook alongside this provider. + #### reboot_pending? We have added a ```reboot_pending?``` method to the recipe DSL. This method returns true or false if the operating system diff --git a/lib/chef/provider/package/windows.rb b/lib/chef/provider/package/windows.rb new file mode 100644 index 0000000000..be1de0b969 --- /dev/null +++ b/lib/chef/provider/package/windows.rb @@ -0,0 +1,80 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# 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 'chef/resource/windows_package' +require 'chef/provider/package' + +class Chef + class Provider + class Package + class Windows < Chef::Provider::Package + + # Depending on the installer, we may need to examine installer_type or + # source attributes, or search for text strings in the installer file + # binary to determine the installer type for the user. Since the file + # must be on disk to do so, we have to make this choice in the provider. + require 'chef/provider/package/windows/msi.rb' + + # load_current_resource is run in Chef::Provider#run_action when not in whyrun_mode? + def load_current_resource + @current_resource = Chef::Resource::WindowsPackage.new(@new_resource.name) + @current_resource.version(package_provider.installed_version) + @new_resource.version(package_provider.package_version) + @current_resource + end + + def package_provider + @package_provider ||= begin + case installer_type + when :msi + Chef::Provider::Package::Windows::MSI.new(@new_resource) + else + raise "Unable to find a Chef::Provider::Package::Windows provider for installer_type '#{installer_type}'" + end + end + end + + def installer_type + @installer_type ||= begin + if @new_resource.installer_type + @new_resource.installer_type + else + file_extension = ::File.basename(@new_resource.source).split(".").last.downcase + + if file_extension == "msi" + :msi + else + raise ArgumentError, "Installer type for Windows Package '#{@new_resource.name}' not specified and cannot be determined from file extension '#{file_extension}'" + end + end + end + end + + # Chef::Provider::Package action_install + action_remove call install_package + remove_package + # Pass those calls to the correct sub-provider + def install_package(name, version) + package_provider.install_package(name, version) + end + + def remove_package(name, version) + package_provider.remove_package(name, version) + end + end + end + end +end diff --git a/lib/chef/provider/package/windows/msi.rb b/lib/chef/provider/package/windows/msi.rb new file mode 100644 index 0000000000..0764a15901 --- /dev/null +++ b/lib/chef/provider/package/windows/msi.rb @@ -0,0 +1,69 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# 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. +# + +# TODO: Allow @new_resource.source to be a Product Code as a GUID for uninstall / network install + +require 'chef/mixin/shell_out' +require 'chef/win32/api/installer' + +class Chef + class Provider + class Package + class Windows + class MSI + include Chef::ReservedNames::Win32::API::Installer + include Chef::Mixin::ShellOut + + def initialize(resource) + @new_resource = resource + end + + # From Chef::Provider::Package + def expand_options(options) + options ? " #{options}" : "" + end + + # Returns a version if the package is installed or nil if it is not. + def installed_version + Chef::Log.debug("#{@new_resource} getting product code for package at #{@new_resource.source}") + product_code = get_product_property(@new_resource.source, "ProductCode") + Chef::Log.debug("#{@new_resource} checking package status and verion for #{product_code}") + get_installed_version(product_code) + end + + def package_version + Chef::Log.debug("#{@new_resource} getting product version for package at #{@new_resource.source}") + get_product_property(@new_resource.source, "ProductVersion") + end + + def install_package(name, version) + # We could use MsiConfigureProduct here, but we'll start off with msiexec + Chef::Log.debug("#{@new_resource} installing MSI package '#{@new_resource.source}'") + shell_out!("msiexec /qn /i \"#{@new_resource.source}\" #{expand_options(@new_resource.options)}", {:timeout => @new_resource.timeout, :returns => @new_resource.returns}) + end + + def remove_package(name, version) + # We could use MsiConfigureProduct here, but we'll start off with msiexec + Chef::Log.debug("#{@new_resource} removing MSI package '#{@new_resource.source}'") + shell_out!("msiexec /qn /x \"#{@new_resource.source}\" #{expand_options(@new_resource.options)}", {:timeout => @new_resource.timeout, :returns => @new_resource.returns}) + end + end + end + end + end +end diff --git a/lib/chef/resource/windows_package.rb b/lib/chef/resource/windows_package.rb new file mode 100644 index 0000000000..ff80b47115 --- /dev/null +++ b/lib/chef/resource/windows_package.rb @@ -0,0 +1,79 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# 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 'chef/resource/package' +require 'chef/provider/package/windows' +require 'chef/win32/error' + +class Chef + class Resource + class WindowsPackage < Chef::Resource::Package + + provides :package, :on_platforms => ["windows"] + + def initialize(name, run_context=nil) + super + @allowed_actions = [ :install, :remove ] + @provider = Chef::Provider::Package::Windows + @resource_name = :windows_package + @source ||= source(@package_name) + + # Unique to this resource + @installer_type = nil + @timeout = 600 + # In the past we accepted return code 127 for an unknown reason and 42 because of a bug + @returns = [ 0 ] + end + + def installer_type(arg=nil) + set_or_return( + :installer_type, + arg, + :kind_of => [ String ] + ) + end + + def timeout(arg=nil) + set_or_return( + :timeout, + arg, + :kind_of => [ String, Integer ] + ) + end + + def returns(arg=nil) + set_or_return( + :returns, + arg, + :kind_of => [ String, Integer, Array ] + ) + end + + def source(arg=nil) + if arg == nil && self.instance_variable_defined?(:@source) == true + @source + else + raise ArgumentError, "Bad type for WindowsPackage resource, use a String" unless arg.is_a?(String) + Chef::Log.debug("#{package_name}: sanitizing source path '#{arg}'") + @source = ::File.absolute_path(arg).gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR) + end + end + end + end +end + diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 76adb6f1e1..711becef8c 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -69,6 +69,7 @@ require 'chef/resource/template' require 'chef/resource/timestamped_deploy' require 'chef/resource/user' require 'chef/resource/whyrun_safe_ruby_block' +require 'chef/resource/windows_package' require 'chef/resource/yum_package' require 'chef/resource/lwrp_base' require 'chef/resource/bff_package' diff --git a/lib/chef/win32/api/installer.rb b/lib/chef/win32/api/installer.rb new file mode 100644 index 0000000000..745802d260 --- /dev/null +++ b/lib/chef/win32/api/installer.rb @@ -0,0 +1,166 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# 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 'chef/exceptions' +require 'chef/win32/api' +require 'chef/win32/error' +require 'pathname' + +class Chef + module ReservedNames::Win32 + module API + module Installer + extend Chef::ReservedNames::Win32 + extend Chef::ReservedNames::Win32::API + + ############################################### + # Win32 API Constants + ############################################### + + + ############################################### + # Win32 API Bindings + ############################################### + + ffi_lib 'msi' + +=begin +UINT MsiOpenPackage( + _In_ LPCTSTR szPackagePath, + _Out_ MSIHANDLE *hProduct +); +=end + safe_attach_function :msi_open_package, :MsiOpenPackageExA, [ :string, :int, :pointer ], :int + +=begin +UINT MsiGetProductProperty( + _In_ MSIHANDLE hProduct, + _In_ LPCTSTR szProperty, + _Out_ LPTSTR lpValueBuf, + _Inout_ DWORD *pcchValueBuf +); +=end + safe_attach_function :msi_get_product_property, :MsiGetProductPropertyA, [ :pointer, :pointer, :pointer, :pointer ], :int + +=begin +UINT MsiGetProductInfo( + _In_ LPCTSTR szProduct, + _In_ LPCTSTR szProperty, + _Out_ LPTSTR lpValueBuf, + _Inout_ DWORD *pcchValueBuf +); +=end + safe_attach_function :msi_get_product_info, :MsiGetProductInfoA, [ :pointer, :pointer, :pointer, :pointer ], :int + +=begin +UINT MsiCloseHandle( + _In_ MSIHANDLE hAny +); +=end + safe_attach_function :msi_close_handle, :MsiCloseHandle, [ :pointer ], :int + + ############################################### + # Helpers + ############################################### + + # Opens a Microsoft Installer (MSI) file from an absolute path and returns the specified property + def get_product_property(package_path, property_name) + pkg_ptr = open_package(package_path) + + buffer = 0.chr + buffer_length = FFI::Buffer.new(:long).write_long(0) + + # Fetch the length of the property + status = msi_get_product_property(pkg_ptr.read_pointer, property_name, buffer, buffer_length) + + # We expect error ERROR_MORE_DATA (234) here because we passed a buffer length of 0 + if status != 234 + msg = "msi_get_product_property: returned unknown error #{status} when retrieving #{property_name}: " + msg << Chef::ReservedNames::Win32::Error.format_message(status) + raise Chef::Exceptions::Package, msg + end + + buffer_length = FFI::Buffer.new(:long).write_long(buffer_length.read_long + 1) + buffer = 0.chr * buffer_length.read_long + + # Fetch the property + status = msi_get_product_property(pkg_ptr.read_pointer, property_name, buffer, buffer_length) + + if status != 0 + msg = "msi_get_product_property: returned unknown error #{status} when retrieving #{property_name}: " + msg << Chef::ReservedNames::Win32::Error.format_message(status) + raise Chef::Exceptions::Package, msg + end + + msi_close_handle(pkg_ptr.read_pointer) + return buffer + end + + # Opens a Microsoft Installer (MSI) file from an absolute path and returns a pointer to a handle + # Remember to close the handle with msi_close_handle() + def open_package(package_path) + # MsiOpenPackage expects a perfect absolute Windows path to the MSI + raise ArgumentError, "Provided path '#{package_path}' must be an absolute path" unless Pathname.new(package_path).absolute? + + pkg_ptr = FFI::MemoryPointer.new(:pointer, 4) + status = msi_open_package(package_path, 1, pkg_ptr) + case status + when 0 + # success + else + raise Chef::Exceptions::Package, "msi_open_package: unexpected status #{status}: #{Chef::ReservedNames::Win32::Error.format_message(status)}" + end + return pkg_ptr + end + + # All installed product_codes should have a VersionString + # Returns a version if installed, nil if not installed + def get_installed_version(product_code) + version = 0.chr + version_length = FFI::Buffer.new(:long).write_long(0) + + status = msi_get_product_info(product_code, "VersionString", version, version_length) + + return nil if status == 1605 # ERROR_UNKNOWN_PRODUCT (0x645) + + # We expect error ERROR_MORE_DATA (234) here because we passed a buffer length of 0 + if status != 234 + msg = "msi_get_product_info: product code '#{product_code}' returned unknown error #{status} when retrieving VersionString: " + msg << Chef::ReservedNames::Win32::Error.format_message(status) + raise Chef::Exceptions::Package, msg + end + + # We could fetch the product version now that we know the variable length, but we don't need it here. + + version_length = FFI::Buffer.new(:long).write_long(version_length.read_long + 1) + version = 0.chr * version_length.read_long + + status = msi_get_product_info(product_code, "VersionString", version, version_length) + + if status != 0 + msg = "msi_get_product_info: product code '#{product_code}' returned unknown error #{status} when retrieving VersionString: " + msg << Chef::ReservedNames::Win32::Error.format_message(status) + raise Chef::Exceptions::Package, msg + end + + version + end + end + end + end +end diff --git a/spec/unit/provider/package/windows/msi_spec.rb b/spec/unit/provider/package/windows/msi_spec.rb new file mode 100644 index 0000000000..69322a609d --- /dev/null +++ b/spec/unit/provider/package/windows/msi_spec.rb @@ -0,0 +1,60 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# 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::Provider::Package::Windows::MSI do + let(:node) { double('Chef::Node') } + let(:events) { double('Chef::Events').as_null_object } # mock all the methods + let(:run_context) { double('Chef::RunContext', :node => node, :events => events) } + let(:new_resource) { Chef::Resource::WindowsPackage.new("calculator.msi") } + let(:provider) { Chef::Provider::Package::Windows::MSI.new(new_resource) } + + describe "expand_options" do + it "returns an empty string if passed no options" do + expect(provider.expand_options(nil)).to eql "" + end + + it "returns a string with a leading space if passed options" do + expect(provider.expand_options("--train nope --town no_way")).to eql(" --train nope --town no_way") + end + end + + describe "installed_version" do + it "returns the installed version" do + provider.stub(:get_product_property).and_return("{23170F69-40C1-2702-0920-000001000000}") + provider.stub(:get_installed_version).with("{23170F69-40C1-2702-0920-000001000000}").and_return("3.14159.1337.42") + expect(provider.installed_version).to eql("3.14159.1337.42") + end + end + + describe "package_version" do + it "returns the version of a package" do + provider.stub(:get_product_property).with(/calculator.msi$/, "ProductVersion").and_return(42) + expect(provider.package_version).to eql(42) + end + end + + describe "install_package" do + # calls shell_out! + end + + describe "remove_package" do + # calls shell_out! + end +end diff --git a/spec/unit/provider/package/windows_spec.rb b/spec/unit/provider/package/windows_spec.rb new file mode 100644 index 0000000000..962bf6fddf --- /dev/null +++ b/spec/unit/provider/package/windows_spec.rb @@ -0,0 +1,80 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# 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::Provider::Package::Windows do + let(:node) { double('Chef::Node') } + let(:events) { double('Chef::Events').as_null_object } # mock all the methods + let(:run_context) { double('Chef::RunContext', :node => node, :events => events) } + let(:new_resource) { Chef::Resource::WindowsPackage.new("calculator.msi") } + let(:provider) { Chef::Provider::Package::Windows.new(new_resource, run_context) } + + describe "load_current_resource" do + before(:each) do + provider.stub(:package_provider).and_return(double('package_provider', + :installed_version => "1.0", :package_version => "2.0")) + end + + it "creates a current resource with the name of the new resource" do + provider.load_current_resource + expect(provider.current_resource).to be_a(Chef::Resource::WindowsPackage) + expect(provider.current_resource.name).to eql("calculator.msi") + end + + it "sets the current version if the package is installed" do + provider.load_current_resource + expect(provider.current_resource.version).to eql("1.0") + end + + it "sets the version to be installed" do + provider.load_current_resource + expect(provider.new_resource.version).to eql("2.0") + end + end + + describe "package_provider" do + it "sets the package provider to MSI if the the installer type is :msi" do + provider.stub(:installer_type).and_return(:msi) + expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::MSI) + end + + it "raises an error if the installer_type is unknown" do + provider.stub(:installer_type).and_return(:apt_for_windows) + expect { provider.package_provider }.to raise_error + end + end + + describe "installer_type" do + it "it returns @installer_type if it is set" do + provider.new_resource.installer_type("downeaster") + expect(provider.installer_type).to eql("downeaster") + end + + it "sets installer_type to msi if the source ends in .msi" do + provider.new_resource.source("microsoft_installer.msi") + expect(provider.installer_type).to eql(:msi) + end + + it "raises an error if it cannot determine the installer type" do + provider.new_resource.installer_type(nil) + provider.new_resource.source("tomfoolery.now") + expect { provider.installer_type }.to raise_error(ArgumentError) + end + end +end diff --git a/spec/unit/resource/windows_package_spec.rb b/spec/unit/resource/windows_package_spec.rb new file mode 100644 index 0000000000..23454e97e4 --- /dev/null +++ b/spec/unit/resource/windows_package_spec.rb @@ -0,0 +1,74 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# 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::Resource::WindowsPackage, "initialize" do + + let(:resource) { Chef::Resource::WindowsPackage.new("solitaire.msi") } + + it "returns a Chef::Resource::WindowsPackage" do + expect(resource).to be_a_kind_of(Chef::Resource::WindowsPackage) + end + + it "sets the resource_name to :windows_package" do + expect(resource.resource_name).to eql(:windows_package) + end + + it "sets the provider to Chef::Provider::Package::Windows" do + expect(resource.provider).to eql(Chef::Provider::Package::Windows) + end + + it "supports setting installer_type" do + resource.installer_type("msi") + expect(resource.installer_type).to eql("msi") + end + + # String, Integer + [ "600", 600 ].each do |val| + it "supports setting a timeout as a #{val.class}" do + resource.timeout(val) + expect(resource.timeout).to eql(val) + end + end + + # String, Integer, Array + [ "42", 42, [47, 48, 49] ].each do |val| + it "supports setting an alternate return value as a #{val.class}" do + resource.returns(val) + expect(resource.returns).to eql(val) + end + end + + it "coverts a source to an absolute path" do + ::File.stub(:absolute_path).and_return("c:\\Files\\frost.msi") + resource.source("frost.msi") + expect(resource.source).to eql "c:\\Files\\frost.msi" + end + + it "converts slashes to backslashes in the source path" do + ::File.stub(:absolute_path).and_return("c:\\frost.msi") + resource.source("c:/frost.msi") + expect(resource.source).to eql "c:\\frost.msi" + end + + it "defaults source to the resource name" do + # it's a little late to stub out File.absolute_path + expect(resource.source).to include("solitaire.msi") + end +end |