process_helper 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 545dfa70d2c4398cb9e9a83bf98cd7541506d857
4
+ data.tar.gz: 7257372f2aa37bb3d2fd40c4b40cff450dfaee71
5
+ SHA512:
6
+ metadata.gz: 6c0478bcbf1cf87ec7afbfc1b0e1394ecdabbdbe222f6b83bb09ede1e35abaaee6f6660c2a20a215fe3c9f4bac560e08bfa5ca28c6b0959ad146f21aaf7dc0b9
7
+ data.tar.gz: d2be7bb16ee0d16f3d6eee824e9c33747912af1427ef75f0859300ccc4b5f6daf087a1ef4c61dcd9a0b9a068a52f4f080095504d048d32118c3d4151b06f82a9
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .idea
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ *.bundle
20
+ *.so
21
+ *.o
22
+ *.a
23
+ mkmf.log
data/.rubocop.yml ADDED
@@ -0,0 +1,29 @@
1
+ ---
2
+ # https://github.com/bbatsov/rubocop/blob/master/config/default.yml
3
+ Metrics/LineLength:
4
+ Max: 99
5
+
6
+ Metrics/MethodLength:
7
+ CountComments: false # count full line comments?
8
+ Max: 20
9
+
10
+ Style/CaseIndentation:
11
+ IndentOneStep: true
12
+
13
+ Style/FirstParameterIndentation:
14
+ Enabled: false
15
+
16
+ Style/MultilineBlockChain:
17
+ Enabled: false
18
+
19
+ Style/MultilineOperationIndentation:
20
+ Enabled: false
21
+
22
+ Style/SpaceAroundEqualsInParameterDefault:
23
+ # compatibility with RubyMine defaults (apparently can't override?)
24
+ EnforcedStyle: space
25
+
26
+ Style/TrailingComma:
27
+ # can't make this only apply to arrays and not params, so it's disabled
28
+ Enabled: false
29
+ #EnforcedStyleForMultiline: comma
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.1
data/.travis.yml ADDED
@@ -0,0 +1,2 @@
1
+ ---
2
+ sudo: false
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in process_helper.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <http://unlicense.org/>
data/README.md ADDED
@@ -0,0 +1,180 @@
1
+ [![Travis-CI Build Status](https://travis-ci.org/thewoolleyman/process_helper.svg?branch=master)](https://travis-ci.org/thewoolleyman/process_helper)
2
+
3
+ [Pivotal Tracker Project](https://www.pivotaltracker.com/n/projects/1117814)
4
+
5
+ # process_helper
6
+
7
+ Makes it easy to spawn Ruby sub-processes with guaranteed exit status handling, passing of lines to STDIN, and capturing of STDOUT and STDERR streams.
8
+
9
+ ## Goals
10
+
11
+ * Always raise an exception on unexpected exit status (i.e. return code or `$!`)
12
+ * Combine and interleave STDOUT and STDERR streams into STDOUT (using [Open3.popen2e](http://ruby-doc.org/stdlib-2.1.5/libdoc/open3/rdoc/Open3.html#method-c-popen2e)),
13
+ so you don't have to worry about how to capture the output of both streams.
14
+ * Provide useful options for suppressing output and including output when an exception
15
+ is raised due to an unexpected exit status
16
+ * Provide real-time streaming of combined STDOUT/STDERR streams in addition to returning full combined output as a string returned from the method and/or in the exception.
17
+ * Support passing multi-line input to the STDIN stream via arrays of strings.
18
+ * Allow override of the expected exit status(es) (zero is expected by default)
19
+ * Provide short forms of all options for terse, concise usage.
20
+
21
+ ## Non-Goals
22
+
23
+ * Any explicit support for process forks, multiple threads, or anything other
24
+ than a single direct child process.
25
+ * Any support for separate handling of STDOUT and STDERR streams
26
+
27
+ ## Why Yet Another Ruby Process Wrapper Library?
28
+
29
+ There's many other libraries to make it easier to work with processes in Ruby (see the Resources section). However, `process_helper` was created because none of them made it *easy* to run processes while meeting **all** of these requirements (redundant details are repeated above in Goals section):
30
+
31
+ * Combine STDOUT/STDERR output streams ***interleaved chronologically as emitted***
32
+ * Stream STDOUT/STDERR real-time ***while process is still running***, in addition to returning full output as a string and/or in an exception
33
+ * Guarantee an exception is ***always raised*** on an unexpected exit status (and allow specification of ***multiple nonzero values*** as expected exit statuses)
34
+ * Can be used ***very concisely***. I.e. All behavior can be invoked via a single mixed-in module with single public method call using terse options with sensible defaults, no need to use IO streams directly or have any blocks or local variables declared.
35
+
36
+
37
+ ## Installation
38
+
39
+ Add this line to your application's Gemfile:
40
+
41
+ gem 'process_helper'
42
+
43
+ And then execute:
44
+
45
+ $ bundle
46
+
47
+ Or install it yourself as:
48
+
49
+ $ gem install process_helper
50
+
51
+ ## Usage
52
+
53
+ `ProcessHelper` is a Ruby module you can include in any Ruby code,
54
+ and then call `#process` to run a command, like this:
55
+
56
+ ```
57
+ require 'process_helper'
58
+ include ProcessHelper
59
+ process('echo "Hello"')
60
+ ```
61
+
62
+ By default, ProcessHelper will combine any STDERR and STDOUT, and output it to STDOUT,
63
+ and also return it as the result of the `#process` method.
64
+
65
+ ## Options
66
+
67
+ ### `:expected_exit_status` (short form `:exp_st`)
68
+
69
+ Expected Integer exit status, or array of expected Integer exit statuses.
70
+ Default value is `[0]`.
71
+
72
+ An exception will be raised by the `ProcessHelper#process` method if the
73
+ actual exit status of the processed command is not (one of) the
74
+ expected exit status(es).
75
+
76
+ Here's an example of expecting a nonzero failure exit status which matches the actual exit status
77
+ (the actual exit status of a failed `ls` command will be 1 on OSX, 2 on Linux):
78
+
79
+ ```
80
+ # The following will NOT raise an exception:
81
+ process('ls /does_not_exist', expected_exit_status: [1,2])
82
+ ```
83
+
84
+ ...but it **WILL** still print the output (in this case STDERR output from the failed `ls`
85
+ command) to STDOUT:
86
+
87
+ ```
88
+ ls: /does_not_exist: No such file or directory
89
+ ```
90
+
91
+ Here's a second example of expecting a nonzero failure exit status but the command succeeds:
92
+
93
+ ```
94
+ # The following WILL raise an exception:
95
+ process('printf FAIL', expected_exit_status: 1)
96
+ ```
97
+
98
+ Here's the output of the above example:
99
+
100
+ ```
101
+ FAIL
102
+ ProcessHelper::UnexpectedExitStatusError: Command succeeded but was expected to fail, pid 62974 exit 0 (expected [1]). Command: `printf FAIL`. Command Output: "FAIL"
103
+ ```
104
+
105
+ ### `:include_output_in_exception` (short form `:out_ex`)
106
+
107
+ Boolean flag indicating whether output should be included in the message of the Exception (error)
108
+ which will be raised by the `ProcessHelper#process` method if the command fails (has an unexpected exit status).
109
+
110
+ Here's an example of a failing command:
111
+
112
+ ```
113
+ process('ls /does_not_exist', include_output_in_exception: true)
114
+ ```
115
+
116
+ Here's the exception generated by the above example. Notice the "Command Output"
117
+ with the *"...No such file or directory"* STDERR output of the failed command:
118
+
119
+ ```
120
+ ProcessHelper::UnexpectedExitStatusError: Command failed, pid 64947 exit 1. Command: `ls /does_not_exist`. Command Output: "ls: /does_not_exist: No such file or directory
121
+ "
122
+ ```
123
+
124
+ ### `:puts_output` (short form `:out`)
125
+
126
+ Valid values are `:always`, `:error`, and `:never`. Default value is `:always`.
127
+
128
+ * `:always` will always print output to STDOUT
129
+ * `:error` will only print output to STDOUT if command has an
130
+ error - i.e. non-zero or unexpected exit status
131
+ * `:never` will never print output to STDOUT
132
+
133
+ ## Warnings if failure output will be suppressed based on options
134
+
135
+ ProcessHelper will give you a warning if you pass a combination of options that would
136
+ prevent **ANY** output from being printed or included in the Exception message if
137
+ a command were to fail.
138
+
139
+ For example, in this case there is no output, and the expected exit status includes
140
+ the actual failure status which is returned, so the warning is printed, and the only
141
+ place the output will be seen is in the return value of the `ProcessHelper#process` method:
142
+
143
+ ```
144
+ > process('ls /does_not_exist', expected_exit_status: [1,2], puts_output: :never, include_output_in_exception: false)
145
+ WARNING: Check your ProcessHelper options - :puts_output is :never, and :include_output_in_exception is false, so all error output will be suppressed if process fails.
146
+ => "ls: /does_not_exist: No such file or directory\n"
147
+ ```
148
+
149
+ ## Version
150
+
151
+ You can see the version of ProcessHelper in the `ProcessHelper::VERSION` constant:
152
+
153
+ ```
154
+ ProcessHelper::VERSION
155
+ => "0.0.1"
156
+ ```
157
+
158
+ ## Contributing
159
+
160
+ 1. Fork it ( https://github.com/thewoolleyman/process_helper/fork )
161
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
162
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
163
+ 4. Push to the branch (`git push origin my-new-feature`)
164
+ 5. If you are awesome, use `git rebase --interactive` to ensure
165
+ you have a single atomic commit on your branch.
166
+ 6. Create a new Pull Request
167
+
168
+ ## Resources
169
+
170
+ Other Ruby Process tools/libraries
171
+
172
+ * [open4](https://github.com/ahoward/open4) - a solid and useful library - the main thing I missed in it was easily combining real-time streaming interleaved STDOUT/STDERR streams
173
+ * [open4 on ruby toolbox](https://www.ruby-toolbox.com/projects/open4) - see if there's some useful higher-level gem that depends on it and gives you functionality you may need
174
+ * A great series of blog posts by Devver:
175
+ * [https://devver.wordpress.com/2009/06/30/a-dozen-or-so-ways-to-start-sub-processes-in-ruby-part-1/](https://devver.wordpress.com/2009/06/30/a-dozen-or-so-ways-to-start-sub-processes-in-ruby-part-1/)
176
+ * [https://devver.wordpress.com/2009/07/13/a-dozen-or-so-ways-to-start-sub-processes-in-ruby-part-2/](https://devver.wordpress.com/2009/07/13/a-dozen-or-so-ways-to-start-sub-processes-in-ruby-part-2/)
177
+ * [https://devver.wordpress.com/2009/10/12/ruby-subprocesses-part_3/](https://devver.wordpress.com/2009/10/12/ruby-subprocesses-part_3/)
178
+
179
+
180
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rspec/core/rake_task'
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,5 @@
1
+ # Error which is raised when a command is empty
2
+ module ProcessHelper
3
+ class EmptyCommandError < RuntimeError
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ # Error which is raised when options are invalid
2
+ module ProcessHelper
3
+ class InvalidOptionsError < RuntimeError
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ # Error which is raised when a command returns an unexpected exit status (return code)
2
+ module ProcessHelper
3
+ class UnexpectedExitStatusError < RuntimeError
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ # Error which is raised when command exists while input lines remain unprocessed
2
+ module ProcessHelper
3
+ class UnprocessedInputError < RuntimeError
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ # Library version
2
+ module ProcessHelper
3
+ VERSION = '0.0.1'
4
+ end
@@ -0,0 +1,258 @@
1
+ require_relative 'process_helper/version'
2
+ require_relative 'process_helper/empty_command_error'
3
+ require_relative 'process_helper/invalid_options_error'
4
+ require_relative 'process_helper/unexpected_exit_status_error'
5
+ require_relative 'process_helper/unprocessed_input_error'
6
+ require 'open3'
7
+
8
+ # Makes it easier to spawn ruby sub-processes with proper capturing of stdout and stderr streams.
9
+ module ProcessHelper
10
+ def process(cmd, options = {})
11
+ cmd = cmd.to_s
12
+ fail ProcessHelper::EmptyCommandError, 'command must not be empty' if cmd.empty?
13
+ options = options.dup
14
+ options_processing(options)
15
+ Open3.popen2e(cmd) do |stdin, stdout_and_stderr, wait_thr|
16
+ always_puts_output = (options[:puts_output] == :always)
17
+ output = get_output(
18
+ stdin,
19
+ stdout_and_stderr,
20
+ options[:input_lines],
21
+ always_puts_output,
22
+ options[:timeout]
23
+ )
24
+ stdin.close
25
+ handle_exit_status(cmd, options, output, wait_thr)
26
+ output
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def warn_if_output_may_be_suppressed_on_error(options)
33
+ return unless options[:puts_output] == :never
34
+
35
+ if options[:include_output_in_exception] == false
36
+ err_msg = 'WARNING: Check your ProcessHelper options - ' \
37
+ ':puts_output is :never, and :include_output_in_exception ' \
38
+ 'is false, so all error output will be suppressed if process fails.'
39
+ else
40
+ err_msg = 'WARNING: Check your ProcessHelper options - ' \
41
+ ':puts_output is :never, ' \
42
+ 'so all error output will be suppressed unless process ' \
43
+ "fails with an exit code other than #{options[:expected_exit_status]} " \
44
+ '(in which case exception will include output ' \
45
+ 'because :include_output_in_exception is true)'
46
+ end
47
+ $stderr.puts(err_msg)
48
+ end
49
+
50
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
51
+ # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
52
+ def get_output(stdin, stdout_and_stderr, original_input_lines, always_puts_output, timeout)
53
+ input_lines = original_input_lines.dup
54
+ input_lines_processed = 0
55
+ current_input_line_processed = false
56
+ output = ''
57
+ begin
58
+ while (output_line = readline_nonblock(stdout_and_stderr))
59
+ current_input_line_processed = true
60
+ puts output_line if always_puts_output
61
+ output += output_line
62
+ output_line = nil
63
+ end
64
+ rescue EOFError
65
+ input_lines_processed -= 1 if !original_input_lines.empty? && !current_input_line_processed
66
+ fail_unless_all_input_lines_processed(original_input_lines, input_lines_processed)
67
+ rescue IO::WaitReadable
68
+ if input_lines.empty?
69
+ result = IO.select([stdout_and_stderr], nil, nil, timeout)
70
+ retry unless result.nil?
71
+ else
72
+ current_input_line_processed = false
73
+ puts_input_line_to_stdin(stdin, input_lines)
74
+ input_lines_processed += 1
75
+ result = IO.select([stdout_and_stderr], nil, nil, timeout)
76
+ retry
77
+ end
78
+ end
79
+ output
80
+ end
81
+
82
+ def readline_nonblock(io)
83
+ buffer = ''
84
+ while (ch = io.read_nonblock(1))
85
+ buffer << ch
86
+ if ch == "\n"
87
+ result = buffer
88
+ return result
89
+ end
90
+ end
91
+ end
92
+
93
+ def fail_unless_all_input_lines_processed(original_input_lines, input_lines_processed)
94
+ unprocessed_input_lines = original_input_lines.length - input_lines_processed
95
+ msg = "Output stream closed with #{unprocessed_input_lines} " \
96
+ 'input lines left unprocessed:' \
97
+ "#{original_input_lines[-(unprocessed_input_lines)..-1]}"
98
+ fail(
99
+ ProcessHelper::UnprocessedInputError,
100
+ msg
101
+ ) unless unprocessed_input_lines == 0
102
+ end
103
+
104
+ def puts_input_line_to_stdin(stdin, input_lines)
105
+ return if input_lines.empty?
106
+ input_line = input_lines.shift
107
+ stdin.puts(input_line)
108
+ end
109
+
110
+ def handle_exit_status(cmd, options, output, wait_thr)
111
+ expected_exit_status = options[:expected_exit_status]
112
+ exit_status = wait_thr.value
113
+ return if expected_exit_status.include?(exit_status.exitstatus)
114
+
115
+ exception_message = create_exception_message(cmd, exit_status, expected_exit_status)
116
+ if options[:include_output_in_exception]
117
+ exception_message += " Command Output: \"#{output}\""
118
+ end
119
+ puts_output_only_on_exception(options, output)
120
+ fail ProcessHelper::UnexpectedExitStatusError, exception_message
121
+ end
122
+
123
+ def create_exception_message(cmd, exit_status, expected_exit_status)
124
+ if expected_exit_status == [0]
125
+ result_msg = 'failed'
126
+ exit_status_msg = ''
127
+ elsif !expected_exit_status.include?(0)
128
+ result_msg = 'succeeded but was expected to fail'
129
+ exit_status_msg = " (expected #{expected_exit_status})"
130
+ else
131
+ result_msg = 'did not exit with one of the expected exit statuses'
132
+ exit_status_msg = " (expected #{expected_exit_status})"
133
+ end
134
+
135
+ "Command #{result_msg}, #{exit_status}#{exit_status_msg}. " \
136
+ "Command: `#{cmd}`."
137
+ end
138
+
139
+ def puts_output_only_on_exception(options, output)
140
+ return if options[:puts_output] == :always
141
+ puts output if options[:puts_output] == :error
142
+ end
143
+
144
+ def options_processing(options)
145
+ validate_long_vs_short_option_uniqueness(options)
146
+ convert_short_options(options)
147
+ set_option_defaults(options)
148
+ validate_option_values(options)
149
+ convert_scalar_expected_exit_status_to_array(options)
150
+ warn_if_output_may_be_suppressed_on_error(options)
151
+ end
152
+
153
+ # rubocop:disable Style/AccessorMethodName
154
+ def set_option_defaults(options)
155
+ options[:puts_output] = :always if options[:puts_output].nil?
156
+ options[:include_output_in_exception] = true if options[:include_output_in_exception].nil?
157
+ options[:expected_exit_status] = [0] if options[:expected_exit_status].nil?
158
+ options[:input_lines] = [] if options[:input_lines].nil?
159
+ end
160
+
161
+ def valid_option_pairs
162
+ pairs = [
163
+ %w(expected_exit_status exp_st),
164
+ %w(include_output_in_exception out_ex),
165
+ %w(input_lines in),
166
+ %w(puts_output out),
167
+ %w(timeout kill),
168
+ ]
169
+ pairs.each do |pair|
170
+ pair.each_with_index do |opt, index|
171
+ pair[index] = opt.to_sym
172
+ end
173
+ end
174
+ end
175
+
176
+ def valid_options
177
+ valid_option_pairs.flatten
178
+ end
179
+
180
+ def validate_long_vs_short_option_uniqueness(options)
181
+ invalid_options = (options.keys - valid_options)
182
+ fail(
183
+ ProcessHelper::InvalidOptionsError,
184
+ "Invalid option(s) '#{invalid_options.join(', ')}' given. " \
185
+ "Valid options are: #{valid_options.join(', ')}") unless invalid_options.empty?
186
+ valid_option_pairs.each do |pair|
187
+ long_option_name, short_option_name = pair
188
+ both_long_and_short_option_specified =
189
+ options[long_option_name] && options[short_option_name]
190
+ next unless both_long_and_short_option_specified
191
+ fail(
192
+ ProcessHelper::InvalidOptionsError,
193
+ "Cannot specify both '#{long_option_name}' and '#{short_option_name}'")
194
+ end
195
+ end
196
+
197
+ def convert_short_options(options)
198
+ valid_option_pairs.each do |pair|
199
+ long, short = pair
200
+ options[long] = options.delete(short) unless options[short].nil?
201
+ end
202
+ end
203
+
204
+ def validate_option_values(options)
205
+ options.each do |option, value|
206
+ valid_option_pairs.each do |pair|
207
+ long_option_name, _ = pair
208
+ next unless option == long_option_name
209
+ validate_integer(pair, value) if option.to_s == 'expected_exit_status'
210
+ validate_boolean(pair, value) if option.to_s == 'include_output_in_exception'
211
+ validate_puts_output(pair, value) if option.to_s == 'puts_output'
212
+ end
213
+ end
214
+ end
215
+
216
+ def validate_integer(pair, value)
217
+ valid =
218
+ case
219
+ when value.is_a?(Integer)
220
+ true
221
+ when value.is_a?(Array) && value.all? { |v| v.is_a?(Integer) }
222
+ true
223
+ else
224
+ false
225
+ end
226
+
227
+ fail(
228
+ ProcessHelper::InvalidOptionsError,
229
+ "#{quote_and_join_pair(pair)} options must be an Integer or an array of Integers"
230
+ ) unless valid
231
+ end
232
+
233
+ def validate_boolean(pair, value)
234
+ fail(
235
+ ProcessHelper::InvalidOptionsError,
236
+ "#{quote_and_join_pair(pair)} options must be a boolean"
237
+ ) unless value == true || value == false
238
+ end
239
+
240
+ def validate_puts_output(pair, value)
241
+ valid_values = [:always, :error, :never]
242
+ fail(
243
+ ProcessHelper::InvalidOptionsError,
244
+ "#{quote_and_join_pair(pair)} options must be one of the following: " +
245
+ valid_values.map { |v| ":#{v}" }.join(', ')
246
+ ) unless valid_values.include?(value)
247
+ end
248
+
249
+ def quote_and_join_pair(pair)
250
+ pair.map { |o| "'#{o}'" }.join(',')
251
+ end
252
+
253
+ def convert_scalar_expected_exit_status_to_array(options)
254
+ return if options[:expected_exit_status].is_a?(Array)
255
+ options[:expected_exit_status] =
256
+ [options[:expected_exit_status]]
257
+ end
258
+ end