cem_acpt 0.1.0 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -21,6 +21,7 @@ module CemAcpt
21
21
  # @return [String] Path to the built package.
22
22
  def self.build_module_package(module_dir, target_dir = nil, should_log: false)
23
23
  require 'puppet/modulebuilder'
24
+ require 'fileutils'
24
25
 
25
26
  builder_logger = should_log ? logger : nil
26
27
  builder = Puppet::Modulebuilder::Builder.new(File.expand_path(module_dir), target_dir, builder_logger)
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ require 'open3'
5
+ require 'pty'
6
+ require 'shellwords'
7
+ require_relative 'logging'
8
+
9
+ module CemAcpt
10
+ # Module that provides methods and objects for running and managing RSpec
11
+ module RSpecUtils
12
+ class BundlerNotFoundError < StandardError; end
13
+ class RSpecNotFoundError < StandardError; end
14
+
15
+ # Holds and formats a RSpec command
16
+ class Command
17
+ include CemAcpt::LoggingAsync
18
+ attr_reader :debug, :format, :test_path, :use_bundler, :pty_pid
19
+
20
+ # @param opts [Hash] options hash for the RSpec command
21
+ # @option opts [String] :test_path The path (or glob path) to the test file(s) to run. If blank, runs all.
22
+ # @option opts [Hash] :format Format options for rspec where the key is the format (documentation, json, etc)
23
+ # and the value is the out-file path. If you do not want to save the results of a format to a file, the
24
+ # value should be `nil`.
25
+ # @option opts [Boolean] :debug True if RSpec should run in debug mode, false if not. Mutually exclusive with
26
+ # `:quiet`. Default is `false`.
27
+ # @option opts [Boolean] :quiet True if no output should be logged from RSpec command, false if output should
28
+ # be logged. Mutually exclusive with `:debug`. Default is `false`.
29
+ # @option opts [Boolean] :use_bundler Whether or not `bundle exec` should be used to run the RSpec command.
30
+ # Default is `true`.
31
+ # @option opts [Boolean] :bundle_install Whether or not to run `bundle install` before the RSpec command
32
+ # if `use_bundler` is `true`.
33
+ # @option opts [Boolean] :use_shell Whether or not to add `$SHELL` as a prefix to the command
34
+ # @option opts [Hash] :env Environment variables to prepend to the command
35
+ def initialize(opts = {})
36
+ @test_path = opts[:test_path]&.shellescape
37
+ @format = opts.fetch(:format, {})
38
+ @debug = opts.fetch(:debug, false)
39
+ @quiet = @debug ? false : opts.fetch(:quiet, false)
40
+ @use_bundler = opts.fetch(:use_bundler, false)
41
+ @bundle_install = opts.fetch(:bundle_install, false)
42
+ @env = opts.fetch(:env, {})
43
+ @pty_pid = nil
44
+ validate_and_set_bin_paths(opts)
45
+ end
46
+
47
+ # Sets debug mode to `true`
48
+ def set_debug
49
+ @debug = true
50
+ if @quiet
51
+ async_debug('Setting :quiet to false because :debug is now true.')
52
+ @quiet = false
53
+ end
54
+ end
55
+
56
+ # Sets debug mode to `false`
57
+ def unset_debug
58
+ @debug = false
59
+ end
60
+
61
+ def quiet
62
+ @quiet && !debug
63
+ end
64
+
65
+ # Adds a new format to the RSpec command
66
+ # @param fmt [String] The name of the format (i.e. "documentation", "json", etc.)
67
+ # @param out [String] If specified, saves the specified format to a file at this path
68
+ def with_format(fmt, out: nil)
69
+ @format[fmt.to_sym] = out
70
+ end
71
+
72
+ # Environment variables that will be used for the RSpec command
73
+ # @return [Hash] A Hash of environment variables with each key pair being: <var name> => <var value>
74
+ def env
75
+ @debug ? @env.merge({ 'RSPEC_DEBUG' => 'true' }) : @env
76
+ end
77
+
78
+ # Returns an array representation of the RSpec command
79
+ def to_a
80
+ cmd = cmd_base.dup
81
+ cmd << test_path if test_path
82
+ format.each do |fmt, out|
83
+ cmd += ['--format', fmt.to_s.shellescape]
84
+ cmd += ['--out', out.to_s.shellescape] if out
85
+ end
86
+ cmd.compact
87
+ end
88
+
89
+ # Returns a string representation of the RSpec command
90
+ def to_s
91
+ to_a.join(' ')
92
+ end
93
+
94
+ # Executes the RSpec command on the current machine
95
+ # @param pty [Boolean] If true, execute command in a PTY. If false, execute command directly.
96
+ # @param log_prefix [String] A prefix to add to log messages generated while the command is running.
97
+ def execute(pty: true, log_prefix: 'RSPEC')
98
+ if pty
99
+ execute_pty(log_prefix: log_prefix)
100
+ else
101
+ execute_no_pty(log_prefix: log_prefix)
102
+ end
103
+ end
104
+
105
+ # Executes the RSpec command in a psuedo-terminal (PTY). First, it spawns a process
106
+ # for $SHELL, sets environment variables `export_envs`, then calls the current RSpec
107
+ # command in the shell and exits with the last exit code `$?`. Output is read from the
108
+ # RSpec command in near real-time in a blocking manner unless the `:quiet` option has
109
+ # been specified.
110
+ # @param log_prefix [String] A prefix to add to the log messages that are output from
111
+ # the RSpec command.
112
+ # @return [Integer] The exit code of the RSpec command
113
+ def execute_pty(log_prefix: 'RSPEC')
114
+ async_debug("Executing RSpec command '#{self}' in PTY...", log_prefix)
115
+ PTY.spawn(env, ENV['SHELL']) do |r, w, pid|
116
+ @pty_pid = pid
117
+ async_debug("Spawned RSpec PTY with PID #{@pty_pid}", log_prefix)
118
+ export_envs(w)
119
+ w.puts "#{self}; exit $?"
120
+ quiet ? wait_io(r) : read_io(r, log_prefix: log_prefix)
121
+ end
122
+ $CHILD_STATUS
123
+ end
124
+
125
+ # Executes the RSpec command using Open3.popen2e(). The output stream, which is both
126
+ # stderr and stdout, is read in real-time in a non-blocking manner.
127
+ # @param log_prefix [String] A prefix to add to the log messages that are output from
128
+ # the RSpec command.
129
+ # @return [Integer] The exit code of the RSpec command
130
+ def execute_no_pty(log_prefix: 'RSPEC')
131
+ async_info("Executing RSpec command '#{self}' with Open3.popen2e()...", log_prefix)
132
+ exit_status = nil
133
+ Open3.popen2e(env, to_s) do |stdin, std_out_err, wait_thr|
134
+ stdin.close
135
+ quiet ? wait_io(std_out_err) : read_io(std_out_err, log_prefix: log_prefix)
136
+ exit_status = wait_thr.value
137
+ end
138
+ exit_status
139
+ end
140
+
141
+ # Kills the PTY process with `SIGKILL` if the process exists
142
+ def kill_pty
143
+ Process.kill('KILL', @pty_pid) unless @pty_pid.nil?
144
+ rescue Errno::ESRCH
145
+ true
146
+ end
147
+
148
+ private
149
+
150
+ # Detects if the current Ruby context is JRuby
151
+ def jruby?
152
+ File.basename(RbConfig.ruby) == 'jruby'
153
+ end
154
+
155
+ # The base RSpec command
156
+ def cmd_base
157
+ use_bundler ? cmd_base_bundler : cmd_base_rspec
158
+ end
159
+
160
+ # The base RSpec command if `:use_bundler` is `true`.
161
+ def cmd_base_bundler
162
+ base = [@bundle, 'exec', 'rspec']
163
+ base.unshift("#{@bundle} install;") if @bundle_install
164
+ base
165
+ end
166
+
167
+ # The base RSpec command if `:use_bundler` is `false`
168
+ def cmd_base_rspec
169
+ [@rspec]
170
+ end
171
+
172
+ # Puts export statements for each key-value pair in `env` to the given writer.
173
+ # Writer is the write pipe of a PTY session, or a similar IO object that can
174
+ # pass the statements to a shell.
175
+ # @param writer [IO] An IO object that supprts `puts` and can send statements to a shell
176
+ def export_envs(writer)
177
+ env.each do |key, val|
178
+ writer.puts "export #{key}=#{val}"
179
+ end
180
+ end
181
+
182
+ # Finds and sets the paths to the `bundle` and `rspec` binaries. The paths can
183
+ # be either passed in as options in the `opts` Hash or interrogated from the
184
+ # system.
185
+ # @param opts [Hash] The options hash
186
+ # @option opts [String] :bundle An absolute path on the system to the `bundle` binary.
187
+ # @option opts [String] :rspec An absolute path on the system to the `rspec` binary.
188
+ # @raise [CemAcpt::RSpecUtils::BundlerNotFoundError] if `@use_bundler` is true and
189
+ # `bundle` binary is not found.
190
+ # @raise [CemAcpt::RSpecUtils::RSpecNotFoundError] if `rspec` binary is not found.
191
+ def validate_and_set_bin_paths(opts = {})
192
+ %i[bundle rspec].each do |bin|
193
+ bin_path = opts[bin] || `command -v #{bin}`.strip
194
+ bin_not_found(bin, bin_path) unless bin_path && File.exist?(bin_path)
195
+ instance_variable_set("@#{bin}", bin_path)
196
+ end
197
+ end
198
+
199
+ # Handles binary paths which are not found
200
+ # @param bin [Symbol] The binary that was not found, either :bundle or :rspec.
201
+ # @param bin_path [String] The path to the binary that was checked.
202
+ # @raise [CemAcpt::RSpecUtils::BundlerNotFoundError] if `@use_bundler` is true and
203
+ # `bundle` binary is not found.
204
+ # @raise [CemAcpt::RSpecUtils::RSpecNotFoundError] if `rspec` binary is not found.
205
+ # @raise [RuntimeError] if `bin` is not :bundle or :rspec.
206
+ def bin_not_found(bin, bin_path)
207
+ msg_base = "#{bin} not found."
208
+ msg = bin_path.nil? ? "#{msg_base} Path is nil." : "#{msg_base} Path: #{bin_path}"
209
+ case bin
210
+ when :bundle
211
+ raise BundlerNotFoundError, msg if @use_bundler
212
+ when :rspec
213
+ raise RSpecNotFoundError, msg
214
+ else
215
+ raise "bin #{bin} not recognized!"
216
+ end
217
+ end
218
+
219
+ # Blocking wait on an IO stream. Wait stops once the IO stream has reached
220
+ # end of file.
221
+ # @param stdout [IO] An IO stream with output that can be read from.
222
+ def wait_io(stdout)
223
+ sleep(0.01) until stdout.eof?
224
+ end
225
+
226
+ # Reads and logs data from `stdout` in a near real-time, blocking manner.
227
+ # @param stdout [IO] An IO stream with output that can be read from.
228
+ # @param log_prefix [String] A string prefix that is added to log messages.
229
+ def read_io(stdout, log_prefix: 'RSPEC')
230
+ loop do
231
+ chunk = stdout.read_nonblock(4096).strip
232
+ async_info(chunk, log_prefix) unless chunk.nil? || chunk.empty?
233
+ rescue IO::WaitReadable
234
+ IO.select([stdout])
235
+ retry
236
+ rescue EOFError
237
+ break
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
@@ -3,7 +3,6 @@
3
3
  require 'concurrent-ruby'
