diff --git a/lib/puppet/resource/catalog.rb b/lib/puppet/resource/catalog.rb index fed0f46da..98c29657e 100644 --- a/lib/puppet/resource/catalog.rb +++ b/lib/puppet/resource/catalog.rb @@ -1,591 +1,653 @@ require 'puppet/node' require 'puppet/indirector' require 'puppet/simple_graph' require 'puppet/transaction' require 'puppet/util/cacher' require 'puppet/util/pson' require 'puppet/util/tagging' # This class models a node catalog. It is the thing # meant to be passed from server to client, and it contains all # of the information in the catalog, including the resources # and the relationships between them. class Puppet::Resource::Catalog < Puppet::SimpleGraph class DuplicateResourceError < Puppet::Error; end extend Puppet::Indirector indirects :catalog, :terminus_setting => :catalog_terminus include Puppet::Util::Tagging extend Puppet::Util::Pson include Puppet::Util::Cacher::Expirer # The host name this is a catalog for. attr_accessor :name # The catalog version. Used for testing whether a catalog # is up to date. attr_accessor :version # How long this catalog took to retrieve. Used for reporting stats. attr_accessor :retrieval_duration # Whether this is a host catalog, which behaves very differently. # In particular, reports are sent, graphs are made, and state is # stored in the state database. If this is set incorrectly, then you often # end up in infinite loops, because catalogs are used to make things # that the host catalog needs. attr_accessor :host_config # Whether this catalog was retrieved from the cache, which affects # whether it is written back out again. attr_accessor :from_cache # Some metadata to help us compile and generally respond to the current state. attr_accessor :client_version, :server_version # Add classes to our class list. def add_class(*classes) classes.each do |klass| @classes << klass end # Add the class names as tags, too. tag(*classes) end def title_key_for_ref( ref ) ref =~ /^([-\w:]+)\[(.*)\]$/m [$1, $2] end # Add a resource to our graph and to our resource table. # This is actually a relatively complicated method, because it handles multiple # aspects of Catalog behaviour: # * Add the resource to the resource table # * Add the resource to the resource graph # * Add the resource to the relationship graph # * Add any aliases that make sense for the resource (e.g., name != title) def add_resource(*resource) add_resource(*resource[0..-2]) if resource.length > 1 resource = resource.pop raise ArgumentError, "Can only add objects that respond to :ref, not instances of #{resource.class}" unless resource.respond_to?(:ref) fail_on_duplicate_type_and_title(resource) title_key = title_key_for_ref(resource.ref) @transient_resources << resource if applying? @resource_table[title_key] = resource # If the name and title differ, set up an alias if resource.respond_to?(:name) and resource.respond_to?(:title) and resource.respond_to?(:isomorphic?) and resource.name != resource.title self.alias(resource, resource.uniqueness_key) if resource.isomorphic? end resource.catalog = self if resource.respond_to?(:catalog=) add_vertex(resource) @relationship_graph.add_vertex(resource) if @relationship_graph end # Create an alias for a resource. def alias(resource, key) resource.ref =~ /^(.+)\[/ class_name = $1 || resource.class.name newref = [class_name, key] if key.is_a? String ref_string = "#{class_name}[#{key}]" return if ref_string == resource.ref end # LAK:NOTE It's important that we directly compare the references, # because sometimes an alias is created before the resource is # added to the catalog, so comparing inside the below if block # isn't sufficient. if existing = @resource_table[newref] return if existing == resource raise(ArgumentError, "Cannot alias #{resource.ref} to #{key.inspect}; resource #{newref.inspect} already exists") end @resource_table[newref] = resource @aliases[resource.ref] ||= [] @aliases[resource.ref] << newref end # Apply our catalog to the local host. Valid options # are: # :tags - set the tags that restrict what resources run # during the transaction # :ignoreschedules - tell the transaction to ignore schedules # when determining the resources to run def apply(options = {}) @applying = true # Expire all of the resource data -- this ensures that all # data we're operating against is entirely current. expire Puppet::Util::Storage.load if host_config? transaction = Puppet::Transaction.new(self) transaction.report = options[:report] if options[:report] transaction.tags = options[:tags] if options[:tags] transaction.ignoreschedules = true if options[:ignoreschedules] transaction.add_times :config_retrieval => self.retrieval_duration || 0 begin transaction.evaluate rescue Puppet::Error => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not apply complete catalog: #{detail}" rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Got an uncaught exception of type #{detail.class}: #{detail}" ensure # Don't try to store state unless we're a host config # too recursive. Puppet::Util::Storage.store if host_config? end yield transaction if block_given? return transaction ensure @applying = false cleanup end # Are we in the middle of applying the catalog? def applying? @applying end def clear(remove_resources = true) super() # We have to do this so that the resources clean themselves up. @resource_table.values.each { |resource| resource.remove } if remove_resources @resource_table.clear if @relationship_graph @relationship_graph.clear @relationship_graph = nil end end def classes @classes.dup end # Create a new resource and register it in the catalog. def create_resource(type, options) unless klass = Puppet::Type.type(type) raise ArgumentError, "Unknown resource type #{type}" end return unless resource = klass.new(options) add_resource(resource) resource end def dependent_data_expired?(ts) if applying? return super else return true end end # Turn our catalog graph into an old-style tree of TransObjects and TransBuckets. # LAK:NOTE(20081211): This is a pre-0.25 backward compatibility method. # It can be removed as soon as xmlrpc is killed. def extract top = nil current = nil buckets = {} unless main = resource(:stage, "main") raise Puppet::DevError, "Could not find 'main' stage; cannot generate catalog" end if stages = vertices.find_all { |v| v.type == "Stage" and v.title != "main" } and ! stages.empty? Puppet.warning "Stages are not supported by 0.24.x client; stage(s) #{stages.collect { |s| s.to_s }.join(', ') } will be ignored" end bucket = nil walk(main, :out) do |source, target| # The sources are always non-builtins. unless tmp = buckets[source.to_s] if tmp = buckets[source.to_s] = source.to_trans bucket = tmp else # This is because virtual resources return nil. If a virtual # container resource contains realized resources, we still need to get # to them. So, we keep a reference to the last valid bucket # we returned and use that if the container resource is virtual. end end bucket = tmp || bucket if child = target.to_trans raise "No bucket created for #{source}" unless bucket bucket.push child # It's important that we keep a reference to any TransBuckets we've created, so # we don't create multiple buckets for children. buckets[target.to_s] = child unless target.builtin? end end # Retrieve the bucket for the top-level scope and set the appropriate metadata. unless result = buckets[main.to_s] # This only happens when the catalog is entirely empty. result = buckets[main.to_s] = main.to_trans end result.classes = classes # Clear the cache to encourage the GC buckets.clear result end # Make sure all of our resources are "finished". def finalize make_default_resources @resource_table.values.each { |resource| resource.finish } write_graph(:resources) end def host_config? host_config end def initialize(name = nil) super() @name = name if name @classes = [] @resource_table = {} @transient_resources = [] @applying = false @relationship_graph = nil @host_config = true @aliases = {} if block_given? yield(self) finalize end end # Make the default objects necessary for function. def make_default_resources # We have to add the resources to the catalog, or else they won't get cleaned up after # the transaction. # First create the default scheduling objects Puppet::Type.type(:schedule).mkdefaultschedules.each { |res| add_resource(res) unless resource(res.ref) } # And filebuckets if bucket = Puppet::Type.type(:filebucket).mkdefaultbucket add_resource(bucket) unless resource(bucket.ref) end end # Create a graph of all of the relationships in our catalog. def relationship_graph unless @relationship_graph # It's important that we assign the graph immediately, because # the debug messages below use the relationships in the # relationship graph to determine the path to the resources # spitting out the messages. If this is not set, # then we get into an infinite loop. @relationship_graph = Puppet::SimpleGraph.new # First create the dependency graph self.vertices.each do |vertex| @relationship_graph.add_vertex vertex vertex.builddepends.each do |edge| @relationship_graph.add_edge(edge) end end # Lastly, add in any autorequires @relationship_graph.vertices.each do |vertex| vertex.autorequire(self).each do |edge| unless @relationship_graph.edge?(edge.source, edge.target) # don't let automatic relationships conflict with manual ones. unless @relationship_graph.edge?(edge.target, edge.source) vertex.debug "Autorequiring #{edge.source}" @relationship_graph.add_edge(edge) else vertex.debug "Skipping automatic relationship with #{(edge.source == vertex ? edge.target : edge.source)}" end end end end @relationship_graph.write_graph(:relationships) if host_config? # Then splice in the container information - @relationship_graph.splice!(self, Puppet::Type::Component) + splice!(@relationship_graph) @relationship_graph.write_graph(:expanded_relationships) if host_config? end @relationship_graph end + # Impose our container information on another graph by using it + # to replace any container vertices X with a pair of verticies + # { admissible_X and completed_X } such that that + # + # 0) completed_X depends on admissible_X + # 1) contents of X each depend on admissible_X + # 2) completed_X depends on each on the contents of X + # 3) everything which depended on X depens on completed_X + # 4) admissible_X depends on everything X depended on + # 5) the containers and their edges must be removed + # + # Note that this requires attention to the possible case of containers + # which contain or depend on other containers, but has the advantage + # that the number of new edges created scales linearly with the number + # of contained verticies regardless of how containers are related; + # alternatives such as replacing container-edges with content-edges + # scale as the product of the number of external dependences, which is + # to say geometrically in the case of nested / chained containers. + # + Default_label = { :callback => :refresh, :event => :ALL_EVENTS } + def splice!(other) + stage_class = Puppet::Type.type(:stage) + whit_class = Puppet::Type.type(:whit) + component_class = Puppet::Type.type(:component) + containers = vertices.find_all { |v| (v.is_a?(component_class) or v.is_a?(stage_class)) and vertex?(v) } + # + # These two hashes comprise the aforementioned attention to the possible + # case of containers that contain / depend on other containers; they map + # containers to their sentinals but pass other verticies through. Thus we + # can "do the right thing" for references to other verticies that may or + # may not be containers. + # + admissible = Hash.new { |h,k| k } + completed = Hash.new { |h,k| k } + containers.each { |x| + admissible[x] = whit_class.new(:name => "admissible_#{x.name}", :catalog => self) + completed[x] = whit_class.new(:name => "completed_#{x.name}", :catalog => self) + } + # + # Implement the six requierments listed above + # + containers.each { |x| + contents = adjacent(x, :direction => :out) + other.add_edge(admissible[x],completed[x]) if contents.empty? # (0) + contents.each { |v| + other.add_edge(admissible[x],admissible[v],Default_label) # (1) + other.add_edge(completed[v], completed[x], Default_label) # (2) + } + # (3) & (5) + other.adjacent(x,:direction => :in,:type => :edges).each { |e| + other.add_edge(completed[e.source],admissible[x],e.label) + other.remove_edge! e + } + # (4) & (5) + other.adjacent(x,:direction => :out,:type => :edges).each { |e| + other.add_edge(completed[x],admissible[e.target],e.label) + other.remove_edge! e + } + } + containers.each { |x| other.remove_vertex! x } # (5) + end + # Remove the resource from our catalog. Notice that we also call # 'remove' on the resource, at least until resource classes no longer maintain # references to the resource instances. def remove_resource(*resources) resources.each do |resource| @resource_table.delete(resource.ref) if aliases = @aliases[resource.ref] aliases.each { |res_alias| @resource_table.delete(res_alias) } @aliases.delete(resource.ref) end remove_vertex!(resource) if vertex?(resource) @relationship_graph.remove_vertex!(resource) if @relationship_graph and @relationship_graph.vertex?(resource) resource.remove end end # Look a resource up by its reference (e.g., File[/etc/passwd]). def resource(type, title = nil) # Always create a resource reference, so that it always canonizes how we # are referring to them. if title res = Puppet::Resource.new(type, title) else # If they didn't provide a title, then we expect the first # argument to be of the form 'Class[name]', which our # Reference class canonizes for us. res = Puppet::Resource.new(nil, type) end title_key = [res.type, res.title.to_s] uniqueness_key = [res.type, res.uniqueness_key] @resource_table[title_key] || @resource_table[uniqueness_key] end def resource_refs resource_keys.collect{ |type, name| name.is_a?( String ) ? "#{type}[#{name}]" : nil}.compact end def resource_keys @resource_table.keys end def resources @resource_table.values.uniq end def self.from_pson(data) result = new(data['name']) if tags = data['tags'] result.tag(*tags) end if version = data['version'] result.version = version end if resources = data['resources'] resources = PSON.parse(resources) if resources.is_a?(String) resources.each do |res| resource_from_pson(result, res) end end if edges = data['edges'] edges = PSON.parse(edges) if edges.is_a?(String) edges.each do |edge| edge_from_pson(result, edge) end end if classes = data['classes'] result.add_class(*classes) end result end def self.edge_from_pson(result, edge) # If no type information was presented, we manually find # the class. edge = Puppet::Relationship.from_pson(edge) if edge.is_a?(Hash) unless source = result.resource(edge.source) raise ArgumentError, "Could not convert from pson: Could not find relationship source #{edge.source.inspect}" end edge.source = source unless target = result.resource(edge.target) raise ArgumentError, "Could not convert from pson: Could not find relationship target #{edge.target.inspect}" end edge.target = target result.add_edge(edge) end def self.resource_from_pson(result, res) res = Puppet::Resource.from_pson(res) if res.is_a? Hash result.add_resource(res) end PSON.register_document_type('Catalog',self) def to_pson_data_hash { 'document_type' => 'Catalog', 'data' => { 'tags' => tags, 'name' => name, 'version' => version, 'resources' => vertices.collect { |v| v.to_pson_data_hash }, 'edges' => edges. collect { |e| e.to_pson_data_hash }, 'classes' => classes }, 'metadata' => { 'api_version' => 1 } } end def to_pson(*args) to_pson_data_hash.to_pson(*args) end # Convert our catalog into a RAL catalog. def to_ral to_catalog :to_ral end # Convert our catalog into a catalog of Puppet::Resource instances. def to_resource to_catalog :to_resource end # filter out the catalog, applying +block+ to each resource. # If the block result is false, the resource will # be kept otherwise it will be skipped def filter(&block) to_catalog :to_resource, &block end # Store the classes in the classfile. def write_class_file ::File.open(Puppet[:classfile], "w") do |f| f.puts classes.join("\n") end rescue => detail Puppet.err "Could not create class file #{Puppet[:classfile]}: #{detail}" end # Produce the graph files if requested. def write_graph(name) # We only want to graph the main host catalog. return unless host_config? super end private def cleanup # Expire any cached data the resources are keeping. expire end # Verify that the given resource isn't defined elsewhere. def fail_on_duplicate_type_and_title(resource) # Short-curcuit the common case, return unless existing_resource = @resource_table[title_key_for_ref(resource.ref)] # If we've gotten this far, it's a real conflict msg = "Duplicate definition: #{resource.ref} is already defined" msg << " in file #{existing_resource.file} at line #{existing_resource.line}" if existing_resource.file and existing_resource.line msg << "; cannot redefine" if resource.line or resource.file raise DuplicateResourceError.new(msg) end # An abstracted method for converting one catalog into another type of catalog. # This pretty much just converts all of the resources from one class to another, using # a conversion method. def to_catalog(convert) result = self.class.new(self.name) result.version = self.version map = {} vertices.each do |resource| next if virtual_not_exported?(resource) next if block_given? and yield resource #This is hackity hack for 1094 #Aliases aren't working in the ral catalog because the current instance of the resource #has a reference to the catalog being converted. . . So, give it a reference to the new one #problem solved. . . if resource.class == Puppet::Resource resource = resource.dup resource.catalog = result elsif resource.is_a?(Puppet::TransObject) resource = resource.dup resource.catalog = result elsif resource.is_a?(Puppet::Parser::Resource) resource = resource.to_resource resource.catalog = result end if resource.is_a?(Puppet::Resource) and convert.to_s == "to_resource" newres = resource else newres = resource.send(convert) end # We can't guarantee that resources don't munge their names # (like files do with trailing slashes), so we have to keep track # of what a resource got converted to. map[resource.ref] = newres result.add_resource newres end message = convert.to_s.gsub "_", " " edges.each do |edge| # Skip edges between virtual resources. next if virtual_not_exported?(edge.source) next if block_given? and yield edge.source next if virtual_not_exported?(edge.target) next if block_given? and yield edge.target unless source = map[edge.source.ref] raise Puppet::DevError, "Could not find resource #{edge.source.ref} when converting #{message} resources" end unless target = map[edge.target.ref] raise Puppet::DevError, "Could not find resource #{edge.target.ref} when converting #{message} resources" end result.add_edge(source, target, edge.label) end map.clear result.add_class(*self.classes) result.tag(*self.tags) result end def virtual_not_exported?(resource) resource.respond_to?(:virtual?) and resource.virtual? and (resource.respond_to?(:exported?) and not resource.exported?) end end diff --git a/lib/puppet/simple_graph.rb b/lib/puppet/simple_graph.rb index 31858f136..671eef150 100644 --- a/lib/puppet/simple_graph.rb +++ b/lib/puppet/simple_graph.rb @@ -1,602 +1,553 @@ require 'puppet/external/dot' require 'puppet/relationship' require 'set' # A hopefully-faster graph class to replace the use of GRATR. class Puppet::SimpleGraph # # All public methods of this class must maintain (assume ^ ensure) the following invariants, where "=~=" means # equiv. up to order: # # @in_to.keys =~= @out_to.keys =~= all vertices # @in_to.values.collect { |x| x.values }.flatten =~= @out_from.values.collect { |x| x.values }.flatten =~= all edges # @in_to[v1][v2] =~= @out_from[v2][v1] =~= all edges from v1 to v2 # @in_to [v].keys =~= vertices with edges leading to v # @out_from[v].keys =~= vertices with edges leading from v # no operation may shed reference loops (for gc) # recursive operation must scale with the depth of the spanning trees, or better (e.g. no recursion over the set # of all vertices, etc.) # # This class is intended to be used with DAGs. However, if the # graph has a cycle, it will not cause non-termination of any of the - # algorithms. The topsort method detects and reports cycles. + # algorithms. # def initialize @in_to = {} @out_from = {} @upstream_from = {} @downstream_from = {} end # Clear our graph. def clear @in_to.clear @out_from.clear @upstream_from.clear @downstream_from.clear end # Which resources depend upon the given resource. def dependencies(resource) vertex?(resource) ? upstream_from_vertex(resource).keys : [] end def dependents(resource) vertex?(resource) ? downstream_from_vertex(resource).keys : [] end # Whether our graph is directed. Always true. Used to produce dot files. def directed? true end # Determine all of the leaf nodes below a given vertex. def leaves(vertex, direction = :out) tree_from_vertex(vertex, direction).keys.find_all { |c| adjacent(c, :direction => direction).empty? } end # Collect all of the edges that the passed events match. Returns # an array of edges. def matching_edges(event, base = nil) source = base || event.resource unless vertex?(source) Puppet.warning "Got an event from invalid vertex #{source.ref}" return [] end # Get all of the edges that this vertex should forward events # to, which is the same thing as saying all edges directly below # This vertex in the graph. @out_from[source].values.flatten.find_all { |edge| edge.match?(event.name) } end # Return a reversed version of this graph. def reversal result = self.class.new vertices.each { |vertex| result.add_vertex(vertex) } edges.each do |edge| result.add_edge edge.class.new(edge.target, edge.source, edge.label) end result end # Return the size of the graph. def size vertices.size end def to_a vertices end # This is a simple implementation of Tarjan's algorithm to find strongly # connected components in the graph; this is a fairly ugly implementation, # because I can't just decorate the vertices themselves. # # This method has an unhealthy relationship with the find_cycles_in_graph # method below, which contains the knowledge of how the state object is # maintained. def tarjan(root, s) # initialize the recursion stack we use to work around the nasty lack of a # decent Ruby stack. recur = [{ :node => root }] while not recur.empty? do frame = recur.last vertex = frame[:node] case frame[:step] when nil then s[:index][vertex] = s[:number] s[:lowlink][vertex] = s[:number] s[:number] = s[:number] + 1 s[:stack].push(vertex) s[:seen][vertex] = true frame[:children] = adjacent(vertex) frame[:step] = :children when :children then if frame[:children].length > 0 then child = frame[:children].shift if ! s[:index][child] then # Never seen, need to recurse. frame[:step] = :after_recursion frame[:child] = child recur.push({ :node => child }) elsif s[:seen][child] then s[:lowlink][vertex] = [s[:lowlink][vertex], s[:index][child]].min end else if s[:lowlink][vertex] == s[:index][vertex] then this_scc = [] begin top = s[:stack].pop s[:seen][top] = false this_scc << top end until top == vertex # NOTE: if we don't reverse we get the components in the opposite # order to what a human being would expect; reverse should be an # O(1) operation, without even copying, because we know the length # of the source, but I worry that an implementation will get this # wrong. Still, the worst case is O(n) for n vertices as we can't # possibly put a vertex into two SCCs. # # Also, my feeling is that most implementations are going to do # better with a reverse operation than a string of 'unshift' # insertions at the head of the array; if they were going to mess # up the performance of one, it would be unshift. s[:scc] << this_scc.reverse end recur.pop # done with this node, finally. end when :after_recursion then s[:lowlink][vertex] = [s[:lowlink][vertex], s[:lowlink][frame[:child]]].min frame[:step] = :children else fail "#{frame[:step]} is an unknown step" end end end # Find all cycles in the graph by detecting all the strongly connected # components, then eliminating everything with a size of one as # uninteresting - which it is, because it can't be a cycle. :) # # This has an unhealthy relationship with the 'tarjan' method above, which # it uses to implement the detection of strongly connected components. def find_cycles_in_graph state = { :number => 0, :index => {}, :lowlink => {}, :scc => [], :stack => [], :seen => {} } # we usually have a disconnected graph, must walk all possible roots vertices.each do |vertex| if ! state[:index][vertex] then tarjan vertex, state end end state[:scc].select { |c| c.length > 1 } end # Perform a BFS on the sub graph representing the cycle, with a view to # generating a sufficient set of paths to report the cycle meaningfully, and # ideally usefully, for the end user. # # BFS is preferred because it will generally report the shortest paths # through the graph first, which are more likely to be interesting to the # user. I think; it would be interesting to verify that. --daniel 2011-01-23 def paths_in_cycle(cycle, max_paths = 1) raise ArgumentError, "negative or zero max_paths" if max_paths < 1 # Calculate our filtered outbound vertex lists... adj = {} cycle.each do |vertex| adj[vertex] = adjacent(vertex).select{|s| cycle.member? s} end found = [] # frame struct is vertex, [path] stack = [[cycle.first, []]] while frame = stack.shift do if frame[1].member?(frame[0]) then found << frame[1] + [frame[0]] break if found.length >= max_paths else adj[frame[0]].each do |to| stack.push [to, frame[1] + [frame[0]]] end end end return found end def report_cycles_in_graph cycles = find_cycles_in_graph n = cycles.length # where is "pluralize"? --daniel 2011-01-22 + return if n == 0 s = n == 1 ? '' : 's' message = "Found #{n} dependency cycle#{s}:\n" cycles.each do |cycle| paths = paths_in_cycle(cycle) message += paths.map{ |path| '(' + path.join(" => ") + ')'}.join("\n") + "\n" end if Puppet[:graph] then filename = write_cycles_to_graph(cycles) message += "Cycle graph written to #{filename}." else message += "Try the '--graph' option and opening the " message += "resulting '.dot' file in OmniGraffle or GraphViz" end raise Puppet::Error, message end def write_cycles_to_graph(cycles) # This does not use the DOT graph library, just writes the content # directly. Given the complexity of this, there didn't seem much point # using a heavy library to generate exactly the same content. --daniel 2011-01-27 Puppet.settings.use(:graphing) graph = ["digraph Resource_Cycles {"] graph << ' label = "Resource Cycles"' cycles.each do |cycle| paths_in_cycle(cycle, 10).each do |path| graph << path.map { |v| '"' + v.to_s.gsub(/"/, '\\"') + '"' }.join(" -> ") end end graph << '}' filename = File.join(Puppet[:graphdir], "cycles.dot") File.open(filename, "w") { |f| f.puts graph } return filename end - # Provide a topological sort. - def topsort - degree = {} - zeros = [] - result = [] - - # Collect each of our vertices, with the number of in-edges each has. - vertices.each do |v| - edges = @in_to[v] - zeros << v if edges.empty? - degree[v] = edges.length - end - - # Iterate over each 0-degree vertex, decrementing the degree of - # each of its out-edges. - while v = zeros.pop - result << v - @out_from[v].each { |v2,es| - zeros << v2 if (degree[v2] -= 1) == 0 - } - end - - # If we have any vertices left with non-zero in-degrees, then we've found a cycle. - if cycles = degree.values.reject { |ns| ns == 0 } and cycles.length > 0 - report_cycles_in_graph - end - - result - end - # Add a new vertex to the graph. def add_vertex(vertex) @in_to[vertex] ||= {} @out_from[vertex] ||= {} end # Remove a vertex from the graph. def remove_vertex!(v) return unless vertex?(v) @upstream_from.clear @downstream_from.clear (@in_to[v].values+@out_from[v].values).flatten.each { |e| remove_edge!(e) } @in_to.delete(v) @out_from.delete(v) end # Test whether a given vertex is in the graph. def vertex?(v) @in_to.include?(v) end # Return a list of all vertices. def vertices @in_to.keys end # Add a new edge. The graph user has to create the edge instance, # since they have to specify what kind of edge it is. def add_edge(e,*a) return add_relationship(e,*a) unless a.empty? @upstream_from.clear @downstream_from.clear add_vertex(e.source) add_vertex(e.target) @in_to[ e.target][e.source] ||= []; @in_to[ e.target][e.source] |= [e] @out_from[e.source][e.target] ||= []; @out_from[e.source][e.target] |= [e] end def add_relationship(source, target, label = nil) add_edge Puppet::Relationship.new(source, target, label) end # Find all matching edges. def edges_between(source, target) (@out_from[source] || {})[target] || [] end # Is there an edge between the two vertices? def edge?(source, target) vertex?(source) and vertex?(target) and @out_from[source][target] end def edges @in_to.values.collect { |x| x.values }.flatten end def each_edge @in_to.each { |t,ns| ns.each { |s,es| es.each { |e| yield e }}} end # Remove an edge from our graph. def remove_edge!(e) if edge?(e.source,e.target) @upstream_from.clear @downstream_from.clear @in_to [e.target].delete e.source if (@in_to [e.target][e.source] -= [e]).empty? @out_from[e.source].delete e.target if (@out_from[e.source][e.target] -= [e]).empty? end end # Find adjacent edges. def adjacent(v, options = {}) return [] unless ns = (options[:direction] == :in) ? @in_to[v] : @out_from[v] (options[:type] == :edges) ? ns.values.flatten : ns.keys end - # Take container information from another graph and use it - # to replace any container vertices with their respective leaves. - # This creates direct relationships where there were previously - # indirect relationships through the containers. - def splice!(other, type) - # We have to get the container list via a topological sort on the - # configuration graph, because otherwise containers that contain - # other containers will add those containers back into the - # graph. We could get a similar affect by only setting relationships - # to container leaves, but that would result in many more - # relationships. - stage_class = Puppet::Type.type(:stage) - whit_class = Puppet::Type.type(:whit) - containers = other.topsort.find_all { |v| (v.is_a?(type) or v.is_a?(stage_class)) and vertex?(v) } - containers.each do |container| - # Get the list of children from the other graph. - children = other.adjacent(container, :direction => :out) - - # MQR TODO: Luke suggests that it should be possible to refactor the system so that - # container nodes are retained, thus obviating the need for the whit. - children = [whit_class.new(:name => container.name, :catalog => other)] if children.empty? - - # First create new edges for each of the :in edges - [:in, :out].each do |dir| - edges = adjacent(container, :direction => dir, :type => :edges) - edges.each do |edge| - children.each do |child| - if dir == :in - s = edge.source - t = child - else - s = child - t = edge.target - end - - add_edge(s, t, edge.label) - end - - # Now get rid of the edge, so remove_vertex! works correctly. - remove_edge!(edge) - end - end - remove_vertex!(container) - end - end - # Just walk the tree and pass each edge. def walk(source, direction) # Use an iterative, breadth-first traversal of the graph. One could do # this recursively, but Ruby's slow function calls and even slower # recursion make the shorter, recursive algorithm cost-prohibitive. stack = [source] seen = Set.new until stack.empty? node = stack.shift next if seen.member? node connected = adjacent(node, :direction => direction) connected.each do |target| yield node, target end stack.concat(connected) seen << node end end # A different way of walking a tree, and a much faster way than the # one that comes with GRATR. def tree_from_vertex(start, direction = :out) predecessor={} walk(start, direction) do |parent, child| predecessor[child] = parent end predecessor end def downstream_from_vertex(v) return @downstream_from[v] if @downstream_from[v] result = @downstream_from[v] = {} @out_from[v].keys.each do |node| result[node] = 1 result.update(downstream_from_vertex(node)) end result end def direct_dependents_of(v) - @out_from[v].keys + (@out_from[v] || {}).keys end def upstream_from_vertex(v) return @upstream_from[v] if @upstream_from[v] result = @upstream_from[v] = {} @in_to[v].keys.each do |node| result[node] = 1 result.update(upstream_from_vertex(node)) end result end def direct_dependencies_of(v) - @in_to[v].keys + (@in_to[v] || {}).keys + end + + # Return an array of the edge-sets between a series of n+1 vertices (f=v0,v1,v2...t=vn) + # connecting the two given verticies. The ith edge set is an array containing all the + # edges between v(i) and v(i+1); these are (by definition) never empty. + # + # * if f == t, the list is empty + # * if they are adjacent the result is an array consisting of + # a single array (the edges from f to t) + # * and so on by induction on a vertex m between them + # * if there is no path from f to t, the result is nil + # + # This implementation is not particularly efficient; it's used in testing where clarity + # is more important than last-mile efficiency. + # + def path_between(f,t) + if f==t + [] + elsif direct_dependents_of(f).include?(t) + [edges_between(f,t)] + elsif dependents(f).include?(t) + m = (dependents(f) & direct_dependencies_of(t)).first + path_between(f,m) + path_between(m,t) + else + nil + end end # LAK:FIXME This is just a paste of the GRATR code with slight modifications. # Return a DOT::DOTDigraph for directed graphs or a DOT::DOTSubgraph for an # undirected Graph. _params_ can contain any graph property specified in # rdot.rb. If an edge or vertex label is a kind of Hash then the keys # which match +dot+ properties will be used as well. def to_dot_graph (params = {}) params['name'] ||= self.class.name.gsub(/:/,'_') fontsize = params['fontsize'] ? params['fontsize'] : '8' graph = (directed? ? DOT::DOTDigraph : DOT::DOTSubgraph).new(params) edge_klass = directed? ? DOT::DOTDirectedEdge : DOT::DOTEdge vertices.each do |v| name = v.to_s params = {'name' => '"'+name+'"', 'fontsize' => fontsize, 'label' => name} v_label = v.to_s params.merge!(v_label) if v_label and v_label.kind_of? Hash graph << DOT::DOTNode.new(params) end edges.each do |e| params = {'from' => '"'+ e.source.to_s + '"', 'to' => '"'+ e.target.to_s + '"', 'fontsize' => fontsize } e_label = e.to_s params.merge!(e_label) if e_label and e_label.kind_of? Hash graph << edge_klass.new(params) end graph end # Output the dot format as a string def to_dot (params={}) to_dot_graph(params).to_s; end # Call +dotty+ for the graph which is written to the file 'graph.dot' # in the # current directory. def dotty (params = {}, dotfile = 'graph.dot') File.open(dotfile, 'w') {|f| f << to_dot(params) } system('dotty', dotfile) end # Produce the graph files if requested. def write_graph(name) return unless Puppet[:graph] Puppet.settings.use(:graphing) file = File.join(Puppet[:graphdir], "#{name}.dot") File.open(file, "w") { |f| f.puts to_dot("name" => name.to_s.capitalize) } end # This flag may be set to true to use the new YAML serialzation # format (where @vertices is a simple list of vertices rather than a # list of VertexWrapper objects). Deserialization supports both # formats regardless of the setting of this flag. class << self attr_accessor :use_new_yaml_format end self.use_new_yaml_format = false # Stub class to allow graphs to be represented in YAML using the old # (version 2.6) format. class VertexWrapper attr_reader :vertex, :adjacencies def initialize(vertex, adjacencies) @vertex = vertex @adjacencies = adjacencies end def inspect { :@adjacencies => @adjacencies, :@vertex => @vertex.to_s }.inspect end end # instance_variable_get is used by Object.to_zaml to get instance # variables. Override it so that we can simulate the presence of # instance variables @edges and @vertices for serialization. def instance_variable_get(v) case v.to_s when '@edges' then edges when '@vertices' then if self.class.use_new_yaml_format vertices else result = {} vertices.each do |vertex| adjacencies = {} [:in, :out].each do |direction| adjacencies[direction] = {} adjacent(vertex, :direction => direction, :type => :edges).each do |edge| other_vertex = direction == :in ? edge.source : edge.target (adjacencies[direction][other_vertex] ||= Set.new).add(edge) end end result[vertex] = Puppet::SimpleGraph::VertexWrapper.new(vertex, adjacencies) end result end else super(v) end end def to_yaml_properties other_vars = instance_variables. map {|v| v.to_s}. reject { |v| %w{@in_to @out_from @upstream_from @downstream_from}.include?(v) } (other_vars + %w{@vertices @edges}).sort.uniq end def yaml_initialize(tag, var) initialize() vertices = var.delete('vertices') edges = var.delete('edges') if vertices.is_a?(Hash) # Support old (2.6) format vertices = vertices.keys end vertices.each { |v| add_vertex(v) } edges.each { |e| add_edge(e) } var.each do |varname, value| instance_variable_set("@#{varname}", value) end end end diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index c502fc627..8118178c3 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -1,371 +1,372 @@ # the class that actually walks our resource/property tree, collects the changes, # and performs them require 'puppet' require 'puppet/util/tagging' require 'puppet/application' require 'sha1' class Puppet::Transaction require 'puppet/transaction/event' require 'puppet/transaction/event_manager' require 'puppet/transaction/resource_harness' require 'puppet/resource/status' attr_accessor :component, :catalog, :ignoreschedules attr_accessor :configurator # The report, once generated. attr_accessor :report # Routes and stores any events and subscriptions. attr_reader :event_manager # Handles most of the actual interacting with resources attr_reader :resource_harness include Puppet::Util include Puppet::Util::Tagging # Wraps application run state check to flag need to interrupt processing def stop_processing? Puppet::Application.stop_requested? end # Add some additional times for reporting def add_times(hash) hash.each do |name, num| report.add_times(name, num) end end # Are there any failed resources in this transaction? def any_failed? report.resource_statuses.values.detect { |status| status.failed? } end # Apply all changes for a resource def apply(resource, ancestor = nil) status = resource_harness.evaluate(resource) add_resource_status(status) event_manager.queue_events(ancestor || resource, status.events) rescue => detail resource.err "Could not evaluate: #{detail}" end # Find all of the changed resources. def changed? report.resource_statuses.values.find_all { |status| status.changed }.collect { |status| catalog.resource(status.resource) } end # Find all of the applied resources (including failed attempts). def applied_resources report.resource_statuses.values.collect { |status| catalog.resource(status.resource) } end # Copy an important relationships from the parent to the newly-generated # child resource. - def make_parent_child_relationship(parent, child) + def add_conditional_directed_dependency(parent, child, label=nil) relationship_graph.add_vertex(child) edge = parent.depthfirst? ? [child, parent] : [parent, child] if relationship_graph.edge?(*edge.reverse) parent.debug "Skipping automatic relationship to #{child}" else - relationship_graph.add_edge(*edge) + relationship_graph.add_edge(edge[0],edge[1],label) end end # Evaluate a single resource. def eval_resource(resource, ancestor = nil) if skip?(resource) resource_status(resource).skipped = true else resource_status(resource).scheduled = true apply(resource, ancestor) end # Check to see if there are any events queued for this resource event_manager.process_events(resource) end # This method does all the actual work of running a transaction. It # collects all of the changes, executes them, and responds to any # necessary events. def evaluate # Start logging. Puppet::Util::Log.newdestination(@report) prepare Puppet.info "Applying configuration version '#{catalog.version}'" if catalog.version begin relationship_graph.traverse do |resource| if resource.is_a?(Puppet::Type::Component) Puppet.warning "Somehow left a component in the relationship graph" else seconds = thinmark { eval_resource(resource) } resource.info "Evaluated in %0.2f seconds" % seconds if Puppet[:evaltrace] and @catalog.host_config? end end ensure # And then close the transaction log. Puppet::Util::Log.close(@report) end Puppet.debug "Finishing transaction #{object_id}" end def events event_manager.events end def failed?(resource) s = resource_status(resource) and s.failed? end # Does this resource have any failed dependencies? def failed_dependencies?(resource) # First make sure there are no failed dependencies. To do this, # we check for failures in any of the vertexes above us. It's not # enough to check the immediate dependencies, which is why we use # a tree from the reversed graph. found_failed = false relationship_graph.dependencies(resource).each do |dep| next unless failed?(dep) resource.notice "Dependency #{dep} has failures: #{resource_status(dep).failed}" found_failed = true end found_failed end def eval_generate(resource) - return [] unless resource.respond_to?(:eval_generate) + raise Puppet::DevError,"Depthfirst resources are not supported by eval_generate" if resource.depthfirst? begin - made = resource.eval_generate + made = resource.eval_generate.uniq.reverse rescue => detail puts detail.backtrace if Puppet[:trace] resource.err "Failed to generate additional resources using 'eval_generate: #{detail}" + return end - parents = [resource] - [made].flatten.compact.uniq.each do |res| + made.each do |res| begin res.tag(*resource.tags) @catalog.add_resource(res) res.finish - make_parent_child_relationship(parents.reverse.find { |r| r.name == res.name[0,r.name.length]}, res) - parents << res rescue Puppet::Resource::Catalog::DuplicateResourceError res.info "Duplicate generated resource; skipping" end end + sentinal = Puppet::Type::Whit.new(:name => "completed_#{resource.title}", :catalog => resource.catalog) + relationship_graph.adjacent(resource,:direction => :out,:type => :edges).each { |e| + add_conditional_directed_dependency(sentinal, e.target, e.label) + relationship_graph.remove_edge! e + } + default_label = Puppet::Resource::Catalog::Default_label + made.each do |res| + add_conditional_directed_dependency(made.find { |r| r != res && r.name == res.name[0,r.name.length]} || resource, res) + add_conditional_directed_dependency(res, sentinal, default_label) + end + add_conditional_directed_dependency(resource, sentinal, default_label) end # A general method for recursively generating new resources from a # resource. def generate_additional_resources(resource) - return [] unless resource.respond_to?(:generate) + return unless resource.respond_to?(:generate) begin made = resource.generate rescue => detail puts detail.backtrace if Puppet[:trace] resource.err "Failed to generate additional resources using 'generate': #{detail}" end - return [] unless made + return unless made made = [made] unless made.is_a?(Array) - made.uniq.find_all do |res| + made.uniq.each do |res| begin res.tag(*resource.tags) @catalog.add_resource(res) res.finish - make_parent_child_relationship(resource, res) + add_conditional_directed_dependency(resource, res) generate_additional_resources(res) - true rescue Puppet::Resource::Catalog::DuplicateResourceError res.info "Duplicate generated resource; skipping" - false end end end # Collect any dynamically generated resources. This method is called # before the transaction starts. - def generate - list = @catalog.vertices - newlist = [] - while ! list.empty? - list.each do |resource| - newlist += generate_additional_resources(resource) - end - list = newlist - newlist = [] - end + def xgenerate + @catalog.vertices.each { |resource| generate_additional_resources(resource) } end # Should we ignore tags? def ignore_tags? ! (@catalog.host_config? or Puppet[:name] == "puppet") end # this should only be called by a Puppet::Type::Component resource now # and it should only receive an array def initialize(catalog) @catalog = catalog @report = Puppet::Transaction::Report.new("apply") @event_manager = Puppet::Transaction::EventManager.new(self) @resource_harness = Puppet::Transaction::ResourceHarness.new(self) end # Prefetch any providers that support it. We don't support prefetching # types, just providers. def prefetch prefetchers = {} @catalog.vertices.each do |resource| if provider = resource.provider and provider.class.respond_to?(:prefetch) prefetchers[provider.class] ||= {} prefetchers[provider.class][resource.name] = resource end end # Now call prefetch, passing in the resources so that the provider instances can be replaced. prefetchers.each do |provider, resources| Puppet.debug "Prefetching #{provider.name} resources for #{provider.resource_type.name}" begin provider.prefetch(resources) rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not prefetch #{provider.resource_type.name} provider '#{provider.name}': #{detail}" end end end # Prepare to evaluate the resources in a transaction. def prepare # Now add any dynamically generated resources - generate + xgenerate # Then prefetch. It's important that we generate and then prefetch, # so that any generated resources also get prefetched. prefetch end # We want to monitor changes in the relationship graph of our # catalog but this is complicated by the fact that the catalog # both is_a graph and has_a graph, by the fact that changes to # the structure of the object can have adverse serialization # effects, by threading issues, by order-of-initialization issues, # etc. # # Since the proper lifetime/scope of the monitoring is a transaction # and the transaction is already commiting a mild law-of-demeter # transgression, we cut the Gordian knot here by simply wrapping the # transaction's view of the resource graph to capture and maintain # the information we need. Nothing outside the transaction needs # this information, and nothing outside the transaction can see it # except via the Transaction#relationship_graph class Relationship_graph_wrapper attr_reader :real_graph,:transaction,:ready,:generated,:done,:unguessable_deterministic_key def initialize(real_graph,transaction) @real_graph = real_graph @transaction = transaction @ready = {} @generated = {} @done = {} @unguessable_deterministic_key = Hash.new { |h,k| h[k] = Digest::SHA1.hexdigest("NaCl, MgSO4 (salts) and then #{k.title}") } vertices.each { |v| check_if_now_ready(v) } end def method_missing(*args,&block) real_graph.send(*args,&block) end def add_vertex(v) real_graph.add_vertex(v) + check_if_now_ready(v) # ????????????????????????????????????????? end - def add_edge(f,t) + def add_edge(f,t,label=nil) ready.delete(t) - real_graph.add_edge(f,t) + real_graph.add_edge(f,t,label) end def check_if_now_ready(r) ready[r] = true if direct_dependencies_of(r).all? { |r2| done[r2] } end def next_resource ready.keys.sort_by { |r0| unguessable_deterministic_key[r0] }.first end def traverse(&block) + real_graph.report_cycles_in_graph while (r = next_resource) && !transaction.stop_processing? - if !generated[r] + if !generated[r] && r.respond_to?(:eval_generate) transaction.eval_generate(r) generated[r] = true else ready.delete(r) yield r done[r] = true direct_dependents_of(r).each { |v| check_if_now_ready(v) } end end end end def relationship_graph @relationship_graph ||= Relationship_graph_wrapper.new(catalog.relationship_graph,self) end def add_resource_status(status) report.add_resource_status status end def resource_status(resource) report.resource_statuses[resource.to_s] || add_resource_status(Puppet::Resource::Status.new(resource)) end # Is the resource currently scheduled? def scheduled?(resource) self.ignoreschedules or resource_harness.scheduled?(resource_status(resource), resource) end # Should this resource be skipped? def skip?(resource) if missing_tags?(resource) resource.debug "Not tagged with #{tags.join(", ")}" elsif ! scheduled?(resource) resource.debug "Not scheduled" elsif failed_dependencies?(resource) resource.warning "Skipping because of failed dependencies" elsif resource.virtual? resource.debug "Skipping because virtual" else return false end true end # The tags we should be checking. def tags self.tags = Puppet[:tags] unless defined?(@tags) super end def handle_qualified_tags( qualified ) # The default behavior of Puppet::Util::Tagging is # to split qualified tags into parts. That would cause # qualified tags to match too broadly here. return end # Is this resource tagged appropriately? def missing_tags?(resource) return false if ignore_tags? return false if tags.empty? not resource.tagged?(*tags) end end require 'puppet/transaction/report' diff --git a/lib/puppet/transaction/event_manager.rb b/lib/puppet/transaction/event_manager.rb index 3ebb0a9d3..a21bbf892 100644 --- a/lib/puppet/transaction/event_manager.rb +++ b/lib/puppet/transaction/event_manager.rb @@ -1,99 +1,102 @@ require 'puppet/transaction' class Puppet::Transaction::EventManager attr_reader :transaction, :events def initialize(transaction) @transaction = transaction @event_queues = {} @events = [] end def relationship_graph transaction.relationship_graph end # Respond to any queued events for this resource. def process_events(resource) restarted = false queued_events(resource) do |callback, events| r = process_callback(resource, callback, events) restarted ||= r end if restarted queue_events(resource, [resource.event(:name => :restarted, :status => "success")]) transaction.resource_status(resource).restarted = true end end # Queue events for other resources to respond to. All of these events have # to be from the same resource. def queue_events(resource, events) - @events += events + #@events += events # Do some basic normalization so we're not doing so many # graph queries for large sets of events. events.inject({}) do |collection, event| collection[event.name] ||= [] collection[event.name] << event collection end.collect do |name, list| # It doesn't matter which event we use - they all have the same source # and name here. event = list[0] # Collect the targets of any subscriptions to those events. We pass # the parent resource in so it will override the source in the events, # since eval_generated children can't have direct relationships. + received = (event.name != :restarted) relationship_graph.matching_edges(event, resource).each do |edge| + received ||= true unless edge.target.is_a?(Puppet::Type::Whit) next unless method = edge.callback next unless edge.target.respond_to?(method) queue_events_for_resource(resource, edge.target, method, list) end + @events << event if received queue_events_for_resource(resource, resource, :refresh, [event]) if resource.self_refresh? and ! resource.deleting? end end def queue_events_for_resource(source, target, callback, events) source.info "Scheduling #{callback} of #{target}" @event_queues[target] ||= {} @event_queues[target][callback] ||= [] @event_queues[target][callback] += events end def queued_events(resource) return unless callbacks = @event_queues[resource] callbacks.each do |callback, events| yield callback, events end end private def process_callback(resource, callback, events) process_noop_events(resource, callback, events) and return false unless events.detect { |e| e.status != "noop" } resource.send(callback) resource.notice "Triggered '#{callback}' from #{events.length} events" return true rescue => detail resource.err "Failed to call #{callback}: #{detail}" transaction.resource_status(resource).failed_to_restart = true puts detail.backtrace if Puppet[:trace] return false end def process_noop_events(resource, callback, events) resource.notice "Would have triggered '#{callback}' from #{events.length} events" # And then add an event for it. queue_events(resource, [resource.event(:status => "noop", :name => :noop_restart)]) true # so the 'and if' works end end diff --git a/lib/puppet/type/whit.rb b/lib/puppet/type/whit.rb index 55bfcfb46..55ed0386e 100644 --- a/lib/puppet/type/whit.rb +++ b/lib/puppet/type/whit.rb @@ -1,11 +1,17 @@ Puppet::Type.newtype(:whit) do desc "The smallest possible resource type, for when you need a resource and naught else." newparam :name do desc "The name of the whit, because it must have one." end def to_s - "Class[#{name}]" + "(#{name})" + end + + def refresh + # We don't do anything with them, but we need this to + # show that we are "refresh aware" and not break the + # chain of propogation. end end diff --git a/spec/unit/parser/functions/create_resources_spec.rb b/spec/unit/parser/functions/create_resources_spec.rb index d4095b777..366fb536c 100755 --- a/spec/unit/parser/functions/create_resources_spec.rb +++ b/spec/unit/parser/functions/create_resources_spec.rb @@ -1,135 +1,137 @@ require 'puppet' require File.dirname(__FILE__) + '/../../../spec_helper' describe 'function for dynamically creating resources' do def get_scope @topscope = Puppet::Parser::Scope.new # This is necessary so we don't try to use the compiler to discover our parent. @topscope.parent = nil @scope = Puppet::Parser::Scope.new @scope.compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("floppy", :environment => 'production')) @scope.parent = @topscope @compiler = @scope.compiler end before :each do get_scope Puppet::Parser::Functions.function(:create_resources) end it "should exist" do Puppet::Parser::Functions.function(:create_resources).should == "function_create_resources" end it 'should require two arguments' do lambda { @scope.function_create_resources(['foo']) }.should raise_error(ArgumentError, 'create_resources(): wrong number of arguments (1; must be 2)') end describe 'when creating native types' do before :each do Puppet[:code]='notify{test:}' get_scope @scope.resource=Puppet::Parser::Resource.new('class', 't', :scope => @scope) end it 'empty hash should not cause resources to be added' do @scope.function_create_resources(['file', {}]) @compiler.catalog.resources.size == 1 end it 'should be able to add' do @scope.function_create_resources(['file', {'/etc/foo'=>{'ensure'=>'present'}}]) @compiler.catalog.resource(:file, "/etc/foo")['ensure'].should == 'present' end it 'should accept multiple types' do type_hash = {} type_hash['foo'] = {'message' => 'one'} type_hash['bar'] = {'message' => 'two'} @scope.function_create_resources(['notify', type_hash]) @compiler.catalog.resource(:notify, "foo")['message'].should == 'one' @compiler.catalog.resource(:notify, "bar")['message'].should == 'two' end it 'should fail to add non-existing type' do lambda { @scope.function_create_resources(['foo', {}]) }.should raise_error(ArgumentError, 'could not create resource of unknown type foo') end it 'should be able to add edges' do @scope.function_create_resources(['notify', {'foo'=>{'require' => 'Notify[test]'}}]) @scope.compiler.compile - edge = @scope.compiler.catalog.to_ral.relationship_graph.edges.detect do |edge| - edge.source.title == 'test' - end - edge.source.title.should == 'test' - edge.target.title.should == 'foo' + rg = @scope.compiler.catalog.to_ral.relationship_graph + test = rg.vertices.find { |v| v.title == 'test' } + foo = rg.vertices.find { |v| v.title == 'foo' } + test.should be + foo.should be + rg.path_between(test,foo).should be end end describe 'when dynamically creating resource types' do before :each do Puppet[:code]= 'define foo($one){notify{$name: message => $one}} notify{test:} ' get_scope @scope.resource=Puppet::Parser::Resource.new('class', 't', :scope => @scope) Puppet::Parser::Functions.function(:create_resources) end it 'should be able to create defined resoure types' do @scope.function_create_resources(['foo', {'blah'=>{'one'=>'two'}}]) # still have to compile for this to work... # I am not sure if this constraint ruins the tests @scope.compiler.compile @compiler.catalog.resource(:notify, "blah")['message'].should == 'two' end it 'should fail if defines are missing params' do @scope.function_create_resources(['foo', {'blah'=>{}}]) lambda { @scope.compiler.compile }.should raise_error(Puppet::ParseError, 'Must pass one to Foo[blah] at line 1') end it 'should be able to add multiple defines' do hash = {} hash['blah'] = {'one' => 'two'} hash['blaz'] = {'one' => 'three'} @scope.function_create_resources(['foo', hash]) # still have to compile for this to work... # I am not sure if this constraint ruins the tests @scope.compiler.compile @compiler.catalog.resource(:notify, "blah")['message'].should == 'two' @compiler.catalog.resource(:notify, "blaz")['message'].should == 'three' end it 'should be able to add edges' do @scope.function_create_resources(['foo', {'blah'=>{'one'=>'two', 'require' => 'Notify[test]'}}]) @scope.compiler.compile - edge = @scope.compiler.catalog.to_ral.relationship_graph.edges.detect do |edge| - edge.source.title == 'test' - end - edge.source.title.should == 'test' - edge.target.title.should == 'blah' + rg = @scope.compiler.catalog.to_ral.relationship_graph + test = rg.vertices.find { |v| v.title == 'test' } + blah = rg.vertices.find { |v| v.title == 'blah' } + test.should be + blah.should be + # (Yoda speak like we do) + rg.path_between(test,blah).should be @compiler.catalog.resource(:notify, "blah")['message'].should == 'two' end end describe 'when creating classes' do before :each do Puppet[:code]= 'class bar($one){notify{test: message => $one}} notify{tester:} ' get_scope @scope.resource=Puppet::Parser::Resource.new('class', 't', :scope => @scope) Puppet::Parser::Functions.function(:create_resources) end it 'should be able to create classes' do @scope.function_create_resources(['class', {'bar'=>{'one'=>'two'}}]) @scope.compiler.compile @compiler.catalog.resource(:notify, "test")['message'].should == 'two' @compiler.catalog.resource(:class, "bar").should_not be_nil#['message'].should == 'two' end it 'should fail to create non-existing classes' do lambda { @scope.function_create_resources(['class', {'blah'=>{'one'=>'two'}}]) }.should raise_error(ArgumentError ,'could not find hostclass blah') end it 'should be able to add edges' do @scope.function_create_resources(['class', {'bar'=>{'one'=>'two', 'require' => 'Notify[tester]'}}]) @scope.compiler.compile - edge = @scope.compiler.catalog.to_ral.relationship_graph.edges.detect do |e| - e.source.title == 'tester' - end - edge.source.title.should == 'tester' - edge.target.title.should == 'test' - #@compiler.catalog.resource(:notify, "blah")['message'].should == 'two' + rg = @scope.compiler.catalog.to_ral.relationship_graph + test = rg.vertices.find { |v| v.title == 'test' } + tester = rg.vertices.find { |v| v.title == 'tester' } + test.should be + tester.should be + rg.path_between(tester,test).should be end - end end diff --git a/spec/unit/resource/catalog_spec.rb b/spec/unit/resource/catalog_spec.rb index 411d10b32..78d1b3223 100755 --- a/spec/unit/resource/catalog_spec.rb +++ b/spec/unit/resource/catalog_spec.rb @@ -1,1062 +1,1062 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') describe Puppet::Resource::Catalog, "when compiling" do before do @basepath = Puppet.features.posix? ? "/somepath" : "C:/somepath" # stub this to not try to create state.yaml Puppet::Util::Storage.stubs(:store) end it "should be an Expirer" do Puppet::Resource::Catalog.ancestors.should be_include(Puppet::Util::Cacher::Expirer) end it "should always be expired if it's not applying" do @catalog = Puppet::Resource::Catalog.new("host") @catalog.expects(:applying?).returns false @catalog.should be_dependent_data_expired(Time.now) end it "should not be expired if it's applying and the timestamp is late enough" do @catalog = Puppet::Resource::Catalog.new("host") @catalog.expire @catalog.expects(:applying?).returns true @catalog.should_not be_dependent_data_expired(Time.now) end it "should be able to write its list of classes to the class file" do @catalog = Puppet::Resource::Catalog.new("host") @catalog.add_class "foo", "bar" Puppet.settings.expects(:value).with(:classfile).returns "/class/file" fh = mock 'filehandle' File.expects(:open).with("/class/file", "w").yields fh fh.expects(:puts).with "foo\nbar" @catalog.write_class_file end it "should have a client_version attribute" do @catalog = Puppet::Resource::Catalog.new("host") @catalog.client_version = 5 @catalog.client_version.should == 5 end it "should have a server_version attribute" do @catalog = Puppet::Resource::Catalog.new("host") @catalog.server_version = 5 @catalog.server_version.should == 5 end describe "when compiling" do it "should accept tags" do config = Puppet::Resource::Catalog.new("mynode") config.tag("one") config.tags.should == %w{one} end it "should accept multiple tags at once" do config = Puppet::Resource::Catalog.new("mynode") config.tag("one", "two") config.tags.should == %w{one two} end it "should convert all tags to strings" do config = Puppet::Resource::Catalog.new("mynode") config.tag("one", :two) config.tags.should == %w{one two} end it "should tag with both the qualified name and the split name" do config = Puppet::Resource::Catalog.new("mynode") config.tag("one::two") config.tags.include?("one").should be_true config.tags.include?("one::two").should be_true end it "should accept classes" do config = Puppet::Resource::Catalog.new("mynode") config.add_class("one") config.classes.should == %w{one} config.add_class("two", "three") config.classes.should == %w{one two three} end it "should tag itself with passed class names" do config = Puppet::Resource::Catalog.new("mynode") config.add_class("one") config.tags.should == %w{one} end end describe "when extracting transobjects" do def mkscope @node = Puppet::Node.new("mynode") @compiler = Puppet::Parser::Compiler.new(@node) # XXX This is ridiculous. @compiler.send(:evaluate_main) @scope = @compiler.topscope end def mkresource(type, name) Puppet::Parser::Resource.new(type, name, :source => @source, :scope => @scope) end it "should fail if no 'main' stage can be found" do lambda { Puppet::Resource::Catalog.new("mynode").extract }.should raise_error(Puppet::DevError) end it "should warn if any non-main stages are present" do config = Puppet::Resource::Catalog.new("mynode") @scope = mkscope @source = mock 'source' main = mkresource("stage", "main") config.add_resource(main) other = mkresource("stage", "other") config.add_resource(other) Puppet.expects(:warning) config.extract end it "should always create a TransBucket for the 'main' stage" do config = Puppet::Resource::Catalog.new("mynode") @scope = mkscope @source = mock 'source' main = mkresource("stage", "main") config.add_resource(main) result = config.extract result.type.should == "Stage" result.name.should == "main" end # Now try it with a more complicated graph -- a three tier graph, each tier it "should transform arbitrarily deep graphs into isomorphic trees" do config = Puppet::Resource::Catalog.new("mynode") @scope = mkscope @scope.stubs(:tags).returns([]) @source = mock 'source' # Create our scopes. top = mkresource "stage", "main" config.add_resource top topbucket = [] topbucket.expects(:classes=).with([]) top.expects(:to_trans).returns(topbucket) topres = mkresource "file", "/top" topres.expects(:to_trans).returns(:topres) config.add_edge top, topres middle = mkresource "class", "middle" middle.expects(:to_trans).returns([]) config.add_edge top, middle midres = mkresource "file", "/mid" midres.expects(:to_trans).returns(:midres) config.add_edge middle, midres bottom = mkresource "class", "bottom" bottom.expects(:to_trans).returns([]) config.add_edge middle, bottom botres = mkresource "file", "/bot" botres.expects(:to_trans).returns(:botres) config.add_edge bottom, botres toparray = config.extract # This is annoying; it should look like: # [[[:botres], :midres], :topres] # but we can't guarantee sort order. toparray.include?(:topres).should be_true midarray = toparray.find { |t| t.is_a?(Array) } midarray.include?(:midres).should be_true botarray = midarray.find { |t| t.is_a?(Array) } botarray.include?(:botres).should be_true end end describe " when converting to a Puppet::Resource catalog" do before do @original = Puppet::Resource::Catalog.new("mynode") @original.tag(*%w{one two three}) @original.add_class *%w{four five six} @top = Puppet::TransObject.new 'top', "class" @topobject = Puppet::TransObject.new '/topobject', "file" @middle = Puppet::TransObject.new 'middle', "class" @middleobject = Puppet::TransObject.new '/middleobject', "file" @bottom = Puppet::TransObject.new 'bottom', "class" @bottomobject = Puppet::TransObject.new '/bottomobject', "file" @resources = [@top, @topobject, @middle, @middleobject, @bottom, @bottomobject] @original.add_resource(*@resources) @original.add_edge(@top, @topobject) @original.add_edge(@top, @middle) @original.add_edge(@middle, @middleobject) @original.add_edge(@middle, @bottom) @original.add_edge(@bottom, @bottomobject) @catalog = @original.to_resource end it "should copy over the version" do @original.version = "foo" @original.to_resource.version.should == "foo" end it "should convert parser resources to plain resources" do resource = Puppet::Parser::Resource.new(:file, "foo", :scope => stub("scope", :environment => nil, :namespaces => nil), :source => stub("source")) catalog = Puppet::Resource::Catalog.new("whev") catalog.add_resource(resource) new = catalog.to_resource new.resource(:file, "foo").class.should == Puppet::Resource end it "should add all resources as Puppet::Resource instances" do @resources.each { |resource| @catalog.resource(resource.ref).should be_instance_of(Puppet::Resource) } end it "should copy the tag list to the new catalog" do @catalog.tags.sort.should == @original.tags.sort end it "should copy the class list to the new catalog" do @catalog.classes.should == @original.classes end it "should duplicate the original edges" do @original.edges.each do |edge| @catalog.edge?(@catalog.resource(edge.source.ref), @catalog.resource(edge.target.ref)).should be_true end end it "should set itself as the catalog for each converted resource" do @catalog.vertices.each { |v| v.catalog.object_id.should equal(@catalog.object_id) } end end describe "when converting to a RAL catalog" do before do @original = Puppet::Resource::Catalog.new("mynode") @original.tag(*%w{one two three}) @original.add_class *%w{four five six} @top = Puppet::Resource.new :class, 'top' @topobject = Puppet::Resource.new :file, @basepath+'/topobject' @middle = Puppet::Resource.new :class, 'middle' @middleobject = Puppet::Resource.new :file, @basepath+'/middleobject' @bottom = Puppet::Resource.new :class, 'bottom' @bottomobject = Puppet::Resource.new :file, @basepath+'/bottomobject' @resources = [@top, @topobject, @middle, @middleobject, @bottom, @bottomobject] @original.add_resource(*@resources) @original.add_edge(@top, @topobject) @original.add_edge(@top, @middle) @original.add_edge(@middle, @middleobject) @original.add_edge(@middle, @bottom) @original.add_edge(@bottom, @bottomobject) @catalog = @original.to_ral end it "should add all resources as RAL instances" do @resources.each { |resource| @catalog.resource(resource.ref).should be_instance_of(Puppet::Type) } end it "should copy the tag list to the new catalog" do @catalog.tags.sort.should == @original.tags.sort end it "should copy the class list to the new catalog" do @catalog.classes.should == @original.classes end it "should duplicate the original edges" do @original.edges.each do |edge| @catalog.edge?(@catalog.resource(edge.source.ref), @catalog.resource(edge.target.ref)).should be_true end end it "should set itself as the catalog for each converted resource" do @catalog.vertices.each { |v| v.catalog.object_id.should equal(@catalog.object_id) } end # This tests #931. it "should not lose track of resources whose names vary" do changer = Puppet::TransObject.new 'changer', 'test' config = Puppet::Resource::Catalog.new('test') config.add_resource(changer) config.add_resource(@top) config.add_edge(@top, changer) resource = stub 'resource', :name => "changer2", :title => "changer2", :ref => "Test[changer2]", :catalog= => nil, :remove => nil #changer is going to get duplicated as part of a fix for aliases 1094 changer.expects(:dup).returns(changer) changer.expects(:to_ral).returns(resource) newconfig = nil proc { @catalog = config.to_ral }.should_not raise_error @catalog.resource("Test[changer2]").should equal(resource) end after do # Remove all resource instances. @catalog.clear(true) end end describe "when filtering" do before :each do @original = Puppet::Resource::Catalog.new("mynode") @original.tag(*%w{one two three}) @original.add_class *%w{four five six} @r1 = stub_everything 'r1', :ref => "File[/a]" @r1.stubs(:respond_to?).with(:ref).returns(true) @r1.stubs(:dup).returns(@r1) @r1.stubs(:is_a?).returns(Puppet::Resource).returns(true) @r2 = stub_everything 'r2', :ref => "File[/b]" @r2.stubs(:respond_to?).with(:ref).returns(true) @r2.stubs(:dup).returns(@r2) @r2.stubs(:is_a?).returns(Puppet::Resource).returns(true) @resources = [@r1,@r2] @original.add_resource(@r1,@r2) end it "should transform the catalog to a resource catalog" do @original.expects(:to_catalog).with { |h,b| h == :to_resource } @original.filter end it "should scan each catalog resource in turn and apply filtering block" do @resources.each { |r| r.expects(:test?) } @original.filter do |r| r.test? end end it "should filter out resources which produce true when the filter block is evaluated" do @original.filter do |r| r == @r1 end.resource("File[/a]").should be_nil end it "should not consider edges against resources that were filtered out" do @original.add_edge(@r1,@r2) @original.filter do |r| r == @r1 end.edge?(@r1,@r2).should_not be end end describe "when functioning as a resource container" do before do @catalog = Puppet::Resource::Catalog.new("host") @one = Puppet::Type.type(:notify).new :name => "one" @two = Puppet::Type.type(:notify).new :name => "two" @dupe = Puppet::Type.type(:notify).new :name => "one" end it "should provide a method to add one or more resources" do @catalog.add_resource @one, @two @catalog.resource(@one.ref).should equal(@one) @catalog.resource(@two.ref).should equal(@two) end it "should add resources to the relationship graph if it exists" do relgraph = @catalog.relationship_graph @catalog.add_resource @one relgraph.should be_vertex(@one) end it "should set itself as the resource's catalog if it is not a relationship graph" do @one.expects(:catalog=).with(@catalog) @catalog.add_resource @one end it "should make all vertices available by resource reference" do @catalog.add_resource(@one) @catalog.resource(@one.ref).should equal(@one) @catalog.vertices.find { |r| r.ref == @one.ref }.should equal(@one) end it "should canonize how resources are referred to during retrieval when both type and title are provided" do @catalog.add_resource(@one) @catalog.resource("notify", "one").should equal(@one) end it "should canonize how resources are referred to during retrieval when just the title is provided" do @catalog.add_resource(@one) @catalog.resource("notify[one]", nil).should equal(@one) end it "should not allow two resources with the same resource reference" do @catalog.add_resource(@one) proc { @catalog.add_resource(@dupe) }.should raise_error(Puppet::Resource::Catalog::DuplicateResourceError) end it "should not store objects that do not respond to :ref" do proc { @catalog.add_resource("thing") }.should raise_error(ArgumentError) end it "should remove all resources when asked" do @catalog.add_resource @one @catalog.add_resource @two @one.expects :remove @two.expects :remove @catalog.clear(true) end it "should support a mechanism for finishing resources" do @one.expects :finish @two.expects :finish @catalog.add_resource @one @catalog.add_resource @two @catalog.finalize end it "should make default resources when finalizing" do @catalog.expects(:make_default_resources) @catalog.finalize end it "should add default resources to the catalog upon creation" do @catalog.make_default_resources @catalog.resource(:schedule, "daily").should_not be_nil end it "should optionally support an initialization block and should finalize after such blocks" do @one.expects :finish @two.expects :finish config = Puppet::Resource::Catalog.new("host") do |conf| conf.add_resource @one conf.add_resource @two end end it "should inform the resource that it is the resource's catalog" do @one.expects(:catalog=).with(@catalog) @catalog.add_resource @one end it "should be able to find resources by reference" do @catalog.add_resource @one @catalog.resource(@one.ref).should equal(@one) end it "should be able to find resources by reference or by type/title tuple" do @catalog.add_resource @one @catalog.resource("notify", "one").should equal(@one) end it "should have a mechanism for removing resources" do @catalog.add_resource @one @one.expects :remove @catalog.remove_resource(@one) @catalog.resource(@one.ref).should be_nil @catalog.vertex?(@one).should be_false end it "should have a method for creating aliases for resources" do @catalog.add_resource @one @catalog.alias(@one, "other") @catalog.resource("notify", "other").should equal(@one) end it "should ignore conflicting aliases that point to the aliased resource" do @catalog.alias(@one, "other") lambda { @catalog.alias(@one, "other") }.should_not raise_error end it "should create aliases for resources isomorphic resources whose names do not match their titles" do resource = Puppet::Type::File.new(:title => "testing", :path => @basepath+"/something") @catalog.add_resource(resource) @catalog.resource(:file, @basepath+"/something").should equal(resource) end it "should not create aliases for resources non-isomorphic resources whose names do not match their titles" do resource = Puppet::Type.type(:exec).new(:title => "testing", :command => "echo", :path => %w{/bin /usr/bin /usr/local/bin}) @catalog.add_resource(resource) # Yay, I've already got a 'should' method @catalog.resource(:exec, "echo").object_id.should == nil.object_id end # This test is the same as the previous, but the behaviour should be explicit. it "should alias using the class name from the resource reference, not the resource class name" do @catalog.add_resource @one @catalog.alias(@one, "other") @catalog.resource("notify", "other").should equal(@one) end it "should ignore conflicting aliases that point to the aliased resource" do @catalog.alias(@one, "other") lambda { @catalog.alias(@one, "other") }.should_not raise_error end it "should fail to add an alias if the aliased name already exists" do @catalog.add_resource @one proc { @catalog.alias @two, "one" }.should raise_error(ArgumentError) end it "should not fail when a resource has duplicate aliases created" do @catalog.add_resource @one proc { @catalog.alias @one, "one" }.should_not raise_error end it "should not create aliases that point back to the resource" do @catalog.alias(@one, "one") @catalog.resource(:notify, "one").should be_nil end it "should be able to look resources up by their aliases" do @catalog.add_resource @one @catalog.alias @one, "two" @catalog.resource(:notify, "two").should equal(@one) end it "should remove resource aliases when the target resource is removed" do @catalog.add_resource @one @catalog.alias(@one, "other") @one.expects :remove @catalog.remove_resource(@one) @catalog.resource("notify", "other").should be_nil end it "should add an alias for the namevar when the title and name differ on isomorphic resource types" do resource = Puppet::Type.type(:file).new :path => @basepath+"/something", :title => "other", :content => "blah" resource.expects(:isomorphic?).returns(true) @catalog.add_resource(resource) @catalog.resource(:file, "other").should equal(resource) @catalog.resource(:file, @basepath+"/something").ref.should == resource.ref end it "should not add an alias for the namevar when the title and name differ on non-isomorphic resource types" do resource = Puppet::Type.type(:file).new :path => @basepath+"/something", :title => "other", :content => "blah" resource.expects(:isomorphic?).returns(false) @catalog.add_resource(resource) @catalog.resource(:file, resource.title).should equal(resource) # We can't use .should here, because the resources respond to that method. raise "Aliased non-isomorphic resource" if @catalog.resource(:file, resource.name) end it "should provide a method to create additional resources that also registers the resource" do args = {:name => "/yay", :ensure => :file} resource = stub 'file', :ref => "File[/yay]", :catalog= => @catalog, :title => "/yay", :[] => "/yay" Puppet::Type.type(:file).expects(:new).with(args).returns(resource) @catalog.create_resource :file, args @catalog.resource("File[/yay]").should equal(resource) end end describe "when applying" do before :each do @catalog = Puppet::Resource::Catalog.new("host") @transaction = mock 'transaction' Puppet::Transaction.stubs(:new).returns(@transaction) @transaction.stubs(:evaluate) @transaction.stubs(:add_times) Puppet.settings.stubs(:use) end it "should create and evaluate a transaction" do @transaction.expects(:evaluate) @catalog.apply end it "should provide the catalog retrieval time to the transaction" do @catalog.retrieval_duration = 5 @transaction.expects(:add_times).with(:config_retrieval => 5) @catalog.apply end it "should use a retrieval time of 0 if none is set in the catalog" do @catalog.retrieval_duration = nil @transaction.expects(:add_times).with(:config_retrieval => 0) @catalog.apply end it "should return the transaction" do @catalog.apply.should equal(@transaction) end it "should yield the transaction if a block is provided" do @catalog.apply do |trans| trans.should equal(@transaction) end end it "should default to being a host catalog" do @catalog.host_config.should be_true end it "should be able to be set to a non-host_config" do @catalog.host_config = false @catalog.host_config.should be_false end it "should pass supplied tags on to the transaction" do @transaction.expects(:tags=).with(%w{one two}) @catalog.apply(:tags => %w{one two}) end it "should set ignoreschedules on the transaction if specified in apply()" do @transaction.expects(:ignoreschedules=).with(true) @catalog.apply(:ignoreschedules => true) end it "should expire cached data in the resources both before and after the transaction" do @catalog.expects(:expire).times(2) @catalog.apply end describe "host catalogs" do # super() doesn't work in the setup method for some reason before do @catalog.host_config = true Puppet::Util::Storage.stubs(:store) end it "should initialize the state database before applying a catalog" do Puppet::Util::Storage.expects(:load) # Short-circuit the apply, so we know we're loading before the transaction Puppet::Transaction.expects(:new).raises ArgumentError proc { @catalog.apply }.should raise_error(ArgumentError) end it "should sync the state database after applying" do Puppet::Util::Storage.expects(:store) @transaction.stubs :any_failed? => false @catalog.apply end after { Puppet.settings.clear } end describe "non-host catalogs" do before do @catalog.host_config = false end it "should never send reports" do Puppet[:report] = true Puppet[:summarize] = true @catalog.apply end it "should never modify the state database" do Puppet::Util::Storage.expects(:load).never Puppet::Util::Storage.expects(:store).never @catalog.apply end after { Puppet.settings.clear } end end describe "when creating a relationship graph" do before do Puppet::Type.type(:component) @catalog = Puppet::Resource::Catalog.new("host") @compone = Puppet::Type::Component.new :name => "one" @comptwo = Puppet::Type::Component.new :name => "two", :require => "Class[one]" @file = Puppet::Type.type(:file) @one = @file.new :path => @basepath+"/one" @two = @file.new :path => @basepath+"/two" @sub = @file.new :path => @basepath+"/two/subdir" @catalog.add_edge @compone, @one @catalog.add_edge @comptwo, @two @three = @file.new :path => @basepath+"/three" @four = @file.new :path => @basepath+"/four", :require => "File[#{@basepath}/three]" @five = @file.new :path => @basepath+"/five" @catalog.add_resource @compone, @comptwo, @one, @two, @three, @four, @five, @sub @relationships = @catalog.relationship_graph end it "should be able to create a relationship graph" do @relationships.should be_instance_of(Puppet::SimpleGraph) end it "should not have any components" do @relationships.vertices.find { |r| r.instance_of?(Puppet::Type::Component) }.should be_nil end it "should have all non-component resources from the catalog" do # The failures print out too much info, so i just do a class comparison @relationships.vertex?(@five).should be_true end it "should have all resource relationships set as edges" do @relationships.edge?(@three, @four).should be_true end it "should copy component relationships to all contained resources" do - @relationships.edge?(@one, @two).should be_true + @relationships.path_between(@one, @two).should be end it "should add automatic relationships to the relationship graph" do @relationships.edge?(@two, @sub).should be_true end it "should get removed when the catalog is cleaned up" do @relationships.expects(:clear) @catalog.clear @catalog.instance_variable_get("@relationship_graph").should be_nil end it "should write :relationships and :expanded_relationships graph files if the catalog is a host catalog" do @catalog.clear graph = Puppet::SimpleGraph.new Puppet::SimpleGraph.expects(:new).returns graph graph.expects(:write_graph).with(:relationships) graph.expects(:write_graph).with(:expanded_relationships) @catalog.host_config = true @catalog.relationship_graph end it "should not write graph files if the catalog is not a host catalog" do @catalog.clear graph = Puppet::SimpleGraph.new Puppet::SimpleGraph.expects(:new).returns graph graph.expects(:write_graph).never @catalog.host_config = false @catalog.relationship_graph end it "should create a new relationship graph after clearing the old one" do @relationships.expects(:clear) @catalog.clear @catalog.relationship_graph.should be_instance_of(Puppet::SimpleGraph) end it "should remove removed resources from the relationship graph if it exists" do @catalog.remove_resource(@one) @catalog.relationship_graph.vertex?(@one).should be_false end end describe "when writing dot files" do before do @catalog = Puppet::Resource::Catalog.new("host") @name = :test @file = File.join(Puppet[:graphdir], @name.to_s + ".dot") end it "should only write when it is a host catalog" do File.expects(:open).with(@file).never @catalog.host_config = false Puppet[:graph] = true @catalog.write_graph(@name) end after do Puppet.settings.clear end end describe "when indirecting" do before do @real_indirection = Puppet::Resource::Catalog.indirection @indirection = stub 'indirection', :name => :catalog Puppet::Util::Cacher.expire end it "should use the value of the 'catalog_terminus' setting to determine its terminus class" do # Puppet only checks the terminus setting the first time you ask # so this returns the object to the clean state # at the expense of making this test less pure Puppet::Resource::Catalog.indirection.reset_terminus_class Puppet.settings[:catalog_terminus] = "rest" Puppet::Resource::Catalog.indirection.terminus_class.should == :rest end it "should allow the terminus class to be set manually" do Puppet::Resource::Catalog.indirection.terminus_class = :rest Puppet::Resource::Catalog.indirection.terminus_class.should == :rest end after do Puppet::Util::Cacher.expire @real_indirection.reset_terminus_class end end describe "when converting to yaml" do before do @catalog = Puppet::Resource::Catalog.new("me") @catalog.add_edge("one", "two") end it "should be able to be dumped to yaml" do YAML.dump(@catalog).should be_instance_of(String) end end describe "when converting from yaml" do before do @catalog = Puppet::Resource::Catalog.new("me") @catalog.add_edge("one", "two") text = YAML.dump(@catalog) @newcatalog = YAML.load(text) end it "should get converted back to a catalog" do @newcatalog.should be_instance_of(Puppet::Resource::Catalog) end it "should have all vertices" do @newcatalog.vertex?("one").should be_true @newcatalog.vertex?("two").should be_true end it "should have all edges" do @newcatalog.edge?("one", "two").should be_true end end end describe Puppet::Resource::Catalog, "when converting to pson", :if => Puppet.features.pson? do before do @catalog = Puppet::Resource::Catalog.new("myhost") end def pson_output_should @catalog.class.expects(:pson_create).with { |hash| yield hash }.returns(:something) end # LAK:NOTE For all of these tests, we convert back to the resource so we can # trap the actual data structure then. it "should set its document_type to 'Catalog'" do pson_output_should { |hash| hash['document_type'] == "Catalog" } PSON.parse @catalog.to_pson end it "should set its data as a hash" do pson_output_should { |hash| hash['data'].is_a?(Hash) } PSON.parse @catalog.to_pson end [:name, :version, :tags, :classes].each do |param| it "should set its #{param} to the #{param} of the resource" do @catalog.send(param.to_s + "=", "testing") unless @catalog.send(param) pson_output_should { |hash| hash['data'][param.to_s] == @catalog.send(param) } PSON.parse @catalog.to_pson end end it "should convert its resources to a PSON-encoded array and store it as the 'resources' data" do one = stub 'one', :to_pson_data_hash => "one_resource", :ref => "Foo[one]" two = stub 'two', :to_pson_data_hash => "two_resource", :ref => "Foo[two]" @catalog.add_resource(one) @catalog.add_resource(two) # TODO this should really guarantee sort order PSON.parse(@catalog.to_pson,:create_additions => false)['data']['resources'].sort.should == ["one_resource", "two_resource"].sort end it "should convert its edges to a PSON-encoded array and store it as the 'edges' data" do one = stub 'one', :to_pson_data_hash => "one_resource", :ref => 'Foo[one]' two = stub 'two', :to_pson_data_hash => "two_resource", :ref => 'Foo[two]' three = stub 'three', :to_pson_data_hash => "three_resource", :ref => 'Foo[three]' @catalog.add_edge(one, two) @catalog.add_edge(two, three) @catalog.edges_between(one, two )[0].expects(:to_pson_data_hash).returns "one_two_pson" @catalog.edges_between(two, three)[0].expects(:to_pson_data_hash).returns "two_three_pson" PSON.parse(@catalog.to_pson,:create_additions => false)['data']['edges'].sort.should == %w{one_two_pson two_three_pson}.sort end end describe Puppet::Resource::Catalog, "when converting from pson", :if => Puppet.features.pson? do def pson_result_should Puppet::Resource::Catalog.expects(:new).with { |hash| yield hash } end before do @data = { 'name' => "myhost" } @pson = { 'document_type' => 'Puppet::Resource::Catalog', 'data' => @data, 'metadata' => {} } @catalog = Puppet::Resource::Catalog.new("myhost") Puppet::Resource::Catalog.stubs(:new).returns @catalog end it "should be extended with the PSON utility module" do Puppet::Resource::Catalog.singleton_class.ancestors.should be_include(Puppet::Util::Pson) end it "should create it with the provided name" do Puppet::Resource::Catalog.expects(:new).with('myhost').returns @catalog PSON.parse @pson.to_pson end it "should set the provided version on the catalog if one is set" do @data['version'] = 50 PSON.parse @pson.to_pson @catalog.version.should == @data['version'] end it "should set any provided tags on the catalog" do @data['tags'] = %w{one two} PSON.parse @pson.to_pson @catalog.tags.should == @data['tags'] end it "should set any provided classes on the catalog" do @data['classes'] = %w{one two} PSON.parse @pson.to_pson @catalog.classes.should == @data['classes'] end it 'should convert the resources list into resources and add each of them' do @data['resources'] = [Puppet::Resource.new(:file, "/foo"), Puppet::Resource.new(:file, "/bar")] @catalog.expects(:add_resource).times(2).with { |res| res.type == "File" } PSON.parse @pson.to_pson end it 'should convert resources even if they do not include "type" information' do @data['resources'] = [Puppet::Resource.new(:file, "/foo")] @data['resources'][0].expects(:to_pson).returns '{"title":"/foo","tags":["file"],"type":"File"}' @catalog.expects(:add_resource).with { |res| res.type == "File" } PSON.parse @pson.to_pson end it 'should convert the edges list into edges and add each of them' do one = Puppet::Relationship.new("osource", "otarget", :event => "one", :callback => "refresh") two = Puppet::Relationship.new("tsource", "ttarget", :event => "two", :callback => "refresh") @data['edges'] = [one, two] @catalog.stubs(:resource).returns("eh") @catalog.expects(:add_edge).with { |edge| edge.event == "one" } @catalog.expects(:add_edge).with { |edge| edge.event == "two" } PSON.parse @pson.to_pson end it "should be able to convert relationships that do not include 'type' information" do one = Puppet::Relationship.new("osource", "otarget", :event => "one", :callback => "refresh") one.expects(:to_pson).returns "{\"event\":\"one\",\"callback\":\"refresh\",\"source\":\"osource\",\"target\":\"otarget\"}" @data['edges'] = [one] @catalog.stubs(:resource).returns("eh") @catalog.expects(:add_edge).with { |edge| edge.event == "one" } PSON.parse @pson.to_pson end it "should set the source and target for each edge to the actual resource" do edge = Puppet::Relationship.new("source", "target") @data['edges'] = [edge] @catalog.expects(:resource).with("source").returns("source_resource") @catalog.expects(:resource).with("target").returns("target_resource") @catalog.expects(:add_edge).with { |edge| edge.source == "source_resource" and edge.target == "target_resource" } PSON.parse @pson.to_pson end it "should fail if the source resource cannot be found" do edge = Puppet::Relationship.new("source", "target") @data['edges'] = [edge] @catalog.expects(:resource).with("source").returns(nil) @catalog.stubs(:resource).with("target").returns("target_resource") lambda { PSON.parse @pson.to_pson }.should raise_error(ArgumentError) end it "should fail if the target resource cannot be found" do edge = Puppet::Relationship.new("source", "target") @data['edges'] = [edge] @catalog.stubs(:resource).with("source").returns("source_resource") @catalog.expects(:resource).with("target").returns(nil) lambda { PSON.parse @pson.to_pson }.should raise_error(ArgumentError) end describe "#title_key_for_ref" do it "should parse a resource ref string into a pair" do @catalog.title_key_for_ref("Title[name]").should == ["Title", "name"] end it "should parse a resource ref string into a pair, even if there's a newline inside the name" do @catalog.title_key_for_ref("Title[na\nme]").should == ["Title", "na\nme"] end end end diff --git a/spec/unit/simple_graph_spec.rb b/spec/unit/simple_graph_spec.rb index c106f550b..99db2a55c 100755 --- a/spec/unit/simple_graph_spec.rb +++ b/spec/unit/simple_graph_spec.rb @@ -1,865 +1,904 @@ #!/usr/bin/env ruby # # Created by Luke Kanies on 2007-11-1. # Copyright (c) 2006. All rights reserved. require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') require 'puppet/simple_graph' describe Puppet::SimpleGraph do it "should return the number of its vertices as its length" do @graph = Puppet::SimpleGraph.new @graph.add_vertex("one") @graph.add_vertex("two") @graph.size.should == 2 end it "should consider itself a directed graph" do Puppet::SimpleGraph.new.directed?.should be_true end it "should provide a method for reversing the graph" do @graph = Puppet::SimpleGraph.new @graph.add_edge(:one, :two) @graph.reversal.edge?(:two, :one).should be_true end it "should be able to produce a dot graph" do @graph = Puppet::SimpleGraph.new @graph.add_edge(:one, :two) proc { @graph.to_dot_graph }.should_not raise_error end describe "when managing vertices" do before do @graph = Puppet::SimpleGraph.new end it "should provide a method to add a vertex" do @graph.add_vertex(:test) @graph.vertex?(:test).should be_true end it "should reset its reversed graph when vertices are added" do rev = @graph.reversal @graph.add_vertex(:test) @graph.reversal.should_not equal(rev) end it "should ignore already-present vertices when asked to add a vertex" do @graph.add_vertex(:test) proc { @graph.add_vertex(:test) }.should_not raise_error end it "should return true when asked if a vertex is present" do @graph.add_vertex(:test) @graph.vertex?(:test).should be_true end it "should return false when asked if a non-vertex is present" do @graph.vertex?(:test).should be_false end it "should return all set vertices when asked" do @graph.add_vertex(:one) @graph.add_vertex(:two) @graph.vertices.length.should == 2 @graph.vertices.should include(:one) @graph.vertices.should include(:two) end it "should remove a given vertex when asked" do @graph.add_vertex(:one) @graph.remove_vertex!(:one) @graph.vertex?(:one).should be_false end it "should do nothing when a non-vertex is asked to be removed" do proc { @graph.remove_vertex!(:one) }.should_not raise_error end end describe "when managing edges" do before do @graph = Puppet::SimpleGraph.new end it "should provide a method to test whether a given vertex pair is an edge" do @graph.should respond_to(:edge?) end it "should reset its reversed graph when edges are added" do rev = @graph.reversal @graph.add_edge(:one, :two) @graph.reversal.should_not equal(rev) end it "should provide a method to add an edge as an instance of the edge class" do edge = Puppet::Relationship.new(:one, :two) @graph.add_edge(edge) @graph.edge?(:one, :two).should be_true end it "should provide a method to add an edge by specifying the two vertices" do @graph.add_edge(:one, :two) @graph.edge?(:one, :two).should be_true end it "should provide a method to add an edge by specifying the two vertices and a label" do @graph.add_edge(:one, :two, :callback => :awesome) @graph.edge?(:one, :two).should be_true end describe "when retrieving edges between two nodes" do it "should handle the case of nodes not in the graph" do @graph.edges_between(:one, :two).should == [] end it "should handle the case of nodes with no edges between them" do @graph.add_vertex(:one) @graph.add_vertex(:two) @graph.edges_between(:one, :two).should == [] end it "should handle the case of nodes connected by a single edge" do edge = Puppet::Relationship.new(:one, :two) @graph.add_edge(edge) @graph.edges_between(:one, :two).length.should == 1 @graph.edges_between(:one, :two)[0].should equal(edge) end it "should handle the case of nodes connected by multiple edges" do edge1 = Puppet::Relationship.new(:one, :two, :callback => :foo) edge2 = Puppet::Relationship.new(:one, :two, :callback => :bar) @graph.add_edge(edge1) @graph.add_edge(edge2) Set.new(@graph.edges_between(:one, :two)).should == Set.new([edge1, edge2]) end end it "should add the edge source as a vertex if it is not already" do edge = Puppet::Relationship.new(:one, :two) @graph.add_edge(edge) @graph.vertex?(:one).should be_true end it "should add the edge target as a vertex if it is not already" do edge = Puppet::Relationship.new(:one, :two) @graph.add_edge(edge) @graph.vertex?(:two).should be_true end it "should return all edges as edge instances when asked" do one = Puppet::Relationship.new(:one, :two) two = Puppet::Relationship.new(:two, :three) @graph.add_edge(one) @graph.add_edge(two) edges = @graph.edges edges.should be_instance_of(Array) edges.length.should == 2 edges.should include(one) edges.should include(two) end it "should remove an edge when asked" do edge = Puppet::Relationship.new(:one, :two) @graph.add_edge(edge) @graph.remove_edge!(edge) @graph.edge?(edge.source, edge.target).should be_false end it "should remove all related edges when a vertex is removed" do one = Puppet::Relationship.new(:one, :two) two = Puppet::Relationship.new(:two, :three) @graph.add_edge(one) @graph.add_edge(two) @graph.remove_vertex!(:two) @graph.edge?(:one, :two).should be_false @graph.edge?(:two, :three).should be_false @graph.edges.length.should == 0 end end describe "when finding adjacent vertices" do before do @graph = Puppet::SimpleGraph.new @one_two = Puppet::Relationship.new(:one, :two) @two_three = Puppet::Relationship.new(:two, :three) @one_three = Puppet::Relationship.new(:one, :three) @graph.add_edge(@one_two) @graph.add_edge(@one_three) @graph.add_edge(@two_three) end it "should return adjacent vertices" do adj = @graph.adjacent(:one) adj.should be_include(:three) adj.should be_include(:two) end it "should default to finding :out vertices" do @graph.adjacent(:two).should == [:three] end it "should support selecting :in vertices" do @graph.adjacent(:two, :direction => :in).should == [:one] end it "should default to returning the matching vertices as an array of vertices" do @graph.adjacent(:two).should == [:three] end it "should support returning an array of matching edges" do @graph.adjacent(:two, :type => :edges).should == [@two_three] end # Bug #2111 it "should not consider a vertex adjacent just because it was asked about previously" do @graph = Puppet::SimpleGraph.new @graph.add_vertex("a") @graph.add_vertex("b") @graph.edge?("a", "b") @graph.adjacent("a").should == [] end end describe "when clearing" do before do @graph = Puppet::SimpleGraph.new one = Puppet::Relationship.new(:one, :two) two = Puppet::Relationship.new(:two, :three) @graph.add_edge(one) @graph.add_edge(two) @graph.clear end it "should remove all vertices" do @graph.vertices.should be_empty end it "should remove all edges" do @graph.edges.should be_empty end end describe "when reversing graphs" do before do @graph = Puppet::SimpleGraph.new end it "should provide a method for reversing the graph" do @graph.add_edge(:one, :two) @graph.reversal.edge?(:two, :one).should be_true end it "should add all vertices to the reversed graph" do @graph.add_edge(:one, :two) @graph.vertex?(:one).should be_true @graph.vertex?(:two).should be_true end it "should retain labels on edges" do @graph.add_edge(:one, :two, :callback => :awesome) edge = @graph.reversal.edges_between(:two, :one)[0] edge.label.should == {:callback => :awesome} end end - describe "when sorting the graph" do + describe "when reporting cycles in the graph" do before do @graph = Puppet::SimpleGraph.new end def add_edges(hash) hash.each do |a,b| @graph.add_edge(a, b) end end - it "should sort the graph topologically" do - add_edges :a => :b, :b => :c - @graph.topsort.should == [:a, :b, :c] - end - it "should fail on two-vertex loops" do add_edges :a => :b, :b => :a - proc { @graph.topsort }.should raise_error(Puppet::Error) + proc { @graph.report_cycles_in_graph }.should raise_error(Puppet::Error) end it "should fail on multi-vertex loops" do add_edges :a => :b, :b => :c, :c => :a - proc { @graph.topsort }.should raise_error(Puppet::Error) + proc { @graph.report_cycles_in_graph }.should raise_error(Puppet::Error) end it "should fail when a larger tree contains a small cycle" do add_edges :a => :b, :b => :a, :c => :a, :d => :c - proc { @graph.topsort }.should raise_error(Puppet::Error) + proc { @graph.report_cycles_in_graph }.should raise_error(Puppet::Error) end it "should succeed on trees with no cycles" do add_edges :a => :b, :b => :e, :c => :a, :d => :c - proc { @graph.topsort }.should_not raise_error + proc { @graph.report_cycles_in_graph }.should_not raise_error end it "should produce the correct relationship text" do add_edges :a => :b, :b => :a # cycle detection starts from a or b randomly # so we need to check for either ordering in the error message want = %r{Found 1 dependency cycle:\n\((a => b => a|b => a => b)\)\nTry} - expect { @graph.topsort }.to raise_error(Puppet::Error, want) + expect { @graph.report_cycles_in_graph }.to raise_error(Puppet::Error, want) end it "cycle discovery should be the minimum cycle for a simple graph" do add_edges "a" => "b" add_edges "b" => "a" add_edges "b" => "c" cycles = nil expect { cycles = @graph.find_cycles_in_graph.sort }.should_not raise_error cycles.should be == [["a", "b"]] end it "cycle discovery should handle two distinct cycles" do add_edges "a" => "a1", "a1" => "a" add_edges "b" => "b1", "b1" => "b" cycles = nil expect { cycles = @graph.find_cycles_in_graph.sort }.should_not raise_error cycles.should be == [["a", "a1"], ["b", "b1"]] end it "cycle discovery should handle two cycles in a connected graph" do add_edges "a" => "b", "b" => "c", "c" => "d" add_edges "a" => "a1", "a1" => "a" add_edges "c" => "c1", "c1" => "c2", "c2" => "c3", "c3" => "c" cycles = nil expect { cycles = @graph.find_cycles_in_graph.sort }.should_not raise_error cycles.should be == [%w{a a1}, %w{c c1 c2 c3}] end it "cycle discovery should handle a complicated cycle" do add_edges "a" => "b", "b" => "c" add_edges "a" => "c" add_edges "c" => "c1", "c1" => "a" add_edges "c" => "c2", "c2" => "b" cycles = nil expect { cycles = @graph.find_cycles_in_graph.sort }.should_not raise_error cycles.should be == [%w{a b c c1 c2}] end it "cycle discovery should not fail with large data sets" do limit = 3000 (1..(limit - 1)).each do |n| add_edges n.to_s => (n+1).to_s end cycles = nil expect { cycles = @graph.find_cycles_in_graph.sort }.should_not raise_error cycles.should be == [] end it "path finding should work with a simple cycle" do add_edges "a" => "b", "b" => "c", "c" => "a" cycles = @graph.find_cycles_in_graph.sort paths = @graph.paths_in_cycle(cycles.first, 100) paths.should be == [%w{a b c a}] end it "path finding should work with two independent cycles" do add_edges "a" => "b1" add_edges "a" => "b2" add_edges "b1" => "a", "b2" => "a" cycles = @graph.find_cycles_in_graph.sort cycles.length.should be == 1 paths = @graph.paths_in_cycle(cycles.first, 100) paths.sort.should be == [%w{a b1 a}, %w{a b2 a}] end it "path finding should prefer shorter paths in cycles" do add_edges "a" => "b", "b" => "c", "c" => "a" add_edges "b" => "a" cycles = @graph.find_cycles_in_graph.sort cycles.length.should be == 1 paths = @graph.paths_in_cycle(cycles.first, 100) paths.should be == [%w{a b a}, %w{a b c a}] end it "path finding should respect the max_path value" do (1..20).each do |n| add_edges "a" => "b#{n}", "b#{n}" => "a" end cycles = @graph.find_cycles_in_graph.sort cycles.length.should be == 1 (1..20).each do |n| paths = @graph.paths_in_cycle(cycles.first, n) paths.length.should be == n end paths = @graph.paths_in_cycle(cycles.first, 21) paths.length.should be == 20 end - - # Our graph's add_edge method is smart enough not to add - # duplicate edges, so we use the objects, which it doesn't - # check. - it "should be able to sort graphs with duplicate edges" do - one = Puppet::Relationship.new(:a, :b) - @graph.add_edge(one) - two = Puppet::Relationship.new(:a, :b) - @graph.add_edge(two) - proc { @graph.topsort }.should_not raise_error - end end describe "when writing dot files" do before do @graph = Puppet::SimpleGraph.new @name = :test @file = File.join(Puppet[:graphdir], @name.to_s + ".dot") end it "should only write when graphing is enabled" do File.expects(:open).with(@file).never Puppet[:graph] = false @graph.write_graph(@name) end it "should write a dot file based on the passed name" do File.expects(:open).with(@file, "w").yields(stub("file", :puts => nil)) @graph.expects(:to_dot).with("name" => @name.to_s.capitalize) Puppet[:graph] = true @graph.write_graph(@name) end after do Puppet.settings.clear end end describe Puppet::SimpleGraph do before do @graph = Puppet::SimpleGraph.new end it "should correctly clear vertices and edges when asked" do @graph.add_edge("a", "b") @graph.add_vertex "c" @graph.clear @graph.vertices.should be_empty @graph.edges.should be_empty end end describe "when matching edges" do before do @graph = Puppet::SimpleGraph.new @event = Puppet::Transaction::Event.new(:name => :yay, :resource => "a") @none = Puppet::Transaction::Event.new(:name => :NONE, :resource => "a") @edges = {} @edges["a/b"] = Puppet::Relationship.new("a", "b", {:event => :yay, :callback => :refresh}) @edges["a/c"] = Puppet::Relationship.new("a", "c", {:event => :yay, :callback => :refresh}) @graph.add_edge(@edges["a/b"]) end it "should match edges whose source matches the source of the event" do @graph.matching_edges(@event).should == [@edges["a/b"]] end it "should match always match nothing when the event is :NONE" do @graph.matching_edges(@none).should be_empty end it "should match multiple edges" do @graph.add_edge(@edges["a/c"]) edges = @graph.matching_edges(@event) edges.should be_include(@edges["a/b"]) edges.should be_include(@edges["a/c"]) end end describe "when determining dependencies" do before do @graph = Puppet::SimpleGraph.new @graph.add_edge("a", "b") @graph.add_edge("a", "c") @graph.add_edge("b", "d") end it "should find all dependents when they are on multiple levels" do @graph.dependents("a").sort.should == %w{b c d}.sort end it "should find single dependents" do @graph.dependents("b").sort.should == %w{d}.sort end it "should return an empty array when there are no dependents" do @graph.dependents("c").sort.should == [].sort end it "should find all dependencies when they are on multiple levels" do @graph.dependencies("d").sort.should == %w{a b} end it "should find single dependencies" do @graph.dependencies("c").sort.should == %w{a} end it "should return an empty array when there are no dependencies" do @graph.dependencies("a").sort.should == [] end end require 'puppet/util/graph' - class Container + class Container < Puppet::Type::Component include Puppet::Util::Graph include Enumerable attr_accessor :name def each @children.each do |c| yield c end end def initialize(name, ary) @name = name @children = ary end def push(*ary) ary.each { |c| @children.push(c)} end def to_s @name end end + require "puppet/resource/catalog" describe "when splicing the graph" do def container_graph @one = Container.new("one", %w{a b}) @two = Container.new("two", ["c", "d"]) @three = Container.new("three", ["i", "j"]) @middle = Container.new("middle", ["e", "f", @two]) @top = Container.new("top", ["g", "h", @middle, @one, @three]) @empty = Container.new("empty", []) @whit = Puppet::Type.type(:whit) @stage = Puppet::Type.type(:stage).new(:name => "foo") - @contgraph = @top.to_graph + @contgraph = @top.to_graph(Puppet::Resource::Catalog.new) # We have to add the container to the main graph, else it won't # be spliced in the dependency graph. @contgraph.add_vertex(@empty) end + def containers + @contgraph.vertices.select { |x| !x.is_a? String } + end + + def contents_of(x) + @contgraph.direct_dependents_of(x) + end + def dependency_graph @depgraph = Puppet::SimpleGraph.new @contgraph.vertices.each do |v| @depgraph.add_vertex(v) end # We have to specify a relationship to our empty container, else it # never makes it into the dep graph in the first place. - {@one => @two, "f" => "c", "h" => @middle, "c" => @empty}.each do |source, target| + @explicit_dependencies = {@one => @two, "f" => "c", "h" => @middle, "c" => @empty} + @explicit_dependencies.each do |source, target| @depgraph.add_edge(source, target, :callback => :refresh) end end def splice - @depgraph.splice!(@contgraph, Container) + @contgraph.splice!(@depgraph) + end + + def whit_called(name) + x = @depgraph.vertices.find { |v| v.is_a?(@whit) && v.name =~ /#{name}/ } + x.should_not be_nil + def x.to_s + "Whit[#{name}]" + end + def x.inspect + to_s + end + x + end + + def admissible_sentinal_of(x) + @depgraph.vertex?(x) ? x : whit_called("admissible_#{x.name}") + end + + def completed_sentinal_of(x) + @depgraph.vertex?(x) ? x : whit_called("completed_#{x.name}") end before do container_graph dependency_graph splice end - # This is the real heart of splicing -- replacing all containers in - # our relationship and exploding their relationships so that each - # relationship to a container gets copied to all of its children. + # This is the real heart of splicing -- replacing all containers X in our + # relationship graph with a pair of whits { admissible_X and completed_X } + # such that that + # + # 0) completed_X depends on admissible_X + # 1) contents of X each depend on admissible_X + # 2) completed_X depends on each on the contents of X + # 3) everything which depended on X depends on completed_X + # 4) admissible_X depends on everything X depended on + # 5) the containers and their edges must be removed + # + # Note that this requires attention to the possible case of containers + # which contain or depend on other containers. + # + # Point by point: + + # 0) completed_X depends on admissible_X + # + it "every container's completed sentinal should depend on its admissible sentinal" do + containers.each { |container| + @depgraph.path_between(admissible_sentinal_of(container),completed_sentinal_of(container)).should be + } + end + + # 1) contents of X each depend on admissible_X + # + it "all contained objects should depend on their container's admissible sentinal" do + containers.each { |container| + contents_of(container).each { |leaf| + @depgraph.should be_edge(admissible_sentinal_of(container),admissible_sentinal_of(leaf)) + } + } + end + + # 2) completed_X depends on each on the contents of X + # + it "completed sentinals should depend on their container's contents" do + containers.each { |container| + contents_of(container).each { |leaf| + @depgraph.should be_edge(completed_sentinal_of(leaf),completed_sentinal_of(container)) + } + } + end + + # + # 3) everything which depended on X depends on completed_X + + # + # 4) admissible_X depends on everything X depended on + + # 5) the containers and their edges must be removed + # it "should remove all Container objects from the dependency graph" do @depgraph.vertices.find_all { |v| v.is_a?(Container) }.should be_empty end - # This is a bit hideous, but required to make stages work with relationships - they're - # the top of the graph. it "should remove all Stage resources from the dependency graph" do @depgraph.vertices.find_all { |v| v.is_a?(Puppet::Type.type(:stage)) }.should be_empty end - it "should add container relationships to contained objects" do - @contgraph.leaves(@middle).each do |leaf| - @depgraph.should be_edge("h", leaf) - end - end - - it "should explode container-to-container relationships, making edges between all respective contained objects" do - @one.each do |oobj| - @two.each do |tobj| - @depgraph.should be_edge(oobj, tobj) - end - end - end - - it "should contain a whit-resource to mark the place held by the empty container" do - @depgraph.vertices.find_all { |v| v.is_a?(@whit) }.length.should == 1 - end - - it "should replace edges to empty containers with edges to their residual whit" do - emptys_whit = @depgraph.vertices.find_all { |v| v.is_a?(@whit) }.first - @depgraph.should be_edge("c", emptys_whit) - end - it "should no longer contain anything but the non-container objects" do @depgraph.vertices.find_all { |v| ! v.is_a?(String) and ! v.is_a?(@whit)}.should be_empty end - it "should copy labels" do - @depgraph.edges.each do |edge| - edge.label.should == {:callback => :refresh} - end + it "should retain labels on non-containment edges" do + @explicit_dependencies.each { |f,t| + @depgraph.edges_between(completed_sentinal_of(f),admissible_sentinal_of(t))[0].label.should == {:callback => :refresh} + } end it "should not add labels to edges that have none" do @depgraph.add_edge(@two, @three) splice - @depgraph.edges_between("c", "i")[0].label.should == {} + @depgraph.path_between("c", "i").any? {|segment| segment.all? {|e| e.label == {} }}.should be end it "should copy labels over edges that have none" do @depgraph.add_edge("c", @three, {:callback => :refresh}) splice # And make sure the label got copied. - @depgraph.edges_between("c", "i")[0].label.should == {:callback => :refresh} + @depgraph.path_between("c", "i").flatten.select {|e| e.label == {:callback => :refresh} }.should_not be_empty end it "should not replace a label with a nil label" do # Lastly, add some new label-less edges and make sure the label stays. @depgraph.add_edge(@middle, @three) @depgraph.add_edge("c", @three, {:callback => :refresh}) splice - @depgraph.edges_between("c", "i")[0].label.should == {:callback => :refresh} + @depgraph.path_between("c","i").flatten.select {|e| e.label == {:callback => :refresh} }.should_not be_empty end it "should copy labels to all created edges" do @depgraph.add_edge(@middle, @three) @depgraph.add_edge("c", @three, {:callback => :refresh}) splice @three.each do |child| edge = Puppet::Relationship.new("c", child) - @depgraph.should be_edge(edge.source, edge.target) - @depgraph.edges_between(edge.source, edge.target)[0].label.should == {:callback => :refresh} + (path = @depgraph.path_between(edge.source, edge.target)).should be + path.should_not be_empty + path.flatten.select {|e| e.label == {:callback => :refresh} }.should_not be_empty end end end it "should serialize to YAML using the old format by default" do Puppet::SimpleGraph.use_new_yaml_format.should == false end describe "(yaml tests)" do def empty_graph(graph) end def one_vertex_graph(graph) graph.add_vertex(:a) end def graph_without_edges(graph) [:a, :b, :c].each { |x| graph.add_vertex(x) } end def one_edge_graph(graph) graph.add_edge(:a, :b) end def many_edge_graph(graph) graph.add_edge(:a, :b) graph.add_edge(:a, :c) graph.add_edge(:b, :d) graph.add_edge(:c, :d) end def labeled_edge_graph(graph) graph.add_edge(:a, :b, :callback => :foo, :event => :bar) end def overlapping_edge_graph(graph) graph.add_edge(:a, :b, :callback => :foo, :event => :bar) graph.add_edge(:a, :b, :callback => :biz, :event => :baz) end def self.all_test_graphs [:empty_graph, :one_vertex_graph, :graph_without_edges, :one_edge_graph, :many_edge_graph, :labeled_edge_graph, :overlapping_edge_graph] end def object_ids(enumerable) # Return a sorted list of the object id's of the elements of an # enumerable. enumerable.collect { |x| x.object_id }.sort end def graph_to_yaml(graph, which_format) previous_use_new_yaml_format = Puppet::SimpleGraph.use_new_yaml_format Puppet::SimpleGraph.use_new_yaml_format = (which_format == :new) ZAML.dump(graph) ensure Puppet::SimpleGraph.use_new_yaml_format = previous_use_new_yaml_format end # Test serialization of graph to YAML. [:old, :new].each do |which_format| all_test_graphs.each do |graph_to_test| it "should be able to serialize #{graph_to_test} to YAML (#{which_format} format)" do graph = Puppet::SimpleGraph.new send(graph_to_test, graph) yaml_form = graph_to_yaml(graph, which_format) # Hack the YAML so that objects in the Puppet namespace get # changed to YAML::DomainType objects. This lets us inspect # the serialized objects easily without invoking any # yaml_initialize hooks. yaml_form.gsub!('!ruby/object:Puppet::', '!hack/object:Puppet::') serialized_object = YAML.load(yaml_form) # Check that the object contains instance variables @edges and # @vertices only. @reversal is also permitted, but we don't # check it, because it is going to be phased out. serialized_object.type_id.should == 'object:Puppet::SimpleGraph' serialized_object.value.keys.reject { |x| x == 'reversal' }.sort.should == ['edges', 'vertices'] # Check edges by forming a set of tuples (source, target, # callback, event) based on the graph and the YAML and make sure # they match. edges = serialized_object.value['edges'] edges.should be_a(Array) expected_edge_tuples = graph.edges.collect { |edge| [edge.source, edge.target, edge.callback, edge.event] } actual_edge_tuples = edges.collect do |edge| edge.type_id.should == 'object:Puppet::Relationship' %w{source target}.each { |x| edge.value.keys.should include(x) } edge.value.keys.each { |x| ['source', 'target', 'callback', 'event'].should include(x) } %w{source target callback event}.collect { |x| edge.value[x] } end Set.new(actual_edge_tuples).should == Set.new(expected_edge_tuples) actual_edge_tuples.length.should == expected_edge_tuples.length # Check vertices one by one. vertices = serialized_object.value['vertices'] if which_format == :old vertices.should be_a(Hash) Set.new(vertices.keys).should == Set.new(graph.vertices) vertices.each do |key, value| value.type_id.should == 'object:Puppet::SimpleGraph::VertexWrapper' value.value.keys.sort.should == %w{adjacencies vertex} value.value['vertex'].should equal(key) adjacencies = value.value['adjacencies'] adjacencies.should be_a(Hash) Set.new(adjacencies.keys).should == Set.new([:in, :out]) [:in, :out].each do |direction| adjacencies[direction].should be_a(Hash) expected_adjacent_vertices = Set.new(graph.adjacent(key, :direction => direction, :type => :vertices)) Set.new(adjacencies[direction].keys).should == expected_adjacent_vertices adjacencies[direction].each do |adj_key, adj_value| # Since we already checked edges, just check consistency # with edges. desired_source = direction == :in ? adj_key : key desired_target = direction == :in ? key : adj_key expected_edges = edges.select do |edge| edge.value['source'] == desired_source && edge.value['target'] == desired_target end adj_value.should be_a(Set) if object_ids(adj_value) != object_ids(expected_edges) raise "For vertex #{key.inspect}, direction #{direction.inspect}: expected adjacencies #{expected_edges.inspect} but got #{adj_value.inspect}" end end end end else vertices.should be_a(Array) Set.new(vertices).should == Set.new(graph.vertices) vertices.length.should == graph.vertices.length end end end # Test deserialization of graph from YAML. This presumes the # correctness of serialization to YAML, which has already been # tested. all_test_graphs.each do |graph_to_test| it "should be able to deserialize #{graph_to_test} from YAML (#{which_format} format)" do reference_graph = Puppet::SimpleGraph.new send(graph_to_test, reference_graph) yaml_form = graph_to_yaml(reference_graph, which_format) recovered_graph = YAML.load(yaml_form) # Test that the recovered vertices match the vertices in the # reference graph. expected_vertices = reference_graph.vertices.to_a recovered_vertices = recovered_graph.vertices.to_a Set.new(recovered_vertices).should == Set.new(expected_vertices) recovered_vertices.length.should == expected_vertices.length # Test that the recovered edges match the edges in the # reference graph. expected_edge_tuples = reference_graph.edges.collect do |edge| [edge.source, edge.target, edge.callback, edge.event] end recovered_edge_tuples = recovered_graph.edges.collect do |edge| [edge.source, edge.target, edge.callback, edge.event] end Set.new(recovered_edge_tuples).should == Set.new(expected_edge_tuples) recovered_edge_tuples.length.should == expected_edge_tuples.length # We ought to test that the recovered graph is self-consistent # too. But we're not going to bother with that yet because # the internal representation of the graph is about to change. end end it "should be able to serialize a graph where the vertices contain backreferences to the graph (#{which_format} format)" do reference_graph = Puppet::SimpleGraph.new vertex = Object.new vertex.instance_eval { @graph = reference_graph } reference_graph.add_edge(vertex, :other_vertex) yaml_form = graph_to_yaml(reference_graph, which_format) recovered_graph = YAML.load(yaml_form) recovered_graph.vertices.length.should == 2 recovered_vertex = recovered_graph.vertices.reject { |x| x.is_a?(Symbol) }[0] recovered_vertex.instance_eval { @graph }.should equal(recovered_graph) recovered_graph.edges.length.should == 1 recovered_edge = recovered_graph.edges[0] recovered_edge.source.should equal(recovered_vertex) recovered_edge.target.should == :other_vertex end end it "should serialize properly when used as a base class" do class Puppet::TestDerivedClass < Puppet::SimpleGraph attr_accessor :foo end derived = Puppet::TestDerivedClass.new derived.add_edge(:a, :b) derived.foo = 1234 recovered_derived = YAML.load(YAML.dump(derived)) recovered_derived.class.should equal(Puppet::TestDerivedClass) recovered_derived.edges.length.should == 1 recovered_derived.edges[0].source.should == :a recovered_derived.edges[0].target.should == :b recovered_derived.vertices.length.should == 2 recovered_derived.foo.should == 1234 end end end diff --git a/spec/unit/transaction_spec.rb b/spec/unit/transaction_spec.rb index 8d40136fb..ab76130e3 100755 --- a/spec/unit/transaction_spec.rb +++ b/spec/unit/transaction_spec.rb @@ -1,441 +1,441 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') require 'puppet/transaction' def without_warnings flag = $VERBOSE $VERBOSE = nil yield $VERBOSE = flag end describe Puppet::Transaction do before do @basepath = Puppet.features.posix? ? "/what/ever" : "C:/tmp" @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) end it "should delegate its event list to the event manager" do @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @transaction.event_manager.expects(:events).returns %w{my events} @transaction.events.should == %w{my events} end it "should delegate adding times to its report" do @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @transaction.report.expects(:add_times).with(:foo, 10) @transaction.report.expects(:add_times).with(:bar, 20) @transaction.add_times :foo => 10, :bar => 20 end it "should be able to accept resource status instances" do resource = Puppet::Type.type(:notify).new :title => "foobar" status = Puppet::Resource::Status.new(resource) @transaction.add_resource_status(status) @transaction.resource_status(resource).should equal(status) end it "should be able to look resource status up by resource reference" do resource = Puppet::Type.type(:notify).new :title => "foobar" status = Puppet::Resource::Status.new(resource) @transaction.add_resource_status(status) @transaction.resource_status(resource.to_s).should equal(status) end # This will basically only ever be used during testing. it "should automatically create resource statuses if asked for a non-existent status" do resource = Puppet::Type.type(:notify).new :title => "foobar" @transaction.resource_status(resource).should be_instance_of(Puppet::Resource::Status) end it "should add provided resource statuses to its report" do resource = Puppet::Type.type(:notify).new :title => "foobar" status = Puppet::Resource::Status.new(resource) @transaction.add_resource_status(status) @transaction.report.resource_statuses[resource.to_s].should equal(status) end it "should consider a resource to be failed if a status instance exists for that resource and indicates it is failed" do resource = Puppet::Type.type(:notify).new :name => "yayness" status = Puppet::Resource::Status.new(resource) status.failed = "some message" @transaction.add_resource_status(status) @transaction.should be_failed(resource) end it "should not consider a resource to be failed if a status instance exists for that resource but indicates it is not failed" do resource = Puppet::Type.type(:notify).new :name => "yayness" status = Puppet::Resource::Status.new(resource) @transaction.add_resource_status(status) @transaction.should_not be_failed(resource) end it "should consider there to be failed resources if any statuses are marked failed" do resource = Puppet::Type.type(:notify).new :name => "yayness" status = Puppet::Resource::Status.new(resource) status.failed = "some message" @transaction.add_resource_status(status) @transaction.should be_any_failed end it "should not consider there to be failed resources if no statuses are marked failed" do resource = Puppet::Type.type(:notify).new :name => "yayness" status = Puppet::Resource::Status.new(resource) @transaction.add_resource_status(status) @transaction.should_not be_any_failed end it "should be possible to replace the report object" do report = Puppet::Transaction::Report.new("apply") @transaction.report = report @transaction.report.should == report end it "should consider a resource to have failed dependencies if any of its dependencies are failed" describe "when initializing" do it "should create an event manager" do @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @transaction.event_manager.should be_instance_of(Puppet::Transaction::EventManager) @transaction.event_manager.transaction.should equal(@transaction) end it "should create a resource harness" do @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @transaction.resource_harness.should be_instance_of(Puppet::Transaction::ResourceHarness) @transaction.resource_harness.transaction.should equal(@transaction) end end describe "when evaluating a resource" do before do @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @transaction.stubs(:skip?).returns false @resource = Puppet::Type.type(:file).new :path => @basepath end it "should check whether the resource should be skipped" do @transaction.expects(:skip?).with(@resource).returns false @transaction.eval_resource(@resource) end it "should process events" do @transaction.event_manager.expects(:process_events).with(@resource) @transaction.eval_resource(@resource) end describe "and the resource should be skipped" do before do @transaction.expects(:skip?).with(@resource).returns true end it "should mark the resource's status as skipped" do @transaction.eval_resource(@resource) @transaction.resource_status(@resource).should be_skipped end end end describe "when applying a resource" do before do @resource = Puppet::Type.type(:file).new :path => @basepath @status = Puppet::Resource::Status.new(@resource) @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @transaction.event_manager.stubs(:queue_events) @transaction.resource_harness.stubs(:evaluate).returns(@status) end it "should use its resource harness to apply the resource" do @transaction.resource_harness.expects(:evaluate).with(@resource) @transaction.apply(@resource) end it "should add the resulting resource status to its status list" do @transaction.apply(@resource) @transaction.resource_status(@resource).should be_instance_of(Puppet::Resource::Status) end it "should queue any events added to the resource status" do @status.expects(:events).returns %w{a b} @transaction.event_manager.expects(:queue_events).with(@resource, ["a", "b"]) @transaction.apply(@resource) end it "should log and skip any resources that cannot be applied" do @transaction.resource_harness.expects(:evaluate).raises ArgumentError @resource.expects(:err) @transaction.apply(@resource) @transaction.report.resource_statuses[@resource.to_s].should be_nil end end describe "when generating resources" do it "should call 'generate' on all created resources" do first = Puppet::Type.type(:notify).new(:name => "first") second = Puppet::Type.type(:notify).new(:name => "second") third = Puppet::Type.type(:notify).new(:name => "third") @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) first.expects(:generate).returns [second] second.expects(:generate).returns [third] third.expects(:generate) @transaction.generate_additional_resources(first) end it "should finish all resources" do generator = stub 'generator', :depthfirst? => true, :tags => [] resource = stub 'resource', :tag => nil @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) generator.expects(:generate).returns [resource] @catalog.expects(:add_resource).yields(resource) resource.expects(:finish) @transaction.generate_additional_resources(generator) end it "should skip generated resources that conflict with existing resources" do generator = mock 'generator', :tags => [] resource = stub 'resource', :tag => nil @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) generator.expects(:generate).returns [resource] @catalog.expects(:add_resource).raises(Puppet::Resource::Catalog::DuplicateResourceError.new("foo")) resource.expects(:finish).never resource.expects(:info) # log that it's skipped - @transaction.generate_additional_resources(generator).should be_empty + @transaction.generate_additional_resources(generator) end it "should copy all tags to the newly generated resources" do child = stub 'child' generator = stub 'resource', :tags => ["one", "two"] @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) generator.stubs(:generate).returns [child] @catalog.stubs(:add_resource) child.expects(:tag).with("one", "two") child.expects(:finish) generator.expects(:depthfirst?) @transaction.generate_additional_resources(generator) end end describe "when skipping a resource" do before :each do @resource = Puppet::Type.type(:notify).new :name => "foo" @catalog = Puppet::Resource::Catalog.new @resource.catalog = @catalog @transaction = Puppet::Transaction.new(@catalog) end it "should skip resource with missing tags" do @transaction.stubs(:missing_tags?).returns(true) @transaction.should be_skip(@resource) end it "should skip unscheduled resources" do @transaction.stubs(:scheduled?).returns(false) @transaction.should be_skip(@resource) end it "should skip resources with failed dependencies" do @transaction.stubs(:failed_dependencies?).returns(true) @transaction.should be_skip(@resource) end it "should skip virtual resource" do @resource.stubs(:virtual?).returns true @transaction.should be_skip(@resource) end end describe "when determining if tags are missing" do before :each do @resource = Puppet::Type.type(:notify).new :name => "foo" @catalog = Puppet::Resource::Catalog.new @resource.catalog = @catalog @transaction = Puppet::Transaction.new(@catalog) @transaction.stubs(:ignore_tags?).returns false end it "should not be missing tags if tags are being ignored" do @transaction.expects(:ignore_tags?).returns true @resource.expects(:tagged?).never @transaction.should_not be_missing_tags(@resource) end it "should not be missing tags if the transaction tags are empty" do @transaction.tags = [] @resource.expects(:tagged?).never @transaction.should_not be_missing_tags(@resource) end it "should otherwise let the resource determine if it is missing tags" do tags = ['one', 'two'] @transaction.tags = tags @resource.expects(:tagged?).with(*tags).returns(false) @transaction.should be_missing_tags(@resource) end end describe "when determining if a resource should be scheduled" do before :each do @resource = Puppet::Type.type(:notify).new :name => "foo" @catalog = Puppet::Resource::Catalog.new @resource.catalog = @catalog @transaction = Puppet::Transaction.new(@catalog) end it "should always schedule resources if 'ignoreschedules' is set" do @transaction.ignoreschedules = true @transaction.resource_harness.expects(:scheduled?).never @transaction.should be_scheduled(@resource) end it "should let the resource harness determine whether the resource should be scheduled" do @transaction.resource_harness.expects(:scheduled?).with(@transaction.resource_status(@resource), @resource).returns "feh" @transaction.scheduled?(@resource).should == "feh" end end describe "when prefetching" do it "should match resources by name, not title" do @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) # Have both a title and name resource = Puppet::Type.type(:sshkey).create :title => "foo", :name => "bar", :type => :dsa, :key => "eh" @catalog.add_resource resource resource.provider.class.expects(:prefetch).with("bar" => resource) @transaction.prefetch end end it "should return all resources for which the resource status indicates the resource has changed when determinig changed resources" do @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) names = [] 2.times do |i| name = File.join(@basepath, "file#{i}") resource = Puppet::Type.type(:file).new :path => name names << resource.to_s @catalog.add_resource resource @transaction.add_resource_status Puppet::Resource::Status.new(resource) end @transaction.resource_status(names[0]).changed = true @transaction.changed?.should == [@catalog.resource(names[0])] end describe 'when checking application run state' do before do without_warnings { Puppet::Application = Class.new(Puppet::Application) } @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) end after do without_warnings { Puppet::Application = Puppet::Application.superclass } end it 'should return true for :stop_processing? if Puppet::Application.stop_requested? is true' do Puppet::Application.stubs(:stop_requested?).returns(true) @transaction.stop_processing?.should be_true end it 'should return false for :stop_processing? if Puppet::Application.stop_requested? is false' do Puppet::Application.stubs(:stop_requested?).returns(false) @transaction.stop_processing?.should be_false end describe 'within an evaluate call' do before do @resource = Puppet::Type.type(:notify).new :title => "foobar" @catalog.add_resource @resource @transaction.stubs(:prepare) end it 'should stop processing if :stop_processing? is true' do @transaction.stubs(:stop_processing?).returns(true) @transaction.expects(:eval_resource).never @transaction.evaluate end it 'should continue processing if :stop_processing? is false' do @transaction.stubs(:stop_processing?).returns(false) @transaction.expects(:eval_resource).returns(nil) @transaction.evaluate end end end end describe Puppet::Transaction, " when determining tags" do before do @config = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@config) end it "should default to the tags specified in the :tags setting" do Puppet.expects(:[]).with(:tags).returns("one") @transaction.tags.should == %w{one} end it "should split tags based on ','" do Puppet.expects(:[]).with(:tags).returns("one,two") @transaction.tags.should == %w{one two} end it "should use any tags set after creation" do Puppet.expects(:[]).with(:tags).never @transaction.tags = %w{one two} @transaction.tags.should == %w{one two} end it "should always convert assigned tags to an array" do @transaction.tags = "one::two" @transaction.tags.should == %w{one::two} end it "should accept a comma-delimited string" do @transaction.tags = "one, two" @transaction.tags.should == %w{one two} end it "should accept an empty string" do @transaction.tags = "" @transaction.tags.should == [] end end diff --git a/spec/unit/type/whit_spec.rb b/spec/unit/type/whit_spec.rb index 46eb0ab6e..0a3324afa 100644 --- a/spec/unit/type/whit_spec.rb +++ b/spec/unit/type/whit_spec.rb @@ -1,11 +1,11 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') whit = Puppet::Type.type(:whit).new(:name => "Foo::Bar") describe whit do - it "should stringify as though it were the class it represents" do - whit.to_s.should == "Class[Foo::Bar]" + it "should stringify in a way that users will regognise" do + whit.to_s.should == "(Foo::Bar)" end end diff --git a/test/lib/puppettest/support/assertions.rb b/test/lib/puppettest/support/assertions.rb index 31fa3f1da..758c126ce 100644 --- a/test/lib/puppettest/support/assertions.rb +++ b/test/lib/puppettest/support/assertions.rb @@ -1,64 +1,69 @@ require 'puppettest/support/utils' require 'fileutils' module PuppetTest include PuppetTest::Support::Utils def assert_logged(level, regex, msg = nil) # Skip verifying logs that we're not supposed to send. return unless Puppet::Util::Log.sendlevel?(level) r = @logs.detect { |l| l.level == level and l.message =~ regex } @logs.clear assert(r, msg) end def assert_uid_gid(uid, gid, filename) flunk "Must be uid 0 to run these tests" unless Process.uid == 0 fork do Puppet::Util::SUIDManager.gid = gid Puppet::Util::SUIDManager.uid = uid # FIXME: use the tempfile method from puppettest.rb system("mkfifo #{filename}") f = File.open(filename, "w") f << "#{Puppet::Util::SUIDManager.uid}\n#{Puppet::Util::SUIDManager.gid}\n" yield if block_given? end # avoid a race. true while !File.exists? filename f = File.open(filename, "r") a = f.readlines assert_equal(uid, a[0].chomp.to_i, "UID was incorrect") assert_equal(gid, a[1].chomp.to_i, "GID was incorrect") FileUtils.rm(filename) end def assert_events(events, *resources) trans = nil comp = nil msg = nil raise Puppet::DevError, "Incorrect call of assert_events" unless events.is_a? Array msg = resources.pop if resources[-1].is_a? String config = resources2catalog(*resources) transaction = Puppet::Transaction.new(config) - run_events(:evaluate, transaction, events, msg) + transaction.evaluate + newevents = transaction.events. + reject { |e| ['failure', 'audit'].include? e.status }. + collect { |e| e.name } + + assert_equal(events, newevents, "Incorrect evaluate #{msg} events") transaction end # A simpler method that just applies what we have. def assert_apply(*resources) config = resources2catalog(*resources) events = nil assert_nothing_raised("Failed to evaluate") { events = config.apply.events } events end end diff --git a/test/lib/puppettest/support/utils.rb b/test/lib/puppettest/support/utils.rb index bca5d9634..4ecc3819e 100644 --- a/test/lib/puppettest/support/utils.rb +++ b/test/lib/puppettest/support/utils.rb @@ -1,160 +1,141 @@ module PuppetTest::Support end module PuppetTest::Support::Utils def gcdebug(type) Puppet.warning "#{type}: #{ObjectSpace.each_object(type) { |o| }}" end def basedir(*list) unless defined? @@basedir Dir.chdir(File.dirname(__FILE__)) do @@basedir = File.dirname(File.dirname(File.dirname(File.dirname(Dir.getwd)))) end end if list.empty? @@basedir else File.join(@@basedir, *list) end end def fakedata(dir,pat='*') glob = "#{basedir}/test/#{dir}/#{pat}" files = Dir.glob(glob,File::FNM_PATHNAME) raise Puppet::DevError, "No fakedata matching #{glob}" if files.empty? files end def datadir(*list) File.join(basedir, "test", "data", *list) end # # TODO: I think this method needs to be renamed to something a little more # explanatory. # def newobj(type, name, hash) transport = Puppet::TransObject.new(name, "file") transport[:path] = path transport[:ensure] = "file" assert_nothing_raised { file = transport.to_ral } end # Turn a list of resources, or possibly a catalog and some resources, # into a catalog object. def resources2catalog(*resources) if resources[0].is_a?(Puppet::Resource::Catalog) config = resources.shift resources.each { |r| config.add_resource r } unless resources.empty? elsif resources[0].is_a?(Puppet::Type.type(:component)) raise ArgumentError, "resource2config() no longer accpts components" comp = resources.shift comp.delve else config = Puppet::Resource::Catalog.new resources.each { |res| config.add_resource res } end config end # TODO: rewrite this to use the 'etc' module. # Define a variable that contains the name of my user. def setme # retrieve the user name id = %x{id}.chomp if id =~ /uid=\d+\(([^\)]+)\)/ @me = $1 else puts id end raise "Could not retrieve user name; 'id' did not work" unless defined?(@me) end # Define a variable that contains a group I'm in. def set_mygroup # retrieve the user name group = %x{groups}.chomp.split(/ /)[0] raise "Could not find group to set in @mygroup" unless group @mygroup = group end - def run_events(type, trans, events, msg) - case type - when :evaluate, :rollback # things are hunky-dory - else - raise Puppet::DevError, "Incorrect run_events type" - end - - method = type - - trans.send(method) - newevents = trans.events.reject { |e| ['failure', 'audit'].include? e.status }.collect { |e| - e.name - } - - assert_equal(events, newevents, "Incorrect #{type} #{msg} events") - - trans - end - def fakefile(name) ary = [basedir, "test"] ary += name.split("/") file = File.join(ary) raise Puppet::DevError, "No fakedata file #{file}" unless FileTest.exists?(file) file end # wrap how to retrieve the masked mode def filemode(file) File.stat(file).mode & 007777 end def memory Puppet::Util.memory end # a list of files that we can parse for testing def textfiles textdir = datadir "snippets" Dir.entries(textdir).reject { |f| f =~ /^\./ or f =~ /fail/ }.each { |f| yield File.join(textdir, f) } end def failers textdir = datadir "failers" # only parse this one file now files = Dir.entries(textdir).reject { |file| file =~ %r{\.swp} }.reject { |file| file =~ %r{\.disabled} }.collect { |file| File.join(textdir,file) }.find_all { |file| FileTest.file?(file) }.sort.each { |file| Puppet.debug "Processing #{file}" yield file } end def mk_catalog(*resources) if resources[0].is_a?(String) name = resources.shift else name = :testing end config = Puppet::Resource::Catalog.new :testing do |conf| resources.each { |resource| conf.add_resource resource } end config end end diff --git a/test/other/relationships.rb b/test/other/relationships.rb index 717353c02..e36dcda71 100755 --- a/test/other/relationships.rb +++ b/test/other/relationships.rb @@ -1,98 +1,91 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../lib/puppettest') require 'puppet' require 'puppettest' class TestRelationships < Test::Unit::TestCase include PuppetTest def setup super Puppet::Type.type(:exec) end def newfile assert_nothing_raised { - - return Puppet::Type.type(:file).new( - + return Puppet::Type.type(:file).new( :path => tempfile, - :check => [:mode, :owner, :group] ) } end def check_relationship(sources, targets, out, refresher) if out deps = sources.builddepends sources = [sources] else deps = targets.builddepends targets = [targets] end assert_instance_of(Array, deps) assert(! deps.empty?, "Did not receive any relationships") deps.each do |edge| assert_instance_of(Puppet::Relationship, edge) end sources.each do |source| targets.each do |target| edge = deps.find { |e| e.source == source and e.target == target } assert(edge, "Could not find edge for #{source.ref} => #{target.ref}") if refresher assert_equal(:ALL_EVENTS, edge.event) assert_equal(:refresh, edge.callback) else assert_nil(edge.event) assert_nil(edge.callback, "Got a callback with no events") end end end end def test_autorequire # We know that execs autorequire their cwd, so we'll use that path = tempfile - - - file = Puppet::Type.type(:file).new( - :title => "myfile", :path => path, - - :ensure => :directory) - - exec = Puppet::Type.newexec( - :title => "myexec", :cwd => path, - - :command => "/bin/echo") - + file = Puppet::Type.type(:file).new( + :title => "myfile", :path => path, + :ensure => :directory + ) + exec = Puppet::Type.newexec( + :title => "myexec", :cwd => path, + :command => "/bin/echo" + ) catalog = mk_catalog(file, exec) reqs = nil assert_nothing_raised do reqs = exec.autorequire end assert_instance_of(Puppet::Relationship, reqs[0], "Did not return a relationship edge") assert_equal(file, reqs[0].source, "Did not set the autorequire source correctly") assert_equal(exec, reqs[0].target, "Did not set the autorequire target correctly") # Now make sure that these relationships are added to the # relationship graph catalog.apply do |trans| - assert(catalog.relationship_graph.edge?(file, exec), "autorequire edge was not created") + assert(catalog.relationship_graph.path_between(file, exec), "autorequire edge was not created") end end # Testing #411. It was a problem with builddepends. def test_missing_deps file = Puppet::Type.type(:file).new :path => tempfile, :require => Puppet::Resource.new("file", "/no/such/file") assert_raise(Puppet::Error) do file.builddepends end end end diff --git a/test/other/transactions.rb b/test/other/transactions.rb index be8cef483..812e519ab 100755 --- a/test/other/transactions.rb +++ b/test/other/transactions.rb @@ -1,434 +1,401 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../lib/puppettest') require 'mocha' require 'puppet' require 'puppettest' require 'puppettest/support/resources' require 'puppettest/support/utils' class TestTransactions < Test::Unit::TestCase include PuppetTest::FileTesting include PuppetTest::Support::Resources include PuppetTest::Support::Utils class Fakeprop true) def finish $finished << self.name end end type.class_eval(&block) if block cleanup do Puppet::Type.rmtype(:generator) end type end # Create a new type that generates instances with shorter names. def mkreducer(&block) type = mkgenerator do def eval_generate ret = [] if title.length > 1 ret << self.class.new(:title => title[0..-2]) else return nil end ret end end type.class_eval(&block) if block type end def test_prefetch # Create a type just for testing prefetch name = :prefetchtesting $prefetched = false type = Puppet::Type.newtype(name) do newparam(:name) {} end cleanup do Puppet::Type.rmtype(name) end # Now create a provider type.provide(:prefetch) do def self.prefetch(resources) $prefetched = resources end end # Now create an instance inst = type.new :name => "yay" # Create a transaction trans = Puppet::Transaction.new(mk_catalog(inst)) # Make sure prefetch works assert_nothing_raised do trans.prefetch end assert_equal({inst.title => inst}, $prefetched, "type prefetch was not called") # Now make sure it gets called from within evaluate $prefetched = false assert_nothing_raised do trans.evaluate end assert_equal({inst.title => inst}, $prefetched, "evaluate did not call prefetch") end - # We need to generate resources before we prefetch them, else generated - # resources that require prefetching don't work. - def test_generate_before_prefetch - config = mk_catalog - trans = Puppet::Transaction.new(config) - - generate = nil - prefetch = nil - trans.expects(:generate).with { |*args| generate = Time.now; true } - trans.expects(:prefetch).with { |*args| ! generate.nil? } - trans.prepare - return - - resource = Puppet::Type.type(:file).new :ensure => :present, :path => tempfile - other_resource = mock 'generated' - def resource.generate - [other_resource] - end - - - config = mk_catalog(yay, rah) - trans = Puppet::Transaction.new(config) - - assert_nothing_raised do - trans.generate - end - - %w{ya ra y r}.each do |name| - assert(trans.catalog.vertex?(Puppet::Type.type(:generator)[name]), "Generated #{name} was not a vertex") - assert($finished.include?(name), "#{name} was not finished") - end - end - def test_ignore_tags? config = Puppet::Resource::Catalog.new config.host_config = true transaction = Puppet::Transaction.new(config) assert(! transaction.ignore_tags?, "Ignoring tags when applying a host catalog") config.host_config = false transaction = Puppet::Transaction.new(config) assert(transaction.ignore_tags?, "Not ignoring tags when applying a non-host catalog") end def test_missing_tags? resource = Puppet::Type.type(:notify).new :title => "foo" resource.stubs(:tagged?).returns true config = Puppet::Resource::Catalog.new # Mark it as a host config so we don't care which test is first config.host_config = true transaction = Puppet::Transaction.new(config) assert(! transaction.missing_tags?(resource), "Considered a resource to be missing tags when none are set") # host catalogs pay attention to tags, no one else does. Puppet[:tags] = "three,four" config.host_config = false transaction = Puppet::Transaction.new(config) assert(! transaction.missing_tags?(resource), "Considered a resource to be missing tags when not running a host catalog") # config.host_config = true transaction = Puppet::Transaction.new(config) assert(! transaction.missing_tags?(resource), "Considered a resource to be missing tags when running a host catalog and all tags are present") transaction = Puppet::Transaction.new(config) resource.stubs :tagged? => false assert(transaction.missing_tags?(resource), "Considered a resource not to be missing tags when running a host catalog and tags are missing") end # Make sure changes in contained files still generate callback events. def test_generated_callbacks dir = tempfile maker = tempfile Dir.mkdir(dir) file = File.join(dir, "file") File.open(file, "w") { |f| f.puts "" } File.chmod(0644, file) File.chmod(0755, dir) # So only the child file causes a change dirobj = Puppet::Type.type(:file).new :mode => "755", :recurse => true, :path => dir exec = Puppet::Type.type(:exec).new :title => "make", :command => "touch #{maker}", :path => ENV['PATH'], :refreshonly => true, :subscribe => dirobj assert_apply(dirobj, exec) assert(FileTest.exists?(maker), "Did not make callback file") end # Testing #401 -- transactions are calling refresh on classes that don't support it. def test_callback_availability $called = [] klass = Puppet::Type.newtype(:norefresh) do newparam(:name, :namevar => true) {} def method_missing(method, *args) $called << method end end cleanup do $called = nil Puppet::Type.rmtype(:norefresh) end file = Puppet::Type.type(:file).new :path => tempfile, :content => "yay" one = klass.new :name => "one", :subscribe => file assert_apply(file, one) assert(! $called.include?(:refresh), "Called refresh when it wasn't set as a method") end # Testing #437 - cyclic graphs should throw failures. def test_fail_on_cycle one = Puppet::Type.type(:exec).new(:name => "/bin/echo one") two = Puppet::Type.type(:exec).new(:name => "/bin/echo two") one[:require] = two two[:require] = one config = mk_catalog(one, two) trans = Puppet::Transaction.new(config) assert_raise(Puppet::Error) do - trans.prepare + trans.evaluate end end def test_errors_during_generation type = Puppet::Type.newtype(:failer) do newparam(:name) {} def eval_generate raise ArgumentError, "Invalid value" end def generate raise ArgumentError, "Invalid value" end end cleanup { Puppet::Type.rmtype(:failer) } obj = type.new(:name => "testing") assert_apply(obj) end def test_self_refresh_causes_triggering type = Puppet::Type.newtype(:refresher, :self_refresh => true) do attr_accessor :refreshed, :testing newparam(:name) {} newproperty(:testing) do def retrieve :eh end def sync # noop :ran_testing end end def refresh @refreshed = true end end cleanup { Puppet::Type.rmtype(:refresher)} obj = type.new(:name => "yay", :testing => "cool") assert(! obj.insync?(obj.retrieve), "fake object is already in sync") # Now make sure it gets refreshed when the change happens assert_apply(obj) assert(obj.refreshed, "object was not refreshed during transaction") end # Testing #433 def test_explicit_dependencies_beat_automatic # Create a couple of different resource sets that have automatic relationships and make sure the manual relationships win rels = {} # Now add the explicit relationship # Now files d = tempfile f = File.join(d, "file") file = Puppet::Type.type(:file).new(:path => f, :content => "yay") dir = Puppet::Type.type(:file).new(:path => d, :ensure => :directory, :require => file) rels[dir] = file rels.each do |after, before| config = mk_catalog(before, after) trans = Puppet::Transaction.new(config) str = "from #{before} to #{after}" assert_nothing_raised("Failed to create graph #{str}") do trans.prepare end graph = trans.relationship_graph assert(graph.edge?(before, after), "did not create manual relationship #{str}") assert(! graph.edge?(after, before), "created automatic relationship #{str}") end end # #542 - make sure resources in noop mode still notify their resources, # so that users know if a service will get restarted. def test_noop_with_notify path = tempfile epath = tempfile spath = tempfile file = Puppet::Type.type(:file).new( :path => path, :ensure => :file, :title => "file") exec = Puppet::Type.type(:exec).new( :command => "touch #{epath}", :path => ENV["PATH"], :subscribe => file, :refreshonly => true, :title => 'exec1') exec2 = Puppet::Type.type(:exec).new( :command => "touch #{spath}", :path => ENV["PATH"], :subscribe => exec, :refreshonly => true, :title => 'exec2') Puppet[:noop] = true assert(file.noop, "file not in noop") assert(exec.noop, "exec not in noop") @logs.clear assert_apply(file, exec, exec2) assert(! FileTest.exists?(path), "Created file in noop") assert(! FileTest.exists?(epath), "Executed exec in noop") assert(! FileTest.exists?(spath), "Executed second exec in noop") assert(@logs.detect { |l| l.message =~ /should be/ and l.source == file.property(:ensure).path}, "did not log file change") assert( @logs.detect { |l| l.message =~ /Would have/ and l.source == exec.path }, "did not log first exec trigger") assert( @logs.detect { |l| l.message =~ /Would have/ and l.source == exec2.path }, "did not log second exec trigger") end def test_only_stop_purging_with_relations files = [] paths = [] 3.times do |i| path = tempfile paths << path file = Puppet::Type.type(:file).new( :path => path, :ensure => :absent, :backup => false, :title => "file#{i}") File.open(path, "w") { |f| f.puts "" } files << file end files[0][:ensure] = :file files[0][:require] = files[1..2] # Mark the second as purging files[1].purging assert_apply(*files) assert(FileTest.exists?(paths[1]), "Deleted required purging file") assert(! FileTest.exists?(paths[2]), "Did not delete non-purged file") end def test_flush $state = :absent $flushed = 0 type = Puppet::Type.newtype(:flushtest) do newparam(:name) newproperty(:ensure) do newvalues :absent, :present, :other def retrieve $state end def set(value) $state = value :thing_changed end end def flush $flushed += 1 end end cleanup { Puppet::Type.rmtype(:flushtest) } obj = type.new(:name => "test", :ensure => :present) # first make sure it runs through and flushes assert_apply(obj) assert_equal(:present, $state, "Object did not make a change") assert_equal(1, $flushed, "object was not flushed") # Now run a noop and make sure we don't flush obj[:ensure] = "other" obj[:noop] = true assert_apply(obj) assert_equal(:present, $state, "Object made a change in noop") assert_equal(1, $flushed, "object was flushed in noop") end end diff --git a/test/ral/type/file/target.rb b/test/ral/type/file/target.rb index 272128586..d778f2891 100755 --- a/test/ral/type/file/target.rb +++ b/test/ral/type/file/target.rb @@ -1,359 +1,346 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../../../lib/puppettest') require 'puppettest' require 'puppettest/support/utils' require 'fileutils' class TestFileTarget < Test::Unit::TestCase include PuppetTest::Support::Utils include PuppetTest::FileTesting def setup super @file = Puppet::Type.type(:file) end # Make sure we can create symlinks def test_symlinks path = tempfile link = tempfile File.open(path, "w") { |f| f.puts "yay" } file = nil assert_nothing_raised { - - file = Puppet::Type.type(:file).new( - + file = Puppet::Type.type(:file).new( :title => "somethingelse", :ensure => path, - :path => link ) } assert_events([:link_created], file) assert(FileTest.symlink?(link), "Link was not created") assert_equal(path, File.readlink(link), "Link was created incorrectly") # Make sure running it again works assert_events([], file) assert_events([], file) assert_events([], file) end def test_simplerecursivelinking source = tempfile path = tempfile subdir = File.join(source, "subdir") file = File.join(subdir, "file") system("mkdir -p #{subdir}") system("touch #{file}") link = Puppet::Type.type(:file).new( :ensure => source, :path => path, :recurse => true ) catalog = mk_catalog(link) catalog.apply sublink = File.join(path, "subdir") linkpath = File.join(sublink, "file") assert(File.directory?(path), "dest is not a dir") assert(File.directory?(sublink), "subdest is not a dir") assert(File.symlink?(linkpath), "path is not a link") assert_equal(file, File.readlink(linkpath)) # Use classes for comparison, because the resource inspection is so large assert_events([], link, "Link is not in sync") end def test_recursivelinking source = tempfile dest = tempfile files = [] dirs = [] # Make a bunch of files and dirs Dir.mkdir(source) Dir.chdir(source) do system("mkdir -p #{"some/path/of/dirs"}") system("mkdir -p #{"other/path/of/dirs"}") system("touch #{"file"}") system("touch #{"other/file"}") system("touch #{"some/path/of/file"}") system("touch #{"some/path/of/dirs/file"}") system("touch #{"other/path/of/file"}") files = %x{find . -type f}.chomp.split(/\n/) dirs = %x{find . -type d}.chomp.split(/\n/).reject{|d| d =~ /^\.+$/ } end link = nil assert_nothing_raised { - - link = Puppet::Type.type(:file).new( - + link = Puppet::Type.type(:file).new( :ensure => source, :path => dest, - :recurse => true ) } assert_apply(link) files.each do |f| f.sub!(/^\.#{File::SEPARATOR}/, '') path = File.join(dest, f) assert(FileTest.exists?(path), "Link #{path} was not created") assert(FileTest.symlink?(path), "#{f} is not a link") target = File.readlink(path) assert_equal(File.join(source, f), target) end dirs.each do |d| d.sub!(/^\.#{File::SEPARATOR}/, '') path = File.join(dest, d) assert(FileTest.exists?(path), "Dir #{path} was not created") assert(FileTest.directory?(path), "#{d} is not a directory") end end def test_localrelativelinks dir = tempfile Dir.mkdir(dir) source = File.join(dir, "source") File.open(source, "w") { |f| f.puts "yay" } dest = File.join(dir, "link") link = nil assert_nothing_raised { - - link = Puppet::Type.type(:file).new( - + link = Puppet::Type.type(:file).new( :path => dest, - :ensure => "source" ) } assert_events([:link_created], link) assert(FileTest.symlink?(dest), "Did not create link") assert_equal("source", File.readlink(dest)) assert_equal("yay\n", File.read(dest)) end def test_recursivelinkingmissingtarget source = tempfile dest = tempfile resources = [] - resources << Puppet::Type.type(:exec).new( - + resources << Puppet::Type.type(:exec).new( :command => "mkdir #{source}; touch #{source}/file", :title => "yay", - :path => ENV["PATH"] ) - resources << Puppet::Type.type(:file).new( - + resources << Puppet::Type.type(:file).new( :ensure => source, :path => dest, :recurse => true, - :require => resources[0] ) assert_apply(*resources) link = File.join(dest, "file") assert(FileTest.symlink?(link), "Did not make link") assert_equal(File.join(source, "file"), File.readlink(link)) end def test_insync? source = tempfile dest = tempfile obj = @file.create(:path => source, :target => dest) prop = obj.send(:property, :target) prop.send(:instance_variable_set, "@should", [:nochange]) assert( prop.insync?(prop.retrieve), "Property not in sync with should == :nochange") prop = obj.send(:property, :target) prop.send(:instance_variable_set, "@should", [:notlink]) assert( prop.insync?(prop.retrieve), "Property not in sync with should == :nochange") # Lastly, make sure that we don't try to do anything when we're # recursing, since 'ensure' does the work. obj[:recurse] = true prop.should = dest assert( prop.insync?(prop.retrieve), "Still out of sync during recursion") end def test_replacedirwithlink Puppet[:trace] = false path = tempfile link = tempfile File.open(path, "w") { |f| f.puts "yay" } Dir.mkdir(link) File.open(File.join(link, "yay"), "w") do |f| f.puts "boo" end file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :ensure => path, :path => link, :backup => false ) } # First run through without :force assert_events([], file) assert(FileTest.directory?(link), "Link replaced dir without force") assert_nothing_raised { file[:force] = true } assert_events([:link_created], file) assert(FileTest.symlink?(link), "Link was not created") assert_equal(path, File.readlink(link), "Link was created incorrectly") end def test_replace_links_with_files base = tempfile Dir.mkdir(base) file = File.join(base, "file") link = File.join(base, "link") File.open(file, "w") { |f| f.puts "yayness" } File.symlink(file, link) obj = Puppet::Type.type(:file).new( :path => link, :ensure => "file" ) assert_apply(obj) assert_equal( "yayness\n", File.read(file), "Original file got changed") assert_equal("file", File.lstat(link).ftype, "File is still a link") end def test_no_erase_linkedto_files base = tempfile Dir.mkdir(base) dirs = {} %w{other source target}.each do |d| dirs[d] = File.join(base, d) Dir.mkdir(dirs[d]) end file = File.join(dirs["other"], "file") sourcefile = File.join(dirs["source"], "sourcefile") link = File.join(dirs["target"], "sourcefile") File.open(file, "w") { |f| f.puts "other" } File.open(sourcefile, "w") { |f| f.puts "source" } File.symlink(file, link) obj = Puppet::Type.type(:file).new( :path => dirs["target"], :ensure => :file, :source => dirs["source"], :recurse => true ) config = mk_catalog obj config.apply newfile = File.join(dirs["target"], "sourcefile") assert(File.directory?(dirs["target"]), "Dir did not get created") assert(File.file?(newfile), "File did not get copied") assert_equal(File.read(sourcefile), File.read(newfile), "File did not get copied correctly.") assert_equal( "other\n", File.read(file), "Original file got changed") assert_equal("file", File.lstat(link).ftype, "File is still a link") end def test_replace_links dest = tempfile otherdest = tempfile link = tempfile File.open(dest, "w") { |f| f.puts "boo" } File.open(otherdest, "w") { |f| f.puts "yay" } obj = Puppet::Type.type(:file).new( :path => link, :ensure => otherdest ) assert_apply(obj) assert_equal(otherdest, File.readlink(link), "Link did not get created") obj[:ensure] = dest assert_apply(obj) assert_equal(dest, File.readlink(link), "Link did not get changed") end end