process_helper 0.0.3 → 0.0.4.pre.beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +40 -11
- data/.rubocop_todo.yml +62 -0
- data/.ruby-version +1 -1
- data/.travis.yml +5 -0
- data/README.md +69 -10
- data/lib/process_helper.rb +105 -78
- data/process_helper.gemspec +7 -8
- data/ruby-lint.yml +3 -2
- data/spec/input_handling_spec.rb +74 -99
- data/spec/module_visibility_spec.rb +33 -0
- data/spec/options/include_output_in_exception_spec.rb +2 -2
- data/spec/options/puts_output_spec.rb +1 -1
- data/spec/options/validation_spec.rb +1 -1
- data/spec/output_handling_spec.rb +25 -2
- data/spec/pty_handling_spec.rb +48 -0
- data/spec/spec_helper.rb +3 -5
- data/spec/timeout_handling_spec.rb +79 -0
- metadata +35 -16
- data/spec/version_spec.rb +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3d1de91d830fe7cc5e79d69b97de7d248351318d
|
4
|
+
data.tar.gz: ff9505e3dede36dbb1888c54c3d3e8a4250f5232
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b082df1e1da68ee594d86823aa064ce1342049069ac69d660cdcff25c783c465de9cb336b1d4e5ca24fd331837e09b6ba635f032102919a0581cf7cf6f94e2f3
|
7
|
+
data.tar.gz: 8ee4d6e370b5c534f3b8a3ff5f4fcf7dd36295ce3f3a65611e9dcc0ad9c3d6147c766d160a4312b6b7e76749bfd0af7767f1c1ee419d33e49d257822f06fc744
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
---
|
2
2
|
# https://github.com/bbatsov/rubocop/blob/master/config/default.yml
|
3
|
+
|
4
|
+
inherit_from: .rubocop_todo.yml
|
5
|
+
|
6
|
+
AllCops:
|
7
|
+
Include:
|
8
|
+
- '**/Rakefile'
|
9
|
+
- '**/config.ru'
|
10
|
+
Exclude:
|
11
|
+
- 'client/node_modules/**/*'
|
12
|
+
- 'db/**/*'
|
13
|
+
- 'vendor/bundle/**/*'
|
14
|
+
|
3
15
|
Metrics/LineLength:
|
4
16
|
Max: 99
|
5
17
|
|
@@ -7,23 +19,40 @@ Metrics/MethodLength:
|
|
7
19
|
CountComments: false # count full line comments?
|
8
20
|
Max: 20
|
9
21
|
|
10
|
-
|
11
|
-
|
22
|
+
Metrics/ModuleLength:
|
23
|
+
Max: 300
|
12
24
|
|
13
|
-
Style/
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
25
|
+
Style/FileName:
|
26
|
+
# File names listed in AllCops:Include are excluded by default. Add extra
|
27
|
+
# excludes here.
|
28
|
+
Exclude:
|
29
|
+
- bin/ruby-lint
|
18
30
|
|
19
31
|
Style/MultilineOperationIndentation:
|
20
|
-
|
32
|
+
EnforcedStyle: indented
|
33
|
+
|
34
|
+
Style/SignalException:
|
35
|
+
# prefer fail for app code, but rails generates raise in binstubs so allow that too
|
36
|
+
EnforcedStyle: semantic
|
21
37
|
|
22
38
|
Style/SpaceAroundEqualsInParameterDefault:
|
23
39
|
# compatibility with RubyMine defaults (apparently can't override?)
|
24
40
|
EnforcedStyle: space
|
25
41
|
|
26
|
-
Style/
|
27
|
-
|
42
|
+
Style/TrailingCommaInLiteral:
|
43
|
+
EnforcedStyleForMultiline: comma
|
44
|
+
|
45
|
+
Style/TrailingCommaInArguments:
|
46
|
+
EnforcedStyleForMultiline: no_comma
|
47
|
+
|
48
|
+
Style/MultilineMethodCallIndentation:
|
49
|
+
# can't convince rubymine to not indent 2 spaces with mult-line method calls
|
28
50
|
Enabled: false
|
29
|
-
|
51
|
+
|
52
|
+
Style/MultilineOperationIndentation:
|
53
|
+
# can't convince rubymine 7 to not indent 4 spaces with mult-line expect(x).to receive ...
|
54
|
+
Enabled: false
|
55
|
+
|
56
|
+
Style/RegexpLiteral:
|
57
|
+
# This is required or else rubocop argues with itself when trying to escape a slash on a single-line regex
|
58
|
+
AllowInnerSlashes: true
|
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config`
|
3
|
+
# on 2016-07-25 21:30:11 -0700 using RuboCop version 0.38.0.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
8
|
+
|
9
|
+
# Offense count: 2
|
10
|
+
# Cop supports --auto-correct.
|
11
|
+
# Configuration parameters: IndentWhenRelativeTo, SupportedStyles, IndentOneStep, IndentationWidth.
|
12
|
+
# SupportedStyles: case, end
|
13
|
+
Style/CaseIndentation:
|
14
|
+
Enabled: false
|
15
|
+
|
16
|
+
# Offense count: 3
|
17
|
+
# Cop supports --auto-correct.
|
18
|
+
Style/ClosingParenthesisIndentation:
|
19
|
+
Exclude:
|
20
|
+
- 'spec/options/expected_exit_status_spec.rb'
|
21
|
+
|
22
|
+
# Offense count: 18
|
23
|
+
# Cop supports --auto-correct.
|
24
|
+
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
|
25
|
+
# SupportedStyles: consistent, special_for_inner_method_call, special_for_inner_method_call_in_parentheses
|
26
|
+
Style/FirstParameterIndentation:
|
27
|
+
Exclude:
|
28
|
+
- 'spec/error_handling_spec.rb'
|
29
|
+
- 'spec/input_handling_spec.rb'
|
30
|
+
- 'spec/options/expected_exit_status_spec.rb'
|
31
|
+
- 'spec/options/include_output_in_exception_spec.rb'
|
32
|
+
- 'spec/options/puts_output_spec.rb'
|
33
|
+
- 'spec/options/validation_spec.rb'
|
34
|
+
|
35
|
+
# Offense count: 1
|
36
|
+
# Cop supports --auto-correct.
|
37
|
+
Style/RedundantParentheses:
|
38
|
+
Exclude:
|
39
|
+
- 'lib/process_helper.rb'
|
40
|
+
|
41
|
+
# Offense count: 3
|
42
|
+
# Cop supports --auto-correct.
|
43
|
+
# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
|
44
|
+
# SupportedStyles: comma, consistent_comma, no_comma
|
45
|
+
Style/TrailingCommaInArguments:
|
46
|
+
Exclude:
|
47
|
+
- 'spec/options/expected_exit_status_spec.rb'
|
48
|
+
|
49
|
+
# Offense count: 1
|
50
|
+
# Cop supports --auto-correct.
|
51
|
+
# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
|
52
|
+
# SupportedStyles: comma, consistent_comma, no_comma
|
53
|
+
Style/TrailingCommaInLiteral:
|
54
|
+
Exclude:
|
55
|
+
- 'spec/input_handling_spec.rb'
|
56
|
+
|
57
|
+
# Offense count: 1
|
58
|
+
# Cop supports --auto-correct.
|
59
|
+
# Configuration parameters: AllowNamedUnderscoreVariables.
|
60
|
+
Style/TrailingUnderscoreVariable:
|
61
|
+
Exclude:
|
62
|
+
- 'lib/process_helper.rb'
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby-2.
|
1
|
+
ruby-2.3.3
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,18 +1,20 @@
|
|
1
|
-
[](https://travis-ci.org/thewoolleyman/process_helper) | [](https://codeclimate.com/github/thewoolleyman/process_helper) | [](https://codeclimate.com/github/thewoolleyman/process_helper) | [Pivotal Tracker Project](https://www.pivotaltracker.com/n/projects/1117814)
|
1
|
+
[](https://travis-ci.org/thewoolleyman/process_helper) | [](https://codeclimate.com/github/thewoolleyman/process_helper) | [](https://codeclimate.com/github/thewoolleyman/process_helper) | [](https://badge.fury.io/rb/process_helper) | [Pivotal Tracker Project](https://www.pivotaltracker.com/n/projects/1117814)
|
2
2
|
|
3
3
|
# process_helper
|
4
4
|
|
5
|
-
Makes it easy to spawn Ruby sub-processes with guaranteed exit status handling,
|
5
|
+
Makes it easy to spawn Ruby sub-processes with guaranteed exit status handling, capturing and/or suppressing combined STDOUT and STDERR streams, providing STDIN input, timeouts, and running via a pseudo terminal.
|
6
6
|
|
7
7
|
## Goals
|
8
8
|
|
9
9
|
* Always raise an exception on unexpected exit status (i.e. return code or `$!`)
|
10
|
-
* 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))
|
10
|
+
* 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))
|
11
|
+
or [PTY.spawn](https://ruby-doc.org/stdlib-2.2.3/libdoc/pty/rdoc/PTY.html#method-c-spawn),
|
11
12
|
so you don't have to worry about how to capture the output of both streams.
|
12
13
|
* Provide useful options for suppressing output and including output when an exception
|
13
14
|
is raised due to an unexpected exit status
|
14
15
|
* 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.
|
15
|
-
* Support passing
|
16
|
+
* Support passing input to the STDIN stream via string or StringIO.
|
17
|
+
* Support running commands in a pseudo terminal.
|
16
18
|
* Allow override of the expected exit status(es) (zero is expected by default)
|
17
19
|
* Provide short forms of all options for terse, concise usage.
|
18
20
|
|
@@ -24,7 +26,8 @@ Makes it easy to spawn Ruby sub-processes with guaranteed exit status handling,
|
|
24
26
|
|
25
27
|
## Why Yet Another Ruby Process Wrapper Library?
|
26
28
|
|
27
|
-
There's many other libraries to make it easier to work with processes in Ruby (see the Resources section).
|
29
|
+
There's many other libraries to make it easier to work with processes in Ruby (see the Resources section).
|
30
|
+
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):
|
28
31
|
|
29
32
|
* Combine STDOUT/STDERR output streams ***interleaved chronologically as emitted***
|
30
33
|
* Stream STDOUT/STDERR real-time ***while process is still running***, in addition to returning full output as a string and/or in an exception
|
@@ -32,6 +35,26 @@ There's many other libraries to make it easier to work with processes in Ruby (s
|
|
32
35
|
* 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.
|
33
36
|
|
34
37
|
|
38
|
+
## Table of Contents
|
39
|
+
|
40
|
+
* [Goals](#goals)
|
41
|
+
* [Non-Goals](#non-goals)
|
42
|
+
* [Why Yet Another Ruby Process Wrapper Library](#why-yet-another-ruby-process-wrapper-library)
|
43
|
+
* [Installation](#installation)
|
44
|
+
* [Usage](#usage)
|
45
|
+
* [Options](#options)
|
46
|
+
* [`:expected_exit_status` (short form `:exp_st`)](#expected_exit_status-short-form-exp_st)
|
47
|
+
* [`:include_output_in_exception` (short form `:out_ex`)](#include_output_in_exception-short-form-out_ex)
|
48
|
+
* [`:input` (short form `:in`)](#input-short-form-in)
|
49
|
+
* [`:pseudo_terminal` (short form `:pty`)](#pseudo_terminal-short-form-pty)
|
50
|
+
* [`:puts_output` (short form `:out`)](#puts_output-short-form-out)
|
51
|
+
* [`:timeout` (short form `:kill`)](#timeout-short-form-kill)
|
52
|
+
* [Warnings if failure output will be suppressed based on options](#warnings-if-failure-output-will-be-suppressed-based-on-options)
|
53
|
+
* [Version](#version)
|
54
|
+
* [Contributing](#contributing)
|
55
|
+
* [(Un)License](#unlicense)
|
56
|
+
* [Resources](#resources)
|
57
|
+
|
35
58
|
## Installation
|
36
59
|
|
37
60
|
Add this line to your application's Gemfile:
|
@@ -131,6 +154,36 @@ ProcessHelper::UnexpectedExitStatusError: Command failed, pid 64947 exit 1. Comm
|
|
131
154
|
"
|
132
155
|
```
|
133
156
|
|
157
|
+
### `:input` (short form `:in`)
|
158
|
+
|
159
|
+
A String or StringIO object which will be supplied as standard input to the command.
|
160
|
+
|
161
|
+
The entire string will be read and piped to the command prior to outputting any output,
|
162
|
+
but this behavior may be changed in the future to allow a separator character for
|
163
|
+
processing input as "lines".
|
164
|
+
|
165
|
+
### `:pseudo_terminal` (short form `:pty`)
|
166
|
+
|
167
|
+
Valid values are `true` and `false`. Default value is `false`.
|
168
|
+
|
169
|
+
When this option is `true`, it will cause the command to be processed via
|
170
|
+
[PTY.spawn](https://ruby-doc.org/stdlib-2.2.3/libdoc/pty/rdoc/PTY.html#method-c-spawn)
|
171
|
+
in a pseudo-terminal.
|
172
|
+
|
173
|
+
Some commands require a terminal, or "tty" to work properly, or to work at all.
|
174
|
+
For example, some commands may not emit colored output unless they
|
175
|
+
detect that they are running via a terminal.
|
176
|
+
|
177
|
+
It is important to note that this can change the behavior of a command.
|
178
|
+
|
179
|
+
For example, in most default Linux and OSX (BSD) terminals, newlines (`\n`) in output
|
180
|
+
will be translated to carriage-return + newline (`\r\n`). This will normally
|
181
|
+
have no effect, and can be controlled by the `onlcr (-onlcr)` option
|
182
|
+
of the [stty command](https://www.freebsd.org/cgi/man.cgi?query=stty&sektion=1).
|
183
|
+
Use `stty -a` to get info on the current terminal.
|
184
|
+
|
185
|
+
Also, any input given to the command may be echoed to the output as well.
|
186
|
+
|
134
187
|
### `:puts_output` (short form `:out`)
|
135
188
|
|
136
189
|
Valid values are `:always`, `:error`, and `:never`. Default value is `:always`.
|
@@ -142,18 +195,19 @@ Valid values are `:always`, `:error`, and `:never`. Default value is `:always`.
|
|
142
195
|
|
143
196
|
### `:timeout` (short form `:kill`)
|
144
197
|
|
145
|
-
***
|
198
|
+
***NOTE: This option will be changed in a future release.***
|
146
199
|
|
147
200
|
Valid value is a float, e.g. `1.5`. Default value is nil/undefined.
|
148
201
|
|
149
|
-
*
|
150
|
-
a blocked IO stream before timing out (via [IO.select](http://ruby-doc.org/core-2.2.0/IO.html#method-c-select)).
|
202
|
+
* Controls how long `process_helper` will wait to read from
|
203
|
+
a blocked IO stream before timing out (via [IO.select](http://ruby-doc.org/core-2.2.0/IO.html#method-c-select)).
|
204
|
+
For example, invoking `cat` with no arguments, which by default will continue accepting input until killed.
|
205
|
+
* Will also kill long running processes which are ***not*** in blocked waiting on an IO stream read (i.e. kill process regardless of any IO state, not just via [IO.selects](http://ruby-doc.org/core-2.2.0/IO.html#method-c-select) timeout support).
|
151
206
|
* If undefined (default), there will be no timeout, and `process_helper` will hang if a process hangs while waiting to read from IO.
|
152
207
|
|
153
208
|
***The following changes are planned for this option:***
|
154
209
|
|
155
210
|
* Add validation of value (enforced to be a float).
|
156
|
-
* Add ability for the timeout value to also kill long running processes which are ***not*** in blocked waiting on an IO stream read (i.e. kill process regardless of any IO state, not just via [IO.selects](http://ruby-doc.org/core-2.2.0/IO.html#method-c-select) timeout support).
|
157
211
|
* Have both types of timeouts raise different and unique exception classes.
|
158
212
|
* Possibly have different option names to allow different timeout values for the two types of timeouts.
|
159
213
|
|
@@ -202,14 +256,19 @@ ProcessHelper::VERSION
|
|
202
256
|
|
203
257
|
## Resources
|
204
258
|
|
205
|
-
Other Ruby Process tools/libraries
|
259
|
+
Other Ruby Process tools/libraries:
|
206
260
|
|
261
|
+
* a [comprehensive StackOverflow article](http://stackoverflow.com/questions/7212573/when-to-use-each-method-of-launching-a-subprocess-in-ruby) describing, in detail, the myriad methods of launching Ruby subprocesses.
|
207
262
|
* [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
|
208
263
|
* [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
|
209
264
|
* A great series of blog posts by Devver:
|
210
265
|
* [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/)
|
211
266
|
* [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/)
|
212
267
|
* [https://devver.wordpress.com/2009/10/12/ruby-subprocesses-part_3/](https://devver.wordpress.com/2009/10/12/ruby-subprocesses-part_3/)
|
268
|
+
* [A gist exploring ruby PTY behavior](https://gist.github.com/thewoolleyman/6a060574f22eafd42955812a1a2a7842#file-pty_check_test-rb)
|
213
269
|
|
270
|
+
Some notes on why you should use printf over echo:
|
214
271
|
|
272
|
+
* [echo vs. println](http://unix.stackexchange.com/a/219274)
|
273
|
+
* [Why is printf better than echo?](http://unix.stackexchange.com/questions/65803/why-is-printf-better-than-echo/65819#65819)
|
215
274
|
|
data/lib/process_helper.rb
CHANGED
@@ -1,117 +1,129 @@
|
|
1
1
|
require 'open3'
|
2
|
+
require 'pty'
|
3
|
+
require 'timeout'
|
2
4
|
|
3
5
|
# Makes it easier to spawn ruby sub-processes with proper capturing of stdout and stderr streams.
|
4
6
|
module ProcessHelper
|
5
|
-
|
7
|
+
# Don't forget to keep version in sync with gemspec
|
8
|
+
VERSION = '0.0.4.pre.beta.2'.freeze
|
9
|
+
|
10
|
+
# rubocop:disable Style/ModuleFunction
|
11
|
+
extend self
|
6
12
|
|
7
13
|
def process(cmd, options = {})
|
8
14
|
cmd = cmd.to_s
|
9
15
|
fail ProcessHelper::EmptyCommandError, 'command must not be empty' if cmd.empty?
|
10
16
|
options = options.dup
|
11
17
|
options_processing(options)
|
18
|
+
output, process_status =
|
19
|
+
if options[:pseudo_terminal]
|
20
|
+
process_with_pseudo_terminal(cmd, options)
|
21
|
+
else
|
22
|
+
process_with_popen(cmd, options)
|
23
|
+
end
|
24
|
+
handle_exit_status(cmd, options, output, process_status)
|
25
|
+
output
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def process_with_popen(cmd, options)
|
12
31
|
Open3.popen2e(cmd) do |stdin, stdout_and_stderr, wait_thr|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
handle_exit_status(cmd, options, output, wait_thr)
|
23
|
-
output
|
32
|
+
begin
|
33
|
+
output = get_output(stdin, stdout_and_stderr, options)
|
34
|
+
rescue TimeoutError
|
35
|
+
# ensure the thread is killed
|
36
|
+
wait_thr.kill
|
37
|
+
raise
|
38
|
+
end
|
39
|
+
process_status = wait_thr.value
|
40
|
+
return [output, process_status]
|
24
41
|
end
|
25
42
|
end
|
26
43
|
|
27
|
-
|
44
|
+
def process_with_pseudo_terminal(cmd, options)
|
45
|
+
PTY.spawn(cmd) do |stdout_and_stderr, stdin, pid|
|
46
|
+
output = get_output(stdin, stdout_and_stderr, options)
|
47
|
+
process_status = PTY.check(pid)
|
48
|
+
# TODO: come up with a test that illustrates pid not exiting
|
49
|
+
fail "ERROR: pid #{pid} did not exit" unless process_status
|
50
|
+
return [output, process_status]
|
51
|
+
end
|
52
|
+
end
|
28
53
|
|
29
54
|
def warn_if_output_may_be_suppressed_on_error(options)
|
30
|
-
return unless options[:puts_output] == :never
|
55
|
+
return unless options[:puts_output] == :never &&
|
56
|
+
options[:include_output_in_exception] == false
|
31
57
|
|
32
|
-
|
33
|
-
err_msg = 'WARNING: Check your ProcessHelper options - ' \
|
58
|
+
err_msg = 'WARNING: Check your ProcessHelper options - ' \
|
34
59
|
':puts_output is :never, and :include_output_in_exception ' \
|
35
60
|
'is false, so all error output will be suppressed if process fails.'
|
36
|
-
else
|
37
|
-
err_msg = 'WARNING: Check your ProcessHelper options - ' \
|
38
|
-
':puts_output is :never, ' \
|
39
|
-
'so all error output will be suppressed unless process ' \
|
40
|
-
"fails with an exit code other than #{options[:expected_exit_status]} " \
|
41
|
-
'(in which case exception will include output ' \
|
42
|
-
'because :include_output_in_exception is true)'
|
43
|
-
end
|
44
61
|
$stderr.puts(err_msg)
|
45
62
|
end
|
46
63
|
|
47
64
|
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
48
65
|
# rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
|
49
|
-
def get_output(stdin, stdout_and_stderr,
|
50
|
-
|
51
|
-
|
52
|
-
|
66
|
+
def get_output(stdin, stdout_and_stderr, options)
|
67
|
+
input = options[:input]
|
68
|
+
always_puts_output = (options[:puts_output] == :always)
|
69
|
+
timeout = options[:timeout]
|
53
70
|
output = ''
|
54
71
|
begin
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
+
begin
|
73
|
+
until input.eof?
|
74
|
+
Timeout.timeout(timeout) do
|
75
|
+
in_ch = input.read_nonblock(1)
|
76
|
+
stdin.write_nonblock(in_ch)
|
77
|
+
end
|
78
|
+
stdin.flush
|
79
|
+
end
|
80
|
+
ch = nil
|
81
|
+
loop do
|
82
|
+
Timeout.timeout(timeout) do
|
83
|
+
ch = stdout_and_stderr.read_nonblock(1)
|
84
|
+
end
|
85
|
+
break unless ch
|
86
|
+
printf ch if always_puts_output
|
87
|
+
output += ch
|
88
|
+
stdout_and_stderr.flush
|
89
|
+
end
|
90
|
+
rescue EOFError
|
91
|
+
return output
|
92
|
+
rescue IO::WaitReadable
|
72
93
|
result = IO.select([stdout_and_stderr], nil, nil, timeout)
|
94
|
+
raise Timeout::Error if result.nil?
|
95
|
+
retry
|
96
|
+
rescue IO::WaitWritable
|
97
|
+
result = IO.select(nil, [stdin], nil, timeout)
|
98
|
+
raise Timeout::Error if result.nil?
|
73
99
|
retry
|
74
100
|
end
|
101
|
+
rescue Timeout::Error
|
102
|
+
handle_timeout_error(output, options)
|
103
|
+
ensure
|
104
|
+
stdout_and_stderr.close
|
105
|
+
stdin.close
|
75
106
|
end
|
107
|
+
# TODO: Why do we sometimes get here with no EOFError occurring, but instead
|
108
|
+
# via IO::WaitReadable with a nil select result? (via popen, not sure if via tty)
|
76
109
|
output
|
77
110
|
end
|
78
111
|
|
79
|
-
def
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
if ch == "\n"
|
84
|
-
result = buffer
|
85
|
-
return result
|
86
|
-
end
|
112
|
+
def handle_timeout_error(output, options)
|
113
|
+
msg = "Timed out after #{options.fetch(:timeout)} seconds."
|
114
|
+
if options[:include_output_in_exception]
|
115
|
+
msg += " Command output prior to timeout: \"#{output}\""
|
87
116
|
end
|
117
|
+
fail(TimeoutError, msg)
|
88
118
|
end
|
89
119
|
|
90
|
-
def
|
91
|
-
unprocessed_input_lines = original_input_lines.length - input_lines_processed
|
92
|
-
msg = "Output stream closed with #{unprocessed_input_lines} " \
|
93
|
-
'input lines left unprocessed:' \
|
94
|
-
"#{original_input_lines[-(unprocessed_input_lines)..-1]}"
|
95
|
-
fail(
|
96
|
-
ProcessHelper::UnprocessedInputError,
|
97
|
-
msg
|
98
|
-
) unless unprocessed_input_lines == 0
|
99
|
-
end
|
100
|
-
|
101
|
-
def puts_input_line_to_stdin(stdin, input_lines)
|
102
|
-
return if input_lines.empty?
|
103
|
-
input_line = input_lines.shift
|
104
|
-
stdin.puts(input_line)
|
105
|
-
end
|
106
|
-
|
107
|
-
def handle_exit_status(cmd, options, output, wait_thr)
|
120
|
+
def handle_exit_status(cmd, options, output, process_status)
|
108
121
|
expected_exit_status = options[:expected_exit_status]
|
109
|
-
|
110
|
-
return if expected_exit_status.include?(exit_status.exitstatus)
|
122
|
+
return if expected_exit_status.include?(process_status.exitstatus)
|
111
123
|
|
112
|
-
exception_message = create_exception_message(cmd,
|
124
|
+
exception_message = create_exception_message(cmd, process_status, expected_exit_status)
|
113
125
|
if options[:include_output_in_exception]
|
114
|
-
exception_message += " Command
|
126
|
+
exception_message += " Command output: \"#{output}\""
|
115
127
|
end
|
116
128
|
puts_output_only_on_exception(options, output)
|
117
129
|
fail ProcessHelper::UnexpectedExitStatusError, exception_message
|
@@ -141,25 +153,35 @@ module ProcessHelper
|
|
141
153
|
def options_processing(options)
|
142
154
|
validate_long_vs_short_option_uniqueness(options)
|
143
155
|
convert_short_options(options)
|
156
|
+
validate_input_option(options[:input]) if options[:input]
|
144
157
|
set_option_defaults(options)
|
145
158
|
validate_option_values(options)
|
146
159
|
convert_scalar_expected_exit_status_to_array(options)
|
147
160
|
warn_if_output_may_be_suppressed_on_error(options)
|
148
161
|
end
|
149
162
|
|
163
|
+
def validate_input_option(input_option)
|
164
|
+
fail(
|
165
|
+
ProcessHelper::InvalidOptionsError,
|
166
|
+
"#{quote_and_join_pair(%w(input in))} options must be a String or a StringIO"
|
167
|
+
) unless input_option.is_a?(String) || input_option.is_a?(StringIO)
|
168
|
+
end
|
169
|
+
|
150
170
|
# rubocop:disable Style/AccessorMethodName
|
151
171
|
def set_option_defaults(options)
|
152
172
|
options[:puts_output] = :always if options[:puts_output].nil?
|
153
173
|
options[:include_output_in_exception] = true if options[:include_output_in_exception].nil?
|
174
|
+
options[:pseudo_terminal] = false if options[:pseudo_terminal].nil?
|
154
175
|
options[:expected_exit_status] = [0] if options[:expected_exit_status].nil?
|
155
|
-
options[:
|
176
|
+
options[:input] = StringIO.new(options[:input].to_s) unless options[:input].is_a?(StringIO)
|
156
177
|
end
|
157
178
|
|
158
179
|
def valid_option_pairs
|
159
180
|
pairs = [
|
160
181
|
%w(expected_exit_status exp_st),
|
161
182
|
%w(include_output_in_exception out_ex),
|
162
|
-
%w(
|
183
|
+
%w(input in),
|
184
|
+
%w(pseudo_terminal pty),
|
163
185
|
%w(puts_output out),
|
164
186
|
%w(timeout kill),
|
165
187
|
]
|
@@ -205,6 +227,7 @@ module ProcessHelper
|
|
205
227
|
next unless option == long_option_name
|
206
228
|
validate_integer(pair, value) if option.to_s == 'expected_exit_status'
|
207
229
|
validate_boolean(pair, value) if option.to_s == 'include_output_in_exception'
|
230
|
+
validate_boolean(pair, value) if option.to_s == 'pseudo_terminal'
|
208
231
|
validate_puts_output(pair, value) if option.to_s == 'puts_output'
|
209
232
|
end
|
210
233
|
end
|
@@ -263,11 +286,15 @@ module ProcessHelper
|
|
263
286
|
class InvalidOptionsError < RuntimeError
|
264
287
|
end
|
265
288
|
|
289
|
+
# Error which is raised when any read or write operation takes longer than timeout (kill) option
|
290
|
+
class TimeoutError < RuntimeError
|
291
|
+
end
|
292
|
+
|
266
293
|
# Error which is raised when a command returns an unexpected exit status (return code)
|
267
294
|
class UnexpectedExitStatusError < RuntimeError
|
268
295
|
end
|
269
296
|
|
270
|
-
# Error which is raised when command exists while input
|
297
|
+
# Error which is raised when command exists while input remains unprocessed
|
271
298
|
class UnprocessedInputError < RuntimeError
|
272
299
|
end
|
273
300
|
end
|
data/process_helper.gemspec
CHANGED
@@ -1,12 +1,10 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require 'process_helper'
|
5
2
|
|
6
3
|
Gem::Specification.new do |spec|
|
7
4
|
spec.name = 'process_helper'
|
8
|
-
|
9
|
-
spec.
|
5
|
+
# Don't forget to keep version in sync with ProcessHelper::Version
|
6
|
+
spec.version = '0.0.4.pre.beta.2'
|
7
|
+
spec.authors = ['Chad Woolley', 'Glenn Oppegard']
|
10
8
|
spec.email = ['oppegard@gmail.com', 'thewoolleyman@gmail.com']
|
11
9
|
spec.summary = "Makes it easier to spawn ruby sub-processes with proper capturing /
|
12
10
|
of stdout and stderr streams."
|
@@ -21,10 +19,11 @@ Gem::Specification.new do |spec|
|
|
21
19
|
spec.required_ruby_version = '>= 1.9.2'
|
22
20
|
|
23
21
|
spec.add_development_dependency 'bundler', '~> 1.6'
|
24
|
-
spec.add_development_dependency 'codeclimate-test-reporter'
|
22
|
+
spec.add_development_dependency 'codeclimate-test-reporter', '~> 1'
|
25
23
|
spec.add_development_dependency 'rake', '~> 10'
|
26
24
|
spec.add_development_dependency 'rspec', '~> 3.1'
|
27
25
|
spec.add_development_dependency 'rspec-retry', '~> 0.4'
|
28
|
-
spec.add_development_dependency 'rubocop', '= 0.
|
29
|
-
spec.add_development_dependency 'ruby-lint', '= 2.0
|
26
|
+
spec.add_development_dependency 'rubocop', '= 0.38.0' # exact version for static analyis libs
|
27
|
+
spec.add_development_dependency 'ruby-lint', '= 2.1.0' # exact version for static analyis libs
|
28
|
+
spec.add_development_dependency 'simplecov', '~> 0'
|
30
29
|
end
|
data/ruby-lint.yml
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
---
|
2
2
|
# http://code.yorickpeterse.com/ruby-lint/latest/
|
3
3
|
analysis_classes:
|
4
|
-
- argument_amount
|
4
|
+
# - argument_amount
|
5
5
|
- pedantics
|
6
6
|
- shadowing_variables
|
7
7
|
# - undefined_methods
|
8
8
|
# - undefined_variables
|
9
|
-
- unused_variables
|
9
|
+
# - unused_variables
|
10
10
|
- useless_equality_checks
|
11
11
|
directories:
|
12
|
+
- lib
|
12
13
|
- spec
|
data/spec/input_handling_spec.rb
CHANGED
@@ -5,49 +5,32 @@ RSpec.describe 'input handling' do
|
|
5
5
|
|
6
6
|
before do
|
7
7
|
@clazz = Clazz.new
|
8
|
-
@max_process_wait = ENV['MAX_PROCESS_WAIT'] ? ENV['MAX_PROCESS_WAIT'].to_f : 0.5
|
9
8
|
end
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
.and(not_output.to_stderr)
|
21
|
-
end
|
22
|
-
|
23
|
-
it 'handles multiple lines of STDIN to STDOUT with ruby output flushing' do
|
24
|
-
expect do
|
25
|
-
clazz.process(
|
26
|
-
"ruby -e 'while(i=$stdin.gets) do puts i; $stdout.flush; end'",
|
27
|
-
input_lines: %w(input1 input2),
|
28
|
-
timeout: max_process_wait
|
29
|
-
)
|
30
|
-
end.to output(/input1\ninput2\n/).to_stdout
|
31
|
-
.and(not_output.to_stderr)
|
32
|
-
end
|
10
|
+
it 'handles a single line of STDIN to STDOUT with ruby output flushing' do
|
11
|
+
expect do
|
12
|
+
clazz.process(
|
13
|
+
"ruby -e 'i=$stdin.gets; puts i; $stdout.flush'",
|
14
|
+
input: "input1\n"
|
15
|
+
)
|
16
|
+
end.to output(/input1\n/).to_stdout
|
17
|
+
.and(not_output.to_stderr)
|
18
|
+
end
|
33
19
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
end.to output(/.*1\tline1\n.*2\tline2\n.*3\tline3\n.*4\t\u0004\n/).to_stdout
|
44
|
-
.and(not_output.to_stderr)
|
45
|
-
end
|
20
|
+
it 'handles multiple lines of STDIN to STDOUT with ruby output flushing' do
|
21
|
+
expect do
|
22
|
+
clazz.process(
|
23
|
+
"ruby -e 'while(i=$stdin.gets) do puts i; $stdout.flush; ; if i =~ /2/; break; end; end'",
|
24
|
+
input: "input1\ninput2\n"
|
25
|
+
)
|
26
|
+
end.to output(/input1\ninput2\n/).to_stdout
|
27
|
+
.and(not_output.to_stderr)
|
28
|
+
end
|
46
29
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
30
|
+
it 'handles interleaved stdout and stderr based on stdin input' do
|
31
|
+
expect do
|
32
|
+
cmd =
|
33
|
+
'while ' \
|
51
34
|
' line = $stdin.readline; ' \
|
52
35
|
' $stdout.puts("out:#{line}"); ' \
|
53
36
|
' $stdout.flush; ' \
|
@@ -55,71 +38,63 @@ RSpec.describe 'input handling' do
|
|
55
38
|
' $stderr.flush; ' \
|
56
39
|
' exit 0 if line =~ /exit/; ' \
|
57
40
|
'end'
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
.and(not_output.to_stderr)
|
65
|
-
end
|
41
|
+
clazz.process(
|
42
|
+
%(ruby -e '#{cmd}'),
|
43
|
+
input: "line1\nline2\nline3\nexit\n"
|
44
|
+
)
|
45
|
+
end.to output(/out:line1\nerr:line1\nout:line2\nerr:line2\nout:line3\nerr:line3\n/).to_stdout
|
46
|
+
.and(not_output.to_stderr)
|
66
47
|
end
|
67
48
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
end
|
49
|
+
it 'handles stdout and stderr triggered via stdin' do
|
50
|
+
expect do
|
51
|
+
clazz.process(
|
52
|
+
'irb -f --prompt=default',
|
53
|
+
input: [
|
54
|
+
'$stdout.puts "hi"',
|
55
|
+
'$stdout.flush',
|
56
|
+
'$stderr.puts "aaa\nbbb\nccc"',
|
57
|
+
'$stderr.flush',
|
58
|
+
'$stdout.puts "bye"',
|
59
|
+
'$stdout.flush',
|
60
|
+
"exit\n"
|
61
|
+
].join("\n")
|
62
|
+
)
|
63
|
+
end.to output(/\nhi\n.*\naaa\nbbb\nccc.*\nbye\n/m).to_stdout
|
64
|
+
.and(not_output.to_stderr)
|
65
|
+
end
|
86
66
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
67
|
+
it 'handles unexpected exit status' do
|
68
|
+
expect do
|
69
|
+
clazz.process(
|
70
|
+
"ruby -e 'i=$stdin.gets; $stdout.puts i; $stdout.flush; " \
|
91
71
|
"$stderr.puts i; $stderr.flush; exit 1'",
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
72
|
+
puts_output: :error,
|
73
|
+
input: "hi\n"
|
74
|
+
)
|
75
|
+
end.to raise_error(
|
76
|
+
ProcessHelper::UnexpectedExitStatusError,
|
77
|
+
/Command failed/)
|
78
|
+
.and(output(/hi\nhi\n/).to_stdout)
|
79
|
+
end
|
100
80
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
81
|
+
it 'pipes input before processing output' do
|
82
|
+
expect do
|
83
|
+
clazz.process(
|
84
|
+
"ruby -e 'i=$stdin.gets; $stdout.puts i; $stdout.flush; exit'",
|
85
|
+
input: "hi\n"
|
86
|
+
)
|
87
|
+
end.to output(/hi\n/m).to_stdout
|
88
|
+
.and(not_output.to_stderr)
|
89
|
+
end
|
110
90
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
end.to raise_error(
|
120
|
-
ProcessHelper::UnprocessedInputError,
|
121
|
-
/Output stream closed with 1 input lines left unprocessed/)
|
122
|
-
.and(output(/hi\n/).to_stdout)
|
123
|
-
end
|
91
|
+
it 'allows a StringIO as input' do
|
92
|
+
expect do
|
93
|
+
clazz.process(
|
94
|
+
"ruby -e 'i=$stdin.gets; $stdout.puts i; $stdout.flush; exit'",
|
95
|
+
input: StringIO.new("hi\n")
|
96
|
+
)
|
97
|
+
end.to output(/hi\n/m).to_stdout
|
98
|
+
.and(not_output.to_stderr)
|
124
99
|
end
|
125
100
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'module visibility' do
|
4
|
+
attr_reader :clazz
|
5
|
+
|
6
|
+
before do
|
7
|
+
@clazz = Clazz.new
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'can be called as an included module' do
|
11
|
+
expect do
|
12
|
+
clazz.process('echo stdout > /dev/stdout')
|
13
|
+
end.to output("stdout\n").to_stdout
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'can be called as a module method' do
|
17
|
+
expect do
|
18
|
+
ProcessHelper.process('echo stdout > /dev/stdout')
|
19
|
+
end.to output("stdout\n").to_stdout
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'does not expose private methods' do
|
23
|
+
expect(clazz.private_methods).to include(:process_with_popen)
|
24
|
+
|
25
|
+
expect do
|
26
|
+
clazz.process_with_popen
|
27
|
+
end.to raise_error(NoMethodError)
|
28
|
+
|
29
|
+
expect do
|
30
|
+
ProcessHelper.process_with_popen
|
31
|
+
end.to raise_error(NoMethodError)
|
32
|
+
end
|
33
|
+
end
|
@@ -18,7 +18,7 @@ RSpec.describe ':include_output_in_exception option' do
|
|
18
18
|
)
|
19
19
|
end.to raise_error(
|
20
20
|
ProcessHelper::UnexpectedExitStatusError,
|
21
|
-
/Command
|
21
|
+
/Command output: "ls:.*\/does_not_exist: No such file or directory\n"/)
|
22
22
|
.and(output(/No such file or directory/).to_stdout)
|
23
23
|
end
|
24
24
|
|
@@ -31,7 +31,7 @@ RSpec.describe ':include_output_in_exception option' do
|
|
31
31
|
include_output_in_exception: true)
|
32
32
|
end.to raise_error(
|
33
33
|
ProcessHelper::UnexpectedExitStatusError,
|
34
|
-
/Command
|
34
|
+
/Command output: "stdout\n"/)
|
35
35
|
.and(output("stdout\n").to_stdout)
|
36
36
|
end
|
37
37
|
end
|
@@ -55,7 +55,7 @@ RSpec.describe 'options validation raises InvalidOptionError' do
|
|
55
55
|
"'puts_output','out' options must be one of the following: :always, :error, :never")
|
56
56
|
end
|
57
57
|
|
58
|
-
it 'when
|
58
|
+
it 'when input is passed invalid value'
|
59
59
|
|
60
60
|
it 'when timeout is passed non-float'
|
61
61
|
|
@@ -53,9 +53,32 @@ RSpec.describe 'output handling' do
|
|
53
53
|
clazz.process(
|
54
54
|
'echo stdout > /dev/stdout',
|
55
55
|
puts_output: :never)
|
56
|
-
end.to
|
57
|
-
.and(not_output.
|
56
|
+
end.to not_output.to_stdout
|
57
|
+
.and(not_output.to_stderr)
|
58
58
|
end
|
59
59
|
end
|
60
60
|
end
|
61
|
+
|
62
|
+
describe 'when output does not have a newline' do
|
63
|
+
it 'captures output' do
|
64
|
+
expect do
|
65
|
+
clazz.process(
|
66
|
+
'printf stdout',
|
67
|
+
puts_output: :always)
|
68
|
+
end.to output('stdout').to_stdout
|
69
|
+
.and(not_output.to_stderr)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe 'when output is colored' do
|
74
|
+
it 'preserves color' do
|
75
|
+
colored_text = "\e[0;31mSTDOUT\e[0m"
|
76
|
+
expect do
|
77
|
+
clazz.process(
|
78
|
+
'printf "\033[0;31mSTDOUT\033[0m" > /dev/stdout',
|
79
|
+
puts_output: :always)
|
80
|
+
end.to output(colored_text).to_stdout
|
81
|
+
.and(not_output.to_stderr)
|
82
|
+
end
|
83
|
+
end
|
61
84
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'pty handling' do
|
4
|
+
attr_reader :clazz, :max_process_wait
|
5
|
+
|
6
|
+
before do
|
7
|
+
skip('pty specs do not work on travis or circle ci (try docker with -t option)') if ENV['CI']
|
8
|
+
@clazz = Clazz.new
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'handles cat cmd' do
|
12
|
+
expect do
|
13
|
+
clazz.process(
|
14
|
+
# -u disables output buffering, -n numbers output lines (to distinguish from input)
|
15
|
+
'cat -u -n',
|
16
|
+
input: "line1\nline2\nline3\n\C-d\n",
|
17
|
+
pty: true
|
18
|
+
)
|
19
|
+
end.to output(/1\tline1\r\n.*2\tline2\r\n.*3\tline3\r\n/).to_stdout
|
20
|
+
.and(not_output.to_stderr)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'respects "stty -onlcr"' do
|
24
|
+
# NOTE: the `stty -a` default on OSX and Linux terminals seems to be 'onlcr', so all PTY slave
|
25
|
+
# terminals which inherit that will transform "\n" into "\r\n". This test shows how to
|
26
|
+
# avoid that behavior by prepending 'stty -onlcr && ' to the command
|
27
|
+
expect do
|
28
|
+
clazz.process(
|
29
|
+
'stty -onlcr && printf "stdout\n" > /dev/stdout && printf "stderr\n" > /dev/stderr',
|
30
|
+
puts_output: :always,
|
31
|
+
pty: true
|
32
|
+
)
|
33
|
+
end.to output("stdout\nstderr\n").to_stdout
|
34
|
+
.and(not_output.to_stderr)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'does not require a newline or flush via getch' do
|
38
|
+
skip('TODO: this test just hangs, including in a debugger')
|
39
|
+
expect do
|
40
|
+
clazz.process(
|
41
|
+
%q(ruby -e 'require "io/console"; i=$stdin.getch; puts "4" + i;'),
|
42
|
+
input: '2',
|
43
|
+
pty: true
|
44
|
+
)
|
45
|
+
end.to output(/42/).to_stdout
|
46
|
+
.and(not_output.to_stderr)
|
47
|
+
end
|
48
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
CodeClimate::TestReporter.start
|
4
|
-
end
|
1
|
+
require 'simplecov'
|
2
|
+
SimpleCov.start if ENV['CI']
|
5
3
|
|
6
4
|
require 'rspec'
|
7
5
|
require 'rspec/retry'
|
@@ -13,7 +11,7 @@ RSpec::Matchers.define_negated_matcher :not_raise_error, :raise_error
|
|
13
11
|
# RSpec config
|
14
12
|
RSpec.configure do |config|
|
15
13
|
config.verbose_retry = true
|
16
|
-
config.default_retry_count =
|
14
|
+
config.default_retry_count = ENV['CI'] ? 3 : 0
|
17
15
|
config.default_sleep_interval = 1
|
18
16
|
end
|
19
17
|
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'timout handling with non-exiting blocking cmd requiring timeout' do
|
4
|
+
attr_reader :clazz, :max_process_wait
|
5
|
+
|
6
|
+
before do
|
7
|
+
@clazz = Clazz.new
|
8
|
+
@max_process_wait = ENV['MAX_PROCESS_WAIT'] ? ENV['MAX_PROCESS_WAIT'].to_f : 0.5
|
9
|
+
end
|
10
|
+
|
11
|
+
# TODO: ensure every place that can raise a timeout is specifically exercised by a test
|
12
|
+
|
13
|
+
it 'raises on a sleep' do
|
14
|
+
expect do
|
15
|
+
clazz.process(
|
16
|
+
'sleep 999',
|
17
|
+
timeout: max_process_wait
|
18
|
+
)
|
19
|
+
end.to raise_error(
|
20
|
+
ProcessHelper::TimeoutError,
|
21
|
+
"Timed out after #{@max_process_wait} seconds. Command output prior to timeout: \"\""
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'does not raise error if timeout is not exceeded' do
|
26
|
+
expect do
|
27
|
+
clazz.process(
|
28
|
+
'sleep 0.01',
|
29
|
+
timeout: max_process_wait
|
30
|
+
)
|
31
|
+
end.to not_output.to_stdout
|
32
|
+
.and(not_output.to_stderr)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'handles a single line of STDIN to STDOUT with ruby output flushing' do
|
36
|
+
expect do
|
37
|
+
clazz.process(
|
38
|
+
"ruby -e 'while(i=$stdin.gets) do puts i; $stdout.flush; end'",
|
39
|
+
input: "input1\n",
|
40
|
+
puts_output: :never,
|
41
|
+
timeout: max_process_wait
|
42
|
+
)
|
43
|
+
end.to raise_error(
|
44
|
+
ProcessHelper::TimeoutError,
|
45
|
+
"Timed out after #{@max_process_wait} seconds. Command output prior to timeout: \"" \
|
46
|
+
"input1\n\""
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'handles multiple lines of STDIN to STDOUT with ruby output flushing' do
|
51
|
+
expect do
|
52
|
+
clazz.process(
|
53
|
+
"ruby -e 'while(i=$stdin.gets) do puts i; $stdout.flush; end'",
|
54
|
+
input: "input1\ninput2\n",
|
55
|
+
puts_output: :never,
|
56
|
+
timeout: max_process_wait
|
57
|
+
)
|
58
|
+
end.to raise_error(
|
59
|
+
ProcessHelper::TimeoutError,
|
60
|
+
"Timed out after #{@max_process_wait} seconds. Command output prior to timeout: \"" \
|
61
|
+
"input1\ninput2\n\""
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'handles cat cmd' do
|
66
|
+
expect do
|
67
|
+
clazz.process(
|
68
|
+
# -u disables output buffering, -n numbers output lines (to distinguish from input)
|
69
|
+
'cat -u -n',
|
70
|
+
input: "line1\nline2\nline3\n",
|
71
|
+
puts_output: :never,
|
72
|
+
timeout: max_process_wait
|
73
|
+
)
|
74
|
+
end.to raise_error(
|
75
|
+
ProcessHelper::TimeoutError,
|
76
|
+
/Timed out.*1\tline1\n.*2\tline2\n.*3\tline3\n/
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: process_helper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4.pre.beta.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
- Glenn Oppegard
|
8
7
|
- Chad Woolley
|
8
|
+
- Glenn Oppegard
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2017-01-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -29,16 +29,16 @@ dependencies:
|
|
29
29
|
name: codeclimate-test-reporter
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
31
31
|
requirements:
|
32
|
-
- - "
|
32
|
+
- - "~>"
|
33
33
|
- !ruby/object:Gem::Version
|
34
|
-
version: '
|
34
|
+
version: '1'
|
35
35
|
type: :development
|
36
36
|
prerelease: false
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
|
-
- - "
|
39
|
+
- - "~>"
|
40
40
|
- !ruby/object:Gem::Version
|
41
|
-
version: '
|
41
|
+
version: '1'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: rake
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
@@ -87,28 +87,42 @@ dependencies:
|
|
87
87
|
requirements:
|
88
88
|
- - '='
|
89
89
|
- !ruby/object:Gem::Version
|
90
|
-
version: 0.
|
90
|
+
version: 0.38.0
|
91
91
|
type: :development
|
92
92
|
prerelease: false
|
93
93
|
version_requirements: !ruby/object:Gem::Requirement
|
94
94
|
requirements:
|
95
95
|
- - '='
|
96
96
|
- !ruby/object:Gem::Version
|
97
|
-
version: 0.
|
97
|
+
version: 0.38.0
|
98
98
|
- !ruby/object:Gem::Dependency
|
99
99
|
name: ruby-lint
|
100
100
|
requirement: !ruby/object:Gem::Requirement
|
101
101
|
requirements:
|
102
102
|
- - '='
|
103
103
|
- !ruby/object:Gem::Version
|
104
|
-
version: 2.0
|
104
|
+
version: 2.1.0
|
105
105
|
type: :development
|
106
106
|
prerelease: false
|
107
107
|
version_requirements: !ruby/object:Gem::Requirement
|
108
108
|
requirements:
|
109
109
|
- - '='
|
110
110
|
- !ruby/object:Gem::Version
|
111
|
-
version: 2.0
|
111
|
+
version: 2.1.0
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: simplecov
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - "~>"
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - "~>"
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
112
126
|
description: Wrapper around Open3#popen2e with other useful options.
|
113
127
|
email:
|
114
128
|
- oppegard@gmail.com
|
@@ -119,6 +133,7 @@ extra_rdoc_files: []
|
|
119
133
|
files:
|
120
134
|
- ".gitignore"
|
121
135
|
- ".rubocop.yml"
|
136
|
+
- ".rubocop_todo.yml"
|
122
137
|
- ".ruby-version"
|
123
138
|
- ".travis.yml"
|
124
139
|
- Gemfile
|
@@ -130,14 +145,16 @@ files:
|
|
130
145
|
- ruby-lint.yml
|
131
146
|
- spec/error_handling_spec.rb
|
132
147
|
- spec/input_handling_spec.rb
|
148
|
+
- spec/module_visibility_spec.rb
|
133
149
|
- spec/options/expected_exit_status_spec.rb
|
134
150
|
- spec/options/include_output_in_exception_spec.rb
|
135
151
|
- spec/options/puts_output_spec.rb
|
136
152
|
- spec/options/validation_spec.rb
|
137
153
|
- spec/output_handling_spec.rb
|
154
|
+
- spec/pty_handling_spec.rb
|
138
155
|
- spec/spec_helper.rb
|
139
156
|
- spec/static_analysis_spec.rb
|
140
|
-
- spec/
|
157
|
+
- spec/timeout_handling_spec.rb
|
141
158
|
homepage: https://github.com/thewoolleyman/process_helper
|
142
159
|
licenses:
|
143
160
|
- Unlicense
|
@@ -153,12 +170,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
153
170
|
version: 1.9.2
|
154
171
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
155
172
|
requirements:
|
156
|
-
- - "
|
173
|
+
- - ">"
|
157
174
|
- !ruby/object:Gem::Version
|
158
|
-
version:
|
175
|
+
version: 1.3.1
|
159
176
|
requirements: []
|
160
177
|
rubyforge_project:
|
161
|
-
rubygems_version: 2.
|
178
|
+
rubygems_version: 2.6.8
|
162
179
|
signing_key:
|
163
180
|
specification_version: 4
|
164
181
|
summary: Makes it easier to spawn ruby sub-processes with proper capturing / of stdout
|
@@ -166,11 +183,13 @@ summary: Makes it easier to spawn ruby sub-processes with proper capturing / of
|
|
166
183
|
test_files:
|
167
184
|
- spec/error_handling_spec.rb
|
168
185
|
- spec/input_handling_spec.rb
|
186
|
+
- spec/module_visibility_spec.rb
|
169
187
|
- spec/options/expected_exit_status_spec.rb
|
170
188
|
- spec/options/include_output_in_exception_spec.rb
|
171
189
|
- spec/options/puts_output_spec.rb
|
172
190
|
- spec/options/validation_spec.rb
|
173
191
|
- spec/output_handling_spec.rb
|
192
|
+
- spec/pty_handling_spec.rb
|
174
193
|
- spec/spec_helper.rb
|
175
194
|
- spec/static_analysis_spec.rb
|
176
|
-
- spec/
|
195
|
+
- spec/timeout_handling_spec.rb
|