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.
- checksums.yaml +4 -4
- data/exe/bolt-inventory-pdb +8 -2
- data/lib/bolt/applicator.rb +89 -11
- data/lib/bolt/bolt_option_parser.rb +18 -3
- data/lib/bolt/boltdir.rb +42 -0
- data/lib/bolt/catalog.rb +7 -52
- data/lib/bolt/catalog/compiler.rb +48 -0
- data/lib/bolt/catalog/logging.rb +15 -0
- data/lib/bolt/cli.rb +146 -85
- data/lib/bolt/config.rb +121 -156
- data/lib/bolt/error.rb +25 -2
- data/lib/bolt/executor.rb +3 -4
- data/lib/bolt/inventory.rb +3 -3
- data/lib/bolt/logger.rb +3 -3
- data/lib/bolt/outputter/human.rb +10 -0
- data/lib/bolt/outputter/json.rb +6 -0
- data/lib/bolt/pal.rb +10 -11
- data/lib/bolt/pal/logging.rb +3 -14
- data/lib/bolt/puppetdb/client.rb +7 -27
- data/lib/bolt/puppetdb/config.rb +50 -23
- data/lib/bolt/r10k_log_proxy.rb +30 -0
- data/lib/bolt/util.rb +2 -2
- data/lib/bolt/util/puppet_log_level.rb +20 -0
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_ext/puppetdb_inventory.rb +3 -9
- data/lib/bolt_spec/plans.rb +174 -0
- data/lib/bolt_spec/plans/mock_executor.rb +217 -0
- metadata +23 -3
- data/lib/bolt/util/on_access.rb +0 -26
@@ -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.
|
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-
|
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/
|
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
|