Skip to content

Commit

Permalink
Add setting to use custom Facter implementation
Browse files Browse the repository at this point in the history
Starting with versions 7.12.0/6.25.0, Puppet was changed not to directly
depend on Facter anymore, but to use a `Puppet::Runtime` implementation
instead (e.g. calls to `Facter` were changed to
`Puppet.runtime[:facter]` to allow for pluggable Facter backends).

rspec-puppet stubs facts from facterdb by setting custom facts with
higher weights, meaning that Facter 4 will still resolve the underlying
core facts (which is by design), leading to noticeable performance hits
when compiling catalogs with Facter 4 as opposed to Facter 2 (which just
returned the custom fact).

This means we can achieve a pretty big performance improvement with
rspec-puppet by registering a custom Facter implementation that bypasses
Facter altogether and just saves facts to hash.

This behavior cand be activated by setting `facter_implementation` to
`rspec` in `RSpec.configure`. By default, the setting has the value of
`facter` which maintains the old behavior of going through Facter. If
`rspec` is set but the Puppet version does not support Facter
implementations, rspec will warn and fall back to using Facter.
  • Loading branch information
GabrielNagy committed Nov 8, 2021
1 parent 84a9cfe commit 48c5605
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 5 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,20 @@ In some circumstances (e.g. where your nodename/certname is not the same as
your FQDN), this behaviour is undesirable and can be disabled by changing this
setting to `false`.

#### facter\_implementation
Type | Default | Puppet Version(s)
------- | -------- | -----------------
String | `facter` | 6.25+, 7.12+

Configures rspec-puppet to use a specific Facter implementation for running
unit tests. If the `rspec` implementation is set and Puppet does not support
it, rspec-puppet will warn and fall back to the `facter` implementation.
Setting an unsupported option will make rspec-puppet raise an error.

* `facter` - Use the default implementation, honoring the Facter version specified in the Gemfile
* `rspec` - Use a custom hash-based implementation of Facter defined in
rspec-puppet (this provides a considerable gain in speed if tests are run with Facter 4)

## Naming conventions

For clarity and consistency, I recommend that you use the following directory
Expand Down
1 change: 1 addition & 0 deletions lib/rspec-puppet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def self.current_example
c.add_setting :default_node_params, :default => {}
c.add_setting :default_trusted_facts, :default => {}
c.add_setting :default_trusted_external_data, :default => {}
c.add_setting :facter_implementation, :default => 'facter'
c.add_setting :hiera_config, :default => Puppet::Util::Platform.actually_windows? ? 'c:/nul/' : '/dev/null'
c.add_setting :parser, :default => 'current'
c.add_setting :trusted_node_data, :default => false
Expand Down
59 changes: 59 additions & 0 deletions lib/rspec-puppet/adapters.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'rspec-puppet/facter_impl'

module RSpec::Puppet
module Adapters

Expand Down Expand Up @@ -108,6 +110,17 @@ def manifest
end

class Adapter40 < Base
#
# @api private
#
# Set the FacterImpl constant to the given Facter implementation.
# The method noops if the constant is already set
#
# @param impl [Object]
def set_facter_impl(impl)
Object.send(:const_set, :FacterImpl, impl) unless defined? FacterImpl
end

def setup_puppet(example_group)
super

Expand Down Expand Up @@ -186,6 +199,12 @@ def manifest
end

class Adapter4X < Adapter40
def setup_puppet(example_group)
super

set_facter_impl(Facter)
end

