cem_acpt 0.8.7 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require_relative '../logging'
5
+
6
+ module CemAcpt
7
+ module Utils
8
+ # Utility classes and methods for files
9
+ module Files
10
+ class << self
11
+ # Reads a file based on its extension
12
+ # @param file [String] Path to the file
13
+ # @param log_level [Symbol] Log level to use
14
+ # @param log_prefix [String] Log prefix to use
15
+ # @param kwargs [Hash] Keyword arguments to pass to the file utility
16
+ # @option kwargs [String] :log_msg Log message to use when logging the file operation
17
+ # @option kwargs [Array] :permitted_classes Array of classes to permit when loading YAML files
18
+ # @return [Object] The result of the file utility's read method
19
+ def read(file, *args, log_level: :debug, log_prefix: 'CemAcpt', **kwargs)
20
+ return from_content_registry(file, :content) unless file_changed?(file)
21
+
22
+ content = new_file_util_for(file, log_level: log_level, log_prefix: log_prefix).read(file, *args, **kwargs)
23
+ add_to_content_registry(file, :content, content)
24
+ content
25
+ end
26
+
27
+ def write(file, content, *args, log_level: :debug, log_prefix: 'CemAcpt', **kwargs)
28
+ new_file_util_for(file, log_level: log_level, log_prefix: log_prefix).write(file, content, *args, **kwargs)
29
+ end
30
+
31
+ def delete(file, *args, log_level: :debug, log_prefix: 'CemAcpt', **kwargs)
32
+ new_file_util_for(file, log_level: log_level, log_prefix: log_prefix).delete(file, *args, **kwargs)
33
+ end
34
+
35
+ private
36
+
37
+ def mutex
38
+ @mutex ||= Mutex.new
39
+ end
40
+
41
+ def content_registry
42
+ @content_registry ||= {}
43
+ end
44
+
45
+ def file_changed?(file)
46
+ return true unless File.exist?(file)
47
+ fstat = File.stat(file)
48
+ rmtime = from_content_registry(file, :mtime)
49
+ check_res = rmtime.nil? || (fstat.mtime != rmtime)
50
+ add_to_content_registry(file, :mtime, fstat.mtime)
51
+ check_res
52
+ end
53
+
54
+ def add_to_content_registry(file, property, value)
55
+ mutex.synchronize do
56
+ content_registry[file] ||= {}
57
+ content_registry[file][property.to_sym] = value
58
+ end
59
+ end
60
+
61
+ def from_content_registry(file, property)
62
+ mutex.synchronize do
63
+ content_registry[file] ||= {}
64
+ content_registry[file][property.to_sym]
65
+ end
66
+ end
67
+
68
+ def new_file_util_for(file, log_level: :debug, log_prefix: 'CemAcpt')
69
+ case File.extname(file)
70
+ when *YamlUtil::VALID_EXTS
71
+ YamlUtil.new(log_level: log_level, log_prefix: log_prefix)
72
+ when *JsonUtil::VALID_EXTS
73
+ JsonUtil.new(log_level: log_level, log_prefix: log_prefix)
74
+ else
75
+ FileUtil.new(log_level: log_level, log_prefix: log_prefix)
76
+ end
77
+ end
78
+ end
79
+
80
+ # Generic file utility class
81
+ class FileUtil
82
+ include CemAcpt::Logging
83
+
84
+ attr_reader :log_level, :log_prefix, :file_exts
85
+
86
+ def initialize(log_level: :debug, log_prefix: 'CemAcpt', file_exts: [])
87
+ @log_level = log_level
88
+ @log_prefix = log_prefix
89
+ @file_exts = file_exts
90
+ end
91
+
92
+ def log_level=(level)
93
+ level = level.downcase.to_sym
94
+ raise ArgumentError, "Invalid log level #{level}" unless logger.respond_to?(level)
95
+
96
+ @log_level = level
97
+ end
98
+
99
+ def log_prefix=(prefix)
100
+ @log_prefix = prefix.to_s
101
+ end
102
+
103
+ def file_exts=(exts)
104
+ raise ArgumentError, 'file_exts must be an Array' unless exts.is_a?(Array)
105
+
106
+ @file_ext = exts
107
+ end
108
+
109
+ def write(file, content, *_args, log_msg: 'Writing file %s...', **_kwargs)
110
+ validate_and_log(file, log_msg)
111
+ File.write(file, content)
112
+ end
113
+
114
+ def read(file, *_args, log_msg: 'Reading file %s...', **_kwargs)
115
+ validate_and_log(file, log_msg)
116
+ File.read(file)
117
+ end
118
+
119
+ def delete(file, *_args, log_msg: 'Deleting file %s...', **_kwargs)
120
+ validate_and_log(file, log_msg)
121
+ FileUtils.rm_f(file)
122
+ end
123
+
124
+ private
125
+
126
+ def validate_and_log(file, log_msg)
127
+ file = validate_ext(file)
128
+ logger.send(log_level, log_prefix) { log_msg.to_s % file }
129
+ end
130
+
131
+ def validate_ext(file)
132
+ return if file_exts.empty?
133
+
134
+ ext = File.extname(file)
135
+ raise ArgumentError, "Invalid file extension #{ext}! Valid file extensions are #{file_exts}" unless file_exts.include?(ext)
136
+ end
137
+ end
138
+
139
+ # Utility class for working with YAML files
140
+ class YamlUtil < FileUtil
141
+ VALID_EXTS = %w[.yaml .yml].freeze
142
+ DEFAULT_PERMITTED_CLASSES = [Symbol].freeze
143
+
144
+ def initialize(log_level: :debug, log_prefix: 'CemAcpt', file_exts: VALID_EXTS)
145
+ super(log_level: log_level, log_prefix: log_prefix, file_exts: file_exts)
146
+ require 'yaml'
147
+ end
148
+
149
+ def write(file, content, *_args, log_msg: 'Writing YAML file %s...', **_kwargs)
150
+ raise ArgumentError, 'content must be a Hash' unless content.is_a?(Hash)
151
+
152
+ super(file, content.to_yaml, log_msg)
153
+ end
154
+
155
+ def read(file, *_args, log_msg: 'Reading YAML file %s...', permitted_classes: DEFAULT_PERMITTED_CLASSES, **_kwargs)
156
+ YAML.safe_load(super(file, log_msg), permitted_classes: permitted_classes)
157
+ end
158
+
159
+ def delete(file, *_args, log_msg: 'Deleting YAML file %s...', **_kwargs)
160
+ super(file, log_msg)
161
+ end
162
+ end
163
+
164
+ # Utility class for working with JSON files
165
+ class JsonUtil < FileUtil
166
+ VALID_EXTS = %w[.json].freeze
167
+
168
+ def initialize(log_level: :debug, log_prefix: 'CemAcpt', file_exts: VALID_EXTS)
169
+ super(log_level: log_level, log_prefix: log_prefix, file_exts: file_exts)
170
+ require 'json'
171
+ end
172
+
173
+ def write(file, content, *_args, log_msg: 'Writing JSON file %s...', **_kwargs)
174
+ raise ArgumentError, 'content must be a Hash' unless content.is_a?(Hash)
175
+
176
+ super(file, content.to_json, log_msg)
177
+ end
178
+
179
+ def read(file, *_args, log_msg: 'Reading JSON file %s...', **_kwargs)
180
+ JSON.parse(super(file, log_msg))
181
+ end
182
+
183
+ def delete(file, *_args, log_msg: 'Deleting JSON file %s...', **_kwargs)
184
+ super(file, log_msg)
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CemAcpt
4
+ module Utils
5
+ class FinalizerQueueError < StandardError; end
6
+
7
+ # A queue that can be finalized.
8
+ # When a queue is finalized, no more items can be added to it, and the
9
+ # queue is closed and converted to a frozen array.
10
+ class FinalizerQueue
11
+ def initialize
12
+ @queue = Queue.new
13
+ @array = []
14
+ @finalized = false
15
+ @mutex = Mutex.new
16
+ end
17
+
18
+ def finalize!
19
+ return if finalized?
20
+
21
+ @finalized = true
22
+ new_array
23
+ end
24
+
25
+ def finalized?
26
+ @finalized
27
+ end
28
+
29
+ def to_a
30
+ raise FinalizerQueueError, 'Cannot convert to array until finalized' unless finalized?
31
+
32
+ @array
33
+ end
34
+
35
+ def method_missing(method_name, *args, **kwargs, &block)
36
+ if finalized?
37
+ @array.send(method_name, *args, **kwargs, &block)
38
+ elsif @queue.respond_to?(method_name)
39
+ @queue.send(method_name, *args, **kwargs, &block)
40
+ else
41
+ super
42
+ end
43
+ rescue StandardError => e
44
+ raise e if e.is_a?(NoMethodError) || e.is_a?(FinalizerQueueError)
45
+
46
+ new_err = FinalizerQueueError.new("Error calling #{method_name} on FinalizerQueue: #{e}")
47
+ new_err.set_backtrace(e.backtrace)
48
+ raise new_err
49
+ end
50
+
51
+ def respond_to_missing?(method_name, include_private = false)
52
+ @array.respond_to?(method_name, include_private) if finalized?
53
+
54
+ super
55
+ end
56
+
57
+ private
58
+
59
+ def new_array
60
+ @queue.close unless @queue.closed?
61
+ @array << @queue.pop until @queue.empty?
62
+ @array.compact!
63
+ @array.freeze
64
+ end
65
+
66
+ def require_finalized(caller_binding)
67
+ return if finalized?
68
+
69
+ raise FinalizerQueueError, "Cannot call #{caller_binding.eval('__method__')} on unfinalized #{self.class.name}"
70
+ end
71
+ end
72
+ end
73
+ end
@@ -6,6 +6,7 @@ require 'stringio'
6
6
  module CemAcpt
