cem_acpt 0.8.8 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/spec.yml +0 -3
  3. data/Gemfile.lock +9 -1
  4. data/README.md +95 -13
  5. data/cem_acpt.gemspec +2 -1
  6. data/lib/cem_acpt/action_result.rb +8 -2
  7. data/lib/cem_acpt/actions.rb +153 -0
  8. data/lib/cem_acpt/bolt/cmd/base.rb +174 -0
  9. data/lib/cem_acpt/bolt/cmd/output.rb +315 -0
  10. data/lib/cem_acpt/bolt/cmd/task.rb +59 -0
  11. data/lib/cem_acpt/bolt/cmd.rb +22 -0
  12. data/lib/cem_acpt/bolt/errors.rb +49 -0
  13. data/lib/cem_acpt/bolt/helpers.rb +52 -0
  14. data/lib/cem_acpt/bolt/inventory.rb +62 -0
  15. data/lib/cem_acpt/bolt/project.rb +38 -0
  16. data/lib/cem_acpt/bolt/summary_results.rb +96 -0
  17. data/lib/cem_acpt/bolt/tasks.rb +181 -0
  18. data/lib/cem_acpt/bolt/tests.rb +415 -0
  19. data/lib/cem_acpt/bolt/yaml_file.rb +74 -0
  20. data/lib/cem_acpt/bolt.rb +142 -0
  21. data/lib/cem_acpt/cli.rb +6 -0
  22. data/lib/cem_acpt/config/base.rb +4 -0
  23. data/lib/cem_acpt/config/cem_acpt.rb +7 -1
  24. data/lib/cem_acpt/core_ext.rb +25 -0
  25. data/lib/cem_acpt/goss/api/action_response.rb +4 -0
  26. data/lib/cem_acpt/goss/api.rb +23 -25
  27. data/lib/cem_acpt/image_builder/provision_commands.rb +43 -0
  28. data/lib/cem_acpt/logging/formatter.rb +3 -3
  29. data/lib/cem_acpt/logging.rb +17 -1
  30. data/lib/cem_acpt/provision/terraform/linux.rb +2 -2
  31. data/lib/cem_acpt/test_data.rb +2 -0
  32. data/lib/cem_acpt/test_runner/log_formatter/base.rb +73 -0
  33. data/lib/cem_acpt/test_runner/log_formatter/bolt_error_formatter.rb +65 -0
  34. data/lib/cem_acpt/test_runner/log_formatter/bolt_output_formatter.rb +54 -0
  35. data/lib/cem_acpt/test_runner/log_formatter/bolt_summary_results_formatter.rb +64 -0
  36. data/lib/cem_acpt/test_runner/log_formatter/goss_action_response.rb +17 -30
  37. data/lib/cem_acpt/test_runner/log_formatter/goss_error_formatter.rb +31 -0
  38. data/lib/cem_acpt/test_runner/log_formatter/standard_error_formatter.rb +35 -0
  39. data/lib/cem_acpt/test_runner/log_formatter.rb +17 -5
  40. data/lib/cem_acpt/test_runner/test_results.rb +150 -0
  41. data/lib/cem_acpt/test_runner.rb +153 -53
  42. data/lib/cem_acpt/utils/files.rb +189 -0
  43. data/lib/cem_acpt/utils/finalizer_queue.rb +73 -0
  44. data/lib/cem_acpt/utils/shell.rb +13 -4
  45. data/lib/cem_acpt/version.rb +1 -1
  46. data/sample_config.yaml +13 -0
  47. metadata +41 -5
  48. data/lib/cem_acpt/test_runner/log_formatter/error_formatter.rb +0 -33
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'cmd'
5
+ require_relative 'errors'
6
+ require_relative '../core_ext'
7
+ require_relative '../logging'
8
+ require_relative '../utils/shell'
9
+
10
+ module CemAcpt
11
+ module Bolt
12
+ using CemAcpt::CoreExt::ExtendedArray
13
+
14
+ # Provides a wrapper for Bolt tasks that holds data and provides methods for interacting with them
15
+ # This should probably be genericized as ObjectWrapper to account for Bolt plans, as well
16
+ class TaskWrapper
17
+ include CemAcpt::Logging
18
+ attr_reader :full_name, :name, :description, :module_name, :last_cmd_executed
19
+
20
+ def initialize(config, task_array, project: nil, inventory: nil)
21
+ @config = config
22
+ @full_name = task_array[0]
23
+ @description = task_array[1]
24
+ @module_name = @full_name.split('::').first
25
+ @name = @full_name.split('::').last
26
+ @show_cmd = CemAcpt::Bolt::Cmd::TaskShow.new(@config, full_name, project: project)
27
+ @run_cmd = CemAcpt::Bolt::Cmd::TaskRun.new(@config, full_name, project: project, inventory: inventory)
28
+ @last_cmd_executed = nil
29
+ end
30
+
31
+ def inspect
32
+ "#<#{self.class}:\"#{full_name}\">"
33
+ end
34
+
35
+ def show(**params)
36
+ logger.verbose('CemAcpt::Bolt::TaskWrapper') { "Executing task show with params: #{params}" }
37
+ @last_cmd_executed = @show_cmd
38
+ show_cmd.run(**params)
39
+ end
40
+
41
+ def run(**params)
42
+ logger.verbose('CemAcpt::Bolt::TaskWrapper') { "Executing task run with params: #{params}" }
43
+ @last_cmd_executed = @run_cmd
44
+ run_cmd.run(**params)
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :show_cmd, :run_cmd
50
+ end
51
+
52
+ # Holds and manages the list of tasks in the current bolt project
53
+ class TaskList
54
+ include CemAcpt::Logging
55
+ include CemAcpt::Bolt::Helpers
56
+
57
+ attr_reader :metadata
58
+
59
+ def initialize(config, inventory = nil, project = nil)
60
+ @config = config
61
+ @inventory = inventory
62
+ @project = project
63
+ @tasks = []
64
+ @metadata = {}
65
+ end
66
+
67
+ def unfiltered
68
+ find_all_tasks
69
+ end
70
+
71
+ def tasks
72
+ return @tasks unless @tasks.nil? || @tasks.empty?
73
+
74
+ @tasks = filter_tasks(unfiltered)
75
+ @tasks
76
+ end
77
+
78
+ def module_pattern
79
+ @module_pattern ||= get_pattern('bolt.tasks.module_pattern')
80
+ end
81
+
82
+ def name_filter
83
+ @name_pattern ||= get_pattern('bolt.tasks.name_filter', default: %r{^$})
84
+ end
85
+
86
+ def ignore
87
+ @ignore ||= @config.get('bolt.tasks.ignore') || []
88
+ end
89
+
90
+ def only
91
+ @only ||= @config.get('bolt.tasks.only') || []
92
+ end
93
+
94
+ def method_missing(method, *args, **kwargs, &block)
95
+ if tasks.respond_to?(method)
96
+ tasks.send(method, *args, **kwargs, &block)
97
+ else
98
+ super
99
+ end
100
+ end
101
+
102
+ def respond_to_missing?(method, include_private = false)
103
+ tasks.respond_to?(method, include_private) || super
104
+ end
105
+
106
+ def inspect
107
+ tasks.inspect
108
+ end
109
+
110
+ private
111
+
112
+ def get_pattern(config_key, default: %r{.*})
113
+ pat = @config.get(config_key)
114
+ return default unless pat
115
+
116
+ Regexp.new(pat)
117
+ end
118
+
119
+ def find_all_tasks
120
+ logger.debug('CemAcpt::Bolt::TaskList') { 'Finding all Bolt tasks...' }
121
+ out = CemAcpt::Bolt::Cmd::TaskShow.new(@config, project: @project).run(strict: false)
122
+ if out.error?
123
+ logger.error('CemAcpt::Bolt::TaskList') { "Error finding Bolt tasks:\n#{out}" }
124
+ logger.debug('CemAcpt::Bolt::TaskList') { "Backtrace:\n#{out.error.backtrace.join("\n")}" } if out.error.respond_to?(:backtrace)
125
+ raise out.error if out.error.is_a?(StandardError)
126
+ raise CemAcpt::Bolt::BoltActionError.new("Error finding Bolt tasks: #{out.error.msg}",
127
+ out.error,
128
+ out.error.kind,
129
+ out.error.details)
130
+
131
+ end
132
+ out.to_h['tasks'].map { |task_array| TaskWrapper.new(@config, task_array) }
133
+ end
134
+
135
+ def filter_tasks(td_array)
136
+ @metadata[:filter] = {}
137
+ td_array.select do |t|
138
+ @metadata[:filter][t.full_name] = []
139
+ unless module_pattern.match?(t.module_name)
140
+ @metadata[:filter][t.full_name] << {
141
+ rule: 'module_pattern',
142
+ value: t.module_name,
143
+ pattern: module_pattern,
144
+ }
145
+ next
146
+ end
147
+ if name_filter.match?(t.name)
148
+ @metadata[:filter][t.full_name] << {
149
+ rule: 'name_filter',
150
+ value: t.name,
151
+ pattern: name_filter,
152
+ }
153
+ next
154
+ end
155
+
156
+ only_check = only.empty? || only.any? { |o| t.full_name.match?(o) }
157
+ ignore_check = ignore.empty? || ignore.none? { |i| t.full_name.match?(i) }
158
+ if only_check && ignore_check
159
+ true
160
+ else
161
+ unless only_check
162
+ @metadata[:filter][t.full_name] << {
163
+ rule: 'only',
164
+ value: t.name,
165
+ pattern: only,
166
+ }
167
+ end
168
+ unless ignore_check
169
+ @metadata[:filter][t.full_name] << {
170
+ rule: 'ignore',
171
+ value: t.name,
172
+ pattern: ignore,
173
+ }
174
+ end
175
+ false
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,415 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require_relative 'errors'
5
+ require_relative 'tasks'
6
+ require_relative '../core_ext'
7
+ require_relative '../logging'
8
+
9
+ module CemAcpt
10
+ module Bolt
11
+ module Tests
12
+ using CemAcpt::CoreExt::ExtendedArray
13
+
14
+ # Provides an abstraction for the specific Bolt test data
15
+ class TestData
16
+ attr_reader :prop_validators, :params, :status
17
+
18
+ def initialize(**prop_validators)
19
+ @prop_validators = { params: {}, status: 'success' }.merge(prop_validators.transform_keys(&:to_sym))
20
+ @params = @prop_validators[:params]
21
+ @status = @prop_validators[:status]
22
+ end
23
+
24
+ def to_h
25
+ prop_validators
26
+ end
27
+
28
+ def validation_properties
29
+ @validation_properties ||= prop_validators.keys
30
+ end
31
+
32
+ # Compares prop_validators with another object that implements any of the properties as methods.
33
+ # @param other [Object] the object to compare against
34
+ # @return [Array<Hash>] an array of the hashes representing the results of the validation
35
+ # @raise [ArgumentError] if other does not implement any of the properties
36
+ def validate_props(other)
37
+ if (other.methods & validation_properties).empty?
38
+ raise ArgumentError, "Object does not implement any of the properties: #{validation_properties}"
39
+ end
40
+
41
+ prop_validators.each_with_object([]) do |(k, v), ary|
42
+ unless other.respond_to?(k)
43
+ ary << validation_hash(k, 'skipped', 'property_not_implemented', v, nil)
44
+ next
45
+ end
46
+
47
+ other_val = other.send(k)
48
+ begin
49
+ case v
50
+ when String
51
+ ary << validation_hash(k, result_from_bool(other_val == v), 'string_eq', v, other_val)
52
+ when Hash
53
+ v.transform_keys!(&:to_sym)
54
+ unknown_validators_val = validate_validators(v, :match, :not_match)
55
+ match_val = validate_match(k, v[:match], other_val)
56
+ not_match_val = validate_match(k, v[:not_match], other_val, negate: true)
57
+ [match_val, not_match_val, unknown_validators_val].compact.each { |h| ary << h }
58
+ else
59
+ ary << validation_hash(k, result_from_bool(other_val == v), 'simple_eq', v, other_val)
60
+ end
61
+ rescue StandardError => e
62
+ ary << validation_hash(k, 'failure', 'ruby_error', e, nil)
63
+ end
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def validate_match(prop, this_v, other_v, negate: false)
70
+ return if this_v.nil?
71
+
72
+ res = if other_v.is_a?(Hash)
73
+ JSON.generate(other_v).match?(this_v)
74
+ else
75
+ other_v.match?(this_v)
76
+ end
77
+ res = !res if negate
78
+ validation_hash(prop, result_from_bool(res), 'match', other_v, this_v)
79
+ end
80
+
81
+ def validate_validators(this_v, *keys)
82
+ return if this_v.nil?
83
+
84
+ unknown_keys = this_v.keys - keys.flatten.map(&:to_sym)
85
+ return if unknown_keys.empty?
86
+
87
+ validation_hash(:validators, 'failure', 'unknown_validators', this_v, unknown_keys)
88
+ end
89
+
90
+ def result_from_bool(bool)
91
+ bool ? 'success' : 'failure'
92
+ end
93
+
94
+ def validation_hash(prop, result, validator, validation_v, other_v)
95
+ { prop: prop, result: result, validator: validator, validation_value: validation_v, other_value: other_v }
96
+ end
97
+ end
98
+
99
+ # Contains the results of a single Bolt task test
100
+ class TestResult
101
+ attr_reader :name, :command_result, :validation_results, :details
102
+
103
+ def initialize(name, command_result, validation_results, **details)
104
+ @name = name
105
+ @command_result = command_result
106
+ @validation_results = validation_results
107
+ @details = details
108
+ end
109
+
110
+ def failed_validation_results
111
+ @failed_validation_results ||= validation_results.select { |vr| vr[:result] == 'failure' }
112
+ end
113
+
114
+ def skipped_validation_results
115
+ @skipped_validation_results ||= validation_results.select { |vr| vr[:result] == 'skipped' }
116
+ end
117
+
118
+ def success_validation_results
119
+ @success_validation_results ||= validation_results.select { |vr| vr[:result] == 'success' }
120
+ end
121
+
122
+ def action
123
+ @action ||= if @command_result.is_a?(CemAcpt::Bolt::BoltActionError)
124
+ @command_result.bolt_action
125
+ elsif @command_result.respond_to?(:action)
126
+ @command_result.action
127
+ else
128
+ 'unknown'
129
+ end
130
+ end
131
+
132
+ def target
133
+ @target ||= if @command_result.is_a?(CemAcpt::Bolt::BoltActionError)
134
+ @command_result.bolt_target
135
+ elsif @command_result.respond_to?(:target)
136
+ @command_result.target
137
+ else
138
+ 'unknown'
139
+ end
140
+ end
141
+
142
+ def object
143
+ @object ||= if @command_result.is_a?(CemAcpt::Bolt::BoltActionError)
144
+ @command_result.bolt_object
145
+ elsif @command_result.respond_to?(:object)
146
+ @command_result.object
147
+ else
148
+ 'unknown'
149
+ end
150
+ end
151
+
152
+ def success?
153
+ @validation_results.none? { |vr| vr[:result] == 'failure' }
154
+ end
155
+
156
+ def status
157
+ @status ||= success? ? 'success' : 'failure'
158
+ end
159
+
160
+ def error?
161
+ @command_result.is_a?(CemAcpt::Bolt::BoltActionError)
162
+ end
163
+
164
+ def error
165
+ @command_result if error?
166
+ end
167
+
168
+ def to_h
169
+ {
170
+ name: name,
171
+ success: success?,
172
+ command_result: command_result.to_h,
173
+ validation_results: validation_results,
174
+ details: details,
175
+ }
176
+ end
177
+ end
178
+
179
+ # Contains the results of running a Bolt task test
180
+ class TestResults
181
+ attr_reader :test_name, :results
182
+
183
+ def initialize(test_name, results = [])
184
+ @test_name = test_name
185
+ @results = results
186
+ end
187
+
188
+ def success?
189
+ @results.all? { |r| r.success? }
190
+ end
191
+
192
+ def status
193
+ @status ||= success? ? 'success' : 'failure'
194
+ end
195
+
196
+ def action
197
+ @action ||= @results.empty? ? 'unknown' : @results.first.action
198
+ end
199
+
200
+ def target
201
+ @target ||= @results.empty? ? 'unknown' : @results.first.target
202
+ end
203
+
204
+ def object
205
+ @object ||= @results.empty? ? 'unknown' : @results.first.object
206
+ end
207
+
208
+ def to_h
209
+ {
210
+ test_name: test_name,
211
+ success: success?,
212
+ results: @results.map(&:to_h),
213
+ }
214
+ end
215
+
216
+ def method_missing(method_name, *args, **kwargs, &block)
217
+ if @results.respond_to?(method_name)
218
+ @results.send(method_name, *args, **kwargs, &block)
219
+ else
220
+ super
221
+ end
222
+ end
223
+
224
+ def respond_to_missing?(method_name, include_private = false)
225
+ @results.respond_to?(method_name, include_private) || super
226
+ end
227
+
228
+ def inspect
229
+ "#<#{self.class}:#{object_id.to_s(16)} with #{results.count} result(s)>"
230
+ end
231
+ end
232
+
233
+ # Provides an interface for running tests against Bolt tasks
234
+ class Test
235
+ include CemAcpt::Logging
236
+
237
+ attr_reader :name, :data, :object, :groups, :command_result, :result
238
+
239
+ def initialize(name, data, object, *groups)
240
+ @name = name
241
+ @data = data.is_a?(Hash) ? TestData.new(**data) : TestData.new
242
+ @object = object
243
+ @groups = groups
244
+ @command_result = nil
245
+ @result = TestResults.new(name)
246
+ @validation_results = {}
247
+ end
248
+
249
+ # Groups in this context refers to the acceptance test names. Because the acceptance tests act like logical
250
+ # groups, we refer to them as groups here to avoid confusion with the Bolt tests.
251
+ def add_group(group)
252
+ @groups << group unless @groups.include?(group)
253
+ end
254
+
255
+ def run
256
+ logger.debug('CemAcpt::Bolt::Tests::Test') { "Running test #{name}" }
257
+ @command_result = @object.run(**@data.params)
258
+ validate!
259
+ rescue StandardError => e
260
+ new_wrapped_error("Error running test #{name}", e)
261
+ end
262
+
263
+ def to_s
264
+ "#<#{self.class}:#{object_id.to_s(16)} #{name},#{object},#{groups.join(',')}>"
265
+ end
266
+
267
+ def inspect
268
+ to_s
269
+ end
270
+
271
+ private
272
+
273
+ # Loops through the results of the Bolt task and validates the properties of each result.
274
+ # @return [TaskResults] the results of the validation
275
+ def validate!
276
+ logger.debug('CemAcpt::Bolt::Tests::Test') { "Validating test #{name}" }
277
+ begin
278
+ @command_result.results.each do |r|
279
+ prop_vals = @data.validate_props(r)
280
+ @validation_results[r] = prop_vals
281
+ @result << TestResult.new(name, r, prop_vals, test_groups: groups)
282
+ end
283
+ rescue StandardError => e
284
+ logger.debug('CemAcpt::Bolt::Tests::Test') { "Error validating command result for test #{name}:\n#{e}" }
285
+ wrapped_error = new_wrapped_error("Error validating test #{name}", e)
286
+ @result << TestResult.new(
287
+ name,
288
+ wrapped_error,
289
+ [],
290
+ test_groups: groups,
291
+ backtrace: e.backtrace,
292
+ )
293
+ end
294
+ rescue StandardError => e
295
+ logger.debug('CemAcpt::Bolt::Tests::Test') { "Error validating test #{name}:\n#{e}" }
296
+ wrapped_error = new_wrapped_error("Error validating test #{name}", e)
297
+ @result << TestResult.new(name, wrapped_error, [], test_groups: groups)
298
+ end
299
+
300
+ def new_wrapped_error(errmsg, original_err = nil)
301
+ last_cmd = @object.last_cmd_executed
302
+ cmd_family = last_cmd.nil? ? 'unknown' : last_cmd.command_family
303
+ CemAcpt::Bolt::BoltActionError.new(errmsg, original_err, cmd_family, last_cmd)
304
+ end
305
+ end
306
+
307
+ # Loads test data and provides an interface for running tests against Bolt tasks
308
+ class TestList
309
+ include CemAcpt::Logging
310
+ attr_accessor :config, :run_data, :inventory, :project
311
+ attr_reader :test_data, :tasks, :tests
312
+
313
+ def initialize(config = nil, run_data = nil, inventory = nil, project = nil)
314
+ @config = config
315
+ @run_data = run_data
316
+ @inventory = inventory
317
+ @project = project
318
+ @test_data = nil
319
+ @tasks = []
320
+ @tests = []
321
+ @loaded = false
322
+ @setup = false
323
+ end
324
+
325
+ # Loads the test data from the Bolt test files specified in the run data
326
+ def load!
327
+ return if loaded?
328
+ raise 'Run data has not been set, assign it to the #run_data attribute first' unless @run_data
329
+
330
+ @test_data = load_test_data(@run_data)
331
+ @loaded = true
332
+ end
333
+
334
+ def loaded?
335
+ @loaded
336
+ end
337
+
338
+ # Sets up the tests by loading the Bolt tasks and creating a TestWrapper for each test
339
+ def setup!
340
+ load! unless loaded?
341
+ @tasks = CemAcpt::Bolt::TaskList.new(@config)
342
+ @tasks.tasks # Ensure tasks are loaded
343
+ logger.verbose('CemAcpt::Bolt::Tests') { "TaskList metadata:\n#{@tasks.metadata.to_yaml}" }
344
+ @tests = new_test_array(@tasks, @test_data)
345
+ @setup = true
346
+ end
347
+
348
+ def setup?
349
+ @setup
350
+ end
351
+
352
+ def to_a
353
+ @tests
354
+ end
355
+
356
+ def split_into_groups(num_groups)
357
+ raise 'Tests have not been setup, call the #setup! method first' unless setup?
358
+
359
+ @tests.split_into_groups(num_groups)
360
+ end
361
+
362
+ def length
363
+ @tests.length
364
+ end
365
+ alias count length
366
+ alias size length
367
+
368
+ def to_s
369
+ "#<#{self.class}:#{object_id.to_s(16)} #{tests.join(',')}>"
370
+ end
371
+
372
+ def inspect
373
+ to_s
374
+ end
375
+
376
+ private
377
+
378
+ def load_test_data(run_data)
379
+ run_data[:test_data].each_with_object({}) do |tdata, h|
380
+ test_name = tdata[:test_name]
381
+ next if h.key?(test_name) || tdata[:bolt_test].nil?
382
+
383
+ logger.debug('CemAcpt::Bolt::Tests') { "Loading test data for test #{test_name}" }
384
+ begin
385
+ h[test_name] = YAML.safe_load_file(tdata[:bolt_test])
386
+ rescue Psych::SyntaxError, NoMethodError
387
+ h[test_name] = YAML.safe_load(File.read(tdata[:bolt_test]))
388
+ end
389
+ logger.debug('CemAcpt::Bolt::Tests') { "Loaded test data for test #{test_name}" }
390
+ logger.verbose('CemAcpt::Bolt::Tests') { "Test data for test #{test_name}:\n#{h[test_name].to_yaml}" }
391
+ end
392
+ end
393
+
394
+ def new_test_array(tasks, test_data)
395
+ tw_created = {}
396
+ logger.debug('CemAcpt::Bolt::Tests') { 'Creating tests for Bolt tasks' }
397
+ tasks.each do |task|
398
+ test_data.each do |group, tdata|
399
+ next unless @config.get('tests').include?(group)
400
+
401
+ if tw_created.key?(task.full_name)
402
+ tw_created[task.full_name].add_group(group)
403
+ logger.debug('CemAcpt::Bolt::Tests') { "Added group #{group} to test for task #{task.full_name}" }
404
+ else
405
+ tw_created[task.full_name] = Test.new(task.full_name, tdata[task.full_name], task, group)
406
+ logger.debug('CemAcpt::Bolt::Tests') { "Created test for task #{task.full_name}" }
407
+ end
408
+ end
409
+ end
410
+ tw_created.values
411
+ end
412
+ end
413
+ end
414
+ end
415
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../logging'
4
+ require_relative '../utils/files'
5
+
6
+ module CemAcpt
7
+ module Bolt
8
+ # Provides an abstraction for a YAML file
9
+ # Used as a base class for other YAML file abstractions
10
+ class YamlFile
11
+ include CemAcpt::Logging
12
+
13
+ attr_reader :path
14
+
15
+ def initialize(path)
16
+ @path = path
17
+ @hash = {}
18
+ @saved_hash = nil
19
+ end
20
+
21
+ def to_h
22
+ @hash
23
+ end
24
+
25
+ def load!
26
+ logger.debug(self.class.to_s) { "Loading #{path}..." }
27
+ @saved_hash = CemAcpt::Utils::Files.read(path, log_prefix: self.class.to_s)
28
+ logger.verbose(self.class.to_s) { "Loaded #{path} contents: #{@saved_hash}" }
29
+ @hash = @saved_hash.dup
30
+ end
31
+
32
+ def save!
33
+ return if latest_saved?
34
+
35
+ logger.debug(self.class.to_s) { 'Saving file...' }
36
+ CemAcpt::Utils::Files.delete(path, log_prefix: self.class.to_s) if File.exist?(path)
37
+ logger.debug(self.class.to_s) { "Creating #{path}..." }
38
+ logger.verbose(self.class.to_s) { "New #{path} contents: #{@hash}" }
39
+ CemAcpt::Utils::Files.write(path, @hash, log_prefix: self.class.to_s)
40
+ @saved_hash = @hash
41
+ end
42
+
43
+ def delete!
44
+ CemAcpt::Utils::Files.delete(path, log_prefix: self.class.to_s)
45
+ @saved_hash = nil
46
+ end
47
+
48
+ def latest_saved?
49
+ (@hash == @saved_hash) || eq_to_disk?
50
+ end
51
+
52
+ def eq_to_disk?
53
+ return false unless File.exist?(path)
54
+
55
+ disk_contents = CemAcpt::Utils::Files.read(path, log_prefix: self.class.to_s)
56
+ @hash.sort.to_h == disk_contents.sort.to_h
57
+ end
58
+
59
+ def gte_to_disk?
60
+ return false unless File.exist?(path)
61
+
62
+ disk_contents = CemAcpt::Utils::Files.read(path, log_prefix: self.class.to_s)
63
+ @hash.sort.to_h >= disk_contents.sort.to_h
64
+ end
65
+
66
+ def lte_to_disk?
67
+ return false unless File.exist?(path)
68
+
69
+ disk_contents = CemAcpt::Utils::Files.read(path, log_prefix: self.class.to_s)
70
+ @hash.sort.to_h <= disk_contents.sort.to_h
71
+ end
72
+ end
73
+ end
74
+ end