diff --git a/lib/puppet/provider/yumrepo/inifile.rb b/lib/puppet/provider/yumrepo/inifile.rb index 4882dedb8..748096c5a 100644 --- a/lib/puppet/provider/yumrepo/inifile.rb +++ b/lib/puppet/provider/yumrepo/inifile.rb @@ -1,187 +1,227 @@ require 'puppet/util/inifile' Puppet::Type.type(:yumrepo).provide(:inifile) do desc 'Manage yum repos' PROPERTIES = Puppet::Type.type(:yumrepo).validproperties # @return [Array] Return all the providers built up from # discovered content on the local node. def self.instances instances = [] # Iterate over each section of our virtual file. virtual_inifile.each_section do |section| attributes_hash = {:name => section.name, :ensure => :present, :provider => :yumrepo} # We need to build up a attributes hash section.entries.each do |key, value| key = key.to_sym if valid_property?(key) # We strip the values here to handle cases where distros set values # like enabled = 1 with spaces. - attributes_hash[key] = value.strip + attributes_hash[key] = value + elsif key == :name + attributes_hash[:descr] = value end end instances << new(attributes_hash) end return instances end # @param resources [Array] Resources to prefetch. # @return [Array] Resources with providers set. def self.prefetch(resources) repos = instances resources.keys.each do |name| if provider = repos.find { |repo| repo.name == name } resources[name].provider = provider end end end # Return a list of existing directories that could contain repo files. Fail if none found. # @param conf [String] Configuration file to look for directories in. # @param dirs [Array] Default locations for yum repos. # @return [Array] Directories that were found to exist on the node. def self.reposdir(conf='/etc/yum.conf', dirs=['/etc/yum.repos.d', '/etc/yum/repos.d']) reposdir = find_conf_value('reposdir', conf) dirs << reposdir if reposdir # We can't use the below due to Ruby 1.8.7 # dirs.select! { |dir| Puppet::FileSystem.exist?(dir) } dirs.delete_if { |dir| ! Puppet::FileSystem.exist?(dir) } if dirs.empty? fail('No yum directories were found on the local filesystem') else return dirs end end # Helper method to look up specific values in ini style files. # @todo Migrate this into Puppet::Util::IniConfig. # @param value [String] Value to look for in the configuration file. # @param conf [String] Configuration file to check for value. # @return [String] The value of a looked up key from the configuration file. def self.find_conf_value(value, conf='/etc/yum.conf') if Puppet::FileSystem.exist?(conf) contents = Puppet::FileSystem.read(conf) match = /^#{value}\s*=\s*(.*)/.match(contents) end return match.captures[0] if match end # Build a virtual inifile by reading in numerous .repo # files into a single virtual file to ease manipulation. # @return [Puppet::Util::IniConfig::File] The virtual inifile representing # multiple real files. def self.virtual_inifile unless @virtual @virtual = Puppet::Util::IniConfig::File.new reposdir.each do |dir| Dir.glob("#{dir}/*.repo").each do |file| @virtual.read(file) if Puppet::FileSystem.file?(file) end end end return @virtual end # @param key [String] The property to look up. # @return [Boolean] Returns true if the property is defined in the type. def self.valid_property?(key) PROPERTIES.include?(key) end # We need to return a valid section from the larger virtual inifile here, # which we do by first looking it up and then creating a new section for # the appropriate name if none was found. # @param name [String] Section name to lookup in the virtual inifile. # @return [Puppet::Util::IniConfig] The IniConfig section def self.section(name) result = self.virtual_inifile[name] # Create a new section if not found. unless result # Previously we did an .each on reposdir with the effect that we # constantly created and overwrote result until the last entry of # the array. This was done because the ordering is # [defaults, custom] for reposdir and we want to use the custom if # we have it and the defaults if not. path = ::File.join(reposdir.last, "#{name}.repo") Puppet.info("create new repo #{name} in file #{path}") result = self.virtual_inifile.add_section(name, path) end result end # Here we store all modifications to disk, forcing the output file to 0644 if it differs. # @return [void] def self.store inifile = self.virtual_inifile inifile.store target_mode = 0644 inifile.each_file do |file| current_mode = Puppet::FileSystem.stat(file).mode & 0777 unless current_mode == target_mode Puppet.info "changing mode of #{file} from %03o to %03o" % [current_mode, target_mode] Puppet::FileSystem.chmod(target_mode, file) end end end # @return [void] def create @property_hash[:ensure] = :present - new_section = section(@resource[:name]) + new_section = current_section # We fetch a list of properties from the type, then iterate # over them, avoiding ensure. We're relying on .should to # check if the property has been set and should be modified, # and if so we set it in the virtual inifile. PROPERTIES.each do |property| next if property == :ensure + + if value = @resource.should(property) - new_section[property.to_s] = value - @property_hash[property] = value + self.send("#{property}=", value) end end end + # @return [Boolean] Returns true if ensure => present. + def exists? + @property_hash[:ensure] == :present + end + # We don't actually destroy the file here, merely mark it for # destruction in the section. # @return [void] def destroy # Flag file for deletion on flush. - section(@resource[:name]).destroy=(true) + current_section.destroy=(true) @property_hash.clear end # @return [void] def flush self.class.store end - # @return [void] - def section(name) - self.class.section(name) - end - - # Create all of our setters. - mk_resource_methods + # Generate setters and getters for our INI properties. PROPERTIES.each do |property| - # Exclude ensure, as we don't need to create an ensure= + # The ensure property uses #create, #exists, and #destroy we can't generate + # meaningful setters and getters for this next if property == :ensure - # Builds the property= method. - define_method("#{property.to_s}=") do |value| - section(@property_hash[:name])[property.to_s] = value - @property_hash[property] = value + + define_method(property) do + get_property(property) + end + + define_method("#{property}=") do |value| + set_property(property, value) end end - # @return [Boolean] Returns true if ensure => present. - def exists? - @property_hash[:ensure] == :present + # Map the yumrepo 'descr' type property to the 'name' INI property. + def descr + if ! @property_hash.has_key?(:descr) + @property_hash[:descr] = current_section['name'] + end + value = @property_hash[:descr] + value.nil? ? :absent : value + end + + def descr=(value) + value = (value == :absent ? nil : value) + current_section['name'] = value + @property_hash[:descr] = value + end + + private + + def get_property(property) + if ! @property_hash.has_key?(property) + @property_hash[property] = current_section[property.to_s] + end + value = @property_hash[property] + value.nil? ? :absent : value end + def set_property(property, value) + value = (value == :absent ? nil : value) + current_section[property.to_s] = value + @property_hash[property] = value + end + + # @return [void] + def section(name) + self.class.section(name) + end + + def current_section + self.class.section(self.name) + end end diff --git a/lib/puppet/type/yumrepo.rb b/lib/puppet/type/yumrepo.rb index d173155dc..e28e9081c 100644 --- a/lib/puppet/type/yumrepo.rb +++ b/lib/puppet/type/yumrepo.rb @@ -1,270 +1,304 @@ require 'uri' Puppet::Type.newtype(:yumrepo) do @doc = "The client-side description of a yum repository. Repository configurations are found by parsing `/etc/yum.conf` and the files indicated by the `reposdir` option in that file (see `yum.conf(5)` for details). Most parameters are identical to the ones documented in the `yum.conf(5)` man page. Continuation lines that yum supports (for the `baseurl`, for example) are not supported. This type does not attempt to read or verify the exinstence of files listed in the `include` attribute." # Ensure yumrepos can be removed too. ensurable # Doc string for properties that can be made 'absent' ABSENT_DOC="Set this to `absent` to remove it from the file completely." # False can be false/0/no and True can be true/1/yes in yum. - YUM_BOOLEAN=/(True|False|0|1|No|Yes)/ + YUM_BOOLEAN=/(True|False|0|1|No|Yes)/i YUM_BOOLEAN_DOC="Valid values are: False/0/No or True/1/Yes." + VALID_SCHEMES = %w[file http https ftp] + newparam(:name, :namevar => true) do desc "The name of the repository. This corresponds to the `repositoryid` parameter in `yum.conf(5)`." end newparam(:target) do desc "The filename to write the yum repository to." defaultto :absent end newproperty(:descr) do desc "A human-readable description of the repository. This corresponds to the name parameter in `yum.conf(5)`. #{ABSENT_DOC}" newvalues(/.*/, :absent) end newproperty(:mirrorlist) do desc "The URL that holds the list of mirrors for this repository. #{ABSENT_DOC}" newvalues(/.*/, :absent) validate do |value| + next if value.to_s == 'absent' parsed = URI.parse(value) - fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme) + + unless VALID_SCHEMES.include?(parsed.scheme) + raise "Must be a valid URL" + end end end newproperty(:baseurl) do desc "The URL for this repository. #{ABSENT_DOC}" newvalues(/.*/, :absent) validate do |value| - parsed = URI.parse(value) - fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme) + next if value.to_s == 'absent' + + value.split(/\s+/).each do |uri| + + parsed = URI.parse(uri) + + unless VALID_SCHEMES.include?(parsed.scheme) + raise "Must be a valid URL" + end + end end end newproperty(:enabled) do desc "Whether this repository is enabled. #{YUM_BOOLEAN_DOC} #{ABSENT_DOC}" newvalues(YUM_BOOLEAN, :absent) end newproperty(:gpgcheck) do desc "Whether to check the GPG signature on packages installed from this repository. #{YUM_BOOLEAN_DOC} #{ABSENT_DOC}" newvalues(YUM_BOOLEAN, :absent) end newproperty(:repo_gpgcheck) do desc "Whether to check the GPG signature on repodata. #{YUM_BOOLEAN_DOC} #{ABSENT_DOC}" newvalues(YUM_BOOLEAN, :absent) end newproperty(:gpgkey) do desc "The URL for the GPG key with which packages from this repository are signed. #{ABSENT_DOC}" newvalues(/.*/, :absent) validate do |value| - parsed = URI.parse(value) - fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme) + next if value.to_s == 'absent' + + value.split(/\s+/).each do |uri| + + parsed = URI.parse(uri) + + unless VALID_SCHEMES.include?(parsed.scheme) + raise "Must be a valid URL" + end + end end end newproperty(:include) do desc "The URL of a remote file containing additional yum configuration settings. Puppet does not check for this file's existence or validity. #{ABSENT_DOC}" newvalues(/.*/, :absent) validate do |value| + next if value.to_s == 'absent' parsed = URI.parse(value) - fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme) + + unless VALID_SCHEMES.include?(parsed.scheme) + raise "Must be a valid URL" + end end end newproperty(:exclude) do desc "List of shell globs. Matching packages will never be considered in updates or installs for this repo. #{ABSENT_DOC}" newvalues(/.*/, :absent) end newproperty(:includepkgs) do desc "List of shell globs. If this is set, only packages matching one of the globs will be considered for update or install from this repo. #{ABSENT_DOC}" newvalues(/.*/, :absent) end newproperty(:enablegroups) do desc "Whether yum will allow the use of package groups for this repository. #{YUM_BOOLEAN_DOC} #{ABSENT_DOC}" newvalues(YUM_BOOLEAN, :absent) end newproperty(:failovermethod) do desc "The failover method for this repository; should be either `roundrobin` or `priority`. #{ABSENT_DOC}" newvalues(/roundrobin|priority/, :absent) end newproperty(:keepalive) do desc "Whether HTTP/1.1 keepalive should be used with this repository. #{YUM_BOOLEAN_DOC} #{ABSENT_DOC}" newvalues(YUM_BOOLEAN, :absent) end newproperty(:http_caching) do desc "What to cache from this repository. #{ABSENT_DOC}" newvalues(/(packages|all|none)/, :absent) end newproperty(:timeout) do desc "Number of seconds to wait for a connection before timing out. #{ABSENT_DOC}" newvalues(/[0-9]+/, :absent) end newproperty(:metadata_expire) do desc "Number of seconds after which the metadata will expire. #{ABSENT_DOC}" newvalues(/[0-9]+/, :absent) end newproperty(:protect) do desc "Enable or disable protection for this repository. Requires that the `protectbase` plugin is installed and enabled. #{YUM_BOOLEAN_DOC} #{ABSENT_DOC}" newvalues(YUM_BOOLEAN, :absent) end newproperty(:priority) do desc "Priority of this repository from 1-99. Requires that the `priorities` plugin is installed and enabled. #{ABSENT_DOC}" newvalues(/.*/, :absent) validate do |value| unless value == :absent or (1..99).include?(value.to_i) fail("Must be within range 1-99") end end end newproperty(:cost) do desc "Cost of this repository. #{ABSENT_DOC}" newvalues(/\d+/, :absent) end newproperty(:proxy) do desc "URL to the proxy server for this repository. #{ABSENT_DOC}" newvalues(/.*/, :absent) validate do |value| + next if value.to_s == 'absent' parsed = URI.parse(value) - fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme) + + unless VALID_SCHEMES.include?(parsed.scheme) + raise "Must be a valid URL" + end end end newproperty(:proxy_username) do desc "Username for this proxy. #{ABSENT_DOC}" newvalues(/.*/, :absent) end newproperty(:proxy_password) do desc "Password for this proxy. #{ABSENT_DOC}" newvalues(/.*/, :absent) end newproperty(:s3_enabled) do desc "Access the repo via S3. #{YUM_BOOLEAN_DOC} #{ABSENT_DOC}" newvalues(YUM_BOOLEAN, :absent) end newproperty(:sslcacert) do desc "Path to the directory containing the databases of the certificate authorities yum should use to verify SSL certificates. #{ABSENT_DOC}" newvalues(/.*/, :absent) end newproperty(:sslverify) do desc "Should yum verify SSL certificates/hosts at all. #{YUM_BOOLEAN_DOC} #{ABSENT_DOC}" newvalues(YUM_BOOLEAN, :absent) end newproperty(:sslclientcert) do desc "Path to the SSL client certificate yum should use to connect to repos/remote sites. #{ABSENT_DOC}" newvalues(/.*/, :absent) end newproperty(:sslclientkey) do desc "Path to the SSL client key yum should use to connect to repos/remote sites. #{ABSENT_DOC}" newvalues(/.*/, :absent) end newproperty(:metalink) do desc "Metalink for mirrors. #{ABSENT_DOC}" newvalues(/.*/, :absent) validate do |value| + next if value.to_s == 'absent' parsed = URI.parse(value) - fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme) + + unless VALID_SCHEMES.include?(parsed.scheme) + raise "Must be a valid URL" + end end end end diff --git a/lib/puppet/util/inifile.rb b/lib/puppet/util/inifile.rb index df3c59850..23ce18adb 100644 --- a/lib/puppet/util/inifile.rb +++ b/lib/puppet/util/inifile.rb @@ -1,218 +1,341 @@ # Module Puppet::IniConfig # A generic way to parse .ini style files and manipulate them in memory # One 'file' can be made up of several physical files. Changes to sections # on the file are tracked so that only the physical files in which # something has changed are written back to disk # Great care is taken to preserve comments and blank lines from the original # files # # The parsing tries to stay close to python's ConfigParser require 'puppet/util/filetype' +require 'puppet/error' module Puppet::Util::IniConfig # A section in a .ini file class Section attr_reader :name, :file, :entries attr_writer :destroy def initialize(name, file) @name = name @file = file @dirty = false @entries = [] @destroy = false end - # Has this section been modified since it's been read in - # or written back to disk + # Does this section need to be updated in/removed from the associated file? + # + # @note This section is dirty if a key has been modified _or_ if the + # section has been modified so the associated file can be rewritten + # without this section. def dirty? - @dirty + @dirty or @destroy + end + + def mark_dirty + @dirty = true end # Should only be used internally def mark_clean @dirty = false end # Should the file be destroyed? def destroy? @destroy end # Add a line of text (e.g., a comment) Such lines # will be written back out in exactly the same # place they were read in def add_line(line) @entries << line end # Set the entry 'key=value'. If no entry with the # given key exists, one is appended to teh end of the section def []=(key, value) entry = find_entry(key) @dirty = true if entry.nil? @entries << [key, value] else entry[1] = value end end # Return the value associated with KEY. If no such entry # exists, return nil def [](key) entry = find_entry(key) return(entry.nil? ? nil : entry[1]) end # Format the section as text in the way it should be # written to file def format - text = "[#{name}]\n" - @entries.each do |entry| - if entry.is_a?(Array) - key, value = entry - text << "#{key}=#{value}\n" unless value.nil? - else - text << entry + if @destroy + text = "" + else + text = "[#{name}]\n" + @entries.each do |entry| + if entry.is_a?(Array) + key, value = entry + text << "#{key}=#{value}\n" unless value.nil? + else + text << entry + end end end text end private def find_entry(key) @entries.each do |entry| return entry if entry.is_a?(Array) && entry[0] == key end nil end end - # A logical .ini-file that can be spread across several physical - # files. For each physical file, call #read with the filename - class File - def initialize - @files = {} + class PhysicalFile + + # @!attribute [r] filetype + # @api private + # @return [Puppet::Util::FileType::FileTypeFlat] + attr_reader :filetype + + # @!attribute [r] contents + # @api private + # @return [Array] + attr_reader :contents + + # @!attribute [rw] destroy_empty + # Whether empty files should be removed if no sections are defined. + # Defaults to false + attr_accessor :destroy_empty + + # @!attribute [rw] file_collection + # @return [Puppet::Util::IniConfig::FileCollection] + attr_accessor :file_collection + + def initialize(file, options = {}) + @file = file + @contents = [] + @filetype = Puppet::Util::FileType.filetype(:flat).new(file) + + @destroy_empty = options.fetch(:destroy_empty, false) end - # Add the contents of the file with name FILE to the - # already existing sections - def read(file) - text = Puppet::Util::FileType.filetype(:flat).new(file).read - raise "Could not find #{file}" if text.nil? + # Read and parse the on-disk file associated with this object + def read + text = @filetype.read + if text.nil? + raise IniParseError, "Cannot read nonexistent file #{@file.inspect}" + end + parse(text) + end + + INI_COMMENT = Regexp.union( + /^\s*$/, + /^[#;]/, + /^\s*rem\s/i + ) + INI_CONTINUATION = /^[ \t\r\n\f]/ + INI_SECTION_NAME = /^\[([^\]]+)\]/ + INI_PROPERTY = /^\s*([^\s=]+)\s*\=(.*)$/ + # @api private + def parse(text) section = nil # The name of the current section optname = nil # The name of the last option in section - line = 0 - @files[file] = [] + line_num = 0 + text.each_line do |l| - line += 1 - if l.strip.empty? || "#;".include?(l[0,1]) || - (l.split(nil, 2)[0].downcase == "rem" && l[0,1].downcase == "r") + line_num += 1 + if l.match(INI_COMMENT) # Whitespace or comment if section.nil? - @files[file] << l + @contents << l else section.add_line(l) end - elsif " \t\r\n\f".include?(l[0,1]) && section && optname + elsif l.match(INI_CONTINUATION) && section && optname # continuation line section[optname] += "\n#{l.chomp}" - elsif l =~ /^\[([^\]]+)\]/ + elsif (match = l.match(INI_SECTION_NAME)) # section heading - section.mark_clean unless section.nil? - section = add_section($1, file) + section.mark_clean if section + + section_name = match[1] + + section = add_section(section_name) optname = nil - elsif l =~ /^\s*([^\s=]+)\s*\=(.*)$/ + elsif (match = l.match(INI_PROPERTY)) # We allow space around the keys, but not the values # For the values, we don't know if space is significant + key = match[1] + val = match[2] + if section.nil? - raise "#{file}:#{line}:Key/value pair outside of a section for key #{$1}" - else - section[$1] = $2 - optname = $1 + raise IniParseError.new("Property with key #{key.inspect} outside of a section") end + + section[key] = val + optname = key else - raise "#{file}:#{line}: Can't parse '#{l.chomp}'" + raise IniParseError.new("Can't parse line '#{l.chomp}'", @file, line_num) end end section.mark_clean unless section.nil? end - # Store all modifications made to sections in this file back - # to the physical files. If no modifications were made to - # a physical file, nothing is written - def store - @files.each do |file, lines| - text = "" - dirty = false - destroy = false - lines.each do |l| - if l.is_a?(Section) - destroy ||= l.destroy? - dirty ||= l.dirty? - text << l.format - l.mark_clean - else - text << l - end - end - # We delete the file and then remove it from the list of files. - if destroy - ::File.unlink(file) - @files.delete(file) + # @return [Array] All sections defined in + # this file. + def sections + @contents.select { |entry| entry.is_a? Section } + end + + # @return [Puppet::Util::IniConfig::Section, nil] The section with the + # given name if it exists, else nil. + def get_section(name) + @contents.find { |entry| entry.is_a? Section and entry.name == name } + end + + def format + text = "" + + @contents.each do |content| + if content.is_a? Section + text << content.format else - if dirty - Puppet::Util::FileType.filetype(:flat).new(file).write(text) - return file - end + text << content end end + + text + end + + def store + if @destroy_empty and (sections.empty? or sections.all?(&:destroy?)) + ::File.unlink(@file) + elsif sections.any?(&:dirty?) + text = self.format + @filetype.write(text) + end + sections.each(&:mark_clean) + end + + # Create a new section and store it in the file contents + # + # @api private + # @param name [String] The name of the section to create + # @return [Puppet::Util::IniConfig::Section] + def add_section(name) + if section_exists?(name) + raise IniParseError.new("Section #{name.inspect} is already defined, cannot redefine", @file) + end + + section = Section.new(name, @file) + @contents << section + + section + end + + private + + def section_exists?(name) + if self.get_section(name) + true + elsif @file_collection and @file_collection.get_section(name) + true + else + false + end + end + end + + class FileCollection + + attr_reader :files + + def initialize + @files = {} + end + + # Read and parse a file and store it in the collection. If the file has + # already been read it will be destroyed and re-read. + def read(file) + new_physical_file(file).read + end + + def store + @files.values.each do |file| + file.store + end end - # Execute BLOCK, passing each section in this file - # as an argument def each_section(&block) - @files.each do |file, list| - list.each do |entry| - yield(entry) if entry.is_a?(Section) + @files.values.each do |file| + file.sections.each do |section| + yield section end end end - # Execute BLOCK, passing each file constituting this inifile - # as an argument def each_file(&block) - @files.keys.each do |file| - yield(file) + @files.keys.each do |path| + yield path end end - # Return the Section with the given name or nil - def [](name) - name = name.to_s - each_section do |section| - return section if section.name == name + def get_section(name) + sect = nil + @files.values.each do |file| + if (current = file.get_section(name)) + sect = current + end end - nil + sect end + alias [] get_section - # Return true if the file contains a section with name NAME def include?(name) - ! self[name].nil? + !! get_section(name) end - # Add a section to be stored in FILE when store is called def add_section(name, file) - raise "A section with name #{name} already exists" if include?(name) - result = Section.new(name, file) - @files[file] ||= [] - @files[file] << result - result + get_physical_file(file).add_section(name) + end + + private + + # Return a file if it's already been defined, create a new file if it hasn't + # been defined. + def get_physical_file(file) + if @files[file] + @files[file] + else + new_physical_file(file) + end + end + + # Create a new physical file and set required attributes on that file. + def new_physical_file(file) + @files[file] = PhysicalFile.new(file) + @files[file].file_collection = self + @files[file] end end -end + File = FileCollection + + class IniParseError < Puppet::Error + include Puppet::ExternalFileError + end +end diff --git a/spec/unit/provider/yumrepo/inifile_spec.rb b/spec/unit/provider/yumrepo/inifile_spec.rb index ef3beb8a1..ac755ebb5 100644 --- a/spec/unit/provider/yumrepo/inifile_spec.rb +++ b/spec/unit/provider/yumrepo/inifile_spec.rb @@ -1,105 +1,153 @@ require 'spec_helper' describe Puppet::Type.type(:yumrepo).provider(:inifile) do let(:virtual_inifile) { stub('virtual inifile') } before :each do described_class.stubs(:virtual_inifile).returns(virtual_inifile) end describe 'self.instances' do let(:updates_section) do - stub('inifile updates section', - :name => 'updates', - :entries => {'name' => 'updates', 'enabled' => '1', 'descr' => 'test updates'}) + sect = Puppet::Util::IniConfig::Section.new('updates', '/some/imaginary/file') + sect.entries << ['name', 'Some long description of the repo'] + sect.entries << ['enabled', '1'] + + sect end it 'finds any existing sections' do virtual_inifile.expects(:each_section).yields(updates_section) + virtual_inifile.stubs(:[]).with('updates').returns(updates_section) providers = described_class.instances providers.should have(1).items providers[0].name.should == 'updates' + providers[0].descr.should == 'Some long description of the repo' providers[0].enabled.should == '1' end end - describe "methods used by ensurable" do + describe "setting and getting properties" do - let(:type) do + let(:type_instance) do Puppet::Type.type(:yumrepo).new( :name => 'puppetlabs-products', :ensure => :present, :baseurl => 'http://yum.puppetlabs.com/el/6/products/$basearch', :descr => 'Puppet Labs Products El 6 - $basearch', :enabled => '1', :gpgcheck => '1', :gpgkey => 'file:///etc/pki/rpm-gpg/RPM-GPG-KEY-puppetlabs' ) end - let(:provider) { type.provider } + let(:provider) do + described_class.new(type_instance) + end - let(:puppetlabs_section) { stub('inifile puppetlabs section', :name => 'puppetlabs-products') } + let(:section) do + stub('inifile puppetlabs section', :name => 'puppetlabs-products') + end - it "#create sets the yumrepo properties on the according section" do - described_class.expects(:section).returns(puppetlabs_section) - puppetlabs_section.expects(:[]=).with('baseurl', 'http://yum.puppetlabs.com/el/6/products/$basearch') - puppetlabs_section.expects(:[]=).with('descr', 'Puppet Labs Products El 6 - $basearch') - puppetlabs_section.expects(:[]=).with('enabled', '1') - puppetlabs_section.expects(:[]=).with('gpgcheck', '1') - puppetlabs_section.expects(:[]=).with('gpgkey', 'file:///etc/pki/rpm-gpg/RPM-GPG-KEY-puppetlabs') + before do + type_instance.provider = provider + described_class.stubs(:section).with('puppetlabs-products').returns(section) + end - provider.create + describe "methods used by ensurable" do + it "#create sets the yumrepo properties on the according section" do + section.expects(:[]=).with('baseurl', 'http://yum.puppetlabs.com/el/6/products/$basearch') + section.expects(:[]=).with('name', 'Puppet Labs Products El 6 - $basearch') + section.expects(:[]=).with('enabled', '1') + section.expects(:[]=).with('gpgcheck', '1') + section.expects(:[]=).with('gpgkey', 'file:///etc/pki/rpm-gpg/RPM-GPG-KEY-puppetlabs') + + provider.create + end + + it "#exists? checks if the repo has been marked as present" do + described_class.stubs(:section).returns(stub(:[]= => nil)) + provider.create + expect(provider).to be_exist + end + + it "#destroy deletes the associated ini file section" do + described_class.expects(:section).returns(section) + section.expects(:destroy=).with(true) + provider.destroy + end end - it "#exists? checks if the repo has been marked as present" do - described_class.stubs(:section).returns(stub(:[]= => nil)) - provider.create - expect(provider).to be_exist + describe "getting properties" do + it "maps the 'descr' property to the 'name' INI property" do + section.expects(:[]).with('name').returns 'Some rather long description of the repository' + expect(provider.descr).to eq 'Some rather long description of the repository' + end + + it "gets the property from the INI section" do + section.expects(:[]).with('enabled').returns '1' + expect(provider.enabled).to eq '1' + end + + it "sets the property as :absent if the INI property is nil" do + section.expects(:[]).with('exclude').returns nil + expect(provider.exclude).to eq :absent + end end - it "#destroy deletes the associated ini file section" do - described_class.expects(:section).returns(puppetlabs_section) - puppetlabs_section.expects(:destroy=).with(true) - provider.destroy + describe "setting properties" do + it "maps the 'descr' property to the 'name' INI property" do + section.expects(:[]=).with('name', 'Some rather long description of the repository') + provider.descr = 'Some rather long description of the repository' + end + + it "sets the property on the INI section" do + section.expects(:[]=).with('enabled', '0') + provider.enabled = '0' + end + + it "sets the section field to nil when the specified value is absent" do + section.expects(:[]=).with('exclude', nil) + provider.exclude = :absent + end end end describe 'reposdir' do let(:defaults) { ['/etc/yum.repos.d', '/etc/yum/repos.d'] } before do Puppet::FileSystem.stubs(:exist?).with('/etc/yum.repos.d').returns(true) Puppet::FileSystem.stubs(:exist?).with('/etc/yum/repos.d').returns(true) end it "returns the default directories if yum.conf doesn't contain a `reposdir` entry" do described_class.stubs(:find_conf_value).with('reposdir', '/etc/yum.conf') described_class.reposdir('/etc/yum.conf').should == defaults end it "includes the directory specified by the yum.conf 'reposdir' entry when the directory is present" do Puppet::FileSystem.expects(:exist?).with("/etc/yum/extra.repos.d").returns(true) described_class.expects(:find_conf_value).with('reposdir', '/etc/yum.conf').returns "/etc/yum/extra.repos.d" described_class.reposdir('/etc/yum.conf').should include("/etc/yum/extra.repos.d") end it "doesn't the directory specified by the yum.conf 'reposdir' entry when the directory is absent" do Puppet::FileSystem.expects(:exist?).with("/etc/yum/extra.repos.d").returns(false) described_class.expects(:find_conf_value).with('reposdir', '/etc/yum.conf').returns "/etc/yum/extra.repos.d" described_class.reposdir('/etc/yum.conf').should_not include("/etc/yum/extra.repos.d") end it "raises an entry if none of the specified repo directories exist" do Puppet::FileSystem.unstub(:exist?) Puppet::FileSystem.stubs(:exist?).returns false described_class.stubs(:find_conf_value).with('reposdir', '/etc/yum.conf') expect { described_class.reposdir('/etc/yum.conf') }.to raise_error('No yum directories were found on the local filesystem') end end end diff --git a/spec/unit/type/yumrepo_spec.rb b/spec/unit/type/yumrepo_spec.rb index 25dd8d833..54c720ee2 100644 --- a/spec/unit/type/yumrepo_spec.rb +++ b/spec/unit/type/yumrepo_spec.rb @@ -1,78 +1,243 @@ require 'spec_helper' require 'puppet' -describe Puppet::Type.type(:yumrepo) do - let(:yumrepo) { - Puppet::Type.type(:yumrepo).new( - :name => "puppetlabs" - ) - } +shared_examples_for "a yumrepo parameter that can be absent" do |param| + it "can be set as :absent" do + described_class.new(:name => 'puppetlabs', param => :absent) + end +end - describe "When validating attributes" do - it "should have a 'name' parameter'" do - yumrepo[:name].should == "puppetlabs" - end +shared_examples_for "a yumrepo parameter that expects a boolean parameter" do |param| + valid_values = %w[True False 0 1 No Yes] - [:baseurl, :cost, :descr, :enabled, :enablegroups, :exclude, :failovermethod, - :gpgcheck, :repo_gpgcheck, :gpgkey, :http_caching, :include, :includepkgs, :keepalive, - :metadata_expire, :mirrorlist, :priority, :protect, :proxy, :proxy_username, - :proxy_password, :timeout, :sslcacert, :sslverify, :sslclientcert, - :sslclientkey, :s3_enabled, :metalink].each do |param| - it "should have a '#{param}' parameter" do - Puppet::Type.type(:yumrepo).attrtype(param).should == :property - end + valid_values.each do |value| + it "accepts a valid value of #{value}" do + instance = described_class.new(:name => 'puppetlabs', param => value) + expect(instance[param]).to eq value + end + it "accepts #{value} downcased to #{value.downcase}" do + instance = described_class.new(:name => 'puppetlabs', param => value.downcase) + expect(instance[param]).to eq value.downcase end end - describe "When validating attribute values" do - [:cost, :enabled, :enablegroups, :failovermethod, :gpgcheck, :repo_gpgcheck, :http_caching, - :keepalive, :metadata_expire, :priority, :protect, :timeout].each do |param| - it "should support :absent as a value to '#{param}' parameter" do - Puppet::Type.type(:yumrepo).new(:name => 'puppetlabs', param => :absent) + it "rejects invalid boolean values" do + expect { + described_class.new(:name => 'puppetlabs', param => 'flase') + }.to raise_error(Puppet::ResourceError, /Parameter #{param} failed/) + end +end + +shared_examples_for "a yumrepo parameter that accepts a single URL" do |param| + it "can accept a single URL" do + described_class.new( + :name => 'puppetlabs', + param => 'http://localhost/yumrepos' + ) + end + + it "fails if an invalid URL is provided" do + expect { + described_class.new( + :name => 'puppetlabs', + param => "that's no URL!" + ) + }.to raise_error(Puppet::ResourceError, /Parameter #{param} failed/) + end + + it "fails if a valid URL uses an invalid URI scheme" do + expect { + described_class.new( + :name => 'puppetlabs', + param => 'ldap://localhost/yumrepos' + ) + }.to raise_error(Puppet::ResourceError, /Parameter #{param} failed/) + end +end + +shared_examples_for "a yumrepo parameter that accepts multiple URLs" do |param| + it "can accept multiple URLs" do + described_class.new( + :name => 'puppetlabs', + param => 'http://localhost/yumrepos http://localhost/more-yumrepos' + ) + end + + it "fails if multiple URLs are given and one is invalid" do + expect { + described_class.new( + :name => 'puppetlabs', + param => "http://localhost/yumrepos That's no URL!" + ) + }.to raise_error(Puppet::ResourceError, /Parameter #{param} failed/) + end +end + +describe Puppet::Type.type(:yumrepo) do + it "has :name as its namevar" do + expect(described_class.key_attributes).to eq [:name] + end + + describe "validating" do + + describe "name" do + it "is a valid parameter" do + instance = described_class.new(:name => 'puppetlabs') + expect(instance.name).to eq 'puppetlabs' end end - [:cost, :enabled, :enablegroups, :gpgcheck, :repo_gpgcheck, :keepalive, :metadata_expire, - :priority, :protect, :timeout].each do |param| - it "should fail if '#{param}' is not true/false, 0/1, or yes/no" do - expect { Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", param => "notanumber") }.to raise_error - end + describe "target" do + it_behaves_like "a yumrepo parameter that can be absent", :target end - [:enabled, :enabledgroups, :gpgcheck, :repo_gpgcheck, :keepalive, :protect, :s3_enabled].each do |param| - it "should fail if '#{param}' does not have one of the following values (0|1)" do - expect { Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", param => "2") }.to raise_error - end + describe "descr" do + it_behaves_like "a yumrepo parameter that can be absent", :descr + end + + describe "mirrorlist" do + it_behaves_like "a yumrepo parameter that accepts a single URL", :mirrorlist + it_behaves_like "a yumrepo parameter that can be absent", :mirrorlist + end + + describe "baseurl" do + it_behaves_like "a yumrepo parameter that can be absent", :baseurl + it_behaves_like "a yumrepo parameter that accepts a single URL", :baseurl + it_behaves_like "a yumrepo parameter that accepts multiple URLs", :baseurl + end + + describe "enabled" do + it_behaves_like "a yumrepo parameter that expects a boolean parameter", :enabled + it_behaves_like "a yumrepo parameter that can be absent", :enabled + end + + describe "gpgcheck" do + it_behaves_like "a yumrepo parameter that expects a boolean parameter", :gpgcheck + it_behaves_like "a yumrepo parameter that can be absent", :gpgcheck + end + + describe "repo_gpgcheck" do + it_behaves_like "a yumrepo parameter that expects a boolean parameter", :repo_gpgcheck + it_behaves_like "a yumrepo parameter that can be absent", :repo_gpgcheck + end + + describe "gpgkey" do + it_behaves_like "a yumrepo parameter that can be absent", :gpgkey + it_behaves_like "a yumrepo parameter that accepts a single URL", :gpgkey + it_behaves_like "a yumrepo parameter that accepts multiple URLs", :gpgkey + end + + describe "include" do + it_behaves_like "a yumrepo parameter that can be absent", :include + it_behaves_like "a yumrepo parameter that accepts a single URL", :include end - it "should fail if 'failovermethod' does not have one of the following values (roundrobin|priority)" do - expect { Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", :failovermethod => "notavalidvalue") }.to raise_error + describe "exclude" do + it_behaves_like "a yumrepo parameter that can be absent", :exclude end - it "should fail if 'http_caching' does not have one of the following values (packages|all|none)" do - expect { Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", :http_caching => "notavalidvalue") }.to raise_error + describe "includepkgs" do + it_behaves_like "a yumrepo parameter that can be absent", :includepkgs end - it "should fail if 'sslverify' does not have one of the following values (True|False)" do - expect { Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", :sslverify => "notavalidvalue") }.to raise_error + describe "enablegroups" do + it_behaves_like "a yumrepo parameter that expects a boolean parameter", :enablegroups + it_behaves_like "a yumrepo parameter that can be absent", :enablegroups + end + + describe "failovermethod" do + + %w[roundrobin priority].each do |value| + it "accepts a value of #{value}" do + described_class.new(:name => "puppetlabs", :failovermethod => value) + end + end + + it "raises an error if an invalid value is given" do + expect { + described_class.new(:name => "puppetlabs", :failovermethod => "notavalidvalue") + }.to raise_error(Puppet::ResourceError, /Parameter failovermethod failed/) + end + + it_behaves_like "a yumrepo parameter that can be absent", :failovermethod end - it "should succeed if 'sslverify' has one of the following values (True|False)" do - Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", :sslverify => "True")[:sslverify].should == "True" - Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", :sslverify => "False")[:sslverify].should == "False" + describe "keepalive" do + it_behaves_like "a yumrepo parameter that expects a boolean parameter", :keepalive + it_behaves_like "a yumrepo parameter that can be absent", :keepalive end - [:mirrorlist, :baseurl, :gpgkey, :include, :proxy, :metalink].each do |param| - it "should succeed if '#{param}' uses one of the following protocols (file|http|https|ftp)" do - Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", param => "file:///srv/example/")[param].should =~ %r{\Afile://} - Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", param => "http://example.com/")[param].should =~ %r{\Ahttp://} - Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", param => "https://example.com/")[param].should =~ %r{\Ahttps://} - Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", param => "ftp://example.com/")[param].should =~ %r{\Aftp://} + describe "http_caching" do + %w[packages all none].each do |value| + it "accepts a valid value of #{value}" do + described_class.new(:name => 'puppetlabs', :http_caching => value) + end end - it "should fail if '#{param}' does not use one of the following protocols (file|http|https|ftp)" do - expect { Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", param => "gopher://example.com/") }.to raise_error + it "rejects invalid values" do + expect { + described_class.new(:name => 'puppetlabs', :http_caching => 'yes') + }.to raise_error(Puppet::ResourceError, /Parameter http_caching failed/) end + + it_behaves_like "a yumrepo parameter that can be absent", :http_caching + end + + describe "timeout" do + it_behaves_like "a yumrepo parameter that can be absent", :timeout + end + + describe "metadata_expire" do + it_behaves_like "a yumrepo parameter that can be absent", :metadata_expire + end + + describe "protect" do + it_behaves_like "a yumrepo parameter that expects a boolean parameter", :protect + it_behaves_like "a yumrepo parameter that can be absent", :protect + end + + describe "priority" do + it_behaves_like "a yumrepo parameter that can be absent", :priority + end + + describe "proxy" do + it_behaves_like "a yumrepo parameter that can be absent", :proxy + it_behaves_like "a yumrepo parameter that accepts a single URL", :proxy + end + + describe "proxy_username" do + it_behaves_like "a yumrepo parameter that can be absent", :proxy_username + end + + describe "proxy_password" do + it_behaves_like "a yumrepo parameter that can be absent", :proxy_password + end + + describe "s3_enabled" do + it_behaves_like "a yumrepo parameter that expects a boolean parameter", :s3_enabled + it_behaves_like "a yumrepo parameter that can be absent", :s3_enabled + end + + describe "sslcacert" do + it_behaves_like "a yumrepo parameter that can be absent", :sslcacert + end + + describe "sslverify" do + it_behaves_like "a yumrepo parameter that expects a boolean parameter", :sslverify + it_behaves_like "a yumrepo parameter that can be absent", :sslverify + end + + describe "sslclientcert" do + it_behaves_like "a yumrepo parameter that can be absent", :sslclientcert + end + + describe "sslclientkey" do + it_behaves_like "a yumrepo parameter that can be absent", :sslclientkey + end + + describe "metalink" do + it_behaves_like "a yumrepo parameter that can be absent", :metalink + it_behaves_like "a yumrepo parameter that accepts a single URL", :metalink end end end diff --git a/spec/unit/util/inifile_spec.rb b/spec/unit/util/inifile_spec.rb new file mode 100644 index 000000000..f81647ad7 --- /dev/null +++ b/spec/unit/util/inifile_spec.rb @@ -0,0 +1,492 @@ +require 'spec_helper' +require 'puppet/util/inifile' + +describe Puppet::Util::IniConfig::Section do + + subject { described_class.new('testsection', '/some/imaginary/file') } + + describe "determining if the section is dirty" do + it "is not dirty on creation" do + expect(subject).to_not be_dirty + end + + it "is dirty if a key is changed" do + subject['hello'] = 'world' + expect(subject).to be_dirty + end + + it "is dirty if the section has been explicitly marked as dirty" do + subject.mark_dirty + expect(subject).to be_dirty + end + + it "is dirty if the section is marked for deletion" do + subject.destroy = true + expect(subject).to be_dirty + end + + it "is clean if the section has been explicitly marked as clean" do + subject['hello'] = 'world' + subject.mark_clean + expect(subject).to_not be_dirty + end + end + + describe "reading an entry" do + it "returns nil if the key is not present" do + expect(subject['hello']).to be_nil + end + + it "returns the value if the key is specified" do + subject.entries << ['hello', 'world'] + expect(subject['hello']).to eq 'world' + end + + it "ignores comments when looking for a match" do + subject.entries << '#this = comment' + expect(subject['#this']).to be_nil + end + end + + describe "formatting the section" do + it "prefixes the output with the section header" do + expect(subject.format).to eq "[testsection]\n" + end + + it "restores comments and blank lines" do + subject.entries << "#comment\n" + subject.entries << " " + expect(subject.format).to eq( + "[testsection]\n" + + "#comment\n" + + " " + ) + end + + it "adds all keys that have values" do + subject.entries << ['somekey', 'somevalue'] + expect(subject.format).to eq("[testsection]\nsomekey=somevalue\n") + end + + it "excludes keys that have a value of nil" do + subject.entries << ['empty', nil] + expect(subject.format).to eq("[testsection]\n") + end + + it "preserves the order of the section" do + subject.entries << ['firstkey', 'firstval'] + subject.entries << "# I am a comment, hear me roar\n" + subject.entries << ['secondkey', 'secondval'] + + expect(subject.format).to eq( + "[testsection]\n" + + "firstkey=firstval\n" + + "# I am a comment, hear me roar\n" + + "secondkey=secondval\n" + ) + end + + it "is empty if the section is marked for deletion" do + subject.entries << ['firstkey', 'firstval'] + subject.destroy = true + expect(subject.format).to eq('') + end + end +end + +describe Puppet::Util::IniConfig::PhysicalFile do + subject { described_class.new('/some/nonexistent/file') } + + let(:first_sect) do + sect = Puppet::Util::IniConfig::Section.new('firstsection', '/some/imaginary/file') + sect.entries << "# comment\n" << ['onefish', 'redfish'] << "\n" + sect + end + + let(:second_sect) do + sect = Puppet::Util::IniConfig::Section.new('secondsection', '/some/imaginary/file') + sect.entries << ['twofish', 'bluefish'] + sect + end + + describe "when reading a file" do + it "raises an error if the file does not exist" do + subject.filetype.stubs(:read) + expect { + subject.read + }.to raise_error(%r[Cannot read nonexistent file .*/some/nonexistent/file]) + end + + it "passes the contents of the file to #parse" do + subject.filetype.stubs(:read).returns "[section]" + subject.expects(:parse).with("[section]") + + subject.read + end + + end + + describe "when parsing a file" do + describe "parsing sections" do + it "creates new sections the first time that the section is found" do + text = "[mysect]\n" + + subject.parse(text) + + expect(subject.contents).to have(1).items + sect = subject.contents[0] + expect(sect.name).to eq "mysect" + end + + it "raises an error if a section is redefined in the file" do + text = "[mysect]\n[mysect]\n" + + expect { + subject.parse(text) + }.to raise_error(Puppet::Util::IniConfig::IniParseError, + /Section "mysect" is already defined, cannot redefine/) + end + + it "raises an error if a section is redefined in the file collection" do + subject.file_collection = stub('file collection', :get_section => true) + text = "[mysect]\n[mysect]\n" + + expect { + subject.parse(text) + }.to raise_error(Puppet::Util::IniConfig::IniParseError, + /Section "mysect" is already defined, cannot redefine/) + end + + end + + describe "parsing properties" do + it "raises an error if the property is not within a section" do + text = "key=val\n" + + expect { + subject.parse(text) + }.to raise_error(Puppet::Util::IniConfig::IniParseError, + /Property with key "key" outside of a section/) + end + + it "adds the property to the current section" do + text = "[main]\nkey=val\n" + + subject.parse(text) + expect(subject.contents).to have(1).items + sect = subject.contents[0] + expect(sect['key']).to eq "val" + end + end + + describe "parsing line continuations" do + + it "adds the continued line to the last parsed property" do + text = "[main]\nkey=val\n moreval" + + subject.parse(text) + expect(subject.contents).to have(1).items + sect = subject.contents[0] + expect(sect['key']).to eq "val\n moreval" + end + end + + describe "parsing comments and whitespace" do + it "treats # as a comment leader" do + text = "# octothorpe comment" + + subject.parse(text) + expect(subject.contents).to eq ["# octothorpe comment"] + end + + it "treats ; as a comment leader" do + text = "; semicolon comment" + + subject.parse(text) + expect(subject.contents).to eq ["; semicolon comment"] + end + + it "treates 'rem' as a comment leader" do + text = "rem rapid eye movement comment" + + subject.parse(text) + expect(subject.contents).to eq ["rem rapid eye movement comment"] + end + + it "stores comments and whitespace in a section in the correct section" do + text = "[main]\n; main section comment" + + subject.parse(text) + + sect = subject.get_section("main") + expect(sect.entries).to eq ["; main section comment"] + end + end + end + + it "can return all sections" do + text = "[first]\n" + + "; comment\n" + + "[second]\n" + + "key=value" + + subject.parse(text) + + sections = subject.sections + expect(sections).to have(2).items + expect(sections[0].name).to eq "first" + expect(sections[1].name).to eq "second" + end + + it "can retrieve a specific section" do + text = "[first]\n" + + "; comment\n" + + "[second]\n" + + "key=value" + + subject.parse(text) + + section = subject.get_section("second") + expect(section.name).to eq "second" + expect(section["key"]).to eq "value" + end + + describe "formatting" do + + it "concatenates each formatted section in order" do + subject.contents << first_sect << second_sect + + expected = "[firstsection]\n" + + "# comment\n" + + "onefish=redfish\n" + + "\n" + + "[secondsection]\n" + + "twofish=bluefish\n" + + expect(subject.format).to eq expected + end + + it "includes comments that are not within a section" do + subject.contents << "# This comment is not in a section\n" << first_sect << second_sect + + expected = "# This comment is not in a section\n" + + "[firstsection]\n" + + "# comment\n" + + "onefish=redfish\n" + + "\n" + + "[secondsection]\n" + + "twofish=bluefish\n" + + expect(subject.format).to eq expected + end + + it "excludes sections that are marked to be destroyed" do + subject.contents << first_sect << second_sect + first_sect.destroy = true + + expected = "[secondsection]\n" + "twofish=bluefish\n" + + expect(subject.format).to eq expected + end + end + + describe "storing the file" do + describe "with empty contents" do + describe "and destroy_empty is true" do + before { subject.destroy_empty = true } + it "removes the file if there are no sections" do + File.expects(:unlink) + subject.store + end + + it "removes the file if all sections are marked to be destroyed" do + subject.contents << first_sect << second_sect + first_sect.destroy = true + second_sect.destroy = true + + File.expects(:unlink) + subject.store + end + + it "doesn't remove the file if not all sections are marked to be destroyed" do + subject.contents << first_sect << second_sect + first_sect.destroy = true + second_sect.destroy = false + + File.expects(:unlink).never + subject.filetype.stubs(:write) + subject.store + end + end + + it "rewrites the file if destroy_empty is false" do + subject.contents << first_sect << second_sect + first_sect.destroy = true + second_sect.destroy = true + + File.expects(:unlink).never + subject.stubs(:format).returns "formatted" + subject.filetype.expects(:write).with("formatted") + subject.store + end + end + + it "rewrites the file if any section is dirty" do + subject.contents << first_sect << second_sect + first_sect.mark_dirty + second_sect.mark_clean + + subject.stubs(:format).returns "formatted" + subject.filetype.expects(:write).with("formatted") + subject.store + end + + it "doesn't modify the file if all sections are clean" do + subject.contents << first_sect << second_sect + first_sect.mark_clean + second_sect.mark_clean + + subject.stubs(:format).returns "formatted" + subject.filetype.expects(:write).never + subject.store + end + end +end + +describe Puppet::Util::IniConfig::FileCollection do + + let(:path_a) { '/some/nonexistent/file/a' } + let(:path_b) { '/some/nonexistent/file/b' } + + let(:file_a) { Puppet::Util::IniConfig::PhysicalFile.new(path_a) } + let(:file_b) { Puppet::Util::IniConfig::PhysicalFile.new(path_b) } + + let(:sect_a1) { Puppet::Util::IniConfig::Section.new('sect_a1', path_a) } + let(:sect_a2) { Puppet::Util::IniConfig::Section.new('sect_a2', path_a) } + + let(:sect_b1) { Puppet::Util::IniConfig::Section.new('sect_b1', path_b) } + let(:sect_b2) { Puppet::Util::IniConfig::Section.new('sect_b2', path_b) } + + before do + file_a.contents << sect_a1 << sect_a2 + file_b.contents << sect_b1 << sect_b2 + end + + describe "reading a file" do + let(:stub_file) { stub('Physical file') } + + it "creates a new PhysicalFile and uses that to read the file" do + stub_file.expects(:read) + stub_file.expects(:file_collection=) + Puppet::Util::IniConfig::PhysicalFile.expects(:new).with(path_a).returns stub_file + + subject.read(path_a) + end + + it "stores the PhysicalFile and the path to the file" do + stub_file.stubs(:read) + stub_file.stubs(:file_collection=) + Puppet::Util::IniConfig::PhysicalFile.stubs(:new).with(path_a).returns stub_file + subject.read(path_a) + + path, physical_file = subject.files.first + + expect(path).to eq(path_a) + expect(physical_file).to eq stub_file + end + end + + describe "storing all files" do + before do + subject.files[path_a] = file_a + subject.files[path_b] = file_b + end + + it "stores all files in the collection" do + file_a.expects(:store).once + file_b.expects(:store).once + + subject.store + end + end + + describe "iterating over sections" do + before do + subject.files[path_a] = file_a + subject.files[path_b] = file_b + end + + it "yields every section from every file" do + [sect_a1, sect_a2, sect_b1, sect_b2].each do |sect| + sect.expects(:touch).once + end + + subject.each_section do |sect| + sect.touch + end + end + end + + describe "iterating over files" do + before do + subject.files[path_a] = file_a + subject.files[path_b] = file_b + end + + it "yields the path to every file in the collection" do + seen = [] + subject.each_file do |file| + seen << file + end + + expect(seen).to include(path_a) + expect(seen).to include(path_b) + end + end + + describe "retrieving a specific section" do + before do + subject.files[path_a] = file_a + subject.files[path_b] = file_b + end + + it "retrieves the first section defined" do + expect(subject.get_section('sect_b1')).to eq sect_b1 + end + + it "returns nil if there was no section with the given name" do + expect(subject.get_section('nope')).to be_nil + end + + it "allows #[] to be used as an alias to #get_section" do + expect(subject['b2']).to eq subject.get_section('b2') + end + end + + describe "checking if a section has been defined" do + before do + subject.files[path_a] = file_a + subject.files[path_b] = file_b + end + + it "is true if a section with the given name is defined" do + expect(subject.include?('sect_a1')).to be_true + end + + it "is false if a section with the given name can't be found" do + expect(subject.include?('nonexistent')).to be_false + end + end + + describe "adding a new section" do + before do + subject.files[path_a] = file_a + subject.files[path_b] = file_b + end + + it "adds the section to the appropriate file" do + file_a.expects(:add_section).with('newsect') + subject.add_section('newsect', path_a) + end + end +end