4
4
  require 'deep_merge'
5
5
  require 'json'
6
- require 'ostruct'
7
6
  require 'yaml'
8
7
  require_relative 'core_extensions'
9
8
  require_relative 'logging'
@@ -49,7 +48,16 @@ module CemAcpt
49
48
  attr_reader :config_file
50
49
 
51
50
  def initialize
52
- @opts = {}
51
+ @opts = {
52
+ thread_pool: {
53
+ min_threads: [2, Concurrent.processor_count - 1].max,
54
+ max_threads: [2, Concurrent.processor_count - 1].max,
55
+ max_queue: [2, Concurrent.processor_count - 1].max * 10,
56
+ fallback_policy: :caller_runs,
57
+ idletime: 1200,
58
+ auto_terminate: true,
59
+ },
60
+ }
53
61
  end
54
62
 
55
63
  # Returns the value of the dot-separated key.
@@ -67,18 +75,25 @@ module CemAcpt
67
75
  !!get(dot_key)
68
76
  end
69
77
 
78
+ # Checks to see if cem_acpt is set to run in debug mode (log level is set to debug)
79
+ # @return [TrueClass] If cem_acpt is running in debug mode
80
+ # @return [FalseClass] If cem_acpt is not runnint in debug mode
81
+ def debug_mode?
82
+ @opts.dot_dig('log_level') == 'debug'
83
+ end
84
+
70
85
  # Loads the config from specified file path. Config files must be in YAML