7
7
  # Error class for shell commands
8
8
  class ShellCommandError < StandardError; end
9
+ class ShellCommandNotFoundError < ShellCommandError; end
9
10
 
10
11
  module Utils
11
12
  # Generic utilities for running local shell commands
@@ -23,7 +24,7 @@ module CemAcpt
23
24
  io_outerr = StringIO.new
24
25
  if output.respond_to?(:debug)
25
26
  output.debug('CemAcpt::Utils::Shell') { "Running command:\n\t#{cmd}\nWith environment:\n\t#{env}" }
26
- else
27
+ elsif output
27
28
  output << "Running command:\n\t#{cmd}\nWith environment:\n\t#{env}\n"
28
29
  end
29
30
  val = Open3.popen2e(env, cmd) do |stdin, outerr, wait_thr|
@@ -47,18 +48,26 @@ module CemAcpt
47
48
 
48
49
  # Mimics the behavior of the `which` command.
49
50
  # @param cmd [String] The command to find
50
- # @return [String] The path to the command
51
- # @return [nil] If the command is not found
52
- def self.which(cmd)
51
+ # @param include_ruby_bin [Boolean] Whether to include Ruby bin directories in the search.
52
+ # Setting this to true can cause errors to be raised if cem_acpt attempts to use a Ruby
53
+ # command that is not available to cem_acpt, such as when running with `bundle exec`.
54
+ # @param raise_if_not_found [Boolean] Whether to raise an error if the command is not found
55
+ # @return [String, nil] The path to the command or nil if not found
56
+ # @raise [CemAcpt::ShellCommandNotFoundError] If the command is not found and raise_if_not_found is true
57
+ def self.which(cmd, include_ruby_bin: false, raise_if_not_found: false)
53
58
  return cmd if File.executable?(cmd) && !File.directory?(cmd)
