diff --git a/lib/puppet/relationship.rb b/lib/puppet/relationship.rb index 05b7dc39e..984dffeeb 100644 --- a/lib/puppet/relationship.rb +++ b/lib/puppet/relationship.rb @@ -1,69 +1,62 @@ #!/usr/bin/env ruby # # Created by Luke A. Kanies on 2006-11-24. # Copyright (c) 2006. All rights reserved. # subscriptions are permanent associations determining how different # objects react to an event # This is Puppet's class for modeling edges in its configuration graph. # It used to be a subclass of GRATR::Edge, but that class has weird hash # overrides that dramatically slow down the graphing. class Puppet::Relationship - attr_accessor :source, :target, :label + attr_accessor :source, :target, :callback - # Return the callback - def callback - if label - label[:callback] - else - nil - end - end + attr_reader :event - # Return our event. - def event - if label - label[:event] - else - nil + def event=(event) + if event != :NONE and ! callback + raise ArgumentError, "You must pass a callback for non-NONE events" end + @event = event end - - def initialize(source, target, label = {}) - if label - unless label.is_a?(Hash) - raise ArgumentError, "Relationship labels must be a hash" - end - - if label[:event] and label[:event] != :NONE and ! label[:callback] - raise ArgumentError, "You must pass a callback for non-NONE events" + + def initialize(source, target, options = {}) + @source, @target = source, target + + options ||= {} + [:callback, :event].each do |option| + if value = options[option] + send(option.to_s + "=", value) end - else - label = {} end - - @source, @target, @label = source, target, label end # Does the passed event match our event? This is where the meaning # of :NONE comes from. def match?(event) if self.event.nil? or event == :NONE or self.event == :NONE return false elsif self.event == :ALL_EVENTS or event == self.event return true else return false end end + + def label + result = {} + result[:callback] = callback if callback + result[:event] = event if event + result + end def ref "%s => %s" % [source, target] end def to_s ref end end diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index cb17de7b9..884206b4c 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -1,697 +1,695 @@ # the class that actually walks our resource/property tree, collects the changes, # and performs them require 'puppet' module Puppet class Transaction require 'puppet/transaction/change' require 'puppet/transaction/event' attr_accessor :component, :catalog, :ignoreschedules attr_accessor :sorted_resources, :configurator # The report, once generated. attr_reader :report # The list of events generated in this transaction. attr_reader :events include Puppet::Util # Add some additional times for reporting def addtimes(hash) hash.each do |name, num| @timemetrics[name] = num end end # Check to see if we should actually allow processing, but this really only # matters when a resource is getting deleted. def allow_processing?(resource, changes) # If a resource is going to be deleted but it still has # dependencies, then don't delete it unless it's implicit or the # dependency is itself being deleted. if resource.purging? and resource.deleting? if deps = relationship_graph.dependents(resource) and ! deps.empty? and deps.detect { |d| ! d.deleting? } resource.warning "%s still depend%s on me -- not purging" % [deps.collect { |r| r.ref }.join(","), deps.length > 1 ? "":"s"] return false end end return true end # Are there any failed resources in this transaction? def any_failed? failures = @failures.inject(0) { |failures, array| failures += array[1]; failures } if failures > 0 failures else false end end # Apply all changes for a resource, returning a list of the events # generated. def apply(resource) begin changes = resource.evaluate rescue => detail if Puppet[:trace] puts detail.backtrace end resource.err "Failed to retrieve current state of resource: %s" % detail # Mark that it failed @failures[resource] += 1 # And then return return [] end changes = [changes] unless changes.is_a?(Array) if changes.length > 0 @resourcemetrics[:out_of_sync] += 1 end return [] if changes.empty? or ! allow_processing?(resource, changes) resourceevents = apply_changes(resource, changes) # If there were changes and the resource isn't in noop mode... unless changes.empty? or resource.noop # Record when we last synced resource.cache(:synced, Time.now) # Flush, if appropriate if resource.respond_to?(:flush) resource.flush end # And set a trigger for refreshing this resource if it's a # self-refresher if resource.self_refresh? and ! resource.deleting? # Create an edge with this resource as both the source and # target. The triggering method treats these specially for # logging. events = resourceevents.collect { |e| e.name } set_trigger(Puppet::Relationship.new(resource, resource, :callback => :refresh, :event => events)) end end resourceevents end # Apply each change in turn. def apply_changes(resource, changes) changes.collect { |change| @changes << change @count += 1 events = nil begin # use an array, so that changes can return more than one # event if they want events = [change.forward].flatten.reject { |e| e.nil? } rescue => detail if Puppet[:trace] puts detail.backtrace end change.property.err "change from %s to %s failed: %s" % [change.property.is_to_s(change.is), change.property.should_to_s(change.should), detail] @failures[resource] += 1 next # FIXME this should support using onerror to determine # behaviour; or more likely, the client calling us # should do so end # Mark that our change happened, so it can be reversed # if we ever get to that point unless events.nil? or (events.is_a?(Array) and (events.empty?) or events.include?(:noop)) change.changed = true @resourcemetrics[:applied] += 1 end events }.flatten.reject { |e| e.nil? } end # Find all of the changed resources. def changed? @changes.find_all { |change| change.changed }.collect { |change| unless change.property.resource raise "No resource for %s" % change.inspect end change.property.resource }.uniq end # Do any necessary cleanup. If we don't get rid of the graphs, the # contained resources might never get cleaned up. def cleanup if defined? @generated catalog.remove_resource(*@generated) end end # Copy an important relationships from the parent to the newly-generated # child resource. def make_parent_child_relationship(resource, children) depthfirst = resource.depthfirst? children.each do |gen_child| if depthfirst edge = [gen_child, resource] else edge = [resource, gen_child] end relationship_graph.add_vertex(gen_child) unless relationship_graph.edge?(edge[1], edge[0]) relationship_graph.add_edge(*edge) else resource.debug "Skipping automatic relationship to %s" % gen_child end end end # Are we deleting this resource? def deleting?(changes) changes.detect { |change| change.property.name == :ensure and change.should == :absent } end # See if the resource generates new resources at evaluation time. def eval_generate(resource) generate_additional_resources(resource, :eval_generate) end # Evaluate a single resource. def eval_resource(resource, checkskip = true) events = [] if resource.is_a?(Puppet::Type::Component) raise Puppet::DevError, "Got a component to evaluate" end if checkskip and skip?(resource) @resourcemetrics[:skipped] += 1 else @resourcemetrics[:scheduled] += 1 changecount = @changes.length # We need to generate first regardless, because the recursive # actions sometimes change how the top resource is applied. children = eval_generate(resource) if children and resource.depthfirst? children.each do |child| # The child will never be skipped when the parent isn't events += eval_resource(child, false) end end # Perform the actual changes seconds = thinmark do events += apply(resource) end if children and ! resource.depthfirst? children.each do |child| events += eval_resource(child, false) end end # A bit of hackery here -- if skipcheck is true, then we're the # top-level resource. If that's the case, then make sure all of # the changes list this resource as a proxy. This is really only # necessary for rollback, since we know the generating resource # during forward changes. if children and checkskip @changes[changecount..-1].each { |change| change.proxy = resource } end # Keep track of how long we spend in each type of resource @timemetrics[resource.class.name] += seconds end # Check to see if there are any events for this resource if triggedevents = trigger(resource) events += triggedevents end # 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. relationship_graph.matching_edges(events, resource).each do |orig_edge| # We have to dup the label here, else we modify the original edge label, # which affects whether a given event will match on the next run, which is, # of course, bad. - edge = orig_edge.class.new(orig_edge.source, orig_edge.target) - label = orig_edge.label.dup - label[:event] = events.collect { |e| e.name } - edge.label = label + edge = orig_edge.class.new(orig_edge.source, orig_edge.target, orig_edge.label) + edge.event = events.collect { |e| e.name } set_trigger(edge) end # And return the events for collection events 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 @count = 0 # Start logging. Puppet::Util::Log.newdestination(@report) prepare() begin allevents = @sorted_resources.collect { |resource| if resource.is_a?(Puppet::Type::Component) Puppet.warning "Somehow left a component in the relationship graph" next end ret = nil seconds = thinmark do ret = eval_resource(resource) end if Puppet[:evaltrace] and @catalog.host_config? resource.info "Evaluated in %0.2f seconds" % seconds end ret }.flatten.reject { |e| e.nil? } ensure # And then close the transaction log. Puppet::Util::Log.close(@report) end Puppet.debug "Finishing transaction %s with %s changes" % [self.object_id, @count] @events = allevents allevents end # Determine whether a given resource has failed. def failed?(obj) if @failures[obj] > 0 return @failures[obj] else return false end 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. skip = false deps = relationship_graph.dependencies(resource) deps.each do |dep| if fails = failed?(dep) resource.notice "Dependency %s[%s] has %s failures" % [dep.class.name, dep.name, @failures[dep]] skip = true end end return skip end # A general method for recursively generating new resources from a # resource. def generate_additional_resources(resource, method) return [] unless resource.respond_to?(method) begin made = resource.send(method) rescue => detail puts detail.backtrace if Puppet[:trace] resource.err "Failed to generate additional resources using '%s': %s" % [method, detail] end return [] unless made made = [made] unless made.is_a?(Array) made.uniq! made.each do |res| @catalog.add_resource(res) { |r| r.finish } end make_parent_child_relationship(resource, made) made 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, :generate) end list = newlist newlist = [] end end # Generate a transaction report. def generate_report @resourcemetrics[:failed] = @failures.find_all do |name, num| num > 0 end.length # Get the total time spent @timemetrics[:total] = @timemetrics.inject(0) do |total, vals| total += vals[1] total end # Add all of the metrics related to resource count and status @report.newmetric(:resources, @resourcemetrics) # Record the relative time spent in each resource. @report.newmetric(:time, @timemetrics) # Then all of the change-related metrics @report.newmetric(:changes, :total => @changes.length ) @report.time = Time.now return @report 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(resources) if resources.is_a?(Puppet::Resource::Catalog) @catalog = resources elsif resources.is_a?(Puppet::SimpleGraph) raise "Transactions should get catalogs now, not SimpleGraph" else raise "Transactions require catalogs" end @resourcemetrics = { :total => @catalog.vertices.length, :out_of_sync => 0, # The number of resources that had changes :applied => 0, # The number of resources fixed :skipped => 0, # The number of resources skipped :restarted => 0, # The number of resources triggered :failed_restarts => 0, # The number of resources that fail a trigger :scheduled => 0 # The number of resources scheduled } # Metrics for distributing times across the different types. @timemetrics = Hash.new(0) # The number of resources that were triggered in this run @triggered = Hash.new { |hash, key| hash[key] = Hash.new(0) } # Targets of being triggered. @targets = Hash.new do |hash, key| hash[key] = [] end # The changes we're performing @changes = [] # The resources that have failed and the number of failures each. This # is used for skipping resources because of failed dependencies. @failures = Hash.new do |h, key| h[key] = 0 end @report = Report.new @count = 0 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.title] = 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 %s resources for %s" % [provider.name, provider.resource_type.name] begin provider.prefetch(resources) rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err "Could not prefetch %s provider '%s': %s" % [provider.resource_type.name, provider.name, detail] end end end # Prepare to evaluate the resources in a transaction. def prepare # Now add any dynamically generated resources generate() # Then prefetch. It's important that we generate and then prefetch, # so that any generated resources also get prefetched. prefetch() # This will throw an error if there are cycles in the graph. @sorted_resources = relationship_graph.topsort end def relationship_graph catalog.relationship_graph end # Send off the transaction report. def send_report begin report = generate_report() rescue => detail Puppet.err "Could not generate report: %s" % detail return end if Puppet[:rrdgraph] == true report.graph() end if Puppet[:summarize] puts report.summary end if Puppet[:report] begin report.save() rescue => detail Puppet.err "Reporting failed: %s" % detail end end end # Roll all completed changes back. def rollback @targets.clear @triggered.clear allevents = @changes.reverse.collect { |change| # skip changes that were never actually run unless change.changed Puppet.debug "%s was not changed" % change.to_s next end begin events = change.backward rescue => detail Puppet.err("%s rollback failed: %s" % [change,detail]) if Puppet[:trace] puts detail.backtrace end next # at this point, we would normally do error handling # but i haven't decided what to do for that yet # so just record that a sync failed for a given resource #@@failures[change.property.parent] += 1 # this still could get hairy; what if file contents changed, # but a chmod failed? how would i handle that error? dern end # FIXME This won't work right now. relationship_graph.matching_edges(events).each do |edge| @targets[edge.target] << edge end # Now check to see if there are any events for this child. # Kind of hackish, since going backwards goes a change at a # time, not a child at a time. trigger(change.property.resource) # And return the events for collection events }.flatten.reject { |e| e.nil? } end # Is the resource currently scheduled? def scheduled?(resource) self.ignoreschedules or resource.scheduled? end # Set an edge to be triggered when we evaluate its target. def set_trigger(edge) return unless method = edge.callback return unless edge.target.respond_to?(method) if edge.target.respond_to?(:ref) unless edge.source == edge.target edge.source.info "Scheduling %s of %s" % [edge.callback, edge.target.ref] end end @targets[edge.target] << edge end # Should this resource be skipped? def skip?(resource) skip = false if missing_tags?(resource) resource.debug "Not tagged with %s" % tags.join(", ") elsif ! scheduled?(resource) resource.debug "Not scheduled" elsif failed_dependencies?(resource) resource.warning "Skipping because of failed dependencies" else return false end return true end # The tags we should be checking. def tags unless defined? @tags tags = Puppet[:tags] if tags.nil? or tags == "" @tags = [] else @tags = tags.split(/\s*,\s*/) end end @tags end def tags=(tags) tags = [tags] unless tags.is_a?(Array) @tags = tags end # Is this resource tagged appropriately? def missing_tags?(resource) return false if self.ignore_tags? or tags.empty? return true unless resource.tagged?(tags) end # Are there any edges that target this resource? def targeted?(resource) # The default value is a new array so we have to test the length of it. @targets.include?(resource) and @targets[resource].length > 0 end # Trigger any subscriptions to a child. This does an upwardly recursive # search -- it triggers the passed resource, but also the resource's parent # and so on up the tree. def trigger(resource) return nil unless targeted?(resource) callbacks = Hash.new { |hash, key| hash[key] = [] } trigged = [] @targets[resource].each do |edge| # Collect all of the subs for each callback callbacks[edge.callback] << edge end callbacks.each do |callback, subs| noop = true subs.each do |edge| if edge.event.nil? or ! edge.event.include?(:noop) noop = false end end if noop resource.notice "Would have triggered %s from %s dependencies" % [callback, subs.length] # And then add an event for it. return [Puppet::Transaction::Event.new(:noop, resource)] end if subs.length == 1 and subs[0].source == resource message = "Refreshing self" else message = "Triggering '%s' from %s dependencies" % [callback, subs.length] end resource.notice message # At this point, just log failures, don't try to react # to them in any way. begin resource.send(callback) @resourcemetrics[:restarted] += 1 rescue => detail resource.err "Failed to call %s on %s: %s" % [callback, resource, detail] @resourcemetrics[:failed_restarts] += 1 if Puppet[:trace] puts detail.backtrace end end # And then add an event for it. trigged << Puppet::Transaction::Event.new(:triggered, resource) triggered(resource, callback) end if trigged.empty? return nil else return trigged end end def triggered(resource, method) @triggered[resource][method] += 1 end def triggered?(resource, method) @triggered[resource][method] end end end require 'puppet/transaction/report' diff --git a/spec/unit/relationship.rb b/spec/unit/relationship.rb index 5f96cdf8c..4d1b75856 100755 --- a/spec/unit/relationship.rb +++ b/spec/unit/relationship.rb @@ -1,149 +1,146 @@ #!/usr/bin/env ruby # # Created by Luke Kanies on 2007-11-1. # Copyright (c) 2006. All rights reserved. require File.dirname(__FILE__) + '/../spec_helper' require 'puppet/relationship' describe Puppet::Relationship do before do @edge = Puppet::Relationship.new(:a, :b) end it "should have a :source attribute" do @edge.should respond_to(:source) end it "should have a :target attribute" do @edge.should respond_to(:target) end - it "should have a :label attribute" do - @edge.should respond_to(:label) + it "should have a :callback attribute" do + @edge.callback = :foo + @edge.callback.should == :foo end - it "should provide a :ref method that describes the edge" do - @edge = Puppet::Relationship.new("a", "b") - @edge.ref.should == "a => b" + it "should have an :event attribute" do + @edge.event = :NONE + @edge.event.should == :NONE end -end -describe Puppet::Relationship, " when initializing" do - before do - @edge = Puppet::Relationship.new(:a, :b, :testing => :foo) + it "should require a callback if a non-NONE event is specified" do + proc { @edge.event = :something }.should raise_error(ArgumentError) end - it "should use the first argument as the source" do - @edge.source.should == :a + it "should have a :label attribute" do + @edge.should respond_to(:label) end - it "should use the second argument as the target" do - @edge.target.should == :b + it "should provide a :ref method that describes the edge" do + @edge = Puppet::Relationship.new("a", "b") + @edge.ref.should == "a => b" end - it "should use the third argument as the label" do - @edge.label.should == {:testing => :foo} - end + it "should be able to produce a label as a hash with its event and callback" do + @edge.callback = :foo + @edge.event = :bar - it "should require a callback if a non-NONE event is specified" do - proc { Puppet::Relationship.new(:a, :b, :event => :something) }.should raise_error(ArgumentError) + @edge.label.should == {:callback => :foo, :event => :bar} end - it "should require the label to be a hash" do - proc { Puppet::Relationship.new(:a, :b, :event) }.should raise_error(ArgumentError) + it "should work if nil options are provided" do + lambda { Puppet::Relationship.new("a", "b", nil) }.should_not raise_error end end -describe Puppet::Relationship, " when interpreting the label" do - it "should default to an event of nil" do +describe Puppet::Relationship, " when initializing" do + before do @edge = Puppet::Relationship.new(:a, :b) - @edge.event.should be_nil end - it "should expose a provided event via the :event method" do - @edge = Puppet::Relationship.new(:a, :b, :event => :something, :callback => :whatever) - @edge.event.should == :something + it "should use the first argument as the source" do + @edge.source.should == :a end - it "should default to a nil callback" do - @edge = Puppet::Relationship.new(:a, :b) - @edge.callback.should be_nil + it "should use the second argument as the target" do + @edge.target.should == :b end - it "should expose a provided callback via the :callback method" do - @edge = Puppet::Relationship.new(:a, :b, :callback => :testing) - @edge.callback.should == :testing + it "should set the rest of the arguments as the event and callback" do + @edge = Puppet::Relationship.new(:a, :b, :callback => :foo, :event => :bar) + @edge.callback.should == :foo + @edge.event.should == :bar end end describe Puppet::Relationship, " when matching edges with no specified event" do before do @edge = Puppet::Relationship.new(:a, :b) end it "should not match :NONE" do @edge.should_not be_match(:NONE) end it "should not match :ALL_EVENTS" do @edge.should_not be_match(:NONE) end it "should not match any other events" do @edge.should_not be_match(:whatever) end end describe Puppet::Relationship, " when matching edges with :NONE as the event" do before do @edge = Puppet::Relationship.new(:a, :b, :event => :NONE) end it "should not match :NONE" do @edge.should_not be_match(:NONE) end it "should not match :ALL_EVENTS" do @edge.should_not be_match(:ALL_EVENTS) end it "should not match other events" do @edge.should_not be_match(:yayness) end end describe Puppet::Relationship, " when matching edges with :ALL as the event" do before do @edge = Puppet::Relationship.new(:a, :b, :event => :ALL_EVENTS, :callback => :whatever) end it "should not match :NONE" do @edge.should_not be_match(:NONE) end it "should match :ALL_EVENTS" do @edge.should be_match(:ALLEVENTS) end it "should match all other events" do @edge.should be_match(:foo) end end describe Puppet::Relationship, " when matching edges with a non-standard event" do before do @edge = Puppet::Relationship.new(:a, :b, :event => :random, :callback => :whatever) end it "should not match :NONE" do @edge.should_not be_match(:NONE) end it "should not match :ALL_EVENTS" do @edge.should_not be_match(:ALL_EVENTS) end it "should match events with the same name" do @edge.should be_match(:random) end end diff --git a/spec/unit/simple_graph.rb b/spec/unit/simple_graph.rb index a73e75a42..2c061ae1a 100755 --- a/spec/unit/simple_graph.rb +++ b/spec/unit/simple_graph.rb @@ -1,520 +1,520 @@ #!/usr/bin/env ruby # # Created by Luke Kanies on 2007-11-1. # Copyright (c) 2006. All rights reserved. require 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, :stuff => :awesome) + @graph.add_edge(:one, :two, :callback => :awesome) @graph.edge?(:one, :two).should be_true end it "should provide a method for retrieving an edge label" do - edge = Puppet::Relationship.new(:one, :two, :stuff => :awesome) + edge = Puppet::Relationship.new(:one, :two, :callback => :awesome) @graph.add_edge(edge) - @graph.edge_label(:one, :two).should == {:stuff => :awesome} + @graph.edge_label(:one, :two).should == {:callback => :awesome} end it "should provide a method for retrieving an edge" do edge = Puppet::Relationship.new(:one, :two) @graph.add_edge(edge) @graph.edge(:one, :two).should equal(edge) 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.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 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, :stuff => :awesome) + @graph.add_edge(:one, :two, :callback => :awesome) edge = @graph.reversal.edge(:two, :one) - edge.label.should == {:stuff => :awesome} + edge.label.should == {:callback => :awesome} end end describe "when sorting 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) 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) 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) 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 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(:yay, "a") @none = Puppet::Transaction::Event.new(:NONE, "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 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 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", []) @contgraph = @top.to_graph # 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 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| @depgraph.add_edge(source, target, :callback => :refresh) end end def splice @depgraph.splice!(@contgraph, Container) 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. it "should remove all Container objects from the dependency graph" do @depgraph.vertices.find_all { |v| v.is_a?(Container) }.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 no longer contain anything but the non-container objects" do @depgraph.vertices.find_all { |v| ! v.is_a?(String) }.should be_empty end it "should copy labels" do @depgraph.edges.each do |edge| edge.label.should == {:callback => :refresh} end end it "should not add labels to edges that have none" do @depgraph.add_edge(@two, @three) splice @depgraph.edge_label("c", "i").should == {} 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.edge_label("c", "i").should == {:callback => :refresh} 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.edge_label("c", "i").should == {:callback => :refresh} 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.edge_label(edge.source, edge.target).should == {:callback => :refresh} end end end end