71
86
  # format. If 'opts' is specified, it will be merged with the config file.
72
87
  # Values in 'opts' will override values in the config file.
73
88
  # @param opts [Hash] Options to be merged with the config file
74
89
  # @param config_file [String] Path to the config file
75
- def load(opts: {}, config_file: '~/.cem_acpt.yaml')
90
+ def load(opts: {}, config_file: './cem_acpt_config.yaml')
76
91
  raise ConfigImmutableError, 'Config is immutable, cannot load more than once' if frozen?
77
92
  raise ArgumentError, 'opts must be a Hash' unless opts.is_a?(Hash)
78
93
  raise ArgumentError, 'config_file must be a String' unless config_file.is_a?(String)
79
94
 
80
95
  @config_file = File.expand_path(config_file)
81
- @opts = load_opts_from_file(File.expand_path(config_file)).deep_merge(opts)
96
+ @opts.deep_merge!(load_opts_from_file(File.expand_path(config_file)).deep_merge!(opts))
82
97
  @opts.format!
83
98
  @opts.freeze
84
99
  freeze
@@ -155,8 +170,13 @@ module CemAcpt
155
170
  end
156
171
  end
157
172
 
173
+ class NodeInventoryFileNotFoundError < StandardError; end
174
+ class NodeInventoryFileLoadError < StandardError; end
158
175
  class NodeClaimedError < StandardError; end
