require 'spec_helper' require 'puppet/graph' describe Puppet::Graph::SimpleGraph do it "should return the number of its vertices as its length" do @graph = Puppet::Graph::SimpleGraph.new @graph.add_vertex("one") @graph.add_vertex("two") expect(@graph.size).to eq(2) end it "should consider itself a directed graph" do expect(Puppet::Graph::SimpleGraph.new.directed?).to be_truthy end it "should provide a method for reversing the graph" do @graph = Puppet::Graph::SimpleGraph.new @graph.add_edge(:one, :two) expect(@graph.reversal.edge?(:two, :one)).to be_truthy end it "should be able to produce a dot graph" do @graph = Puppet::Graph::SimpleGraph.new class FauxVertex def ref "never mind" end end v1 = FauxVertex.new v2 = FauxVertex.new @graph.add_edge(v1, v2) expect { @graph.to_dot_graph }.to_not raise_error end describe "when managing vertices" do before do @graph = Puppet::Graph::SimpleGraph.new end it "should provide a method to add a vertex" do @graph.add_vertex(:test) expect(@graph.vertex?(:test)).to be_truthy end it "should reset its reversed graph when vertices are added" do rev = @graph.reversal @graph.add_vertex(:test) expect(@graph.reversal).not_to equal(rev) end it "should ignore already-present vertices when asked to add a vertex" do @graph.add_vertex(:test) expect { @graph.add_vertex(:test) }.to_not raise_error end it "should return true when asked if a vertex is present" do @graph.add_vertex(:test) expect(@graph.vertex?(:test)).to be_truthy end it "should return false when asked if a non-vertex is present" do expect(@graph.vertex?(:test)).to be_falsey end it "should return all set vertices when asked" do @graph.add_vertex(:one) @graph.add_vertex(:two) expect(@graph.vertices.length).to eq(2) expect(@graph.vertices).to include(:one) expect(@graph.vertices).to include(:two) end it "should remove a given vertex when asked" do @graph.add_vertex(:one) @graph.remove_vertex!(:one) expect(@graph.vertex?(:one)).to be_falsey end it "should do nothing when a non-vertex is asked to be removed" do expect { @graph.remove_vertex!(:one) }.to_not raise_error end describe "when managing resources with quotes in their title" do subject do v = Puppet::Type.type(:notify).new(:name => 'GRANT ALL ON DATABASE "puppetdb" TO "puppetdb"') @graph.add_vertex(v) @graph.to_dot end it "should escape quotes in node name" do expect(subject).to match(/^[[:space:]]{4}"Notify\[GRANT ALL ON DATABASE \\"puppetdb\\" TO \\"puppetdb\\"\]" \[$/) end it "should escape quotes in node label" do expect(subject).to match(/^[[:space:]]{8}label = "Notify\[GRANT ALL ON DATABASE \\"puppetdb\\" TO \\"puppetdb\\"\]"$/) end end end describe "when managing edges" do before do @graph = Puppet::Graph::SimpleGraph.new end it "should provide a method to test whether a given vertex pair is an edge" do expect(@graph).to respond_to(:edge?) end it "should reset its reversed graph when edges are added" do rev = @graph.reversal @graph.add_edge(:one, :two) expect(@graph.reversal).not_to 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) expect(@graph.edge?(:one, :two)).to be_truthy end it "should provide a method to add an edge by specifying the two vertices" do @graph.add_edge(:one, :two) expect(@graph.edge?(:one, :two)).to be_truthy 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) expect(@graph.edge?(:one, :two)).to be_truthy end describe "when managing edges between nodes with quotes in their title" do let(:vertex) do Hash.new do |hash, key| hash[key] = Puppet::Type.type(:notify).new(:name => key.to_s) end end subject do @graph.add_edge(vertex['Postgresql_psql[CREATE DATABASE "ejabberd"]'], vertex['Postgresql_psql[REVOKE CONNECT ON DATABASE "ejabberd" FROM public]']) @graph.to_dot end it "should escape quotes in edge" do expect(subject).to match(/^[[:space:]]{4}"Notify\[Postgresql_psql\[CREATE DATABASE \\"ejabberd\\"\]\]" -> "Notify\[Postgresql_psql\[REVOKE CONNECT ON DATABASE \\"ejabberd\\" FROM public\]\]" \[$/) end end describe "when retrieving edges between two nodes" do it "should handle the case of nodes not in the graph" do expect(@graph.edges_between(:one, :two)).to eq([]) end it "should handle the case of nodes with no edges between them" do @graph.add_vertex(:one) @graph.add_vertex(:two) expect(@graph.edges_between(:one, :two)).to eq([]) end it "should handle the case of nodes connected by a single edge" do edge = Puppet::Relationship.new(:one, :two) @graph.add_edge(edge) expect(@graph.edges_between(:one, :two).length).to eq(1) expect(@graph.edges_between(:one, :two)[0]).to 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) expect(Set.new(@graph.edges_between(:one, :two))).to eq(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) expect(@graph.vertex?(:one)).to be_truthy 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) expect(@graph.vertex?(:two)).to be_truthy 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 expect(edges).to be_instance_of(Array) expect(edges.length).to eq(2) expect(edges).to include(one) expect(edges).to 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) expect(@graph.edge?(edge.source, edge.target)).to be_falsey 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) expect(@graph.edge?(:one, :two)).to be_falsey expect(@graph.edge?(:two, :three)).to be_falsey expect(@graph.edges.length).to eq(0) end end describe "when finding adjacent vertices" do before do @graph = Puppet::Graph::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) expect(adj).to be_include(:three) expect(adj).to be_include(:two) end it "should default to finding :out vertices" do expect(@graph.adjacent(:two)).to eq([:three]) end it "should support selecting :in vertices" do expect(@graph.adjacent(:two, :direction => :in)).to eq([:one]) end it "should default to returning the matching vertices as an array of vertices" do expect(@graph.adjacent(:two)).to eq([:three]) end it "should support returning an array of matching edges" do expect(@graph.adjacent(:two, :type => :edges)).to eq([@two_three]) end # Bug #2111 it "should not consider a vertex adjacent just because it was asked about previously" do @graph = Puppet::Graph::SimpleGraph.new @graph.add_vertex("a") @graph.add_vertex("b") @graph.edge?("a", "b") expect(@graph.adjacent("a")).to eq([]) end end describe "when clearing" do before do @graph = Puppet::Graph::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 expect(@graph.vertices).to be_empty end it "should remove all edges" do expect(@graph.edges).to be_empty end end describe "when reversing graphs" do before do @graph = Puppet::Graph::SimpleGraph.new end it "should provide a method for reversing the graph" do @graph.add_edge(:one, :two) expect(@graph.reversal.edge?(:two, :one)).to be_truthy end it "should add all vertices to the reversed graph" do @graph.add_edge(:one, :two) expect(@graph.vertex?(:one)).to be_truthy expect(@graph.vertex?(:two)).to be_truthy end it "should retain labels on edges" do @graph.add_edge(:one, :two, :callback => :awesome) edge = @graph.reversal.edges_between(:two, :one)[0] expect(edge.label).to eq({:callback => :awesome}) end end describe "when reporting cycles in the graph" do before do @graph = Puppet::Graph::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 |cycle| cycle.map do |resource| resource.name end end end def expect_cycle_to_include(cycle, *resource_names) resource_names.each_with_index do |resource, index| expect(cycle[index].ref).to eq("Notify[#{resource}]") end end it "should report one-vertex loops" do add_edges :a => :a expect(Puppet).to receive(:err).with(/Found 1 dependency cycle:\n\(Notify\[a\] => Notify\[a\]\)/) cycle = @graph.report_cycles_in_graph.first expect_cycle_to_include(cycle, :a) end it "should report two-vertex loops" do add_edges :a => :b, :b => :a expect(Puppet).to receive(:err).with(/Found 1 dependency cycle:\n\(Notify\[a\] => Notify\[b\] => Notify\[a\]\)/) cycle = @graph.report_cycles_in_graph.first expect_cycle_to_include(cycle, :a, :b) end it "should report multi-vertex loops" do add_edges :a => :b, :b => :c, :c => :a expect(Puppet).to receive(:err).with(/Found 1 dependency cycle:\n\(Notify\[a\] => Notify\[b\] => Notify\[c\] => Notify\[a\]\)/) cycle = @graph.report_cycles_in_graph.first expect_cycle_to_include(cycle, :a, :b, :c) end it "should report when a larger tree contains a small cycle" do add_edges :a => :b, :b => :a, :c => :a, :d => :c expect(Puppet).to receive(:err).with(/Found 1 dependency cycle:\n\(Notify\[a\] => Notify\[b\] => Notify\[a\]\)/) cycle = @graph.report_cycles_in_graph.first expect_cycle_to_include(cycle, :a, :b) end it "should succeed on trees with no cycles" do add_edges :a => :b, :b => :e, :c => :a, :d => :c expect(Puppet).not_to receive(:err) expect(@graph.report_cycles_in_graph).to be_nil 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" expect(simplify(@graph.find_cycles_in_graph)).to eq([["a", "b"]]) end it "cycle discovery handles a self-loop cycle" do add_edges :a => :a expect(simplify(@graph.find_cycles_in_graph)).to eq([["a"]]) end it "cycle discovery should handle two distinct cycles" do add_edges "a" => "a1", "a1" => "a" add_edges "b" => "b1", "b1" => "b" expect(simplify(@graph.find_cycles_in_graph)).to eq([["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" expect(simplify(@graph.find_cycles_in_graph)).to eq([%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" expect(simplify(@graph.find_cycles_in_graph)).to eq([%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 expect(simplify(@graph.find_cycles_in_graph)).to eq([]) 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) expect(simplify(paths)).to eq([%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 expect(cycles.length).to eq(1) paths = @graph.paths_in_cycle(cycles.first, 100) expect(simplify(paths)).to eq([%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 expect(cycles.length).to eq(1) paths = @graph.paths_in_cycle(cycles.first, 100) expect(simplify(paths)).to eq([%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 expect(cycles.length).to eq(1) (1..20).each do |n| paths = @graph.paths_in_cycle(cycles.first, n) expect(paths.length).to eq(n) end paths = @graph.paths_in_cycle(cycles.first, 21) expect(paths.length).to eq(20) end end describe "when writing dot files" do before do @graph = Puppet::Graph::SimpleGraph.new @name = :test @file = File.join(Puppet[:graphdir], @name.to_s + ".dot") end it "should only write when graphing is enabled" do expect(File).not_to receive(:open).with(@file) Puppet[:graph] = false @graph.write_graph(@name) end it "should write a dot file based on the passed name" do expect(File).to receive(:open).with(@file, "w:UTF-8").and_yield(double("file", :puts => nil)) expect(@graph).to receive(:to_dot).with("name" => @name.to_s.capitalize) Puppet[:graph] = true @graph.write_graph(@name) end end describe Puppet::Graph::SimpleGraph do before do @graph = Puppet::Graph::SimpleGraph.new end it "should correctly clear vertices and edges when asked" do @graph.add_edge("a", "b") @graph.add_vertex "c" @graph.clear expect(@graph.vertices).to be_empty expect(@graph.edges).to be_empty end end describe "when matching edges" do before do @graph = Puppet::Graph::SimpleGraph.new # Resource is a String here although not for realz. Stub [] to always return nil # because indexing a String with a non-Integer throws an exception (and none of # these tests need anything meaningful from []). resource = "a" allow(resource).to receive(:[]) @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 expect(@graph.matching_edges(@event)).to eq([@edges["a/b"]]) end it "should match always match nothing when the event is :NONE" do expect(@graph.matching_edges(@none)).to be_empty end it "should match multiple edges" do @graph.add_edge(@edges["a/c"]) edges = @graph.matching_edges(@event) expect(edges).to be_include(@edges["a/b"]) expect(edges).to be_include(@edges["a/c"]) end end describe "when determining dependencies" do before do @graph = Puppet::Graph::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 expect(@graph.dependents("a").sort).to eq(%w{b c d}.sort) end it "should find single dependents" do expect(@graph.dependents("b").sort).to eq(%w{d}.sort) end it "should return an empty array when there are no dependents" do expect(@graph.dependents("c").sort).to eq([].sort) end it "should find all dependencies when they are on multiple levels" do expect(@graph.dependencies("d").sort).to eq(%w{a b}) end it "should find single dependencies" do expect(@graph.dependencies("c").sort).to eq(%w{a}) end it "should return an empty array when there are no dependencies" do expect(@graph.dependencies("a").sort).to eq([]) end end it "should serialize to YAML using the old format by default" do expect(Puppet::Graph::SimpleGraph.use_new_yaml_format).to eq(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::Graph::SimpleGraph.use_new_yaml_format Puppet::Graph::SimpleGraph.use_new_yaml_format = (which_format == :new) if block_given? yield else YAML.dump(graph) end ensure Puppet::Graph::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::Graph::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 = Puppet::Util::Yaml.safe_load(yaml_form, [Symbol,Puppet::Graph::SimpleGraph]) # 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. expect(serialized_object.keys.reject { |x| x == 'reversal' }.sort).to eq(['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['edges'] expect(edges).to 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| %w{source target}.each { |x| expect(edge.keys).to include(x) } edge.keys.each { |x| expect(['source', 'target', 'callback', 'event']).to include(x) } %w{source target callback event}.collect { |x| edge[x] } end expect(Set.new(actual_edge_tuples)).to eq(Set.new(expected_edge_tuples.map { |tuple| tuple.map {|e| e.nil? ? nil : e.to_s }})) expect(actual_edge_tuples.length).to eq(expected_edge_tuples.length) # Check vertices one by one. vertices = serialized_object['vertices'] if which_format == :old expect(vertices).to be_a(Hash) expect(Set.new(vertices.keys)).to eq(Set.new(graph.vertices)) vertices.each do |key, value| expect(value.keys.sort).to eq(%w{adjacencies vertex}) expect(value['vertex']).to eq(key) adjacencies = value['adjacencies'] expect(adjacencies).to be_a(Hash) expect(Set.new(adjacencies.keys)).to eq(Set.new(['in', 'out'])) [:in, :out].each do |direction| direction_hash = adjacencies[direction.to_s] expect(direction_hash).to be_a(Hash) expected_adjacent_vertices = Set.new(graph.adjacent(key, :direction => direction, :type => :vertices)) expect(Set.new(direction_hash.keys)).to eq(expected_adjacent_vertices) direction_hash.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['source'] == desired_source && edge['target'] == desired_target end expect(adj_value).to be_a(Array) if adj_value != 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 expect(vertices).to be_a(Array) expect(Set.new(vertices)).to eq(Set.new(graph.vertices)) expect(vertices.length).to eq(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::Graph::SimpleGraph.new send(graph_to_test, reference_graph) yaml_form = graph_to_yaml(reference_graph, which_format) recovered_graph = Puppet::Util::Yaml.safe_load(yaml_form, [Symbol,Puppet::Graph::SimpleGraph]) # 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 expect(Set.new(recovered_vertices)).to eq(Set.new(expected_vertices)) expect(recovered_vertices.length).to eq(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 expect(Set.new(recovered_edge_tuples)).to eq(Set.new(expected_edge_tuples)) expect(recovered_edge_tuples.length).to eq(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 end it "should serialize properly when used as a base class" do class Puppet::TestDerivedClass < Puppet::Graph::SimpleGraph attr_accessor :foo def initialize_from_hash(hash) super(hash) @foo = hash['foo'] end def to_data_hash super.merge('foo' => @foo) end end derived = Puppet::TestDerivedClass.new derived.add_edge('a', 'b') derived.foo = 1234 yaml = YAML.dump(derived) recovered_derived = Puppet::Util::Yaml.safe_load(yaml, [Puppet::TestDerivedClass]) expect(recovered_derived.class).to equal(Puppet::TestDerivedClass) expect(recovered_derived.edges.length).to eq(1) expect(recovered_derived.edges[0].source).to eq('a') expect(recovered_derived.edges[0].target).to eq('b') expect(recovered_derived.vertices.length).to eq(2) expect(recovered_derived.foo).to eq(1234) end end end