54
59
 
55
60
  exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
56
61
  ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
62
+ next if path.include?('/ruby') && !include_ruby_bin
63
+
57
64
  exts.each do |ext|
58
65
  exe = File.join(path, "#{cmd}#{ext}")
59
66
  return exe if File.executable?(exe) && !File.directory?(exe)
60
67
  end
61
68
  end
69
+ raise CemAcpt::ShellCommandNotFoundError, "Command #{cmd} not found in PATH" if raise_if_not_found
70
+
62
71
  nil
63
72
  end
64
73
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CemAcpt
4
- VERSION = '0.8.7'
4
+ VERSION = '0.9.0'
5
5
  end
data/sample_config.yaml CHANGED
@@ -58,6 +58,19 @@ tests:
58
58
  # - stig_rhel-7_firewalld_public_3
59
59
  # - stig_rhel-8_firewalld_public_3
60
60
 
61
+ bolt:
62
+ project:
63
+ name: 'cem-acpt'
64
+ analytics: false
65
+ tests:
66
+ only: [] # Test names from the "tests" array above
67
+ ignore: []
68
+ tasks:
69
+ ignore: [] # Task names to ignore
70
+ only: []
71
+ module_pattern: '^.*$'
72
+ name_filter: '^fake_task$'
73
+
61
74
  cem_acpt_image:
62
75
  no_windows: true
63
76
  no_linux: false
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cem_acpt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.7
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - puppetlabs
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-10-25 00:00:00.000000000 Z
11
+ date: 2024-01-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-http
@@ -180,6 +180,20 @@ dependencies:
180
180
  - - ">="
181
181
  - !ruby/object:Gem::Version
182
182
  version: '0'
