cem_acpt 0.2.5 → 0.6.1

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 +38 -0
  3. data/Gemfile +4 -3
  4. data/Gemfile.lock +85 -56
  5. data/README.md +144 -83
  6. data/cem_acpt.gemspec +8 -7
  7. data/exe/cem_acpt +41 -7
  8. data/lib/cem_acpt/config.rb +345 -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 +70 -20
  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 +88 -56
  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 -345
  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,332 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CemAcpt::Platform::Gcp
4
- require_relative 'cmd'
5
- require_relative File.join(__dir__, '..', '..', 'logging.rb')
6
-
7
- module Helper
8
- def add_cmd(cmd_obj)
9
- @cmd = cmd_obj
10
- end
11
-
12
- def cmd
13
- @cmd ||= CemAcpt::Platform::Gcp::Cmd.new(out_format: 'json')
14
- end
15
-
16
- def cmd_flag_vars?(*vars)
17
- vars.none? { |v| v.nil? || (v.empty? if v.respond_to?(:empty?)) }
18
- end
19
- end
20
-
21
- # This class represents a GCP project
22
- class Project
23
- include Helper
24
-
25
- def initialize(name: nil, zone: nil)
26
- @name = name
27
- @zone = zone
28
- end
29
-
30
- def name
31
- @name ||= cmd.project_from_config
32
- end
33
-
34
- def zone
35
- @zone ||= cmd.zone_from_config
36
- end
37
-
38
- def cmd_flag
39
- "--project=#{name}"
40
- end
41
- end
42
-
43
- # This class represents a GCP VM service account
44
- class ServiceAccount
45
- include Helper
46
-
47
- attr_reader :name, :scopes
48
-
49
- def initialize(name: nil, scopes: ['devstorage.read_only', 'logging.write', 'monitoring.write', 'servicecontrol', 'service.management.readonly', 'trace.append' ])
50
- @name = name
51
- @scopes = scopes.map { |s| "https://www.googleapis.com/auth/#{s}" }
52
- end
53
-
54
- def cmd_flag
55
- "--service-account=#{name} --scopes=#{scopes.join(',')}" if cmd_flag_vars?(name, scopes)
56
- end
57
- end
58
-
59
- # This class represents a GCP VM disk
60
- class Disk
61
- include Helper
62
-
63
- attr_reader :project, :name, :size, :image_name
64
-
65
- def initialize(project: nil, name: 'disk-1', image_name: '', size: 20, type: 'pd-standard')
66
- @project = project
67
- @name = name
68
- @image_name = image_name
69
- @size = size
70
- @type = type
71
- end
72
-
73
- def cmd_flag
74
- return unless cmd_flag_vars?(name, image, size, type)
75
-
76
- '--create-disk=auto-delete=yes,boot=yes,' \
77
- "device-name=#{name},image=#{image}," \
78
- "mode=rw,size=#{size},type=#{type}"
79
- end
80
-
81
- def type
82
- type_path(@type)
83
- end
84
-
85
- def image
86
- @image ||= image_path(@image_name)
87
- end
88
-
89
- private
90
-
91
- def image_path(image_str)
92
- raise ArgumentError, 'image must be a non-empty string' unless image_str.is_a?(String) && !image_str.empty?
93
-
94
- data = cmd.local_exec('compute images list', out_filter: "name ~ '#{image_str}$'").first
95
- if data.nil? || data.empty?
96
- data = cmd.local_exec('compute images list', out_filter: "family ~ '#{image_str}$'").first
97
- end
98
- raise "Image #{image_str} not found" unless data
99
- raise "Image status invalid for image #{data['selfLink']}: #{data[status]}" unless data['status'] == 'READY'
100
-
101
- data['selfLink'].split('v1/')[-1]
102
- end
103
-
104
- def type_path(type)
105
- raise ArgumentError, 'type must be a non-empty string' unless type.is_a?(String) && !type.empty?
106
-
107
- "projects/#{project.name}/zones/#{project.zone}/diskTypes/#{type}"
108
- end
109
- end
110
-
111
- # This class represents a GCP VM network interface
112
- class NetworkInterface
113
- include Helper
114
-
115
- attr_reader :tier, :subnetwork
116
-
117
- def initialize(tier: 'STANDARD', subnetwork: 'default')
118
- @tier = tier.upcase
119
- @subnetwork = subnetwork
120
- end
121
-
122
- def cmd_flag
123
- "--network-interface=network-tier=#{tier},subnet=#{subnetwork}" if cmd_flag_vars?(tier, subnetwork)
124
- end
125
- end
126
-
127
- # This class represents GCP VM metadata
128
- class Metadata
129
- include Helper
130
-
131
- def initialize(**data)
132
- @data = data.transform_keys(&:to_s)
133
- end
134
-
135
- def data
136
- format_data(@data)
137
- end
138
-
139
- def cmd_flag
140
- return unless cmd_flag_vars?(data)
141
-
142
- data_from_file = data.select { |k, _v| k == 'ssh-keys' }.map { |k, v| "#{k}=#{v}" }.join(',')
143
- normalized_data = data.reject { |k, _v| k == 'ssh-keys' }.map { |k, v| "#{k}=#{v}" }.join(',')
144
- cmd = []
145
- cmd << "--metadata-from-file=#{data_from_file}" unless data_from_file.empty?
146
- cmd << "--metadata=#{normalized_data}" unless normalized_data.empty?
147
- cmd.join(' ')
148
- end
149
-
150
- private
151
-
152
- def format_data(data)
153
- default_data = { 'enable-oslogin' => 'TRUE' }
154
- if data['ephemeral_ssh_key']
155
- args = {
156
- username: (data.dig('ephemeral_ssh_key', 'username') || cmd.user_name),
157
- }
158
- args[:lifetime] = data.dig('ephemeral_ssh_key', 'lifetime') if data.dig('ephemeral_ssh_key', 'lifetime')
159
- args[:keydir] = data.dig('ephemeral_ssh_key', 'keydir') if data.dig('ephemeral_ssh_key', 'keydir')
160
- priv_key_path, ssh_keys = ephemeral_ssh_key(**args)
161
- default_data['priv_key_local_path'] = priv_key_path
162
- default_data['ssh-keys'] = ssh_keys
163
- data.delete('ephemeral_ssh_key')
164
- end
165
- default_data.merge(data)
166
- end
167
-
168
- def ephemeral_ssh_key(username: nil, lifetime: 2, keydir: '/tmp/acpt_ssh_keys')
169
- raise ArgumentError, 'username must be a non-empty string' unless username.is_a?(String) && !username.empty?
170
-
171
- keypath = File.join(keydir, SecureRandom.hex(10))
172
- Dir.mkdir(keydir) unless Dir.exist?(keydir)
173
- lifetime_in_seconds = (lifetime.to_i * 60 * 60)
174
- iso8601_expiration = (Time.now.utc + lifetime_in_seconds).strftime('%Y-%m-%dT%H:%M:%S%z')
175
- keygen_cmd = [
176
- 'ssh-keygen',
177
- '-t ecdsa',
178
- '-b 521',
179
- "-C 'google-ssh {\"userName\":\"#{username}\",\"expireOn\":\"#{iso8601_expiration}\"}'",
180
- "-f #{keypath}",
181
- '-N ""',
182
- '-q',
183
- ].join(' ')
184
- _, stderr, status = Open3.capture3(keygen_cmd)
185
- raise stderr unless status.success?
186
-
187
- File.write("#{keypath}.pub", "#{username}:#{File.read("#{keypath}.pub")}")
188
- [keypath, "#{keypath}.pub"]
189
- rescue StandardError => e
190
- raise "Failed to generate ephemeral SSH key: #{e} #{e.backtrace}"
191
- end
192
- end
193
-
194
- # This class represents a GCP VM. It is composed of various component classes.
195
- class VM
196
- include Helper
197
- include CemAcpt::Logging
198
-
199
- attr_accessor :name, :project, :service_account, :disk, :network_interface, :machine_type, :metadata
200
- attr_reader :cmd
201
-
202
- def initialize(name, components: {})
203
- @name = name
204
- @components = components
205
- @machine_type = components[:machine_type]
206
- @cmd = CemAcpt::Platform::Gcp::Cmd.new(
207
- project: components[:project][:name],
208
- zone: components[:project][:zone],
209
- out_format: 'json',
210
- local_port: components[:local_port],
211
- )
212
- @configured = false
213
- end
214
-
215
- def configure!
216
- return @configured if @configured
217
-
218
- @project = Project.new(**@components[:project])
219
- @project.add_cmd(@cmd)
220
- @service_account = @components.key?(:service_account) ? ServiceAccount.new(**@components[:service_account]) : nil
221
- @disk = Disk.new(project: @project, **@components[:disk])
222
- @disk.add_cmd(@cmd)
223
- @network_interface = NetworkInterface.new(**@components[:network_interface])
224
- @machine_type = @components[:machine_type]
225
- @metadata = Metadata.new(**@components[:metadata])
226
- @metadata.add_cmd(@cmd)
227
- @configured = true
228
- end
229
-
230
- def info
231
- describe_cmd = "compute instances describe #{name}"
232
- logger.debug("Gathering info for VM #{name} with gcloud command: #{describe_cmd}")
233
- data = @cmd.local_exec(describe_cmd, out_format: 'json')
234
- opts = @cmd.ssh_opts(instance_name: name)
235
- {
236
- node_data: data,
237
- transport: :ssh,
238
- ssh_opts: opts,
239
- }
240
- end
241
-
242
- def create
243
- # Add the test ssh key to os-login
244
- logger.debug("Adding test SSH key to os-login for #{name}")
245
- @cmd.local_exec("compute os-login ssh-keys add --key-file #{@cmd.ssh_key}.pub --project #{project.name} --ttl 4h")
246
- @cmd.local_exec(create_cmd)
247
- rescue StandardError => e
248
- raise "Failed to create VM #{name} with command #{create_cmd}: #{e}"
249
- end
250
-
251
- def ready?
252
- logger.debug("Checking if VM #{name} is ready")
253
- instance_status = @cmd.local_exec('compute instances list', out_filter: "NAME = #{name}").first['status']
254
- logger.debug("Instance #{name} status: #{instance_status}")
255
- return false unless instance_status == 'RUNNING'
256
-
257
- logger.debug("Checking instance #{name} SSH connectivity")
258
- @cmd.ssh_ready?(name)
259
- rescue StandardError, Exception
260
- false
261
- end
262
-
263
- def destroy
264
- @cmd.delete_instance(name)
265
- end
266
-
267
- def install_puppet_module_package(module_pkg_path, remote_path, puppet_path)
268
- @cmd.scp_upload(@name, module_pkg_path, remote_path)
269
- logger.info("Uploaded module package #{module_pkg_path} to #{remote_path} on #{@name}")
270
- @cmd.ssh(@name, "sudo #{puppet_path} module install #{remote_path}")
271
- logger.info("Installed module package #{remote_path} on #{@name}")
272
- end
273
-
274
- private
275
-
276
- # Determines whether the given instance name is a fully qualified
277
- # hostname or not.
278
- def hostname?(instance_name)
279
- instance_name.include?('.')
280
- end
281
-
282
- # Validates a GCP instance name.
283
- def validate_instance_name(instance_name)
284
- if instance_name.nil? || !instance_name.is_a?(String) || instance_name.empty?
285
- raise ArgumentError, 'Instance name must be a non-empty string'
286
- end
287
- raise ArgumentError, 'Instance name must be less than 253 characters' if instance_name.length > 253
288
-
289
- return true unless hostname?(instance_name)
290
-
291
- labels = instance_name.split('.')
292
- labels.each do |label|
293
- unless label.match?(%r{^[a-z]([-a-z0-9]*[a-z0-9])?$})
294
- raise ArgumentError, "Instance name portion #{label} contains invalid characters"
295
- end
296
- raise ArgumentError, "Instance name portion #{label} is too long" if label.length > 63
297
- end
298
- true
299
- end
300
-
301
- def create_cmd
302
- validate_instance_name(name)
303
- if hostname?(name)
304
- instance_name = name.split('.')[0]
305
- hostname = name
306
- else
307
- instance_name = name
308
- hostname = nil
309
- end
310
-
311
- cmd = [
312
- 'compute',
313
- 'instances',
314
- 'create',
315
- instance_name,
316
- '--async',
317
- "--machine-type=#{machine_type}",
318
- '--maintenance-policy=MIGRATE',
319
- '--no-shielded-secure-boot',
320
- '--shielded-vtpm',
321
- '--shielded-integrity-monitoring',
322
- '--reservation-affinity=any',
323
- '--tags=acpt-test-node',
324
- ]
325
- [disk&.cmd_flag, network_interface&.cmd_flag, metadata&.cmd_flag, service_account&.cmd_flag].each do |flag|
326
- cmd << flag unless flag.nil?
327
- end
328
- cmd << "--hostname=#{hostname}" unless hostname.nil?
329
- cmd.join(' ')
330
- end
331
- end
332
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Currently a placeholder for future functionality
4
- module Platform
5
- def node
6
- logger.debug 'VMPooler node'
7
- end
8
-
9
- def provision
10
- logger.debug 'VMPooler provision'
11
- end
12
-
13
- def destroy
14
- logger.debug 'VMPooler destroy'
15
- end
16
-
17
- def ready?
18
- logger.debug 'VMPooler ready?'
19
- end
20
-
21
- def install_puppet_module_package
22
- logger.debug 'VMPooler install_puppet_module_package'
23
- end
24
- end
@@ -1,242 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'English'
4
- require 'open3'
5
- require 'pty'
6
- require 'shellwords'
7
- require_relative 'logging'
8
-
9
- module CemAcpt
10
- # Module that provides methods and objects for running and managing RSpec
11
- module RSpecUtils
12
- class BundlerNotFoundError < StandardError; end
13
- class RSpecNotFoundError < StandardError; end
14
-
15
- # Holds and formats a RSpec command
16
- class Command
17
- include CemAcpt::LoggingAsync
18
- attr_reader :debug, :format, :test_path, :use_bundler, :pty_pid
19
-
20
- # @param opts [Hash] options hash for the RSpec command
21
- # @option opts [String] :test_path The path (or glob path) to the test file(s) to run. If blank, runs all.
22
- # @option opts [Hash] :format Format options for rspec where the key is the format (documentation, json, etc)
23
- # 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
- # value should be `nil`.
25
- # @option opts [Boolean] :debug True if RSpec should run in debug mode, false if not. Mutually exclusive with
26
- # `:quiet`. Default is `false`.
27
- # @option opts [Boolean] :quiet True if no output should be logged from RSpec command, false if output should
28
- # be logged. Mutually exclusive with `:debug`. Default is `false`.
29
- # @option opts [Boolean] :use_bundler Whether or not `bundle exec` should be used to run the RSpec command.
30
- # Default is `true`.
31
- # @option opts [Boolean] :bundle_install Whether or not to run `bundle install` before the RSpec command
32
- # if `use_bundler` is `true`.
33
- # @option opts [Boolean] :use_shell Whether or not to add `$SHELL` as a prefix to the command
34
- # @option opts [Hash] :env Environment variables to prepend to the command
35
- def initialize(opts = {})
36
- @test_path = opts[:test_path]&.shellescape
37
- @format = opts.fetch(:format, {})
38
- @debug = opts.fetch(:debug, false)
39
- @quiet = @debug ? false : opts.fetch(:quiet, false)
40
- @use_bundler = opts.fetch(:use_bundler, false)
41
- @bundle_install = opts.fetch(:bundle_install, false)
42
- @env = opts.fetch(:env, {})
43
- @pty_pid = nil
44
- validate_and_set_bin_paths(opts)
45
- end
46
-
47
- # Sets debug mode to `true`
48
- def set_debug
49
- @debug = true
50
- if @quiet
51
- async_debug('Setting :quiet to false because :debug is now true.')
52
- @quiet = false
53
- end
54
- end
55
-
56
- # Sets debug mode to `false`
57
- def unset_debug
58
- @debug = false
59
- end
60
-
61
- def quiet
62
- @quiet && !debug
63
- end
64
-
65
- # Adds a new format to the RSpec command
66
- # @param fmt [String] The name of the format (i.e. "documentation", "json", etc.)
67
- # @param out [String] If specified, saves the specified format to a file at this path
68
- def with_format(fmt, out: nil)
69
- @format[fmt.to_sym] = out
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
76
- end
77
-
78
- # Returns an array representation of the RSpec command
79
- def to_a
80
- cmd = cmd_base.dup
81
- cmd << test_path if test_path
82
- format.each do |fmt, out|
83
- cmd += ['--format', fmt.to_s.shellescape]
84
- cmd += ['--out', out.to_s.shellescape] if out
85
- end
86
- cmd.compact
87
- end
88
-
89
- # Returns a string representation of the RSpec command
90
- def to_s
91
- to_a.join(' ')
92
- end
93
-
94
- # Executes the RSpec command on the current machine
95
- # @param pty [Boolean] If true, execute command in a PTY. If false, execute command directly.
96
- # @param log_prefix [String] A prefix to add to log messages generated while the command is running.
97
- def execute(pty: true, log_prefix: 'RSPEC')
98
- if pty
99
- execute_pty(log_prefix: log_prefix)
100
- else
101
- execute_no_pty(log_prefix: log_prefix)
102
- end
103
- end
104
-
105
- # Executes the RSpec command in a psuedo-terminal (PTY). First, it spawns a process
106
- # for $SHELL, sets environment variables `export_envs`, then calls the current RSpec
107
- # command in the shell and exits with the last exit code `$?`. Output is read from the
108
- # RSpec command in near real-time in a blocking manner unless the `:quiet` option has
109
- # been specified.
110
- # @param log_prefix [String] A prefix to add to the log messages that are output from
111
- # the RSpec command.
112
- # @return [Integer] The exit code of the RSpec command
113
- def execute_pty(log_prefix: 'RSPEC')
114
- async_debug("Executing RSpec command '#{self}' in PTY...", log_prefix)
115
- PTY.spawn(env, ENV['SHELL']) do |r, w, pid|
116
- @pty_pid = pid
117
- async_debug("Spawned RSpec PTY with PID #{@pty_pid}", log_prefix)
118
- export_envs(w)
119
- w.puts "#{self}; exit $?"
120
- quiet ? wait_io(r) : read_io(r, log_prefix: log_prefix)
121
- end
122
- $CHILD_STATUS
123
- end
124
-
125
- # Executes the RSpec command using Open3.popen2e(). The output stream, which is both
126
- # stderr and stdout, is read in real-time in a non-blocking manner.
127
- # @param log_prefix [String] A prefix to add to the log messages that are output from
128
- # the RSpec command.
129
- # @return [Integer] The exit code of the RSpec command
130
- def execute_no_pty(log_prefix: 'RSPEC')
131
- async_info("Executing RSpec command '#{self}' with Open3.popen2e()...", log_prefix)
132
- exit_status = nil
133
- Open3.popen2e(env, to_s) do |stdin, std_out_err, wait_thr|
134
- stdin.close
135
- quiet ? wait_io(std_out_err) : read_io(std_out_err, log_prefix: log_prefix)
136
- exit_status = wait_thr.value
137
- end
138
- exit_status
139
- end
140
-
141
- # Kills the PTY process with `SIGKILL` if the process exists
142
- def kill_pty
143
- Process.kill('KILL', @pty_pid) unless @pty_pid.nil?
144
- rescue Errno::ESRCH
145
- true
146
- end
147
-
148
- private
149
-
150
- # Detects if the current Ruby context is JRuby
151
- def jruby?
152
- File.basename(RbConfig.ruby) == 'jruby'
153
- end
154
-
155
- # The base RSpec command
156
- def cmd_base
157
- use_bundler ? cmd_base_bundler : cmd_base_rspec
158
- end
159
-
160
- # The base RSpec command if `:use_bundler` is `true`.
161
- def cmd_base_bundler
162
- base = [@bundle, 'exec', 'rspec']
163
- base.unshift("#{@bundle} install;") if @bundle_install
164
- base
165
- end
166
-
167
- # The base RSpec command if `:use_bundler` is `false`
168
- def cmd_base_rspec
169
- [@rspec]
170
- end
171
-
172
- # Puts export statements for each key-value pair in `env` to the given writer.
173
- # Writer is the write pipe of a PTY session, or a similar IO object that can
174
- # pass the statements to a shell.
175
- # @param writer [IO] An IO object that supprts `puts` and can send statements to a shell
176
- def export_envs(writer)
177
- env.each do |key, val|
178
- writer.puts "export #{key}=#{val}"
179
- end
180
- end
181
-
182
- # Finds and sets the paths to the `bundle` and `rspec` binaries. The paths can
183
- # be either passed in as options in the `opts` Hash or interrogated from the
184
- # system.
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
217
- end
218
-
219
- # Blocking wait on an IO stream. Wait stops once the IO stream has reached
220
- # end of file.
221
- # @param stdout [IO] An IO stream with output that can be read from.
222
- def wait_io(stdout)
223
- sleep(0.01) until stdout.eof?
224
- end
225
-
226
- # Reads and logs data from `stdout` in a near real-time, blocking manner.
227
- # @param stdout [IO] An IO stream with output that can be read from.
228
- # @param log_prefix [String] A string prefix that is added to log messages.
229
- def read_io(stdout, log_prefix: 'RSPEC')
230
- loop do
231
- chunk = stdout.read_nonblock(4096).strip
232
- async_info(chunk, log_prefix) unless chunk.nil? || chunk.empty?
233
- rescue IO::WaitReadable
234
- IO.select([stdout])
235
- retry
236
- rescue EOFError
237
- break
238
- end
239
- end
240
- end
241
- end
242
- end