cem_acpt 0.1.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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CODEOWNERS +1 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +68 -0
- data/README.md +146 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/cem_acpt.gemspec +37 -0
- data/exe/cem_acpt +58 -0
- data/lib/cem_acpt/bootstrap/bootstrapper.rb +206 -0
- data/lib/cem_acpt/bootstrap/operating_system/rhel_family.rb +129 -0
- data/lib/cem_acpt/bootstrap/operating_system.rb +17 -0
- data/lib/cem_acpt/bootstrap.rb +12 -0
- data/lib/cem_acpt/context.rb +60 -0
- data/lib/cem_acpt/core_extensions.rb +111 -0
- data/lib/cem_acpt/image_name_builder.rb +104 -0
- data/lib/cem_acpt/logging.rb +193 -0
- data/lib/cem_acpt/platform/base/cmd.rb +65 -0
- data/lib/cem_acpt/platform/base.rb +78 -0
- data/lib/cem_acpt/platform/gcp/cmd.rb +313 -0
- data/lib/cem_acpt/platform/gcp/compute.rb +327 -0
- data/lib/cem_acpt/platform/gcp.rb +85 -0
- data/lib/cem_acpt/platform/vmpooler.rb +24 -0
- data/lib/cem_acpt/platform.rb +103 -0
- data/lib/cem_acpt/puppet_helpers.rb +38 -0
- data/lib/cem_acpt/runner.rb +304 -0
- data/lib/cem_acpt/shared_objects.rb +416 -0
- data/lib/cem_acpt/spec_helper_acceptance.rb +176 -0
- data/lib/cem_acpt/test_data.rb +157 -0
- data/lib/cem_acpt/utils.rb +70 -0
- data/lib/cem_acpt/version.rb +5 -0
- data/lib/cem_acpt.rb +27 -0
- data/sample_config.yaml +58 -0
- metadata +195 -0
@@ -0,0 +1,304 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent-ruby'
|
4
|
+
require 'json'
|
5
|
+
require 'open3'
|
6
|
+
|
7
|
+
module CemAcpt
|
8
|
+
require_relative 'bootstrap'
|
9
|
+
require_relative 'context'
|
10
|
+
require_relative 'logging'
|
11
|
+
require_relative 'puppet_helpers'
|
12
|
+
require_relative 'test_data'
|
13
|
+
require_relative 'utils'
|
14
|
+
|
15
|
+
# RunHandler orchestrates the acceptance test suites, including
|
16
|
+
# creating Runner objects, handling input and output, and exception
|
17
|
+
# handling.
|
18
|
+
class RunHandler
|
19
|
+
include CemAcpt::Logging
|
20
|
+
|
21
|
+
attr_accessor :params, :config_file
|
22
|
+
|
23
|
+
# @param params [Hash] the parameters passed from the command line
|
24
|
+
def initialize(params)
|
25
|
+
@params = params
|
26
|
+
@config_file = params[:config_file] || File.expand_path('./cem_acpt_config.yaml')
|
27
|
+
@module_pkg_path = Concurrent::IVar.new
|
28
|
+
@start_time = nil
|
29
|
+
@runners = Concurrent::Array.new
|
30
|
+
@results = Concurrent::Map.new
|
31
|
+
end
|
32
|
+
|
33
|
+
# Runs the acceptance test suites.
|
34
|
+
def run
|
35
|
+
@start_time = Time.now
|
36
|
+
logger.info("Running acceptance test suite at #{@start_time}")
|
37
|
+
logger.debug("Params: #{@params}")
|
38
|
+
logger.debug("Config file: #{@config_file}")
|
39
|
+
logger.info("Using module directory: #{@params[:module_dir]}")
|
40
|
+
context_opts = {
|
41
|
+
config_opts: @params,
|
42
|
+
config_file: @config_file,
|
43
|
+
}
|
44
|
+
build_module_package
|
45
|
+
begin
|
46
|
+
keep_terminal_alive if params[:CI] || ENV['CI'] || ENV['GITHUB_ACTION']
|
47
|
+
create_and_start_runners(context_opts)
|
48
|
+
rescue StandardError, SignalException, SystemExit => e
|
49
|
+
handle_fatal_error(e)
|
50
|
+
ensure
|
51
|
+
@keep_terminal_alive&.exit
|
52
|
+
end
|
53
|
+
handle_test_results
|
54
|
+
exit_code = @runners.map(&:spec_exit_code).any? { |rc| rc != 0 } ? 1 : 0
|
55
|
+
exit exit_code
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Prints periods to the terminal in a single line to keep the terminal
|
61
|
+
# alive. This is used when running in CI mode.
|
62
|
+
def keep_terminal_alive
|
63
|
+
@keep_terminal_alive = Thread.new do
|
64
|
+
loop do
|
65
|
+
$stdout.print("|\r")
|
66
|
+
sleep(1)
|
67
|
+
$stdout.print("/\r")
|
68
|
+
sleep(1)
|
69
|
+
$stdout.print("-\r")
|
70
|
+
sleep(1)
|
71
|
+
$stdout.print("\\\r")
|
72
|
+
sleep(1)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return [String] the module directory that contains the acceptance tests
|
78
|
+
def module_dir
|
79
|
+
if @params.key?(:working_dir)
|
80
|
+
@params[:working_dir]
|
81
|
+
else
|
82
|
+
Dir.pwd
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Builds the module package in a separate thread.
|
87
|
+
# This thread is set to abort_on_exception so that any
|
88
|
+
# exceptions raised in the thread will be caught and
|
89
|
+
# handled by the main thread.
|
90
|
+
def build_module_package
|
91
|
+
pkg_thread = Thread.new do
|
92
|
+
pkg_path = CemAcpt::PuppetHelpers::Module.build_module_package(@params[:module_dir])
|
93
|
+
@module_pkg_path.set(pkg_path)
|
94
|
+
end
|
95
|
+
pkg_thread.abort_on_exception = true
|
96
|
+
end
|
97
|
+
|
98
|
+
# Creates and starts Runner objects for each node in the acceptance test suites.
|
99
|
+
# @param context_opts [Hash] the options to pass to the Context object
|
100
|
+
# @param timeout [Integer] the timeout to use for the Runner object threads in seconds
|
101
|
+
def create_and_start_runners(context_opts, timeout = 600)
|
102
|
+
CemAcpt::Context.with(**context_opts) do |nodes, conf, tdata, node_inv|
|
103
|
+
nodes.each do |node|
|
104
|
+
runner = CemAcpt::Runner.new(node, conf, tdata, node_inv, @module_pkg_path, @results)
|
105
|
+
runner.start
|
106
|
+
@runners << runner
|
107
|
+
end
|
108
|
+
@runners.each { |r| r.join(timeout) }
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Handles how test results are logged.
|
113
|
+
def handle_test_results
|
114
|
+
@results.each_pair do |node, result|
|
115
|
+
logger.info("SUMMARY: #{result['summary_line']} on node #{node}")
|
116
|
+
next unless test_failures?(result)
|
117
|
+
|
118
|
+
failed = result['examples'].reject { |e| e['status'] == 'passed' }
|
119
|
+
failed.each do |e|
|
120
|
+
logger.error(test_error_msg(node, e))
|
121
|
+
end
|
122
|
+
end
|
123
|
+
debug_test_results if logger.debug?
|
124
|
+
end
|
125
|
+
|
126
|
+
# Formats a test result for tests that have failed. Is used for logging.
|
127
|
+
# @param node [String] the name of the node the test ran on
|
128
|
+
# @param result [Hash] the test result to format
|
129
|
+
# @return [String] the formatted test result
|
130
|
+
def test_error_msg(node, result)
|
131
|
+
[
|
132
|
+
"TEST FAILED: #{result['id']}",
|
133
|
+
"DESCRIPTION: #{result['full_description']}",
|
134
|
+
"STATUS: #{result['status']}",
|
135
|
+
"LOCATION: #{result['file_path']}:#{result['line_number']}",
|
136
|
+
"NODE: #{node}",
|
137
|
+
result['exception']['message'],
|
138
|
+
"\n",
|
139
|
+
].join("\n")
|
140
|
+
end
|
141
|
+
|
142
|
+
# Logs performance data for the acceptance test suites.
|
143
|
+
# This is only logged if the log level is set to debug.
|
144
|
+
def debug_test_results
|
145
|
+
examples_by_time = []
|
146
|
+
@results.each_pair do |node, result|
|
147
|
+
result['examples'].each do |e|
|
148
|
+
examples_by_time << [e['run_time'], e['id'], e['status'], e['line_number'], node]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
logger.debug('Showing test results in order of execution time...')
|
152
|
+
examples_by_time.sort_by(&:first).reverse.each do |e|
|
153
|
+
logger.debug("RUNTIME: #{e[0]}; ID: #{e[1]}; STATUS: #{e[2]}; LINE: #{e[3]}; HOST: #{e[4]};")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Checks for failures in the test results.
|
158
|
+
# @param result [Hash] the test result to check
|
159
|
+
# @return [Boolean] whether or not there are test failures in result
|
160
|
+
def test_failures?(result)
|
161
|
+
result['summary']['failure_count'].positive? || result['summary']['errors_outside_of_examples_count'].positive?
|
162
|
+
end
|
163
|
+
|
164
|
+
# Gracefully handles a fatal error and exits the program.
|
165
|
+
# @param err [StandardError, Exception] the error that caused the fatal error
|
166
|
+
def handle_fatal_error(err)
|
167
|
+
@keep_terminal_alive&.exit
|
168
|
+
logger.fatal("Fatal error: #{err.message}")
|
169
|
+
logger.debug(err.backtrace.join('; '))
|
170
|
+
kill_runners
|
171
|
+
logger.fatal("Exiting with status 1 after #{Time.now - @start_time} seconds")
|
172
|
+
exit(1)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Kills all running Runner objects.
|
176
|
+
def kill_runners
|
177
|
+
@runners.each(&:kill) unless @runners.empty?
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Runner is a class that runs a single acceptance test suite on a single node.
|
182
|
+
# It is responsible for managing the lifecycle of the test suite and
|
183
|
+
# reporting the results back to the main thread. Runner objects are created
|
184
|
+
# by the RunHandler and then, when started, execute their logic in a thread.
|
185
|
+
class Runner
|
186
|
+
include CemAcpt::Logging
|
187
|
+
|
188
|
+
attr_reader :spec_exit_code
|
189
|
+
|
190
|
+
# @param node [String] the name of the node to run the acceptance test suite on
|
191
|
+
# @param conf [CemAcpt::Config] the acceptance test suite configuration
|
192
|
+
# @param tdata [Hash] the test data to use for the acceptance test suite
|
193
|
+
# @param node_inv [CemAcpt::NodeInventory] the node inventory to use for the acceptance test suite
|
194
|
+
# @param module_pkg_path [Concurrent::IVar] the path to the module package
|
195
|
+
# @param results [Concurrent::Map] the results map to use for reporting test results
|
196
|
+
def initialize(node, conf, tdata, node_inv, module_pkg_path, results)
|
197
|
+
@node = node
|
198
|
+
@conf = conf
|
199
|
+
@tdata = tdata
|
200
|
+
@node_inv = node_inv
|
201
|
+
@module_pkg_path = module_pkg_path
|
202
|
+
@results = results
|
203
|
+
@spec_exit_code = 0
|
204
|
+
validate!
|
205
|
+
end
|
206
|
+
|
207
|
+
# Starts the runner thread and runs the lifecycle stages of the
|
208
|
+
# acceptance test suite.
|
209
|
+
def start
|
210
|
+
logger.info("Starting test suite for #{@node.node_name}")
|
211
|
+
@thread = Thread.new do
|
212
|
+
provision
|
213
|
+
bootstrap
|
214
|
+
run_tests
|
215
|
+
destroy
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Joins the runner thread.
|
220
|
+
# @param timeout [Integer] thread timeout in seconds
|
221
|
+
def join(timeout = 600)
|
222
|
+
@thread.join(timeout)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Runner thread exits and runner node is destroyed, if it exists.
|
226
|
+
def exit
|
227
|
+
@thread.exit
|
228
|
+
@node&.destroy
|
229
|
+
end
|
230
|
+
|
231
|
+
# Runner thread is killed and runner node is destroyed, if it exists.
|
232
|
+
def kill
|
233
|
+
@thread.kill
|
234
|
+
@node&.destroy
|
235
|
+
end
|
236
|
+
|
237
|
+
private
|
238
|
+
|
239
|
+
# Provisions the node for the acceptance test suite.
|
240
|
+
def provision
|
241
|
+
logger.info("Provisioning #{@node.node_name}...")
|
242
|
+
start_time = Time.now
|
243
|
+
@node.provision
|
244
|
+
sleep(1) until @node.ready?
|
245
|
+
logger.info("Node #{@node.node_name} is ready...")
|
246
|
+
node_desc = {
|
247
|
+
test_data: @node.test_data,
|
248
|
+
platform: @conf.get('platform'),
|
249
|
+
local_port: @node.local_port,
|
250
|
+
}.merge(@node.node)
|
251
|
+
@node_inv.add(@node.node_name, node_desc)
|
252
|
+
logger.info("Node #{@node.node_name} provisioned in #{Time.now - start_time} seconds")
|
253
|
+
@node_inv.save(File.join(@conf.get('module_dir'), 'spec', 'fixtures', 'node_inventory.yaml'))
|
254
|
+
end
|
255
|
+
|
256
|
+
# Bootstraps the node for the acceptance test suite. Currently, this
|
257
|
+
# just uploads and installs the module package.
|
258
|
+
def bootstrap
|
259
|
+
logger.info("Bootstrapping #{@node.node_name}...")
|
260
|
+
until File.exist?(@module_pkg_path.value)
|
261
|
+
logger.debug("Waiting for module package #{@module_pkg_path.value} to exist...")
|
262
|
+
sleep(1)
|
263
|
+
end
|
264
|
+
logger.info("Installing module package #{@module_pkg_path.value}...")
|
265
|
+
@node.install_puppet_module_package(@module_pkg_path.value)
|
266
|
+
end
|
267
|
+
|
268
|
+
# Runs the acceptance test suite via rspec.
|
269
|
+
def run_tests
|
270
|
+
require 'json'
|
271
|
+
|
272
|
+
logger.info("Running tests for #{@node.node_name}...")
|
273
|
+
stdout = nil
|
274
|
+
stderr = nil
|
275
|
+
# ENV['RSPEC_DEBUG'] = 'true' if @conf.get('log_level') == 'debug'
|
276
|
+
test_command = "cd #{@conf.get('module_dir')} && bundle exec rspec #{@node.test_data[:test_file]} --format json"
|
277
|
+
@node.run_tests do
|
278
|
+
stdout, stderr, status = Open3.capture3(test_command)
|
279
|
+
@spec_exit_code = status.exitstatus
|
280
|
+
end
|
281
|
+
logger.info("Tests completed with exit code: #{@spec_exit_code}")
|
282
|
+
@results.put_if_absent(@node.node_name, JSON.parse(stdout))
|
283
|
+
end
|
284
|
+
|
285
|
+
# Destroys the node for the acceptance test suite.
|
286
|
+
def destroy
|
287
|
+
if @conf.get('no_destroy_nodes')
|
288
|
+
logger.info("Not destroying node #{@node.node_name} because 'no_destroy_nodes' is set to true")
|
289
|
+
return
|
290
|
+
end
|
291
|
+
logger.info("Destroying #{@node.node_name}...")
|
292
|
+
@node.destroy
|
293
|
+
logger.info("Node #{@node.node_name} destroyed successfully")
|
294
|
+
end
|
295
|
+
|
296
|
+
# Validates the runner configuration.
|
297
|
+
def validate!
|
298
|
+
raise 'No node provided' unless @node
|
299
|
+
raise 'No config provided' unless @conf
|
300
|
+
raise 'No test data provided' unless @tdata
|
301
|
+
raise 'No node inventory provided' unless @node_inv
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|