Skip to content

Commit 48c5605

Browse files
committed
Add setting to use custom Facter implementation
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.
1 parent 84a9cfe commit 48c5605

File tree

7 files changed

+268
-5
lines changed

7 files changed

+268
-5
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,20 @@ In some circumstances (e.g. where your nodename/certname is not the same as
230230
your FQDN), this behaviour is undesirable and can be disabled by changing this
231231
setting to `false`.
232232

233+
#### facter\_implementation
234+
Type | Default | Puppet Version(s)
235+
------- | -------- | -----------------
236+
String | `facter` | 6.25+, 7.12+
237+
238+
Configures rspec-puppet to use a specific Facter implementation for running
239+
unit tests. If the `rspec` implementation is set and Puppet does not support
240+
it, rspec-puppet will warn and fall back to the `facter` implementation.
241+
Setting an unsupported option will make rspec-puppet raise an error.
242+
243+
* `facter` - Use the default implementation, honoring the Facter version specified in the Gemfile
244+
* `rspec` - Use a custom hash-based implementation of Facter defined in
245+
rspec-puppet (this provides a considerable gain in speed if tests are run with Facter 4)
246+
233247
## Naming conventions
234248

235249
For clarity and consistency, I recommend that you use the following directory

lib/rspec-puppet.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def self.current_example
4343
c.add_setting :default_node_params, :default => {}
4444
c.add_setting :default_trusted_facts, :default => {}
4545
c.add_setting :default_trusted_external_data, :default => {}
46+
c.add_setting :facter_implementation, :default => 'facter'
4647
c.add_setting :hiera_config, :default => Puppet::Util::Platform.actually_windows? ? 'c:/nul/' : '/dev/null'
4748
c.add_setting :parser, :default => 'current'
4849
c.add_setting :trusted_node_data, :default => false

lib/rspec-puppet/adapters.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'rspec-puppet/facter_impl'
2+
13
module RSpec::Puppet
24
module Adapters
35

@@ -108,6 +110,17 @@ def manifest
108110
end
109111

110112
class Adapter40 < Base
113+
#
114+
# @api private
115+
#
116+
# Set the FacterImpl constant to the given Facter implementation.
117+
# The method noops if the constant is already set
118+
#
119+
# @param impl [Object]
120+
def set_facter_impl(impl)
121+
Object.send(:const_set, :FacterImpl, impl) unless defined? FacterImpl
122+
end
123+
111124
def setup_puppet(example_group)
112125
super
113126

@@ -186,6 +199,12 @@ def manifest
186199
end
187200