183
+ - !ruby/object:Gem::Dependency
184
+ name: simplecov
185
+ requirement: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ type: :development
191
+ prerelease: false
192
+ version_requirements: !ruby/object:Gem::Requirement
193
+ requirements:
194
+ - - ">="
195
+ - !ruby/object:Gem::Version
196
+ version: '0'
183
197
  description: Litmus-like library focusing on CEM Acceptance Tests
184
198
  email:
185
199
  - abide-team@puppet.com
@@ -205,6 +219,20 @@ files:
205
219
  - exe/cem_acpt_image
206
220
  - lib/cem_acpt.rb
207
221
  - lib/cem_acpt/action_result.rb
222
+ - lib/cem_acpt/actions.rb
223
+ - lib/cem_acpt/bolt.rb
224
+ - lib/cem_acpt/bolt/cmd.rb
225
+ - lib/cem_acpt/bolt/cmd/base.rb
226
+ - lib/cem_acpt/bolt/cmd/output.rb
227
+ - lib/cem_acpt/bolt/cmd/task.rb
228
+ - lib/cem_acpt/bolt/errors.rb
229
+ - lib/cem_acpt/bolt/helpers.rb
230
+ - lib/cem_acpt/bolt/inventory.rb
231
+ - lib/cem_acpt/bolt/project.rb
232
+ - lib/cem_acpt/bolt/summary_results.rb
233
+ - lib/cem_acpt/bolt/tasks.rb
234
+ - lib/cem_acpt/bolt/tests.rb
235
+ - lib/cem_acpt/bolt/yaml_file.rb
208
236
  - lib/cem_acpt/cli.rb
209
237
  - lib/cem_acpt/config.rb
210
238
  - lib/cem_acpt/config/base.rb
@@ -233,9 +261,17 @@ files:
233
261
  - lib/cem_acpt/test_data.rb
234
262
  - lib/cem_acpt/test_runner.rb
235
263
  - lib/cem_acpt/test_runner/log_formatter.rb
236
- - lib/cem_acpt/test_runner/log_formatter/error_formatter.rb
264
+ - lib/cem_acpt/test_runner/log_formatter/base.rb
265
+ - lib/cem_acpt/test_runner/log_formatter/bolt_error_formatter.rb
266
+ - lib/cem_acpt/test_runner/log_formatter/bolt_output_formatter.rb
267
+ - lib/cem_acpt/test_runner/log_formatter/bolt_summary_results_formatter.rb
237
268
  - lib/cem_acpt/test_runner/log_formatter/goss_action_response.rb
269
+ - lib/cem_acpt/test_runner/log_formatter/goss_error_formatter.rb
270
+ - lib/cem_acpt/test_runner/log_formatter/standard_error_formatter.rb
271
+ - lib/cem_acpt/test_runner/test_results.rb
238
272
  - lib/cem_acpt/utils.rb
273
+ - lib/cem_acpt/utils/files.rb
274
+ - lib/cem_acpt/utils/finalizer_queue.rb
239
275
  - lib/cem_acpt/utils/puppet.rb
240
276
  - lib/cem_acpt/utils/shell.rb
241
277
  - lib/cem_acpt/utils/ssh.rb
@@ -270,14 +306,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
270
306
  requirements:
271
307
  - - ">="
272
308
  - !ruby/object:Gem::Version
273
- version: 2.6.0
309
+ version: 3.0.0
274
310
  required_rubygems_version: !ruby/object:Gem::Requirement
275
311
  requirements:
276
312
  - - ">="
277
313
  - !ruby/object:Gem::Version
278
314
  version: '0'
279
315
  requirements: []
280
- rubygems_version: 3.4.19
316
+ rubygems_version: 3.4.22
281
317
  signing_key:
282
318
  specification_version: 4
283
319
  summary: CEM Acceptance Tests
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CemAcpt
4
- module TestRunner
5
- module LogFormatter
6
- class ErrorFormatter
7
- def inspect
8
- to_s
9
- end
10
-
11
- def to_s
12
- "#<#{self.class.name}:0x#{object_id.to_s(16)}>"
13
- end
14
-
15
- def summary(response)
16
- "Error: #{response.summary}"
17
- end
18
-
19
- def results(response)
20
- [response.summary, response.results.join("\n")]
21
- end
22
-
23
- def host_name(response)
24
- "Error: #{response.error.class.name}"
25
- end
26
-
27
- def test_name(response)
28
- "Error: #{response.error.class.name}"
29
- end
30
- end
31
- end
32
- end
33
- end