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
@@ -1,184 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module CemAcpt
|
4
|
-
require_relative 'shared_objects'
|
5
|
-
|
6
|
-
# This methods is used in spec_helper_acceptance.rb to set up
|
7
|
-
# baseline configurations used in acceptance tests.
|
8
|
-
def self.configure_spec_helper!
|
9
|
-
require 'serverspec'
|
10
|
-
require 'yaml'
|
11
|
-
|
12
|
-
RSpec.configure do |config|
|
13
|
-
config.include CemAcpt::SpecHelperAcceptance
|
14
|
-
config.extend CemAcpt::SpecHelperAcceptance
|
15
|
-
config.add_setting :acpt_test_data, default: {}
|
16
|
-
config.add_setting :acpt_node_inventory
|
17
|
-
config.threadsafe = true
|
18
|
-
config.color_mode = :off
|
19
|
-
config.fail_fast = false
|
20
|
-
end
|
21
|
-
|
22
|
-
node_inventory = NodeInventory.new
|
23
|
-
node_inventory.load
|
24
|
-
RSpec.configuration.acpt_node_inventory = node_inventory
|
25
|
-
end
|
26
|
-
|
27
|
-
# This module provides methods used in accpetance tests.
|
28
|
-
module SpecHelperAcceptance
|
29
|
-
# This method must be used inside of a `describe` block as the first
|
30
|
-
# statement. This prepares the ServerSpec configuration for the test node
|
31
|
-
# and sets up the test data.
|
32
|
-
def initialize_test_environment!
|
33
|
-
test_file = caller_locations.first.path
|
34
|
-
|
35
|
-
node_name, node_data = RSpec.configuration.acpt_node_inventory.get_by_property('test_data.test_file', test_file)
|
36
|
-
raise "Failed to get node data for node #{node_name}" unless node_data
|
37
|
-
raise "Node data format is incorrect: #{node_data}" unless node_data.is_a?(Hash)
|
38
|
-
|
39
|
-
backend = nil
|
40
|
-
host = nil
|
41
|
-
ssh_options = nil
|
42
|
-
puppet_path = nil
|
43
|
-
|
44
|
-
# Set remote communication variables based on transport type
|
45
|
-
case node_data[:transport]
|
46
|
-
when /ssh/
|
47
|
-
backend = :ssh
|
48
|
-
host = node_data[:node_name]
|
49
|
-
ssh_options = node_data[:ssh_opts]
|
50
|
-
sudo_options = '-n -u root -i'
|
51
|
-
puppet_path = '/opt/puppetlabs/bin/puppet'
|
52
|
-
when /winrm/
|
53
|
-
backend = :winrm
|
54
|
-
puppet_path = 'C:\Program Files\Puppet Labs\Puppet\bin\puppet.bat'
|
55
|
-
else
|
56
|
-
raise "Unknown transport: #{node[:transport]}"
|
57
|
-
end
|
58
|
-
|
59
|
-
# Set serverspec transport options and host for remote communication.
|
60
|
-
set :backend, backend
|
61
|
-
set :host, host
|
62
|
-
set(:ssh_options, ssh_options) if ssh_options
|
63
|
-
set(:sudo_options, sudo_options) if sudo_options
|
64
|
-
set(:os, family: 'windows') if backend == :winrm
|
65
|
-
|
66
|
-
# Get the command provider from the node's platform
|
67
|
-
# We add this as a RSpec config option so that we can use it in
|
68
|
-
# other functions.
|
69
|
-
require 'cem_acpt/platform'
|
70
|
-
|
71
|
-
acpt_test_data = {
|
72
|
-
test_file: test_file,
|
73
|
-
node_name: node_name,
|
74
|
-
node_data: node_data,
|
75
|
-
backend: backend,
|
76
|
-
platform: CemAcpt::Platform.get(node_data[:platform]),
|
77
|
-
puppet_path: puppet_path,
|
78
|
-
}
|
79
|
-
acpt_test_data[:host] = host if host
|
80
|
-
RSpec.configuration.acpt_test_data = acpt_test_data
|
81
|
-
end
|
82
|
-
|
83
|
-
# This method formats Puppet Apply options
|
84
|
-
def puppet_apply_options(opts = {})
|
85
|
-
if [opts[:catch_changes], opts[:expect_changes], opts[:catch_failures], opts[:expect_failures]].compact.length > 1
|
86
|
-
raise ArgumentError,
|
87
|
-
'Please specify only one of "catch_changes", "expect_changes", "catch_failures", or "expect_failures"'
|
88
|
-
end
|
89
|
-
|
90
|
-
apply_opts = {}.merge(opts)
|
91
|
-
|
92
|
-
if opts[:catch_changes]
|
93
|
-
apply_opts[:detailed_exit_codes] = true
|
94
|
-
apply_opts[:acceptable_exit_codes] = [0]
|
95
|
-
elsif opts[:catch_failures]
|
96
|
-
apply_opts[:detailed_exit_codes] = true
|
97
|
-
apply_opts[:acceptable_exit_codes] = [0, 2]
|
98
|
-
elsif opts[:expect_failures]
|
99
|
-
apply_opts[:detailed_exit_codes] = true
|
100
|
-
apply_opts[:acceptable_exit_codes] = [1, 4, 6]
|
101
|
-
elsif opts[:expect_changes]
|
102
|
-
apply_opts[:detailed_exit_codes] = true
|
103
|
-
apply_opts[:acceptable_exit_codes] = [2]
|
104
|
-
else
|
105
|
-
apply_opts[:detailed_exit_codes] = false
|
106
|
-
apply_opts[:acceptable_exit_codes] = [0]
|
107
|
-
end
|
108
|
-
apply_opts
|
109
|
-
end
|
110
|
-
|
111
|
-
# This methods handles formatting the output from Puppet apply.
|
112
|
-
def handle_puppet_apply_output(result, apply_opts)
|
113
|
-
exit_code = result.exitstatus
|
114
|
-
output = result.to_s
|
115
|
-
if apply_opts[:catch_changes] && !apply_opts[:acceptable_exit_codes].include?(exit_code)
|
116
|
-
failure = <<~ERROR
|
117
|
-
Apply manifest expected no changes. Puppet Apply returned exit code #{exit_code}
|
118
|
-
====== Start output of Puppet apply with unexpected changes ======
|
119
|
-
#{output}
|
120
|
-
====== End output of Puppet apply with unexpected changes ======
|
121
|
-
ERROR
|
122
|
-
raise failure
|
123
|
-
elsif !apply_opts[:acceptable_exit_codes].include?(exit_code)
|
124
|
-
failure = <<~ERROR
|
125
|
-
Apply manifest failed with exit code #{exit_code} (expected: #{apply_opts[:acceptable_exit_codes]})
|
126
|
-
====== Start output of failed Puppet apply ======
|
127
|
-
#{output}
|
128
|
-
====== End output of failed Puppet apply ======
|
129
|
-
ERROR
|
130
|
-
raise failure
|
131
|
-
end
|
132
|
-
|
133
|
-
yield result if block_given?
|
134
|
-
|
135
|
-
if ENV['RSPEC_DEBUG']
|
136
|
-
run_result = <<~RUNRES
|
137
|
-
apply manifest succeded with status #{exit_code}
|
138
|
-
===== Start output of successful Puppet apply ======
|
139
|
-
#{output}
|
140
|
-
===== End output of successful Puppet apply ======
|
141
|
-
RUNRES
|
142
|
-
puts run_result
|
143
|
-
end
|
144
|
-
result
|
145
|
-
end
|
146
|
-
|
147
|
-
# This method runs a shell command on the test node.
|
148
|
-
def run_shell(cmd, opts = {})
|
149
|
-
cmd = cmd.join(' ') if cmd.is_a?(Array)
|
150
|
-
|
151
|
-
host = RSpec.configuration.acpt_test_data[:node_name]
|
152
|
-
opts[:ssh_opts] = RSpec.configuration.acpt_test_data[:node_data][:ssh_opts]
|
153
|
-
RSpec.configuration.acpt_test_data[:platform].run_shell(host, cmd, opts)
|
154
|
-
end
|
155
|
-
|
156
|
-
# This method runs puppet apply on the test node using the provided manifest.
|
157
|
-
def apply_manifest(manifest, opts = {})
|
158
|
-
host = RSpec.configuration.acpt_test_data[:node_name]
|
159
|
-
opts = {
|
160
|
-
apply: puppet_apply_options(opts),
|
161
|
-
ssh_opts: RSpec.configuration.acpt_test_data[:node_data][:ssh_opts],
|
162
|
-
puppet_path: RSpec.configuration.acpt_test_data[:puppet_path],
|
163
|
-
}
|
164
|
-
result = RSpec.configuration.acpt_test_data[:platform].apply_manifest(host, manifest, opts)
|
165
|
-
handle_puppet_apply_output(result, opts[:apply])
|
166
|
-
end
|
167
|
-
|
168
|
-
# This method runs puppet apply on the test node using the provided manifest twice and
|
169
|
-
# asserts that the second run has no changes.
|
170
|
-
def idempotent_apply(manifest, opts = {})
|
171
|
-
opts.reject! { |k, _| %i[catch_changes expect_changes catch_failures expect_failures].include?(k) }
|
172
|
-
begin
|
173
|
-
apply_manifest(manifest, opts.merge({ catch_failures: true }))
|
174
|
-
rescue StandardError => e
|
175
|
-
raise "Idempotent apply failed during first apply: #{e.message}\n#{e.backtrace.join("\n")}"
|
176
|
-
end
|
177
|
-
begin
|
178
|
-
apply_manifest(manifest, opts.merge({ catch_changes: true }))
|
179
|
-
rescue StandardError => e
|
180
|
-
raise "Idempotent apply failed during second apply: #{e.message}\n#{e.backtrace.join("\n")}"
|
181
|
-
end
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
@@ -1,187 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'concurrent-ruby'
|
4
|
-
require 'open3'
|
5
|
-
require_relative '../context'
|
6
|
-
require_relative '../logging'
|
7
|
-
require_relative '../puppet_helpers'
|
8
|
-
require_relative 'runner'
|
9
|
-
|
10
|
-
module CemAcpt
|
11
|
-
module TestRunner
|
12
|
-
# Raised when the RunHandler needs to be operating within the module directory and isn't
|
13
|
-
class RunHandlerNotInModuleDirError < StandardError; end
|
14
|
-
|
15
|
-
# Raised when the RunHandler executes a system command that fails
|
16
|
-
class RunHandlerSystemCommandError < StandardError; end
|
17
|
-
|
18
|
-
# RunHandler orchestrates the acceptance test suites, including
|
19
|
-
# creating Runner objects, handling input and output, and exception
|
20
|
-
# handling.
|
21
|
-
class RunHandler
|
22
|
-
include CemAcpt::Logging
|
23
|
-
|
24
|
-
attr_accessor :run_context
|
25
|
-
|
26
|
-
# @param run_context [CemAcpt::Context::Ctx] The context object for this run
|
27
|
-
def initialize(run_context)
|
28
|
-
@run_context = run_context
|
29
|
-
@module_pkg_path = Concurrent::IVar.new
|
30
|
-
@runners = Concurrent::Array.new
|
31
|
-
@thread_pool = Concurrent::SimpleExecutorService.new
|
32
|
-
end
|
33
|
-
|
34
|
-
# Gets the overall exit code for all runners
|
35
|
-
def exit_code
|
36
|
-
@runners&.map { |r| r.run_result.exit_status }&.all?(&:zero?) ? 0 : 1
|
37
|
-
end
|
38
|
-
|
39
|
-
# Runs the acceptance test suites.
|
40
|
-
def run
|
41
|
-
logger.info('RUN HANDLER: Creating and starting test runners...')
|
42
|
-
create_runners
|
43
|
-
logger.info("RUN HANDLER: Created #{@runners.length} runners...")
|
44
|
-
@runners.map { |r| @thread_pool.post { r.start } }
|
45
|
-
@thread_pool.shutdown
|
46
|
-
@thread_pool.wait_for_termination
|
47
|
-
@thread_pool = Concurrent::SimpleExecutorService.new
|
48
|
-
logger.info('Test runners have all exited...')
|
49
|
-
rescue StandardError => e
|
50
|
-
handle_fatal_error(e)
|
51
|
-
ensure
|
52
|
-
handle_test_results
|
53
|
-
end
|
54
|
-
|
55
|
-
def destroy_test_nodes
|
56
|
-
return if @runners.empty? || @runners.none?(&:node_exists)
|
57
|
-
|
58
|
-
@runners.each do |r|
|
59
|
-
r.send(:destroy) if r.node_exists
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
private
|
64
|
-
|
65
|
-
# Creates and starts Runner objects for each node in the acceptance test suites.
|
66
|
-
def create_runners
|
67
|
-
@run_context.nodes.each do |platform, nodes|
|
68
|
-
@runners += nodes.map { |node| create_runner(node, platform) }.compact
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
def create_runner(node, platform)
|
73
|
-
logger.info("RUN HANDLER: Creating runner for #{node.test_data[:test_name]} on node #{node.node_name}...")
|
74
|
-
runner = CemAcpt::TestRunner::Runner.new(node, @run_context, platform)
|
75
|
-
runner || runner_creation_error(node)
|
76
|
-
end
|
77
|
-
|
78
|
-
def runner_creation_error(node)
|
79
|
-
msg = [
|
80
|
-
"Failed to create runner object for node #{node.node_name}.",
|
81
|
-
"Cannot run test #{node.test_data[:test_name]}",
|
82
|
-
].join(' ')
|
83
|
-
logger.error(msg)
|
84
|
-
nil
|
85
|
-
end
|
86
|
-
|
87
|
-
# Handles how test results are logged.
|
88
|
-
def handle_test_results
|
89
|
-
@runners.each do |runner|
|
90
|
-
result = runner.run_result.to_h
|
91
|
-
logger << "::group::{Results for #{runner.node.test_data[:test_name]}}\n"
|
92
|
-
if result.key?('summary_line')
|
93
|
-
logger.info("SUMMARY: #{result['summary_line']} for test #{runner.node.test_data[:test_name]}")
|
94
|
-
else
|
95
|
-
handle_runner_error_results(runner)
|
96
|
-
logger << "::endgroup::\n"
|
97
|
-
next
|
98
|
-
end
|
99
|
-
unless runner.test_failures?
|
100
|
-
logger << "::endgroup::\n"
|
101
|
-
next
|
102
|
-
end
|
103
|
-
|
104
|
-
if result.key?('examples') # Log errors outside of examples
|
105
|
-
if result['examples'].empty? && !result['messages'].empty?
|
106
|
-
logger.error(result['messages'].join("\n"))
|
107
|
-
else
|
108
|
-
failed = result['examples'].reject { |e| e['status'] == 'passed' }
|
109
|
-
failed.each do |e|
|
110
|
-
logger.error(test_error_msg(runner.node.node_name, e))
|
111
|
-
end
|
112
|
-
end
|
113
|
-
else
|
114
|
-
handle_runner_error_results(runner)
|
115
|
-
end
|
116
|
-
#debug_test_results(runner) if logger.debug?
|
117
|
-
logger << "::endgroup::\n"
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
# Handles logging the results of the runners that errored.
|
122
|
-
def handle_runner_error_results(runner)
|
123
|
-
logger.error("SUMMARY: Encountered an error with test #{runner.node.test_data[:test_name]} on node #{runner.node.node_name}")
|
124
|
-
runner.run_result.to_h.each do |k, v|
|
125
|
-
logger.error("#{k.upcase}: #{v}")
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
# Formats the error message for a runner that failed.
|
130
|
-
# @param node [Object] the node that failed
|
131
|
-
# @param err [Object] the error that occured
|
132
|
-
# @return [String] the formatted error message
|
133
|
-
def runner_error_msg(node, err)
|
134
|
-
[
|
135
|
-
"Error while running acceptance tests on node #{node.node_name}",
|
136
|
-
"Error: #{err.message}",
|
137
|
-
'Backtrace:',
|
138
|
-
err.backtrace.join("\n"),
|
139
|
-
].join("\n")
|
140
|
-
end
|
141
|
-
|
142
|
-
# Formats a test result for tests that have failed. Is used for logging.
|
143
|
-
# @param node [String] the name of the node the test ran on
|
144
|
-
# @param result [Hash] the test result to format
|
145
|
-
# @return [String] the formatted test result
|
146
|
-
def test_error_msg(node, result)
|
147
|
-
[
|
148
|
-
"TEST FAILED: #{result['id']}",
|
149
|
-
"DESCRIPTION: #{result['full_description']}",
|
150
|
-
"STATUS: #{result['status']}",
|
151
|
-
"LOCATION: #{result['file_path']}:#{result['line_number']}",
|
152
|
-
"NODE: #{node}",
|
153
|
-
result['exception']['message'],
|
154
|
-
"\n",
|
155
|
-
].join("\n")
|
156
|
-
end
|
157
|
-
|
158
|
-
# Logs performance data for the acceptance test suites.
|
159
|
-
# This is only logged if the log level is set to debug.
|
160
|
-
# def debug_test_results(runner)
|
161
|
-
# examples_by_time = []
|
162
|
-
# runner.run_result.to_h.each do |node, result|
|
163
|
-
# next unless result['examples']
|
164
|
-
|
165
|
-
# result['examples'].each do |e|
|
166
|
-
# examples_by_time << [e['run_time'], e['id'], e['status'], e['line_number'], node]
|
167
|
-
# end
|
168
|
-
# end
|
169
|
-
# if examples_by_time
|
170
|
-
# logger.debug('Showing test results in order of execution time...')
|
171
|
-
# examples_by_time.sort_by(&:first).reverse.each do |e|
|
172
|
-
# logger.debug("RUNTIME: #{e[0]}; ID: #{e[1]}; STATUS: #{e[2]}; LINE: #{e[3]}; HOST: #{e[4]};")
|
173
|
-
# end
|
174
|
-
# else
|
175
|
-
# logger.debug('No debug results to show')
|
176
|
-
# end
|
177
|
-
# end
|
178
|
-
|
179
|
-
# Gracefully handles a fatal error and exits the program.
|
180
|
-
# @param err [StandardError, Exception] the error that caused the fatal error
|
181
|
-
def handle_fatal_error(err)
|
182
|
-
logger.fatal("RUN HANDLER: Fatal error: #{err.message}")
|
183
|
-
logger.debug(err.backtrace.join("\n"))
|
184
|
-
end
|
185
|
-
end
|
186
|
-
end
|
187
|
-
end
|
@@ -1,210 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'concurrent-ruby'
|
4
|
-
require 'English'
|
5
|
-
require_relative '../logging'
|
6
|
-
require_relative '../rspec_utils'
|
7
|
-
require_relative 'runner_result'
|
8
|
-
|
9
|
-
module CemAcpt
|
10
|
-
module TestRunner
|
11
|
-
class RunnerError < StandardError; end
|
12
|
-
# Error used to wrap fatal errors raised in Runner steps
|
13
|
-
class RunnerStepError < StandardError
|
14
|
-
attr_reader :step
|
15
|
-
|
16
|
-
def initialize(step, err)
|
17
|
-
@step = step
|
18
|
-
super err
|
19
|
-
set_backtrace err.backtrace if err.respond_to?(:backtrace)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
class RunnerProvisionError < RunnerStepError; end
|
23
|
-
|
24
|
-
# Runner is a class that runs a single acceptance test suite on a single node.
|
25
|
-
# It is responsible for managing the lifecycle of the test suite and
|
26
|
-
# reporting the results back to the main thread. Runner objects are created
|
27
|
-
# by the RunHandler and then, when started, execute their logic in a thread.
|
28
|
-
class Runner
|
29
|
-
include CemAcpt::LoggingAsync
|
30
|
-
|
31
|
-
attr_reader :node, :node_exists, :run_result
|
32
|
-
|
33
|
-
# @param node [String] the name of the node to run the acceptance test suite on
|
34
|
-
# @param ctx [CemAcpt::RunnerCtx] a cem_acpt Ctx (context) object
|
35
|
-
# @param module_pkg_path [Concurrent::IVar] the path to the module package
|
36
|
-
def initialize(node, context, platform)
|
37
|
-
@node = node
|
38
|
-
@context = context
|
39
|
-
@platform = platform
|
40
|
-
@debug_mode = @context.config.debug_mode?
|
41
|
-
@node_inventory = @context.node_inventory
|
42
|
-
@module_pkg_path = @context.module_package_path
|
43
|
-
@node_exists = false
|
44
|
-
@run_result = CemAcpt::TestRunner::RunnerResult.new(@node, debug: @debug_mode)
|
45
|
-
@completed_steps = []
|
46
|
-
validate!
|
47
|
-
end
|
48
|
-
|
49
|
-
def run_step(step_sym)
|
50
|
-
send(step_sym)
|
51
|
-
@completed_steps << step_sym
|
52
|
-
rescue StandardError => e
|
53
|
-
err = CemAcpt::TestRunner::RunnerStepError.new(step_sym, e)
|
54
|
-
step_error_logging(err)
|
55
|
-
@run_result.from_error(err)
|
56
|
-
destroy unless step_sym == :destroy
|
57
|
-
end
|
58
|
-
|
59
|
-
# Executes test suite steps
|
60
|
-
def start
|
61
|
-
async_info("Starting test suite for #{@node.node_name}", log_prefix('RUNNER'))
|
62
|
-
run_step(:provision)
|
63
|
-
run_step(:bootstrap)
|
64
|
-
run_step(:run_tests)
|
65
|
-
run_step(:destroy)
|
66
|
-
true
|
67
|
-
rescue StandardError => e
|
68
|
-
step_error_logging(e)
|
69
|
-
@run_result.from_error(e)
|
70
|
-
destroy
|
71
|
-
end
|
72
|
-
|
73
|
-
# Checks for failures in the test results.
|
74
|
-
# @param result [Hash] the test result to check
|
75
|
-
# @return [Boolean] whether or not there are test failures in result
|
76
|
-
def test_failures?
|
77
|
-
@run_result.result_errors? || @run_result.result_failures?
|
78
|
-
end
|
79
|
-
|
80
|
-
private
|
81
|
-
|
82
|
-
def step_error_logging(err)
|
83
|
-
prefix = err.respond_to?(:step) ? log_prefix(err.step.capitalize) : log_prefix('RUNNER')
|
84
|
-
fatal_msg = ["runner failed: #{err.message}"]
|
85
|
-
async_fatal(fatal_msg, prefix)
|
86
|
-
async_debug("Completed steps: #{@completed_steps}", prefix)
|
87
|
-
async_debug("Failed runner backtrace:\n#{err.backtrace.join("\n")}", prefix)
|
88
|
-
async_debug("Failed runner test data: #{@node.test_data}", prefix)
|
89
|
-
end
|
90
|
-
|
91
|
-
def log_prefix(prefix)
|
92
|
-
"#{prefix}: #{@node.test_data[:test_name]}:"
|
93
|
-
end
|
94
|
-
|
95
|
-
# Provisions the node for the acceptance test suite.
|
96
|
-
def provision
|
97
|
-
async_info("Provisioning #{@node.node_name}...", log_prefix('PROVISION'))
|
98
|
-
start_time = Time.now
|
99
|
-
@node.provision
|
100
|
-
@node_exists = true
|
101
|
-
max_retries = 60 # equals 300 seconds because we check every five seconds
|
102
|
-
until @node.ready?
|
103
|
-
if max_retries <= 0
|
104
|
-
async_fatal("Node #{@node.node_name} failed to provision", log_prefix('PROVISION'))
|
105
|
-
raise CemAcpt::TestRunner::RunnerProvisionError, "Provisioning timed out for node #{@node.node_name}"
|
106
|
-
end
|
107
|
-
|
108
|
-
async_info("Waiting for #{@node.node_name} to be ready for remote connections...", log_prefix('PROVISION'))
|
109
|
-
max_retries -= 1
|
110
|
-
sleep(5)
|
111
|
-
end
|
112
|
-
async_info("Node #{@node.node_name} is ready...", log_prefix('PROVISION'))
|
113
|
-
node_desc = {
|
114
|
-
test_data: @node.test_data,
|
115
|
-
platform: @platform,
|
116
|
-
local_port: @node.local_port,
|
117
|
-
}.merge(@node.node)
|
118
|
-
@node_inventory.add(@node.node_name, node_desc)
|
119
|
-
@node_inventory.save
|
120
|
-
async_info("Node #{@node.node_name} provisioned in #{Time.now - start_time} seconds", log_prefix('PROVISION'))
|
121
|
-
end
|
122
|
-
|
123
|
-
# Bootstraps the node for the acceptance test suite. Currently, this
|
124
|
-
# just uploads and installs the module package.
|
125
|
-
def bootstrap
|
126
|
-
async_info("Bootstrapping #{@node.node_name}...", log_prefix('BOOTSTRAP'))
|
127
|
-
until File.exist?(@module_pkg_path)
|
128
|
-
async_debug("Waiting for module package #{@module_pkg_path} to exist...", log_prefix('BOOTSTRAP'))
|
129
|
-
sleep(1)
|
130
|
-
end
|
131
|
-
async_info("Installing module package #{@module_pkg_path}...", log_prefix('BOOTSTRAP'))
|
132
|
-
@node.install_puppet_module_package(@module_pkg_path)
|
133
|
-
end
|
134
|
-
|
135
|
-
# Runs the acceptance test suite via rspec.
|
136
|
-
def run_tests
|
137
|
-
attempts = 0
|
138
|
-
until File.exist?(@node_inventory.save_file_path)
|
139
|
-
raise 'Node inventory file not found' if (attempts += 1) > 3
|
140
|
-
|
141
|
-
sleep(1)
|
142
|
-
end
|
143
|
-
async_info("Running test #{@node.test_data[:test_name]} on node #{@node.node_name}...", log_prefix('RSPEC'))
|
144
|
-
@node.run_tests do |cmd_env|
|
145
|
-
cmd_opts = rspec_opts
|
146
|
-
cmd_opts[:env].merge!(cmd_env) if cmd_env
|
147
|
-
# Documentation format gets logged in real time, JSON file is read after the fact
|
148
|
-
begin
|
149
|
-
@rspec_cmd = CemAcpt::RSpecUtils::Command.new(cmd_opts)
|
150
|
-
@rspec_cmd.execute(log_prefix: log_prefix('RSPEC'))
|
151
|
-
@run_result.from_json_file(cmd_opts[:format][:json])
|
152
|
-
rescue Errno::EIO => e
|
153
|
-
async_error("failed to run rspec: #{@node.test_data[:test_name]}: #{$ERROR_INFO}", log_prefix('RSPEC'))
|
154
|
-
@run_result.from_error(e)
|
155
|
-
rescue StandardError => e
|
156
|
-
async_error("failed to run rspec: #{@node.test_data[:test_name]}: #{e.message}", log_prefix('RSPEC'))
|
157
|
-
async_debug("Backtrace:\n#{e.backtrace}", log_prefix('RSPEC'))
|
158
|
-
@run_result.from_error(e)
|
159
|
-
end
|
160
|
-
end
|
161
|
-
async_info("Tests completed with exit code: #{@run_result.exit_status}", log_prefix('RSPEC'))
|
162
|
-
end
|
163
|
-
|
164
|
-
# Destroys the node for the acceptance test suite.
|
165
|
-
def destroy
|
166
|
-
kill_spec_pty_if_exists
|
167
|
-
if @context.config.get('no_destroy_nodes')
|
168
|
-
async_info("Not destroying node #{@node.node_name} because 'no_destroy_nodes' is set to true",
|
169
|
-
log_prefix('DESTROY'))
|
170
|
-
else
|
171
|
-
async_info("Destroying #{@node.node_name}...", log_prefix('DESTROY'))
|
172
|
-
@node.destroy
|
173
|
-
@node_exists = false
|
174
|
-
async_info("Node #{@node.node_name} destroyed successfully", log_prefix('DESTROY'))
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
def kill_spec_pty_if_exists
|
179
|
-
@rspec_cmd.kill_pty
|
180
|
-
end
|
181
|
-
|
182
|
-
# Validates the runner configuration.
|
183
|
-
def validate!
|
184
|
-
raise 'No node provided' unless @node
|
185
|
-
raise 'Node does not have config' if @node.config.nil? || @node.config.empty?
|
186
|
-
raise 'Node does not have test data' if @node.test_data.nil? || @node.test_data.empty?
|
187
|
-
raise 'No node inventory provided' unless @node_inventory
|
188
|
-
end
|
189
|
-
|
190
|
-
# Options used with RSpec
|
191
|
-
def rspec_opts
|
192
|
-
opts = {
|
193
|
-
test_path: @node.test_data[:test_file],
|
194
|
-
use_bundler: false,
|
195
|
-
bundle_install: false,
|
196
|
-
format: {
|
197
|
-
json: "results_#{@node.test_data[:test_name]}.json",
|
198
|
-
},
|
199
|
-
debug: (@debug_mode && @context.config.get('verbose')),
|
200
|
-
quiet: @context.config.get('quiet'),
|
201
|
-
env: {
|
202
|
-
'TARGET_HOST' => @node.node_name,
|
203
|
-
}
|
204
|
-
}
|
205
|
-
opts[:format][:documentation] = nil unless @context.config.get('verbose')
|
206
|
-
opts
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end
|
210
|
-
end
|
@@ -1,103 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module CemAcpt
|
4
|
-
module TestRunner
|
5
|
-
require 'json'
|
6
|
-
|
7
|
-
# Error thrown when there are problems parsing the JSON output from Rspec
|
8
|
-
class RspecOutputJSONParserError < StandardError
|
9
|
-
def initialize(err = nil)
|
10
|
-
super err
|
11
|
-
set_backtrace err.backtrace unless err.nil?
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
# Class to process the results of a Runner
|
16
|
-
class RunnerResult
|
17
|
-
attr_reader :run_result
|
18
|
-
|
19
|
-
def initialize(node, debug: false)
|
20
|
-
@node = node
|
21
|
-
@run_result = {}
|
22
|
-
@debug = debug
|
23
|
-
end
|
24
|
-
|
25
|
-
def to_h
|
26
|
-
@run_result
|
27
|
-
end
|
28
|
-
|
29
|
-
def debug?
|
30
|
-
@debug
|
31
|
-
end
|
32
|
-
|
33
|
-
def exit_status
|
34
|
-
if run_result.nil?
|
35
|
-
99
|
36
|
-
elsif run_result.empty?
|
37
|
-
66
|
38
|
-
elsif result_errors? || result_failures?
|
39
|
-
1
|
40
|
-
else
|
41
|
-
0
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def result_array
|
46
|
-
[run_result, exit_status]
|
47
|
-
end
|
48
|
-
|
49
|
-
def from_json_file(file_path)
|
50
|
-
res = JSON.parse(File.read(file_path))
|
51
|
-
new_run_result(res)
|
52
|
-
rescue StandardError => e
|
53
|
-
from_error(RspecOutputJSONParserError.new(e))
|
54
|
-
end
|
55
|
-
|
56
|
-
def from_error(err)
|
57
|
-
label = err.class.to_s.start_with?('Errno::') ? 'system_error' : 'standard_error'
|
58
|
-
res = {
|
59
|
-
label => {
|
60
|
-
'message' => err.message,
|
61
|
-
'error_class' => err.class.to_s,
|
62
|
-
'backtrace' => err.backtrace,
|
63
|
-
'cause' => err.cause,
|
64
|
-
'full_message' => err.full_message,
|
65
|
-
}
|
66
|
-
}
|
67
|
-
res[label]['errno'] = err.errno if label == 'system_error'
|
68
|
-
new_run_result(res)
|
69
|
-
end
|
70
|
-
|
71
|
-
def result_errors?
|
72
|
-
run_result.keys.any? { |k| k.end_with?('error') }
|
73
|
-
end
|
74
|
-
|
75
|
-
def result_failures?
|
76
|
-
if run_result['summary']
|
77
|
-
run_result['summary']['failure_count'].positive? ||
|
78
|
-
run_result['summary']['errors_outside_of_examples_count'].positive?
|
79
|
-
else
|
80
|
-
true
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
private
|
85
|
-
|
86
|
-
def new_run_result(results)
|
87
|
-
@run_result = debug? ? with_debug_data(results) : results
|
88
|
-
end
|
89
|
-
|
90
|
-
def with_debug_data(results)
|
91
|
-
results.merge(
|
92
|
-
{
|
93
|
-
'debug' => {
|
94
|
-
'node' => @node,
|
95
|
-
'config' => @config,
|
96
|
-
'test_data' => @test_data,
|
97
|
-
}
|
98
|
-
}
|
99
|
-
)
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|