def settings_map
super.concat([
[:trusted_server_facts, :trusted_server_facts]
Expand All @@ -194,6 +213,46 @@ def settings_map
end

class Adapter6X < Adapter40
#
# @api private
#
# Check to see if Facter runtime implementations are supported in the
# current Puppet version
#
# @return [Boolean] true if runtime implementations are supported
def supports_facter_runtime?
unless defined?(@supports_facter_runtime)
begin
Puppet.runtime[:facter]
@supports_facter_runtime = true
rescue
@supports_facter_runtime = false
end
end

@supports_facter_runtime
end

def setup_puppet(example_group)
case RSpec.configuration.facter_implementation.to_sym
when :rspec
if supports_facter_runtime?
Puppet.runtime[:facter] = proc { RSpec::Puppet::FacterTestImpl.new }
set_facter_impl(Puppet.runtime[:facter])
else
warn "Facter runtime implementations are not supported in Puppet #{Puppet.version}, continuing with facter_implementation 'facter'"
RSpec.configuration.facter_implementation = 'facter'
set_facter_impl(Facter)
end
when :facter
set_facter_impl(Facter)
else
raise "Unsupported facter_implementation '#{RSpec.configuration.facter_implementation}'"
end

super
end

def settings_map
super.concat([
[:basemodulepath, :basemodulepath],
Expand Down
49 changes: 49 additions & 0 deletions lib/rspec-puppet/facter_impl.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module RSpec::Puppet

# Implements a simple hash-based version of Facter to be used in module tests
# that use rspec-puppet.
class FacterTestImpl
def initialize
@facts = {}
end

def value(fact_name)
@facts[fact_name.to_s]
end

def clear
@facts.clear
end

def to_hash
@facts
end

def add(name, options = {}, &block)
raise 'Facter.add expects a block' unless block_given?
@facts[name.to_s] = instance_eval(&block)
end

# noop methods
def debugging(arg); end

def reset; end

def search(*paths); end

def setup_logging; end

private

def setcode(string = nil, &block)
if block_given?
value = block.call
else
value = string
end

value
end
end
end

10 changes: 5 additions & 5 deletions lib/rspec-puppet/support.rb
Original file line number Diff line number Diff line change
Expand Up @@ -337,16 +337,16 @@ def server_facts_hash
{"servername" => "fqdn",
"serverip" => "ipaddress"
}.each do |var, fact|
if value = Facter.value(fact)
if value = FacterImpl.value(fact)
server_facts[var] = value
else
warn "Could not retrieve fact #{fact}"
end
end

if server_facts["servername"].nil?
host = Facter.value(:hostname)
if domain = Facter.value(:domain)
host = FacterImpl.value(:hostname)
if domain = FacterImpl.value(:domain)
server_facts["servername"] = [host, domain].join(".")
else
server_facts["servername"] = host
Expand Down Expand Up @@ -478,8 +478,8 @@ def build_catalog_without_cache_v2(

def stub_facts!(facts)
Puppet.settings[:autosign] = false if Puppet.settings.include? :autosign
Facter.clear
facts.each { |k, v| Facter.add(k, :weight => 999) { setcode { v } } }
FacterImpl.clear
facts.each { |k, v| FacterImpl.add(k, :weight => 999) { setcode { v } } }
end

def build_catalog(*args)
Expand Down
56 changes: 56 additions & 0 deletions spec/unit/adapters_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,62 @@ def context_double(options = {})
end
end

describe RSpec::Puppet::Adapters::Adapter6X, :if => (6.0 ... 6.25).include?(Puppet.version.to_f) do

let(:test_context) { double :environment => 'rp_env' }

describe '#setup_puppet' do
describe 'when managing the facter_implementation' do
after(:each) do
Object.send(:remove_const, :FacterImpl) if defined? FacterImpl
end

it 'warns and falls back if hash implementation is set and facter runtime is not supported' do
context = context_double
allow(RSpec.configuration).to receive(:facter_implementation).and_return('rspec')
expect(subject).to receive(:warn)
.with("Facter runtime implementations are not supported in Puppet #{Puppet.version}, continuing with facter_implementation 'facter'")
subject.setup_puppet(context)
expect(FacterImpl).to be(Facter)
end
end
end
end

describe RSpec::Puppet::Adapters::Adapter6X, :if => Puppet::Util::Package.versioncmp(Puppet.version, '6.25.0') >= 0 do

let(:test_context) { double :environment => 'rp_env' }

describe '#setup_puppet' do
describe 'when managing the facter_implementation' do
after(:each) do
Object.send(:remove_const, :FacterImpl) if defined? FacterImpl
end

it 'uses facter as default implementation' do
context = context_double
subject.setup_puppet(context)
expect(FacterImpl).to be(Facter)
end

it 'uses the hash implementation if set and if puppet supports runtimes' do
context = context_double
Puppet.runtime[:facter] = 'something'
allow(RSpec.configuration).to receive(:facter_implementation).and_return('rspec')
subject.setup_puppet(context)
expect(FacterImpl).to be_kind_of(RSpec::Puppet::FacterTestImpl)
end

it 'raises if given an unsupported option' do
context = context_double
allow(RSpec.configuration).to receive(:facter_implementation).and_return('salam')
expect { subject.setup_puppet(context) }
.to raise_error(RuntimeError, "Unsupported facter_implementation 'salam'")
end
end
end
end

describe RSpec::Puppet::Adapters::Adapter4X, :if => (4.0 ... 6.0).include?(Puppet.version.to_f) do

let(:test_context) { double :environment => 'rp_env' }
Expand Down
84 changes: 84 additions & 0 deletions spec/unit/facter_impl_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
require 'spec_helper'
require 'rspec-puppet/facter_impl'

describe RSpec::Puppet::FacterTestImpl do
subject(:facter_impl) { RSpec::Puppet::FacterTestImpl.new }
let(:fact_hash) do
{
'string_fact' => 'string_value',
'hash_fact' => { 'key' => 'value' },
'int_fact' => 3,
'true_fact' => true,
'false_fact' => false,
}
end

before do
facter_impl.add(:string_fact) { setcode { 'string_value' } }
facter_impl.add(:hash_fact) { setcode { { 'key' => 'value' } } }
facter_impl.add(:int_fact) { setcode { 3 } }
facter_impl.add(:true_fact) { setcode { true } }
facter_impl.add(:false_fact) { setcode { false } }
end

describe 'noop methods' do
[:debugging, :reset, :search, :setup_logging].each do |method|
it "implements ##{method}" do
expect(facter_impl).to respond_to(method)
end
end
end

describe '#value' do
it 'retrieves a fact of type String' do
expect(facter_impl.value(:string_fact)).to eq('string_value')
end

it 'retrieves a fact of type Hash' do
expect(facter_impl.value(:hash_fact)).to eq({ 'key' => 'value' })
end

it 'retrieves a fact of type Integer' do
expect(facter_impl.value(:int_fact)).to eq(3)
end

it 'retrieves a fact of type TrueClass' do
expect(facter_impl.value(:true_fact)).to eq(true)
end

it 'retrieves a fact of type FalseClass' do
expect(facter_impl.value(:false_fact)).to eq(false)
end
end

describe '#to_hash' do
it 'returns a hash with all added facts' do
expect(facter_impl.to_hash).to eq(fact_hash)
end
end

describe '#clear' do
it 'clears the fact hash' do
facter_impl.clear
expect(facter_impl.to_hash).to be_empty
end
end

describe '#add' do
before { facter_impl.clear }

it 'adds a fact with a setcode block' do
facter_impl.add(:setcode_block) { setcode { 'value' } }
expect(facter_impl.value(:setcode_block)).to eq('value')
end

it 'adds a fact with a setcode string' do
facter_impl.add(:setcode_string) { setcode 'value' }
expect(facter_impl.value(:setcode_string)).to eq('value')
end

it 'fails when not given a block' do
expect { facter_impl.add(:something) }.to raise_error(RuntimeError, 'Facter.add expects a block')
end
end
end

0 comments on commit 48c5605

Please sign in to comment.