159
176
  class NodeDoesNotExistError < StandardError; end
177
+ class PropertyNotFoundError < StandardError; end
178
+ class LockWaitTimeoutError < StandardError; end
179
+ class LockNotRecognizedError < StandardError; end
160
180
 
161
181
  # Provides a thread-safe inventory of test nodes.
162
182
  class NodeInventory
@@ -169,7 +189,8 @@ module CemAcpt
169
189
  @lock = Concurrent::ReadWriteLock.new
170
190
  @claimed = Concurrent::Set.new
171
191
  @save_on_claim = false
172
- @save_file_path = 'spec/fixtures/node_inventory.yaml'
192
+ @save_file_path = 'spec/fixtures/node_inventory'
193
+ @loaded_node_inv = nil
173
194
  end
174
195
 
175
196
  # When called, enables saving the inventory to a file on claim.
@@ -189,12 +210,51 @@ module CemAcpt
189
210
  @inventory.put_if_absent(node_name, node_data)
190
211
  end
191
212
 
213
+ def update(node_name, node_data)
214
+ @inventory.replace_if_exists(node_name, node_data)
215
+ end
216
+
192
217
  # Returns the node data for a given node.
193
218
  # @param node_name [String] The name of the node to get data for.
194
219
  def get(node_name)
195
220
  @inventory[node_name]
196
221
  end
197
222
 
