├── spec ├── fixtures │ ├── hieradata │ │ ├── empty │ │ │ ├── empty.yaml │ │ │ └── not_empty.yaml │ │ ├── valid │ │ │ ├── other.foo │ │ │ ├── sub │ │ │ │ ├── nested.dot.yaml │ │ │ │ └── nested.yaml │ │ │ ├── sub2 │ │ │ │ ├── nested.dot.yaml │ │ │ │ └── nested.dot.yml │ │ │ └── valid.yaml │ │ └── invalid │ │ │ └── invalid.yaml │ └── templates │ │ └── returns_elephant.erb ├── spec_helper.rb ├── require_spec.rb └── classes │ ├── utils_spec.rb │ ├── mock_resource_spec.rb │ ├── template_harness_spec.rb │ ├── hieradata │ ├── yaml_validator_spec.rb │ └── validator_spec.rb │ ├── mock_function_spec.rb │ └── rake │ └── project_tasks_spec.rb ├── .gitignore ├── Gemfile ├── Rakefile ├── lib ├── rspec-puppet-utils.rb └── rspec_puppet_utils │ ├── template_harness.rb │ ├── mock_resource.rb │ ├── mock_function.rb │ ├── hieradata │ ├── yaml_validator.rb │ └── validator.rb │ └── rake │ └── project_tasks.rb ├── rspec-puppet-utils.gemspec ├── DESIGN.md ├── LICENSE.txt └── README.md /spec/fixtures/hieradata/empty/empty.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /spec/fixtures/hieradata/valid/other.foo: -------------------------------------------------------------------------------- 1 | --- 2 | 'smtp': 25 3 | -------------------------------------------------------------------------------- /spec/fixtures/templates/returns_elephant.erb: -------------------------------------------------------------------------------- 1 | <%= 'elephant' %> -------------------------------------------------------------------------------- /spec/fixtures/hieradata/empty/not_empty.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 'key' : 'value' 3 | -------------------------------------------------------------------------------- /spec/fixtures/hieradata/valid/sub/nested.dot.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | bar: 'baz' 3 | -------------------------------------------------------------------------------- /spec/fixtures/hieradata/valid/sub2/nested.dot.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | bar: 'baz' 3 | -------------------------------------------------------------------------------- /spec/fixtures/hieradata/valid/sub2/nested.dot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | bar: 'baz' 3 | -------------------------------------------------------------------------------- /spec/fixtures/hieradata/valid/sub/nested.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 'hello': 'world' 3 | -------------------------------------------------------------------------------- /spec/fixtures/hieradata/valid/valid.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 'string-value' : 'a string' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | .ruby-version 4 | .ruby-gemset 5 | Gemfile.lock 6 | .bundle 7 | pkg -------------------------------------------------------------------------------- /spec/fixtures/hieradata/invalid/invalid.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # really gross yaml 3 | {'hello') 'world' 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org/' 2 | 3 | gemspec 4 | 5 | group :test do 6 | gem 'rake' 7 | end 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :build => :spec 7 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec-puppet' 2 | require 'puppetlabs_spec_helper/module_spec_helper' 3 | 4 | $LOAD_PATH.unshift '.' 5 | 6 | RSpec.configure do |c| 7 | c.color = true 8 | end 9 | -------------------------------------------------------------------------------- /lib/rspec-puppet-utils.rb: -------------------------------------------------------------------------------- 1 | require 'rspec_puppet_utils/mock_function' 2 | require 'rspec_puppet_utils/mock_resource' 3 | require 'rspec_puppet_utils/template_harness' 4 | require 'rspec_puppet_utils/hieradata/validator' 5 | require 'rspec_puppet_utils/hieradata/yaml_validator' 6 | 7 | include RSpecPuppetUtils 8 | -------------------------------------------------------------------------------- /spec/require_spec.rb: -------------------------------------------------------------------------------- 1 | require 'lib/rspec_puppet_utils/mock_function' 2 | 3 | describe 'requires' do 4 | it 'requires mocha properly without the spec_helper' do 5 | Puppet::Parser::Functions.reset 6 | func = RSpecPuppetUtils::MockFunction.new 'func' 7 | expect { func.stubbed }.not_to raise_error 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/classes/utils_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec-puppet-utils' 3 | 4 | include RSpecPuppetUtils 5 | 6 | describe 'rspec-puppet-utils' do 7 | 8 | it 'should require MockFunction' do 9 | expect { MockFunction.class }.to_not raise_error 10 | end 11 | 12 | it 'should require TemplateHarness' do 13 | expect { TemplateHarness.class }.to_not raise_error 14 | end 15 | 16 | it 'should require HieraData Validator' do 17 | expect { HieraData::Validator.class }.to_not raise_error 18 | end 19 | 20 | it 'should require HieraData YamlValidator' do 21 | expect { HieraData::YamlValidator.class }.to_not raise_error 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /rspec-puppet-utils.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | Gem::Specification.new do |gem| 3 | gem.name = 'rspec-puppet-utils' 4 | gem.version = '3.4.0' 5 | gem.description = 'Helper classes for mock/stub functions, templates and hieradata' 6 | gem.summary = '' 7 | gem.author = 'Tom Poulton' 8 | gem.license = 'MIT' 9 | 10 | gem.homepage = 'https://github.com/Accuity/rspec-puppet-utils' 11 | gem.files = `git ls-files`.split($/) 12 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 13 | gem.require_paths = ['lib'] 14 | 15 | gem.add_dependency('puppet') 16 | gem.add_dependency('rspec') 17 | gem.add_dependency('rspec-puppet') 18 | gem.add_dependency('puppetlabs_spec_helper') 19 | gem.add_dependency('mocha') 20 | end 21 | -------------------------------------------------------------------------------- /lib/rspec_puppet_utils/template_harness.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | 3 | module RSpecPuppetUtils 4 | 5 | class TemplateHarness 6 | 7 | def initialize(template, scope = nil) 8 | @template = template 9 | @isolator = Isolator.new(scope) 10 | end 11 | 12 | def set(name, value) 13 | var_name = name.start_with?('@') ? name : "@#{name}" 14 | @isolator.instance_variable_set(var_name, value) 15 | end 16 | 17 | def run 18 | b = @isolator.get_binding 19 | inline = !File.exists?(@template) 20 | template_string = inline ? @template : File.new(@template).read 21 | template = ERB.new(template_string, 0, '-') 22 | template.filename = File.expand_path(@template) unless inline 23 | template.result b 24 | end 25 | 26 | class Isolator 27 | # Isolates the binding so that only the defined set 28 | # of instance variables are available to erb 29 | def initialize scope 30 | @scope = scope 31 | end 32 | def get_binding 33 | scope = @scope 34 | binding 35 | end 36 | end 37 | 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /DESIGN.md: -------------------------------------------------------------------------------- 1 | # Design Thoughts 2 | 3 | This is to explain our design thoughts for anyone that's interested, although it's mainly to remind ourselves in the future ;) 4 | 5 | ## Rake Tasks 6 | 7 | ### Project Tasks 8 | 9 | The spec rake tasks are structured on the basis that each module in a project (ignoring external modules for now) has its own set of rspec tests. 10 | 11 | We could fix the issues with the main `spec` task by changing it to scan all modules' spec directories for spec files and run them: 12 | 13 | ```ruby 14 | RSpec::Core::RakeTask.new :spec do |t| 15 | t.pattern = "#{module_path}/**/#{RSpec::Core::RakeTask::DEFAULT_PATTERN}" 16 | end 17 | ``` 18 | 19 | However that means that we need a toplevel project `spec_helper` file, and there could be other issues as well (I'm not an rspec expert). 20 | 21 | Another option would be to move all module specs into one project `spec` directory, 22 | however (putting aside potential `rspec-puppet` file structure issues) having all specs for all modules in one directory could make tests hard to isolate, 23 | and it will make it hard to separate a module into it's own repo in the future. 24 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2017 Accuity Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the “Software”), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /lib/rspec_puppet_utils/mock_resource.rb: -------------------------------------------------------------------------------- 1 | module RSpecPuppetUtils 2 | 3 | # MockResource is an experimental feature, API may change! 4 | # Use at your own risk... although feedback would be helpful :) 5 | class MockResource 6 | 7 | def initialize(name, resource_definition = {}) 8 | @name = name 9 | @resource_definition = resource_definition 10 | end 11 | 12 | def render 13 | type = @resource_definition[:type] || :class 14 | vars = join_vars @resource_definition[:vars], "\n" 15 | 16 | if @resource_definition[:params].nil? 17 | param_section = '' 18 | else 19 | params = join_vars @resource_definition[:params], ', ' 20 | param_section = "( #{params} )" 21 | end 22 | 23 | "#{type} #{@name} #{param_section} { #{vars} }" 24 | end 25 | 26 | def join_vars(vars, join_string) 27 | return '' unless vars 28 | parsed_vars = [] 29 | vars.each { |key, val| 30 | param = "$#{key}" 31 | value = normalise_value val 32 | val ? parsed_vars.push("#{param} = #{value}") : parsed_vars.push(param) 33 | } 34 | parsed_vars.join join_string 35 | end 36 | 37 | def normalise_value(value) 38 | # If string, wrap with quotes 39 | value.is_a?(String) ? "'#{value}'" : value 40 | end 41 | 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/rspec_puppet_utils/mock_function.rb: -------------------------------------------------------------------------------- 1 | require 'puppet' 2 | require 'mocha/api' 3 | 4 | module RSpec::Puppet 5 | module Support 6 | def self.clear_cache 7 | begin 8 | # Cache is a separate class since rspec-puppet 2.3.0 9 | require 'rspec-puppet/cache' 10 | @@cache = RSpec::Puppet::Cache.new 11 | rescue Gem::LoadError 12 | @@cache = {} 13 | end 14 | end 15 | end 16 | end 17 | 18 | module RSpecPuppetUtils 19 | 20 | class MockFunction 21 | 22 | def initialize(name, options = {}) 23 | parse_options! options 24 | this = self 25 | Puppet::Parser::Functions.newfunction(name.to_sym, options) { |args| this.call args } 26 | yield self if block_given? 27 | end 28 | 29 | def call(args) 30 | execute *args 31 | end 32 | 33 | def execute(*args) 34 | args 35 | end 36 | 37 | def stubbed 38 | self.stubs(:execute) 39 | end 40 | 41 | def expected(*args) 42 | RSpec::Puppet::Support.clear_cache unless args.include? :keep_cache 43 | self.expects(:execute) 44 | end 45 | 46 | # Use stubbed instead, see readme 47 | def stub 48 | self.stubs(:call) 49 | end 50 | 51 | # Use expected instead, see readme 52 | def expect(*args) 53 | RSpec::Puppet::Support.clear_cache unless args.include? :keep_cache 54 | self.expects(:call) 55 | end 56 | 57 | private 58 | 59 | def parse_options!(options) 60 | unless options[:type] 61 | options[:type] = :rvalue 62 | end 63 | unless [:rvalue, :statement].include? options[:type] 64 | raise ArgumentError, "Type should be :rvalue or :statement, not #{options[:type]}" 65 | end 66 | unless options[:arity].nil? || options[:arity].is_a?(Integer) 67 | raise ArgumentError, 'arity should be an integer' 68 | end 69 | end 70 | 71 | end 72 | 73 | end 74 | -------------------------------------------------------------------------------- /lib/rspec_puppet_utils/hieradata/yaml_validator.rb: -------------------------------------------------------------------------------- 1 | require 'rspec_puppet_utils/hieradata/validator' 2 | require 'yaml' 3 | 4 | module RSpecPuppetUtils 5 | module HieraData 6 | 7 | class YamlValidator < HieraData::Validator 8 | 9 | def initialize(directory, extensions = ['yaml', 'yml']) 10 | raise ArgumentError, 'extensions should be an Array' unless extensions.is_a? Array 11 | @directory = directory 12 | @extensions = extensions.map {|ext| ext =~ /\..*/ ? ext : ".#{ext}" } 13 | end 14 | 15 | def load_data(*args) 16 | @load_errors = [] 17 | @data = {} 18 | files = find_yaml_files 19 | files.each { |file| load_data_for_file file, args.include?(:ignore_empty) } 20 | self 21 | end 22 | 23 | # Deprecated - delete soon! 24 | def load(ignore_empty = false) 25 | warn '#load is deprecated, use #load_data instead' 26 | ignore_empty ? load_data(:ignore_empty) : load_data 27 | raise ValidationError, @load_errors[0] unless @load_errors.empty? 28 | end 29 | 30 | private 31 | 32 | # For presentation, return just the subpath within hieradata. 33 | # e.g., /path/hieradata/subdir/foo.yaml becomes subdir/foo.yaml 34 | def subpath(file) 35 | file.sub(/#{@directory}/, '').sub(/#{File::SEPARATOR}/, '') 36 | end 37 | 38 | def load_data_for_file(file, ignore_empty) 39 | file_name = subpath(file) 40 | begin 41 | yaml = File.open(file) { |yf| YAML::load( yf ) } 42 | rescue => e 43 | @load_errors.push "Error in file #{file}: #{e.message}" 44 | return 45 | end 46 | @load_errors.push "Yaml file is empty: #{file}" unless yaml || ignore_empty 47 | @data[file_name.to_sym] = yaml if yaml 48 | end 49 | 50 | def find_yaml_files 51 | Dir.glob(File.join(@directory, '**', '*')).reject { |path| 52 | File.directory?(path) || !@extensions.include?(File.extname path ) 53 | } 54 | end 55 | 56 | end 57 | 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/rspec_puppet_utils/hieradata/validator.rb: -------------------------------------------------------------------------------- 1 | module RSpecPuppetUtils 2 | module HieraData 3 | 4 | class Validator 5 | 6 | attr_reader :data, :load_errors 7 | 8 | def validate(key, required_files = [], &block) 9 | pre_checks(required_files) 10 | @found = false 11 | @data.keys.each do |file| 12 | validate_file file, key, required_files, &block 13 | end 14 | raise ValidationError, "No match for #{key.inspect} was not found in any files" unless @found 15 | raise ValidationError, "No match for #{key.inspect} was not found in: #{required_files.join ', '}" unless required_files.empty? 16 | end 17 | 18 | private 19 | 20 | def pre_checks(required_files) 21 | raise ValidationError, "Errors occurred during data load:\n#{@load_errors.join "\n"}\n" unless @load_errors.empty? 22 | raise StandardError, 'No data available, try #load first' if @data.nil? || @data.empty? 23 | raise ArgumentError, 'required files should be an Array' unless required_files.is_a?(Array) 24 | end 25 | 26 | def validate_file(file, key, required_files, &block) 27 | keys = get_matching_keys key, file 28 | keys.each do |matched_key| 29 | @found = true 30 | begin 31 | required_files.delete file 32 | block.call @data[file][matched_key] 33 | rescue RSpec::Expectations::ExpectationNotMetError => e 34 | raise ValidationError, "#{matched_key} is invalid in #{file}: #{e.message}" 35 | end 36 | end 37 | end 38 | 39 | def get_matching_keys(key, file) 40 | if key.is_a?(String) || key.is_a?(Symbol) 41 | keys = @data[file].has_key?(key) ? [key] : [] 42 | elsif key.is_a?(Regexp) 43 | keys = @data[file].keys.select { |k| k.to_s =~ key } 44 | else 45 | raise ArgumentError, 'Search key must be a String, Symbol or a Regexp' 46 | end 47 | keys 48 | end 49 | 50 | end 51 | 52 | class ValidationError < StandardError 53 | end 54 | 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/classes/mock_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'lib/rspec_puppet_utils/mock_resource' 3 | 4 | include RSpecPuppetUtils 5 | 6 | describe MockResource do 7 | 8 | let(:resource_name) { 'my_mock_resource' } 9 | let(:resource_definition) { 10 | { 11 | :params => { 12 | :string_param => 'hello', 13 | :undef_param => :undef, 14 | :required_param => nil, 15 | }, 16 | :vars => { 17 | :my_var_one => 'uno', 18 | :my_var_two => 2, 19 | } 20 | } 21 | } 22 | 23 | describe 'render' do 24 | 25 | subject(:mock_resource) { MockResource.new(resource_name, resource_definition).render } 26 | 27 | it 'returns the rendered resource' do 28 | expect(mock_resource).to match /class .* \{/ 29 | end 30 | 31 | it 'adds the name to the resource' do 32 | expect(mock_resource).to match /class #{resource_name}/ 33 | end 34 | 35 | it 'renders string params in quotes' do 36 | expect(mock_resource).to match /\(.*\$string_param = 'hello'.*\)/ 37 | end 38 | 39 | it 'renders undef params without quotes' do 40 | expect(mock_resource).to match /\(.*\$undef_param = undef.*\)/ 41 | end 42 | 43 | it 'renders required params without an assigned value' do 44 | expect(mock_resource).to match /\(.*\$required_param(,.*|\s*\))/ 45 | end 46 | 47 | it 'renders string variables with quotes' do 48 | expect(mock_resource).to match /\{.*\$my_var_one = 'uno'.*\}/m 49 | end 50 | 51 | it 'renders numerical variables without quotes' do 52 | expect(mock_resource).to match /\{.*\$my_var_two = 2.*\}/m 53 | end 54 | 55 | context 'when no params are provided' do 56 | 57 | before :each do 58 | resource_definition.delete :params 59 | end 60 | 61 | it 'renders no parenthesis' do 62 | expect(mock_resource).to match /#{resource_name}\s+\{.*\}/m 63 | end 64 | 65 | end 66 | 67 | context 'when type is :define' do 68 | 69 | before :each do 70 | resource_definition[:type] = :define 71 | end 72 | 73 | it 'renders a defined type' do 74 | expect(mock_resource).to match /define #{resource_name}/ 75 | end 76 | 77 | end 78 | 79 | end 80 | 81 | end 82 | -------------------------------------------------------------------------------- /spec/classes/template_harness_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'lib/rspec_puppet_utils/template_harness' 3 | 4 | include RSpecPuppetUtils 5 | 6 | describe TemplateHarness do 7 | 8 | it 'should render template' do 9 | harness = TemplateHarness.new('<%= "inside template" %>') 10 | expect(harness.run).to eq 'inside template' 11 | end 12 | 13 | it 'should handle -%> syntax' do 14 | harness = TemplateHarness.new('<% animal = "penguin" -%><%= animal %>') 15 | expect { harness.run }.to_not raise_error 16 | end 17 | 18 | it 'should provide access to scope' do 19 | scope = PuppetlabsSpec::PuppetInternals.scope 20 | scope.stubs(:lookupvar).with('honey').returns('badger') 21 | harness = TemplateHarness.new('<%= scope.lookupvar("honey") %>', scope) 22 | expect(harness.run).to eq 'badger' 23 | end 24 | 25 | it 'should provide access to instance vars' do 26 | harness = TemplateHarness.new('<%= @foo %>') 27 | harness.set('@foo', 'bar') 28 | expect(harness.run).to eq 'bar' 29 | end 30 | 31 | it 'should add @ to instance vars when missing' do 32 | harness = TemplateHarness.new('<%= @alice %>') 33 | harness.set('alice', 'bob') 34 | expect(harness.run).to eq 'bob' 35 | end 36 | 37 | it 'should isolate instance vars' do 38 | harness = TemplateHarness.new('<%= @not_exist %>') 39 | harness.instance_variable_set('@not_exist', 'pixies') 40 | expect(harness.run).to eq '' 41 | end 42 | 43 | it 'should read file if it exists' do 44 | harness = TemplateHarness.new('spec/fixtures/templates/returns_elephant.erb') 45 | expect(harness.run).to eq 'elephant' 46 | end 47 | 48 | it 'should set filename of template' do 49 | template_path = 'spec/fixtures/templates/returns_elephant.erb' 50 | absolute_path = File.expand_path(template_path) 51 | 52 | fakeplate = Object.new 53 | fakeplate.stubs(:result).returns('') 54 | fakeplate.expects(:filename=).with(absolute_path).once 55 | ERB.stubs(:new).returns(fakeplate) 56 | 57 | TemplateHarness.new(template_path).run 58 | end 59 | 60 | it 'should not set filename of inline template' do 61 | fakeplate = Object.new 62 | fakeplate.stubs(:result).returns('') 63 | fakeplate.expects(:filename=).never 64 | ERB.stubs(:new).returns(fakeplate) 65 | 66 | TemplateHarness.new('<%= "" %>').run 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /spec/classes/hieradata/yaml_validator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'lib/rspec_puppet_utils/hieradata/yaml_validator' 3 | 4 | include RSpecPuppetUtils 5 | 6 | describe HieraData::YamlValidator do 7 | 8 | it 'should be of type Validator' do 9 | validator = HieraData::YamlValidator.new('spec/fixtures/hieradata/valid') 10 | expect(validator).to be_a_kind_of HieraData::Validator 11 | end 12 | 13 | describe '#load_data' do 14 | 15 | context 'with valid yaml' do 16 | 17 | validator = HieraData::YamlValidator.new('spec/fixtures/hieradata/valid') 18 | validator.load_data 19 | 20 | it 'should load yaml files into data' do 21 | expect(validator.data.keys.size).to_not be 0 22 | end 23 | 24 | it 'should load yaml files recursively' do 25 | expect(validator.data.keys).to include :'sub/nested.yaml' 26 | end 27 | 28 | it 'should load yaml data from files' do 29 | expect(validator.data[:'valid.yaml']['string-value']).to eq 'a string' 30 | end 31 | 32 | it 'should load yaml data from files with dot in filename' do 33 | expect(validator.data.keys).to include :'sub/nested.dot.yaml' 34 | end 35 | 36 | it 'should load yaml data from files with same name but different dir' do 37 | expect(validator.data.keys).to include :'sub2/nested.dot.yaml' 38 | end 39 | 40 | it 'should load yaml data from files with same name but different ext' do 41 | expect(validator.data.keys).to include :'sub2/nested.dot.yml' 42 | end 43 | 44 | it 'should not add any load errors' do 45 | expect(validator.load_errors).to be_an Array 46 | expect(validator.load_errors).to be_empty 47 | end 48 | 49 | end 50 | 51 | context 'with multiple extensions' do 52 | 53 | validator = HieraData::YamlValidator.new('spec/fixtures/hieradata/valid', ['yaml', 'foo']) 54 | validator.load_data 55 | 56 | it 'should load yml files into data' do 57 | expect(validator.data).to have_key :'other.foo' 58 | end 59 | 60 | end 61 | 62 | context 'with extensions as string' do 63 | 64 | it 'should load yml files into data' do 65 | expect { HieraData::YamlValidator.new('meh', 'whooops') }.to raise_error ArgumentError, /extensions should be an Array/ 66 | end 67 | 68 | end 69 | 70 | context 'with invalid yaml' do 71 | 72 | validator = HieraData::YamlValidator.new('spec/fixtures/hieradata/invalid') 73 | 74 | it 'should not raise error' do 75 | expect { 76 | validator.load_data 77 | }.to_not raise_error 78 | end 79 | 80 | it 'should add error to load_errors' do 81 | expect(validator.load_errors).to be_an Array 82 | expect(validator.load_errors.size).to eq 1 83 | expect(validator.load_errors[0]).to match /Error in file .*\/invalid.yaml/ 84 | end 85 | 86 | end 87 | 88 | context 'with empty yaml' do 89 | 90 | subject(:validator) { HieraData::YamlValidator.new('spec/fixtures/hieradata/empty') } 91 | 92 | it 'should not raise error by default' do 93 | expect { 94 | validator.load_data 95 | }.to_not raise_error # /Yaml file is empty: .*\/empty.yaml/ 96 | end 97 | 98 | it 'should add error to load_errors' do 99 | validator.load_data 100 | expect(validator.load_errors).to be_an Array 101 | expect(validator.load_errors.size).to eq 1 102 | expect(validator.load_errors[0]).to match /Yaml file is empty: .*\/empty.yaml/ 103 | end 104 | 105 | it 'should ignore empty files when flag is set' do 106 | expect { validator.load_data :ignore_empty }.to_not raise_error 107 | end 108 | 109 | it 'should not add empty files to @data' do 110 | validator.load_data :ignore_empty 111 | expect(validator.data.keys).to_not include :empty 112 | end 113 | 114 | it 'should add non empty files to data' do 115 | validator.load_data :ignore_empty 116 | expect(validator.data.keys).to include :'not_empty.yaml' 117 | end 118 | 119 | end 120 | 121 | it 'should return validator instance' do 122 | validator = HieraData::YamlValidator.new('spec/fixtures/hieradata/valid') 123 | expect(validator.load_data).to eq validator 124 | end 125 | 126 | end 127 | 128 | describe '#load' do 129 | 130 | subject(:validator) { HieraData::YamlValidator.new('spec/fixtures/hieradata/empty') } 131 | before(:each) do 132 | validator.stubs(:warn) # Hide warn message from output 133 | end 134 | 135 | it 'should support old #load method' do 136 | expect { validator.load true }.to_not raise_error 137 | expect(validator.data.keys).to include :'not_empty.yaml' 138 | end 139 | 140 | it 'should still throw errors if necessary' do 141 | expect { 142 | validator.load 143 | }.to raise_error HieraData::ValidationError 144 | end 145 | 146 | it 'should warn when using old #load method' do 147 | validator.expects(:warn).with('#load is deprecated, use #load_data instead').once 148 | validator.load true 149 | end 150 | 151 | end 152 | 153 | end 154 | -------------------------------------------------------------------------------- /spec/classes/hieradata/validator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'lib/rspec_puppet_utils/hieradata/validator' 3 | 4 | include RSpecPuppetUtils 5 | 6 | module HieraData 7 | class TestValidator < Validator 8 | def initialize 9 | @load_errors = [] 10 | end 11 | def load 12 | @data = { 13 | :file1 => { 14 | 'key' => 'value', 15 | :other => 'other value', 16 | 'missmatch' => 'string', 17 | 'cat' => 'black', 18 | }, 19 | :file2 => { 20 | 'hello' => 'world', 21 | 'missmatch' => ['array'], 22 | 'hat' => 'fedora', 23 | }, 24 | :file3 => { 25 | 'squid' => 'giant', 26 | } 27 | } 28 | end 29 | def load_empty 30 | @data = {} 31 | end 32 | def set_load_errors(errors) 33 | @load_errors = errors 34 | end 35 | end 36 | end 37 | 38 | describe HieraData::Validator do 39 | 40 | validator = HieraData::TestValidator.new 41 | validator.load 42 | 43 | it 'should have public data variable' do 44 | expect(validator.data).to have_key :file1 45 | end 46 | 47 | it 'should use block to validate key' do 48 | expect { 49 | validator.validate('key') { |v| expect(v).to eq 'value' } 50 | }.to_not raise_error 51 | 52 | expect { 53 | validator.validate('key') { |v| expect(v).to eq 'oooops' } 54 | }.to raise_error HieraData::ValidationError 55 | end 56 | 57 | it 'should accept symbol as key' do 58 | expect { 59 | validator.validate(:other) { |v| v == 'other value' } 60 | }.to_not raise_error 61 | end 62 | 63 | it 'should validate key in all files' do 64 | expect { 65 | validator.validate('missmatch') { |v| expect(v).to be_a String } 66 | }.to raise_error HieraData::ValidationError 67 | end 68 | 69 | it 'should return key and file in error messages' do 70 | expect { 71 | validator.validate('missmatch') { |v| expect(v).to be_a String } 72 | }.to raise_error HieraData::ValidationError, /missmatch is invalid in file2/ 73 | end 74 | 75 | context 'when matching with regex' do 76 | 77 | it 'should raise error if no match is found' do 78 | expect { 79 | validator.validate(/nonex/) { } 80 | }.to raise_error HieraData::ValidationError, /No match for \/nonex\/ was not found/ 81 | end 82 | 83 | it 'should not raise error if match is found' do 84 | expect { 85 | validator.validate(/at$/) { } 86 | }.to_not raise_error 87 | end 88 | 89 | it 'should validate block against all matches' do 90 | parser = mock() 91 | parser.expects(:parse).with('black').once 92 | parser.expects(:parse).with('fedora').once 93 | validator.validate(/at$/) { |v| parser.parse v } 94 | end 95 | 96 | it 'should match symbols' do 97 | expect { 98 | validator.validate(/other/) { |v| expect(v).to eq 'other value' } 99 | }.to_not raise_error 100 | end 101 | 102 | end 103 | 104 | context 'with required files' do 105 | 106 | it 'should raise error if required files in not an Array' do 107 | expect { 108 | validator.validate('cat', nil) { } 109 | }.to raise_error ArgumentError, 'required files should be an Array' 110 | end 111 | 112 | it 'should raise error when key is not found in required file' do 113 | expect { 114 | validator.validate('cat', [:file2]) { } 115 | }.to raise_error HieraData::ValidationError 116 | end 117 | 118 | it 'should report which files are missing the key' do 119 | expect { 120 | validator.validate('cat', [:file2, :file3]) { } 121 | }.to raise_error HieraData::ValidationError, 'No match for "cat" was not found in: file2, file3' 122 | end 123 | 124 | it 'should report broader error if key is not in any files' do 125 | expect { 126 | validator.validate('dog', [:file1]) { } 127 | }.to raise_error HieraData::ValidationError, 'No match for "dog" was not found in any files' 128 | end 129 | 130 | end 131 | 132 | it 'should raise error if key is not a valid type' do 133 | expect { 134 | validator.validate(['key']) { } 135 | }.to raise_error ArgumentError, 'Search key must be a String, Symbol or a Regexp' 136 | end 137 | 138 | it 'should raise error if there were load errors' do 139 | load_error_validator = HieraData::TestValidator.new 140 | load_error_validator.load_empty 141 | load_error_validator.set_load_errors ['file1 is empty', 'file2 has syntax errors'] 142 | expect { 143 | load_error_validator.validate('') { } 144 | }.to raise_error HieraData::ValidationError, /file1 is empty\nfile2 has syntax errors/ 145 | end 146 | 147 | it 'should raise error if data is nil' do 148 | nil_validator = HieraData::TestValidator.new 149 | expect { 150 | nil_validator.validate('meh') { } 151 | }.to raise_error StandardError, /No data available/ 152 | end 153 | 154 | it 'should raise error if data is empty' do 155 | empty_validator = HieraData::TestValidator.new 156 | empty_validator.load_empty 157 | expect { 158 | empty_validator.validate('meh') { } 159 | }.to raise_error StandardError, /No data available/ 160 | end 161 | 162 | end 163 | 164 | describe HieraData::ValidationError do 165 | 166 | it 'should inherit from StandardError' do 167 | expect(HieraData::ValidationError.ancestors).to include StandardError 168 | end 169 | 170 | end 171 | -------------------------------------------------------------------------------- /spec/classes/mock_function_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'lib/rspec_puppet_utils/mock_function' 3 | 4 | include RSpecPuppetUtils 5 | 6 | describe MockFunction do 7 | 8 | let(:scope) { PuppetlabsSpec::PuppetInternals.scope } 9 | 10 | let(:values_from_let) { [1, 2, 3] } 11 | 12 | describe '#initialize' do 13 | 14 | func_name = 'my_func' 15 | func_sym = func_name.to_sym 16 | 17 | it 'adds new function to puppet' do 18 | name = 'mock_func' 19 | func = MockFunction.new name 20 | expect(Puppet::Parser::Functions.function(name.to_sym)).to eq "function_#{name}" 21 | end 22 | 23 | it 'defaults to :rvalue type' do 24 | func = MockFunction.new func_name 25 | expect(Puppet::Parser::Functions.rvalue?(func_sym)).to eq true 26 | end 27 | 28 | it 'defaults to :rvalue type if missing from options' do 29 | func = MockFunction.new func_name, {} 30 | expect(Puppet::Parser::Functions.rvalue?(func_sym)).to eq true 31 | end 32 | 33 | it 'allows type to be set' do 34 | func = MockFunction.new func_name, {:type => :statement} 35 | expect(Puppet::Parser::Functions.rvalue?(func_sym)).to eq false 36 | end 37 | 38 | it 'only allows :rvalue or :statement for type' do 39 | expect { 40 | MockFunction.new func_name, {:type => :error} 41 | }.to raise_error ArgumentError, 'Type should be :rvalue or :statement, not error' 42 | end 43 | 44 | it 'allows arity to be set' do 45 | func = MockFunction.new func_name, {:arity => 3} 46 | expect(Puppet::Parser::Functions.arity(func_sym)).to eq 3 47 | end 48 | 49 | it 'only allows arity to be an integer' do 50 | expect { 51 | MockFunction.new func_name, {:arity => 'oops'} 52 | }.to raise_error ArgumentError, 'arity should be an integer' 53 | end 54 | 55 | end 56 | 57 | describe '#call' do 58 | 59 | let(:func) { MockFunction.new('func') } 60 | 61 | it 'is stubable' do 62 | func.stubs(:call) 63 | expect(func.respond_to?(:call)).to eq true 64 | end 65 | 66 | it 'is called by puppet function' do 67 | func.stubs(:call).returns('penguin') 68 | result = scope.function_func [] 69 | expect(result).to eq 'penguin' 70 | end 71 | 72 | it 'is passed puppet function args' do 73 | func.expects(:call).with([1, 2, 3]).once 74 | scope.function_func [1, 2, 3] 75 | end 76 | 77 | it 'passes function args to execute method' do 78 | func.expects(:execute).with(1, 2, 3) 79 | func.call [1, 2, 3] 80 | end 81 | 82 | end 83 | 84 | describe '#stubbed' do 85 | 86 | let(:func) { MockFunction.new('func') } 87 | 88 | it 'stubs #execute' do 89 | expectation = func.stubbed 90 | expect(expectation).to be_a Mocha::Expectation 91 | expect(expectation.matches_method? :execute).to eq true 92 | end 93 | 94 | end 95 | 96 | describe '#expected' do 97 | 98 | let(:func) { MockFunction.new('func') } 99 | 100 | it 'registers expect on #execute' do 101 | expectation = func.expected 102 | expect(expectation).to be_a Mocha::Expectation 103 | expect(expectation.matches_method? :execute).to eq true 104 | func.execute # satisfy the expect we just created on #execute! 105 | end 106 | 107 | it 'clears rspec puppet cache' do 108 | RSpec::Puppet::Support.expects(:clear_cache).once 109 | func.expected 110 | func.execute # satisfy the expect we just created on #execute! 111 | end 112 | 113 | it 'works with parameter matchers' do 114 | func.expected.with(regexp_matches(/thing/), anything) 115 | scope.function_func ['something', 1234] 116 | end 117 | 118 | context 'when :keep_cache is set' do 119 | 120 | it 'does not clear rspec puppet cache' do 121 | RSpec::Puppet::Support.expects(:clear_cache).never 122 | func.expected(:keep_cache) 123 | func.execute # satisfy the expect we just created on #execute! 124 | end 125 | 126 | end 127 | 128 | end 129 | 130 | describe '#stub' do 131 | 132 | let(:func) { MockFunction.new('func') } 133 | 134 | it 'stubs #call' do 135 | expectation = func.stub 136 | expect(expectation).to be_a Mocha::Expectation 137 | expect(expectation.matches_method? :call).to eq true 138 | end 139 | 140 | end 141 | 142 | describe '#expect' do 143 | 144 | let(:func) { MockFunction.new('func') } 145 | 146 | it 'registers expect on #call' do 147 | expectation = func.expect 148 | expect(expectation).to be_a Mocha::Expectation 149 | expect(expectation.matches_method? :call).to eq true 150 | func.call [nil] # satisfy the expect we just created on #call! 151 | end 152 | 153 | it 'clears rspec puppet cache' do 154 | RSpec::Puppet::Support.expects(:clear_cache).once 155 | func.expect 156 | func.call [nil] # satisfy the expect we just created on #call! 157 | end 158 | 159 | context 'when :keep_cache is set' do 160 | 161 | it 'does not clear rspec puppet cache' do 162 | RSpec::Puppet::Support.expects(:clear_cache).never 163 | func.expect(:keep_cache) 164 | func.call [nil] # satisfy the expect we just created on #call! 165 | end 166 | 167 | end 168 | 169 | end 170 | 171 | context 'when :type => :statement' do 172 | 173 | let!(:statement) { MockFunction.new 'statement', {:type => :statement} } 174 | 175 | it 'does not raise error' do 176 | expect { 177 | scope.function_statement [] 178 | }.to_not raise_error 179 | end 180 | 181 | it 'responds to #call' do 182 | expect(statement.respond_to? :call).to eq true 183 | end 184 | 185 | end 186 | 187 | context 'when :type => :rvalue' do 188 | 189 | it 'allows setup stubs' do 190 | func = MockFunction.new('func') { |f| f.stubs(:call).returns('badger') } 191 | result = func.call 192 | expect(result).to eq 'badger' 193 | end 194 | 195 | it 'returns values defined by a "let"' do 196 | result = [] 197 | expect { 198 | func = MockFunction.new('func') { |f| f.stubs(:call).returns(values_from_let) } 199 | result = func.call 200 | }.to_not raise_error 201 | expect(result).to eq [1, 2, 3] 202 | end 203 | 204 | end 205 | 206 | end 207 | -------------------------------------------------------------------------------- /lib/rspec_puppet_utils/rake/project_tasks.rb: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rspec/core/rake_task' 3 | require 'fileutils' 4 | 5 | # ToDo: replace zip cmds with ruby zip lib to avoid shelling out 6 | # ToDo: What if no lib dir exists? 7 | 8 | module Rake 9 | 10 | class Puppet 11 | 12 | attr_accessor :excluded_modules, :package_dir, :package_files, :package_version, :package_versioning 13 | 14 | # @librarian_dir (string) Directory where librarian-puppet installs modules (default "modules") 15 | # @site_dir (string) Directory for profiles, roles and components (default "site") 16 | # @lib_dir (string) Directory for custom/internal modules (default "lib") 17 | # @excluded_dirs (string[]) Directories excluded from spec search 18 | # @excluded_modules (string[]) Modules excluded from spec testing 19 | # @package_dir (string) Where the puppet zip package will be created 20 | # @package_files (string[]) Files and directories to include in the package 21 | # @package_name (string) Name of the package 22 | # @package_version (string) The version of the package (e.g. 2.1.0) 23 | # @package_versioning (boolean) Is the version included in the package name? 24 | 25 | def initialize 26 | extend Rake::DSL # makes 'namespace' and 'task' methods available to instance 27 | 28 | @librarian_dir = 'modules' 29 | @site_dir = 'site' 30 | @lib_dir = 'lib' 31 | @excluded_dirs = ['.', '..'] 32 | @excluded_modules = [] 33 | @package_dir = 'pkg' 34 | @package_files = ['hieradata', 'environment.conf'] 35 | @package_name = 'puppet' 36 | @package_version = nil 37 | @package_versioning = true 38 | end 39 | 40 | def load_tasks 41 | validate_unique_module_names 42 | load_module_tasks 43 | load_build_tasks 44 | end 45 | 46 | # private 47 | 48 | def testable_modules 49 | raise ArgumentError, 'excluded_modules must be an array' unless @excluded_modules.is_a? Array 50 | modules = [] 51 | [@lib_dir, @site_dir].each { |module_dir| 52 | raise ArgumentError, "Module path #{module_dir} could not be found" unless Dir.exist?(module_dir) 53 | entries = Dir.entries(module_dir) - @excluded_dirs - @excluded_modules 54 | modules.concat entries.collect { |entry| "#{module_dir}/#{entry}" } 55 | } 56 | filter_modules modules 57 | end 58 | 59 | def filter_modules(modules) 60 | modules.select! { |m| module_has_specs?(m) and module_has_rakefile?(m) } 61 | modules 62 | end 63 | 64 | def module_has_specs?(module_dir) 65 | File.directory? "#{module_dir}/spec" 66 | end 67 | 68 | def module_has_rakefile?(module_dir) 69 | rakefiles = ['rakefile', 'rakefile.rb'] 70 | entries = Dir.entries module_dir 71 | entries.collect! { |f| f.downcase } 72 | rakefiles.each { |rf| return true if entries.include? rf } 73 | false 74 | end 75 | 76 | def validate_unique_module_names 77 | # & == intersection : Returns elements common to the both arrays 78 | duplicates = Dir.entries(@site_dir) & Dir.entries(@librarian_dir) 79 | duplicates += Dir.entries(@librarian_dir) & Dir.entries(@lib_dir) 80 | duplicates += Dir.entries(@lib_dir) & Dir.entries(@site_dir) 81 | duplicates -= @excluded_dirs 82 | fail "Duplicate module names: #{duplicates.join ', '}" unless duplicates.empty? 83 | end 84 | 85 | def load_module_tasks 86 | 87 | modules = testable_modules 88 | module_names = testable_modules.collect { |m| m.split('/')[1] } 89 | spec_tasks = module_names.collect { |mn| "#{mn}:#{:spec}" } 90 | 91 | modules.each_with_index { |module_path, i| 92 | 93 | module_name = module_names[i] 94 | 95 | namespace module_name do 96 | desc "Run #{module_name} module specs" 97 | task :spec do 98 | Dir.chdir module_path do 99 | success = system('rake spec') # This isn't perfect but ... 100 | exit 1 unless success 101 | end 102 | end 103 | end 104 | } 105 | 106 | desc 'Run specs in all modules' 107 | task :spec => spec_tasks 108 | task :default => :spec 109 | end 110 | 111 | def load_build_tasks 112 | 113 | raise ArgumentError, 'Please provide a package_version (e.g. "1.0.0")' if @package_version.nil? 114 | 115 | full_package_name = @package_versioning ? "puppet-#{@package_version}.zip" : 'puppet.zip' 116 | package_desc = @package_versioning ? full_package_name : "#{full_package_name} v#{@package_version}" 117 | package_path = File.expand_path "#{@package_dir}/#{full_package_name}" 118 | build_dir = "#{@package_dir}/puppet" 119 | 120 | namespace :build do 121 | 122 | # Preps build directory 123 | task :prep do 124 | puts 'Preparing build' 125 | 126 | FileUtils.rm package_path if File.exist?(package_path) 127 | FileUtils.rm_r build_dir if File.exist?(build_dir) 128 | FileUtils.mkdir_p build_dir 129 | end 130 | 131 | task :copy_files => [:prep] do 132 | 133 | # Copy librarian and site modules into build dir 134 | puts 'Copying external and site modules' 135 | FileUtils.cp_r @site_dir, build_dir 136 | FileUtils.cp_r @librarian_dir, build_dir 137 | 138 | # Copy lib modules into the librarian build dir 139 | puts 'Copying lib modules' 140 | FileUtils.cp_r "#{@lib_dir}/.", "#{build_dir}/#{@librarian_dir}" 141 | 142 | # Copy other package files 143 | @package_files.each {|f| 144 | fail "Could not find package file or directory #{f}" unless File.exist? f 145 | puts "Copying #{f} to #{build_dir}" 146 | FileUtils.cp_r f, build_dir 147 | } 148 | end 149 | 150 | task :package => [:copy_files] do 151 | puts "Creating #{full_package_name}" 152 | # Exclude all the spec code as it's not needed once deployed 153 | exclude_patterns = ['\*/\*/spec/\*'] 154 | exclude_string = "-x #{exclude_patterns.join(' ')}" 155 | FileUtils.cd(build_dir) { 156 | out = `zip -qr '#{package_path}' . #{exclude_string}` 157 | fail("Error creating package: #{out}") unless $?.exitstatus == 0 158 | } 159 | end 160 | 161 | task :cleanup do 162 | puts "Cleaning up #{build_dir}" 163 | FileUtils.rm_r build_dir if File.exist?(build_dir) 164 | end 165 | end 166 | 167 | desc "Build #{package_desc} without tests" 168 | task :quick_build => ['build:package', 'build:cleanup'] do 169 | puts "Built #{package_desc}" 170 | end 171 | 172 | desc "Build #{package_desc}" 173 | task :build => [:spec, :quick_build] 174 | end 175 | 176 | end 177 | end 178 | -------------------------------------------------------------------------------- /spec/classes/rake/project_tasks_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'lib/rspec_puppet_utils/rake/project_tasks' 3 | 4 | describe Rake::Puppet do 5 | 6 | testable_module_names = [ 'core', 'base' ] 7 | testable_modules = [ 'lib/core', 'lib/base' ] 8 | sample_modules = [ 'lib/core', 'lib/base', 'site/role', 'site/profile' ] 9 | rakefile_names = [ 'Rakefile', 'rakefile', 'Rakefile.rb', 'rakefile.rb' ] 10 | 11 | let(:puppet) { Rake::Puppet.new } 12 | 13 | it 'allows adding to package_files list' do 14 | initial_count = puppet.package_files.count 15 | puppet.package_files << 'extra_file' 16 | expect(puppet.package_files).to include 'extra_file' 17 | expect(puppet.package_files.count).to eq initial_count + 1 18 | end 19 | 20 | describe 'load_tasks' do 21 | 22 | before(:each) do 23 | puppet.stubs(:validate_unique_module_names) 24 | puppet.stubs(:load_module_tasks) 25 | puppet.stubs(:load_build_tasks) 26 | end 27 | 28 | it 'validates module names' do 29 | puppet.expects(:validate_unique_module_names).once 30 | puppet.load_tasks 31 | end 32 | 33 | it 'loads module tasks' do 34 | puppet.expects(:load_module_tasks).once 35 | puppet.load_tasks 36 | end 37 | 38 | it 'loads build tasks' do 39 | puppet.expects(:load_build_tasks).once 40 | puppet.load_tasks 41 | end 42 | 43 | end 44 | 45 | # private 46 | 47 | describe 'load_module_tasks' do 48 | 49 | before(:each) do 50 | puppet.stubs(:testable_modules).returns(testable_modules) # not exactly best practice, but hey 51 | end 52 | 53 | it 'includes namespace and task methods from Rake::DSL' do 54 | # It would throw error on load if task or namespace methods are missing 55 | expect { puppet.load_module_tasks }.to_not raise_error 56 | end 57 | 58 | it 'creates a task for each module' do 59 | puppet.load_module_tasks 60 | testable_module_names.each { |mod| 61 | expect(Rake::Task.task_defined?("#{mod}:spec")).to eq true 62 | } 63 | end 64 | 65 | it 'loads the main spec task' do 66 | puppet.load_module_tasks 67 | expect(Rake::Task.task_defined?(:spec)).to eq true 68 | end 69 | 70 | it 'makes module spec tasks prerequisites of main spec task' do 71 | puppet.load_module_tasks 72 | task_names = testable_module_names.collect { |mn| "#{mn}:spec" } 73 | prerequisites = Rake::Task[:spec].prerequisites 74 | expect(prerequisites).to match_array task_names 75 | end 76 | 77 | end 78 | 79 | describe 'load_build_tasks' do 80 | 81 | it 'fails if no version is provided' do 82 | expect { puppet.load_build_tasks }.to raise_error(ArgumentError, /provide a package_version/) 83 | end 84 | 85 | context 'when version is set' do 86 | 87 | let(:package_version) { '1.2.3' } 88 | before(:each) { 89 | puppet.package_version = package_version 90 | } 91 | 92 | it 'loads the "build" task' do 93 | puppet.load_build_tasks 94 | expect(Rake::Task.task_defined?(:build)).to eq true 95 | end 96 | 97 | it 'loads the "quick_build" task' do 98 | puppet.load_build_tasks 99 | expect(Rake::Task.task_defined?(:quick_build)).to eq true 100 | end 101 | 102 | it 'includes package_version in package name' do 103 | puppet.load_build_tasks 104 | build_task = Rake::Task[:build] 105 | expect(build_task.application.last_description).to match /puppet-#{package_version}.zip/ 106 | end 107 | 108 | context 'when package_versioning is turned off' do 109 | 110 | before(:each) do 111 | puppet.package_versioning = false 112 | end 113 | 114 | it 'omits the version from the package name' do 115 | puppet.load_build_tasks 116 | build_task = Rake::Task[:build] 117 | expect(build_task.application.last_description).to match /puppet.zip/ 118 | end 119 | 120 | it 'includes the version in the task description' do 121 | puppet.load_build_tasks 122 | build_task = Rake::Task[:build] 123 | expect(build_task.application.last_description).to match /v#{package_version}/ 124 | end 125 | 126 | end 127 | 128 | end 129 | 130 | end 131 | 132 | describe 'filter_modules' do 133 | 134 | before(:each) do 135 | Dir.stubs(:entries).returns [] 136 | end 137 | 138 | it 'filters modules with a spec directory' do 139 | Dir.stubs(:entries).returns rakefile_names # bypass Rakefile filter 140 | 141 | File.stubs(:directory?).returns false 142 | File.stubs(:directory?).with(regexp_matches( /^(#{testable_modules.join '|'})\/spec$/ )).returns(true) 143 | 144 | result = puppet.filter_modules sample_modules 145 | expect(result).to match_array testable_modules 146 | end 147 | 148 | rakefile_names.each { |filename| 149 | it 'filters modules with a Rakefile' do 150 | File.stubs(:directory?).returns true # bypass spec dir filter 151 | 152 | Dir.stubs(:entries).with(regexp_matches( /^#{testable_modules.join '|'}/ )).returns([filename]) 153 | 154 | result = puppet.filter_modules sample_modules 155 | expect(result).to match_array testable_modules 156 | end 157 | } 158 | 159 | end 160 | 161 | describe 'testable_modules' do 162 | 163 | before(:each) do 164 | # Bypass the filter logic. Again, not exactly best practice, but hey 165 | def puppet.filter_modules(_modules) 166 | _modules 167 | end 168 | 169 | Dir.stubs(:exist?).returns true 170 | Dir.stubs(:entries).with('lib').returns(['one', 'two']) 171 | Dir.stubs(:entries).with('site').returns(['three', 'four']) 172 | end 173 | 174 | it 'finds modules in all paths' do 175 | modules = ['lib/one', 'lib/two', 'site/three', 'site/four'] 176 | expect(puppet.testable_modules).to match_array modules 177 | end 178 | 179 | it 'ignores excluded directories' do 180 | Dir.stubs(:entries).with('lib').returns(['.', '..', 'one', 'two']) 181 | modules = ['lib/one', 'lib/two', 'site/three', 'site/four'] 182 | expect(puppet.testable_modules).to match_array modules 183 | end 184 | 185 | it 'ignores excluded modules' do 186 | puppet.excluded_modules = ['two', 'four'] 187 | modules = ['lib/one', 'site/three'] 188 | expect(puppet.testable_modules).to match_array modules 189 | end 190 | 191 | it 'raises an error if excluded modules is not an array' do 192 | puppet.excluded_modules = 'not an array' 193 | expect { puppet.testable_modules }.to raise_error(ArgumentError, /must be an array/) 194 | end 195 | 196 | it 'raises an error if a path directory does not exist' do 197 | Dir.stubs(:exist?).with('lib').returns false 198 | expect { 199 | puppet.testable_modules 200 | }.to raise_error(ArgumentError, /lib could not be found/) 201 | end 202 | 203 | end 204 | 205 | describe 'validate_unique_module_names' do 206 | 207 | let(:dir_list_a) { ['.', '..', 'foo', 'bar'] } 208 | let(:dir_list_b) { ['.', '..', 'baz', 'bam'] } 209 | let(:dir_list_c) { ['.', '..', 'fuzzy', 'bazzy'] } 210 | 211 | before(:each) do 212 | Dir.stubs(:entries).with('site').returns dir_list_a 213 | Dir.stubs(:entries).with('modules').returns dir_list_b 214 | Dir.stubs(:entries).with('lib').returns dir_list_c 215 | end 216 | 217 | it 'does not fail if all module names are unique' do 218 | expect { 219 | puppet.validate_unique_module_names 220 | }.to_not raise_error 221 | end 222 | 223 | it 'fails if site and modules dirs have conflicting module names' do 224 | Dir.stubs(:entries).with('modules').returns dir_list_a 225 | expect { 226 | puppet.validate_unique_module_names 227 | }.to raise_error /Duplicate module names/ 228 | end 229 | 230 | it 'fails if modules and lib dirs have conflicting module names' do 231 | Dir.stubs(:entries).with('lib').returns dir_list_b 232 | expect { 233 | puppet.validate_unique_module_names 234 | }.to raise_error /Duplicate module names/ 235 | end 236 | 237 | it 'fails if lib and site dirs have conflicting module names' do 238 | Dir.stubs(:entries).with('site').returns dir_list_c 239 | expect { 240 | puppet.validate_unique_module_names 241 | }.to raise_error /Duplicate module names/ 242 | end 243 | 244 | end 245 | 246 | end 247 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rspec-puppet-utils 2 | 3 | This is a more refined version of a previous project about [rspec-puppet unit testing](https://github.com/TomPoulton/rspec-puppet-unit-testing), it provides a class for mocking functions, a harness for testing templates, and a simple tool for testing hiera data files. The motivation for mocking functions etc is provided in that project so I won't go over it here. 4 | 5 | See [release notes](../../wiki/Release-Notes) about latest version 6 | 7 | ## Updates: 8 | 9 | #### v3.1.0 10 | 11 | By default the zip file now includes the version (e.g. `puppet-1.2.0.zip`). In v3.0.0 the file was just called `puppet.zip` 12 | 13 | You can omit the version from the zip name like so: `puppet.package_versioning = false` 14 | 15 | Given that v3.0.0 was only released the day before, I'll keep it as a minor version update 16 | 17 | #### v3.0.0 18 | 19 | The project is now developed against ruby 2.1.0 and so it may not be backwards compatible when running on ruby 1.8.7. 20 | 21 | The internal file structure has also changed, which shouldn't affect usage, but it might :) 22 | 23 | #### v2.1.0 24 | 25 | The `MockFunction` `#stub` and `#expect` methods have been superseded by `#stubbed` and `#expected` so that you can use parameter matchers. The only difference in usage from previous versions is that the methods take a set of parameters rather than a single array (e.g. `f.expected.with(1, 2, 3)` instead of `f.expect.with([1, 2, 3])`) 26 | 27 | The change is backwards compatible so `#stub` and `#expect` are still available and function as before 28 | 29 | ## Usage 30 | 31 | ### MockFunction 32 | 33 | The basic usage is to create your mock function with `MockFunction.new` and then use `mocha` to stub any particular calls that you need 34 | 35 | ```ruby 36 | require 'spec_helper' 37 | 38 | describe 'foo::bar' do 39 | 40 | let!(:add_stuff) { MockFunction.new('add_stuff') { |f| 41 | f.stubbed.with(1, 2).returns(3) 42 | } 43 | } 44 | 45 | it 'should do something with add_stuff' do 46 | # Specific stub for this test 47 | add_stuff.stubbed.with(2, 3).returns(5) 48 | ... 49 | end 50 | end 51 | ``` 52 | 53 | You can mock a function that doesn't return a value (`:rvalue` is the default): 54 | ```ruby 55 | MockFunction.new('func', {:type => :statement}) 56 | ``` 57 | 58 | You can mock Hiera: 59 | ```ruby 60 | MockFunction.new('hiera') { |f| 61 | f.stubbed.with('non-ex').raises(Puppet::ParseError.new('Key not found')) 62 | f.stubbed.with('db-password').returns('password1') 63 | } 64 | ``` 65 | You handle when the functions are created yourself, e.g. you can assign it to a local variable `func = MockFunction...` create it in a before block `before(:each) do MockFunction... end` or use let `let!(:func) { MockFunction... }` 66 | 67 | If you use let, **use `let!()` and not `let()`**, this is because lets are lazy-loaded, so unless you explicitly reference your function in each test, the function won't be created and puppet won't find it. Using `let!` means that the function will be created before every test regardless. 68 | 69 | Also if you use `let` when mocking hiera, **you can't use `:hiera` as the name due to conflicts** so you have to do something like `let!(:mock_hiera) { MockFunction.new('hiera') }` 70 | 71 | ##### Mocha stubs and expects: 72 | `f.stubbed` and `f.expected` are helper methods for `f.stubs(:execute)` and `f.expects(:execute)` 73 | 74 | Internally `#expected` will clear the rspec-puppet catalog cache. This is because rspec-puppet will only re-compile the catalog for a test if `:title`, `:params`, or `:facts` are changed. This means that if you setup an expectaion in a test, it might not be satisfied because the catalog was already compiled for a previous test, and so the functions weren't called! 75 | 76 | Clearing the cache ensures tests aren't coupled and order dependent. The downside is that the catalog isn't cached and has to be re-compiled which slows down your tests. If you're concerned about performance and you are explicitly changing `:title`, `:params`, or `:facts` for a test, you can keep the cache intact with `f.expected(:keep_cache)` 77 | 78 | ##### Notes: 79 | - You always stub the `execute` method as that gets called internally 80 | - The `execute` method takes a set of arguments instead of an array of arguments 81 | 82 | ### MockResource (experimental feature) 83 | 84 | I've created a rough version for now just to help myself out, if people find it useful or find bugs, let me know 85 | 86 | ##### Usage: 87 | 88 | To stop your tests dissapearing down a rabbit hole, you can use the rspec-puppet `let(:pre_condition) { ... }` feature to create mock versions of resources that your puppet class depends on. For example: 89 | 90 | ```puppet 91 | class my_module::my_class { 92 | 93 | include foo::bar 94 | 95 | $useful_var = $foo::bar::baz 96 | 97 | external_module::complex_type { 'complex thing': 98 | param_one => 'one', 99 | param_two => 'two', 100 | required_param => 'important value', 101 | } 102 | 103 | 104 | } 105 | ``` 106 | 107 | In the tests for `my_class`, you don't want to use the actual `foo::bar` and `external_module::complex_type` resources because it could be a lot of complex setup code, it can be difficult to test multiple scenarios, and you are by extension testing these other classes (which should have tests of their own) 108 | 109 | You can therefore mock these resources by creating fakes that have the same "interface", but empty bodies: 110 | ```ruby 111 | let(:pre_condition) { [ 112 | "class foo::bar { $baz = 'a value' }", 113 | "define external_module::complex_type ( $param_one = 'default', $param_two = undef, $required_param ) {}", 114 | ] } 115 | ``` 116 | 117 | This can get quite complex if there are multiple parameters and/or internal variables. `MockResource` is designed to make it easier to mock out these dependencies 118 | ```ruby 119 | mock_bar = MockResource.new 'foo::bar', { 120 | :vars => { :baz => some_var_you_want_to_use_later } 121 | } 122 | 123 | mock_complex_type = MockResource.new 'external_module::complex_type', { 124 | :type => :define, 125 | :params => { 126 | :param_one => 'default', 127 | :param_two => :undef, 128 | :required_param => nil, 129 | } 130 | } 131 | 132 | let(:pre_condition) { [ 133 | mock_bar.render, 134 | mock_complex_thing.render, 135 | ] } 136 | ``` 137 | 138 | Hopefully you spend less time debugging syntax errors in your test strings, and more time writing useful code 139 | 140 | ### TemplateHarness 141 | 142 | If your templates have some logic in them that you want to test, you'd ideally like to get hold of the generated template so you can inspect it programmatically rather than just using a regex. In this case use `TemplateHarness` 143 | 144 | Given a basic template: 145 | 146 | 147 | ```erb 148 | <% 149 | from_class = @class_var 150 | from_fact = scope.lookupvar('fact-name') 151 | from_hiera = scope.function_hiera('hiera-key') 152 | -%> 153 | <%= "#{from_class} #{from_fact} #{from_hiera}" %> 154 | 155 | ``` 156 | 157 | A test could look like this: 158 | 159 | 160 | ```ruby 161 | require 'spec_helper' 162 | 163 | describe 'my_template' do 164 | 165 | let(:scope) { PuppetlabsSpec::PuppetInternals.scope } 166 | before(:each) do 167 | scope.stubs(:lookupvar).with('fact-name').returns('fact-value') 168 | scope.stubs(:function_hiera).with('hiera-key').returns('hiera-value') 169 | end 170 | 171 | it 'should render template' do 172 | harness = TemplateHarness.new('spec/.../.../my_template.erb', scope) 173 | harness.set('@class_var', 'classy') 174 | result = harness.run 175 | expect(result).to eq 'classy fact-value hiera-value' 176 | end 177 | 178 | end 179 | ``` 180 | 181 | Note: 182 | - The path resolution is pretty simple, just pass it a normal relative path, **not** like the paths you pass into the `template` function in puppet (where you expect puppet to add the `templates` section to the path) 183 | 184 | ### HieraData::Validator 185 | 186 | The motivation behind this is to quickly check that your hiera data files have no syntax errors without having to run all of the possible combinations of your hiera hierarchy. At the moment this only supports yaml, but other file types can be added easily. 187 | 188 | ```ruby 189 | require 'spec_helper' 190 | 191 | describe 'YAML hieradata' do 192 | 193 | # Files are loaded recursively 194 | validator = HieraData::YamlValidator.new('spec/fixtures/hieradata') 195 | validator.load_data :ignore_empty 196 | # Use load_data without args to catch empty files 197 | 198 | # Check types 199 | it 'should use arrays for api host lists' do 200 | validator.validate('my-api-hosts') { |v| 201 | expect(v).to be_an Array 202 | } 203 | end 204 | 205 | # Use regex to match keys 206 | it 'ports should only contain digits' do 207 | validator.validate(/-port$/) { |v| 208 | expect(v).to match /^[0-9]+$/ 209 | } 210 | end 211 | 212 | # Supply a list of files that the key must be in 213 | # (all matches in all other files are still validated) 214 | # :live and :qa correspond to live.yaml and qa.yaml 215 | it 'should override password in live and qa' do 216 | validator.validate('password', [:live, :qa]) { |v| 217 | expect ... 218 | } 219 | end 220 | 221 | end 222 | ``` 223 | 224 | In the examples above all keys in all yaml files are searched and checked 225 | 226 | If there is an error, you'll see the inner RSpec error, as well as which key and which file is incorrect: 227 | 228 | ``` 229 | RSpecPuppetUtils::HieraData::ValidationError: mail-smtp-port is invalid in live: expected "TwoFive" to match /^[0-9]+$/ 230 | Diff: 231 | @@ -1,2 +1,2 @@ 232 | -/^[0-9]+$/ 233 | +"TwoFive" 234 | ``` 235 | 236 | For more about usage see the [wiki page](../../wiki/Hiera-Data-Validator) 237 | 238 | ### Setup 239 | - Add `rspec-puppet-utils` to your Gemfile (or use `gem install rspec-puppet-utils`) 240 | - Add `require 'rspec-puppet-utils'` to the top of your `spec_helper` 241 | 242 | ## Rake Tasks (experimental feature) 243 | 244 | `rspec-puppet-utils` also provides helper classes to add common rake tasks to a Puppet project or module. 245 | 246 | ### Project Tasks 247 | 248 | The `Rake::Puppet` class provides tasks that handle testing and building a Puppet project. 249 | 250 | ##### Usage 251 | 252 | An example `Rakefile` might look like this: 253 | 254 | ```ruby 255 | require 'rspec_puppet_utils/rake/project_tasks' 256 | 257 | puppet = Rake::Puppet.new 258 | puppet.package_version = '1.0.0' 259 | puppet.load_tasks 260 | ``` 261 | 262 | Running `rake -T` should now show a list of spec and build tasks: 263 | 264 | ```bash 265 | $ rake -T 266 | rake build # Build puppet.zip v1.0.0 267 | rake quick_build # Build puppet.zip v1.0.0 without tests 268 | rake spec # Run specs in all modules 269 | rake :spec # Run module specs 270 | rake :spec # Run module specs 271 | ... 272 | ``` 273 | 274 | There is an spec task for each module, as well as a main `spec` task that will run all specs in a project. 275 | 276 | The `build` task will bundle all Puppet code (modules, hiera data file, environment.conf files, etc) into a .zip file which can then be deployed. 277 | 278 | In the example above `package_version` is set as it's a required field. The other accessible properties are: 279 | 280 | - module_path - The directory containing all the modules to test (default: 'modules') 281 | - excluded_modules - Modules to exclude from rspec testing (default: []) 282 | - package_dir - Where the puppet zip package will be created (default: 'pkg') 283 | - package_files - Files and directories to include in the package (default: ['modules', 'modules-lib', 'environment.conf']) 284 | - package_versioning - Is the version included in the package name? (default: true) 285 | 286 | ##### Setup 287 | 288 | The `spec` task for each module actually just executes the `spec` task defined in the module's Rakefile, 289 | and the `Rake::Puppet` class will filter out modules that dont have a Rakefile, 290 | 291 | Therefore each module needs the following two files: 292 | 293 | ```ruby 294 | # /Rakefile 295 | require 'puppetlabs_spec_helper/rake_tasks' 296 | ``` 297 | 298 | ```ruby 299 | # /spec/spec_helper.rb 300 | require 'rspec-puppet-utils' 301 | require 'puppetlabs_spec_helper/module_spec_helper' 302 | ``` 303 | 304 | Extra content/tasks/options/etc can be added to these files, but this is the suggested minimum 305 | 306 | In order for the `modules-lib` modules to be available to the Puppet server, each environment will need an `environment.conf` file to set the module path, 307 | therefore an `environment.conf` file should be present in the project root: 308 | 309 | ``` 310 | # environment.conf 311 | modulepath = ./modules:./modules-lib:$basemodulepath 312 | ``` 313 | 314 | Again, other settings can be changed, this is just the minimum to get the modules-lib pattern working 315 | 316 | ##### NB 317 | 318 | The `package_files` list is setup for the modules-lib pattern by default. In this pattern external (e.g. Puppet Forge) modules are installed in a separate 'modules-lib', leaving the 'modules' dir for project modules such as 'components', 'profiles', 'role', etc. 319 | If you're not using this pattern then just provide a new array for `package_files`. 320 | 321 | Running the `build` or `quick_build` tasks will delete any existing builds in the `pkg` directory. 322 | This is so the same build task can be run over and over on a build server (e.g. Jenkins) without filling up the disk. 323 | It also guarantees that the binary at the end of a build was just built, and wasn't left over from a previous build. 324 | 325 | ##### ToDo 326 | 327 | Currently the `spec` task runs all the `:spec` tasks. If one of these fails then none of the subsequent tasks will run. This isn't ideal! 328 | 329 | The zip commands need to be replaced by ruby zip library to avoid shelling out, this helps with support for Windows environments 330 | --------------------------------------------------------------------------------