cem_acpt 0.8.7 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) 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/logging/formatter.rb +3 -3
  28. data/lib/cem_acpt/logging.rb +17 -1
  29. data/lib/cem_acpt/provision/terraform/linux.rb +1 -1
  30. data/lib/cem_acpt/test_data.rb +2 -0
  31. data/lib/cem_acpt/test_runner/log_formatter/base.rb +73 -0
  32. data/lib/cem_acpt/test_runner/log_formatter/bolt_error_formatter.rb +65 -0
  33. data/lib/cem_acpt/test_runner/log_formatter/bolt_output_formatter.rb +54 -0
  34. data/lib/cem_acpt/test_runner/log_formatter/bolt_summary_results_formatter.rb +64 -0
  35. data/lib/cem_acpt/test_runner/log_formatter/goss_action_response.rb +17 -30
  36. data/lib/cem_acpt/test_runner/log_formatter/goss_error_formatter.rb +31 -0
  37. data/lib/cem_acpt/test_runner/log_formatter/standard_error_formatter.rb +35 -0
  38. data/lib/cem_acpt/test_runner/log_formatter.rb +17 -5
  39. data/lib/cem_acpt/test_runner/test_results.rb +150 -0
  40. data/lib/cem_acpt/test_runner.rb +153 -53
  41. data/lib/cem_acpt/utils/files.rb +189 -0
  42. data/lib/cem_acpt/utils/finalizer_queue.rb +73 -0
  43. data/lib/cem_acpt/utils/shell.rb +13 -4
  44. data/lib/cem_acpt/version.rb +1 -1
  45. data/sample_config.yaml +13 -0
  46. metadata +41 -5
  47. 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