Skip to content

Commit

Permalink
Merge pull request #765 from jeffmccune/759_provider_spec_tests
Browse files Browse the repository at this point in the history
(#759) Add reference spec tests for sensu_check JSON provider
  • Loading branch information
ghoneycutt authored Jul 25, 2017
2 parents 2c64888 + 6fb1efd commit 2662f12
Show file tree
Hide file tree
Showing 8 changed files with 326 additions and 4 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ end
group :development, :unit_tests do
gem 'rake', '< 11.0.0'
gem 'rspec-puppet', '~> 2.5.0', :require => false
gem 'rspec-mocks', :require => false
gem 'puppetlabs_spec_helper', '>= 2.0.0', :require => false
gem 'puppet-lint', "~> 2.0", :require => false
gem 'json', "~> 1.8.3", :require => false
Expand Down
52 changes: 48 additions & 4 deletions lib/puppet/provider/sensu_check/json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,63 @@

SENSU_CHECK_PROPERTIES = Puppet::Type.type(:sensu_check).validproperties.reject { |p| p == :ensure }

# read_file provides a well-known location for spec tests to intercept and
# stub out filesystem calls. File.read itself is not stubbed out because
# File.read is called from many places. This helper method affords precision
# to the spec examples.
#
# @param [String] fpath the fully qualified path to read.
#
# @return [String] the file content.
def self.read_file(fpath)
File.read(fpath)
end

# Passes through to .read_file
def read_file(fpath)
self.class.read_file(fpath)
end

# Write a string to a file. Note, `puts` is used to write data which will
# insert a trailing newline if absent.
#
# @param [String] fpath the full qualified path to write.
#
# @param [String] data the data to write.
def self.write_output(fpath, data)
File.open(fpath, 'w') do |f|
f.puts(data)
end
end

# provide a well-known location for spec tests to intercept and stub out
# filesystem calls.
#
# @param [String] fpath the fully qualified path to read.
#
# @param [<Hash,Array>] obj The JSON object to write out to fpath.
#
# @return [String] the file content.
def self.write_json_object(fpath, obj)
write_output(fpath, JSON.pretty_generate(obj))
end

# Passes through to .write_json_object
def write_json_object(fpath, obj)
self.class.write_json_object(fpath, obj)
end

def conf
begin
@conf ||= JSON.parse(File.read(config_file))
@conf ||= JSON.parse(read_file(config_file))
rescue
@conf ||= {}
end
end

def flush
sort_properties!
File.open(config_file, 'w') do |f|
f.puts JSON.pretty_generate(conf)
end
write_json_object(config_file, conf)
end

def pre_create
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"checks": {
"remote_http": {
"command": "/opt/sensu/embedded/bin/check-http.rb -u http://:::address:::",
"foo": "bar",
"high_flap_threshold": 60,
"interval": 300,
"low_flap_threshold": 20,
"occurrences": 2,
"proxy_requests": {
"client_attributes": {
"subscriptions": "eval: value.include?(\"http\")"
}
},
"refresh": 600,
"standalone": false,
"subscribers": [
"roundrobin:poller"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"checks": {
"remote_http": {
"command": "/opt/sensu/embedded/bin/check-http.rb -u http://:::address:::",
"high_flap_threshold": 60,
"interval": 300,
"low_flap_threshold": 20,
"occurrences": 2,
"proxy_requests": {
"client_attributes": {
"subscriptions": "eval: value.include?(\"http\")"
}
},
"refresh": 600,
"standalone": false,
"subscribers": [
"roundrobin:poller"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"checks": {
"remote_http": {
"boolval": true,
"command": "/opt/sensu/embedded/bin/check-http.rb -u http://:::address:::",
"foo": "bar",
"high_flap_threshold": 60,
"in_array": [
"foo",
"baz"
],
"interval": 300,
"low_flap_threshold": 20,
"numval": 6,
"occurrences": 2,
"proxy_requests": {
"client_attributes": {
"subscriptions": "eval: value.include?(\"http\")"
}
},
"refresh": 600,
"standalone": false,
"subscribers": [
"roundrobin:poller"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"checks": {
"remote_http": {
"command": "/opt/sensu/embedded/bin/check-http.rb -u http://:::address:::",
"high_flap_threshold": 60,
"interval": 300,
"occurrences": 2,
"proxy_requests": {
"client_attributes": {
"subscriptions": "eval: value.include?(\"http\")"
}
},
"refresh": 600,
"standalone": false,
"low_flap_threshold": 20,
"in_array": [
"foo",
"baz"
],
"numval": 6,
"boolval": true,
"foo": "bar",
"subscribers": [
"roundrobin:poller"
]
}
}
}
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
end

RSpec.configure do |config|
config.mock_with :rspec
config.hiera_config = 'spec/fixtures/hiera/hiera.yaml'
config.before :each do
# Ensure that we don't accidentally cache facts and environment between
Expand Down
177 changes: 177 additions & 0 deletions spec/unit/provider/sensu_check/json_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
require 'spec_helper'

# The goal of the let methods are to wire up a provider into a harness used for
# testing. During Puppet runtime, there are multiple contexts a provider
# operates within. The two primary ones are enforcement; e.g. `puppet apply`
# mode, and introspection, e.g. `puppet resource` mode. During enforcement,
# there is an associated resource modeled in the catalog. During introspection,
# a resource is initially absent and the provider provides information to
# initialize the resource.
#
# Terminology used in the let helper methods.
#
# "type_id" refers to the Symbol identifying the Type. e.g. :sensu_check
#
# "resource" is an instance of Puppet::Type.type(type) as it would exist in the
# RAL during catalog application. This resource contains the desired state
# information, the properties and parameters specified in the Puppet DSL.
#
# "provider" is an instance of the provider class being tested. In Puppet,
# provider instances exist primarily in one of two states, either bound or not
# bound to a resource. Provider instances are not bound when the system is
# being introspected, e.g. `puppet resource service` calls the `instances` class
# method which will instantiate provider instances which have no associated
# resource. When applying a Puppet catalog, each provider is associated with
# exactly one resource from the Puppet DSL.
#
# Because of this dual nature, providers must be careful when accessing
# parameter data, e.g. `base_path`. Since `base_path` is a parameter, it will
# not be accessible in the context of self.instances and `puppet resource`,
# because there is not a bound resource when discovering resources.
#
# When building a new provider with spec tests, start with `self.instances`,
# because this approach exercises a provider with the minimal amount of state.
# That is to say, the provider must be well-behaved when there is no associated
# resource.
#
# property_hash or @property_hash is an instance variable describing the current
# state of the resource as it exists on the target system. Take care not to
# confuse this with the data contained in the resource, which describes desired
# state.
#
# property_flush or @property_flush is an instance variable used to modify the
# system from the `flush` method. Setter methods, one for each property of the
# resource type, should modify @property_flush

type_id = :sensu_check

describe Puppet::Type.type(type_id).provider(:json) do
let(:catalog) { Puppet::Resource::Catalog.new }
let(:type) { Puppet::Type.type(type_id) }
# The title of the resource, for convenience
let(:title) { 'remote_http' }

# The default resource hash modeling the resource in a manifest.
let(:rsrc_hsh_base) do
{ name: title, ensure: 'present' }
end
# Override this helper method in nested example groups
let(:rsrc_hsh_override) { {} }
# Combined resource hash. Used to initialize @provider_hash via new()
let(:rsrc_hsh) { rsrc_hsh_base.merge(rsrc_hsh_override) }
# A provider with @property_hash initialized, but without a resource.
let(:bare_provider) { described_class.new(rsrc_hsh) }
# A resource bound to bare_provider. This has the side-effect of associating
# the provider instance to a resource (bare_provider is no longer bare of a
# resource.)
let(:resource) { type.new(rsrc_hsh.merge(provider: bare_provider)) }
# A "harnessed" provider instance suitable for testing. @property_hash is
# initialized and provider.resource returns a Resource.
let(:provider) do
resource.provider
end

context 'during catalog application' do
describe 'parameters (provide data)' do
describe '#name' do
subject { provider.name }
it { is_expected.to eq title }
end
end

# Properties modify the system. Parameters add supporting data.
describe 'properties (take action)' do
describe 'when writing JSON data to the filesystem with #flush' do
describe '#custom' do
context 'with a pre-existing check definition' do
# An existing JSON file the provider will modify.
let(:input) do
File.read(my_fixture('mycheck_example_input.json'))
end
# Stub out the filesystem read with fixture data
before :each do
allow(provider).to receive(:read_file).and_return(input)
end

subject { provider.custom }

context 'without custom configuration' do
it { is_expected.to eq({}) }
end
context 'with custom configuration' do
let(:input) do
File.read(my_fixture('mycheck_custom_input.json'))
end
it { is_expected.to eq({'foo' => 'bar'}) }
end
end
end

describe '#custom=' do
context 'with pre-existing configuration on the system' do
# An existing JSON file the provider will modify.
let(:input) do
File.read(my_fixture('mycheck_example_input.json'))
end

let(:expected_output) do
File.read(my_fixture('mycheck_expected_output.json'))
end

before :each do
# The fixed input for testing. This is an expectation so a
# failure is triggered if the stub becomes mis-matched with the
# implemented behavior.
expect(provider).to receive(:read_file).and_return(input)
end

context 'with custom defined' do
# Example value for the custom property from the README
let(:custom) do
{
'foo' => 'bar',
'numval' => 6,
'boolval' => true,
'in_array' => ['foo','baz'],
}
end

# The desired state from the catalog
let(:rsrc_hsh_override) { {custom: custom} }

it 'writes the configuration file as a JSON object' do
# TODO: Would be nice to make this a shared expectation
expect(provider).to receive(:write_json_object) do |fp, obj|
expect(fp).to eq(provider.config_file)
ex_out = JSON.parse(expected_output)
check_def = ex_out['checks']['remote_http']
# This gives a nice diff if there is an issue
expect(obj['checks']['remote_http']).to eq(check_def)
# This tests the complete configuration
expect(obj).to eq(ex_out)
end

provider.custom = custom
provider.flush
end
end

context 'with unsorted input JSON' do
let(:input) do
File.read(my_fixture('mycheck_unsorted_input.json'))
end
it 'writes sorted JSON output' do
expect(described_class).to receive(:write_output) do |_, data|
# Trailing newlines must match to get a nice diff
# See: https://github.com/rspec/rspec-support/issues/70
expect(data).to eq(expected_output.chomp)
end
provider.flush
end
end
end
end
end
end
end
end

0 comments on commit 2662f12

Please sign in to comment.