223
+ # Returns the node_name and node_data for a given node
224
+ # that has a matching value for the given property.
225
+ # @param property_path [String] The dot-separated property path to check.
226
+ # @param value [Object] The value to check for.
227
+ # @return [Array] Two-item frozen array of node name and node data.
228
+ # @raise [NodeDoesNotExistError] If no nodes are found with the property equal to the value.
229
+ def get_by_property(property_path, value)
230
+ found = []
231
+ @inventory.each_pair do |node_name, node_data|
232
+ next unless node_data.dot_dig(property_path) == value
233
+
234
+ found = [node_name, node_data].freeze
235
+ end
236
+ raise NodeDoesNotExistError, "No node found with property #{property_path} == #{value}" if found.empty?
237
+
238
+ found
239
+ end
240
+
241
+ # Returns all property values of all nodes for the given property path
242
+ # @param property_path [String] The dot-separated property path to check.
243
+ # @return [Array] A frozen array of found property values.
244
+ # @raise [PropertyNotFoundError] If no values are found on any nodes for the property path.
245
+ def get_all_properties(property_path)
246
+ found = []
247
+ @inventory.each_pair do |_, node_data|
248
+ prop = node_data.dot_dig(property_path)
249
+ next unless prop
250
+
251
+ found << prop
252
+ end
253
+ raise PropertyNotFoundError, "No property with path #{property_path} found in nodes" if found.empty?
254
+
255
+ found.freeze
256
+ end
257
+
198
258
  # Sets a specific property on a node in the inventory.
199
259
  # @param node_name [String] The name of the node to set the property on.
200
260
  # @param property_path [String] The dot-separated property path to set.
@@ -230,7 +290,7 @@ module CemAcpt
230
290
  # @raise [NodeDoesNotExistError] If the node does not exist in the inventory.
231
291
  # @raise [NodeClaimedError] If the node is already claimed.
232
292
  def claim(node_name)
233
- @lock.with_write_lock do
293
+ with_lock_retry(:write) do
234
294
  unless @inventory.keys.include?(node_name)
235
295
  raise NodeDoesNotExistError, "Node #{node_name} does not exist in inventory"
236
296
  end
@@ -254,16 +314,24 @@ module CemAcpt
254
314
  # @return [String] The name of the node that was claimed.
255
315
  # @raise [NodeDoesNotExistError] If no valid node is found.
256
316
  def claim_by_property(property_path, value)
257
- claimed_set = @claimed.dup
317
+ attempts ||= 1
258
318
  claim_name = nil
259
319
  @inventory.each_pair do |node_name, node_data|
260
- next if claimed_set.include?(node_name)
320
+ next if @claimed.include?(node_name)
261
321
 
262
322
  claim_name = node_name if node_data.dot_dig(property_path) == value
263
323
  end
264
- return claim(claim_name) unless claim_name.nil?
265
-
266
- raise NodeDoesNotExistError, "No node found with property #{property_path} == #{value}"
324
+ raise NodeDoesNotExistError, "No node found with property #{property_path} == #{value}" if claim_name.nil?
325
+
326
+ claim(claim_name)
327
+ rescue NodeDoesNotExistError => e
328
+ # We sleep than retry three times to help mitigate race conditions
329
+ if (attempts += 1) <= 3
330
+ sleep(1)
331
+ retry
332
+ else
333
+ raise e
334
+ end
267
335
  end
268
336
 
269
337
  # Deletes a node from the inventory.
@@ -286,7 +354,7 @@ module CemAcpt
286
354
 
287
355
  # Clears the inventory and removes claimed nodes. Thread-safe.
288
356
  def clear!
289
- @lock.with_write_lock do
357
+ with_lock_retry(:write) do
290
358
  clear_no_lock!
291
359
  end
292
360
  end
@@ -308,6 +376,16 @@ module CemAcpt
308
376
  h
309
377
  end
310
378
 
379
+ # Returns a YAML string of a specific node's node data
380
+ def node_to_yaml(node_name)
381
+ get(node_name).to_h.to_yaml
382
+ end
383
+
384
+ # Returns a JSON string of a specific node's node data
385
+ def node_to_json(node_name, *args)
386
+ get(node_name).to_h.to_json(*args)
387
+ end
388
+
311
389
  # Returns a YAML string of the inventory.
