Skip to content

Commit

Permalink
Deep freeze only strings, arrays and hashes
Browse files Browse the repository at this point in the history
Freezing a class prevents it from being modified at all, which can
happen in unit tests with mocha or rspec-expections.

Calling a function with `String` as an argument will freeze the entire
class and prevent `allow_any_instance_of(String)...` type expectations
from adding their hooks to the class.

e.g. stdlib:
  https://github.com/puppetlabs/puppetlabs-stdlib/blob/00c881d/spec/functions/is_a_spec.rb#L18
  https://github.com/puppetlabs/puppetlabs-stdlib/blob/00c881d/spec/functions/pw_hash_spec.rb#L57

This whitelists strings, arrays and hashes as safe classes to freeze
when testing functions, others are left thawed to prevent side effects.
Deep freezing is used on arrays/hashes to match Puppet's behaviour of
freezing facts.
  • Loading branch information
domcleal committed Jan 27, 2017
1 parent b381c0e commit 351371b
Show file tree
Hide file tree
Showing 5 changed files with 35 additions and 5 deletions.
22 changes: 19 additions & 3 deletions lib/rspec-puppet/example/function_example_group.rb
Expand Up @@ -15,10 +15,8 @@ def initialize(name, func, overrides)

# This method is used by the `run` matcher to trigger the function execution, and provides a uniform interface across all puppet versions.
def execute(*args)
# puppet 4 arguments are immutable
args.map(&:freeze)
Puppet.override(@overrides, "rspec-test scope") do
@func.call(@overrides[:global_scope], *args)
@func.call(@overrides[:global_scope], *freeze_arg(args))
end
end

Expand All @@ -27,6 +25,24 @@ def call(scope, *args)
RSpec.deprecate("subject.call", :replacement => "is_expected.to run.with().and_raise_error(), or execute()")
execute(*args)
end

private

# Facts, keywords, single-quoted strings etc. are usually frozen in Puppet manifests, so freeze arguments to ensure functions are tested
# under worst-case conditions.
def freeze_arg(arg)
case arg
when Array
arg.each { |a| freeze_arg(a) }
arg.freeze
when Hash
arg.each { |k,v| freeze_arg(k); freeze_arg(v) }
arg.freeze
when String
arg.freeze
end
arg
end
end

class V3FunctionWrapper
Expand Down
@@ -0,0 +1,5 @@
Puppet::Functions.create_function(:frozen_array_function) do
def frozen_array_function(value)
value.frozen? && value.all?(&:frozen?)
end
end
@@ -1,5 +1,5 @@
Puppet::Functions.create_function(:frozen_function) do
def frozen_function(value)
value.reverse!
value.frozen?
end
end
@@ -0,0 +1,5 @@
Puppet::Functions.create_function(:frozen_hash_function) do
def frozen_hash_function(value)
value.frozen? && value.all? { |k,v| k.frozen? && v.frozen? }
end
end
6 changes: 5 additions & 1 deletion spec/functions/test_function_spec.rb
Expand Up @@ -6,5 +6,9 @@
end

describe 'frozen_function', :if => Puppet.version.to_f >= 4.0 do
it { is_expected.to run.with_params('foo').and_raise_error(RuntimeError, %r{can't modify frozen}) }
it { is_expected.to run.with_params('foo').and_return(true) }
it { is_expected.to run.with_params(String).and_return(false) }
it { is_expected.to run.with_params(true).and_return(true) }
it { is_expected.to run.with_params(['foo']).and_return(true) }
it { is_expected.to run.with_params('foo' => 'bar').and_return(true) }
end

0 comments on commit 351371b

Please sign in to comment.