diff --git a/lib/puppet/parser/resource.rb b/lib/puppet/parser/resource.rb index 2edbcd829..ee1a8c436 100644 --- a/lib/puppet/parser/resource.rb +++ b/lib/puppet/parser/resource.rb @@ -1,335 +1,336 @@ require 'puppet/resource' # The primary difference between this class and its # parent is that this class has rules on who can set # parameters class Puppet::Parser::Resource < Puppet::Resource require 'puppet/parser/resource/param' require 'puppet/util/tagging' require 'puppet/file_collection/lookup' require 'puppet/parser/yaml_trimmer' require 'puppet/resource/type_collection_helper' include Puppet::FileCollection::Lookup include Puppet::Resource::TypeCollectionHelper include Puppet::Util include Puppet::Util::MethodHelper include Puppet::Util::Errors include Puppet::Util::Logging include Puppet::Util::Tagging include Puppet::Parser::YamlTrimmer attr_accessor :source, :scope, :collector_id attr_accessor :virtual, :override, :translated, :catalog, :evaluated attr_reader :exported, :parameters # Determine whether the provided parameter name is a relationship parameter. def self.relationship_parameter?(name) @relationship_names ||= Puppet::Type.relationship_params.collect { |p| p.name } @relationship_names.include?(name) end # Set up some boolean test methods def translated?; !!@translated; end def override?; !!@override; end def evaluated?; !!@evaluated; end def [](param) param = symbolize(param) if param == :title return self.title end if @parameters.has_key?(param) @parameters[param].value else nil end end def []=(param, value) set_parameter(param, value) end def eachparam @parameters.each do |name, param| yield param end end def environment scope.environment end # Process the stage metaparameter for a class. A containment edge # is drawn from the class to the stage. The stage for containment # defaults to main, if none is specified. def add_edge_to_stage return unless self.type.to_s.downcase == "class" unless stage = catalog.resource(:stage, self[:stage] || (scope && scope.resource && scope.resource[:stage]) || :main) raise ArgumentError, "Could not find stage #{self[:stage] || :main} specified by #{self}" end self[:stage] ||= stage.title unless stage.title == :main catalog.add_edge(stage, self) end # Retrieve the associated definition and evaluate it. def evaluate return if evaluated? @evaluated = true if klass = resource_type and ! builtin_type? finish evaluated_code = klass.evaluate_code(self) return evaluated_code elsif builtin? devfail "Cannot evaluate a builtin type (#{type})" else self.fail "Cannot find definition #{type}" end end # Mark this resource as both exported and virtual, # or remove the exported mark. def exported=(value) if value @virtual = true @exported = value else @exported = value end end # Do any finishing work on this object, called before evaluation or # before storage/translation. def finish return if finished? @finished = true add_defaults add_metaparams add_scope_tags validate end # Has this resource already been finished? def finished? @finished end def initialize(*args) + raise ArgumentError, "Resources require a hash as last argument" unless args.last.is_a? Hash raise ArgumentError, "Resources require a scope" unless args.last[:scope] super @source ||= scope.source end # Is this resource modeling an isomorphic resource type? def isomorphic? if builtin_type? return resource_type.isomorphic? else return true end end # Merge an override resource in. This will throw exceptions if # any overrides aren't allowed. def merge(resource) # Test the resource scope, to make sure the resource is even allowed # to override. unless self.source.object_id == resource.source.object_id || resource.source.child_of?(self.source) raise Puppet::ParseError.new("Only subclasses can override parameters", resource.line, resource.file) end # Some of these might fail, but they'll fail in the way we want. resource.parameters.each do |name, param| override_parameter(param) end end # Unless we're running >= 0.25, we're in compat mode. def metaparam_compatibility_mode? ! (catalog and ver = (catalog.client_version||'0.0.0').split(".") and (ver[0] > "0" or ver[1].to_i >= 25)) end def name self[:name] || self.title end # A temporary occasion, until I get paths in the scopes figured out. def path to_s end # Define a parameter in our resource. # if we ever receive a parameter named 'tag', set # the resource tags with its value. def set_parameter(param, value = nil) if ! value.nil? param = Puppet::Parser::Resource::Param.new( :name => param, :value => value, :source => self.source ) elsif ! param.is_a?(Puppet::Parser::Resource::Param) raise ArgumentError, "Received incomplete information - no value provided for parameter #{param}" end tag(*param.value) if param.name == :tag # And store it in our parameter hash. @parameters[param.name] = param end def to_hash @parameters.inject({}) do |hash, ary| param = ary[1] # Skip "undef" values. hash[param.name] = param.value if param.value != :undef hash end end # Create a Puppet::Resource instance from this parser resource. # We plan, at some point, on not needing to do this conversion, but # it's sufficient for now. def to_resource result = Puppet::Resource.new(type, title) to_hash.each do |p, v| if v.is_a?(Puppet::Resource) v = Puppet::Resource.new(v.type, v.title) elsif v.is_a?(Array) # flatten resource references arrays v = v.flatten if v.flatten.find { |av| av.is_a?(Puppet::Resource) } v = v.collect do |av| av = Puppet::Resource.new(av.type, av.title) if av.is_a?(Puppet::Resource) av end end # If the value is an array with only one value, then # convert it to a single value. This is largely so that # the database interaction doesn't have to worry about # whether it returns an array or a string. result[p] = if v.is_a?(Array) and v.length == 1 v[0] else v end end result.file = self.file result.line = self.line result.exported = self.exported result.virtual = self.virtual result.tag(*self.tags) result end # Convert this resource to a RAL resource. def to_ral to_resource.to_ral end private # Add default values from our definition. def add_defaults scope.lookupdefaults(self.type).each do |name, param| unless @parameters.include?(name) self.debug "Adding default for #{name}" @parameters[name] = param.dup end end end def add_backward_compatible_relationship_param(name) # Skip metaparams for which we get no value. return unless scope.include?(name.to_s) val = scope[name.to_s] # The default case: just set the value set_parameter(name, val) and return unless @parameters[name] # For relationship params, though, join the values (a la #446). @parameters[name].value = [@parameters[name].value, val].flatten end # Add any metaparams defined in our scope. This actually adds any metaparams # from any parent scope, and there's currently no way to turn that off. def add_metaparams compat_mode = metaparam_compatibility_mode? Puppet::Type.eachmetaparam do |name| next unless self.class.relationship_parameter?(name) add_backward_compatible_relationship_param(name) if compat_mode end end def add_scope_tags if scope_resource = scope.resource tag(*scope_resource.tags) end end # Accept a parameter from an override. def override_parameter(param) # This can happen if the override is defining a new parameter, rather # than replacing an existing one. (set_parameter(param) and return) unless current = @parameters[param.name] # The parameter is already set. Fail if they're not allowed to override it. unless param.source.child_of?(current.source) msg = "Parameter '#{param.name}' is already set on #{self}" msg += " by #{current.source}" if current.source.to_s != "" if current.file or current.line fields = [] fields << current.file if current.file fields << current.line.to_s if current.line msg += " at #{fields.join(":")}" end msg += "; cannot redefine" Puppet.log_exception(ArgumentError.new(), msg) raise Puppet::ParseError.new(msg, param.line, param.file) end # If we've gotten this far, we're allowed to override. # Merge with previous value, if the parameter was generated with the +> # syntax. It's important that we use a copy of the new param instance # here, not the old one, and not the original new one, so that the source # is registered correctly for later overrides but the values aren't # implcitly shared when multiple resources are overrriden at once (see # ticket #3556). if param.add param = param.dup param.value = [current.value, param.value].flatten end set_parameter(param) end # Make sure the resource's parameters are all valid for the type. def validate @parameters.each do |name, param| validate_parameter(name) end rescue => detail fail Puppet::ParseError, detail.to_s end private def extract_parameters(params) params.each do |param| # Don't set the same parameter twice self.fail Puppet::ParseError, "Duplicate parameter '#{param.name}' for on #{self}" if @parameters[param.name] set_parameter(param) end end end diff --git a/lib/puppet/transaction/event.rb b/lib/puppet/transaction/event.rb index d3f25b71c..3e1c289c3 100644 --- a/lib/puppet/transaction/event.rb +++ b/lib/puppet/transaction/event.rb @@ -1,66 +1,73 @@ require 'puppet/transaction' require 'puppet/util/tagging' require 'puppet/util/logging' # A simple struct for storing what happens on the system. class Puppet::Transaction::Event include Puppet::Util::Tagging include Puppet::Util::Logging ATTRIBUTES = [:name, :resource, :property, :previous_value, :desired_value, :historical_value, :status, :message, :file, :line, :source_description, :audited] YAML_ATTRIBUTES = %w{@audited @property @previous_value @desired_value @historical_value @message @name @status @time} attr_accessor *ATTRIBUTES attr_writer :tags attr_accessor :time attr_reader :default_log_level EVENT_STATUSES = %w{noop success failure audit} def initialize(options = {}) @audited = false options.each { |attr, value| send(attr.to_s + "=", value) } @time = Time.now end def property=(prop) @property = prop.to_s end def resource=(res) - if res.respond_to?(:[]) and level = res[:loglevel] - @default_log_level = level + begin + # In Ruby 1.8 looking up a symbol on a string gives nil; in 1.9 it will + # raise a TypeError, which we then catch. This should work on both + # versions, for all that it is a bit naff. --daniel 2012-03-11 + if res.respond_to?(:[]) and level = res[:loglevel] + @default_log_level = level + end + rescue TypeError => e + raise unless e.to_s == "can't convert Symbol into Integer" end @resource = res.to_s end def send_log super(log_level, message) end def status=(value) raise ArgumentError, "Event status can only be #{EVENT_STATUSES.join(', ')}" unless EVENT_STATUSES.include?(value) @status = value end def to_s message end def to_yaml_properties (YAML_ATTRIBUTES.map {|ya| ya.to_s} & instance_variables.map{|iv| iv.to_s}).sort end private # If it's a failure, use 'err', else use either the resource's log level (if available) # or 'notice'. def log_level status == "failure" ? :err : (@default_log_level || :notice) end # Used by the Logging module def log_source source_description || property || resource end end diff --git a/spec/unit/network/http/webrick/rest_spec.rb b/spec/unit/network/http/webrick/rest_spec.rb index 84a2b7791..1c3122d3f 100755 --- a/spec/unit/network/http/webrick/rest_spec.rb +++ b/spec/unit/network/http/webrick/rest_spec.rb @@ -1,180 +1,190 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/network/http' require 'webrick' require 'puppet/network/http/webrick/rest' describe Puppet::Network::HTTP::WEBrickREST do it "should include the Puppet::Network::HTTP::Handler module" do Puppet::Network::HTTP::WEBrickREST.ancestors.should be_include(Puppet::Network::HTTP::Handler) end - describe "when initializing", :'fails_on_ruby_1.9.2' => true do + describe "when initializing" do it "should call the Handler's initialization hook with its provided arguments as the server and handler" do - Puppet::Network::HTTP::WEBrickREST.any_instance.expects(:initialize_for_puppet).with(:server => "my", :handler => "arguments") - Puppet::Network::HTTP::WEBrickREST.new("my", "arguments") + server = WEBrick::HTTPServer.new(:BindAddress => '127.0.0.1', + # Probablistically going to succeed + # even if we run more than one test + # instance at once. + :Port => 40000 + rand(10000), + # Just discard any log output, thanks. + :Logger => stub_everything('logger')) + + Puppet::Network::HTTP::WEBrickREST.any_instance. + expects(:initialize_for_puppet).with(:server => server, :handler => "arguments") + + Puppet::Network::HTTP::WEBrickREST.new(server, "arguments") end end describe "when receiving a request" do before do @request = stub('webrick http request', :query => {}, :peeraddr => %w{eh boo host ip}, :client_cert => nil) @response = stub('webrick http response', :status= => true, :body= => true) @model_class = stub('indirected model class') @webrick = stub('webrick http server', :mount => true, :[] => {}) Puppet::Indirector::Indirection.stubs(:model).with(:foo).returns(@model_class) @handler = Puppet::Network::HTTP::WEBrickREST.new(@webrick, :foo) end it "should delegate its :service method to its :process method" do @handler.expects(:process).with(@request, @response).returns "stuff" @handler.service(@request, @response).should == "stuff" end describe "when using the Handler interface" do it "should use the 'accept' request parameter as the Accept header" do @request.expects(:[]).with("accept").returns "foobar" @handler.accept_header(@request).should == "foobar" end it "should use the 'content-type' request header as the Content-Type header" do @request.expects(:[]).with("content-type").returns "foobar" @handler.content_type_header(@request).should == "foobar" end it "should use the request method as the http method" do @request.expects(:request_method).returns "FOO" @handler.http_method(@request).should == "FOO" end it "should return the request path as the path" do @request.expects(:path).returns "/foo/bar" @handler.path(@request).should == "/foo/bar" end it "should return the request body as the body" do @request.expects(:body).returns "my body" @handler.body(@request).should == "my body" end it "should set the response's 'content-type' header when setting the content type" do @response.expects(:[]=).with("content-type", "text/html") @handler.set_content_type(@response, "text/html") end it "should set the status and body on the response when setting the response for a successful query" do @response.expects(:status=).with 200 @response.expects(:body=).with "mybody" @handler.set_response(@response, "mybody", 200) end describe "when the result is a File" do before(:each) do stat = stub 'stat', :size => 100 @file = stub 'file', :stat => stat, :path => "/tmp/path" @file.stubs(:is_a?).with(File).returns(true) end it "should serve it" do @response.stubs(:[]=) @response.expects(:status=).with 200 @response.expects(:body=).with @file @handler.set_response(@response, @file, 200) end it "should set the Content-Length header" do @response.expects(:[]=).with('content-length', 100) @handler.set_response(@response, @file, 200) end end it "should set the status and message on the response when setting the response for a failed query" do @response.expects(:status=).with 400 @response.expects(:reason_phrase=).with "mybody" @handler.set_response(@response, "mybody", 400) end end describe "and determining the request parameters" do it "should include the HTTP request parameters, with the keys as symbols" do @request.stubs(:query).returns("foo" => "baz", "bar" => "xyzzy") result = @handler.params(@request) result[:foo].should == "baz" result[:bar].should == "xyzzy" end it "should CGI-decode the HTTP parameters" do encoding = CGI.escape("foo bar") @request.expects(:query).returns('foo' => encoding) result = @handler.params(@request) result[:foo].should == "foo bar" end it "should convert the string 'true' to the boolean" do @request.expects(:query).returns('foo' => "true") result = @handler.params(@request) result[:foo].should be_true end it "should convert the string 'false' to the boolean" do @request.expects(:query).returns('foo' => "false") result = @handler.params(@request) result[:foo].should be_false end it "should YAML-load and CGI-decode values that are YAML-encoded" do escaping = CGI.escape(YAML.dump(%w{one two})) @request.expects(:query).returns('foo' => escaping) result = @handler.params(@request) result[:foo].should == %w{one two} end it "should not allow clients to set the node via the request parameters" do @request.stubs(:query).returns("node" => "foo") @handler.stubs(:resolve_node) @handler.params(@request)[:node].should be_nil end it "should not allow clients to set the IP via the request parameters" do @request.stubs(:query).returns("ip" => "foo") @handler.params(@request)[:ip].should_not == "foo" end it "should pass the client's ip address to model find" do @request.stubs(:peeraddr).returns(%w{noidea dunno hostname ipaddress}) @handler.params(@request)[:ip].should == "ipaddress" end it "should set 'authenticated' to true if a certificate is present" do cert = stub 'cert', :subject => [%w{CN host.domain.com}] @request.stubs(:client_cert).returns cert @handler.params(@request)[:authenticated].should be_true end it "should set 'authenticated' to false if no certificate is present" do @request.stubs(:client_cert).returns nil @handler.params(@request)[:authenticated].should be_false end it "should pass the client's certificate name to model method if a certificate is present" do cert = stub 'cert', :subject => [%w{CN host.domain.com}] @request.stubs(:client_cert).returns cert @handler.params(@request)[:node].should == "host.domain.com" end it "should resolve the node name with an ip address look-up if no certificate is present" do @request.stubs(:client_cert).returns nil @handler.expects(:resolve_node).returns(:resolved_node) @handler.params(@request)[:node].should == :resolved_node end end end end diff --git a/spec/unit/simple_graph_spec.rb b/spec/unit/simple_graph_spec.rb index 2cd00bda9..6f10e91e5 100755 --- a/spec/unit/simple_graph_spec.rb +++ b/spec/unit/simple_graph_spec.rb @@ -1,919 +1,933 @@ #!/usr/bin/env rspec require '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 reporting cycles in the graph" do before do @graph = Puppet::SimpleGraph.new end # This works with `add_edges` to auto-vivify the resource instances. let :vertex do Hash.new do |hash, key| hash[key] = Puppet::Type.type(:notify).new(:name => key.to_s) end end def add_edges(hash) hash.each do |a,b| @graph.add_edge(vertex[a], vertex[b]) end end def simplify(cycles) cycles.map do |x| x.map do |y| y.to_s.match(/^Notify\[(.*)\]$/)[1] end end end it "should fail on two-vertex loops" do add_edges :a => :b, :b => :a 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.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.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.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\((Notify\[a\] => Notify\[b\] => Notify\[a\]|Notify\[b\] => Notify\[a\] => Notify\[b\])\)\nTry} 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 }.should_not raise_error simplify(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 }.should_not raise_error simplify(cycles).should be == [["a1", "a"], ["b1", "b"]] 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 }.should_not raise_error simplify(cycles).should be == [%w{a1 a}, %w{c1 c2 c3 c}] 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 }.should_not raise_error simplify(cycles).should be == [%w{a b c1 c2 c}] 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 }.should_not raise_error simplify(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 paths = @graph.paths_in_cycle(cycles.first, 100) simplify(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 cycles.length.should be == 1 paths = @graph.paths_in_cycle(cycles.first, 100) simplify(paths).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 cycles.length.should be == 1 paths = @graph.paths_in_cycle(cycles.first, 100) simplify(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 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 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", :'fails_on_ruby_1.9.2' => true 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") + + # The Ruby 1.8 semantics for String#[] are that treating it like an + # array and asking for `"a"[:whatever]` returns `nil`. Ruby 1.9 + # enforces that your index has to be numeric. + # + # Now, the real object here, a resource, implements [] and does + # something sane, but we don't care about any of the things that get + # asked for. Right now, anyway. + # + # So, in 1.8 we could just pass a string and it worked. For 1.9 we can + # fake it well enough by stubbing out the operator to return nil no + # matter what input we give. --daniel 2012-03-11 + resource = "a" + resource.stubs(:[]) + @event = Puppet::Transaction::Event.new(:name => :yay, :resource => resource) + @none = Puppet::Transaction::Event.new(:name => :NONE, :resource => resource) @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 < 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 def ref "Container[#{self}]" 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(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. @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 @contgraph.splice!(@depgraph) end def whit_called(name) x = @depgraph.vertices.find { |v| v.is_a?(@whit) && v.name =~ /#{Regexp.escape(name)}/ } x.should_not be_nil def x.to_s "Whit[#{name}]" end def x.inspect to_s end x end def admissible_sentinel_of(x) @depgraph.vertex?(x) ? x : whit_called("admissible_#{x.ref}") end def completed_sentinel_of(x) @depgraph.vertex?(x) ? x : whit_called("completed_#{x.ref}") end before do container_graph dependency_graph splice end # 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 sentinel should depend on its admissible sentinel" do containers.each { |container| @depgraph.path_between(admissible_sentinel_of(container),completed_sentinel_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 sentinel" do containers.each { |container| contents_of(container).each { |leaf| @depgraph.should be_edge(admissible_sentinel_of(container),admissible_sentinel_of(leaf)) } } end # 2) completed_X depends on each on the contents of X # it "completed sentinels should depend on their container's contents" do containers.each { |container| contents_of(container).each { |leaf| @depgraph.should be_edge(completed_sentinel_of(leaf),completed_sentinel_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 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 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 retain labels on non-containment edges" do @explicit_dependencies.each { |f,t| @depgraph.edges_between(completed_sentinel_of(f),admissible_sentinel_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.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.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.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) (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