312
390
  def to_yaml
313
391
  to_h.to_yaml
@@ -321,15 +399,21 @@ module CemAcpt
321
399
  # Saves the current node inventory to a file. DOES NOT USE LOCK.
322
400
  # If this is called outside of a lock, it will not be thread-safe.
323
401
  def save_no_lock!(file_path = @save_file_path)
324
- File.open(file_path, 'w') do |file|
325
- file.write(to_yaml)
402
+ Dir.mkdir(file_path) unless Dir.exist?(file_path)
403
+ @inventory.each_pair do |node_name, node_data|
404
+ path = File.join(file_path, "#{node_name}.yaml")
405
+ next if File.file?(path)
406
+
407
+ File.open(path, 'w') do |f|
408
+ f.write(node_data.to_h.to_yaml)
409
+ end
326
410
  end
327
411
  end
328
412
 
329
413
  # Saves the current node inventory to a yaml file. Thread-safe.
330
414
  # @param file_path [String] The path to the file to save to.
331
415
  def save(file_path = @save_file_path)
332
- @lock.with_write_lock do
416
+ with_lock_retry(:write) do
333
417
  save_no_lock!(file_path)
334
418
  end
335
419
  end
@@ -338,23 +422,58 @@ module CemAcpt
338
422
  # If this is called outside of a lock, it will not be thread-safe.
339
423
  def load_no_lock!(file_path = @save_file_path)
340
424
  require 'net/ssh/proxy/command' # If ProxyCommand is used in ssh options, this is required.
341
- clear_no_lock!
342
- YAML.load_file(file_path).each_pair do |node_name, node_data|
343
- if node_name == :claimed
344
- @claimed = Set.new(node_data)
345
- else
346
- add(node_name, node_data)
347
- end
425
+ attempts ||= 1
426
+ Dir.glob('*.yaml', base: file_path) do |file_name|
427
+ node_name = File.basename(file_name, '.yaml')
428
+ node_data = YAML.load_file(File.join(file_path, file_name))
429
+ add(node_name, node_data) || update(node_name, node_data)
348
430
  end
431
+ rescue StandardError => e
432
+ raise e unless (attempts += 1) <= 3
433
+
434
+ sleep(1)
435
+ retry
349
436
  end
350
437
 
438
+ def update_no_lock!; end
439
+
351
440
  # Loads a node inventory from a yaml file. Thread-safe.
352
441
  # @param file_path [String] The path to the file to load from.
353
442
  def load(file_path = @save_file_path)
354
- @lock.with_write_lock do
443
+ with_lock_retry(:write) do
355
444
  load_no_lock!(file_path)
356
445
  end
357
446
  end
447
+
448
+ def clean_local_files(file_path = @save_file_path)
449
+ Dir.glob('*.yaml', base: file_path) do |file_name|
450
+ File.delete(File.expand_path(File.join(file_path, file_name)))
451
+ end
452
+ end
453
+
454
+ private
455
+
456
+ def with_lock_retry(lock_type, max_attempts: 3)
457
+ raise 'No block given' unless block_given?
458
+
459
+ attempts ||= 1
460
+ case lock_type
461
+ when :read
462
+ @lock.with_read_lock { yield }
463
+ when :write
464
+ @lock.with_write_lock { yield }
465
+ else
466
+ raise LockNotRecognizedError, "Lock type #{lock_type} not recognized! Must be :read or :write!"
467
+ end
468
+ rescue Concurrent::ResourceLimitError => e
469
+ if (attempts += 1) <= max_attempts
470
+ sleep(1)
471
+ logger.debug("Failed to acquire lock with error #{e.message}. Retrying...")
472
+ retry
473
+ else
474
+ raise LockWaitTimeoutError, 'Lock could not be aquired'
475
+ end
476
+ end
358
477
  end
359
478
 
360
479
  class LocalPortAllocationError < StandardError; end
