cem_acpt 0.2.8-universal-java-17 → 0.2.11-universal-java-17
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +67 -59
- data/lib/cem_acpt/context.rb +7 -7
- data/lib/cem_acpt/logging/formatter.rb +96 -0
- data/lib/cem_acpt/logging.rb +2 -35
- data/lib/cem_acpt/rspec_utils.rb +92 -84
- data/lib/cem_acpt/test_runner/run_handler.rb +27 -4
- data/lib/cem_acpt/test_runner/runner.rb +18 -16
- data/lib/cem_acpt/utils.rb +70 -10
- data/lib/cem_acpt/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c97a0886b1604384a7e03f5edec960b594842f69502e5b1fcd4d2a6a18ecb3fc
|
4
|
+
data.tar.gz: 8ad5f879c25f70d0050ec0f7fd24891339b7a80271193d2549e8a6a7224d7727
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2279fa0e8ab932d0f02982cf8cacf6aaf4bf8c0a5e709eeb7e3cd0d093a4712f501c175f2a57f64cd0c441e56fa0c51e4238ab1a196d89037f719194981536a1
|
7
|
+
data.tar.gz: 76ca3f9c58e4f1450b3e442b0edc825b68ea5b533796b34e90963630dc219731238128f44dbe1267979199d06efa1a3cef99913318bb3684af663e377ec8b97f
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
cem_acpt (0.2.
|
4
|
+
cem_acpt (0.2.11-universal-java-17)
|
5
5
|
concurrent-ruby (>= 1.1, < 2.0)
|
6
6
|
deep_merge (>= 1.2, < 2.0)
|
7
7
|
ed25519 (>= 1.2, < 2.0)
|
@@ -68,7 +68,7 @@ GEM
|
|
68
68
|
rspec-its
|
69
69
|
specinfra (~> 2.83.1)
|
70
70
|
sfl (2.3)
|
71
|
-
specinfra (2.83.
|
71
|
+
specinfra (2.83.3)
|
72
72
|
net-scp
|
73
73
|
net-ssh (>= 2.7)
|
74
74
|
net-telnet (= 0.1.1)
|
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# CemAcpt
|
2
2
|
|
3
|
-
CemAcpt is an acceptance testing library / command line application for running acceptance tests
|
3
|
+
CemAcpt is an acceptance testing library / command line application for running acceptance tests for the CEM modules. It is heavily inspired by Puppet Litmus. CemAcpt is fully compatible with Puppet Litmus acceptance tests.
|
4
4
|
|
5
|
-
CemAcpt was written to facilitate running a single acceptance test file against one or more test nodes concurrently. This is useful when your module supports a lot of different operating systems and modifies low-level components of those operating systems. For CEM, we manage things such as firewall and bootloader configurations which requires us to test against multiple base images of a single operating system (i.e. RHEL 8 with `iptables` and RHEL 8 with `firewalld`). Additionally, we test our module using both Puppet 6 and Puppet 7. As you can see, we now need to test against four test nodes that are all RHEL 8 with slightly different baseline configs. CemAcpt allows us to do so quickly in parallel.
|
5
|
+
CemAcpt was written to facilitate running a single acceptance test file against one or more individual test nodes concurrently, and have each additional acceptance test file do the same. This is useful when your module supports a lot of different operating systems and modifies low-level components of those operating systems. For CEM, we manage things such as firewall and bootloader configurations which requires us to test against multiple base images of a single operating system (i.e. RHEL 8 with `iptables` and RHEL 8 with `firewalld`). Additionally, we test our module using both Puppet 6 and Puppet 7. As you can see, we now need to test against four test nodes that are all RHEL 8 with slightly different baseline configs. CemAcpt allows us to do so quickly in parallel.
|
6
6
|
|
7
7
|
Major differences between Puppet Litmus and CemAcpt are:
|
8
8
|
|
@@ -10,8 +10,72 @@ Major differences between Puppet Litmus and CemAcpt are:
|
|
10
10
|
- Test node image names can be dynamically defined by configurable parsing of the acceptance test name.
|
11
11
|
- This allows your acceptance test names to define the node it will run against.
|
12
12
|
- CemAcpt has it's own CLI for running provisioning of hosts and the test suite and does not use `rake`.
|
13
|
-
- CemAcpt runs everything, including provisioning the test node, creating a temporary manifest on the test node, and running the acceptance test on the test node, in parallel. No matter how many test / host combinations there are, running the test suite will only take as long as the longest running single test / host combo.
|
13
|
+
- CemAcpt runs everything, including provisioning the test node(s), creating a temporary manifest on the test node(s), and running the acceptance test on the test node(s), in parallel. No matter how many test / host combinations there are, running the test suite will only take as long as the longest running single test / host combo.
|
14
14
|
- CemAcpt is configurable via a YAML file.
|
15
|
+
- CemAcpt provides extensive logging, including the ability to save logs to a file and an option to format log output on `stdout` specifically for GitHub Actions.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
### Command-line usage
|
20
|
+
|
21
|
+
Assuming cem_acpt is already configured in your Puppet module, you can run cem_acpt from your command line.
|
22
|
+
|
23
|
+
`cem_acpt` require JRuby 9.3.3.0 or greater. To install and use JRuby with `rvm`:
|
24
|
+
|
25
|
+
```
|
26
|
+
rvm install jruby-9.3.3.0
|
27
|
+
rvm use jruby-9.3.3.0
|
28
|
+
# If you have a Gemfile.lock already
|
29
|
+
rm Gemfile.lock
|
30
|
+
bundle install
|
31
|
+
jruby --server <jruby opts> -S bundle exec cem_acpt <opts>
|
32
|
+
```
|
33
|
+
|
34
|
+
> If you do not delete Gemfile.lock before running `bundle install`, you will most likely encounter dependency version errors. Just delete Gemfile.lock and run `bundle install` again to get past these.
|
35
|
+
|
36
|
+
### Installing into a Puppet module
|
37
|
+
|
38
|
+
Add this line to your module's Gemfile (usually under `group :development`):
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
gem 'cem_acpt', require: false
|
42
|
+
```
|
43
|
+
|
44
|
+
Then, add the following to your `spec_helper_acceptance.rb` (replacing the Puppet Litmus configuration lines):
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
require 'cem_acpt'
|
48
|
+
|
49
|
+
CemAcpt.configure_spec_helper!
|
50
|
+
```
|
51
|
+
|
52
|
+
To use CemAcpt in your acceptance tests, you must call the method `initialize_test_environment!` in your acceptance test's `describe` block before doing anything else:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
describe 'cem_linux CIS Level 1' do
|
56
|
+
initialize_test_environment!
|
57
|
+
...
|
58
|
+
```
|
59
|
+
|
60
|
+
## Usage
|
61
|
+
|
62
|
+
### RSpec methods
|
63
|
+
|
64
|
+
CemAcpt enables you to use all ServerSpec methods in your acceptance tests, and adds three new methods that can be used (these methods are used the same as in Litmus):
|
65
|
+
|
66
|
+
- `apply_manifest(manifest, opts = {})`: Applies a Puppet manifest given as a string.
|
67
|
+
- `idempotent_apply(manifest, opts = {})`: Applies a Puppet manifest given as a string twice and fails if the second apply reports changes.
|
68
|
+
- `run_shell(command, opts = {})`: Runs a shell command against the test node.
|
69
|
+
|
70
|
+
### Config file
|
71
|
+
|
72
|
+
CemAcpt can use a config file (default path is `./cem_acpt_config.yaml`) to configure it's settings. Below are some of the options available in the config file:
|
73
|
+
|
74
|
+
- `tests`: An array of test names you want to run. These are assumed to exist in the path `spec/acceptance`. You should leave off the suffix `_spec.rb` from the test file names.
|
75
|
+
- `platform`: The backend platform that acceptance tests will run on. Currently, only `gcp` is supported.
|
76
|
+
- `test_data`: Configurations for test data. Allows assigning variables to test data either dynamically or statically.
|
77
|
+
- `node_data`: Configurations for nodes created by the platform. The available options are platform-dependent.
|
78
|
+
- `image_name_builder`: If this key is specified, platforms will be passed a dynamically generated image name based on the configurations under this key.
|
15
79
|
|
16
80
|
## Concepts
|
17
81
|
|
@@ -79,62 +143,6 @@ Rules that allow for processing variables after all other test data rules are ra
|
|
79
143
|
|
80
144
|
Much like `name_pattern_vars`, specifying the `image_name_builder` top-level key in the config allows you to manipulate acceptance test names to create a special test data variable called `image_name`. This is helpful for when you have multiple platform base images and want to use the correct image with the correct test. See [sample_config.yaml](sample_config.yaml) for an example.
|
81
145
|
|
82
|
-
## Installation
|
83
|
-
|
84
|
-
### Installing from RubyGems
|
85
|
-
|
86
|
-
To install the gem locally to use as a CLI tool:
|
87
|
-
|
88
|
-
`gem install cem_acpt --prerelease`
|
89
|
-
|
90
|
-
### Installing into a Puppet module
|
91
|
-
|
92
|
-
Add this line to your module's Gemfile (usually under `group :development`):
|
93
|
-
|
94
|
-
```ruby
|
95
|
-
gem 'cem_acpt', require: false
|
96
|
-
```
|
97
|
-
|
98
|
-
Then, add the following to your `spec_helper_acceptance.rb` (replacing the Puppet Litmus configuration lines):
|
99
|
-
|
100
|
-
```ruby
|
101
|
-
require 'cem_acpt'
|
102
|
-
|
103
|
-
CemAcpt.configure_spec_helper!
|
104
|
-
```
|
105
|
-
|
106
|
-
To use CemAcpt in your acceptance tests, you must call the method `initialize_test_environment!` in your acceptance test's `describe` block before doing anything else:
|
107
|
-
|
108
|
-
```ruby
|
109
|
-
describe 'cem_linux CIS Level 1' do
|
110
|
-
initialize_test_environment!
|
111
|
-
...
|
112
|
-
```
|
113
|
-
|
114
|
-
## Usage
|
115
|
-
|
116
|
-
### RSpec methods
|
117
|
-
|
118
|
-
CemAcpt enables you to use all ServerSpec methods in your acceptance tests, and adds three new methods that can be used (these methods are used the same as in Litmus):
|
119
|
-
|
120
|
-
- `apply_manifest(manifest, opts = {})`: Applies a Puppet manifest given as a string.
|
121
|
-
- `idempotent_apply(manifest, opts = {})`: Applies a Puppet manifest given as a string twice and fails if the second apply reports changes.
|
122
|
-
- `run_shell(command, opts = {})`: Runs a shell command against the test node.
|
123
|
-
|
124
|
-
### Config file
|
125
|
-
|
126
|
-
CemAcpt can use a config file (default path is `./cem_acpt_config.yaml`) to configure it's settings. Below are some of the options available in the config file:
|
127
|
-
|
128
|
-
- `tests`: An array of test names you want to run. These are assumed to exist in the path `spec/acceptance`. You should leave off the suffix `_spec.rb` from the test file names.
|
129
|
-
- `platform`: The backend platform that acceptance tests will run on. Currently, only `gcp` is supported.
|
130
|
-
- `test_data`: Configurations for test data. Allows assigning variables to test data either dynamically or statically.
|
131
|
-
- `node_data`: Configurations for nodes created by the platform. The available options are platform-dependent.
|
132
|
-
- `image_name_builder`: If this key is specified, platforms will be passed a dynamically generated image name based on the configurations under this key.
|
133
|
-
|
134
|
-
### Semantic acceptance test names
|
135
|
-
|
136
|
-
With CemAcpt, acceptance test names can have semantic meaning and influence how your acceptance test nodes are provisioned and how test data is created. The semantic meaning of the test names can be configured in the config file.
|
137
|
-
|
138
146
|
## Development
|
139
147
|
|
140
148
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/cem_acpt/context.rb
CHANGED
@@ -36,10 +36,8 @@ module CemAcpt
|
|
36
36
|
# Creates a SSH key and a SSH known hosts file for the acceptance test suite
|
37
37
|
def new_test_ssh_key
|
38
38
|
log('Creating ephemeral SSH key and known hosts file for acceptance test suites...')
|
39
|
-
@ssh_priv_key, @ssh_pub_key = CemAcpt::Utils::SSH.
|
40
|
-
|
41
|
-
CemAcpt::Utils::SSH.set_ssh_file_permissions(@ssh_priv_key, @ssh_pub_key, @ssh_known_hosts)
|
42
|
-
log('Successfully created SSH files...')
|
39
|
+
@ssh_priv_key, @ssh_pub_key, @ssh_known_hosts = CemAcpt::Utils::SSH::Ephemeral.create
|
40
|
+
log('Successfully created SSH files.')
|
43
41
|
log("SSH private key: #{@ssh_priv_key}", :debug)
|
44
42
|
log("SSH public key: #{@ssh_pub_key}", :debug)
|
45
43
|
log("SSH known hosts: #{@ssh_known_hosts}", :debug)
|
@@ -48,7 +46,9 @@ module CemAcpt
|
|
48
46
|
# Deletes acceptance test suite SSH files
|
49
47
|
def clean_test_ssh_key
|
50
48
|
log('Deleting ephemeral ssh keys and acpt_known_hosts if they exist...')
|
51
|
-
|
49
|
+
cleaned = CemAcpt::Utils::SSH::Ephemeral.clean
|
50
|
+
log('Successfully cleaned ephemeral ssh files.')
|
51
|
+
log("Deleted: #{cleaned}", :debug)
|
52
52
|
end
|
53
53
|
|
54
54
|
# Prints a period to the terminal every 5 seconds in a single line to keep the terminal
|
@@ -62,8 +62,8 @@ module CemAcpt
|
|
62
62
|
end
|
63
63
|
|
64
64
|
def clean_up_test_suite(opts)
|
65
|
-
@ctx
|
66
|
-
@ctx
|
65
|
+
@ctx&.node_inventory&.clear!
|
66
|
+
@ctx&.node_inventory&.clean_local_files
|
67
67
|
clean_test_ssh_key unless opts[:no_ephemeral_ssh_key]
|
68
68
|
@run_handler&.destroy_test_nodes
|
69
69
|
@keep_terminal_alive&.kill
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CemAcpt
|
4
|
+
module Logging
|
5
|
+
module Formatter
|
6
|
+
class << self
|
7
|
+
def for(log_format)
|
8
|
+
all.find { |f| f.log_format == log_format.downcase.to_sym }.get
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def all
|
14
|
+
@all ||= [FileFormatter.new, JSONFormatter.new, TextFormatter.new, GithubActionFormatter.new]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class FileFormatter
|
19
|
+
attr_reader :log_format
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@log_format = :file
|
23
|
+
end
|
24
|
+
|
25
|
+
def get
|
26
|
+
proc do |severity, datetime, progname, msg|
|
27
|
+
format(severity, datetime, progname, msg)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def format(severity, datetime, _progname, msg)
|
34
|
+
"[#{datetime}] #{severity}: #{msg}\n"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class JSONFormatter < FileFormatter
|
39
|
+
def initialize
|
40
|
+
super
|
41
|
+
@log_format = :json
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def format(severity, datetime, progname, msg)
|
47
|
+
require 'json'
|
48
|
+
{
|
49
|
+
timestamp: datetime,
|
50
|
+
progname: progname,
|
51
|
+
severity: severity,
|
52
|
+
message: msg,
|
53
|
+
}.to_json + "\n"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class TextFormatter < FileFormatter
|
58
|
+
def initialize
|
59
|
+
super
|
60
|
+
@log_format = :text
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def format(severity, _datetime, _progname, msg)
|
66
|
+
"#{severity} - #{msg}\n"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class GithubActionFormatter < FileFormatter
|
71
|
+
SEV_MAP = {
|
72
|
+
'DEBUG' => '::debug',
|
73
|
+
'INFO' => '::notice',
|
74
|
+
'WARN' => '::warning',
|
75
|
+
'ERROR' => '::error',
|
76
|
+
'FATAL' => '::error',
|
77
|
+
}.freeze
|
78
|
+
|
79
|
+
def initialize
|
80
|
+
super
|
81
|
+
@log_format = :github_action
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def format(severity, _datetime, _progname, msg)
|
87
|
+
if severity == 'DEBUG'
|
88
|
+
"#{SEV_MAP[severity]}::{#{msg}}\n"
|
89
|
+
else
|
90
|
+
"#{SEV_MAP[severity]} #{msg}\n"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/cem_acpt/logging.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'logger'
|
4
|
+
require 'cem_acpt/logging/formatter'
|
4
5
|
|
5
6
|
module CemAcpt
|
6
7
|
# Logging for CemAcpt
|
@@ -92,41 +93,7 @@ module CemAcpt
|
|
92
93
|
# @param f [Symbol] the log format style to set
|
93
94
|
# @return [Proc] the proc to be passed to Logger#formatter=
|
94
95
|
def new_log_formatter(f)
|
95
|
-
|
96
|
-
when :json
|
97
|
-
require 'json'
|
98
|
-
@current_log_format = :json
|
99
|
-
proc do |severity, datetime, progname, msg|
|
100
|
-
{
|
101
|
-
timestamp: datetime,
|
102
|
-
progname: progname,
|
103
|
-
severity: severity,
|
104
|
-
message: msg,
|
105
|
-
}.to_json + "\n"
|
106
|
-
end
|
107
|
-
when :text
|
108
|
-
@current_log_format = :text
|
109
|
-
proc do |severity, _datetime, _progname, msg|
|
110
|
-
"#{severity} - #{msg}\n"
|
111
|
-
end
|
112
|
-
when :github_action
|
113
|
-
@current_log_format = :github_action
|
114
|
-
sev_map = {
|
115
|
-
'DEBUG' => '::debug::',
|
116
|
-
'INFO' => '::notice::',
|
117
|
-
'WARN' => '::warning::',
|
118
|
-
'ERROR' => '::error::',
|
119
|
-
'FATAL' => '::error::',
|
120
|
-
}
|
121
|
-
proc do |severity, _datetime, _progname, msg|
|
122
|
-
"#{sev_map[severity]}{#{msg}}\n"
|
123
|
-
end
|
124
|
-
else
|
125
|
-
@current_log_format = :file
|
126
|
-
proc do |severity, datetime, _progname, msg|
|
127
|
-
"[#{datetime}] #{severity}: #{msg}\n"
|
128
|
-
end
|
129
|
-
end
|
96
|
+
CemAcpt::Logging::Formatter.for(f)
|
130
97
|
end
|
131
98
|
|
132
99
|
# Returns the current log config if set, or the default if not.
|
data/lib/cem_acpt/rspec_utils.rb
CHANGED
@@ -12,13 +12,17 @@ module CemAcpt
|
|
12
12
|
class BundlerNotFoundError < StandardError; end
|
13
13
|
class RSpecNotFoundError < StandardError; end
|
14
14
|
|
15
|
-
# Holds
|
16
|
-
class
|
17
|
-
|
18
|
-
attr_reader :debug, :format, :test_path, :use_bundler, :pty_pid
|
15
|
+
# Holds RSpec options used by Command
|
16
|
+
class Options
|
17
|
+
OPTIONS = %i[test_path bundle rspec use_bundler bundle_install format debug quiet env].freeze
|
19
18
|
|
19
|
+
attr_accessor(*OPTIONS)
|
20
|
+
|
21
|
+
# @param config [CemAcpt::Config] A config object
|
20
22
|
# @param opts [Hash] options hash for the RSpec command
|
21
23
|
# @option opts [String] :test_path The path (or glob path) to the test file(s) to run. If blank, runs all.
|
24
|
+
# @option opts [String] :bundle The path to the `bundle` binary.
|
25
|
+
# @option opts [String] :rspec The path to the `rspec` binary.
|
22
26
|
# @option opts [Hash] :format Format options for rspec where the key is the format (documentation, json, etc)
|
23
27
|
# and the value is the out-file path. If you do not want to save the results of a format to a file, the
|
24
28
|
# value should be `nil`.
|
@@ -30,56 +34,97 @@ module CemAcpt
|
|
30
34
|
# Default is `true`.
|
31
35
|
# @option opts [Boolean] :bundle_install Whether or not to run `bundle install` before the RSpec command
|
32
36
|
# if `use_bundler` is `true`.
|
33
|
-
# @option opts [Boolean] :use_shell Whether or not to add `$SHELL` as a prefix to the command
|
34
37
|
# @option opts [Hash] :env Environment variables to prepend to the command
|
35
|
-
def initialize(opts
|
36
|
-
@
|
37
|
-
@
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
38
|
+
def initialize(config, **opts)
|
39
|
+
@config = config
|
40
|
+
@opts = opts
|
41
|
+
define_option_instance_vars
|
42
|
+
end
|
43
|
+
|
44
|
+
# Finds and sets the paths to the `bundle` and `rspec` binaries. The paths can
|
45
|
+
# be either passed in as options in the `opts` Hash or interrogated from the
|
46
|
+
# system.
|
47
|
+
# @raise [CemAcpt::RSpecUtils::BundlerNotFoundError] if `@use_bundler` is true and
|
48
|
+
# `bundle` binary is not found.
|
49
|
+
# @raise [CemAcpt::RSpecUtils::RSpecNotFoundError] if `rspec` binary is not found.
|
50
|
+
def resolve_bin_paths
|
51
|
+
%i[bundle rspec].each do |bin|
|
52
|
+
bin_path = instance_variable_get("@#{bin}") || `#{ENV['SHELL']} -c 'command -v #{bin}'`.strip
|
53
|
+
bin_not_found(bin, bin_path) unless bin_path && File.exist?(bin_path)
|
54
|
+
instance_variable_set("@#{bin}", bin_path)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Detects if the current Ruby context is JRuby
|
59
|
+
def jruby?
|
60
|
+
Object.const_defined?('JRUBY_VERSION')
|
45
61
|
end
|
46
62
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
63
|
+
private
|
64
|
+
|
65
|
+
def defaults
|
66
|
+
@defaults ||= { use_bundler: false,
|
67
|
+
bundle_install: false,
|
68
|
+
format: { documentation: nil },
|
69
|
+
debug: false,
|
70
|
+
quiet: false,
|
71
|
+
env: {} }
|
72
|
+
end
|
73
|
+
|
74
|
+
def define_option_instance_vars
|
75
|
+
OPTIONS.each do |o|
|
76
|
+
val = @config.get("rspec.#{o}") || @opts[o] || defaults[o] || nil
|
77
|
+
instance_variable_set("@#{o}", val)
|
53
78
|
end
|
54
79
|
end
|
55
80
|
|
56
|
-
#
|
57
|
-
|
58
|
-
|
81
|
+
# Handles binary paths which are not found
|
82
|
+
# @param bin [Symbol] The binary that was not found, either :bundle or :rspec.
|
83
|
+
# @param bin_path [String] The path to the binary that was checked.
|
84
|
+
# @raise [CemAcpt::RSpecUtils::BundlerNotFoundError] if `@use_bundler` is true and
|
85
|
+
# `bundle` binary is not found.
|
86
|
+
# @raise [CemAcpt::RSpecUtils::RSpecNotFoundError] if `rspec` binary is not found.
|
87
|
+
# @raise [RuntimeError] if `bin` is not :bundle or :rspec.
|
88
|
+
def bin_not_found(bin, bin_path)
|
89
|
+
msg_base = "#{bin} not found."
|
90
|
+
msg = bin_path.nil? ? "#{msg_base} Path is nil." : "#{msg_base} Path: #{bin_path}"
|
91
|
+
case bin
|
92
|
+
when :bundle
|
93
|
+
raise BundlerNotFoundError, msg if opts.use_bundler
|
94
|
+
when :rspec
|
95
|
+
raise RSpecNotFoundError, msg
|
96
|
+
else
|
97
|
+
raise "bin #{bin} not recognized!"
|
98
|
+
end
|
59
99
|
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Holds and formats a RSpec command
|
103
|
+
class Command
|
104
|
+
include CemAcpt::LoggingAsync
|
105
|
+
attr_reader :opts, :pty_pid
|
106
|
+
|
107
|
+
def initialize(opts = Options.new)
|
108
|
+
raise 'opts must be instance of CemAcpt::RSpecUtils::Options' unless opts.is_a?(CemAcpt::RSpecUtils::Options)
|
60
109
|
|
61
|
-
|
62
|
-
@
|
110
|
+
@opts = opts
|
111
|
+
@opts.resolve_bin_paths
|
112
|
+
@opts.env = @opts.env.merge({ 'RSPEC_DEBUG' => 'true' }) if @opts.debug
|
113
|
+
@pty_pid = nil
|
63
114
|
end
|
64
115
|
|
65
116
|
# Adds a new format to the RSpec command
|
66
117
|
# @param fmt [String] The name of the format (i.e. "documentation", "json", etc.)
|
67
118
|
# @param out [String] If specified, saves the specified format to a file at this path
|
68
119
|
def with_format(fmt, out: nil)
|
69
|
-
|
70
|
-
end
|
71
|
-
|
72
|
-
# Environment variables that will be used for the RSpec command
|
73
|
-
# @return [Hash] A Hash of environment variables with each key pair being: <var name> => <var value>
|
74
|
-
def env
|
75
|
-
@debug ? @env.merge({ 'RSPEC_DEBUG' => 'true' }) : @env
|
120
|
+
opts.format[fmt.to_sym] = out
|
76
121
|
end
|
77
122
|
|
78
123
|
# Returns an array representation of the RSpec command
|
79
124
|
def to_a
|
80
125
|
cmd = cmd_base.dup
|
81
|
-
cmd << test_path if test_path
|
82
|
-
format.each do |fmt, out|
|
126
|
+
cmd << opts.test_path if opts.test_path
|
127
|
+
opts.format.each do |fmt, out|
|
83
128
|
cmd += ['--format', fmt.to_s.shellescape]
|
84
129
|
cmd += ['--out', out.to_s.shellescape] if out
|
85
130
|
end
|
@@ -112,12 +157,12 @@ module CemAcpt
|
|
112
157
|
# @return [Integer] The exit code of the RSpec command
|
113
158
|
def execute_pty(log_prefix: 'RSPEC')
|
114
159
|
async_debug("Executing RSpec command '#{self}' in PTY...", log_prefix)
|
115
|
-
PTY.spawn(env, ENV['SHELL']) do |r, w, pid|
|
160
|
+
PTY.spawn(opts.env, ENV['SHELL']) do |r, w, pid|
|
116
161
|
@pty_pid = pid
|
117
162
|
async_debug("Spawned RSpec PTY with PID #{@pty_pid}", log_prefix)
|
118
163
|
export_envs(w)
|
119
164
|
w.puts "#{self}; exit $?"
|
120
|
-
|
165
|
+
handle_io(r, log_prefix: log_prefix)
|
121
166
|
end
|
122
167
|
$CHILD_STATUS
|
123
168
|
end
|
@@ -130,9 +175,9 @@ module CemAcpt
|
|
130
175
|
def execute_no_pty(log_prefix: 'RSPEC')
|
131
176
|
async_info("Executing RSpec command '#{self}' with Open3.popen2e()...", log_prefix)
|
132
177
|
exit_status = nil
|
133
|
-
Open3.popen2e(env, to_s) do |stdin, std_out_err, wait_thr|
|
178
|
+
Open3.popen2e(opts.env, to_s) do |stdin, std_out_err, wait_thr|
|
134
179
|
stdin.close
|
135
|
-
|
180
|
+
handle_io(std_out_err, log_prefix: log_prefix)
|
136
181
|
exit_status = wait_thr.value
|
137
182
|
end
|
138
183
|
exit_status
|
@@ -147,26 +192,21 @@ module CemAcpt
|
|
147
192
|
|
148
193
|
private
|
149
194
|
|
150
|
-
# Detects if the current Ruby context is JRuby
|
151
|
-
def jruby?
|
152
|
-
File.basename(RbConfig.ruby) == 'jruby'
|
153
|
-
end
|
154
|
-
|
155
195
|
# The base RSpec command
|
156
196
|
def cmd_base
|
157
|
-
use_bundler ? cmd_base_bundler : cmd_base_rspec
|
197
|
+
opts.use_bundler ? cmd_base_bundler : cmd_base_rspec
|
158
198
|
end
|
159
199
|
|
160
200
|
# The base RSpec command if `:use_bundler` is `true`.
|
161
201
|
def cmd_base_bundler
|
162
|
-
base = [
|
163
|
-
base.unshift("#{
|
202
|
+
base = [opts.bundle, 'exec', 'rspec']
|
203
|
+
base.unshift("#{opts.bundle} install;") if opts.bundle_install
|
164
204
|
base
|
165
205
|
end
|
166
206
|
|
167
207
|
# The base RSpec command if `:use_bundler` is `false`
|
168
208
|
def cmd_base_rspec
|
169
|
-
[
|
209
|
+
[opts.rspec]
|
170
210
|
end
|
171
211
|
|
172
212
|
# Puts export statements for each key-value pair in `env` to the given writer.
|
@@ -174,46 +214,14 @@ module CemAcpt
|
|
174
214
|
# pass the statements to a shell.
|
175
215
|
# @param writer [IO] An IO object that supprts `puts` and can send statements to a shell
|
176
216
|
def export_envs(writer)
|
177
|
-
env.each do |key, val|
|
217
|
+
@opts.env.each do |key, val|
|
178
218
|
writer.puts "export #{key}=#{val}"
|
179
219
|
end
|
180
220
|
end
|
181
221
|
|
182
|
-
#
|
183
|
-
|
184
|
-
|
185
|
-
# @param opts [Hash] The options hash
|
186
|
-
# @option opts [String] :bundle An absolute path on the system to the `bundle` binary.
|
187
|
-
# @option opts [String] :rspec An absolute path on the system to the `rspec` binary.
|
188
|
-
# @raise [CemAcpt::RSpecUtils::BundlerNotFoundError] if `@use_bundler` is true and
|
189
|
-
# `bundle` binary is not found.
|
190
|
-
# @raise [CemAcpt::RSpecUtils::RSpecNotFoundError] if `rspec` binary is not found.
|
191
|
-
def validate_and_set_bin_paths(opts = {})
|
192
|
-
%i[bundle rspec].each do |bin|
|
193
|
-
bin_path = opts[bin] || `command -v #{bin}`.strip
|
194
|
-
bin_not_found(bin, bin_path) unless bin_path && File.exist?(bin_path)
|
195
|
-
instance_variable_set("@#{bin}", bin_path)
|
196
|
-
end
|
197
|
-
end
|
198
|
-
|
199
|
-
# Handles binary paths which are not found
|
200
|
-
# @param bin [Symbol] The binary that was not found, either :bundle or :rspec.
|
201
|
-
# @param bin_path [String] The path to the binary that was checked.
|
202
|
-
# @raise [CemAcpt::RSpecUtils::BundlerNotFoundError] if `@use_bundler` is true and
|
203
|
-
# `bundle` binary is not found.
|
204
|
-
# @raise [CemAcpt::RSpecUtils::RSpecNotFoundError] if `rspec` binary is not found.
|
205
|
-
# @raise [RuntimeError] if `bin` is not :bundle or :rspec.
|
206
|
-
def bin_not_found(bin, bin_path)
|
207
|
-
msg_base = "#{bin} not found."
|
208
|
-
msg = bin_path.nil? ? "#{msg_base} Path is nil." : "#{msg_base} Path: #{bin_path}"
|
209
|
-
case bin
|
210
|
-
when :bundle
|
211
|
-
raise BundlerNotFoundError, msg if @use_bundler
|
212
|
-
when :rspec
|
213
|
-
raise RSpecNotFoundError, msg
|
214
|
-
else
|
215
|
-
raise "bin #{bin} not recognized!"
|
216
|
-
end
|
222
|
+
# Handles IO output
|
223
|
+
def handle_io(io_stream, **kwargs)
|
224
|
+
opts.quiet ? wait_io(io_stream) : read_io(io_stream, log_prefix: kwargs[:log_prefix])
|
217
225
|
end
|
218
226
|
|
219
227
|
# Blocking wait on an IO stream. Wait stops once the IO stream has reached
|
@@ -105,10 +105,7 @@ module CemAcpt
|
|
105
105
|
if result['examples'].empty? && !result['messages'].empty?
|
106
106
|
logger.error(result['messages'].join("\n"))
|
107
107
|
else
|
108
|
-
|
109
|
-
failed.each do |e|
|
110
|
-
logger.error(test_error_msg(runner.node.node_name, e))
|
111
|
-
end
|
108
|
+
handle_example_error_pending_skipped(runner.node.node_name, result['examples'])
|
112
109
|
end
|
113
110
|
else
|
114
111
|
handle_runner_error_results(runner)
|
@@ -118,6 +115,19 @@ module CemAcpt
|
|
118
115
|
end
|
119
116
|
end
|
120
117
|
|
118
|
+
# Handles logging for any failed / skipped / pending examples.
|
119
|
+
def handle_example_error_pending_skipped(node_name, examples)
|
120
|
+
examples.each do |e|
|
121
|
+
next if e['status'] == 'passed'
|
122
|
+
|
123
|
+
if e['status'] == 'pending'
|
124
|
+
logger.info(test_pending_msg(node_name, e))
|
125
|
+
else
|
126
|
+
logger.error(test_error_msg(node_name, e))
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
121
131
|
# Handles logging the results of the runners that errored.
|
122
132
|
def handle_runner_error_results(runner)
|
123
133
|
logger.error("SUMMARY: Encountered an error with test #{runner.node.test_data[:test_name]} on node #{runner.node.node_name}")
|
@@ -136,6 +146,19 @@ module CemAcpt
|
|
136
146
|
"Error: #{err.message}",
|
137
147
|
'Backtrace:',
|
138
148
|
err.backtrace.join("\n"),
|
149
|
+
"\n",
|
150
|
+
].join("\n")
|
151
|
+
end
|
152
|
+
|
153
|
+
# Formats a test result for tests that are pending or skipped. Is used for logging.
|
154
|
+
# @param node [String] the name of the node the test ran on
|
155
|
+
# @param result [Hash] the test result to format
|
156
|
+
# @return [String] the formatted test result
|
157
|
+
def test_pending_msg(_, result)
|
158
|
+
[
|
159
|
+
"TEST PENDING / SKIPPED: #{result['full_description']}",
|
160
|
+
"REASON: #{result['pending_message']}",
|
161
|
+
"\n",
|
139
162
|
].join("\n")
|
140
163
|
end
|
141
164
|
|
@@ -145,12 +145,12 @@ module CemAcpt
|
|
145
145
|
async_info("Running test #{@node.test_data[:test_name]} on node #{@node.node_name}...", log_prefix('RSPEC'))
|
146
146
|
@node.run_tests do |cmd_env|
|
147
147
|
cmd_opts = rspec_opts
|
148
|
-
cmd_opts
|
148
|
+
cmd_opts.env = cmd_opts.env.merge(cmd_env) if cmd_env
|
149
149
|
# Documentation format gets logged in real time, JSON file is read after the fact
|
150
150
|
begin
|
151
151
|
@rspec_cmd = CemAcpt::RSpecUtils::Command.new(cmd_opts)
|
152
|
-
@rspec_cmd.execute(log_prefix: log_prefix('RSPEC'))
|
153
|
-
@run_result.from_json_file(cmd_opts
|
152
|
+
@rspec_cmd.execute(pty: false, log_prefix: log_prefix('RSPEC'))
|
153
|
+
@run_result.from_json_file(cmd_opts.format[:json])
|
154
154
|
rescue Errno::EIO => e
|
155
155
|
async_error("failed to run rspec: #{@node.test_data[:test_name]}: #{$ERROR_INFO}", log_prefix('RSPEC'))
|
156
156
|
@run_result.from_error(e)
|
@@ -191,19 +191,21 @@ module CemAcpt
|
|
191
191
|
|
192
192
|
# Options used with RSpec
|
193
193
|
def rspec_opts
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
194
|
+
opts_test_path = @node.test_data[:test_file]
|
195
|
+
opts_env = { 'TARGET_HOST' => @node.node_name }
|
196
|
+
opts_debug = (@debug_mode && @context.config.get('verbose'))
|
197
|
+
opts_quiet = @context.config.get('quiet')
|
198
|
+
opts_format = if @context.config.get('verbose')
|
199
|
+
{ json: "results_#{@node.test_data[:test_name]}.json", documentation: nil }
|
200
|
+
else
|
201
|
+
{ json: "results_#{@node.test_data[:test_name]}.json" }
|
202
|
+
end
|
203
|
+
CemAcpt::RSpecUtils::Options.new(@context.config,
|
204
|
+
test_path: opts_test_path,
|
205
|
+
env: opts_env,
|
206
|
+
debug: opts_debug,
|
207
|
+
quiet: opts_quiet,
|
208
|
+
format: opts_format)
|
207
209
|
end
|
208
210
|
end
|
209
211
|
end
|
data/lib/cem_acpt/utils.rb
CHANGED
@@ -81,6 +81,40 @@ module CemAcpt
|
|
81
81
|
|
82
82
|
# SSH-related utilities
|
83
83
|
module SSH
|
84
|
+
module Ephemeral
|
85
|
+
PRIV_KEY = 'acpt_test_key'
|
86
|
+
CREATE_OPTS = {
|
87
|
+
type: 'rsa',
|
88
|
+
bits: '4096',
|
89
|
+
comment: 'Ephemeral for cem_acpt',
|
90
|
+
password: '',
|
91
|
+
known_hosts: 'acpt_known_hosts',
|
92
|
+
overwrite_known_hosts: true,
|
93
|
+
}.freeze
|
94
|
+
|
95
|
+
class << self
|
96
|
+
attr_accessor :ephemeral_keydir
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.create(keydir: CemAcpt::Utils::SSH.default_keydir)
|
100
|
+
self.ephemeral_keydir = keydir
|
101
|
+
@priv_key, @pub_key, @known_hosts = CemAcpt::Utils::SSH.create(PRIV_KEY, keydir: ephemeral_keydir, **CREATE_OPTS)
|
102
|
+
[@priv_key, @pub_key, @known_hosts]
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.clean
|
106
|
+
[@priv_key, @pub_key, @known_hosts].each_with_object([]) do |f, arr|
|
107
|
+
next unless f
|
108
|
+
|
109
|
+
path = CemAcpt::Utils::SSH.file_path(f, keydir: ephemeral_keydir)
|
110
|
+
if ::File.exist?(path)
|
111
|
+
::File.delete(path)
|
112
|
+
arr << path
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
84
118
|
def self.ssh_keygen
|
85
119
|
bin_path = `#{ENV['SHELL']} -c 'command -v ssh-keygen'`.chomp
|
86
120
|
raise 'Cannot find ssh-keygen! Install it and verify PATH' unless bin_path
|
@@ -97,26 +131,52 @@ module CemAcpt
|
|
97
131
|
ssh_dir
|
98
132
|
end
|
99
133
|
|
100
|
-
def self.
|
101
|
-
|
134
|
+
def self.file_path(file_name, keydir: default_keydir)
|
135
|
+
::File.join(keydir, file_name)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Takes a file name (not path) and optional SSH key directory and returns the paths
|
139
|
+
# to the private key and public key based on the file name given.
|
140
|
+
# @param file_name [String] The base name for the keys
|
141
|
+
# @param keydir [String] An optional SSH key directory
|
142
|
+
def self.key_paths(file_name, keydir: default_keydir)
|
143
|
+
[file_path(file_name, keydir: keydir), file_path("#{file_name}.pub", keydir: keydir)]
|
144
|
+
end
|
102
145
|
|
103
|
-
|
104
|
-
|
146
|
+
def self.create(key_name, type: 'rsa', bits: '4096', comment: nil, password: '', known_hosts: nil, overwrite_known_hosts: true, keydir: default_keydir)
|
147
|
+
raise ArgumentError, "Key directory #{keydir} does not exist" unless ::File.directory?(keydir)
|
148
|
+
|
149
|
+
keys = key_paths(key_name, keydir: keydir)
|
150
|
+
# If we don't delete an existing key file, generation will fail
|
151
|
+
keys.each { |f| ::File.delete(f) if ::File.exist?(f) }
|
152
|
+
keygen_cmd = [ssh_keygen, "-t #{type}", "-b #{bits}", "-f #{keys[0]}", "-N '#{password}'"]
|
105
153
|
keygen_cmd << "-C \"#{comment}\"" if comment
|
106
154
|
_, stderr, status = Open3.capture3(keygen_cmd.join(' '))
|
107
|
-
raise "Failed to generate ephemeral SSH key: #{stderr}" unless status.success?
|
155
|
+
raise "Failed to generate ephemeral SSH key: STDOUT: #{stdout}; STDERR: #{stderr}" unless status.success?
|
108
156
|
|
109
|
-
|
157
|
+
keys << create_known_hosts(known_hosts, overwrite: overwrite_known_hosts, keydir: keydir)
|
158
|
+
set_ssh_file_permissions(*keys)
|
159
|
+
keys
|
110
160
|
end
|
111
161
|
|
112
|
-
def self.
|
113
|
-
|
162
|
+
def self.create_known_hosts(known_hosts, overwrite: true, keydir: default_keydir)
|
163
|
+
return nil unless known_hosts
|
164
|
+
|
165
|
+
kh_file = file_path(known_hosts, keydir: keydir)
|
114
166
|
::File.open(kh_file, 'w') { |f| f.write("\n") } unless ::File.exist?(kh_file) && !overwrite
|
115
167
|
kh_file
|
116
168
|
end
|
117
169
|
|
118
|
-
def self.set_ssh_file_permissions(
|
119
|
-
CemAcpt::Utils::File.set_permissions(0o600,
|
170
|
+
def self.set_ssh_file_permissions(*files)
|
171
|
+
CemAcpt::Utils::File.set_permissions(0o600, *files.uniq.compact)
|
172
|
+
end
|
173
|
+
|
174
|
+
def self.ephemeral_ssh_key(keydir: default_keydir)
|
175
|
+
CemAcpt::Utils::SSH::Ephemeral.create(keydir: keydir)
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.clean_ephemeral_keys
|
179
|
+
CemAcpt::Utils::SSH::Ephemeral.clean
|
120
180
|
end
|
121
181
|
end
|
122
182
|
|
data/lib/cem_acpt/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cem_acpt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.11
|
5
5
|
platform: universal-java-17
|
6
6
|
authors:
|
7
7
|
- puppetlabs
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-08-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
@@ -160,6 +160,7 @@ files:
|
|
160
160
|
- lib/cem_acpt/core_extensions.rb
|
161
161
|
- lib/cem_acpt/image_name_builder.rb
|
162
162
|
- lib/cem_acpt/logging.rb
|
163
|
+
- lib/cem_acpt/logging/formatter.rb
|
163
164
|
- lib/cem_acpt/platform.rb
|
164
165
|
- lib/cem_acpt/platform/base.rb
|
165
166
|
- lib/cem_acpt/platform/base/cmd.rb
|