bolt 0.21.3 → 0.21.4

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of bolt might be problematic. Click here for more details.

@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt_spec/plans/mock_executor'
4
+ require 'bolt/config'
5
+
6
+ # These helpers are intended to be used for plan unit testing without calling
7
+ # out to target nodes. It accomplishes this by replacing bolt's executor with a
8
+ # mock executor. The mock executor allows calls to run_* functions to be
9
+ # stubbed out for testing. By default this executor will fail on any run_*
10
+ # call but stubs can be set up with allow_* and expect_* functions.
11
+ #
12
+ # Stub matching
13
+ #
14
+ # Stubs match invocations of run_* functions by default matching any call but
15
+ # with_targets and with_params helpers can further restrict the stub to match
16
+ # more exact invocations. It's possible a call to run_* could match multiple
17
+ # stubs. In this case the mock executor will first check for stubs specifically
18
+ # matching the task being run after which it will use the last stub that
19
+ # matched
20
+ #
21
+ #
22
+ # allow vs expect
23
+ #
24
+ # Stubs have two general modes bases on whether the test is making assertions
25
+ # on whether function was called. Allow stubs allow the run_* invocation to
26
+ # be called any number of times while expect stubs will fail if no run_*
27
+ # invocation matches them. The be_called_times(n) stub method can be used to
28
+ # ensure an allow stub is not called more than n times or that an expect stub
29
+ # is called exactly n times.
30
+ #
31
+ # Configuration
32
+ #
33
+ # By default the plan helpers use the modulepath set up for rspec-puppet and
34
+ # an otherwise empty bolt config and inventory. To create your own values for
35
+ # these override the modulepath, config, or inventory methods.
36
+ #
37
+ #
38
+ # TODO:
39
+ # - allow stubbing for commands, scripts and file uploads
40
+ # - Allow description based stub matching
41
+ # - Better testing of plan errors
42
+ # - Better error collection around call counts. Show what stubs exists and more than a single failure
43
+ # - Allow stubbing with a block(at the double level? As a matched stub?)
44
+ # - package code so that it can be used for testing modules outside of this repo
45
+ # - set subject from describe and provide matchers similar to rspec puppets function tests
46
+ #
47
+ # MAYBE TODO?:
48
+ # - allow stubbing for subplans
49
+ # - validate call expectations at the end of the example instead of in run_plan
50
+ # - resultset matchers to help testing canary like plans?
51
+ # - inventory matchers to help testing plans that change inventory
52
+ #
53
+ # Example:
54
+ # describe "my_plan" do
55
+ # it 'should return' do
56
+ # allow_task('my_task').always_return({'result_key' => 10})
57
+ # expect(run_plan('my_plan', { 'param1' => 10 })).to be
58
+ # end
59
+ #
60
+ # it 'should call task with param1' do
61
+ # expect_task('my_task').with_params('param1' => 10).always_return({'result_key' => 10})
62
+ # expect(run_plan('my_plan', { 'param1' => 10 })).to eq(10)
63
+ # end
64
+ #
65
+ # it 'should call task with param1 once' do
66
+ # expect_task('my_task').with_params('param1' => 10).always_return({'result_key' => 10}).be_called_times(1)
67
+ # expect(run_plan('my_plan', { 'param1' => 10 })).to eq(10)
68
+ # end
69
+ #
70
+ # it 'should not_call task with 100' do
71
+ # allow_task('my_task').always_return({'result_key' => 10})
72
+ # # Any call with param1 => 100 will match this since it's added second
73
+ # expect_task('my_task').with_params('param1' => 100).not_be_called
74
+ # expect(run_plan('my_plan', { 'param1' => 10 })).to eq(10)
75
+ # end
76
+ #
77
+ # it 'should be called on both node1 and node2' do
78
+ # expect_task('my_task').with_targets(['node1', 'node2']).always_return({'result_key' => 10})
79
+ # expect(run_plan('my_plan', { 'param1' => 10 })).to eq(10)
80
+ # end
81
+ #
82
+ # it 'should average results from targets' do
83
+ # expect_task('my_task').return_for_targets({
84
+ # 'node1' => {'result_key' => 20},
85
+ # 'node2' => {'result_key' => 6} })
86
+ # expect(run_plan('my_plan', { 'param1' => 10 })).to eq(13)
87
+ # end
88
+ # end
89
+ #
90
+ module BoltSpec
91
+ module Plans
92
+ # Override in your tests if needed
93
+ def modulepath
94
+ [RSpec.configuration.module_path]
95
+ rescue NoMethodError
96
+ raise "RSpec.configuration.module_path not defined set up rspec puppet or define modulepath for this test"
97
+ end
98
+
99
+ # Override in your tests
100
+ def config
101
+ config = Bolt::Config.new(Bolt::Boltdir.new('.'), {})
102
+ config.modulepath = modulepath
103
+ config
104
+ end
105
+
106
+ # Override in your tests
107
+ def inventory
108
+ @inventory ||= Bolt::Inventory.new({})
109
+ end
110
+
111
+ def puppetdb_client
112
+ @puppetdb_client ||= mock('puppetdb_client')
113
+ end
114
+
115
+ def run_plan(name, params)
116
+ pal = Bolt::PAL.new(config.modulepath, config.hiera_config)
117
+ result = pal.run_plan(name, params, executor, inventory, puppetdb_client)
118
+
119
+ if executor.error_message
120
+ raise executor.error_message
121
+ end
122
+
123
+ executor.assert_call_expectations
124
+
125
+ result
126
+ end
127
+
128
+ # Allowed task stubs can be called up to be_called_times number
129
+ # of times
130
+ def allow_task(task_name)
131
+ executor.stub_task(task_name).add_stub
132
+ end
133
+
134
+ # Expected task stubs must be called exactly the expected number of times
135
+ # or at least once without be_called_times
136
+ def expect_task(task_name)
137
+ allow_task(task_name).expect_call
138
+ end
139
+
140
+ # This stub will catch any task call if there are no stubs specifically for that task
141
+ def allow_any_task
142
+ executor.stub_task(:default).add_stub
143
+ end
144
+
145
+ # Example helpers to mock other run functions
146
+ # The with_targets method makes sense for all stubs
147
+ # with_params could be reused for options
148
+ # They probably need special stub methods for other arguments through
149
+
150
+ # Scripts can be mocked like tasks by their name
151
+ # arguments is an array instead of a hash though
152
+ # so it probably should be set separately
153
+ # def allow_script(script_name)
154
+ #
155
+ # file uploads have a single destination and no arguments
156
+ # def allow_file_upload(source_name)
157
+ #
158
+ # Most of the information in commands is in the command string itself
159
+ # we may need more flexible allows than just the name/command string
160
+ # Only option params exist on a command.
161
+ # def allow_command(command)
162
+ # def allow_command_matching(command_regex)
163
+ # def allow_command(&block)
164
+ #
165
+ # Plan execution does not flow through the executor mocking may make sense but
166
+ # will be a separate effort.
167
+ # def allow_plan(plan_name)
168
+
169
+ # intended to be private below here
170
+ def executor
171
+ @executor ||= BoltSpec::Plans::MockExecutor.new
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/result_set'
5
+ require 'bolt/result'
6
+ require 'set'
7
+
8
+ module BoltSpec
9
+ module Plans
10
+ class UnexpectedInvocation < ArgumentError; end
11
+
12
+ # Nothing in the TaskDouble is 'public'
13
+ class TaskDouble
14
+ def initialize
15
+ @stubs = []
16
+ end
17
+
18
+ def process(targets, task, arguments, options)
19
+ # TODO: should we bother matching at all? or just call each
20
+ # stub until one works?
21
+ matches = @stubs.select { |s| s.matches(targets, task, arguments, options) }
22
+ unless matches.empty?
23
+ matches[0].call(targets, task, arguments, options)
24
+ end
25
+ end
26
+
27
+ def assert_called(taskname)
28
+ @stubs.each { |s| s.assert_called(taskname) }
29
+ end
30
+
31
+ def add_stub
32
+ stub = TaskStub.new
33
+ @stubs.unshift stub
34
+ stub
35
+ end
36
+ end
37
+
38
+ class TaskStub
39
+ attr_reader :invocation
40
+
41
+ def initialize(expect = false)
42
+ @calls = 0
43
+ @expect = expect
44
+ @expected_calls = 1
45
+ # invocation spec
46
+ @invocation = {}
47
+ # return value
48
+ @data = { default: {} }
49
+ end
50
+
51
+ def matches(targets, _task, arguments, options)
52
+ if @invocation[:targets] && Set.new(@invocation[:targets]) != Set.new(targets.map(&:name))
53
+ return false
54
+ end
55
+
56
+ if @invocation[:arguments] && arguments != @invocation[:arguments]
57
+ return false
58
+ end
59
+
60
+ if @invocation[:options] && options != @invocation[:options]
61
+ return false
62
+ end
63
+
64
+ true
65
+ end
66
+
67
+ def call(targets, task, arguments, options)
68
+ @calls += 1
69
+ if @return_block
70
+ # Merge arguments and options into params to match puppet function signature.
71
+ result_set = @return_block.call(targets: targets, task: task, params: arguments.merge(options))
72
+ unless result_set.is_a?(Bolt::ResultSet)
73
+ raise "Return block for #{task} did not return a Bolt::ResultSet"
74
+ end
75
+ result_set
76
+ else
77
+ results = targets.map do |target|
78
+ val = @data[target.name] || @data[:default]
79
+ Bolt::Result.new(target, value: val)
80
+ end
81
+ Bolt::ResultSet.new(results)
82
+ end
83
+ end
84
+
85
+ def assert_called(taskname)
86
+ satisfied = if @expect
87
+ (@expected_calls.nil? && @calls > 0) || @calls == @expected_calls
88
+ else
89
+ @expected_calls.nil? || @calls <= @expected_calls
90
+ end
91
+ unless satisfied
92
+ unless (times = @expected_calls)
93
+ times = @expect ? "at least one" : "any number of"
94
+ end
95
+ message = "Expected #{taskname} to be called #{times} times"
96
+ message += " with targets #{@invocation[:targets]}" if @invocation[:targets]
97
+ message += " with parameters #{@invocations[:parameters]}" if @invocation[:parameters]
98
+ raise message
99
+ end
100
+ end
101
+
102
+ # This changes the stub from an allow to an expect which will validate
103
+ # that it has been called.
104
+ def expect_call
105
+ @expect = true
106
+ self
107
+ end
108
+
109
+ # Below here are the intended 'public' methods of the stub
110
+
111
+ # Restricts the stub to only match invocations with
112
+ # the correct targets
113
+ def with_targets(targets)
114
+ targets = [targets] unless targets.is_a? Array
115
+ @invocation[:targets] = targets.map do |target|
116
+ if target.is_a? String
117
+ target
118
+ else
119
+ target.name
120
+ end
121
+ end
122
+ self
123
+ end
124
+
125
+ # Restricts the stub to only match invocations with certain parameters
126
+ # All parameters must match exactly and since arguments and options are
127
+ # treated differently at the executor this won't work with some '_*' options
128
+ # TODO: Fix handling of '_*' options probably by breaking them into other helpers
129
+ def with_params(params)
130
+ @invocation[:parameters] = params
131
+ @invocation[:arguments] = params.reject { |k, _v| k.start_with?('_') }
132
+ @invocation[:options] = params.select { |k, _v| k.start_with?('_') }
133
+ self
134
+ end
135
+
136
+ # limit the maximum number of times an allow stub may be called or
137
+ # specify how many times an expect stub must be called.
138
+ def be_called_times(times)
139
+ @expected_calls = times
140
+ self
141
+ end
142
+
143
+ # error if the stub is called at all.
144
+ def not_be_called
145
+ @expected_calls = 0
146
+ self
147
+ end
148
+
149
+ def return(&block)
150
+ raise "Cannot set return values and return block." if @data_set
151
+ @return_block = block
152
+ self
153
+ end
154
+
155
+ # Set different result values for each target
156
+ def return_for_targets(data)
157
+ data.each do |target, result|
158
+ raise "Mocked results must be hashes: #{target}: #{result}" unless result.is_a? Hash
159
+ end
160
+ raise "Cannot set return values and return block." if @return_block
161
+ @data = data
162
+ @data_set = true
163
+ self
164
+ end
165
+
166
+ # Set a default return value for all targets, specific targets may be overridden with return_for_targets
167
+ def always_return(default_data)
168
+ return_for_targets(default: default_data)
169
+ end
170
+
171
+ # Set a default error result for all targets.
172
+ def error_with(error_data)
173
+ always_return("_error" => error_data)
174
+ end
175
+ end
176
+
177
+ # Nothing on the executor is 'public'
178
+ class MockExecutor
179
+ attr_reader :noop, :error_message
180
+
181
+ def initialize
182
+ @noop = false
183
+ @task_doubles = {}
184
+ @allow_any_task = true
185
+ @error_message = nil
186
+ end
187
+
188
+ def run_task(targets, task, arguments, options)
189
+ result = nil
190
+ if (doub = @task_doubles[task.name] || @task_doubles[:default])
191
+ result = doub.process(targets, task.name, arguments, options)
192
+ end
193
+ unless result
194
+ targets = targets.map(&:name)
195
+ params = arguments.merge(options)
196
+ @error_message = "Unexpected call to 'run_task(#{task.name}, #{targets}, #{params})'"
197
+ raise UnexpectedInvocation, @error_message
198
+ end
199
+ result
200
+ end
201
+
202
+ def assert_call_expectations
203
+ @task_doubles.map do |taskname, doub|
204
+ doub.assert_called(taskname)
205
+ end
206
+ end
207
+
208
+ def stub_task(task_name)
209
+ @task_doubles[task_name] ||= TaskDouble.new
210
+ end
211
+
212
+ def report_function_call(_function); end
213
+
214
+ def report_bundled_content(_mode, _name); end
215
+ end
216
+ end
217
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.3
4
+ version: 0.21.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-07-17 00:00:00.000000000 Z
11
+ date: 2018-07-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0.2'
97
+ - !ruby/object:Gem::Dependency
98
+ name: r10k
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.6'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.6'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: terminal-table
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -297,7 +311,10 @@ files:
297
311
  - lib/bolt/analytics.rb