188201
class Adapter4X < Adapter40
202+
def setup_puppet(example_group)
203+
super
204+
205+
set_facter_impl(Facter)
206+
end
207+
189208
def settings_map
190209
super.concat([
191210
[:trusted_server_facts, :trusted_server_facts]
@@ -194,6 +213,46 @@ def settings_map
194213
end
195214

196215
class Adapter6X < Adapter40
216+
#
217+
# @api private
218+
#
219+
# Check to see if Facter runtime implementations are supported in the
220+
# current Puppet version
221+
#
222+
# @return [Boolean] true if runtime implementations are supported
223+
def supports_facter_runtime?
224+
unless defined?(@supports_facter_runtime)
225+
begin
226+
Puppet.runtime[:facter]
227+
@supports_facter_runtime = true
228+
rescue
229+
@supports_facter_runtime = false
230+
end
231+
end
232+
233+
@supports_facter_runtime
234+
end
235+
236+
def setup_puppet(example_group)
237+
case RSpec.configuration.facter_implementation.to_sym
238+
when :rspec
239+
if supports_facter_runtime?
240+
Puppet.runtime[:facter] = proc { RSpec::Puppet::FacterTestImpl.new }
241+
set_facter_impl(Puppet.runtime[:facter])
242+
else
243+
warn "Facter runtime implementations are not supported in Puppet #{Puppet.version}, continuing with facter_implementation 'facter'"
244+
RSpec.configuration.facter_implementation = 'facter'
245+
set_facter_impl(Facter)
246+
end
247+
when :facter
248+
set_facter_impl(Facter)
249+
else
250+
raise "Unsupported facter_implementation '#{RSpec.configuration.facter_implementation}'"
251+
end
252+
253+
super
254+
end
255+
197256
def settings_map
198257
super.concat([
199258
[:basemodulepath, :basemodulepath],

lib/rspec-puppet/facter_impl.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
module RSpec::Puppet
2+
3+
# Implements a simple hash-based version of Facter to be used in module tests
4+
# that use rspec-puppet.
5+
class FacterTestImpl
6+
def initialize
7+
@facts = {}
8+
end
9+
10+
def value(fact_name)
11+
@facts[fact_name.to_s]
12+
end
13+
14+
def clear
15+
@facts.clear
16+
end
17+
18+
def to_hash
19+
@facts
20+
end
21+
22+
def add(name, options = {}, &block)
23+
raise 'Facter.add expects a block' unless block_given?
24+
@facts[name.to_s] = instance_eval(&block)
25+
end
26+
27+
# noop methods
28+
def debugging(arg); end
29+
30+
def reset; end
31+
32+
def search(*paths); end
33+
34+
def setup_logging; end
35+
36+
private
37+
38+
def setcode(string = nil, &block)
39+
if block_given?
40+
value = block.call
41+
else
42+
value = string
43+
end
44+
45+
value
46+
end
47+
end
48+
end
49+

lib/rspec-puppet/support.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -337,16 +337,16 @@ def server_facts_hash
337337
{"servername" => "fqdn",
338338
"serverip" => "ipaddress"
339339
}.each do |var, fact|
340-
if value = Facter.value(fact)
340+
if value = FacterImpl.value(fact)
341341
server_facts[var] = value
342342
else
343343
warn "Could not retrieve fact #{fact}"
344344
end
345345
end
346346

347347
if server_facts["servername"].nil?
348-
host = Facter.value(:hostname)
349-
if domain = Facter.value(:domain)
348+
host = FacterImpl.value(:hostname)
349+
if domain = FacterImpl.value(:domain)
350350
server_facts["servername"] = [host, domain].join(".")
351351
else
352352
server_facts["servername"] = host
@@ -478,8 +478,8 @@ def build_catalog_without_cache_v2(
478478

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

485485
def build_catalog(*args)

spec/unit/adapters_spec.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,62 @@ def context_double(options = {})
178178
end
179179
end
180180

181+
describe RSpec::Puppet::Adapters::Adapter6X, :if => (6.0 ... 6.25).include?(Puppet.version.to_f) do
182+
183+
let(:test_context) { double :environment => 'rp_env' }
184+
185+
describe '#setup_puppet' do
186+
describe 'when managing the facter_implementation' do
187+
after(:each) do
188+
Object.send(:remove_const, :FacterImpl) if defined? FacterImpl
189+
end
190+
191+
it 'warns and falls back if hash implementation is set and facter runtime is not supported' do
192+
context = context_double
193+
allow(RSpec.configuration).to receive(:facter_implementation).and_return('rspec')
194+
expect(subject).to receive(:warn)
195+
.with("Facter runtime implementations are not supported in Puppet #{Puppet.version}, continuing with facter_implementation 'facter'")
196+
subject.setup_puppet(context)
197+
expect(FacterImpl).to be(Facter)
198+
end
199+
end
200+
end
201+
end
202+
203+
describe RSpec::Puppet::Adapters::Adapter6X, :if => Puppet::Util::Package.versioncmp(Puppet.version, '6.25.0') >= 0 do
204+
205+
let(:test_context) { double :environment => 'rp_env' }
206+
207+
describe '#setup_puppet' do
208+
describe 'when managing the facter_implementation' do
209+
after(:each) do
210+
Object.send(:remove_const, :FacterImpl) if defined? FacterImpl
211+
end
212+
213+
it 'uses facter as default implementation' do
214+
context = context_double
215+
subject.setup_puppet(context)
216+
expect(FacterImpl).to be(Facter)
217+
end
218+
219+
it 'uses the hash implementation if set and if puppet supports runtimes' do
220+
context = context_double
221+
Puppet.runtime[:facter] = 'something'
222+
allow(RSpec.configuration).to receive(:facter_implementation).and_return('rspec')
223+
subject.setup_puppet(context)
224+
expect(FacterImpl).to be_kind_of(RSpec::Puppet::FacterTestImpl)
225+
end
226+
227+
it 'raises if given an unsupported option' do
228+
context = context_double
229+
allow(RSpec.configuration).to receive(:facter_implementation).and_return('salam')
230+
expect { subject.setup_puppet(context) }
231+
.to raise_error(RuntimeError, "Unsupported facter_implementation 'salam'")
232+
end
233+
end
234+
end
235+
end
236+
181237
describe RSpec::Puppet::Adapters::Adapter4X, :if => (4.0 ... 6.0).include?(Puppet.version.to_f) do
182238

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

spec/unit/facter_impl_spec.rb

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
require 'spec_helper'
2+
require 'rspec-puppet/facter_impl'
3+
4+
describe RSpec::Puppet::FacterTestImpl do
5+
subject(:facter_impl) { RSpec::Puppet::FacterTestImpl.new }
6+
let(:fact_hash) do
7+
{
8+
'string_fact' => 'string_value',
9+
'hash_fact' => { 'key' => 'value' },
10+
'int_fact' => 3,
11+
'true_fact' => true,
12+
'false_fact' => false,
13+
}
14+
end
15+
16+
before do
17+
facter_impl.add(:string_fact) { setcode { 'string_value' } }
18+
facter_impl.add(:hash_fact) { setcode { { 'key' => 'value' } } }
19+
facter_impl.add(:int_fact) { setcode { 3 } }
20+
facter_impl.add(:true_fact) { setcode { true } }
21+
facter_impl.add(:false_fact) { setcode { false } }
22+
end
23+
24+
describe 'noop methods' do
25+
[:debugging, :reset, :search, :setup_logging].each do |method|
26+
it "implements ##{method}" do
27+
expect(facter_impl).to respond_to(method)
28+
end
29+
end
30+
end
31+
32+
describe '#value' do
33+
it 'retrieves a fact of type String' do
34+
expect(facter_impl.value(:string_fact)).to eq('string_value')
35+
end
36+
37+
it 'retrieves a fact of type Hash' do
38+
expect(facter_impl.value(:hash_fact)).to eq({ 'key' => 'value' })
39+
end
40+
41+
it 'retrieves a fact of type Integer' do
42+
expect(facter_impl.value(:int_fact)).to eq(3)
43+
end
44+
45+
it 'retrieves a fact of type TrueClass' do
46+
expect(facter_impl.value(:true_fact)).to eq(true)
47+
end
48+
49+
it 'retrieves a fact of type FalseClass' do
50+
expect(facter_impl.value(:false_fact)).to eq(false)
51+
end
52+
end
53+
54+
describe '#to_hash' do
55+
it 'returns a hash with all added facts' do
56+
expect(facter_impl.to_hash).to eq(fact_hash)
57+
end
58+
end
59+
60+
describe '#clear' do
61+
it 'clears the fact hash' do
62+
facter_impl.clear
63+
expect(facter_impl.to_hash).to be_empty
64+
end
65+
end
66+
67+
describe '#add' do
68+
before { facter_impl.clear }
69+
70+
it 'adds a fact with a setcode block' do
71+
facter_impl.add(:setcode_block) { setcode { 'value' } }
72+
expect(facter_impl.value(:setcode_block)).to eq('value')
73+
end
74+
75+
it 'adds a fact with a setcode string' do
76+
facter_impl.add(:setcode_string) { setcode 'value' }
77+
expect(facter_impl.value(:setcode_string)).to eq('value')
78+
end
79+
80+
it 'fails when not given a block' do
81+
expect { facter_impl.add(:something) }.to raise_error(RuntimeError, 'Facter.add expects a block')
82+
end
83+
end
84+
end

0 commit comments

Comments
 (0)