diff --git a/lib/puppet/provider/package/gem.rb b/lib/puppet/provider/package/gem.rb index 2cd907b7f..98b5ce67f 100755 --- a/lib/puppet/provider/package/gem.rb +++ b/lib/puppet/provider/package/gem.rb @@ -1,121 +1,123 @@ require 'puppet/provider/package' require 'uri' # Ruby gems support. Puppet::Type.type(:package).provide :gem, :parent => Puppet::Provider::Package do desc "Ruby Gem support. If a URL is passed via `source`, then that URL is used as the remote gem repository; if a source is present but is not a valid URL, it will be interpreted as the path to a local gem file. If source is not present at all, the gem will be installed from the default gem repositories." has_feature :versionable commands :gemcmd => "gem" def self.gemlist(options) gem_list_command = [command(:gemcmd), "list"] if options[:local] gem_list_command << "--local" else gem_list_command << "--remote" end if name = options[:justme] gem_list_command << name + "$" end begin - list = execute(gem_list_command).lines.map {|set| gemsplit(set) } + list = execute(gem_list_command).lines. + map {|set| gemsplit(set) }. + reject {|item| item.nil? } rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not list gems: #{detail}" end if options[:justme] return list.shift else return list end end def self.gemsplit(desc) # `gem list` when output console has a line like: # *** LOCAL GEMS *** # but when it's not to the console that line # and all blank lines are stripped # so we don't need to check for them if desc =~ /^(\S+)\s+\((.+)\)/ name = $1 versions = $2.split(/,\s*/) { :name => name, :ensure => versions, :provider => :gem } else - Puppet.warning "Could not match #{desc}" + Puppet.warning "Could not match #{desc}" unless desc.chomp.empty? nil end end def self.instances(justme = false) gemlist(:local => true).collect do |hash| new(hash) end end def install(useversion = true) command = [command(:gemcmd), "install"] command << "-v" << resource[:ensure] if (! resource[:ensure].is_a? Symbol) and useversion # Always include dependencies command << "--include-dependencies" if source = resource[:source] begin uri = URI.parse(source) rescue => detail fail "Invalid source '#{uri}': #{detail}" end case uri.scheme when nil # no URI scheme => interpret the source as a local file command << source when /file/i command << uri.path when 'puppet' # we don't support puppet:// URLs (yet) raise Puppet::Error.new("puppet:// URLs are not supported as gem sources") else # interpret it as a gem repository command << "--source" << "#{source}" << resource[:name] end else command << "--no-rdoc" << "--no-ri" << resource[:name] end output = execute(command) # Apparently some stupid gem versions don't exit non-0 on failure self.fail "Could not install: #{output.chomp}" if output.include?("ERROR") end def latest # This always gets the latest version available. hash = self.class.gemlist(:justme => resource[:name]) hash[:ensure] end def query self.class.gemlist(:justme => resource[:name], :local => true) end def uninstall gemcmd "uninstall", "-x", "-a", resource[:name] end def update self.install(false) end end diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index c898296e8..9d1e56367 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,521 +1,520 @@ # A module to collect utility functions. require 'English' -require 'puppet/util/monkey_patches' require 'puppet/external/lock' require 'puppet/error' require 'puppet/util/execution_stub' require 'uri' require 'sync' require 'monitor' require 'tempfile' require 'pathname' module Puppet - module Util + require 'puppet/util/monkey_patches' require 'benchmark' # These are all for backward compatibility -- these are methods that used # to be in Puppet::Util but have been moved into external modules. require 'puppet/util/posix' extend Puppet::Util::POSIX @@sync_objects = {}.extend MonitorMixin def self.activerecord_version if (defined?(::ActiveRecord) and defined?(::ActiveRecord::VERSION) and defined?(::ActiveRecord::VERSION::MAJOR) and defined?(::ActiveRecord::VERSION::MINOR)) ([::ActiveRecord::VERSION::MAJOR, ::ActiveRecord::VERSION::MINOR].join('.').to_f) else 0 end end # Run some code with a specific environment. Resets the environment back to # what it was at the end of the code. def self.withenv(hash) saved = ENV.to_hash hash.each do |name, val| ENV[name.to_s] = val end yield ensure ENV.clear saved.each do |name, val| ENV[name] = val end end # Execute a given chunk of code with a new umask. def self.withumask(mask) cur = File.umask(mask) begin yield ensure File.umask(cur) end end def self.synchronize_on(x,type) sync_object,users = 0,1 begin @@sync_objects.synchronize { (@@sync_objects[x] ||= [Sync.new,0])[users] += 1 } @@sync_objects[x][sync_object].synchronize(type) { yield } ensure @@sync_objects.synchronize { @@sync_objects.delete(x) unless (@@sync_objects[x][users] -= 1) > 0 } end end # Change the process to a different user def self.chuser if group = Puppet[:group] begin Puppet::Util::SUIDManager.change_group(group, true) rescue => detail Puppet.warning "could not change to group #{group.inspect}: #{detail}" $stderr.puts "could not change to group #{group.inspect}" # Don't exit on failed group changes, since it's # not fatal #exit(74) end end if user = Puppet[:user] begin Puppet::Util::SUIDManager.change_user(user, true) rescue => detail $stderr.puts "Could not change to user #{user}: #{detail}" exit(74) end end end # Create instance methods for each of the log levels. This allows # the messages to be a little richer. Most classes will be calling this # method. def self.logmethods(klass, useself = true) Puppet::Util::Log.eachlevel { |level| klass.send(:define_method, level, proc { |args| args = args.join(" ") if args.is_a?(Array) if useself Puppet::Util::Log.create( :level => level, :source => self, :message => args ) else Puppet::Util::Log.create( :level => level, :message => args ) end }) } end # Proxy a bunch of methods to another object. def self.classproxy(klass, objmethod, *methods) classobj = class << klass; self; end methods.each do |method| classobj.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # Proxy a bunch of methods to another object. def self.proxy(klass, objmethod, *methods) methods.each do |method| klass.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end def benchmark(*args) msg = args.pop level = args.pop object = nil if args.empty? if respond_to?(level) object = self else object = Puppet end else object = args.pop end raise Puppet::DevError, "Failed to provide level to :benchmark" unless level unless level == :none or object.respond_to? level raise Puppet::DevError, "Benchmarked object does not respond to #{level}" end # Only benchmark if our log level is high enough if level != :none and Puppet::Util::Log.sendlevel?(level) result = nil seconds = Benchmark.realtime { yield } object.send(level, msg + (" in %0.2f seconds" % seconds)) return seconds else yield end end def which(bin) if absolute_path?(bin) return bin if FileTest.file? bin and FileTest.executable? bin else ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir| begin dest = File.expand_path(File.join(dir, bin)) rescue ArgumentError => e # if the user's PATH contains a literal tilde (~) character and HOME is not set, we may get # an ArgumentError here. Let's check to see if that is the case; if not, re-raise whatever error # was thrown. raise e unless ((dir =~ /~/) && ((ENV['HOME'].nil? || ENV['HOME'] == ""))) # if we get here they have a tilde in their PATH. We'll issue a single warning about this and then # ignore this path element and carry on with our lives. Puppet::Util::Warnings.warnonce("PATH contains a ~ character, and HOME is not set; ignoring PATH element '#{dir}'.") next end if Puppet.features.microsoft_windows? && File.extname(dest).empty? exts = ENV['PATHEXT'] exts = exts ? exts.split(File::PATH_SEPARATOR) : %w[.COM .EXE .BAT .CMD] exts.each do |ext| destext = File.expand_path(dest + ext) return destext if FileTest.file? destext and FileTest.executable? destext end end return dest if FileTest.file? dest and FileTest.executable? dest end end nil end module_function :which # Determine in a platform-specific way whether a path is absolute. This # defaults to the local platform if none is specified. def absolute_path?(path, platform=nil) # Escape once for the string literal, and once for the regex. slash = '[\\\\/]' name = '[^\\\\/]+' regexes = { :windows => %r!^(([A-Z]:#{slash})|(#{slash}#{slash}#{name}#{slash}#{name})|(#{slash}#{slash}\?#{slash}#{name}))!i, :posix => %r!^/!, } require 'puppet' platform ||= Puppet.features.microsoft_windows? ? :windows : :posix !! (path =~ regexes[platform]) end module_function :absolute_path? # Convert a path to a file URI def path_to_uri(path) return unless path params = { :scheme => 'file' } if Puppet.features.microsoft_windows? path = path.gsub(/\\/, '/') if unc = /^\/\/([^\/]+)(\/[^\/]+)/.match(path) params[:host] = unc[1] path = unc[2] elsif path =~ /^[a-z]:\//i path = '/' + path end end params[:path] = URI.escape(path) begin URI::Generic.build(params) rescue => detail raise Puppet::Error, "Failed to convert '#{path}' to URI: #{detail}" end end module_function :path_to_uri # Get the path component of a URI def uri_to_path(uri) return unless uri.is_a?(URI) path = URI.unescape(uri.path) if Puppet.features.microsoft_windows? and uri.scheme == 'file' if uri.host path = "//#{uri.host}" + path # UNC else path.sub!(/^\//, '') end end path end module_function :uri_to_path # Create an exclusive lock. def threadlock(resource, type = Sync::EX) Puppet::Util.synchronize_on(resource,type) { yield } end module_function :benchmark def memory unless defined?(@pmap) @pmap = which('pmap') end if @pmap %x{#{@pmap} #{Process.pid}| grep total}.chomp.sub(/^\s*total\s+/, '').sub(/K$/, '').to_i else 0 end end def symbolize(value) if value.respond_to? :intern value.intern else value end end def symbolizehash(hash) newhash = {} hash.each do |name, val| if name.is_a? String newhash[name.intern] = val else newhash[name] = val end end end def symbolizehash!(hash) hash.each do |name, val| if name.is_a? String hash[name.intern] = val hash.delete(name) end end hash end module_function :symbolize, :symbolizehash, :symbolizehash! # Just benchmark, with no logging. def thinmark seconds = Benchmark.realtime { yield } seconds end module_function :memory, :thinmark # Because IO#binread is only available in 1.9 def binread(file) File.open(file, 'rb') { |f| f.read } end module_function :binread # utility method to get the current call stack and format it to a human-readable string (which some IDEs/editors # will recognize as links to the line numbers in the trace) def self.pretty_backtrace(backtrace = caller(1)) backtrace.collect do |line| file_path, line_num = line.split(":") file_path = expand_symlinks(File.expand_path(file_path)) file_path + ":" + line_num end .join("\n") end # utility method that takes a path as input, checks each component of the path to see if it is a symlink, and expands # it if it is. returns the expanded path. def self.expand_symlinks(file_path) file_path.split("/").inject do |full_path, next_dir| next_path = full_path + "/" + next_dir if File.symlink?(next_path) then link = File.readlink(next_path) next_path = case link when /^\// then link else File.expand_path(full_path + "/" + link) end end next_path end end # Replace a file, securely. This takes a block, and passes it the file # handle of a file open for writing. Write the replacement content inside # the block and it will safely replace the target file. # # This method will make no changes to the target file until the content is # successfully written and the block returns without raising an error. # # As far as possible the state of the existing file, such as mode, is # preserved. This works hard to avoid loss of any metadata, but will result # in an inode change for the file. # # Arguments: `filename`, `default_mode` # # The filename is the file we are going to replace. # # The default_mode is the mode to use when the target file doesn't already # exist; if the file is present we copy the existing mode/owner/group values # across. def replace_file(file, default_mode, &block) raise Puppet::DevError, "replace_file requires a block" unless block_given? file = Pathname(file) tempfile = Tempfile.new(file.basename.to_s, file.dirname.to_s) file_exists = file.exist? # If the file exists, use its current mode/owner/group. If it doesn't, use # the supplied mode, and default to current user/group. if file_exists if Puppet.features.microsoft_windows? mode = Puppet::Util::Windows::Security.get_mode(file.to_s) uid = Puppet::Util::Windows::Security.get_owner(file.to_s) gid = Puppet::Util::Windows::Security.get_owner(file.to_s) else stat = file.lstat mode = stat.mode uid = stat.uid gid = stat.gid end # We only care about the four lowest-order octets. Higher octets are # filesystem-specific. mode &= 07777 else mode = default_mode uid = Process.euid gid = Process.egid end # Set properties of the temporary file before we write the content, because # Tempfile doesn't promise to be safe from reading by other people, just # that it avoids races around creating the file. if Puppet.features.microsoft_windows? Puppet::Util::Windows::Security.set_mode(mode, tempfile.path) Puppet::Util::Windows::Security.set_owner(uid, tempfile.path) Puppet::Util::Windows::Security.set_group(gid, tempfile.path) else tempfile.chmod(mode) tempfile.chown(uid, gid) end # OK, now allow the caller to write the content of the file. yield tempfile # Now, make sure the data (which includes the mode) is safe on disk. tempfile.flush begin tempfile.fsync rescue NotImplementedError # fsync may not be implemented by Ruby on all platforms, but # there is absolutely no recovery path if we detect that. So, we just # ignore the return code. # # However, don't be fooled: that is accepting that we are running in # an unsafe fashion. If you are porting to a new platform don't stub # that out. end tempfile.close File.rename(tempfile.path, file) # Ideally, we would now fsync the directory as well, but Ruby doesn't # have support for that, and it doesn't matter /that/ much... # Return something true, and possibly useful. file end module_function :replace_file # Executes a block of code, wrapped with some special exception handling. Causes the ruby interpreter to # exit if the block throws an exception. # # @param [String] message a message to log if the block fails # @param [Integer] code the exit code that the ruby interpreter should return if the block fails # @yield def exit_on_fail(message, code = 1) yield rescue ArgumentError, RuntimeError, NotImplementedError => detail Puppet.log_exception(detail, "Could not #{message}: #{detail}") exit(code) end module_function :exit_on_fail ####################################################################################################### # Deprecated methods relating to process execution; these have been moved to Puppet::Util::Execution ####################################################################################################### def execpipe(command, failonfail = true, &block) Puppet.deprecation_warning("Puppet::Util.execpipe is deprecated; please use Puppet::Util::Execution.execpipe") Puppet::Util::Execution.execpipe(command, failonfail, &block) end module_function :execpipe def execfail(command, exception) Puppet.deprecation_warning("Puppet::Util.execfail is deprecated; please use Puppet::Util::Execution.execfail") Puppet::Util::Execution.execfail(command, exception) end module_function :execfail def execute(command, arguments = {}) Puppet.deprecation_warning("Puppet::Util.execute is deprecated; please use Puppet::Util::Execution.execute") Puppet::Util::Execution.execute(command, arguments) end module_function :execute end end require 'puppet/util/errors' require 'puppet/util/methodhelper' require 'puppet/util/metaid' require 'puppet/util/classgen' require 'puppet/util/docs' require 'puppet/util/execution' require 'puppet/util/logging' require 'puppet/util/package' require 'puppet/util/warnings' diff --git a/lib/puppet/util/monkey_patches.rb b/lib/puppet/util/monkey_patches.rb index 58e5401d3..a59332504 100644 --- a/lib/puppet/util/monkey_patches.rb +++ b/lib/puppet/util/monkey_patches.rb @@ -1,207 +1,207 @@ +require 'puppet/util' +module Puppet::Util::MonkeyPatches +end unless defined? JRUBY_VERSION Process.maxgroups = 1024 end module RDoc def self.caller(skip=nil) in_gem_wrapper = false Kernel.caller.reject { |call| in_gem_wrapper ||= call =~ /#{Regexp.escape $0}:\d+:in `load'/ } end end require "yaml" require "puppet/util/zaml.rb" class Symbol def to_zaml(z) z.emit("!ruby/sym ") to_s.to_zaml(z) end def <=> (other) self.to_s <=> other.to_s end unless method_defined? "<=>" end [Object, Exception, Integer, Struct, Date, Time, Range, Regexp, Hash, Array, Float, String, FalseClass, TrueClass, Symbol, NilClass, Class].each { |cls| cls.class_eval do def to_yaml(ignored=nil) ZAML.dump(self) end end } def YAML.dump(*args) ZAML.dump(*args) end # # Workaround for bug in MRI 1.8.7, see # http://redmine.ruby-lang.org/issues/show/2708 # for details # if RUBY_VERSION == '1.8.7' class NilClass def closed? true end end end class Object # ActiveSupport 2.3.x mixes in a dangerous method # that can cause rspec to fork bomb # and other strange things like that. def daemonize raise NotImplementedError, "Kernel.daemonize is too dangerous, please don't try to use it." end end # Workaround for yaml_initialize, which isn't supported before Ruby # 1.8.3. if RUBY_VERSION == '1.8.1' || RUBY_VERSION == '1.8.2' YAML.add_ruby_type( /^object/ ) { |tag, val| type, obj_class = YAML.read_type_class( tag, Object ) r = YAML.object_maker( obj_class, val ) if r.respond_to? :yaml_initialize r.instance_eval { instance_variables.each { |name| remove_instance_variable name } } r.yaml_initialize(tag, val) end r } end class Array # Ruby < 1.8.7 doesn't have this method but we use it in tests def combination(num) return [] if num < 0 || num > size return [[]] if num == 0 return map{|e| [e] } if num == 1 tmp = self.dup self[0, size - (num - 1)].inject([]) do |ret, e| tmp.shift ret += tmp.combination(num - 1).map{|a| a.unshift(e) } end end unless method_defined? :combination alias :count :length unless method_defined? :count end class Symbol def to_proc Proc.new { |*args| args.shift.__send__(self, *args) } end unless method_defined? :to_proc end - class String - def lines(separator = $/) - lines = split(separator) - block_given? and lines.each {|line| yield line } - lines - end unless method_defined? :lines + unless method_defined? :lines + require 'puppet/util/monkey_patches/lines' + include Puppet::Util::MonkeyPatches::Lines + end end class IO - def lines(separator = $/) - lines = split(separator) - block_given? and lines.each {|line| yield line } - lines - end unless method_defined? :lines + unless method_defined? :lines + require 'puppet/util/monkey_patches/lines' + include Puppet::Util::MonkeyPatches::Lines + end def self.binread(name, length = nil, offset = 0) File.open(name, 'rb') do |f| f.seek(offset) if offset > 0 f.read(length) end end unless singleton_methods.include?(:binread) def self.binwrite(name, string, offset = 0) File.open(name, 'wb') do |f| f.write(offset > 0 ? string[offset..-1] : string) end end unless singleton_methods.include?(:binwrite) end class Range def intersection(other) raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range) return unless other === self.first || self === other.first start = [self.first, other.first].max if self.exclude_end? && self.last <= other.last start ... self.last elsif other.exclude_end? && self.last >= other.last start ... other.last else start .. [ self.last, other.last ].min end end unless method_defined? :intersection alias_method :&, :intersection unless method_defined? :& end # Ruby 1.8.5 doesn't have tap module Kernel def tap yield(self) self end unless method_defined?(:tap) end ######################################################################## # The return type of `instance_variables` changes between Ruby 1.8 and 1.9 # releases; it used to return an array of strings in the form "@foo", but # now returns an array of symbols in the form :@foo. # # Nothing else in the stack cares which form you get - you can pass the # string or symbol to things like `instance_variable_set` and they will work # transparently. # # Having the same form in all releases of Puppet is a win, though, so we # pick a unification and enforce than on all releases. That way developers # who do set math on them (eg: for YAML rendering) don't have to handle the # distinction themselves. # # In the sane tradition, we bring older releases into conformance with newer # releases, so we return symbols rather than strings, to be more like the # future versions of Ruby are. # # We also carefully support reloading, by only wrapping when we don't # already have the original version of the method aliased away somewhere. if RUBY_VERSION[0,3] == '1.8' unless Object.respond_to?(:puppet_original_instance_variables) # Add our wrapper to the method. class Object alias :puppet_original_instance_variables :instance_variables def instance_variables puppet_original_instance_variables.map(&:to_sym) end end # The one place that Ruby 1.8 assumes something about the return format of # the `instance_variables` method is actually kind of odd, because it uses # eval to get at instance variables of another object. # # This takes the original code and applies replaces instance_eval with # instance_variable_get through it. All other bugs in the original (such # as equality depending on the instance variables having the same order # without any promise from the runtime) are preserved. --daniel 2012-03-11 require 'resolv' class Resolv::DNS::Resource def ==(other) # :nodoc: return self.class == other.class && self.instance_variables == other.instance_variables && self.instance_variables.collect {|name| self.instance_variable_get name} == other.instance_variables.collect {|name| other.instance_variable_get name} end end end end diff --git a/lib/puppet/util/monkey_patches/lines.rb b/lib/puppet/util/monkey_patches/lines.rb new file mode 100644 index 000000000..e54e90d86 --- /dev/null +++ b/lib/puppet/util/monkey_patches/lines.rb @@ -0,0 +1,13 @@ +require 'puppet/util/monkey_patches' +require 'enumerator' + +module Puppet::Util::MonkeyPatches::Lines + def lines(separator = $/) + if block_given? + self.each_line(separator) {|line| yield line } + return self + else + return enum_for(:each_line, separator) + end + end +end diff --git a/spec/unit/provider/package/pkg_spec.rb b/spec/unit/provider/package/pkg_spec.rb index 50a87cea3..fe0977702 100755 --- a/spec/unit/provider/package/pkg_spec.rb +++ b/spec/unit/provider/package/pkg_spec.rb @@ -1,85 +1,85 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Type.type(:package).provider(:pkg) do before :each do @resource = Puppet::Resource.new(:package, 'dummy', :parameters => {:name => 'dummy', :ensure => :latest}) @provider = described_class.new(@resource) end def self.it_should_respond_to(*actions) actions.each do |action| it "should respond to :#{action}" do @provider.should respond_to(action) end end end it_should_respond_to :install, :uninstall, :update, :query, :latest it "should not be versionable" do described_class.should_not be_versionable end it "should use :install to update" do @provider.expects(:install) @provider.update end describe "when calling instances" do it "should correctly parse lines with preferred publisher" do described_class.expects(:pkg).with(:list,'-H').returns File.read(my_fixture('simple')) @instances = described_class.instances.map { |p| Hash.new(:name => p.get(:name), :ensure => p.get(:ensure)) } @instances.size.should == 4 @instances[0].should == Hash.new(:name => 'SUNPython', :ensure => :present) @instances[1].should == Hash.new(:name => 'SUNWbind', :ensure => :present) @instances[2].should == Hash.new(:name => 'SUNWdistro-license-copyright', :ensure => :present) @instances[3].should == Hash.new(:name => 'SUNWfppd', :ensure => :present) end it "should correctly parse lines with non preferred publisher" do described_class.expects(:pkg).with(:list,'-H').returns File.read(my_fixture('publisher')) @instances = described_class.instances.map { |p| Hash.new(:name => p.get(:name), :ensure => p.get(:ensure)) } @instances.size.should == 2 @instances[0].should == Hash.new(:name => 'SUNWpcre', :ensure => :present) @instances[1].should == Hash.new(:name => 'service/network/ssh', :ensure => :present) end it "should warn about incorrect lines" do fake_output = File.read(my_fixture('incomplete')) - error_line = fake_output.lines[0] + error_line = fake_output.split($/).first described_class.expects(:pkg).with(:list,'-H').returns fake_output described_class.expects(:warning).with "Failed to match 'pkg list' line #{error_line.inspect}" described_class.instances end end describe "when query a package" do it "should find the package" do @provider.stubs(:pkg).with(:list,'-H','dummy').returns File.read(my_fixture('dummy')) @provider.query.should == { :name => 'dummy', :ensure => :present, :version => '2.5.5-0.111', :status => "installed", :provider => :pkg, } end it "should return :absent when the package is not found" do # I dont know what the acutal error looks like, but according to type/pkg.rb we're just # reacting on the Exception anyways @provider.expects(:pkg).with(:list, "-H", "dummy").raises Puppet::ExecutionFailure, 'Not found' @provider.query.should == {:ensure => :absent, :name => "dummy"} end it "should return :absent when the packageline cannot be parsed" do @provider.stubs(:pkg).with(:list,'-H','dummy').returns File.read(my_fixture('incomplete')) @provider.query.should == { :name => 'dummy', :ensure => :absent } end end end diff --git a/spec/unit/util/monkey_patches/lines_spec.rb b/spec/unit/util/monkey_patches/lines_spec.rb new file mode 100755 index 000000000..1ea379182 --- /dev/null +++ b/spec/unit/util/monkey_patches/lines_spec.rb @@ -0,0 +1,83 @@ +#!/usr/bin/env rspec +require 'spec_helper' +require 'puppet/util/monkey_patches/lines' + +class Puppet::Util::MonkeyPatches::Lines::TestHelper < String + include Puppet::Util::MonkeyPatches::Lines +end + +describe Puppet::Util::MonkeyPatches::Lines::TestHelper do + ["\n", "\n\n", "$", "$$", "@", "@@"].each do |sep| + context "with #{sep.inspect}" do + it "should delegate to each_line if given a block" do + subject.expects(:each_line).with(sep) + subject.lines(sep) do |x| nil end + end + + it "should delegate to each_line without a block" do + subject.expects(:enum_for).with(:each_line, sep) + subject.lines(sep) + end + + context "with one line" do + context "with trailing separator" do + subject { described_class.new("foo" + sep) } + + it "should yield the string" do + got = [] + subject.lines(sep) do |x| got << x end + got.should == ["foo#{sep}"] + end + + it "should return the string" do + subject.lines(sep).to_a.should == ["foo#{sep}"] + end + end + + context "without trailing separator" do + subject { described_class.new("foo") } + + it "should yield the string" do + got = [] + subject.lines(sep) do |x| got << x end + got.should == ["foo"] + end + + it "should return the string" do + subject.lines(sep).to_a.should == ["foo"] + end + end + end + + context "with multiple lines" do + context "with trailing separator" do + subject { described_class.new("foo#{sep}bar#{sep}baz#{sep}") } + + it "should yield the strings" do + got = [] + subject.lines(sep) do |x| got << x end + got.should == ["foo#{sep}", "bar#{sep}", "baz#{sep}"] + end + + it "should return the strings" do + subject.lines(sep).to_a.should == ["foo#{sep}", "bar#{sep}", "baz#{sep}"] + end + end + + context "without trailing separator" do + subject { described_class.new("foo#{sep}bar#{sep}baz") } + + it "should yield the strings" do + got = [] + subject.lines(sep) do |x| got << x end + got.should == ["foo#{sep}", "bar#{sep}", "baz"] + end + + it "should return the strings" do + subject.lines(sep).to_a.should == ["foo#{sep}", "bar#{sep}", "baz"] + end + end + end + end + end +end