cem_acpt 0.2.5 → 0.6.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.
- checksums.yaml +4 -4
- data/.github/workflows/spec.yml +30 -0
- data/Gemfile +4 -3
- data/Gemfile.lock +95 -43
- data/README.md +144 -83
- data/cem_acpt.gemspec +12 -7
- data/exe/cem_acpt +41 -7
- data/lib/cem_acpt/config.rb +340 -0
- data/lib/cem_acpt/core_extensions.rb +17 -61
- data/lib/cem_acpt/goss/api/action_response.rb +175 -0
- data/lib/cem_acpt/goss/api.rb +83 -0
- data/lib/cem_acpt/goss.rb +8 -0
- data/lib/cem_acpt/image_name_builder.rb +0 -9
- data/lib/cem_acpt/logging/formatter.rb +97 -0
- data/lib/cem_acpt/logging.rb +168 -142
- data/lib/cem_acpt/platform/base.rb +26 -37
- data/lib/cem_acpt/platform/gcp.rb +48 -62
- data/lib/cem_acpt/platform.rb +30 -28
- data/lib/cem_acpt/provision/terraform/linux.rb +47 -0
- data/lib/cem_acpt/provision/terraform/os_data.rb +72 -0
- data/lib/cem_acpt/provision/terraform/windows.rb +22 -0
- data/lib/cem_acpt/provision/terraform.rb +193 -0
- data/lib/cem_acpt/provision.rb +20 -0
- data/lib/cem_acpt/puppet_helpers.rb +0 -1
- data/lib/cem_acpt/test_data.rb +23 -13
- data/lib/cem_acpt/test_runner/log_formatter/goss_action_response.rb +104 -0
- data/lib/cem_acpt/test_runner/log_formatter.rb +10 -0
- data/lib/cem_acpt/test_runner.rb +170 -3
- data/lib/cem_acpt/utils/puppet.rb +29 -0
- data/lib/cem_acpt/utils/ssh.rb +197 -0
- data/lib/cem_acpt/utils/terminal.rb +27 -0
- data/lib/cem_acpt/utils.rb +4 -138
- data/lib/cem_acpt/version.rb +1 -1
- data/lib/cem_acpt.rb +73 -23
- data/lib/terraform/gcp/linux/goss/puppet_idempotent.yaml +10 -0
- data/lib/terraform/gcp/linux/goss/puppet_noop.yaml +12 -0
- data/lib/terraform/gcp/linux/main.tf +191 -0
- data/lib/terraform/gcp/linux/systemd/goss-acpt.service +8 -0
- data/lib/terraform/gcp/linux/systemd/goss-idempotent.service +8 -0
- data/lib/terraform/gcp/linux/systemd/goss-noop.service +8 -0
- data/lib/terraform/gcp/windows/.keep +0 -0
- data/sample_config.yaml +22 -21
- metadata +151 -51
- data/lib/cem_acpt/bootstrap/bootstrapper.rb +0 -206
- data/lib/cem_acpt/bootstrap/operating_system/rhel_family.rb +0 -129
- data/lib/cem_acpt/bootstrap/operating_system.rb +0 -17
- data/lib/cem_acpt/bootstrap.rb +0 -12
- data/lib/cem_acpt/context.rb +0 -153
- data/lib/cem_acpt/platform/base/cmd.rb +0 -71
- data/lib/cem_acpt/platform/gcp/cmd.rb +0 -345
- data/lib/cem_acpt/platform/gcp/compute.rb +0 -332
- data/lib/cem_acpt/platform/vmpooler.rb +0 -24
- data/lib/cem_acpt/rspec_utils.rb +0 -242
- data/lib/cem_acpt/shared_objects.rb +0 -537
- data/lib/cem_acpt/spec_helper_acceptance.rb +0 -184
- data/lib/cem_acpt/test_runner/run_handler.rb +0 -187
- data/lib/cem_acpt/test_runner/runner.rb +0 -210
- data/lib/cem_acpt/test_runner/runner_result.rb +0 -103
data/exe/cem_acpt
CHANGED
@@ -11,6 +11,25 @@ options = {}
|
|
11
11
|
parser = OptionParser.new do |opts|
|
12
12
|
opts.banner = 'Usage: cem_acpt [options]'
|
13
13
|
|
14
|
+
opts.on('-h', '--help', 'Show this help message') do
|
15
|
+
puts opts
|
16
|
+
exit 0
|
17
|
+
end
|
18
|
+
|
19
|
+
opts.on('-a', '--only-actions ACTIONS', 'Set actions. Example: -a "acpt,noop"') do |a|
|
20
|
+
options[:actions] ||= {}
|
21
|
+
options[:actions][:only] = a.split(',')
|
22
|
+
end
|
23
|
+
|
24
|
+
opts.on('-A', '--except-actions ACTIONS', 'Set excluded actions. Example: -A "noop,idempotent"') do |a|
|
25
|
+
options[:actions] ||= {}
|
26
|
+
options[:actions][:except] = a.split(',')
|
27
|
+
end
|
28
|
+
|
29
|
+
opts.on('-t', '--tests TESTS', 'Set tests. Example: -t "test1,test2"') do |t|
|
30
|
+
options[:tests] = t.split(',')
|
31
|
+
end
|
32
|
+
|
14
33
|
opts.on('-D', '--debug', 'Enable debug logging') do
|
15
34
|
options[:log_level] = 'debug'
|
16
35
|
end
|
@@ -39,7 +58,7 @@ parser = OptionParser.new do |opts|
|
|
39
58
|
end
|
40
59
|
|
41
60
|
opts.on('-I', '--CI', 'Run in CI mode') do
|
42
|
-
options[:
|
61
|
+
options[:ci_mode] = true
|
43
62
|
options[:log_format] = 'github_action'
|
44
63
|
end
|
45
64
|
|
@@ -55,12 +74,21 @@ parser = OptionParser.new do |opts|
|
|
55
74
|
options[:verbose] = true
|
56
75
|
end
|
57
76
|
|
77
|
+
opts.on('-S', '--no-epehemeral-ssh-key', 'Do not generate an ephemeral SSH key for test suites') do
|
78
|
+
options[:no_ephemeral_ssh_key] = true
|
79
|
+
end
|
80
|
+
|
58
81
|
opts.on('-V', '--version', 'Show the cem_acpt version') do
|
59
|
-
|
82
|
+
puts CemAcpt.version(as_str: true)
|
83
|
+
exit 0
|
60
84
|
end
|
61
85
|
|
62
|
-
opts.on('-
|
63
|
-
options[:
|
86
|
+
opts.on('-Y', '--print-yaml-config', 'Loads and prints the config as YAML. Other specified options will be added to the config.') do
|
87
|
+
options[:print_yaml_config] = true
|
88
|
+
end
|
89
|
+
|
90
|
+
opts.on('-X', '--explain-config', 'Loads and prints the config explanation. Other specified options will be added to the config.') do
|
91
|
+
options[:explain_config] = true
|
64
92
|
end
|
65
93
|
|
66
94
|
# NOT IMPLEMENTED
|
@@ -70,12 +98,18 @@ parser = OptionParser.new do |opts|
|
|
70
98
|
end
|
71
99
|
|
72
100
|
parser.parse!
|
73
|
-
if options[:
|
74
|
-
|
101
|
+
if options[:print_yaml_config]
|
102
|
+
options.delete(:print_yaml_config)
|
103
|
+
puts CemAcpt.print_config(options, format: :yaml)
|
104
|
+
exit 0
|
105
|
+
end
|
106
|
+
if options[:explain_config]
|
107
|
+
options.delete(:explain_config)
|
108
|
+
puts CemAcpt.print_config(options, format: :explain)
|
75
109
|
exit 0
|
76
110
|
end
|
111
|
+
# Set CLI defaults
|
77
112
|
options[:module_dir] = Dir.pwd unless options[:module_dir]
|
78
|
-
options[:platforms] = %w[gcp vmpooler] unless options[:platform]
|
79
113
|
if (options[:log_level] == 'debug' || options[:verbose]) && !options[:quiet]
|
80
114
|
puts '#################### RUNNING ACCEPTANCE TEST SUITE ####################'
|
81
115
|
puts "Using options from command line: #{options}"
|
@@ -0,0 +1,340 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'yaml'
|
5
|
+
require_relative 'core_extensions'
|
6
|
+
|
7
|
+
module CemAcpt
|
8
|
+
using CemAcpt::CoreExtensions::ExtendedHash
|
9
|
+
|
10
|
+
# Holds the configuration for cem_acpt
|
11
|
+
class Config
|
12
|
+
KEYS = %i[
|
13
|
+
actions
|
14
|
+
ci_mode
|
15
|
+
config_file
|
16
|
+
image_name_builder
|
17
|
+
log_level
|
18
|
+
log_file
|
19
|
+
log_format
|
20
|
+
module_dir
|
21
|
+
node_data
|
22
|
+
no_ephemeral_ssh_key
|
23
|
+
platform
|
24
|
+
provisioner
|
25
|
+
quiet
|
26
|
+
terraform
|
27
|
+
test_data
|
28
|
+
tests
|
29
|
+
user_config
|
30
|
+
verbose
|
31
|
+
].freeze
|
32
|
+
|
33
|
+
attr_reader :config, :env_vars
|
34
|
+
|
35
|
+
def initialize(opts: {}, config_file: nil, load_user_config: true)
|
36
|
+
@load_user_config = load_user_config
|
37
|
+
load(opts: opts, config_file: config_file)
|
38
|
+
end
|
39
|
+
|
40
|
+
# The default configuration
|
41
|
+
def defaults
|
42
|
+
{
|
43
|
+
actions: {},
|
44
|
+
ci_mode: false,
|
45
|
+
config_file: nil,
|
46
|
+
image_name_builder: {
|
47
|
+
character_substitutions: ['_', '-'],
|
48
|
+
parts: ['cem-acpt', '$image_fam', '$collection', '$firewall'],
|
49
|
+
join_with: '-',
|
50
|
+
},
|
51
|
+
log_level: 'info',
|
52
|
+
log_file: nil,
|
53
|
+
log_format: 'text',
|
54
|
+
module_dir: Dir.pwd,
|
55
|
+
node_data: {},
|
56
|
+
no_ephemeral_ssh_key: false,
|
57
|
+
platform: {
|
58
|
+
name: 'gcp',
|
59
|
+
},
|
60
|
+
quiet: false,
|
61
|
+
test_data: {
|
62
|
+
for_each: {
|
63
|
+
collection: %w[puppet7],
|
64
|
+
},
|
65
|
+
vars: {},
|
66
|
+
name_pattern_vars: %r{^(?<framework>[a-z]+)_(?<image_fam>[a-z0-9-]+)_(?<firewall>[a-z]+)_(?<framework_vars>[-_a-z0-9]+)$},
|
67
|
+
vars_post_processing: {
|
68
|
+
new_vars: [
|
69
|
+
{
|
70
|
+
name: 'profile',
|
71
|
+
string_split: {
|
72
|
+
from: 'framework_vars',
|
73
|
+
using: '_',
|
74
|
+
part: 0,
|
75
|
+
},
|
76
|
+
},
|
77
|
+
{
|
78
|
+
name: 'level',
|
79
|
+
string_split: {
|
80
|
+
from: 'framework_vars',
|
81
|
+
using: '_',
|
82
|
+
part: 1,
|
83
|
+
},
|
84
|
+
},
|
85
|
+
],
|
86
|
+
delete_vars: %w[framework_vars],
|
87
|
+
},
|
88
|
+
},
|
89
|
+
tests: [],
|
90
|
+
verbose: false,
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
# Load the configuration from the environment variables, config file, and opts
|
95
|
+
# The order of precedence is:
|
96
|
+
# 1. environment variables
|
97
|
+
# 2. user config file (config.yaml in user_config_dir)
|
98
|
+
# 3. specified config file (if it exists)
|
99
|
+
# 4. opts
|
100
|
+
# 5. static options (set in this class)
|
101
|
+
# @param opts [Hash] The options to load
|
102
|
+
# @param config_file [String] The config file to load
|
103
|
+
# @return [self] This object with the config loaded
|
104
|
+
def load(opts: {}, config_file: nil)
|
105
|
+
create_config_dirs!
|
106
|
+
init_config!(opts: opts, config_file: config_file)
|
107
|
+
add_env_vars!(@config)
|
108
|
+
@config.merge!(user_config) if user_config && @load_user_config
|
109
|
+
@config.merge!(config_from_file) if config_from_file
|
110
|
+
@config.merge!(@options) if @options
|
111
|
+
add_static_options!(@config)
|
112
|
+
@config.format! # Symbolize keys of all hashes
|
113
|
+
validate_config!
|
114
|
+
# Freeze the config so it can't be modified
|
115
|
+
# This helps with thread safety and deterministic behavior
|
116
|
+
@config.freeze
|
117
|
+
self
|
118
|
+
end
|
119
|
+
alias to_h config
|
120
|
+
|
121
|
+
def user_config_dir
|
122
|
+
@user_config_dir ||= @config.dget('user_config.dir')
|
123
|
+
end
|
124
|
+
|
125
|
+
def explain
|
126
|
+
explanation = {}
|
127
|
+
%i[defaults env_vars user_config config_from_file options].each do |source|
|
128
|
+
source_vals = send(source).dup
|
129
|
+
next if source_vals.nil? || source_vals.empty?
|
130
|
+
|
131
|
+
# The loop below will overwrite the value of explanation[key] if the same key is found in multiple sources
|
132
|
+
# This is intentional, as the last source to set the value is the one that should be used
|
133
|
+
source_vals.each do |key, value|
|
134
|
+
explanation[key] = source if @config.dget(key.to_s) == value
|
135
|
+
end
|
136
|
+
end
|
137
|
+
explained = explanation.each_with_object([]) do |(key, value), ary|
|
138
|
+
ary << "Key '#{key}' from source '#{value}'"
|
139
|
+
end
|
140
|
+
explained.join("\n")
|
141
|
+
end
|
142
|
+
|
143
|
+
def [](key)
|
144
|
+
if key.is_a?(Symbol)
|
145
|
+
@config[key].dup
|
146
|
+
elsif key.is_a?(String)
|
147
|
+
@config.dget(key).dup
|
148
|
+
else
|
149
|
+
raise ArgumentError, "Invalid key type '#{key.class}'"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def get(dot_key)
|
154
|
+
@config.dget(dot_key).dup
|
155
|
+
end
|
156
|
+
alias dget get
|
157
|
+
|
158
|
+
def has?(dot_key)
|
159
|
+
!!get(dot_key)
|
160
|
+
end
|
161
|
+
|
162
|
+
def empty?
|
163
|
+
@config.empty?
|
164
|
+
end
|
165
|
+
|
166
|
+
def ci_mode?
|
167
|
+
!!get('ci_mode') || !!(ENV['GITHUB_ACTIONS'] || ENV['CI'])
|
168
|
+
end
|
169
|
+
alias ci? ci_mode?
|
170
|
+
|
171
|
+
def debug_mode?
|
172
|
+
get('log_level') == 'debug'
|
173
|
+
end
|
174
|
+
alias debug? debug_mode?
|
175
|
+
|
176
|
+
def verbose_mode?
|
177
|
+
!!get('verbose')
|
178
|
+
end
|
179
|
+
alias verbose? verbose_mode?
|
180
|
+
|
181
|
+
def quiet_mode?
|
182
|
+
!!get('quiet')
|
183
|
+
end
|
184
|
+
alias quiet? quiet_mode?
|
185
|
+
|
186
|
+
def to_yaml
|
187
|
+
@config.to_yaml
|
188
|
+
end
|
189
|
+
|
190
|
+
def to_json(*args)
|
191
|
+
@config.to_json(*args)
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
attr_reader :options
|
197
|
+
|
198
|
+
def user_config_dir
|
199
|
+
@user_config_dir ||= File.join(Dir.home, '.cem_acpt')
|
200
|
+
end
|
201
|
+
|
202
|
+
def user_config_file
|
203
|
+
@user_config_file ||= File.join(user_config_dir, 'config.yaml')
|
204
|
+
end
|
205
|
+
|
206
|
+
def terraform_dir
|
207
|
+
@terraform_dir ||= File.join(user_config_dir, 'terraform')
|
208
|
+
end
|
209
|
+
|
210
|
+
def valid_env_var?(env_var)
|
211
|
+
env_var.start_with?('CEM_ACPT_') && ENV[env_var]
|
212
|
+
end
|
213
|
+
|
214
|
+
def env_var_to_dot_key(env_var)
|
215
|
+
env_var.sub('CEM_ACPT_', '').gsub(%r{__}, '.').downcase
|
216
|
+
end
|
217
|
+
|
218
|
+
def add_static_options!(config)
|
219
|
+
config.dset('user_config.dir', user_config_dir)
|
220
|
+
config.dset('user_config.file', user_config_file)
|
221
|
+
config.dset('provisioner', 'terraform')
|
222
|
+
config.dset('terraform.dir', terraform_dir)
|
223
|
+
set_third_party_env_vars!(config)
|
224
|
+
end
|
225
|
+
|
226
|
+
# Certain environment variables are used by other tools like GitHub Actions
|
227
|
+
# This method sets the relative config values for those environment variables.
|
228
|
+
# This is the last step in composing the config, so it will override any
|
229
|
+
# values set by the user.
|
230
|
+
def set_third_party_env_vars!(config)
|
231
|
+
if ENV['RUNNER_DEBUG'] == '1'
|
232
|
+
config.dset('log_level', 'debug')
|
233
|
+
config.dset('verbose', true)
|
234
|
+
end
|
235
|
+
if ENV['GITHUB_ACTIONS'] == 'true' || ENV['CI'] == 'true'
|
236
|
+
config.dset('ci_mode', true)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# Used to source the config during loading of config files.
|
241
|
+
# Because config may not be fully loaded yet, this method
|
242
|
+
# checks the options hash first, then the current config,
|
243
|
+
# then the environment variables for the given key
|
244
|
+
# @param key [String] The key to find in dot notation
|
245
|
+
# @return [Any] The value of the key
|
246
|
+
def find_option(key)
|
247
|
+
return @options.dget(key) if @options.dget(key)
|
248
|
+
return @config.dget(key) if @config.dget(key)
|
249
|
+
ENV.each do |k, v|
|
250
|
+
next unless valid_env_var?(k)
|
251
|
+
|
252
|
+
return v if env_var_to_dot_key(k) == key
|
253
|
+
end
|
254
|
+
nil
|
255
|
+
end
|
256
|
+
|
257
|
+
def init_config!(opts: {}, config_file: nil)
|
258
|
+
# Blank out the config
|
259
|
+
@user_config = {}
|
260
|
+
@config_from_file = {}
|
261
|
+
@options = {}
|
262
|
+
@config = defaults.dup
|
263
|
+
# Set the parameterized defaults
|
264
|
+
config_file = ENV['CEM_ACPT_CONFIG_FILE'] if config_file.nil?
|
265
|
+
@config.dset('config_file', config_file) if config_file
|
266
|
+
@options = opts || {}
|
267
|
+
end
|
268
|
+
|
269
|
+
def add_env_vars!(config)
|
270
|
+
@env_vars = {}
|
271
|
+
# First load known environment variables into their respective config keys
|
272
|
+
# Then load any environment variables that start with CEM_ACPT_<known key> into their respective config keys
|
273
|
+
ENV.each do |env_var, value|
|
274
|
+
next unless valid_env_var?(env_var)
|
275
|
+
|
276
|
+
key = env_var_to_dot_key(env_var) # Convert CEM_ACPT_<key> to <dotkey>
|
277
|
+
next unless KEYS.include?(key.split('.').first.to_sym) # Skip if the key is not a known config key
|
278
|
+
@env_vars[key] = value
|
279
|
+
config.dset(key, value)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def user_config
|
284
|
+
return @user_config unless @user_config.nil? || @user_config.empty?
|
285
|
+
|
286
|
+
@user_config = if user_config_file && File.exist?(user_config_file)
|
287
|
+
load_config_file(user_config_file)
|
288
|
+
else
|
289
|
+
{}
|
290
|
+
end
|
291
|
+
|
292
|
+
@user_config
|
293
|
+
end
|
294
|
+
|
295
|
+
def config_from_file
|
296
|
+
return @config_from_file unless @config_from_file.nil? || @config_from_file.empty?
|
297
|
+
|
298
|
+
conf_file = find_option('config_file')
|
299
|
+
return {} if conf_file.nil? || conf_file.empty?
|
300
|
+
unless conf_file
|
301
|
+
warn "Invalid config_file type '#{conf_file.class}'. Must be a String."
|
302
|
+
return {}
|
303
|
+
end
|
304
|
+
|
305
|
+
mod_dir = find_option('module_dir')
|
306
|
+
if mod_dir && File.exist?(File.join(mod_dir, conf_file))
|
307
|
+
@config_from_file = load_config_file(File.join(mod_dir, conf_file))
|
308
|
+
elsif File.exist?(File.expand_path(conf_file))
|
309
|
+
@config_from_file = load_config_file(File.expand_path(conf_file))
|
310
|
+
else
|
311
|
+
err_msg = [
|
312
|
+
"Config file '#{File.expand_path(conf_file)}' does not exist.",
|
313
|
+
]
|
314
|
+
err_msg << "Config file '#{File.join(mod_dir, conf_file)}' does not exist." if mod_dir
|
315
|
+
raise err_msg.join("\n")
|
316
|
+
end
|
317
|
+
|
318
|
+
@config_from_file
|
319
|
+
end
|
320
|
+
|
321
|
+
def load_config_file(config_file)
|
322
|
+
return {} if config_file.nil? || config_file.empty? || !File.exist?(File.expand_path(config_file))
|
323
|
+
|
324
|
+
loaded = YAML.safe_load_file(File.expand_path(config_file), permitted_classes: [Regexp])
|
325
|
+
loaded.format!
|
326
|
+
loaded
|
327
|
+
end
|
328
|
+
|
329
|
+
def validate_config!
|
330
|
+
@config.each do |key, _value|
|
331
|
+
warn "Unknown config key: #{key}" unless KEYS.include?(key)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
def create_config_dirs!
|
336
|
+
FileUtils.mkdir_p(user_config_dir) unless Dir.exist?(user_config_dir)
|
337
|
+
FileUtils.cp_r(File.expand_path(File.join(__dir__, '..', 'terraform')), user_config_dir)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
@@ -1,66 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# This module holds extensions to Ruby and the Ruby stdlib
|
4
|
-
# Extensions related to deep_freeze were pulled from: https://gist.github.com/steakknife/1a37057b3b8539f4aca3
|
3
|
+
# This module holds extensions and refinements to Ruby and the Ruby stdlib
|
5
4
|
module CemAcpt::CoreExtensions
|
6
|
-
# DeepFreeze recursively freezes all keys and values in a hash
|
7
|
-
# Currently unused, but was used at one point and may be useful again
|
8
|
-
module DeepFreeze
|
9
|
-
# Holds deep_freeze extensions to Kernel
|
10
|
-
module Kernel
|
11
|
-
alias deep_freeze freeze
|
12
|
-
alias deep_frozen? frozen?
|
13
|
-
end
|
14
|
-
|
15
|
-
# Holds deep_freeze extensions to Enumerable
|
16
|
-
module Enumerable
|
17
|
-
def deep_freeze
|
18
|
-
unless @deep_frozen
|
19
|
-
each(&:deep_freeze)
|
20
|
-
@deep_frozen = true
|
21
|
-
end
|
22
|
-
freeze
|
23
|
-
end
|
24
|
-
|
25
|
-
def deep_frozen?
|
26
|
-
!!@deep_frozen
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
# Holds deep_freeze extensions to Hash
|
31
|
-
module Hash
|
32
|
-
def deep_freeze
|
33
|
-
transform_values! do |value|
|
34
|
-
value.respond_to?(:deep_freeze) ? value.deep_freeze : value.freeze
|
35
|
-
end
|
36
|
-
freeze
|
37
|
-
@deep_frozen = true
|
38
|
-
end
|
39
|
-
|
40
|
-
def deep_frozen?
|
41
|
-
!!@deep_frozen
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
# Holds deep_freeze extensions to OpenStruct
|
46
|
-
module OpenStruct
|
47
|
-
def deep_freeze
|
48
|
-
unless deep_frozen?
|
49
|
-
@table.reduce({}) do |h, (key, value)|
|
50
|
-
fkey = key.respond_to?(:deep_freeze) ? key.deep_freeze : key
|
51
|
-
fval = value.respond_to?(:deep_freeze) ? value.deep_freeze : value
|
52
|
-
h.merge(fkey => fval)
|
53
|
-
end.freeze
|
54
|
-
@deep_frozen = true
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def deep_frozen?
|
59
|
-
!!@deep_frozen
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
5
|
# Refines the Hash class with some convenience methods.
|
65
6
|
# Must call `using CemAcpt::CoreExtensions::HashExtensions`
|
66
7
|
# before these methods will be available.
|
@@ -83,6 +24,7 @@ module CemAcpt::CoreExtensions
|
|
83
24
|
def has?(path)
|
84
25
|
!!dot_dig(path)
|
85
26
|
end
|
27
|
+
alias dhas? has?
|
86
28
|
|
87
29
|
# Digs into a Hash using a dot-separated path.
|
88
30
|
# If the path is not found, returns nil.
|
@@ -91,7 +33,16 @@ module CemAcpt::CoreExtensions
|
|
91
33
|
# hash.dot_dig('a.b.c') # => 1
|
92
34
|
def dot_dig(path)
|
93
35
|
dig(*path.split('.').map(&:to_sym)) || dig(*path.split('.'))
|
36
|
+
rescue TypeError
|
37
|
+
# TypeError is raised if parts of the path don't have the #dig method
|
38
|
+
# This can happen if you do something like:
|
39
|
+
# hash = {a: {b: {c: 1}}}
|
40
|
+
# hash.dot_dig('a.b.c.d')
|
41
|
+
# The integer 1 doesn't have the #dig method, so we get a TypeError
|
42
|
+
# Since this means the path is invalid, we return nil
|
43
|
+
nil
|
94
44
|
end
|
45
|
+
alias dget dot_dig
|
95
46
|
|
96
47
|
# Stores a value in a nested Hash using a dot-separated path
|
97
48
|
# to dig through keys.
|
@@ -99,10 +50,15 @@ module CemAcpt::CoreExtensions
|
|
99
50
|
# hash = {a: {b: {c: 1}}}
|
100
51
|
# hash.dot_store('a.b.c', 2)
|
101
52
|
# hash #=> {a: {b: {c: 2}}}
|
53
|
+
# hash.dot_store('a.b.d', 3)
|
54
|
+
# hash #=> {a: {b: {c: 2, d: 3}}}
|
102
55
|
def dot_store(path, value)
|
103
56
|
*key, last = path.split('.').map(&:to_sym)
|
104
|
-
key.inject(self
|
57
|
+
key.inject(self) do |memo, k|
|
58
|
+
memo[k] ||= {}
|
59
|
+
end[last] = value
|
105
60
|
end
|
61
|
+
alias dset dot_store
|
106
62
|
end
|
107
63
|
end
|
108
64
|
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CemAcpt
|
4
|
+
module Goss
|
5
|
+
module Api
|
6
|
+
class ActionResponse
|
7
|
+
attr_reader :host, :action, :body
|
8
|
+
|
9
|
+
def initialize(host, action, status, body)
|
10
|
+
@host = host
|
11
|
+
@action = action
|
12
|
+
@status = status
|
13
|
+
@body = body
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)}>"
|
18
|
+
end
|
19
|
+
|
20
|
+
def inspect
|
21
|
+
to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_h
|
25
|
+
{
|
26
|
+
host: host,
|
27
|
+
action: action,
|
28
|
+
status: @status,
|
29
|
+
body: @body,
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def status
|
34
|
+
@status.to_i
|
35
|
+
end
|
36
|
+
alias http_status status
|
37
|
+
|
38
|
+
def success?
|
39
|
+
status == 200
|
40
|
+
end
|
41
|
+
|
42
|
+
def results
|
43
|
+
@results ||= @body['results'].map { |r| ActionResponseResult.new(r) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def results?
|
47
|
+
!results.nil? && !results.empty?
|
48
|
+
end
|
49
|
+
|
50
|
+
def summary
|
51
|
+
@summary ||= ActionResponseSummary.new(@body['summary'])
|
52
|
+
end
|
53
|
+
|
54
|
+
def summary?
|
55
|
+
!summary.nil? && !summary.empty?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
module DurationHandler
|
60
|
+
DURATION_UNITS = %i[nanoseconds milliseconds seconds].freeze
|
61
|
+
|
62
|
+
Duration = Struct.new(:duration, :unit, :round) do
|
63
|
+
def to_f
|
64
|
+
return @to_f if defined?(@to_f)
|
65
|
+
return 0.0 if duration.nil?
|
66
|
+
|
67
|
+
case unit
|
68
|
+
when :nanoseconds
|
69
|
+
@to_f = duration.to_f.round(round)
|
70
|
+
when :milliseconds
|
71
|
+
@to_f = (duration.to_f / 1_000_000).round(round)
|
72
|
+
when :seconds
|
73
|
+
@to_f = (duration.to_f / 1_000_000_000).round(round)
|
74
|
+
else
|
75
|
+
raise ArgumentError, "Invalid unit #{unit}, must be one of #{DURATION_UNITS}"
|
76
|
+
end
|
77
|
+
@to_f
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_s
|
81
|
+
return @to_s if defined?(@to_s)
|
82
|
+
|
83
|
+
case unit
|
84
|
+
when :nanoseconds
|
85
|
+
@to_s = "#{to_f}ns"
|
86
|
+
when :milliseconds
|
87
|
+
@to_s = "#{to_f}ms"
|
88
|
+
when :seconds
|
89
|
+
@to_s = "#{to_f}s"
|
90
|
+
else
|
91
|
+
raise ArgumentError, "Invalid unit #{unit}, must be one of #{DURATION_UNITS}"
|
92
|
+
end
|
93
|
+
@to_s
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# @param unit [Symbol] The unit to return the duration in
|
98
|
+
# @param round [Integer] The number of decimal places to round to
|
99
|
+
# @return [Duration] The Duration object
|
100
|
+
def duration(unit: :seconds, round: 3)
|
101
|
+
@all_durations ||= {}
|
102
|
+
@all_durations[unit] ||= {}
|
103
|
+
@all_durations[unit][round] ||= Duration.new(@duration, unit, round)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class ActionResponseSummary
|
108
|
+
include DurationHandler
|
109
|
+
|
110
|
+
attr_reader :failed_count, :summary_line, :test_count
|
111
|
+
|
112
|
+
def initialize(summary)
|
113
|
+
@summary = summary
|
114
|
+
@duration = @summary['total-duration']
|
115
|
+
@failed_count = @summary['failed-count']
|
116
|
+
@summary_line = @summary['summary-line']
|
117
|
+
@test_count = @summary['test-count']
|
118
|
+
end
|
119
|
+
alias to_s summary_line
|
120
|
+
alias total_duration duration
|
121
|
+
|
122
|
+
def to_h
|
123
|
+
@summary
|
124
|
+
end
|
125
|
+
|
126
|
+
def failed_percentage
|
127
|
+
@failed_percentage ||= (test_count.zero? ? 0.00 : (failed_count.to_f / test_count.to_f) * 100).round(2)
|
128
|
+
end
|
129
|
+
|
130
|
+
def passed_count
|
131
|
+
@passed_count ||= test_count - failed_count
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class ActionResponseResult
|
136
|
+
include DurationHandler
|
137
|
+
|
138
|
+
attr_reader(:duration, :err, :expected, :found, :human, :meta, :property,
|
139
|
+
:resource_id, :resource_type, :result, :skipped, :successful,
|
140
|
+
:summary_line, :test_type, :title)
|
141
|
+
|
142
|
+
def initialize(raw_result)
|
143
|
+
@raw_result = raw_result
|
144
|
+
@duration = @raw_result['duration']
|
145
|
+
@err = @raw_result['err']
|
146
|
+
@expected = @raw_result['expected']
|
147
|
+
@found = @raw_result['found']
|
148
|
+
@human = @raw_result['human']
|
149
|
+
@meta = @raw_result['meta']
|
150
|
+
@property = @raw_result['property']
|
151
|
+
@resource_id = @raw_result['resource-id']
|
152
|
+
@resource_type = @raw_result['resource-type']
|
153
|
+
@result = @raw_result['result']
|
154
|
+
@skipped = @raw_result['skipped']
|
155
|
+
@successful = @raw_result['successful']
|
156
|
+
@summary_line = @raw_result['summary-line']
|
157
|
+
@test_type = @raw_result['test-type']
|
158
|
+
@title = @raw_result['title']
|
159
|
+
end
|
160
|
+
alias error err
|
161
|
+
alias to_s summary_line
|
162
|
+
alias skipped? skipped
|
163
|
+
alias success? successful
|
164
|
+
|
165
|
+
def to_h
|
166
|
+
@raw_result
|
167
|
+
end
|
168
|
+
|
169
|
+
def error?
|
170
|
+
!err.nil?
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|