diff options
author | Luke Kanies <luke@madstop.com> | 2009-02-13 18:24:34 -0600 |
---|---|---|
committer | Luke Kanies <luke@madstop.com> | 2009-02-13 18:24:34 -0600 |
commit | 3fbec120768d84d208b14f574dfe916e25cfdbef (patch) | |
tree | 865d59f4ea9cf3782db46ce1ae7fd54b95945035 | |
parent | a2270b4a4f093c6c4f171dcf0c0e05fe101dd979 (diff) | |
parent | 2561c8e252dcf66890513458750bb1329a03beec (diff) | |
download | puppet-3fbec120768d84d208b14f574dfe916e25cfdbef.tar.gz puppet-3fbec120768d84d208b14f574dfe916e25cfdbef.tar.xz puppet-3fbec120768d84d208b14f574dfe916e25cfdbef.zip |
Merge branch '0.24.x'
Conflicts:
lib/puppet/indirector/facts/facter.rb
lib/puppet/provider/augeas/augeas.rb
lib/puppet/util/filetype.rb
spec/unit/indirector/facts/facter.rb
spec/unit/provider/augeas/augeas.rb
test/util/filetype.rb
25 files changed, 856 insertions, 344 deletions
@@ -14,6 +14,22 @@ Fixed #1840 - Bug fixes and improvements for Emacs puppet-mode.el 0.24.8 + Fixed #1831 - Added sprintf function + + Fixed #1830 - Added regsubst function + + Updated up2date and service confines to add support for Oracle EL and VM + + Fixing #1948 and #1953 - augeas ins bug: wrong number of arguments (1 for 3) + + Fixing #944 - changing error message from warning to info - connection recycled + + Fixed #961 - puppetd creating too many/not closing TCP connections + + Fixed #1959 - Added column protection for environment schema migration + + Fixing #1869 - autoloaded files should never leak exceptions + Fixing #1543 - Nagios parse errors no longer kill Puppet Fixed #1420 - nagios_serviceescalation not allowing host_name more than one type diff --git a/ext/vim/syntax/puppet.vim b/ext/vim/syntax/puppet.vim index 5a50f5ce0..e54d32aa8 100644 --- a/ext/vim/syntax/puppet.vim +++ b/ext/vim/syntax/puppet.vim @@ -16,7 +16,7 @@ elseif exists("b:current_syntax") finish endif -syn region puppetDefine start="^\s*\(class\|define\|site\|node\)" end="{" contains=puppetDefType,puppetDefName,puppetDefArguments +syn region puppetDefine start="^\s*\(class\|define\|node\)" end="{" contains=puppetDefType,puppetDefName,puppetDefArguments syn keyword puppetDefType class define site node inherits contained syn keyword puppetInherits inherits contained syn region puppetDefArguments start="(" end=")" contains=puppetArgument diff --git a/lib/puppet/external/nagios/base.rb b/lib/puppet/external/nagios/base.rb index d95b808dd..6a0c1831c 100755 --- a/lib/puppet/external/nagios/base.rb +++ b/lib/puppet/external/nagios/base.rb @@ -416,18 +416,20 @@ class Nagios::Base :dependent_service_description, :host_name, :hostgroup_name, :service_description, :inherits_parent, :execution_failure_criteria, :notification_failure_criteria, :dependency_period, - :register, :use + :register, :use, + :_naginator_name - setnamevar :service_description + setnamevar :_naginator_name end newtype :serviceescalation do setparameters :host_name, :hostgroup_name, :service_description, :contacts, :contact_groups, :first_notification, :last_notification, :notification_interval, :escalation_period, :escalation_options, - :register, :use + :register, :use, + :_naginator_name - setnamevar :service_description + setnamevar :_naginator_name end newtype :hostdependency do @@ -435,18 +437,20 @@ class Nagios::Base setparameters :dependent_host_name, :dependent_hostgroup_name, :host_name, :hostgroup_name, :inherits_parent, :execution_failure_criteria, :notification_failure_criteria, :dependency_period, - :register, :use + :register, :use, + :_naginator_name - setnamevar :host_name + setnamevar :_naginator_name end newtype :hostescalation do setparameters :host_name, :hostgroup_name, :contacts, :contact_groups, :first_notification, :last_notification, :notification_interval, :escalation_period, :escalation_options, - :register, :use + :register, :use, + :_naginator_name - setnamevar :host_name + setnamevar :_naginator_name end newtype :hostextinfo do @@ -463,9 +467,10 @@ class Nagios::Base setparameters :host_name, :service_description, :notes, :notes_url, :action_url, :icon_image, :icon_image_alt, - :register, :use + :register, :use, + :_naginator_name - setnamevar :service_description + setnamevar :_naginator_name end end diff --git a/lib/puppet/indirector/facts/facter.rb b/lib/puppet/indirector/facts/facter.rb index 6376b71ca..e664e17c7 100644 --- a/lib/puppet/indirector/facts/facter.rb +++ b/lib/puppet/indirector/facts/facter.rb @@ -8,8 +8,12 @@ class Puppet::Node::Facts::Facter < Puppet::Indirector::Code def self.load_fact_plugins - # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] - x = Puppet[:factpath].split(":").each do |dir| + # Add any per-module fact directories to the factpath + module_fact_dirs = Puppet[:modulepath].split(":").collect do |d| + Dir.glob("%s/*/plugins/facter" % d) + end.flatten + dirs = module_fact_dirs + Puppet[:factpath].split(":") + x = dirs.each do |dir| load_facts_in_dir(dir) end end diff --git a/lib/puppet/network/xmlrpc/client.rb b/lib/puppet/network/xmlrpc/client.rb index 37ace2101..678ab6c00 100644 --- a/lib/puppet/network/xmlrpc/client.rb +++ b/lib/puppet/network/xmlrpc/client.rb @@ -36,57 +36,7 @@ module Puppet::Network interface.methods.each { |ary| method = ary[0] newclient.send(:define_method,method) { |*args| - Puppet.debug "Calling %s.%s" % [namespace, method] - begin - call("%s.%s" % [namespace, method.to_s],*args) - rescue OpenSSL::SSL::SSLError => detail - if detail.message =~ /bad write retry/ - Puppet.warning "Transient SSL write error; restarting connection and retrying" - self.recycle_connection - retry - end - ["certificate verify failed", "hostname was not match", "hostname not match"].each do |str| - if detail.message.include?(str) - Puppet.warning "Certificate validation failed; consider using the certname configuration option" - end - end - raise XMLRPCClientError, - "Certificates were not trusted: %s" % detail - rescue ::XMLRPC::FaultException => detail - raise XMLRPCClientError, detail.faultString - rescue Errno::ECONNREFUSED => detail - msg = "Could not connect to %s on port %s" % - [@host, @port] - raise XMLRPCClientError, msg - rescue SocketError => detail - Puppet.err "Could not find server %s: %s" % - [@host, detail.to_s] - error = XMLRPCClientError.new( - "Could not find server %s" % @host - ) - error.set_backtrace detail.backtrace - raise error - rescue Errno::EPIPE, EOFError - Puppet.warning "Other end went away; restarting connection and retrying" - self.recycle_connection - retry - rescue Timeout::Error => detail - Puppet.err "Connection timeout calling %s.%s: %s" % - [namespace, method, detail.to_s] - error = XMLRPCClientError.new("Connection Timeout") - error.set_backtrace(detail.backtrace) - raise error - rescue => detail - if detail.message =~ /^Wrong size\. Was \d+, should be \d+$/ - Puppet.warning "XMLRPC returned wrong size. Retrying." - retry - end - Puppet.err "Could not call %s.%s: %s" % - [namespace, method, detail.inspect] - error = XMLRPCClientError.new(detail.to_s) - error.set_backtrace detail.backtrace - raise error - end + make_rpc_call(namespace, method, *args) } } @@ -97,13 +47,119 @@ module Puppet::Network @clients[handler] || self.mkclient(handler) end + class ErrorHandler + def initialize(&block) + metaclass.define_method(:execute, &block) + end + end + + # Use a class variable so all subclasses have access to it. + @@error_handlers = {} + + def self.error_handler(exception) + if handler = @@error_handlers[exception.class] + return handler + else + return @@error_handlers[:default] + end + end + + def self.handle_error(*exceptions, &block) + handler = ErrorHandler.new(&block) + + exceptions.each do |exception| + @@error_handlers[exception] = handler + end + end + + handle_error(OpenSSL::SSL::SSLError) do |client, detail, namespace, method| + if detail.message =~ /bad write retry/ + Puppet.warning "Transient SSL write error; restarting connection and retrying" + client.recycle_connection + return :retry + end + ["certificate verify failed", "hostname was not match", "hostname not match"].each do |str| + if detail.message.include?(str) + Puppet.warning "Certificate validation failed; consider using the certname configuration option" + end + end + raise XMLRPCClientError, "Certificates were not trusted: %s" % detail + end + + handle_error(:default) do |client, detail, namespace, method| + if detail.message.to_s =~ /^Wrong size\. Was \d+, should be \d+$/ + Puppet.warning "XMLRPC returned wrong size. Retrying." + return :retry + end + Puppet.err "Could not call %s.%s: %s" % [namespace, method, detail.inspect] + error = XMLRPCClientError.new(detail.to_s) + error.set_backtrace detail.backtrace + raise error + end + + handle_error(OpenSSL::SSL::SSLError) do |client, detail, namespace, method| + if detail.message =~ /bad write retry/ + Puppet.warning "Transient SSL write error; restarting connection and retrying" + client.recycle_connection + return :retry + end + ["certificate verify failed", "hostname was not match", "hostname not match"].each do |str| + if detail.message.include?(str) + Puppet.warning "Certificate validation failed; consider using the certname configuration option" + end + end + raise XMLRPCClientError, "Certificates were not trusted: %s" % detail + end + + handle_error(::XMLRPC::FaultException) do |client, detail, namespace, method| + raise XMLRPCClientError, detail.faultString + end + + handle_error(Errno::ECONNREFUSED) do |client, detail, namespace, method| + msg = "Could not connect to %s on port %s" % [client.host, client.port] + raise XMLRPCClientError, msg + end + + handle_error(SocketError) do |client, detail, namespace, method| + Puppet.err "Could not find server %s: %s" % [@host, detail.to_s] + error = XMLRPCClientError.new("Could not find server %s" % client.host) + error.set_backtrace detail.backtrace + raise error + end + + handle_error(Errno::EPIPE, EOFError) do |client, detail, namespace, method| + Puppet.info "Other end went away; restarting connection and retrying" + client.recycle_connection + return :retry + end + + handle_error(Timeout::Error) do |client, detail, namespace, method| + Puppet.err "Connection timeout calling %s.%s: %s" % [namespace, method, detail.to_s] + error = XMLRPCClientError.new("Connection Timeout") + error.set_backtrace(detail.backtrace) + raise error + end + + def make_rpc_call(namespace, method, *args) + Puppet.debug "Calling %s.%s" % [namespace, method] + begin + call("%s.%s" % [namespace, method.to_s],*args) + rescue Exception => detail + retry if self.class.error_handler(detail).execute(self, detail, namespace, method) == :retry + end + ensure + http.finish if http.started? + end + def http unless @http - @http = Puppet::Network::HttpPool.http_instance(@host, @port, true) + @http = Puppet::Network::HttpPool.http_instance(host, port, true) end @http end + attr_reader :host, :port + def initialize(hash = {}) hash[:Path] ||= "/RPC2" hash[:Server] ||= Puppet[:server] @@ -135,7 +191,11 @@ module Puppet::Network # or we've just downloaded certs and we need to create new http instances # with the certs added. def recycle_connection - @http = Puppet::Network::HttpPool.http_instance(@host, @port, true) # reset the instance + if http.started? + http.finish + end + @http = nil + self.http # force a new one end def start diff --git a/lib/puppet/parser/functions/regsubst.rb b/lib/puppet/parser/functions/regsubst.rb new file mode 100644 index 000000000..067d75c51 --- /dev/null +++ b/lib/puppet/parser/functions/regsubst.rb @@ -0,0 +1,93 @@ +module Puppet::Parser::Functions + newfunction(:regsubst, :type => :rvalue, + :doc => "\ + Perform regexp replacement on a string. + + Parameters (in order): + + :str: + The string to operate on. + + :regexp: + The regular expression matching the string. If you want it + anchored at the start and/or end of the string, you must do + that with ^ and $ yourself. + + :replacement: + Replacement string. Can contain back references to what was + matched using \\0, \\1, and so on. + + :flags: + Optional. String of single letter flags for how the regexp + is interpreted: + + - **E** + Extended regexps + - **I** + Ignore case in regexps + - **M** + Multiline regexps + - **G** + Global replacement; all occurances of the regexp in + the string will be replaced. Without this, only the + first occurance will be replaced. + + :lang: + Optional. How to handle multibyte characters. A + single-character string with the following values: + + - **N** + None + - **E** + EUC + - **S** + SJIS + - **U** + UTF-8 + + **Examples** + + Get the third octet from the node's IP address: :: + + $i3 = regsubst($ipaddress, + '^([0-9]+)[.]([0-9]+)[.]([0-9]+)[.]([0-9]+)$', + '\\\\3') + + Put angle brackets around each octet in the node's IP address: :: + + $x = regsubst($ipaddress, '([0-9]+)', '<\\\\1>', 'G') + ") \ + do |args| + flag_mapping = { + "E" => Regexp::EXTENDED, + "I" => Regexp::IGNORECASE, + "M" => Regexp::MULTILINE, + } + if args.length < 3 or args.length > 5 + raise Puppet::ParseError, ("regsub(): wrong number of arguments" + + " (#{args.length}; min 3, max 5)") + end + str, regexp, replacement, flags, lang = args + reflags = 0 + global = false + (flags or "").each_byte do |f| + f = f.chr + if f == "G" + global = true + else + fvalue = flag_mapping[f] + if !fvalue + raise Puppet::ParseError, "regsub(): bad flag `#{f}'" + end + reflags |= fvalue + end + end + re = Regexp.compile(regexp, reflags, lang) + if global + result = str.gsub(re, replacement) + else + result = str.sub(re, replacement) + end + return result + end +end diff --git a/lib/puppet/parser/functions/sprintf.rb b/lib/puppet/parser/functions/sprintf.rb new file mode 100644 index 000000000..79744104d --- /dev/null +++ b/lib/puppet/parser/functions/sprintf.rb @@ -0,0 +1,17 @@ +module Puppet::Parser::Functions + newfunction(:sprintf, :type => :rvalue, + :doc => "\ + Perform printf-style formatting of text. + + The first parameter is format string describing how the rest of the + parameters should be formatted. See the documentation for the + ``Kernel::sprintf()`` function in Ruby for all the details. + ") \ + do |args| + if args.length < 1 + raise Puppet::ParseError, 'sprintf() needs at least one argument' + end + fmt = args.shift() + return sprintf(fmt, *args) + end +end diff --git a/lib/puppet/provider/augeas/augeas.rb b/lib/puppet/provider/augeas/augeas.rb index 919b377e7..2457840d1 100644 --- a/lib/puppet/provider/augeas/augeas.rb +++ b/lib/puppet/provider/augeas/augeas.rb @@ -69,7 +69,6 @@ Puppet::Type.type(:augeas).provide(:augeas) do commands.concat(parse_commands(datum)) end end - return commands end @@ -181,15 +180,36 @@ Puppet::Type.type(:augeas).provide(:augeas) do fail("invalid command #{cmd_array.join[" "]}") if cmd_array.length < 2 command = cmd_array[0] cmd_array.shift() - loc = cmd_array[0] - cmd_array[0]=File.join(context, loc) - debug("sending command '#{command}' with params #{cmd_array.inspect}") begin case command - when "set": aug.set(cmd_array[0], cmd_array[1]) - when "rm", "remove": aug.rm(cmd_array[0]) - when "clear": aug.clear(cmd_array[0]) - when "insert", "ins": aug.insert(cmd_array[0]) + when "set": + cmd_array[0]=File.join(context, cmd_array[0]) + debug("sending command '#{command}' with params #{cmd_array.inspect}") + aug.set(cmd_array[0], cmd_array[1]) + when "rm", "remove": + cmd_array[0]=File.join(context, cmd_array[0]) + debug("sending command '#{command}' with params #{cmd_array.inspect}") + aug.rm(cmd_array[0]) + when "clear": + cmd_array[0]=File.join(context, cmd_array[0]) + debug("sending command '#{command}' with params #{cmd_array.inspect}") + aug.clear(cmd_array[0]) + when "insert", "ins" + + ext_array = cmd_array[1].split(" ") ; + if cmd_array.size < 2 or ext_array.size < 2 + fail("ins requires 3 parameters") + end + label = cmd_array[0] + where = ext_array[0] + path = File.join(context, ext_array[1]) + case where + when "before": before = true + when "after": before = false + else fail("Invalid value '#{where}' for where param") + end + debug("sending command '#{command}' with params #{[label, where, path].inspect()}") + aug.insert(path, label, before) else fail("Command '#{command}' is not supported") end rescue Exception => e diff --git a/lib/puppet/provider/naginator.rb b/lib/puppet/provider/naginator.rb index 233d82eb6..5510eb9c8 100644 --- a/lib/puppet/provider/naginator.rb +++ b/lib/puppet/provider/naginator.rb @@ -7,6 +7,7 @@ require 'puppet/external/nagios' # The base class for all Naginator providers. class Puppet::Provider::Naginator < Puppet::Provider::ParsedFile + NAME_STRING = "## --PUPPET_NAME-- (called '_naginator_name' in the manifest)" # Retrieve the associated class from Nagios::Base. def self.nagios_type unless defined?(@nagios_type) and @nagios_type @@ -24,14 +25,14 @@ class Puppet::Provider::Naginator < Puppet::Provider::ParsedFile def self.parse(text) begin - Nagios::Parser.new.parse(text) + Nagios::Parser.new.parse(text.gsub(NAME_STRING, "_naginator_name")) rescue => detail raise Puppet::Error, "Could not parse configuration for %s: %s" % [resource_type.name, detail] end end def self.to_file(records) - header + records.collect { |record| record.to_s }.join("\n") + header + records.collect { |record| record.to_s }.join("\n").gsub("_naginator_name", NAME_STRING) end def self.skip_record?(record) diff --git a/lib/puppet/provider/package/up2date.rb b/lib/puppet/provider/package/up2date.rb index aa7c9eaf0..5708905cc 100644 --- a/lib/puppet/provider/package/up2date.rb +++ b/lib/puppet/provider/package/up2date.rb @@ -3,9 +3,11 @@ Puppet::Type.type(:package).provide :up2date, :parent => :rpm, :source => :rpm d mechanism." commands :up2date => "/usr/sbin/up2date-nox" - defaultfor :operatingsystem => :redhat, + + defaultfor :operatingsystem => [:redhat, :oel, :ovm] :lsbdistrelease => ["2.1", "3", "4"] - confine :operatingsystem => :redhat + + confine :operatingsystem => [:redhat, :oel, :ovm] # Install a package using 'up2date'. def install diff --git a/lib/puppet/provider/parsedfile.rb b/lib/puppet/provider/parsedfile.rb index a4c4bc87c..45eae57ff 100755 --- a/lib/puppet/provider/parsedfile.rb +++ b/lib/puppet/provider/parsedfile.rb @@ -78,8 +78,22 @@ class Puppet::Provider::ParsedFile < Puppet::Provider @modified.reject! { |t| flushed.include?(t) } end + # Make sure our file is backed up, but only back it up once per transaction. + # We cheat and rely on the fact that @records is created on each prefetch. + def self.backup_target(target) + unless defined?(@backup_stats) + @backup_stats = {} + end + return nil if @backup_stats[target] == @records.object_id + + target_object(target).backup + @backup_stats[target] = @records.object_id + end + # Flush all of the records relating to a specific target. def self.flush_target(target) + backup_target(target) + records = target_records(target).reject { |r| r[:ensure] == :absent } diff --git a/lib/puppet/provider/service/redhat.rb b/lib/puppet/provider/service/redhat.rb index c6c3540f5..031db46c1 100755 --- a/lib/puppet/provider/service/redhat.rb +++ b/lib/puppet/provider/service/redhat.rb @@ -9,7 +9,7 @@ Puppet::Type.type(:service).provide :redhat, :parent => :init do commands :chkconfig => "/sbin/chkconfig", :service => "/sbin/service" - defaultfor :operatingsystem => [:redhat, :fedora, :suse, :centos, :sles] + defaultfor :operatingsystem => [:redhat, :fedora, :suse, :centos, :sles, :oel, :ovm] def self.defpath superclass.defpath diff --git a/lib/puppet/rails/database/003_add_environment_to_host.rb b/lib/puppet/rails/database/003_add_environment_to_host.rb index 4593a06f7..3ed10e946 100644 --- a/lib/puppet/rails/database/003_add_environment_to_host.rb +++ b/lib/puppet/rails/database/003_add_environment_to_host.rb @@ -1,9 +1,13 @@ class AddEnvironmentToHost < ActiveRecord::Migration def self.up - add_column :hosts, :environment, :string + unless ActiveRecord::Base.connection.columns(:hosts).collect {|c| c.name}.include?("environment") + add_column :hosts, :environment, :string + end end def self.down - remove_column :hosts, :environment + if ActiveRecord::Base.connection.columns(:hosts).collect {|c| c.name}.include?("environment") + remove_column :hosts, :environment + end end end diff --git a/lib/puppet/type/augeas.rb b/lib/puppet/type/augeas.rb index 67b62e886..c89400b5e 100644 --- a/lib/puppet/type/augeas.rb +++ b/lib/puppet/type/augeas.rb @@ -92,10 +92,12 @@ Puppet::Type.newtype(:augeas) do rm [PATH] Removes the node at location PATH remove [PATH] Synonym for rm clear [PATH] Keeps the node at PATH, but removes the value. - ins [PATH] Inserts an empty node at PATH. - insert [PATH] Synonym for ins + ins [LABEL] [WHERE] [PATH] + Inserts an empty node LABEL either [WHERE={before|after}] PATH. + insert [LABEL] [WHERE] [PATH] + Synonym for ins - If the parameter 'context' is set that that value is prepended to PATH" + If the parameter 'context' is set that value is prepended to PATH" munge do |value| provider.parse_commands(value) diff --git a/lib/puppet/util/autoload.rb b/lib/puppet/util/autoload.rb index 535d9ef2e..0c80f8b06 100644 --- a/lib/puppet/util/autoload.rb +++ b/lib/puppet/util/autoload.rb @@ -78,7 +78,7 @@ class Puppet::Util::Autoload name = symbolize(name) loaded name, file return true - rescue LoadError => detail + rescue Exception => detail # I have no idea what's going on here, but different versions # of ruby are raising different errors on missing files. unless detail.to_s =~ /^no such file/i @@ -115,7 +115,7 @@ class Puppet::Util::Autoload begin Kernel.require file loaded(name, file) - rescue => detail + rescue Exception => detail if Puppet[:trace] puts detail.backtrace end diff --git a/lib/puppet/util/filetype.rb b/lib/puppet/util/filetype.rb index a58b9a23b..0f184d5c2 100755 --- a/lib/puppet/util/filetype.rb +++ b/lib/puppet/util/filetype.rb @@ -74,7 +74,7 @@ class Puppet::Util::FileType # Back the file up before replacing it. def backup - bucket.backup(@path) if FileTest.exists?(@path) + bucket.backup(@path) if File.exists?(@path) end # Pick or create a filebucket to use. @@ -94,7 +94,7 @@ class Puppet::Util::FileType newfiletype(:flat) do # Read the file. def read - if File.exists?(@path) + if File.exist?(@path) File.read(@path) else return nil @@ -103,17 +103,13 @@ class Puppet::Util::FileType # Remove the file. def remove - if File.exists?(@path) + if File.exist?(@path) File.unlink(@path) end end # Overwrite the file. def write(text) - backup() - - raise("Cannot create file %s in absent directory" % @path) unless FileTest.exist?(File.dirname(@path)) - require "tempfile" tf = Tempfile.new("puppet") tf.print text; tf.flush @@ -255,92 +251,4 @@ class Puppet::Util::FileType output_file.delete end end - - # Treat netinfo tables as a single file, just for simplicity of certain - # types - newfiletype(:netinfo) do - class << self - attr_accessor :format - end - def read - %x{nidump -r /#{@path} /} - end - - # This really only makes sense for cron tabs. - def remove - %x{nireport / /#{@path} name}.split("\n").each do |name| - newname = name.gsub(/\//, '\/').sub(/\s+$/, '') - output = %x{niutil -destroy / '/#{@path}/#{newname}'} - - unless $? == 0 - raise Puppet::Error, "Could not remove %s from %s" % - [name, @path] - end - end - end - - # Convert our table to an array of hashes. This only works for - # handling one table at a time. - def to_array(text = nil) - unless text - text = read - end - - hash = nil - - # Initialize it with the first record - records = [] - text.split("\n").each do |line| - next if line =~ /^[{}]$/ # Skip the wrapping lines - next if line =~ /"name" = \( "#{@path}" \)/ # Skip the table name - next if line =~ /CHILDREN = \(/ # Skip this header - next if line =~ /^ \)/ # and its closer - - # Now we should have nothing but records, wrapped in braces - - case line - when /^\s+\{/: hash = {} - when /^\s+\}/: records << hash - when /\s+"(\w+)" = \( (.+) \)/ - field = $1 - values = $2 - - # Always use an array - hash[field] = [] - - values.split(/, /).each do |value| - if value =~ /^"(.*)"$/ - hash[field] << $1 - else - raise ArgumentError, "Could not match value %s" % value - end - end - else - raise ArgumentError, "Could not match line %s" % line - end - end - - records - end - - def write(text) - text.gsub!(/^#.*\n/,'') - text.gsub!(/^$/,'') - if text == "" or text == "\n" - self.remove - return - end - unless format = self.class.format - raise Puppe::DevError, "You must define the NetInfo format to inport" - end - IO.popen("niload -d #{format} . 1>/dev/null 2>/dev/null", "w") { |p| - p.print text - } - - unless $? == 0 - raise ArgumentError, "Failed to write %s" % @path - end - end - end end - diff --git a/spec/unit/indirector/facts/facter.rb b/spec/unit/indirector/facts/facter.rb index c0b9dce36..ea68f63d6 100755 --- a/spec/unit/indirector/facts/facter.rb +++ b/spec/unit/indirector/facts/facter.rb @@ -93,15 +93,6 @@ describe Puppet::Node::Facts::Facter do end end - it "should load each directory in the Fact path when loading fact plugins" do - Puppet.settings.expects(:value).with(:factpath).returns("one%stwo" % File::PATH_SEPARATOR) - - Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("one") - Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("two") - - Puppet::Node::Facts::Facter.load_fact_plugins - end - it "should skip files when asked to load a directory" do FileTest.expects(:directory?).with("myfile").returns false @@ -119,4 +110,33 @@ describe Puppet::Node::Facts::Facter do Puppet::Node::Facts::Facter.load_facts_in_dir("mydir") end + + describe Puppet::Node::Facts::Facter, "when loading fact plugins from disk" do + it "should load each directory in the Fact path" do + Puppet.settings.stubs(:value).returns "foo" + Puppet.settings.expects(:value).with(:factpath).returns("one%stwo" % File::PATH_SEPARATOR) + + Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("one") + Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("two") + + Puppet::Node::Facts::Facter.load_fact_plugins + end + + it "should load all facts from the modules" do + Puppet.settings.stubs(:value).returns "foo" + Puppet::Node::Facts::Facter.stubs(:load_facts_in_dir) + + Puppet.settings.expects(:value).with(:modulepath).returns("one%stwo" % File::PATH_SEPARATOR) + + Dir.expects(:glob).with("one/*/plugins/facter").returns %w{oneA oneB} + Dir.expects(:glob).with("two/*/plugins/facter").returns %w{twoA twoB} + + Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("oneA") + Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("oneB") + Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("twoA") + Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("twoB") + + Puppet::Node::Facts::Facter.load_fact_plugins + end + end end diff --git a/spec/unit/network/xmlrpc/client.rb b/spec/unit/network/xmlrpc/client.rb index a0a2e77fb..36e59429c 100755 --- a/spec/unit/network/xmlrpc/client.rb +++ b/spec/unit/network/xmlrpc/client.rb @@ -2,12 +2,170 @@ Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } -describe Puppet::Network do - it "should raise an XMLRPCClientError if a generated class raises a Timeout::Error" do - http = mock 'http' - Puppet::Network::HttpPool.stubs(:http_instance).returns http - file = Puppet::Network::Client.file.new({:Server => "foo.com"}) - http.stubs(:post2).raises Timeout::Error - lambda { file.retrieve }.should raise_error(Puppet::Network::XMLRPCClientError) +describe Puppet::Network::XMLRPCClient do + describe "when performing the rpc call" do + before do + @client = Puppet::Network::Client.report.xmlrpc_client.new + @client.stubs(:call).returns "foo" + + end + + it "should call the specified namespace and method, with the specified arguments" do + @client.expects(:call).with("puppetreports.report", "eh").returns "foo" + @client.report("eh") + end + + it "should return the results from the call" do + @client.expects(:call).returns "foo" + @client.report("eh").should == "foo" + end + + it "should always close the http connection if it is still open after the call" do + http = mock 'http' + @client.stubs(:http).returns http + + http.expects(:started?).returns true + http.expects(:finish) + + @client.report("eh").should == "foo" + end + + it "should always close the http connection if it is still open after a call that raises an exception" do + http = mock 'http' + @client.stubs(:http).returns http + + @client.expects(:call).raises RuntimeError + + http.expects(:started?).returns true + http.expects(:finish) + + lambda { @client.report("eh") }.should raise_error + end + + describe "when returning the http instance" do + it "should use the http pool to create the instance" do + @client.instance_variable_set("@http", nil) + @client.expects(:host).returns "myhost" + @client.expects(:port).returns "myport" + Puppet::Network::HttpPool.expects(:http_instance).with("myhost", "myport", true).returns "http" + + @client.http.should == "http" + end + + it "should reuse existing instances" do + @client.http.should equal(@client.http) + end + end + + describe "when recycling the connection" do + it "should close the existing instance if it's open" do + http = mock 'http' + @client.stubs(:http).returns http + + http.expects(:started?).returns true + http.expects(:finish) + + @client.recycle_connection + end + + it "should force creation of a new instance" do + Puppet::Network::HttpPool.expects(:http_instance).returns "second_http" + + @client.recycle_connection + + @client.http.should == "second_http" + end + end + + describe "and an exception is raised" do + it "should raise XMLRPCClientError if XMLRPC::FaultException is raised" do + error = XMLRPC::FaultException.new("foo", "bar") + + @client.expects(:call).raises(error) + + lambda { @client.report("eh") }.should raise_error(Puppet::Network::XMLRPCClientError) + end + + it "should raise XMLRPCClientError if Errno::ECONNREFUSED is raised" do + @client.expects(:call).raises(Errno::ECONNREFUSED) + + lambda { @client.report("eh") }.should raise_error(Puppet::Network::XMLRPCClientError) + end + + it "should log and raise XMLRPCClientError if Timeout::Error is raised" do + Puppet.expects(:err) + @client.expects(:call).raises(Timeout::Error) + + lambda { @client.report("eh") }.should raise_error(Puppet::Network::XMLRPCClientError) + end + + it "should log and raise XMLRPCClientError if SocketError is raised" do + Puppet.expects(:err) + @client.expects(:call).raises(SocketError) + + lambda { @client.report("eh") }.should raise_error(Puppet::Network::XMLRPCClientError) + end + + it "should log, recycle the connection, and retry if Errno::EPIPE is raised" do + @client.expects(:call).times(2).raises(Errno::EPIPE).then.returns "eh" + + Puppet.expects(:warning) + @client.expects(:recycle_connection) + + @client.report("eh") + end + + it "should log, recycle the connection, and retry if EOFError is raised" do + @client.expects(:call).times(2).raises(EOFError).then.returns "eh" + + Puppet.expects(:warning) + @client.expects(:recycle_connection) + + @client.report("eh") + end + + it "should log and retry if an exception containing 'Wrong size' is raised" do + error = RuntimeError.new("Wrong size. Was 15, should be 30") + @client.expects(:call).times(2).raises(error).then.returns "eh" + + Puppet.expects(:warning) + + @client.report("eh") + end + + it "should raise XMLRPCClientError if OpenSSL::SSL::SSLError is raised" do + @client.expects(:call).raises(OpenSSL::SSL::SSLError) + + lambda { @client.report("eh") }.should raise_error(Puppet::Network::XMLRPCClientError) + end + + it "should log and raise XMLRPCClientError if OpenSSL::SSL::SSLError is raised with certificate issues" do + error = OpenSSL::SSL::SSLError.new("hostname was not match") + @client.expects(:call).raises(error) + + Puppet.expects(:warning) + + lambda { @client.report("eh") }.should raise_error(Puppet::Network::XMLRPCClientError) + end + + it "should log, recycle the connection, and retry if OpenSSL::SSL::SSLError is raised containing 'bad write retry'" do + error = OpenSSL::SSL::SSLError.new("bad write retry") + @client.expects(:call).times(2).raises(error).then.returns "eh" + + @client.expects(:recycle_connection) + + Puppet.expects(:warning) + + @client.report("eh") + end + + it "should log and raise XMLRPCClientError if any other exception is raised" do + @client.expects(:call).raises(RuntimeError) + + Puppet.expects(:err) + + lambda { @client.report("eh") }.should raise_error(Puppet::Network::XMLRPCClientError) + end + end end end diff --git a/spec/unit/parser/functions/regsubst.rb b/spec/unit/parser/functions/regsubst.rb new file mode 100644 index 000000000..18f49f7d4 --- /dev/null +++ b/spec/unit/parser/functions/regsubst.rb @@ -0,0 +1,88 @@ +#! /usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +describe "the regsubst function" do + + before :each do + @scope = Puppet::Parser::Scope.new() + end + + it "should exist" do + Puppet::Parser::Functions.function("regsubst").should == "function_regsubst" + end + + it "should raise a ParseError if there is less than 3 arguments" do + lambda { @scope.function_regsubst(["foo", "bar"]) }.should( + raise_error(Puppet::ParseError)) + end + + it "should raise a ParseError if there is more than 5 arguments" do + lambda { @scope.function_regsubst(["foo", "bar", "gazonk", "del", "x", "y"]) }.should( + raise_error(Puppet::ParseError)) + end + + + it "should raise a ParseError when given a bad flag" do + lambda { @scope.function_regsubst(["foo", "bar", "gazonk", "X"]) }.should( + raise_error(Puppet::ParseError)) + end + + it "should handle groups" do + result = @scope.function_regsubst( + [ '130.236.254.10', + '^([0-9]+)[.]([0-9]+)[.]([0-9]+)[.]([0-9]+)$', + '\4-\3-\2-\1' + ]) + result.should(eql("10-254-236-130")) + end + + it "should handle simple regexps" do + result = @scope.function_regsubst( + [ "the monkey breaks banana trees", + "b[an]*a", + "coconut" + ]) + result.should(eql("the monkey breaks coconut trees")) + end + + it "should handle case-sensitive regexps" do + result = @scope.function_regsubst( + [ "the monkey breaks baNAna trees", + "b[an]+a", + "coconut" + ]) + result.should(eql("the monkey breaks baNAna trees")) + end + + it "should handle case-insensitive regexps" do + result = @scope.function_regsubst( + [ "the monkey breaks baNAna trees", + "b[an]+a", + "coconut", + "I" + ]) + result.should(eql("the monkey breaks coconut trees")) + end + + it "should handle global substitutions" do + result = @scope.function_regsubst( + [ "the monkey breaks\tbanana trees", + "[ \t]", + "--", + "G" + ]) + result.should(eql("the--monkey--breaks--banana--trees")) + end + + it "should handle global substitutions with groups" do + result = @scope.function_regsubst( + [ '130.236.254.10', + '([0-9]+)', + '<\1>', + 'G' + ]) + result.should(eql('<130>.<236>.<254>.<10>')) + end + +end diff --git a/spec/unit/parser/functions/sprintf.rb b/spec/unit/parser/functions/sprintf.rb new file mode 100644 index 000000000..8654b18fc --- /dev/null +++ b/spec/unit/parser/functions/sprintf.rb @@ -0,0 +1,42 @@ +#! /usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +describe "the sprintf function" do + + before :each do + @scope = Puppet::Parser::Scope.new() + end + + it "should exist" do + Puppet::Parser::Functions.function("sprintf").should == "function_sprintf" + end + + it "should raise a ParseError if there is less than 1 argument" do + lambda { @scope.function_sprintf([]) }.should( + raise_error(Puppet::ParseError)) + end + + it "should format integers" do + result = @scope.function_sprintf(["%+05d", "23"]) + result.should(eql("+0023")) + end + + it "should format floats" do + result = @scope.function_sprintf(["%+.2f", "2.7182818284590451"]) + result.should(eql("+2.72")) + end + + it "should format large floats" do + result = @scope.function_sprintf(["%+.2e", "27182818284590451"]) + result.should(eql("+2.72e+16")) + end + + it "should perform more complex formatting" do + result = @scope.function_sprintf( + [ "<%.8s:%#5o %#8X (%-8s)>", + "overlongstring", "23", "48879", "foo" ]) + result.should(eql("<overlong: 027 0XBEEF (foo )>")) + end + +end diff --git a/spec/unit/provider/augeas/augeas.rb b/spec/unit/provider/augeas/augeas.rb index 083448b4a..affc66676 100644 --- a/spec/unit/provider/augeas/augeas.rb +++ b/spec/unit/provider/augeas/augeas.rb @@ -285,32 +285,42 @@ describe provider_class do @augeas.expects(:clear).with("/foo/Jar/Jar") @augeas.expects(:save).returns(true) @provider.execute_changes.should == :executed - end + end + - it "should handle insert commands" do - command = [["insert", "/Jar/Jar"]] + it "should handle ins commands with before" do + command = [["ins", "Binks", "before /Jar/Jar"]] context = "/foo" @resource.expects(:[]).times(2).returns(command).then.returns(context) - @augeas.expects(:insert).with("/foo/Jar/Jar") + @augeas.expects(:insert).with("/foo/Jar/Jar", "Binks", true) @augeas.expects(:save).returns(true) @provider.execute_changes.should == :executed end - it "should handle ins commands" do - command = [["ins", "/Jar/Jar"]] + it "should handle ins commands with before" do + command = [["ins", "Binks", "after /Jar/Jar"]] context = "/foo" @resource.expects(:[]).times(2).returns(command).then.returns(context) - @augeas.expects(:insert).with("/foo/Jar/Jar") + @augeas.expects(:insert).with("/foo/Jar/Jar", "Binks", false) @augeas.expects(:save).returns(true) @provider.execute_changes.should == :executed end + it "should handle ins with no context" do + command = [["ins", "Binks", "after /Jar/Jar"]] + context = "" # this is the default + @resource.expects(:[]).times(2).returns(command).then.returns(context) + @augeas.expects(:insert).with("/Jar/Jar", "Binks", false) + @augeas.expects(:save).returns(true) + @provider.execute_changes.should == :executed + end + it "should handle multiple commands" do - command = [["ins", "/Jar/Jar"], ["clear", "/Jar/Jar"]] + command = [["ins", "Binks", "after /Jar/Jar"], ["clear", "/Jar/Jar"]] context = "/foo" @resource.expects(:[]).times(2).returns(command).then.returns(context) - @augeas.expects(:insert).with("/foo/Jar/Jar") - @augeas.expects(:clear).with("/foo/Jar/Jar") + @augeas.expects(:insert).with("/foo/Jar/Jar", "Binks", false) + @augeas.expects(:clear).with("/foo/Jar/Jar") @augeas.expects(:save).returns(true) @provider.execute_changes.should == :executed end diff --git a/spec/unit/provider/parsedfile.rb b/spec/unit/provider/parsedfile.rb index 05e9de3ab..11a91c8d7 100755 --- a/spec/unit/provider/parsedfile.rb +++ b/spec/unit/provider/parsedfile.rb @@ -47,4 +47,40 @@ describe Puppet::Provider::ParsedFile do @class.instances end end + + describe "when flushing a file's records to disk" do + before do + # This way we start with some @records, like we would in real life. + @class.stubs(:retrieve).returns [] + @class.default_target = "/foo/bar" + @class.initvars + @class.prefetch + + @filetype = mock 'filetype' + Puppet::Util::FileType.filetype(:flat).expects(:new).with("/my/file").returns @filetype + + @filetype.stubs(:write) + end + + it "should back up the file being written" do + @filetype.expects(:backup) + + @class.flush_target("/my/file") + end + + it "should not back up the file more than once between calls to 'prefetch'" do + @filetype.expects(:backup).once + + @class.flush_target("/my/file") + @class.flush_target("/my/file") + end + + it "should back the file up again once the file has been reread" do + @filetype.expects(:backup).times(2) + + @class.flush_target("/my/file") + @class.prefetch + @class.flush_target("/my/file") + end + end end diff --git a/spec/unit/util/autoload.rb b/spec/unit/util/autoload.rb new file mode 100755 index 000000000..ff717d6c5 --- /dev/null +++ b/spec/unit/util/autoload.rb @@ -0,0 +1,39 @@ +#!/usr/bin/env ruby + +Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } + +require 'puppet/util/autoload' + +describe Puppet::Util::Autoload do + before do + @autoload = Puppet::Util::Autoload.new("foo", "tmp") + + @autoload.stubs(:eachdir).yields "/my/dir" + end + + describe "when loading a file" do + [RuntimeError, LoadError, SyntaxError].each do |error| + it "should not die an if a #{error.to_s} exception is thrown" do + FileTest.stubs(:exists?).returns true + + Kernel.expects(:load).raises error + + lambda { @autoload.load("foo") }.should_not raise_error + end + end + end + + describe "when loading all files" do + before do + Dir.stubs(:glob).returns "file.rb" + end + + [RuntimeError, LoadError, SyntaxError].each do |error| + it "should not die an if a #{error.to_s} exception is thrown" do + Kernel.expects(:require).raises error + + lambda { @autoload.loadall }.should_not raise_error + end + end + end +end diff --git a/spec/unit/util/filetype.rb b/spec/unit/util/filetype.rb new file mode 100644 index 000000000..0506b6b47 --- /dev/null +++ b/spec/unit/util/filetype.rb @@ -0,0 +1,110 @@ +#!/usr/bin/env ruby + +Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } + +require 'puppet/util/filetype' + +# XXX Import all of the tests into this file. +describe Puppet::Util::FileType do + describe "when backing up a file" do + before do + @file = Puppet::Util::FileType.filetype(:flat).new("/my/file") + end + + it "should do nothing if the file does not exist" do + File.expects(:exists?).with("/my/file").returns false + @file.expects(:bucket).never + @file.backup + end + + it "should use its filebucket to backup the file if it exists" do + File.expects(:exists?).with("/my/file").returns true + + bucket = mock 'bucket' + bucket.expects(:backup).with("/my/file") + + @file.expects(:bucket).returns bucket + @file.backup + end + + it "should use the filebucket named 'puppet' if it finds one" do + bucket = mock 'bucket' + bucket.expects(:bucket).returns "mybucket" + + Puppet::Type.type(:filebucket).expects(:[]).with("puppet").returns bucket + + @file.bucket.should == "mybucket" + end + + it "should use the default filebucket if none named 'puppet' is found" do + bucket = mock 'bucket' + bucket.expects(:bucket).returns "mybucket" + + Puppet::Type.type(:filebucket).expects(:[]).with("puppet").returns nil + Puppet::Type.type(:filebucket).expects(:mkdefaultbucket).returns bucket + + @file.bucket.should == "mybucket" + end + end + + describe "the flat filetype" do + before do + @type = Puppet::Util::FileType.filetype(:flat) + end + it "should exist" do + @type.should_not be_nil + end + + describe "when the file already exists" do + it "should return the file's contents when asked to read it" do + file = @type.new("/my/file") + File.expects(:exist?).with("/my/file").returns true + File.expects(:read).with("/my/file").returns "my text" + + file.read.should == "my text" + end + + it "should unlink the file when asked to remove it" do + file = @type.new("/my/file") + File.expects(:exist?).with("/my/file").returns true + File.expects(:unlink).with("/my/file") + + file.remove + end + end + + describe "when the file does not exist" do + it "should return an empty string when asked to read the file" do + file = @type.new("/my/file") + File.expects(:exist?).with("/my/file").returns false + + file.read.should == "" + end + end + + describe "when writing the file" do + before do + @file = @type.new("/my/file") + FileUtils.stubs(:cp) + + @tempfile = stub 'tempfile', :print => nil, :close => nil, :flush => nil, :path => "/other/file" + Tempfile.stubs(:new).returns @tempfile + end + + it "should first create a temp file and copy its contents over to the file location" do + Tempfile.expects(:new).with("puppet").returns @tempfile + @tempfile.expects(:print).with("my text") + @tempfile.expects(:flush) + @tempfile.expects(:close) + FileUtils.expects(:cp).with(@tempfile.path, "/my/file") + + @file.write "my text" + end + + it "should set the selinux default context on the file" do + @file.expects(:set_selinux_default_context).with("/my/file") + @file.write "eh" + end + end + end +end diff --git a/test/util/filetype.rb b/test/util/filetype.rb deleted file mode 100755 index 24a968552..000000000 --- a/test/util/filetype.rb +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env ruby - -require File.dirname(__FILE__) + '/../lib/puppettest' - -require 'puppettest' -require 'puppet/util/filetype' -require 'mocha' - -class TestFileType < Test::Unit::TestCase - include PuppetTest - - def test_flat - obj = nil - path = tempfile() - type = nil - - assert_nothing_raised { - type = Puppet::Util::FileType.filetype(:flat) - } - - assert(type, "Could not retrieve flat filetype") - - assert_nothing_raised { - obj = type.new(path) - } - - text = "This is some text\n" - - newtext = nil - assert_nothing_raised { - newtext = obj.read - } - - # The base class doesn't allow a return of nil - assert_equal("", newtext, "Somehow got some text") - - assert_nothing_raised { - obj.write(text) - } - assert_nothing_raised { - newtext = obj.read - } - - assert_equal(text, newtext, "Text was changed somehow") - - File.open(path, "w") { |f| f.puts "someyayness" } - - text = File.read(path) - assert_nothing_raised { - newtext = obj.read - } - - assert_equal(text, newtext, "Text was changed somehow") - end - - # Make sure that modified files are backed up before they're changed. - def test_backup_is_called - path = tempfile - File.open(path, "w") { |f| f.print 'yay' } - - obj = Puppet::Util::FileType.filetype(:flat).new(path) - - obj.expects(:backup) - - obj.write("something") - - assert_equal("something", File.read(path), "File did not get changed") - end - - def test_backup - path = tempfile - type = Puppet::Type.type(:filebucket) - - obj = Puppet::Util::FileType.filetype(:flat).new(path) - - # First try it when the file does not yet exist. - assert_nothing_raised("Could not call backup when file does not exist") do - obj.backup - end - - # Then create the file - File.open(path, "w") { |f| f.print 'one' } - - # Then try it with no filebucket objects - assert_nothing_raised("Could not call backup with no buckets") do - obj.backup - end - puppet = type.mkdefaultbucket - assert(puppet, "Did not create default filebucket") - - assert_equal("one", puppet.bucket.getfile(Digest::MD5.hexdigest(File.read(path))), "Could not get file from backup") - - # Try it again when the default already exists - File.open(path, "w") { |f| f.print 'two' } - assert_nothing_raised("Could not call backup with no buckets") do - obj.backup - end - - assert_equal("two", puppet.bucket.getfile(Digest::MD5.hexdigest(File.read(path))), "Could not get file from backup") - end - - if Facter["operatingsystem"].value == "Darwin" and Facter["operatingsystemrelease"] != "9.1.0" - def test_ninfotoarray - obj = nil - type = nil - - assert_nothing_raised { - type = Puppet::Util::FileType.filetype(:netinfo) - } - - assert(type, "Could not retrieve netinfo filetype") - %w{users groups aliases}.each do |map| - assert_nothing_raised { - obj = type.new(map) - } - - assert_nothing_raised("could not read map %s" % map) { - obj.read - } - - array = nil - - assert_nothing_raised("Failed to parse %s map" % map) { - array = obj.to_array - } - - assert_instance_of(Array, array) - - array.each do |record| - assert_instance_of(Hash, record) - assert(record.length != 0) - end - end - end - end -end - |