process_helper 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/.rubocop.yml +29 -0
- data/.ruby-version +1 -0
- data/.travis.yml +2 -0
- data/Gemfile +4 -0
- data/LICENSE +24 -0
- data/README.md +180 -0
- data/Rakefile +6 -0
- data/lib/process_helper/empty_command_error.rb +5 -0
- data/lib/process_helper/invalid_options_error.rb +5 -0
- data/lib/process_helper/unexpected_exit_status_error.rb +5 -0
- data/lib/process_helper/unprocessed_input_error.rb +5 -0
- data/lib/process_helper/version.rb +4 -0
- data/lib/process_helper.rb +258 -0
- data/process_helper.gemspec +29 -0
- data/ruby-lint.yml +12 -0
- data/spec/error_handling_spec.rb +21 -0
- data/spec/input_handling_spec.rb +125 -0
- data/spec/options/expected_exit_status_spec.rb +138 -0
- data/spec/options/include_output_in_exception_spec.rb +75 -0
- data/spec/options/puts_output_spec.rb +76 -0
- data/spec/options/validation_spec.rb +69 -0
- data/spec/output_handling_spec.rb +61 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/static_analysis_spec.rb +18 -0
- data/spec/version_spec.rb +7 -0
- metadata +167 -0
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
data/Gemfile
ADDED
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,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
|