From b4b6c5ced7995ffdbf888dd24ec9a4a2ac8d347c Mon Sep 17 00:00:00 2001 From: Tim Sharpe Date: Tue, 19 Sep 2017 16:22:34 +1000 Subject: [PATCH 1/3] Basic resource matching --- lib/rspec-puppet.rb | 2 ++ lib/rspec-puppet/support.rb | 2 +- lib/rspec-puppet/v3.rb | 44 +++++++++++++++++++++++++++++++++ rspec-puppet.gemspec | 1 + spec/classes/test_basic_spec.rb | 37 +++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 lib/rspec-puppet/v3.rb diff --git a/lib/rspec-puppet.rb b/lib/rspec-puppet.rb index b32cc579d..22d53e216 100644 --- a/lib/rspec-puppet.rb +++ b/lib/rspec-puppet.rb @@ -1,5 +1,6 @@ require 'puppet' require 'rspec' +require 'rspec/its' require 'fileutils' require 'tmpdir' require 'rspec-puppet/errors' @@ -9,6 +10,7 @@ require 'rspec-puppet/coverage' require 'rspec-puppet/adapters' require 'rspec-puppet/consts' +require 'rspec-puppet/v3' begin require 'puppet/test/test_helper' diff --git a/lib/rspec-puppet/support.rb b/lib/rspec-puppet/support.rb index 0789a9e6a..1f09e30f0 100644 --- a/lib/rspec-puppet/support.rb +++ b/lib/rspec-puppet/support.rb @@ -7,7 +7,7 @@ module Support @@cache = RSpec::Puppet::Cache.new def subject - lambda { catalogue } + described_class || lambda { catalogue } end def environment diff --git a/lib/rspec-puppet/v3.rb b/lib/rspec-puppet/v3.rb new file mode 100644 index 000000000..55d444da3 --- /dev/null +++ b/lib/rspec-puppet/v3.rb @@ -0,0 +1,44 @@ +module RSpec::Puppet + module V3 + def puppet_resource(type, title) + RSpec::Puppet::V3::Resource.new(type, title) + end + + class Resource + def initialize(type, title) + @type = type + @title = title + end + + def to_s + "#{@type.capitalize}[#{@title}]" + end + alias_method :inspect, :to_s + + def catalogue + @catalogue ||= RSpec.current_example.example_group_instance.catalogue + end + + def catalogue_resource + @catalogue_resource ||= catalogue.resource(@type, @title) + end + + def parameters + exist? ? catalogue_resource.to_hash : {} + end + alias_method :params, :parameters + + def exist? + RSpec::Puppet::Coverage.cover!(catalogue_resource) + !catalogue_resource.nil? + end + alias_method :in_catalogue?, :exist? + alias_method :in_catalog?, :exist? + end + end +end + +# TODO: Limit this to only rspec-puppet example groups +class RSpec::Core::ExampleGroup + extend RSpec::Puppet::V3 +end diff --git a/rspec-puppet.gemspec b/rspec-puppet.gemspec index c154fc2ce..4deff5092 100644 --- a/rspec-puppet.gemspec +++ b/rspec-puppet.gemspec @@ -11,6 +11,7 @@ Gem::Specification.new do |s| s.files = Dir['CHANGELOG.md', 'LICENSE.md', 'README.md', 'lib/**/*', 'bin/**/*'] s.add_dependency 'rspec' + s.add_dependency 'rspec-its' s.authors = ['Tim Sharpe'] s.email = 'tim@sharpe.id.au' diff --git a/spec/classes/test_basic_spec.rb b/spec/classes/test_basic_spec.rb index 63427309b..6cf2be1a6 100644 --- a/spec/classes/test_basic_spec.rb +++ b/spec/classes/test_basic_spec.rb @@ -3,6 +3,13 @@ describe 'test::basic' do it { should contain_fake('foo').with_three([{'foo' => 'bar'}]) } + context 'using the new syntax', :if => RSpec::Core::Version::STRING.start_with?('3') do + describe puppet_resource('fake', 'foo') do + it { is_expected.to be_in_catalogue } + its(:params) { is_expected.to include(:three => [{'foo' => 'bar'}]) } + end + end + context 'testing node based facts' do let(:pre_condition) { 'notify { $::fqdn: }' } let(:node) { 'test123.test.com' } @@ -18,11 +25,31 @@ it { should contain_notify('test123.test.com') } it { should_not contain_notify('notthis.test.com') } + context 'using the new syntax', :if => RSpec::Core::Version::STRING.start_with?('3') do + describe puppet_resource('notify', 'test123.test.com') do + it { is_expected.to exist } + end + + describe puppet_resource('notify', 'notthis.test.com') do + it { is_expected.not_to exist } + end + end + context 'existing networking facts should not be clobbered', :if => Puppet.version.to_f >= 4.0 do let(:pre_condition) { 'notify { [$facts["networking"]["primary"], $facts["networking"]["hostname"]]: }' } it { should contain_notify('eth0') } it { should contain_notify('test123') } + + context 'using the new syntax', :if => RSpec::Core::Version::STRING.start_with?('3') do + describe puppet_resource('notify', 'eth0') do + it { is_expected.to exist } + end + + describe puppet_resource('notify', 'test123') do + it { is_expected.to exist } + end + end end context 'when derive_node_facts_from_nodename => false' do @@ -40,6 +67,16 @@ it { should contain_notify('myhostname.test.com') } it { should_not contain_notify('mycertname.test.com') } + + context 'using the new syntax', :if => RSpec::Core::Version::STRING.start_with?('3') do + describe puppet_resource('notify', 'myhostname.test.com') do + it { is_expected.to exist } + end + + describe puppet_resource('notify', 'mycertname.test.com') do + it { is_expected.not_to exist } + end + end end end end From ddf6ed402fc7daeba6d90b42afe5bda33f997772 Mon Sep 17 00:00:00 2001 From: Tim Sharpe Date: Tue, 19 Sep 2017 16:26:46 +1000 Subject: [PATCH 2/3] Split relationship matching into helper module --- lib/rspec-puppet.rb | 1 + lib/rspec-puppet/helpers.rb | 6 ++ lib/rspec-puppet/helpers/relationships.rb | 112 ++++++++++++++++++++ lib/rspec-puppet/matchers/create_generic.rb | 111 +------------------ 4 files changed, 122 insertions(+), 108 deletions(-) create mode 100644 lib/rspec-puppet/helpers.rb create mode 100644 lib/rspec-puppet/helpers/relationships.rb diff --git a/lib/rspec-puppet.rb b/lib/rspec-puppet.rb index 22d53e216..f26cecd3b 100644 --- a/lib/rspec-puppet.rb +++ b/lib/rspec-puppet.rb @@ -4,6 +4,7 @@ require 'fileutils' require 'tmpdir' require 'rspec-puppet/errors' +require 'rspec-puppet/helpers' require 'rspec-puppet/matchers' require 'rspec-puppet/example' require 'rspec-puppet/setup' diff --git a/lib/rspec-puppet/helpers.rb b/lib/rspec-puppet/helpers.rb new file mode 100644 index 000000000..ef173e814 --- /dev/null +++ b/lib/rspec-puppet/helpers.rb @@ -0,0 +1,6 @@ +module RSpec::Puppet + module Helpers + end +end + +require 'rspec-puppet/helpers/relationships' diff --git a/lib/rspec-puppet/helpers/relationships.rb b/lib/rspec-puppet/helpers/relationships.rb new file mode 100644 index 000000000..790e3ce91 --- /dev/null +++ b/lib/rspec-puppet/helpers/relationships.rb @@ -0,0 +1,112 @@ +module RSpec::Puppet::Helpers + module Relationships + def resource_ref(resource) + resource.respond_to?(:to_ref) ? resource.to_ref : resource + end + + def resource_from_ref(ref) + ref.is_a?(Puppet::Resource) ? ref : catalogue.resource(ref) + end + + def canonicalize_resource(resource) + res = resource_from_ref(resource_ref(resource)) + + if res.nil? + resource = Struct.new(:type, :title).new(*catalogue.title_key_for_ref(resource)) if resource.is_a?(String) + + res = catalogue.resource_keys.select { |type, title| + type == resource.type + }.map { |type, title| + catalogue.resource(type, title) + }.compact.find { |cat_res| + cat_res.builtin_type? && cat_res.uniqueness_key.first == resource.title + } + end + + res + end + + def canonicalize_resource_ref(ref) + resource_ref(resource_from_ref(ref)) + end + + def relationship_refs(resource, relationship_type, visited = Set.new) + resource = canonicalize_resource(resource) + results = Set.new + return results if resource.nil? + + if visited.include?(resource.object_id) + return [canonicalize_resource_ref(resource)] + end + + visited << resource.object_id + + Array[resource[relationship_type]].flatten.compact.each do |r| + results << canonicalize_resource_ref(r) + results << relationship_refs(r, relationship_type, visited) + + res = canonicalize_resource(r) + if res && res.builtin_type? + results << res.to_ref + results << "#{res.type.to_s.capitalize}[#{res.uniqueness_key.first}]" + end + end + + # Add any autorequires + Puppet::Type.suppress_provider + if relationship_type == :require && resource.resource_type.respond_to?(:eachautorequire) + resource.resource_type.eachautorequire do |t, b| + Array(resource.to_ral.instance_eval(&b)).each do |dep| + res = "#{t.to_s.capitalize}[#{dep}]" + + if r = relationship_refs(res, relationship_type, visited) + results << res + results << r + end + end + end + end + Puppet::Type.unsuppress_provider + + results.flatten + end + + def self_or_upstream(vertex) + [vertex] + catalogue.upstream_from_vertex(vertex).keys + end + + def precedes?(first, second) + return false if first.nil? || second.nil? + + self_or_upstream(first).each do |u| + self_or_upstream(second).each do |v| + before_refs = relationship_refs(u, :before) + relationship_refs(u, :notify) + require_refs = relationship_refs(v, :require) + relationship_refs(u, :subscribe) + + if before_refs.include?(v.to_ref) || require_refs.include?(u.to_ref) || (before_refs & require_refs).any? + return true + end + end + end + + false + end + + def notifies?(first, second) + return false if first.nil? || second.nil? + + self_or_upstream(first).each do |u| + self_or_upstream(second).each do |v| + notify_refs = relationship_refs(u, :notify) + subscribe_refs = relationship_refs(v, :subscribe) + + if notify_refs.include?(v.to_ref) || subscribe_refs.include?(u.to_ref) + return true + end + end + end + + false + end + end +end diff --git a/lib/rspec-puppet/matchers/create_generic.rb b/lib/rspec-puppet/matchers/create_generic.rb index 25f59c09d..1e75c57ca 100644 --- a/lib/rspec-puppet/matchers/create_generic.rb +++ b/lib/rspec-puppet/matchers/create_generic.rb @@ -5,6 +5,9 @@ module RSpec::Puppet module ManifestMatchers class CreateGeneric include RSpec::Puppet::Errors + include RSpec::Puppet::Helpers::Relationships + + attr_reader :catalogue def initialize(*args, &block) @exp_resource_type = args.shift.to_s.gsub(/^(create|contain)_/, '') @@ -244,114 +247,6 @@ def check_subscribes(catalogue, resource) end end - def resource_ref(resource) - resource.respond_to?(:to_ref) ? resource.to_ref : resource - end - - def resource_from_ref(ref) - ref.is_a?(Puppet::Resource) ? ref : @catalogue.resource(ref) - end - - def canonicalize_resource(resource) - res = resource_from_ref(resource_ref(resource)) - if res.nil? - resource = Struct.new(:type, :title).new(*@catalogue.title_key_for_ref(resource)) if resource.is_a?(String) - res = @catalogue.resource_keys.select { |type, name| - type == resource.type - }.map { |type, name| - @catalogue.resource(type, name) - }.compact.find { |cat_res| - cat_res.builtin_type? && cat_res.uniqueness_key.first == resource.title - } - end - res - end - - def canonicalize_resource_ref(ref) - resource_ref(resource_from_ref(ref)) - end - - def relationship_refs(resource, type, visited = Set.new) - resource = canonicalize_resource(resource) - results = Set.new - return results unless resource - - # guard to prevent infinite recursion - if visited.include?(resource.object_id) - return [canonicalize_resource_ref(resource)] - else - visited << resource.object_id - end - - Array[resource[type]].flatten.compact.each do |r| - results << canonicalize_resource_ref(r) - results << relationship_refs(r, type, visited) - - res = canonicalize_resource(r) - if res && res.builtin_type? - results << res.to_ref - results << "#{res.type.to_s.capitalize}[#{res.uniqueness_key.first}]" - end - end - - Puppet::Type.suppress_provider - # Add autorequires if any - if type == :require and resource.resource_type.respond_to? :eachautorequire - resource.resource_type.eachautorequire do |t, b| - Array(resource.to_ral.instance_eval(&b)).each do |dep| - res = "#{t.to_s.capitalize}[#{dep}]" - if r = relationship_refs(res, type, visited) - results << res - results << r - end - end - end - end - Puppet::Type.unsuppress_provider - - results.flatten - end - - def self_or_upstream(vertex) - [vertex] + @catalogue.upstream_from_vertex(vertex).keys - end - - def precedes?(first, second) - return false if first.nil? || second.nil? - - self_or_upstream(first).each do |u| - self_or_upstream(second).each do |v| - before_refs = relationship_refs(u, :before) + relationship_refs(u, :notify) - require_refs = relationship_refs(v, :require) + relationship_refs(u, :subscribe) - - if before_refs.include?(v.to_ref) || require_refs.include?(u.to_ref) || (before_refs & require_refs).any? - return true - end - end - end - - # Nothing found - return false - end - - def notifies?(first, second) - return false if first.nil? || second.nil? - - self_or_upstream(first).each do |u| - self_or_upstream(second).each do |v| - notify_refs = relationship_refs(u, :notify) - subscribe_refs = relationship_refs(v, :subscribe) - - if notify_refs.include?(v.to_ref) || subscribe_refs.include?(u.to_ref) - return true - end - end - end - - # Nothing found - return false - end - # @param resource [Hash] The resource in the catalog # @param list [Array] The expected values of the resource # @param type [:should, :not] Whether the given parameters should/not match From 3b0c8c2f2c65c562cccc76d4f161d594e14f62af Mon Sep 17 00:00:00 2001 From: Tim Sharpe Date: Tue, 19 Sep 2017 18:03:02 +1000 Subject: [PATCH 3/3] Add relationship matchers for new syntax --- lib/rspec-puppet/matchers.rb | 5 +++ lib/rspec-puppet/matchers/come_before.rb | 25 +++++++++++++ lib/rspec-puppet/matchers/notify.rb | 25 +++++++++++++ lib/rspec-puppet/matchers/require.rb | 25 +++++++++++++ lib/rspec-puppet/matchers/subscribe_to.rb | 25 +++++++++++++ lib/rspec-puppet/v3.rb | 43 +++++++++++++++++++++++ spec/classes/relationship__before_spec.rb | 37 +++++++++++++++++++ spec/classes/relationship__notify_spec.rb | 31 ++++++++++++++++ 8 files changed, 216 insertions(+) create mode 100644 lib/rspec-puppet/matchers/come_before.rb create mode 100644 lib/rspec-puppet/matchers/notify.rb create mode 100644 lib/rspec-puppet/matchers/require.rb create mode 100644 lib/rspec-puppet/matchers/subscribe_to.rb diff --git a/lib/rspec-puppet/matchers.rb b/lib/rspec-puppet/matchers.rb index eb3c307e8..b4dc17c73 100644 --- a/lib/rspec-puppet/matchers.rb +++ b/lib/rspec-puppet/matchers.rb @@ -6,3 +6,8 @@ require 'rspec-puppet/matchers/dynamic_matchers' require 'rspec-puppet/matchers/type_matchers' require 'rspec-puppet/matchers/allow_value' + +require 'rspec-puppet/matchers/notify' +require 'rspec-puppet/matchers/subscribe_to' +require 'rspec-puppet/matchers/require' +require 'rspec-puppet/matchers/come_before' diff --git a/lib/rspec-puppet/matchers/come_before.rb b/lib/rspec-puppet/matchers/come_before.rb new file mode 100644 index 000000000..937c16b43 --- /dev/null +++ b/lib/rspec-puppet/matchers/come_before.rb @@ -0,0 +1,25 @@ +module RSpec::Puppet + module ManifestMatchers + extend RSpec::Matchers::DSL + + matcher :come_before do |*preceding_resources| + match do |resource| + preceding_resources.flatten.all? do |expected_precede| + resource.comes_before_resource?(expected_precede) + end + end + + description do + "come before #{preceding_resources.flatten.join(', ')}" + end + + failure_message do |resource| + "expected to come before #{preceding_resources.flatten.join(', ')}" + end + + failure_message_when_negated do |resource| + "expected not to come before #{preceding_resources.flatten.join(', ')}" + end + end + end +end diff --git a/lib/rspec-puppet/matchers/notify.rb b/lib/rspec-puppet/matchers/notify.rb new file mode 100644 index 000000000..f01d8d348 --- /dev/null +++ b/lib/rspec-puppet/matchers/notify.rb @@ -0,0 +1,25 @@ +module RSpec::Puppet + module ManifestMatchers + extend RSpec::Matchers::DSL + + matcher :notify do |*notified_resources| + match do |resource| + notified_resources.flatten.all? do |expected_notify| + resource.notifies_resource?(expected_notify) + end + end + + description do + "notify #{notified_resources.flatten.join(', ')}" + end + + failure_message do |resource| + "expected to notify #{notified_resources.flatten.join(', ')}" + end + + failure_message_when_negated do |resource| + "expected not to notify #{notified_resources.flatten.join(', ')}" + end + end + end +end diff --git a/lib/rspec-puppet/matchers/require.rb b/lib/rspec-puppet/matchers/require.rb new file mode 100644 index 000000000..3905d5b00 --- /dev/null +++ b/lib/rspec-puppet/matchers/require.rb @@ -0,0 +1,25 @@ +module RSpec::Puppet + module ManifestMatchers + extend RSpec::Matchers::DSL + + matcher :require do |*required_resources| + match do |resource| + required_resources.flatten.all? do |expected_require| + resource.requires_resource?(expected_require) + end + end + + description do + "require #{required_resources.flatten.join(', ')}" + end + + failure_message do |resource| + "expected to require #{required_resources.flatten.join(', ')}" + end + + failure_message_when_negated do |resource| + "expected not to require #{required_resources.flatten.join(', ')}" + end + end + end +end diff --git a/lib/rspec-puppet/matchers/subscribe_to.rb b/lib/rspec-puppet/matchers/subscribe_to.rb new file mode 100644 index 000000000..963b4beb8 --- /dev/null +++ b/lib/rspec-puppet/matchers/subscribe_to.rb @@ -0,0 +1,25 @@ +module RSpec::Puppet + module ManifestMatchers + extend RSpec::Matchers::DSL + + matcher :subscribe_to do |*subscribed_resources| + match do |resource| + subscribed_resources.flatten.all? do |expected_subscribe| + resource.subscribes_to_resource?(expected_subscribe) + end + end + + description do + "subscribe to #{subscribed_resources.flatten.join(', ')}" + end + + failure_message do |resource| + "expected to subscribe to #{subscribed_resources.flatten.join(', ')}" + end + + failure_message_when_negated do |resource| + "expected not to subscribe to #{subscribed_resources.flatten.join(', ')}" + end + end + end +end diff --git a/lib/rspec-puppet/v3.rb b/lib/rspec-puppet/v3.rb index 55d444da3..d183c3424 100644 --- a/lib/rspec-puppet/v3.rb +++ b/lib/rspec-puppet/v3.rb @@ -5,6 +5,8 @@ def puppet_resource(type, title) end class Resource + include RSpec::Puppet::Helpers::Relationships + def initialize(type, title) @type = type @title = title @@ -34,6 +36,46 @@ def exist? end alias_method :in_catalogue?, :exist? alias_method :in_catalog?, :exist? + + def sanitise_resource(resource) + if resource.is_a?(self.class) + resource.catalogue_resource + else + canonicalize_resource(resource) + end + end + + def notifies_resource?(other_resource) + return false unless exist? + other_resource = sanitise_resource(other_resource) + return false if other_resource.nil? + + notifies?(catalogue_resource, other_resource) + end + + def subscribes_to_resource?(other_resource) + return false unless exist? + other_resource = sanitise_resource(other_resource) + return false if other_resource.nil? + + notifies?(other_resource, catalogue_resource) + end + + def requires_resource?(other_resource) + return false unless exist? + other_resource = sanitise_resource(other_resource) + return false if other_resource.nil? + + precedes?(other_resource, catalogue_resource) + end + + def comes_before_resource?(other_resource) + return false unless exist? + other_resource = sanitise_resource(other_resource) + return false if other_resource.nil? + + precedes?(catalogue_resource, other_resource) + end end end end @@ -41,4 +83,5 @@ def exist? # TODO: Limit this to only rspec-puppet example groups class RSpec::Core::ExampleGroup extend RSpec::Puppet::V3 + include RSpec::Puppet::V3 end diff --git a/spec/classes/relationship__before_spec.rb b/spec/classes/relationship__before_spec.rb index 206a1f5b3..69209001c 100644 --- a/spec/classes/relationship__before_spec.rb +++ b/spec/classes/relationship__before_spec.rb @@ -40,4 +40,41 @@ it { should_not contain_class('relationship::before::pre').that_comes_before('Class[relationship::before::unknown]') } it { should_not contain_class('relationship::before::post').that_requires('Class[relationship::before::unknown]') } + + context 'using the new syntax', :if => RSpec::Core::Version::STRING.start_with?('3') do + describe puppet_resource('notify', 'foo') do + it { is_expected.to come_before('Notify[bar]') } + it { is_expected.to come_before('Notify[baz]') } + it { is_expected.to come_before('Notify[bar]').and come_before('Notify[baz]') } + it { is_expected.to come_before('Notify[bar]', 'Notify[baz]') } + it { is_expected.to come_before(['Notify[bar]', 'Notify[baz]']) } + end + + describe puppet_resource('notify', 'bar') do + it { is_expected.to come_before('Notify[baz]') } + it { is_expected.to require('Notify[foo]') } + end + + describe puppet_resource('notify', 'baz') do + it { is_expected.to require('Notify[foo]') } + it { is_expected.to require('Notify[bar]') } + it { is_expected.to require('Notify[foo]', 'Notify[bar]') } + end + + describe puppet_resource('class', 'relationship::before::pre') do + it { is_expected.to come_before('Class[relationship::before::post]') } + end + + describe puppet_resource('class', 'relationship::before::post') do + it { is_expected.to require('Class[relationship::before::pre]') } + end + + describe puppet_resource('file', '/tmp/foo') do + it { is_expected.to come_before('File[/tmp/foo/bar]') } + end + + describe puppet_resource('file', '/tmp/foo/bar') do + it { is_expected.to require('File[/tmp/foo]') } + end + end end diff --git a/spec/classes/relationship__notify_spec.rb b/spec/classes/relationship__notify_spec.rb index c4d4c2f58..47bd4e2e7 100644 --- a/spec/classes/relationship__notify_spec.rb +++ b/spec/classes/relationship__notify_spec.rb @@ -17,4 +17,35 @@ it { should contain_notify('pre').that_notifies(['Notify[post]']) } it { should contain_notify('post').that_subscribes_to(['Notify[pre]']) } + + context 'using the new syntax', :if => RSpec::Core::Version::STRING.start_with?('3') do + describe puppet_resource('notify', 'foo') do + it { is_expected.to notify('Notify[bar]') } + end + + describe puppet_resource('notify', 'baz') do + it { is_expected.to notify('Notify[bar]') } + it { is_expected.to notify('Notify[gronk]') } + it { is_expected.to notify(['Notify[bar]', 'Notify[gronk]']) } + it { is_expected.to notify('Notify[bar]', 'Notify[gronk]') } + end + + describe puppet_resource('notify', 'gronk') do + it { is_expected.to subscribe_to('Notify[baz]') } + end + + describe puppet_resource('notify', 'bar') do + it { is_expected.to subscribe_to('Notify[baz]', 'Notify[foo]') } + it { is_expected.to subscribe_to(['Notify[baz]', 'Notify[foo]']) } + end + + describe puppet_resource('notify', 'pre') do + it { is_expected.to notify('Notify[post]') } + it { is_expected.to notify(puppet_resource('notify', 'post')) } + end + + describe puppet_resource('notify', 'post') do + it { is_expected.to subscribe_to('Notify[pre]') } + end + end end