cem_acpt 0.2.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/spec.yml +30 -0
  3. data/Gemfile +4 -3
  4. data/Gemfile.lock +95 -43
  5. data/README.md +144 -83
  6. data/cem_acpt.gemspec +12 -7
  7. data/exe/cem_acpt +41 -7
  8. data/lib/cem_acpt/config.rb +340 -0
  9. data/lib/cem_acpt/core_extensions.rb +17 -61
  10. data/lib/cem_acpt/goss/api/action_response.rb +175 -0
  11. data/lib/cem_acpt/goss/api.rb +83 -0
  12. data/lib/cem_acpt/goss.rb +8 -0
  13. data/lib/cem_acpt/image_name_builder.rb +0 -9
  14. data/lib/cem_acpt/logging/formatter.rb +97 -0
  15. data/lib/cem_acpt/logging.rb +168 -142
  16. data/lib/cem_acpt/platform/base.rb +26 -37
  17. data/lib/cem_acpt/platform/gcp.rb +48 -62
  18. data/lib/cem_acpt/platform.rb +30 -28
  19. data/lib/cem_acpt/provision/terraform/linux.rb +47 -0
  20. data/lib/cem_acpt/provision/terraform/os_data.rb +72 -0
  21. data/lib/cem_acpt/provision/terraform/windows.rb +22 -0
  22. data/lib/cem_acpt/provision/terraform.rb +193 -0
  23. data/lib/cem_acpt/provision.rb +20 -0
  24. data/lib/cem_acpt/puppet_helpers.rb +0 -1
  25. data/lib/cem_acpt/test_data.rb +23 -13
  26. data/lib/cem_acpt/test_runner/log_formatter/goss_action_response.rb +104 -0
  27. data/lib/cem_acpt/test_runner/log_formatter.rb +10 -0
  28. data/lib/cem_acpt/test_runner.rb +170 -3
  29. data/lib/cem_acpt/utils/puppet.rb +29 -0
  30. data/lib/cem_acpt/utils/ssh.rb +197 -0
  31. data/lib/cem_acpt/utils/terminal.rb +27 -0
  32. data/lib/cem_acpt/utils.rb +4 -138
  33. data/lib/cem_acpt/version.rb +1 -1
  34. data/lib/cem_acpt.rb +73 -23
  35. data/lib/terraform/gcp/linux/goss/puppet_idempotent.yaml +10 -0
  36. data/lib/terraform/gcp/linux/goss/puppet_noop.yaml +12 -0
  37. data/lib/terraform/gcp/linux/main.tf +191 -0
  38. data/lib/terraform/gcp/linux/systemd/goss-acpt.service +8 -0
  39. data/lib/terraform/gcp/linux/systemd/goss-idempotent.service +8 -0
  40. data/lib/terraform/gcp/linux/systemd/goss-noop.service +8 -0
  41. data/lib/terraform/gcp/windows/.keep +0 -0
  42. data/sample_config.yaml +22 -21
  43. metadata +151 -51
  44. data/lib/cem_acpt/bootstrap/bootstrapper.rb +0 -206
  45. data/lib/cem_acpt/bootstrap/operating_system/rhel_family.rb +0 -129
  46. data/lib/cem_acpt/bootstrap/operating_system.rb +0 -17
  47. data/lib/cem_acpt/bootstrap.rb +0 -12
  48. data/lib/cem_acpt/context.rb +0 -153
  49. data/lib/cem_acpt/platform/base/cmd.rb +0 -71
  50. data/lib/cem_acpt/platform/gcp/cmd.rb +0 -353
  51. data/lib/cem_acpt/platform/gcp/compute.rb +0 -332
  52. data/lib/cem_acpt/platform/vmpooler.rb +0 -24
  53. data/lib/cem_acpt/rspec_utils.rb +0 -242
  54. data/lib/cem_acpt/shared_objects.rb +0 -537
  55. data/lib/cem_acpt/spec_helper_acceptance.rb +0 -184
  56. data/lib/cem_acpt/test_runner/run_handler.rb +0 -187
  57. data/lib/cem_acpt/test_runner/runner.rb +0 -210
  58. 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