File: function_example_group.rb

package info (click to toggle)
ruby-rspec-puppet 4.0.2%2Bds-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,444 kB
  • sloc: ruby: 6,377; makefile: 6
file content (188 lines) | stat: -rw-r--r-- 5,368 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# frozen_string_literal: true

module RSpec::Puppet
  module FunctionExampleGroup
    include RSpec::Puppet::FunctionMatchers
    include RSpec::Puppet::ManifestMatchers
    include RSpec::Puppet::Support

    class V4FunctionWrapper
      attr_reader :func, :func_name

      def initialize(name, func, overrides)
        @func_name = name
        @func = func
        @overrides = overrides
      end

      # 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, &block)
        Puppet.override(@overrides, 'rspec-test scope') do
          @func.call(@overrides[:global_scope], *freeze_arg(args), &block)
        end
      end

      # compatibility alias for existing tests
      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 do |k, v|
            freeze_arg(k)
            freeze_arg(v)
          end
          arg.freeze
        when String
          arg.freeze
        end
        arg
      end
    end

    class V3FunctionWrapper
      attr_accessor :func_name

      def initialize(name, func)
        @func_name = name
        @func = func
      end

      # 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)
        if args.nil?
          @func.call
        else
          @func.call(args)
        end
      end

      # This method was formerly used by the `run` matcher to trigger the function execution, and provides puppet versions dependant interface.
      def call(*args)
        RSpec.deprecate('subject.call', replacement: 'is_expected.to run.with().and_raise_error(), or execute()')
        if args.nil?
          @func.call
        else
          @func.call(*args)
        end
      end
    end

    # (at least) rspec 3.5 doesn't seem to memoize `subject` when called from
    # a before(:each) hook, so we need to memoize it ourselves.
    def subject
      @subject ||= find_function
    end

    def find_function(function_name = self.class.top_level_description)
      with_vardir do
        env = adapter.current_environment

        context_overrides = compiler.context_overrides
        func = nil
        loaders = Puppet.lookup(:loaders)
        Puppet.override(context_overrides, 'rspec-test scope') do
          func = V4FunctionWrapper.new(function_name,
                                       loaders.private_environment_loader.load(:function, function_name), context_overrides)
          @scope = context_overrides[:global_scope]
        end

        return func if func.func

        if Puppet::Parser::Functions.function(function_name)
          V3FunctionWrapper.new(function_name, scope.method(:"function_#{function_name}"))
        end
      end
    end

    def call_function(function_name, *args)
      scope.call_function(function_name, args)
    end

    def scope
      @scope ||= build_scope(compiler, nodename(:function))
    end

    def catalogue
      @catalogue ||= compiler.catalog
    end

    def rspec_puppet_cleanup
      @subject = nil
      @catalogue = nil
      @compiler = nil
      @scope = nil
    end

    private

    def compiler
      @compiler ||= build_compiler
    end

    # get a compiler with an attached compiled catalog
    def build_compiler
      node_name   = nodename(:function)
      fact_values = facts_hash(node_name)
      trusted_values = trusted_facts_hash(node_name)

      # Allow different Hiera configurations:
      HieraPuppet.instance_variable_set(:@hiera, nil) if defined? HieraPuppet

      # if we specify a pre_condition, we should ensure that we compile that
      # code into a catalog that is accessible from the scope where the
      # function is called
      Puppet[:code] = pre_cond

      node_facts = Puppet::Node::Facts.new(node_name, fact_values.dup)

      node_options = {
        parameters: fact_values,
        facts: node_facts
      }

      stub_facts! fact_values

      node = build_node(node_name, node_options)

      Puppet.push_context(
        {
          trusted_information: Puppet::Context::TrustedInformation.new('remote', node_name, trusted_values)
        },
        'Context for spec trusted hash'
      )

      compiler = Puppet::Parser::Compiler.new(node)
      compiler.compile
      loaders = Puppet::Pops::Loaders.new(adapter.current_environment)
      Puppet.push_context(
        {
          loaders: loaders,
          global_scope: compiler.context_overrides[:global_scope]
        },
        'set globals'
      )
      compiler
    end

    def build_scope(compiler, _node_name)
      compiler.context_overrides[:global_scope]
    end

    def build_node(name, opts = {})
      node_environment = adapter.current_environment
      opts[:environment] = node_environment
      Puppet::Node.new(name, opts)
    end
  end
end