cem_acpt 0.1.0 → 0.2.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/.gitignore +7 -0
- data/Gemfile.lock +36 -15
- data/README.md +8 -4
- data/cem_acpt.gemspec +10 -10
- data/exe/cem_acpt +29 -3
- data/lib/cem_acpt/context.rb +132 -39
- data/lib/cem_acpt/core_extensions.rb +9 -12
- data/lib/cem_acpt/logging.rb +177 -19
- data/lib/cem_acpt/platform/base/cmd.rb +8 -2
- data/lib/cem_acpt/platform/gcp/cmd.rb +162 -79
- data/lib/cem_acpt/platform/gcp/compute.rb +6 -1
- data/lib/cem_acpt/platform/gcp.rb +3 -3
- data/lib/cem_acpt/puppet_helpers.rb +1 -0
- data/lib/cem_acpt/rspec_utils.rb +242 -0
- data/lib/cem_acpt/shared_objects.rb +147 -26
- data/lib/cem_acpt/spec_helper_acceptance.rb +21 -13
- data/lib/cem_acpt/test_data.rb +3 -14
- data/lib/cem_acpt/test_runner/run_handler.rb +187 -0
- data/lib/cem_acpt/test_runner/runner.rb +228 -0
- data/lib/cem_acpt/test_runner/runner_result.rb +103 -0
- data/lib/cem_acpt/test_runner.rb +10 -0
- data/lib/cem_acpt/utils.rb +84 -12
- data/lib/cem_acpt/version.rb +1 -1
- data/lib/cem_acpt.rb +18 -11
- metadata +47 -44
- data/.travis.yml +0 -6
- data/lib/cem_acpt/runner.rb +0 -304
data/lib/cem_acpt/test_data.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'core_extensions'
|
4
|
+
require_relative 'logging'
|
5
|
+
|
3
6
|
module CemAcpt
|
4
7
|
# This module provides a method and class for extracting and formatting
|
5
8
|
# test data.
|
6
9
|
module TestData
|
7
|
-
require_relative 'core_extensions'
|
8
|
-
require_relative 'logging'
|
9
|
-
|
10
10
|
# Returns a hash of test data.
|
11
11
|
# @param config [CemAcpt::Config] the config object
|
12
12
|
# @return [Array<Hash>] an array of test data hashes
|
@@ -82,7 +82,6 @@ module CemAcpt
|
|
82
82
|
def process_static_vars(test_data)
|
83
83
|
return unless @config.has?('test_data.vars')
|
84
84
|
|
85
|
-
logger.debug('Processing static variables...')
|
86
85
|
vars = @config.get('test_data.vars').each_with_object({}) do |(k, v), h|
|
87
86
|
logger.debug("Static variable: #{k}")
|
88
87
|
h[k] = v
|
@@ -92,7 +91,6 @@ module CemAcpt
|
|
92
91
|
end
|
93
92
|
|
94
93
|
def process_name_pattern_vars(test_name, test_data)
|
95
|
-
logger.debug('Processing test name pattern...')
|
96
94
|
name_pattern_vars = name_pattern_matches(test_name) || {}
|
97
95
|
test_data.merge!(name_pattern_vars)
|
98
96
|
test_data.format!
|
@@ -102,20 +100,17 @@ module CemAcpt
|
|
102
100
|
return test_name unless @config.has?('test_data.name_pattern_vars')
|
103
101
|
|
104
102
|
pattern = @config.get('test_data.name_pattern_vars')
|
105
|
-
logger.debug("Test name pattern: #{pattern.inspect}")
|
106
103
|
pattern_matches = test_name.match(pattern)&.named_captures
|
107
104
|
unless pattern_matches
|
108
105
|
logger.error("Test name #{test_name} does not match pattern #{pattern.inspect}")
|
109
106
|
return
|
110
107
|
end
|
111
|
-
logger.debug("Test name matches pattern: #{pattern_matches}")
|
112
108
|
pattern_matches
|
113
109
|
end
|
114
110
|
|
115
111
|
def vars_post_processing!(test_data)
|
116
112
|
return unless @config.has?('test_data.vars_post_processing')
|
117
113
|
|
118
|
-
logger.debug("Running test name pattern post processing with test data: #{test_data}")
|
119
114
|
post_processing_new_vars!(test_data)
|
120
115
|
post_processing_delete_vars!(test_data)
|
121
116
|
end
|
@@ -124,7 +119,6 @@ module CemAcpt
|
|
124
119
|
new_vars = @config.get('test_data.vars_post_processing.new_vars')
|
125
120
|
return unless new_vars
|
126
121
|
|
127
|
-
logger.debug("Processing new variables: #{new_vars}")
|
128
122
|
new_vars.each do |var|
|
129
123
|
if var.has?('string_split')
|
130
124
|
test_data[var.dot_dig('name')] = op_string_split(test_data, var)
|
@@ -135,13 +129,9 @@ module CemAcpt
|
|
135
129
|
end
|
136
130
|
|
137
131
|
def op_string_split(test_data, var)
|
138
|
-
logger.debug("Processing string split for new var #{var}")
|
139
132
|
from_var = var.dot_dig('string_split.from')
|
140
|
-
logger.debug("String split from var: #{from_var}")
|
141
133
|
from = test_data.dot_dig(from_var)
|
142
|
-
logger.debug("String split from value: #{from}")
|
143
134
|
parts = from.split(var.dot_dig('string_split.using'))
|
144
|
-
logger.debug("String split parts: #{parts}")
|
145
135
|
var.has?('string_split.part') ? parts[var.dot_dig('string_split.part')] : parts
|
146
136
|
end
|
147
137
|
|
@@ -149,7 +139,6 @@ module CemAcpt
|
|
149
139
|
delete_vars = @config.get('test_data.vars_post_processing.delete_vars')
|
150
140
|
return unless delete_vars
|
151
141
|
|
152
|
-
logger.debug('Processing delete variables...')
|
153
142
|
test_data.reject! { |k, _| delete_vars.include?(k.to_s) }
|
154
143
|
end
|
155
144
|
end
|
@@ -0,0 +1,187 @@
|
|
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
|
@@ -0,0 +1,228 @@
|
|
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
|
+
<<<<<<< HEAD
|
46
|
+
validate!
|
47
|
+
end
|
48
|
+
|
49
|
+
=======
|
50
|
+
@completed_steps = []
|
51
|
+
validate!
|
52
|
+
end
|
53
|
+
|
54
|
+
def run_step(step_sym)
|
55
|
+
send(step_sym)
|
56
|
+
@completed_steps << step_sym
|
57
|
+
rescue StandardError => e
|
58
|
+
err = CemAcpt::TestRunner::RunnerStepError.new(step_sym, e)
|
59
|
+
step_error_logging(err)
|
60
|
+
@run_result.from_error(err)
|
61
|
+
destroy unless step_sym == :destroy
|
62
|
+
end
|
63
|
+
|
64
|
+
>>>>>>> 489757f (Fixes for race conditions)
|
65
|
+
# Executes test suite steps
|
66
|
+
def start
|
67
|
+
async_info("Starting test suite for #{@node.node_name}", log_prefix('RUNNER'))
|
68
|
+
run_step(:provision)
|
69
|
+
run_step(:bootstrap)
|
70
|
+
run_step(:run_tests)
|
71
|
+
run_step(:destroy)
|
72
|
+
true
|
73
|
+
rescue StandardError => e
|
74
|
+
step_error_logging(e)
|
75
|
+
@run_result.from_error(e)
|
76
|
+
destroy
|
77
|
+
end
|
78
|
+
|
79
|
+
# Checks for failures in the test results.
|
80
|
+
# @param result [Hash] the test result to check
|
81
|
+
# @return [Boolean] whether or not there are test failures in result
|
82
|
+
def test_failures?
|
83
|
+
@run_result.result_errors? || @run_result.result_failures?
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
<<<<<<< HEAD
|
89
|
+
def run_step(step_sym)
|
90
|
+
send(step_sym)
|
91
|
+
rescue StandardError => e
|
92
|
+
raise CemAcpt::TestRunner::RunnerStepError.new(step_sym, e)
|
93
|
+
end
|
94
|
+
|
95
|
+
=======
|
96
|
+
>>>>>>> 489757f (Fixes for race conditions)
|
97
|
+
def step_error_logging(err)
|
98
|
+
prefix = err.respond_to?(:step) ? log_prefix(err.step.capitalize) : log_prefix('RUNNER')
|
99
|
+
fatal_msg = ["runner failed: #{err.message}"]
|
100
|
+
async_fatal(fatal_msg, prefix)
|
101
|
+
<<<<<<< HEAD
|
102
|
+
=======
|
103
|
+
async_debug("Completed steps: #{@completed_steps}", prefix)
|
104
|
+
>>>>>>> 489757f (Fixes for race conditions)
|
105
|
+
async_debug("Failed runner backtrace:\n#{err.backtrace.join("\n")}", prefix)
|
106
|
+
async_debug("Failed runner test data: #{@node.test_data}", prefix)
|
107
|
+
end
|
108
|
+
|
109
|
+
def log_prefix(prefix)
|
110
|
+
"#{prefix}: #{@node.test_data[:test_name]}:"
|
111
|
+
end
|
112
|
+
|
113
|
+
# Provisions the node for the acceptance test suite.
|
114
|
+
def provision
|
115
|
+
async_info("Provisioning #{@node.node_name}...", log_prefix('PROVISION'))
|
116
|
+
start_time = Time.now
|
117
|
+
@node.provision
|
118
|
+
@node_exists = true
|
119
|
+
max_retries = 60 # equals 300 seconds because we check every five seconds
|
120
|
+
until @node.ready?
|
121
|
+
if max_retries <= 0
|
122
|
+
async_fatal("Node #{@node.node_name} failed to provision", log_prefix('PROVISION'))
|
123
|
+
raise CemAcpt::TestRunner::RunnerProvisionError, "Provisioning timed out for node #{@node.node_name}"
|
124
|
+
end
|
125
|
+
|
126
|
+
async_info("Waiting for #{@node.node_name} to be ready for remote connections...", log_prefix('PROVISION'))
|
127
|
+
max_retries -= 1
|
128
|
+
sleep(5)
|
129
|
+
end
|
130
|
+
async_info("Node #{@node.node_name} is ready...", log_prefix('PROVISION'))
|
131
|
+
node_desc = {
|
132
|
+
test_data: @node.test_data,
|
133
|
+
platform: @platform,
|
134
|
+
local_port: @node.local_port,
|
135
|
+
}.merge(@node.node)
|
136
|
+
@node_inventory.add(@node.node_name, node_desc)
|
137
|
+
@node_inventory.save
|
138
|
+
async_info("Node #{@node.node_name} provisioned in #{Time.now - start_time} seconds", log_prefix('PROVISION'))
|
139
|
+
end
|
140
|
+
|
141
|
+
# Bootstraps the node for the acceptance test suite. Currently, this
|
142
|
+
# just uploads and installs the module package.
|
143
|
+
def bootstrap
|
144
|
+
async_info("Bootstrapping #{@node.node_name}...", log_prefix('BOOTSTRAP'))
|
145
|
+
until File.exist?(@module_pkg_path)
|
146
|
+
async_debug("Waiting for module package #{@module_pkg_path} to exist...", log_prefix('BOOTSTRAP'))
|
147
|
+
sleep(1)
|
148
|
+
end
|
149
|
+
async_info("Installing module package #{@module_pkg_path}...", log_prefix('BOOTSTRAP'))
|
150
|
+
@node.install_puppet_module_package(@module_pkg_path)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Runs the acceptance test suite via rspec.
|
154
|
+
def run_tests
|
155
|
+
attempts = 0
|
156
|
+
until File.exist?(@node_inventory.save_file_path)
|
157
|
+
raise 'Node inventory file not found' if (attempts += 1) > 3
|
158
|
+
|
159
|
+
sleep(1)
|
160
|
+
end
|
161
|
+
async_info("Running test #{@node.test_data[:test_name]} on node #{@node.node_name}...", log_prefix('RSPEC'))
|
162
|
+
@node.run_tests do |cmd_env|
|
163
|
+
cmd_opts = rspec_opts
|
164
|
+
cmd_opts[:env].merge!(cmd_env) if cmd_env
|
165
|
+
# Documentation format gets logged in real time, JSON file is read after the fact
|
166
|
+
begin
|
167
|
+
@rspec_cmd = CemAcpt::RSpecUtils::Command.new(cmd_opts)
|
168
|
+
@rspec_cmd.execute(log_prefix: log_prefix('RSPEC'))
|
169
|
+
@run_result.from_json_file(cmd_opts[:format][:json])
|
170
|
+
rescue Errno::EIO => e
|
171
|
+
async_error("failed to run rspec: #{@node.test_data[:test_name]}: #{$ERROR_INFO}", log_prefix('RSPEC'))
|
172
|
+
@run_result.from_error(e)
|
173
|
+
rescue StandardError => e
|
174
|
+
async_error("failed to run rspec: #{@node.test_data[:test_name]}: #{e.message}", log_prefix('RSPEC'))
|
175
|
+
async_debug("Backtrace:\n#{e.backtrace}", log_prefix('RSPEC'))
|
176
|
+
@run_result.from_error(e)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
async_info("Tests completed with exit code: #{@run_result.exit_status}", log_prefix('RSPEC'))
|
180
|
+
end
|
181
|
+
|
182
|
+
# Destroys the node for the acceptance test suite.
|
183
|
+
def destroy
|
184
|
+
kill_spec_pty_if_exists
|
185
|
+
if @context.config.get('no_destroy_nodes')
|
186
|
+
async_info("Not destroying node #{@node.node_name} because 'no_destroy_nodes' is set to true",
|
187
|
+
log_prefix('DESTROY'))
|
188
|
+
else
|
189
|
+
async_info("Destroying #{@node.node_name}...", log_prefix('DESTROY'))
|
190
|
+
@node.destroy
|
191
|
+
@node_exists = false
|
192
|
+
async_info("Node #{@node.node_name} destroyed successfully", log_prefix('DESTROY'))
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def kill_spec_pty_if_exists
|
197
|
+
@rspec_cmd.kill_pty
|
198
|
+
end
|
199
|
+
|
200
|
+
# Validates the runner configuration.
|
201
|
+
def validate!
|
202
|
+
raise 'No node provided' unless @node
|
203
|
+
raise 'Node does not have config' if @node.config.nil? || @node.config.empty?
|
204
|
+
raise 'Node does not have test data' if @node.test_data.nil? || @node.test_data.empty?
|
205
|
+
raise 'No node inventory provided' unless @node_inventory
|
206
|
+
end
|
207
|
+
|
208
|
+
# Options used with RSpec
|
209
|
+
def rspec_opts
|
210
|
+
opts = {
|
211
|
+
test_path: @node.test_data[:test_file],
|
212
|
+
use_bundler: false,
|
213
|
+
bundle_install: false,
|
214
|
+
format: {
|
215
|
+
json: "results_#{@node.test_data[:test_name]}.json",
|
216
|
+
},
|
217
|
+
debug: (@debug_mode && @context.config.get('verbose')),
|
218
|
+
quiet: @context.config.get('quiet'),
|
219
|
+
env: {
|
220
|
+
'TARGET_HOST' => @node.node_name,
|
221
|
+
}
|
222
|
+
}
|
223
|
+
opts[:format][:documentation] = nil unless @context.config.get('verbose')
|
224
|
+
opts
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
@@ -0,0 +1,103 @@
|
|
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
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CemAcpt
|
4
|
+
# Namespace for all Runner-related classes and modules
|
5
|
+
module TestRunner
|
6
|
+
require_relative 'test_runner/run_handler'
|
7
|
+
require_relative 'test_runner/runner'
|
8
|
+
require_relative 'test_runner/runner_result'
|
9
|
+
end
|
10
|
+
end
|