diff --git a/lib/puppet/module.rb b/lib/puppet/module.rb index b68cd87e8..db27abbc4 100644 --- a/lib/puppet/module.rb +++ b/lib/puppet/module.rb @@ -1,263 +1,314 @@ require 'puppet/util/logging' require 'semver' require 'puppet/module_tool/applications' # Support for modules class Puppet::Module class Error < Puppet::Error; end class MissingModule < Error; end class IncompatibleModule < Error; end class UnsupportedPlatform < Error; end class IncompatiblePlatform < Error; end class MissingMetadata < Error; end class InvalidName < Error; end include Puppet::Util::Logging TEMPLATES = "templates" FILES = "files" MANIFESTS = "manifests" PLUGINS = "plugins" FILETYPES = [MANIFESTS, FILES, TEMPLATES, PLUGINS] # Find and return the +module+ that +path+ belongs to. If +path+ is # absolute, or if there is no module whose name is the first component # of +path+, return +nil+ def self.find(modname, environment = nil) return nil unless modname Puppet::Node::Environment.new(environment).module(modname) end attr_reader :name, :environment attr_writer :environment attr_accessor :dependencies, :forge_name attr_accessor :source, :author, :version, :license, :puppetversion, :summary, :description, :project_page def has_metadata? return false unless metadata_file return false unless FileTest.exist?(metadata_file) metadata = PSON.parse File.read(metadata_file) return metadata.is_a?(Hash) && !metadata.keys.empty? end def initialize(name, options = {}) @name = name @path = options[:path] assert_validity if options[:environment].is_a?(Puppet::Node::Environment) @environment = options[:environment] else @environment = Puppet::Node::Environment.new(options[:environment]) end load_metadata if has_metadata? validate_puppet_version end FILETYPES.each do |type| # A boolean method to let external callers determine if # we have files of a given type. define_method(type +'?') do return false unless path return false unless FileTest.exist?(subpath(type)) return true end # A method for returning a given file of a given type. # e.g., file = mod.manifest("my/manifest.pp") # # If the file name is nil, then the base directory for the # file type is passed; this is used for fileserving. define_method(type.to_s.sub(/s$/, '')) do |file| return nil unless path # If 'file' is nil then they're asking for the base path. # This is used for things like fileserving. if file full_path = File.join(subpath(type), file) else full_path = subpath(type) end return nil unless FileTest.exist?(full_path) return full_path end end def exist? ! path.nil? end # Find the first 'files' directory. This is used by the XMLRPC fileserver. def file_directory subpath("files") end def license_file return @license_file if defined?(@license_file) return @license_file = nil unless path @license_file = File.join(path, "License") end def load_metadata data = PSON.parse File.read(metadata_file) @forge_name = data['name'].gsub('-', '/') if data['name'] [:source, :author, :version, :license, :puppetversion, :dependencies].each do |attr| unless value = data[attr.to_s] unless attr == :puppetversion raise MissingMetadata, "No #{attr} module metadata provided for #{self.name}" end end + + # NOTICE: The fallback to `versionRequirement` is something we'd like to + # not have to support, but we have a reasonable number of releases that + # don't use `version_requirement`. When we can deprecate this, we should. + if attr == :dependencies + value.tap do |dependencies| + dependencies.each do |dep| + dep['version_requirement'] ||= dep['versionRequirement'] || '>= 0.0.0' + end + end + end + send(attr.to_s + "=", value) end end # Return the list of manifests matching the given glob pattern, # defaulting to 'init.{pp,rb}' for empty modules. def match_manifests(rest) pat = File.join(path, MANIFESTS, rest || 'init') [manifest("init.pp"),manifest("init.rb")].compact + Dir. glob(pat + (File.extname(pat).empty? ? '.{pp,rb}' : '')). reject { |f| FileTest.directory?(f) } end def metadata_file return @metadata_file if defined?(@metadata_file) return @metadata_file = nil unless path @metadata_file = File.join(path, "metadata.json") end # Find this module in the modulepath. def path @path ||= environment.modulepath.collect { |path| File.join(path, name) }.find { |d| FileTest.directory?(d) } end + def modulepath + File.dirname(path) if path + end + # Find all plugin directories. This is used by the Plugins fileserving mount. def plugin_directory subpath("plugins") end def supports(name, version = nil) @supports ||= [] @supports << [name, version] end def to_s result = "Module #{name}" result += "(#{path})" if path result end def dependencies_as_modules dependent_modules = [] dependencies and dependencies.each do |dep| author, dep_name = dep["name"].split('/') found_module = environment.module(dep_name) dependent_modules << found_module if found_module end dependent_modules end def required_by environment.module_requirements[self.forge_name] || {} end def has_local_changes? changes = Puppet::Module::Tool::Applications::Checksummer.run(path) !changes.empty? end - def unmet_dependencies - return [] unless dependencies + def local_changes + Puppet::Module::Tool::Applications::Checksummer.run(path) + end + # Identify and mark unmet dependencies. A dependency will be marked unmet + # for the following reasons: + # + # * not installed and is thus considered missing + # * installed and does not meet the version requirements for this module + # * installed and doesn't use semantic versioning + # + # Returns a list of hashes representing the details of an unmet dependency. + # + # Example: + # + # [ + # { + # :reason => :missing, + # :name => 'puppetlabs-mysql', + # :version_constraint => 'v0.0.1', + # :mod_details => { + # :installed_version => '0.0.1' + # } + # :parent => { + # :name => 'puppetlabs-bacula', + # :version => 'v1.0.0' + # } + # } + # ] + # + def unmet_dependencies unmet_dependencies = [] + return unmet_dependencies unless dependencies dependencies.each do |dependency| forge_name = dependency['name'] - author, dep_name = forge_name.split('/') - version_string = dependency['version_requirement'] + version_string = dependency['version_requirement'] || '>= 0.0.0' - equality, dep_version = version_string ? version_string.split("\s") : [nil, nil] - - unless dep_mod = environment.module(dep_name) - msg = "Missing dependency `#{dep_name}`:\n" - msg += " `#{self.name}` (#{self.version}) requires `#{forge_name}` (#{version_string})\n" - unmet_dependencies << { :name => forge_name, :error => msg } - next + dep_mod = begin + environment.module_by_forge_name(forge_name) + rescue => e + nil end - if dep_version && !dep_mod.version - msg = "Unversioned dependency `#{dep_mod.name}`:\n" - msg += " `#{self.name}` (#{self.version}) requires `#{forge_name}` (#{version_string})\n" - unmet_dependencies << { :name => forge_name, :error => msg } + error_details = { + :name => forge_name, + :version_constraint => version_string.gsub(/^(?=\d)/, "v"), + :parent => { + :name => self.forge_name, + :version => self.version.gsub(/^(?=\d)/, "v") + }, + :mod_details => { + :installed_version => dep_mod.nil? ? nil : dep_mod.version + } + } + + unless dep_mod + error_details[:reason] = :missing + unmet_dependencies << error_details next end - if dep_version + if version_string begin - required_version_semver = SemVer.new(dep_version) + required_version_semver_range = SemVer[version_string] actual_version_semver = SemVer.new(dep_mod.version) rescue ArgumentError - msg = "Non semantic version dependency `#{dep_mod.name}` (#{dep_mod.version}):\n" - msg += " `#{self.name}` (#{self.version}) requires `#{forge_name}` (#{version_string})\n" - unmet_dependencies << { :name => forge_name, :error => msg } + error_details[:reason] = :non_semantic_version + unmet_dependencies << error_details next end - if !actual_version_semver.send(equality, required_version_semver) - msg = "Version dependency mismatch `#{dep_mod.name}` (#{dep_mod.version}):\n" - msg += " `#{self.name}` (#{self.version}) requires `#{forge_name}` (#{version_string})\n" - unmet_dependencies << { :name => forge_name, :error => msg } + unless required_version_semver_range.include? actual_version_semver + error_details[:reason] = :version_mismatch + unmet_dependencies << error_details next end end end + unmet_dependencies end def validate_puppet_version return unless puppetversion and puppetversion != Puppet.version raise IncompatibleModule, "Module #{self.name} is only compatible with Puppet version #{puppetversion}, not #{Puppet.version}" end private def subpath(type) return File.join(path, type) unless type.to_s == "plugins" backward_compatible_plugins_dir end def backward_compatible_plugins_dir if dir = File.join(path, "plugins") and FileTest.exist?(dir) Puppet.warning "using the deprecated 'plugins' directory for ruby extensions; please move to 'lib'" return dir else return File.join(path, "lib") end end def assert_validity raise InvalidName, "Invalid module name #{name}; module names must be alphanumeric (plus '-'), not '#{name}'" unless name =~ /^[-\w]+$/ end def ==(other) self.name == other.name && - self.version == other.version && - self.path == other.path && - self.environment == other.environment + self.version == other.version && + self.path == other.path && + self.environment == other.environment end end diff --git a/lib/puppet/node/environment.rb b/lib/puppet/node/environment.rb index 717b9f28a..b16d15137 100644 --- a/lib/puppet/node/environment.rb +++ b/lib/puppet/node/environment.rb @@ -1,215 +1,217 @@ require 'puppet/util' require 'puppet/util/cacher' require 'monitor' # Just define it, so this class has fewer load dependencies. class Puppet::Node end # Model the environment that a node can operate in. This class just # provides a simple wrapper for the functionality around environments. class Puppet::Node::Environment module Helper def environment Puppet::Node::Environment.new(@environment) end def environment=(env) if env.is_a?(String) or env.is_a?(Symbol) @environment = env else @environment = env.name end end end include Puppet::Util::Cacher @seen = {} # Return an existing environment instance, or create a new one. def self.new(name = nil) return name if name.is_a?(self) name ||= Puppet.settings.value(:environment) raise ArgumentError, "Environment name must be specified" unless name symbol = name.to_sym return @seen[symbol] if @seen[symbol] obj = self.allocate obj.send :initialize, symbol @seen[symbol] = obj end def self.current Thread.current[:environment] || root end def self.current=(env) Thread.current[:environment] = new(env) end def self.root @root end def self.clear @seen.clear end attr_reader :name # Return an environment-specific setting. def [](param) Puppet.settings.value(param, self.name) end def initialize(name) @name = name extend MonitorMixin end def known_resource_types # This makes use of short circuit evaluation to get the right thread-safe # per environment semantics with an efficient most common cases; we almost # always just return our thread's known-resource types. Only at the start # of a compilation (after our thread var has been set to nil) or when the # environment has changed do we delve deeper. Thread.current[:known_resource_types] = nil if (krt = Thread.current[:known_resource_types]) && krt.environment != self Thread.current[:known_resource_types] ||= synchronize { if @known_resource_types.nil? or @known_resource_types.require_reparse? @known_resource_types = Puppet::Resource::TypeCollection.new(self) @known_resource_types.import_ast(perform_initial_import, '') end @known_resource_types } end def module(name) mod = Puppet::Module.new(name, :environment => self) return nil unless mod.exist? mod end def module_by_forge_name(forge_name) author, modname = forge_name.split('/') found_mod = self.module(modname) found_mod and found_mod.forge_name == forge_name ? found_mod : nil end # Cache the modulepath, so that we aren't searching through # all known directories all the time. cached_attr(:modulepath, Puppet[:filetimeout]) do dirs = self[:modulepath].split(File::PATH_SEPARATOR) dirs = ENV["PUPPETLIB"].split(File::PATH_SEPARATOR) + dirs if ENV["PUPPETLIB"] validate_dirs(dirs) end # Return all modules from this environment. # Cache the list, because it can be expensive to create. cached_attr(:modules, Puppet[:filetimeout]) do module_names = modulepath.collect { |path| Dir.entries(path) }.flatten.uniq module_names.collect do |path| begin Puppet::Module.new(path, :environment => self) rescue Puppet::Module::Error => e nil end end.compact end # Modules broken out by directory in the modulepath def modules_by_path modules_by_path = {} modulepath.each do |path| Dir.chdir(path) do - module_names = Dir.glob('*').select { |d| FileTest.directory? d } - modules_by_path[path] = module_names.map do |name| + module_names = Dir.glob('*').select do |d| + FileTest.directory?(d) && (File.basename(d) =~ /^[-\w]+$/) + end + modules_by_path[path] = module_names.sort.map do |name| Puppet::Module.new(name, :environment => self, :path => File.join(path, name)) end end end modules_by_path end def module_requirements deps = {} modules.each do |mod| next unless mod.forge_name deps[mod.forge_name] ||= [] mod.dependencies and mod.dependencies.each do |mod_dep| deps[mod_dep['name']] ||= [] dep_details = { 'name' => mod.forge_name, 'version' => mod.version, 'version_requirement' => mod_dep['version_requirement'] } deps[mod_dep['name']] << dep_details end end deps.each do |mod, mod_deps| deps[mod] = mod_deps.sort_by {|d| d['name']} end deps end def to_s name.to_s end def to_sym to_s.to_sym end # The only thing we care about when serializing an environment is its # identity; everything else is ephemeral and should not be stored or # transmitted. def to_zaml(z) self.to_s.to_zaml(z) end def validate_dirs(dirs) dirs.collect do |dir| unless Puppet::Util.absolute_path?(dir) File.expand_path(File.join(Dir.getwd, dir)) else dir end end.find_all do |p| Puppet::Util.absolute_path?(p) && FileTest.directory?(p) end end private def perform_initial_import return empty_parse_result if Puppet.settings[:ignoreimport] parser = Puppet::Parser::Parser.new(self) if code = Puppet.settings.uninterpolated_value(:code, name.to_s) and code != "" parser.string = code else file = Puppet.settings.value(:manifest, name.to_s) parser.file = file end parser.parse rescue => detail known_resource_types.parse_failed = true msg = "Could not parse for environment #{self}: #{detail}" error = Puppet::Error.new(msg) error.set_backtrace(detail.backtrace) raise error end def empty_parse_result # Return an empty toplevel hostclass to use as the result of # perform_initial_import when no file was actually loaded. return Puppet::Parser::AST::Hostclass.new('') end @root = new(:'*root*') end diff --git a/spec/unit/module_spec.rb b/spec/unit/module_spec.rb index 657ca403a..adabf55b3 100755 --- a/spec/unit/module_spec.rb +++ b/spec/unit/module_spec.rb @@ -1,731 +1,864 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet_spec/files' require 'puppet_spec/modules' require 'puppet/module_tool/checksums' describe Puppet::Module do include PuppetSpec::Files before do # This is necessary because of the extra checks we have for the deprecated # 'plugins' directory FileTest.stubs(:exist?).returns false end it "should have a class method that returns a named module from a given environment" do env = mock 'module' env.expects(:module).with("mymod").returns "yep" Puppet::Node::Environment.expects(:new).with("myenv").returns env Puppet::Module.find("mymod", "myenv").should == "yep" end it "should return nil if asked for a named module that doesn't exist" do env = mock 'module' env.expects(:module).with("mymod").returns nil Puppet::Node::Environment.expects(:new).with("myenv").returns env Puppet::Module.find("mymod", "myenv").should be_nil end it "should support a 'version' attribute" do mod = Puppet::Module.new("mymod") mod.version = 1.09 mod.version.should == 1.09 end it "should support a 'source' attribute" do mod = Puppet::Module.new("mymod") mod.source = "http://foo/bar" mod.source.should == "http://foo/bar" end it "should support a 'project_page' attribute" do mod = Puppet::Module.new("mymod") mod.project_page = "http://foo/bar" mod.project_page.should == "http://foo/bar" end it "should support an 'author' attribute" do mod = Puppet::Module.new("mymod") mod.author = "Luke Kanies " mod.author.should == "Luke Kanies " end it "should support a 'license' attribute" do mod = Puppet::Module.new("mymod") mod.license = "GPL2" mod.license.should == "GPL2" end it "should support a 'summary' attribute" do mod = Puppet::Module.new("mymod") mod.summary = "GPL2" mod.summary.should == "GPL2" end it "should support a 'description' attribute" do mod = Puppet::Module.new("mymod") mod.description = "GPL2" mod.description.should == "GPL2" end it "should support specifying a compatible puppet version" do mod = Puppet::Module.new("mymod") mod.puppetversion = "0.25" mod.puppetversion.should == "0.25" end it "should validate that the puppet version is compatible" do mod = Puppet::Module.new("mymod") mod.puppetversion = "0.25" Puppet.expects(:version).returns "0.25" mod.validate_puppet_version end it "should fail if the specified puppet version is not compatible" do mod = Puppet::Module.new("mymod") mod.puppetversion = "0.25" Puppet.stubs(:version).returns "0.24" lambda { mod.validate_puppet_version }.should raise_error(Puppet::Module::IncompatibleModule) end describe "when finding unmet dependencies" do before do - @mod = Puppet::Module.new("mymod") - @mod.stubs(:dependencies).returns [ - { - "version_requirement" => ">= 2.2.0", - "name" => "baz/foobar" - } - ] + FileTest.unstub(:exist?) + @modpath = tmpdir('modpath') + Puppet.settings[:modulepath] = @modpath end it "should list modules that are missing" do - @mod.unmet_dependencies.should == [{ - :name => 'baz/foobar', - :error => <<-HEREDOC.gsub(/^\s{10}/, '') - Missing dependency `foobar`: - `mymod` () requires `baz/foobar` (>= 2.2.0) - HEREDOC + mod = PuppetSpec::Modules.create( + 'needy', + @modpath, + :metadata => { + :dependencies => [{ + "version_requirement" => ">= 2.2.0", + "name" => "baz/foobar" + }] + } + ) + mod.unmet_dependencies.should == [{ + :reason => :missing, + :name => "baz/foobar", + :version_constraint => ">= 2.2.0", + :parent => { :name => 'puppetlabs/needy', :version => 'v9.9.9' }, + :mod_details => { :installed_version => nil } }] end - it "should list modules with unmet version" do - foobar = Puppet::Module.new("foobar") - foobar.version = '2.0.0' - @mod.environment.expects(:module).with("foobar").returns foobar - - @mod.unmet_dependencies.should == [{ - :name => 'baz/foobar', - :error => <<-HEREDOC.gsub(/^\s{10}/, '') - Version dependency mismatch `foobar` (2.0.0): - `mymod` () requires `baz/foobar` (>= 2.2.0) - HEREDOC + it "should list modules that are missing and have invalid names" do + mod = PuppetSpec::Modules.create( + 'needy', + @modpath, + :metadata => { + :dependencies => [{ + "version_requirement" => ">= 2.2.0", + "name" => "baz/foobar=bar" + }] + } + ) + mod.unmet_dependencies.should == [{ + :reason => :missing, + :name => "baz/foobar=bar", + :version_constraint => ">= 2.2.0", + :parent => { :name => 'puppetlabs/needy', :version => 'v9.9.9' }, + :mod_details => { :installed_version => nil } }] end - it "should consider a dependency without a version requirement to be satisfied" do - mod = Puppet::Module.new("mymod") - mod.stubs(:dependencies).returns [{ "name" => "baz/foobar" }] - - foobar = Puppet::Module.new("foobar") - mod.environment.expects(:module).with("foobar").returns foobar - - mod.unmet_dependencies.should be_empty - end + it "should list modules with unmet version requirement" do + mod = PuppetSpec::Modules.create( + 'foobar', + @modpath, + :metadata => { + :dependencies => [{ + "version_requirement" => ">= 2.2.0", + "name" => "baz/foobar" + }] + } + ) + mod2 = PuppetSpec::Modules.create( + 'foobaz', + @modpath, + :metadata => { + :dependencies => [{ + "version_requirement" => "1.0.0", + "name" => "baz/foobar" + }] + } + ) - it "should consider a dependency without a version to be unmet" do - foobar = Puppet::Module.new("foobar") - @mod.environment.expects(:module).with("foobar").returns foobar + PuppetSpec::Modules.create( + 'foobar', + @modpath, + :metadata => { :version => '2.0.0', :author => 'baz' } + ) - @mod.unmet_dependencies.should == [{ - :name => 'baz/foobar', - :error => <<-HEREDOC.gsub(/^\s{10}/, '') - Unversioned dependency `foobar`: - `mymod` () requires `baz/foobar` (>= 2.2.0) - HEREDOC + mod.unmet_dependencies.should == [{ + :reason => :version_mismatch, + :name => "baz/foobar", + :version_constraint => ">= 2.2.0", + :parent => { :version => "v9.9.9", :name => "puppetlabs/foobar" }, + :mod_details => { :installed_version => "2.0.0" } }] - end - it "should consider a dependency without a semantic version to be unmet" do - foobar = Puppet::Module.new("foobar") - foobar.version = '5.1' - @mod.environment.expects(:module).with("foobar").returns foobar - - @mod.unmet_dependencies.should == [{ - :name => 'baz/foobar', - :error => <<-HEREDOC.gsub(/^\s{10}/, '') - Non semantic version dependency `foobar` (5.1): - `mymod` () requires `baz/foobar` (>= 2.2.0) - HEREDOC + mod2.unmet_dependencies.should == [{ + :reason => :version_mismatch, + :name => "baz/foobar", + :version_constraint => "v1.0.0", + :parent => { :version => "v9.9.9", :name => "puppetlabs/foobaz" }, + :mod_details => { :installed_version => "2.0.0" } }] + end - it "should consider a dependency requirement without a semantic version to be unmet" do - foobar = Puppet::Module.new("foobar") - foobar.version = '5.1.0' + it "should consider a dependency without a version requirement to be satisfied" do + mod = PuppetSpec::Modules.create( + 'foobar', + @modpath, + :metadata => { + :dependencies => [{ + "name" => "baz/foobar" + }] + } + ) + PuppetSpec::Modules.create( + 'foobar', + @modpath, + :metadata => { + :version => '2.0.0', + :author => 'baz' + } + ) + + mod.unmet_dependencies.should be_empty + end - mod = Puppet::Module.new("mymod") - mod.stubs(:dependencies).returns [{ "name" => "baz/foobar", "version_requirement" => '> 2.0' }] - mod.environment.expects(:module).with("foobar").returns foobar + it "should consider a dependency without a semantic version to be unmet" do + mod = PuppetSpec::Modules.create( + 'foobar', + @modpath, + :metadata => { + :dependencies => [{ + "name" => "baz/foobar" + }] + } + ) + PuppetSpec::Modules.create( + 'foobar', + @modpath, + :metadata => { + :version => '5.1', + :author => 'baz' + } + ) mod.unmet_dependencies.should == [{ - :name => 'baz/foobar', - :error => <<-HEREDOC.gsub(/^\s{10}/, '') - Non semantic version dependency `foobar` (5.1.0): - `mymod` () requires `baz/foobar` (> 2.0) - HEREDOC + :reason => :non_semantic_version, + :parent => { :version => "v9.9.9", :name => "puppetlabs/foobar" }, + :mod_details => { :installed_version => "5.1" }, + :name => "baz/foobar", + :version_constraint => ">= 0.0.0" }] end it "should have valid dependencies when no dependencies have been specified" do - mod = Puppet::Module.new("mymod") + mod = PuppetSpec::Modules.create( + 'foobar', + @modpath, + :metadata => { + :dependencies => [] + } + ) mod.unmet_dependencies.should == [] end it "should only list unmet dependencies" do - mod = Puppet::Module.new("mymod") - mod.stubs(:dependencies).returns [ - { - "version_requirement" => ">= 2.2.0", - "name" => "baz/satisfied" - }, - { - "version_requirement" => ">= 2.2.0", - "name" => "baz/notsatisfied" + mod = PuppetSpec::Modules.create( + 'mymod', + @modpath, + :metadata => { + :dependencies => [ + { + "version_requirement" => ">= 2.2.0", + "name" => "baz/satisfied" + }, + { + "version_requirement" => ">= 2.2.0", + "name" => "baz/notsatisfied" + } + ] } - ] - - satisfied = Puppet::Module.new("satisfied") - satisfied.version = "3.3.0" - - mod.environment.expects(:module).with("satisfied").returns satisfied - mod.environment.expects(:module).with("notsatisfied").returns nil + ) + PuppetSpec::Modules.create( + 'satisfied', + @modpath, + :metadata => { + :version => '3.3.0', + :author => 'baz' + } + ) mod.unmet_dependencies.should == [{ - :name => 'baz/notsatisfied', - :error => <<-HEREDOC.gsub(/^\s{10}/, '') - Missing dependency `notsatisfied`: - `mymod` () requires `baz/notsatisfied` (>= 2.2.0) - HEREDOC + :reason => :missing, + :mod_details => { :installed_version => nil }, + :parent => { :version => "v9.9.9", :name => "puppetlabs/mymod" }, + :name => "baz/notsatisfied", + :version_constraint => ">= 2.2.0" }] end it "should be empty when all dependencies are met" do - mod = Puppet::Module.new("mymod") - mod.stubs(:dependencies).returns [ - { - "version_requirement" => ">= 2.2.0", - "name" => "baz/satisfied" - }, - { - "version_requirement" => "< 2.2.0", - "name" => "baz/alsosatisfied" + mod = PuppetSpec::Modules.create( + 'mymod2', + @modpath, + :metadata => { + :dependencies => [ + { + "version_requirement" => ">= 2.2.0", + "name" => "baz/satisfied" + }, + { + "version_requirement" => "< 2.2.0", + "name" => "baz/alsosatisfied" + } + ] } - ] - satisfied = Puppet::Module.new("satisfied") - satisfied.version = "3.3.0" - alsosatisfied = Puppet::Module.new("alsosatisfied") - alsosatisfied.version = "2.1.0" - - mod.environment.expects(:module).with("satisfied").returns satisfied - mod.environment.expects(:module).with("alsosatisfied").returns alsosatisfied + ) + PuppetSpec::Modules.create( + 'satisfied', + @modpath, + :metadata => { + :version => '3.3.0', + :author => 'baz' + } + ) + PuppetSpec::Modules.create( + 'alsosatisfied', + @modpath, + :metadata => { + :version => '2.1.0', + :author => 'baz' + } + ) mod.unmet_dependencies.should be_empty end end describe "when managing supported platforms" do it "should support specifying a supported platform" do mod = Puppet::Module.new("mymod") mod.supports "solaris" end it "should support specifying a supported platform and version" do mod = Puppet::Module.new("mymod") mod.supports "solaris", 1.0 end it "should fail when not running on a supported platform" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") Facter.expects(:value).with("operatingsystem").returns "Solaris" mod.supports "hpux" lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::UnsupportedPlatform) end it "should fail when supported platforms are present but of the wrong version" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") Facter.expects(:value).with("operatingsystem").returns "Solaris" Facter.expects(:value).with("operatingsystemrelease").returns 2.0 mod.supports "Solaris", 1.0 lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::IncompatiblePlatform) end it "should be considered supported when no supported platforms have been specified" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") lambda { mod.validate_supported_platform }.should_not raise_error end it "should be considered supported when running on a supported platform" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") Facter.expects(:value).with("operatingsystem").returns "Solaris" Facter.expects(:value).with("operatingsystemrelease").returns 2.0 mod.supports "Solaris", 1.0 lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::IncompatiblePlatform) end it "should be considered supported when running on any of multiple supported platforms" do pending "Not sure how to send client platform to the module" end it "should validate its platform support on initialization" do pending "Not sure how to send client platform to the module" end end it "should return nil if asked for a module whose name is 'nil'" do Puppet::Module.find(nil, "myenv").should be_nil end it "should provide support for logging" do Puppet::Module.ancestors.should be_include(Puppet::Util::Logging) end it "should be able to be converted to a string" do Puppet::Module.new("foo").to_s.should == "Module foo" end it "should add the path to its string form if the module is found" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a" mod.to_s.should == "Module foo(/a)" end it "should fail if its name is not alphanumeric" do lambda { Puppet::Module.new(".something") }.should raise_error(Puppet::Module::InvalidName) end it "should require a name at initialization" do lambda { Puppet::Module.new }.should raise_error(ArgumentError) end it "should convert an environment name into an Environment instance" do Puppet::Module.new("foo", :environment => "prod").environment.should be_instance_of(Puppet::Node::Environment) end it "should accept an environment at initialization" do Puppet::Module.new("foo", :environment => :prod).environment.name.should == :prod end it "should use the default environment if none is provided" do env = Puppet::Node::Environment.new Puppet::Module.new("foo").environment.should equal(env) end it "should use any provided Environment instance" do env = Puppet::Node::Environment.new Puppet::Module.new("foo", :environment => env).environment.should equal(env) end describe ".path" do before do dir = tmpdir("deep_path") @first = File.join(dir, "first") @second = File.join(dir, "second") Puppet[:modulepath] = "#{@first}#{File::PATH_SEPARATOR}#{@second}" FileUtils.mkdir_p(@first) FileUtils.mkdir_p(@second) end it "should return the path to the first found instance in its environment's module paths as its path" do modpath = File.join(@first, "foo") FileUtils.mkdir_p(modpath) # Make a second one, which we shouldn't find FileUtils.mkdir_p(File.join(@second, "foo")) mod = Puppet::Module.new("foo") mod.path.should == modpath end it "should be able to find itself in a directory other than the first directory in the module path" do modpath = File.join(@second, "foo") FileUtils.mkdir_p(modpath) mod = Puppet::Module.new("foo") mod.should be_exist mod.path.should == modpath end it "should be able to find itself in a directory other than the first directory in the module path even when it exists in the first" do environment = Puppet::Node::Environment.new first_modpath = File.join(@first, "foo") FileUtils.mkdir_p(first_modpath) second_modpath = File.join(@second, "foo") FileUtils.mkdir_p(second_modpath) mod = Puppet::Module.new("foo", :environment => environment, :path => second_modpath) mod.path.should == File.join(@second, "foo") mod.environment.should == environment end end + describe '#modulepath' do + it "should return the directory the module is installed in, if a path exists" do + mod = Puppet::Module.new("foo") + mod.stubs(:path).returns "/a/foo" + mod.modulepath.should == '/a' + end + + it "should return nil if no path exists" do + mod = Puppet::Module.new("foo") + mod.stubs(:path).returns nil + mod.modulepath.should be_nil + end + end + it "should be considered existent if it exists in at least one module path" do mod = Puppet::Module.new("foo") mod.expects(:path).returns "/a/foo" mod.should be_exist end it "should be considered nonexistent if it does not exist in any of the module paths" do mod = Puppet::Module.new("foo") mod.expects(:path).returns nil mod.should_not be_exist end [:plugins, :templates, :files, :manifests].each do |filetype| dirname = filetype == :plugins ? "lib" : filetype.to_s it "should be able to return individual #{filetype}" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname, "my/file") FileTest.expects(:exist?).with(path).returns true mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should == path end it "should consider #{filetype} to be present if their base directory exists" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname) FileTest.expects(:exist?).with(path).returns true mod.send(filetype.to_s + "?").should be_true end it "should consider #{filetype} to be absent if their base directory does not exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname) FileTest.expects(:exist?).with(path).returns false mod.send(filetype.to_s + "?").should be_false end it "should consider #{filetype} to be absent if the module base directory does not exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns nil mod.send(filetype.to_s + "?").should be_false end it "should return nil if asked to return individual #{filetype} that don't exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname, "my/file") FileTest.expects(:exist?).with(path).returns false mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should be_nil end it "should return nil when asked for individual #{filetype} if the module does not exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns nil mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should be_nil end it "should return the base directory if asked for a nil path" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" base = File.join("/a/foo", dirname) FileTest.expects(:exist?).with(base).returns true mod.send(filetype.to_s.sub(/s$/, ''), nil).should == base end end %w{plugins files}.each do |filetype| short = filetype.sub(/s$/, '') dirname = filetype == "plugins" ? "lib" : filetype.to_s it "should be able to return the #{short} directory" do Puppet::Module.new("foo").should respond_to(short + "_directory") end it "should return the path to the #{short} directory" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" mod.send(short + "_directory").should == "/a/foo/#{dirname}" end end it "should throw a warning if plugins are in a 'plugins' directory rather than a 'lib' directory" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" FileTest.expects(:exist?).with("/a/foo/plugins").returns true mod.plugin_directory.should == "/a/foo/plugins" @logs.first.message.should == "using the deprecated 'plugins' directory for ruby extensions; please move to 'lib'" @logs.first.level.should == :warning end it "should default to 'lib' for the plugins directory" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" mod.plugin_directory.should == "/a/foo/lib" end end describe Puppet::Module, "when finding matching manifests" do before do @mod = Puppet::Module.new("mymod") @mod.stubs(:path).returns "/a" @pq_glob_with_extension = "yay/*.xx" @fq_glob_with_extension = "/a/manifests/#{@pq_glob_with_extension}" end it "should return all manifests matching the glob pattern" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{foo bar}) FileTest.stubs(:directory?).returns false @mod.match_manifests(@pq_glob_with_extension).should == %w{foo bar} end it "should not return directories" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{foo bar}) FileTest.expects(:directory?).with("foo").returns false FileTest.expects(:directory?).with("bar").returns true @mod.match_manifests(@pq_glob_with_extension).should == %w{foo} end it "should default to the 'init' file if no glob pattern is specified" do Dir.expects(:glob).with("/a/manifests/init.{pp,rb}").returns(%w{/a/manifests/init.pp}) @mod.match_manifests(nil).should == %w{/a/manifests/init.pp} end it "should return all manifests matching the glob pattern in all existing paths" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{a b}) @mod.match_manifests(@pq_glob_with_extension).should == %w{a b} end it "should match the glob pattern plus '.{pp,rb}' if no extention is specified" do Dir.expects(:glob).with("/a/manifests/yay/foo.{pp,rb}").returns(%w{yay}) @mod.match_manifests("yay/foo").should == %w{yay} end it "should return an empty array if no manifests matched" do Dir.expects(:glob).with(@fq_glob_with_extension).returns([]) @mod.match_manifests(@pq_glob_with_extension).should == [] end end describe Puppet::Module do include PuppetSpec::Files before do @modpath = tmpdir('modpath') @module = PuppetSpec::Modules.create('mymod', @modpath) end it "should use 'License' in its current path as its metadata file" do @module.license_file.should == "#{@modpath}/mymod/License" end it "should return nil as its license file when the module has no path" do Puppet::Module.any_instance.stubs(:path).returns nil Puppet::Module.new("foo").license_file.should be_nil end it "should cache the license file" do @module.expects(:path).once.returns nil @module.license_file @module.license_file end it "should use 'metadata.json' in its current path as its metadata file" do @module.metadata_file.should == "#{@modpath}/mymod/metadata.json" end it "should return nil as its metadata file when the module has no path" do Puppet::Module.any_instance.stubs(:path).returns nil Puppet::Module.new("foo").metadata_file.should be_nil end it "should cache the metadata file" do Puppet::Module.any_instance.expects(:path).once.returns nil mod = Puppet::Module.new("foo") mod.metadata_file.should == mod.metadata_file end it "should have metadata if it has a metadata file and its data is not empty" do FileTest.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "{\"foo\" : \"bar\"}" @module.should be_has_metadata end it "should have metadata if it has a metadata file and its data is not empty" do FileTest.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "{\"foo\" : \"bar\"}" @module.should be_has_metadata end it "should not have metadata if has a metadata file and its data is empty" do FileTest.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "/* +-----------------------------------------------------------------------+ | | | ==> DO NOT EDIT THIS FILE! <== | | | | You should edit the `Modulefile` and run `puppet-module build` | | to generate the `metadata.json` file for your releases. | | | +-----------------------------------------------------------------------+ */ {}" @module.should_not be_has_metadata end it "should know if it is missing a metadata file" do FileTest.expects(:exist?).with(@module.metadata_file).returns false @module.should_not be_has_metadata end it "should be able to parse its metadata file" do @module.should respond_to(:load_metadata) end it "should parse its metadata file on initialization if it is present" do Puppet::Module.any_instance.expects(:has_metadata?).returns true Puppet::Module.any_instance.expects(:load_metadata) Puppet::Module.new("yay") end describe "when loading the metadata file", :if => Puppet.features.pson? do before do @data = { :license => "GPL2", :author => "luke", :version => "1.0", :source => "http://foo/", :puppetversion => "0.25", :dependencies => [] } @text = @data.to_pson @module = Puppet::Module.new("foo") @module.stubs(:metadata_file).returns "/my/file" File.stubs(:read).with("/my/file").returns @text end %w{source author version license}.each do |attr| it "should set #{attr} if present in the metadata file" do @module.load_metadata @module.send(attr).should == @data[attr.to_sym] end it "should fail if #{attr} is not present in the metadata file" do @data.delete(attr.to_sym) @text = @data.to_pson File.stubs(:read).with("/my/file").returns @text lambda { @module.load_metadata }.should raise_error( Puppet::Module::MissingMetadata, "No #{attr} module metadata provided for foo" ) end end it "should set puppetversion if present in the metadata file" do @module.load_metadata @module.puppetversion.should == @data[:puppetversion] end + context "when versionRequirement is used for dependency version info" do + before do + @data = { + :license => "GPL2", + :author => "luke", + :version => "1.0", + :source => "http://foo/", + :puppetversion => "0.25", + :dependencies => [ + { + "versionRequirement" => "0.0.1", + "name" => "pmtacceptance/stdlib" + }, + { + "versionRequirement" => "0.1.0", + "name" => "pmtacceptance/apache" + } + ] + } + @text = @data.to_pson + + @module = Puppet::Module.new("foo") + @module.stubs(:metadata_file).returns "/my/file" + File.stubs(:read).with("/my/file").returns @text + end + + it "should set the dependency version_requirement key" do + @module.load_metadata + @module.dependencies[0]['version_requirement'].should == "0.0.1" + end + + it "should set the version_requirement key for all dependencies" do + @module.load_metadata + @module.dependencies[0]['version_requirement'].should == "0.0.1" + @module.dependencies[1]['version_requirement'].should == "0.1.0" + end + end + it "should fail if the discovered name is different than the metadata name" end - it "should be able to tell if there are local changes" do + it "should be able to tell if there are local changes", :fails_on_windows => true do modpath = tmpdir('modpath') foo_checksum = 'acbd18db4cc2f85cedef654fccc4a4d8' checksummed_module = PuppetSpec::Modules.create( 'changed', modpath, :metadata => { :checksums => { "foo" => foo_checksum, } } ) foo_path = Pathname.new(File.join(checksummed_module.path, 'foo')) IO.binwrite(foo_path, 'notfoo') Puppet::Module::Tool::Checksums.new(foo_path).checksum(foo_path).should_not == foo_checksum checksummed_module.has_local_changes?.should be_true IO.binwrite(foo_path, 'foo') Puppet::Module::Tool::Checksums.new(foo_path).checksum(foo_path).should == foo_checksum checksummed_module.has_local_changes?.should be_false end it "should know what other modules require it" do Puppet.settings[:modulepath] = @modpath dependable = PuppetSpec::Modules.create( 'dependable', @modpath, :metadata => {:author => 'puppetlabs'} ) PuppetSpec::Modules.create( 'needy', @modpath, :metadata => { :author => 'beggar', :dependencies => [{ "version_requirement" => ">= 2.2.0", "name" => "puppetlabs/dependable" }] } ) PuppetSpec::Modules.create( 'wantit', @modpath, :metadata => { :author => 'spoiled', :dependencies => [{ "version_requirement" => "< 5.0.0", "name" => "puppetlabs/dependable" }] } ) dependable.required_by.should =~ [ { "name" => "beggar/needy", "version" => "9.9.9", "version_requirement" => ">= 2.2.0" }, { "name" => "spoiled/wantit", "version" => "9.9.9", "version_requirement" => "< 5.0.0" } ] end end diff --git a/spec/unit/node/environment_spec.rb b/spec/unit/node/environment_spec.rb index 1af6151e1..62aef593a 100755 --- a/spec/unit/node/environment_spec.rb +++ b/spec/unit/node/environment_spec.rb @@ -1,432 +1,444 @@ #!/usr/bin/env rspec require 'spec_helper' require 'tmpdir' require 'puppet/node/environment' require 'puppet/util/execution' require 'puppet_spec/modules' describe Puppet::Node::Environment do let(:env) { Puppet::Node::Environment.new("testing") } include PuppetSpec::Files after do Puppet::Node::Environment.clear end it "should use the filetimeout for the ttl for the modulepath" do Puppet::Node::Environment.attr_ttl(:modulepath).should == Integer(Puppet[:filetimeout]) end it "should use the filetimeout for the ttl for the module list" do Puppet::Node::Environment.attr_ttl(:modules).should == Integer(Puppet[:filetimeout]) end it "should use the default environment if no name is provided while initializing an environment" do Puppet.settings.expects(:value).with(:environment).returns("one") Puppet::Node::Environment.new.name.should == :one end it "should treat environment instances as singletons" do Puppet::Node::Environment.new("one").should equal(Puppet::Node::Environment.new("one")) end it "should treat an environment specified as names or strings as equivalent" do Puppet::Node::Environment.new(:one).should equal(Puppet::Node::Environment.new("one")) end it "should return its name when converted to a string" do Puppet::Node::Environment.new(:one).to_s.should == "one" end it "should just return any provided environment if an environment is provided as the name" do one = Puppet::Node::Environment.new(:one) Puppet::Node::Environment.new(one).should equal(one) end describe "when managing known resource types" do before do @collection = Puppet::Resource::TypeCollection.new(env) env.stubs(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) Thread.current[:known_resource_types] = nil end it "should create a resource type collection if none exists" do Puppet::Resource::TypeCollection.expects(:new).with(env).returns @collection env.known_resource_types.should equal(@collection) end it "should reuse any existing resource type collection" do env.known_resource_types.should equal(env.known_resource_types) end it "should perform the initial import when creating a new collection" do env.expects(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) env.known_resource_types end it "should return the same collection even if stale if it's the same thread" do Puppet::Resource::TypeCollection.stubs(:new).returns @collection env.known_resource_types.stubs(:stale?).returns true env.known_resource_types.should equal(@collection) end it "should return the current thread associated collection if there is one" do Thread.current[:known_resource_types] = @collection env.known_resource_types.should equal(@collection) end it "should give to all threads using the same environment the same collection if the collection isn't stale" do original_thread_type_collection = Puppet::Resource::TypeCollection.new(env) Puppet::Resource::TypeCollection.expects(:new).with(env).returns original_thread_type_collection env.known_resource_types.should equal(original_thread_type_collection) original_thread_type_collection.expects(:require_reparse?).returns(false) Puppet::Resource::TypeCollection.stubs(:new).with(env).returns @collection t = Thread.new { env.known_resource_types.should equal(original_thread_type_collection) } t.join end it "should generate a new TypeCollection if the current one requires reparsing" do old_type_collection = env.known_resource_types old_type_collection.stubs(:require_reparse?).returns true Thread.current[:known_resource_types] = nil new_type_collection = env.known_resource_types new_type_collection.should be_a Puppet::Resource::TypeCollection new_type_collection.should_not equal(old_type_collection) end end it "should validate the modulepath directories" do real_file = tmpdir('moduledir') path = %W[/one /two #{real_file}].join(File::PATH_SEPARATOR) Puppet[:modulepath] = path env.modulepath.should == [real_file] end it "should prefix the value of the 'PUPPETLIB' environment variable to the module path if present" do Puppet::Util::Execution.withenv("PUPPETLIB" => %w{/l1 /l2}.join(File::PATH_SEPARATOR)) do module_path = %w{/one /two}.join(File::PATH_SEPARATOR) env.expects(:validate_dirs).with(%w{/l1 /l2 /one /two}).returns %w{/l1 /l2 /one /two} env.expects(:[]).with(:modulepath).returns module_path env.modulepath.should == %w{/l1 /l2 /one /two} end end describe "when validating modulepath or manifestdir directories" do before :each do @path_one = tmpdir("path_one") @path_two = tmpdir("path_one") sep = File::PATH_SEPARATOR Puppet[:modulepath] = "#{@path_one}#{sep}#{@path_two}" end it "should not return non-directories" do FileTest.expects(:directory?).with(@path_one).returns true FileTest.expects(:directory?).with(@path_two).returns false env.validate_dirs([@path_one, @path_two]).should == [@path_one] end it "should use the current working directory to fully-qualify unqualified paths" do FileTest.stubs(:directory?).returns true two = File.expand_path(File.join(Dir.getwd, "two")) env.validate_dirs([@path_one, 'two']).should == [@path_one, two] end end describe "when modeling a specific environment" do it "should have a method for returning the environment name" do Puppet::Node::Environment.new("testing").name.should == :testing end it "should provide an array-like accessor method for returning any environment-specific setting" do env.should respond_to(:[]) end it "should ask the Puppet settings instance for the setting qualified with the environment name" do Puppet.settings.expects(:value).with("myvar", :testing).returns("myval") env["myvar"].should == "myval" end it "should be able to return an individual module that exists in its module path" do mod = mock 'module' Puppet::Module.expects(:new).with("one", :environment => env).returns mod mod.expects(:exist?).returns true env.module("one").should equal(mod) end it "should return nil if asked for a module that does not exist in its path" do modpath = tmpdir('modpath') env.modulepath = [modpath] env.module("one").should be_nil end describe "module data" do before do dir = tmpdir("deep_path") @first = File.join(dir, "first") @second = File.join(dir, "second") Puppet[:modulepath] = "#{@first}#{File::PATH_SEPARATOR}#{@second}" FileUtils.mkdir_p(@first) FileUtils.mkdir_p(@second) end describe "#modules_by_path" do it "should return an empty list if there are no modules" do env.modules_by_path.should == { @first => [], @second => [] } end it "should include modules even if they exist in multiple dirs in the modulepath" do modpath1 = File.join(@first, "foo") FileUtils.mkdir_p(modpath1) modpath2 = File.join(@second, "foo") FileUtils.mkdir_p(modpath2) env.modules_by_path.should == { @first => [Puppet::Module.new('foo', :environment => env, :path => modpath1)], @second => [Puppet::Module.new('foo', :environment => env, :path => modpath2)] } end + + it "should ignore modules with invalid names" do + FileUtils.mkdir_p(File.join(@first, 'foo')) + FileUtils.mkdir_p(File.join(@first, 'foo2')) + FileUtils.mkdir_p(File.join(@first, 'foo-bar')) + FileUtils.mkdir_p(File.join(@first, 'foo_bar')) + FileUtils.mkdir_p(File.join(@first, 'foo=bar')) + FileUtils.mkdir_p(File.join(@first, 'foo bar')) + FileUtils.mkdir_p(File.join(@first, 'foo.bar')) + + env.modules_by_path[@first].collect{|mod| mod.name}.sort.should == %w{foo foo-bar foo2 foo_bar} + end + end describe "#module_requirements" do it "should return a list of what modules depend on other modules" do PuppetSpec::Modules.create( 'foo', @first, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => ">= 1.0.0" }] } ) PuppetSpec::Modules.create( 'bar', @second, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/foo', "version_requirement" => "<= 2.0.0" }] } ) PuppetSpec::Modules.create( 'baz', @first, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => "3.0.0" }] } ) PuppetSpec::Modules.create( 'alpha', @first, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => "~3.0.0" }] } ) env.module_requirements.should == { 'puppetlabs/alpha' => [], 'puppetlabs/foo' => [ { "name" => "puppetlabs/bar", "version" => "9.9.9", "version_requirement" => "<= 2.0.0" } ], 'puppetlabs/bar' => [ { "name" => "puppetlabs/alpha", "version" => "9.9.9", "version_requirement" => "~3.0.0" }, { "name" => "puppetlabs/baz", "version" => "9.9.9", "version_requirement" => "3.0.0" }, { "name" => "puppetlabs/foo", "version" => "9.9.9", "version_requirement" => ">= 1.0.0" } ], 'puppetlabs/baz' => [] } end end describe ".module_by_forge_name" do it "should find modules by forge_name" do mod = PuppetSpec::Modules.create( 'baz', @first, :metadata => {:author => 'puppetlabs'}, :environment => env ) env.module_by_forge_name('puppetlabs/baz').should == mod end it "should not find modules with same name by the wrong author" do mod = PuppetSpec::Modules.create( 'baz', @first, :metadata => {:author => 'sneakylabs'}, :environment => env ) env.module_by_forge_name('puppetlabs/baz').should == nil end it "should return nil when the module can't be found" do env.module_by_forge_name('ima/nothere').should be_nil end end describe ".modules" do it "should return an empty list if there are no modules" do env.modules.should == [] end it "should return a module named for every directory in each module path" do %w{foo bar}.each do |mod_name| FileUtils.mkdir_p(File.join(@first, mod_name)) end %w{bee baz}.each do |mod_name| FileUtils.mkdir_p(File.join(@second, mod_name)) end env.modules.collect{|mod| mod.name}.sort.should == %w{foo bar bee baz}.sort end it "should remove duplicates" do FileUtils.mkdir_p(File.join(@first, 'foo')) FileUtils.mkdir_p(File.join(@second, 'foo')) env.modules.collect{|mod| mod.name}.sort.should == %w{foo} end it "should ignore modules with invalid names" do FileUtils.mkdir_p(File.join(@first, 'foo')) FileUtils.mkdir_p(File.join(@first, 'foo2')) FileUtils.mkdir_p(File.join(@first, 'foo-bar')) FileUtils.mkdir_p(File.join(@first, 'foo_bar')) FileUtils.mkdir_p(File.join(@first, 'foo=bar')) FileUtils.mkdir_p(File.join(@first, 'foo bar')) env.modules.collect{|mod| mod.name}.sort.should == %w{foo foo-bar foo2 foo_bar} end it "should create modules with the correct environment" do FileUtils.mkdir_p(File.join(@first, 'foo')) - env.modules.each {|mod| mod.environment.should == env } end end end it "should cache the module list" do env.modulepath = %w{/a} Dir.expects(:entries).once.with("/a").returns %w{foo} env.modules env.modules end end describe Puppet::Node::Environment::Helper do before do @helper = Object.new @helper.extend(Puppet::Node::Environment::Helper) end it "should be able to set and retrieve the environment as a symbol" do @helper.environment = :foo @helper.environment.name.should == :foo end it "should accept an environment directly" do @helper.environment = Puppet::Node::Environment.new(:foo) @helper.environment.name.should == :foo end it "should accept an environment as a string" do @helper.environment = 'foo' @helper.environment.name.should == :foo end end describe "when performing initial import" do before do @parser = Puppet::Parser::Parser.new("test") Puppet::Parser::Parser.stubs(:new).returns @parser end it "should set the parser's string to the 'code' setting and parse if code is available" do Puppet.settings[:code] = "my code" @parser.expects(:string=).with "my code" @parser.expects(:parse) env.instance_eval { perform_initial_import } end it "should set the parser's file to the 'manifest' setting and parse if no code is available and the manifest is available" do filename = tmpfile('myfile') File.open(filename, 'w'){|f| } Puppet.settings[:manifest] = filename @parser.expects(:file=).with filename @parser.expects(:parse) env.instance_eval { perform_initial_import } end it "should pass the manifest file to the parser even if it does not exist on disk" do filename = tmpfile('myfile') Puppet.settings[:code] = "" Puppet.settings[:manifest] = filename @parser.expects(:file=).with(filename).once @parser.expects(:parse).once env.instance_eval { perform_initial_import } end it "should fail helpfully if there is an error importing" do File.stubs(:exist?).returns true env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(env) @parser.expects(:file=).once @parser.expects(:parse).raises ArgumentError lambda { env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error) end it "should not do anything if the ignore_import settings is set" do Puppet.settings[:ignoreimport] = true @parser.expects(:string=).never @parser.expects(:file=).never @parser.expects(:parse).never env.instance_eval { perform_initial_import } end it "should mark the type collection as needing a reparse when there is an error parsing" do @parser.expects(:parse).raises Puppet::ParseError.new("Syntax error at ...") env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(env) lambda { env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error, /Syntax error at .../) env.known_resource_types.require_reparse?.should be_true end end end