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/utils.rb
CHANGED
@@ -15,12 +15,21 @@ module CemAcpt
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
-
|
18
|
+
# File-related utilities
|
19
|
+
module File
|
20
|
+
def self.set_permissions(permission, *file_paths)
|
21
|
+
file_paths.map { |p| File.chmod(permission, p) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Puppet-related utilities
|
26
|
+
module Puppet
|
19
27
|
DEFAULT_PUPPET_PATH = {
|
20
28
|
nix: '/opt/puppetlabs/bin/puppet',
|
21
29
|
windows: 'C:/Program Files/Puppet Labs/Puppet/bin/puppet.bat',
|
22
30
|
}.freeze
|
23
31
|
|
32
|
+
# Finds and returns the Puppet executable
|
24
33
|
def self.puppet_executable
|
25
34
|
this_os = CemAcpt::Utils.os
|
26
35
|
if File.file?(DEFAULT_PUPPET_PATH[this_os]) && File.executable?(DEFAULT_PUPPET_PATH[this_os])
|
@@ -39,32 +48,95 @@ module CemAcpt
|
|
39
48
|
end
|
40
49
|
raise 'Could not find Puppet executable! Is Puppet installed?'
|
41
50
|
end
|
51
|
+
|
52
|
+
# Builds a Puppet module package.
|
53
|
+
# @param module_dir [String] Path to the module directory. If target_dir
|
54
|
+
# is specified as a relative path, it will be relative to the module dir.
|
55
|
+
# @param target_dir [String] Path to the target directory where the package
|
56
|
+
# will be built. This defaults to the relative path 'pkg/'.
|
57
|
+
# @param should_log [Boolean] Whether or not to log the build process.
|
58
|
+
# @return [String] Path to the built package.
|
59
|
+
def self.build_module_package(module_dir, target_dir = nil, should_log: false)
|
60
|
+
require 'puppet/modulebuilder'
|
61
|
+
require 'fileutils'
|
62
|
+
|
63
|
+
builder_logger = should_log ? logger : nil
|
64
|
+
builder = ::Puppet::Modulebuilder::Builder.new(::File.expand_path(module_dir), target_dir, builder_logger)
|
65
|
+
|
66
|
+
# Validates module metadata by raising exception if invalid
|
67
|
+
_metadata = builder.metadata
|
68
|
+
|
69
|
+
# Builds the module package
|
70
|
+
builder.build
|
71
|
+
end
|
42
72
|
end
|
43
73
|
|
44
|
-
|
74
|
+
# Node-related utilities
|
75
|
+
module Node
|
45
76
|
def self.random_instance_name(prefix: 'cem-acpt-', length: 24)
|
46
77
|
rand_length = length - prefix.length
|
47
78
|
"#{prefix}#{SecureRandom.hex(rand_length)}"
|
48
79
|
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# SSH-related utilities
|
83
|
+
module SSH
|
84
|
+
def self.keygen
|
85
|
+
bin_path = `command -v ssh-keygen`.chomp
|
86
|
+
raise 'Cannot find ssh-keygen! Install it and verify PATH' unless bin_path
|
87
|
+
|
88
|
+
bin_path
|
89
|
+
end
|
49
90
|
|
50
|
-
def self.
|
91
|
+
def self.default_keydir
|
92
|
+
ssh_dir = File.join(ENV['HOME'], '.ssh')
|
93
|
+
raise "SSH directory at #{ssh_dir} does not exist" unless File.directory?(ssh_dir)
|
94
|
+
|
95
|
+
ssh_dir
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.ephemeral_ssh_key(type: 'rsa', bits: '4096', comment: nil, keydir: default_keydir)
|
51
99
|
raise ArgumentError, 'keydir does not exist' unless File.directory?(keydir)
|
52
100
|
|
53
|
-
keyfile =
|
54
|
-
keygen_cmd = [
|
55
|
-
'ssh-keygen',
|
56
|
-
"-t #{type}",
|
57
|
-
"-b #{bits}",
|
58
|
-
"-f #{keyfile}",
|
59
|
-
'-N ""',
|
60
|
-
'-q',
|
61
|
-
]
|
101
|
+
keyfile = File.join(keydir, SecureRandom.hex(16))
|
102
|
+
keygen_cmd = [ssh_keygen, "-t #{type}", "-b #{bits}", "-f #{keyfile}", '-N ""']
|
62
103
|
keygen_cmd << "-C \"#{comment}\"" if comment
|
63
104
|
_, stderr, status = Open3.capture3(keygen_cmd.join(' '))
|
64
105
|
raise "Failed to generate ephemeral SSH key: #{stderr}" unless status.success?
|
65
106
|
|
66
107
|
[keyfile, "#{keyfile}.pub"]
|
67
108
|
end
|
109
|
+
|
110
|
+
def self.acpt_known_hosts(keydir: default_keydir, file_name: 'acpt_known_hosts', overwrite: true)
|
111
|
+
kh_file = File.join(keydir, file_name)
|
112
|
+
File.open(kh_file, 'w') { |f| f.write("\n") } unless File.exist?(kh_file) && !overwrite
|
113
|
+
kh_file
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.set_ssh_file_permissions(priv_key, pub_key, known_hosts)
|
117
|
+
CemAcpt::Utils::File.set_permissions(0o600, priv_key, pub_key, known_hosts)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Terminal-related utilities
|
122
|
+
module Terminal
|
123
|
+
def self.keep_terminal_alive
|
124
|
+
require 'concurrent-ruby'
|
125
|
+
executor = Concurrent::SingleThreadExecutor.new
|
126
|
+
executor.post do
|
127
|
+
loop do
|
128
|
+
$stdout.print(".\r")
|
129
|
+
sleep(1)
|
130
|
+
$stdout.print("..\r")
|
131
|
+
sleep(1)
|
132
|
+
$stdout.print("...\r")
|
133
|
+
sleep(1)
|
134
|
+
$stdout.print(" \r")
|
135
|
+
sleep(1)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
executor
|
139
|
+
end
|
68
140
|
end
|
69
141
|
end
|
70
142
|
end
|
data/lib/cem_acpt/version.rb
CHANGED
data/lib/cem_acpt.rb
CHANGED
@@ -10,18 +10,25 @@ module CemAcpt
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.run(params)
|
13
|
-
require_relative 'cem_acpt/
|
13
|
+
require_relative 'cem_acpt/context'
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
logger.formatter = new_log_formatter(params[:log_format])
|
15
|
+
log_level = params[:log_level]
|
16
|
+
log_formatter = params[:log_format] ? new_log_formatter(params[:log_format]) : nil
|
17
|
+
logdevs = [$stdout]
|
18
|
+
if params[:log_file] && params[:quiet]
|
19
|
+
logdevs = [params[:log_file]]
|
20
|
+
elsif params[:log_file] && !params[:quiet]
|
21
|
+
logdevs << params[:log_file]
|
23
22
|
end
|
24
|
-
|
25
|
-
|
23
|
+
if (params[:CI] || ENV['CI'] || ENV['GITHUB_ACTION']) && !logdevs.include?($stdout)
|
24
|
+
logdevs << $stdout
|
25
|
+
end
|
26
|
+
new_logger(
|
27
|
+
*logdevs,
|
28
|
+
level: log_level,
|
29
|
+
formatter: log_formatter,
|
30
|
+
)
|
31
|
+
exit_code = CemAcpt::Context.with(params, &:run)
|
32
|
+
exit exit_code
|
26
33
|
end
|
27
34
|
end
|
metadata
CHANGED
@@ -1,127 +1,127 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cem_acpt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Heston Snodgrass
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-06-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name: concurrent-ruby
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
16
15
|
requirements:
|
17
16
|
- - "~>"
|
18
17
|
- !ruby/object:Gem::Version
|
19
18
|
version: 1.1.9
|
20
|
-
|
19
|
+
name: concurrent-ruby
|
21
20
|
prerelease: false
|
21
|
+
type: :runtime
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 1.1.9
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name: deep_merge
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
30
29
|
requirements:
|
31
30
|
- - "~>"
|
32
31
|
- !ruby/object:Gem::Version
|
33
32
|
version: 1.2.2
|
34
|
-
|
33
|
+
name: deep_merge
|
35
34
|
prerelease: false
|
35
|
+
type: :runtime
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: 1.2.2
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name: net-scp
|
43
42
|
requirement: !ruby/object:Gem::Requirement
|
44
43
|
requirements:
|
45
44
|
- - "~>"
|
46
45
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
48
|
-
|
46
|
+
version: 7.0.0.beta1
|
47
|
+
name: net-ssh
|
49
48
|
prerelease: false
|
49
|
+
type: :runtime
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: 7.0.0.beta1
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name: net-ssh
|
57
56
|
requirement: !ruby/object:Gem::Requirement
|
58
57
|
requirements:
|
59
|
-
- - "
|
58
|
+
- - ">"
|
60
59
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
62
|
-
|
60
|
+
version: 0.0.1
|
61
|
+
name: puppet-modulebuilder
|
63
62
|
prerelease: false
|
63
|
+
type: :runtime
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - ">"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: 0.0.1
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name: puppet-modulebuilder
|
71
70
|
requirement: !ruby/object:Gem::Requirement
|
72
71
|
requirements:
|
73
|
-
- - "
|
72
|
+
- - ">="
|
74
73
|
- !ruby/object:Gem::Version
|
75
|
-
version: 0
|
76
|
-
|
74
|
+
version: '0'
|
75
|
+
name: rake
|
77
76
|
prerelease: false
|
77
|
+
type: :runtime
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- - "
|
80
|
+
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: 0
|
82
|
+
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name: rake
|
85
84
|
requirement: !ruby/object:Gem::Requirement
|
86
85
|
requirements:
|
87
|
-
- - "
|
86
|
+
- - ">="
|
88
87
|
- !ruby/object:Gem::Version
|
89
|
-
version: '0
|
90
|
-
|
88
|
+
version: '0'
|
89
|
+
name: rspec
|
91
90
|
prerelease: false
|
91
|
+
type: :runtime
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- - "
|
94
|
+
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '0
|
96
|
+
version: '0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
-
name: rspec
|
99
98
|
requirement: !ruby/object:Gem::Requirement
|
100
99
|
requirements:
|
101
|
-
- - "
|
100
|
+
- - ">="
|
102
101
|
- !ruby/object:Gem::Version
|
103
|
-
version: '0
|
104
|
-
|
102
|
+
version: '0'
|
103
|
+
name: serverspec-cem-acpt
|
105
104
|
prerelease: false
|
105
|
+
type: :runtime
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
|
-
- - "
|
108
|
+
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: '0
|
110
|
+
version: '0'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
|
-
name: serverspec
|
113
112
|
requirement: !ruby/object:Gem::Requirement
|
114
113
|
requirements:
|
115
|
-
- - "
|
114
|
+
- - ">="
|
116
115
|
- !ruby/object:Gem::Version
|
117
|
-
version: '0
|
118
|
-
|
116
|
+
version: '0'
|
117
|
+
name: rubocop
|
119
118
|
prerelease: false
|
119
|
+
type: :development
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
|
-
- - "
|
122
|
+
- - ">="
|
123
123
|
- !ruby/object:Gem::Version
|
124
|
-
version: '0
|
124
|
+
version: '0'
|
125
125
|
description: Litmus-like library focusing on CEM Acceptance Tests
|
126
126
|
email:
|
127
127
|
- hsnodgrass3@gmail.com
|
@@ -132,7 +132,6 @@ extra_rdoc_files: []
|
|
132
132
|
files:
|
133
133
|
- ".gitignore"
|
134
134
|
- ".rspec"
|
135
|
-
- ".travis.yml"
|
136
135
|
- CODEOWNERS
|
137
136
|
- Gemfile
|
138
137
|
- Gemfile.lock
|
@@ -159,16 +158,20 @@ files:
|
|
159
158
|
- lib/cem_acpt/platform/gcp/compute.rb
|
160
159
|
- lib/cem_acpt/platform/vmpooler.rb
|
161
160
|
- lib/cem_acpt/puppet_helpers.rb
|
162
|
-
- lib/cem_acpt/
|
161
|
+
- lib/cem_acpt/rspec_utils.rb
|
163
162
|
- lib/cem_acpt/shared_objects.rb
|
164
163
|
- lib/cem_acpt/spec_helper_acceptance.rb
|
165
164
|
- lib/cem_acpt/test_data.rb
|
165
|
+
- lib/cem_acpt/test_runner.rb
|
166
|
+
- lib/cem_acpt/test_runner/run_handler.rb
|
167
|
+
- lib/cem_acpt/test_runner/runner.rb
|
168
|
+
- lib/cem_acpt/test_runner/runner_result.rb
|
166
169
|
- lib/cem_acpt/utils.rb
|
167
170
|
- lib/cem_acpt/version.rb
|
168
171
|
- sample_config.yaml
|
169
172
|
homepage: https://github.com/puppetlabs/cem_acpt
|
170
173
|
licenses:
|
171
|
-
-
|
174
|
+
- proprietary
|
172
175
|
metadata:
|
173
176
|
homepage_uri: https://github.com/puppetlabs/cem_acpt
|
174
177
|
source_code_uri: https://github.com/puppetlabs/cem_acpt
|
@@ -188,7 +191,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
188
191
|
- !ruby/object:Gem::Version
|
189
192
|
version: '0'
|
190
193
|
requirements: []
|
191
|
-
rubygems_version: 3.
|
194
|
+
rubygems_version: 3.2.29
|
192
195
|
signing_key:
|
193
196
|
specification_version: 4
|
194
197
|
summary: CEM Acceptance Tests
|
data/.travis.yml
DELETED
data/lib/cem_acpt/runner.rb
DELETED
@@ -1,304 +0,0 @@
|
|
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
|