@@ -391,9 +510,11 @@ module CemAcpt
391
510
 
392
511
  ports << port
393
512
  end
394
- return ports[0] if ports.length == 1
395
-
396
- ports
513
+ if ports.length == 1
514
+ ports[0]
515
+ else
516
+ ports
517
+ end
397
518
  end
398
519
 
399
520
  private
@@ -14,15 +14,12 @@ module CemAcpt
14
14
  config.extend CemAcpt::SpecHelperAcceptance
15
15
  config.add_setting :acpt_test_data, default: {}
16
16
  config.add_setting :acpt_node_inventory
17
+ config.threadsafe = true
18
+ config.color_mode = :off
19
+ config.fail_fast = false
17
20
  end
18
21
 
19
- return unless RSpec.configuration.acpt_node_inventory.nil?
20
-
21
- raise 'Node inventory file not found!' unless File.exist?('spec/fixtures/node_inventory.yaml')
22
-
23
22
  node_inventory = NodeInventory.new
24
- node_inventory.save_on_claim
25
- node_inventory.save_file_path = 'spec/fixtures/node_inventory.yaml'
26
23
  node_inventory.load
27
24
  RSpec.configuration.acpt_node_inventory = node_inventory
28
25
  end
@@ -35,8 +32,10 @@ module CemAcpt
35
32
  def initialize_test_environment!
36
33
  test_file = caller_locations.first.path
37
34
 
38
- node_name = RSpec.configuration.acpt_node_inventory.claim_by_property('test_data.test_file', test_file)
39
- node_data = RSpec.configuration.acpt_node_inventory.get(node_name)
35
+ node_name, node_data = RSpec.configuration.acpt_node_inventory.get_by_property('test_data.test_file', test_file)
36
+ raise "Failed to get node data for node #{node_name}" unless node_data
37
+ raise "Node data format is incorrect: #{node_data}" unless node_data.is_a?(Hash)
38
+
40
39
  backend = nil
41
40
  host = nil
42
41
  ssh_options = nil
@@ -81,13 +80,14 @@ module CemAcpt
81
80
  RSpec.configuration.acpt_test_data = acpt_test_data
82
81
  end
83
82
 
84
- # This method formats Puppet Apply options
83
+ # This method formats Puppet Apply options
85
84
  def puppet_apply_options(opts = {})
86
85
  if [opts[:catch_changes], opts[:expect_changes], opts[:catch_failures], opts[:expect_failures]].compact.length > 1
87
- raise ArgumentError, 'Please specify only one of "catch_changes", "expect_changes", "catch_failures", or "expect_failures"'
86
+ raise ArgumentError,
87
+ 'Please specify only one of "catch_changes", "expect_changes", "catch_failures", or "expect_failures"'
88
88
  end
89
89
 
90
- apply_opts = { trace: true }.merge(opts)
90
+ apply_opts = {}.merge(opts)
91
91
 
92
92
  if opts[:catch_changes]
93
93
  apply_opts[:detailed_exit_codes] = true
@@ -169,8 +169,16 @@ module CemAcpt
169
169
  # asserts that the second run has no changes.
170
170
  def idempotent_apply(manifest, opts = {})
171
171
  opts.reject! { |k, _| %i[catch_changes expect_changes catch_failures expect_failures].include?(k) }
172
- apply_manifest(manifest, opts.merge(catch_failures: true))
173
- apply_manifest(manifest, opts.merge(catch_changes: true))
172
+ begin
173
+ apply_manifest(manifest, opts.merge({ catch_failures: true }))
174
+ rescue StandardError => e
175
+ raise "Idempotent apply failed during first apply: #{e.message}\n#{e.backtrace.join("\n")}"
176
+ end
177
+ begin
178
+ apply_manifest(manifest, opts.merge({ catch_changes: true }))
179
+ rescue StandardError => e
180
+ raise "Idempotent apply failed during second apply: #{e.message}\n#{e.backtrace.join("\n")}"
181
+ end
174
182
  end
175
183
  end
176
184
  end