298
312
  - lib/bolt/applicator.rb
299
313
  - lib/bolt/bolt_option_parser.rb
314
+ - lib/bolt/boltdir.rb
300
315
  - lib/bolt/catalog.rb
316
+ - lib/bolt/catalog/compiler.rb
317
+ - lib/bolt/catalog/logging.rb
301
318
  - lib/bolt/cli.rb
302
319
  - lib/bolt/config.rb
303
320
  - lib/bolt/error.rb
@@ -317,6 +334,7 @@ files:
317
334
  - lib/bolt/puppetdb.rb
318
335
  - lib/bolt/puppetdb/client.rb
319
336
  - lib/bolt/puppetdb/config.rb
337
+ - lib/bolt/r10k_log_proxy.rb
320
338
  - lib/bolt/result.rb
321
339
  - lib/bolt/result_set.rb
322
340
  - lib/bolt/target.rb
@@ -330,9 +348,11 @@ files:
330
348
  - lib/bolt/transport/winrm.rb
331
349
  - lib/bolt/transport/winrm/connection.rb
332
350
  - lib/bolt/util.rb
333
- - lib/bolt/util/on_access.rb
351
+ - lib/bolt/util/puppet_log_level.rb
334
352
  - lib/bolt/version.rb
335
353
  - lib/bolt_ext/puppetdb_inventory.rb
354
+ - lib/bolt_spec/plans.rb
355
+ - lib/bolt_spec/plans/mock_executor.rb
336
356
  - libexec/apply_catalog.rb
337
357
  - libexec/bolt_catalog
338
358
  - modules/aggregate/lib/puppet/functions/aggregate/count.rb