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 +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
@@ -0,0 +1,29 @@
|
|
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/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'process_helper'
|
8
|
+
spec.version = ProcessHelper::VERSION
|
9
|
+
spec.authors = ['Glenn Oppegard', 'Chad Woolley']
|
10
|
+
spec.email = ['oppegard@gmail.com', 'thewoolleyman@gmail.com']
|
11
|
+
spec.summary = "Makes it easier to spawn ruby sub-processes with proper capturing /
|
12
|
+
of stdout and stderr streams."
|
13
|
+
spec.description = 'Wrapper around Open3#popen2e with other useful options.'
|
14
|
+
spec.homepage = 'https://github.com/thewoolleyman/process_helper'
|
15
|
+
spec.license = 'Unlicense'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(/^(test|spec|features)\//)
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
spec.required_ruby_version = '>= 1.9.2'
|
22
|
+
|
23
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
24
|
+
spec.add_development_dependency 'codeclimate-test-reporter'
|
25
|
+
spec.add_development_dependency 'rake', '~> 10'
|
26
|
+
spec.add_development_dependency 'rspec', '~> 3.1'
|
27
|
+
spec.add_development_dependency 'rubocop', '= 0.29.1' # exact version for static analyis libs
|
28
|
+
spec.add_development_dependency 'ruby-lint', '= 2.0.2' # exact version for static analyis libs
|
29
|
+
end
|
data/ruby-lint.yml
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
---
|
2
|
+
# http://code.yorickpeterse.com/ruby-lint/latest/
|
3
|
+
analysis_classes:
|
4
|
+
- argument_amount
|
5
|
+
- pedantics
|
6
|
+
- shadowing_variables
|
7
|
+
# - undefined_methods
|
8
|
+
# - undefined_variables
|
9
|
+
- unused_variables
|
10
|
+
- useless_equality_checks
|
11
|
+
directories:
|
12
|
+
- spec
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'error handling' do
|
4
|
+
attr_reader :clazz
|
5
|
+
|
6
|
+
before do
|
7
|
+
@clazz = Clazz.new
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'fails if command is nil' do
|
11
|
+
expect { clazz.process(nil) }.to raise_error(
|
12
|
+
ProcessHelper::EmptyCommandError,
|
13
|
+
'command must not be empty')
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'fails if command is empty' do
|
17
|
+
expect { clazz.process('') }.to raise_error(
|
18
|
+
ProcessHelper::EmptyCommandError,
|
19
|
+
'command must not be empty')
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'input handling' 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
|
+
describe 'with non-exiting blocking cmd requiring timeout' do
|
12
|
+
it 'handles a single line of STDIN to STDOUT with ruby output flushing' do
|
13
|
+
expect do
|
14
|
+
clazz.process(
|
15
|
+
"ruby -e 'while(i=$stdin.gets) do puts i; $stdout.flush; end'",
|
16
|
+
input_lines: ['input1'],
|
17
|
+
timeout: max_process_wait
|
18
|
+
)
|
19
|
+
end.to output(/input1\n/).to_stdout
|
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
|
33
|
+
|
34
|
+
it 'handles cat cmd' do
|
35
|
+
expect do
|
36
|
+
clazz.process(
|
37
|
+
# -u disables output buffering, -n numbers output lines (to distinguish from input)
|
38
|
+
'cat -u -n',
|
39
|
+
# TODO: how to send Ctrl-D to exit without timeout being required?
|
40
|
+
input_lines: ['line1', 'line2', 'line3', "\C-d"],
|
41
|
+
timeout: max_process_wait
|
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
|
46
|
+
|
47
|
+
it 'handles interleaved stdout and stderr based on stdin input' do
|
48
|
+
expect do
|
49
|
+
cmd =
|
50
|
+
'while ' \
|
51
|
+
' line = $stdin.readline; ' \
|
52
|
+
' $stdout.puts("out:#{line}"); ' \
|
53
|
+
' $stdout.flush; ' \
|
54
|
+
' $stderr.puts("err:#{line}"); ' \
|
55
|
+
' $stderr.flush; ' \
|
56
|
+
' exit 0 if line =~ /exit/; ' \
|
57
|
+
'end'
|
58
|
+
clazz.process(
|
59
|
+
%(ruby -e '#{cmd}'),
|
60
|
+
input_lines: %w(line1 line2 line3 exit),
|
61
|
+
timeout: max_process_wait
|
62
|
+
)
|
63
|
+
end.to output(/out:line1\nerr:line1\nout:line2\nerr:line2\nout:line3\nerr:line3\n/).to_stdout
|
64
|
+
.and(not_output.to_stderr)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe 'with exiting cmd' do
|
69
|
+
it 'handles stdout and stderr triggered via stdin' do
|
70
|
+
expect do
|
71
|
+
clazz.process(
|
72
|
+
'irb -f --prompt=default',
|
73
|
+
input_lines: [
|
74
|
+
'$stdout.puts "hi"',
|
75
|
+
'$stdout.flush',
|
76
|
+
'$stderr.puts "aaa\nbbb\nccc"',
|
77
|
+
'$stderr.flush',
|
78
|
+
'$stdout.puts "bye"',
|
79
|
+
'$stdout.flush',
|
80
|
+
'exit'
|
81
|
+
]
|
82
|
+
)
|
83
|
+
end.to output(/\nhi\n.*\naaa\nbbb\nccc.*\nbye\n/m).to_stdout
|
84
|
+
.and(not_output.to_stderr)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'handles unexpected exit status' do
|
88
|
+
expect do
|
89
|
+
clazz.process(
|
90
|
+
"ruby -e 'i=$stdin.gets; $stdout.puts i; $stdout.flush; " \
|
91
|
+
"$stderr.puts i; $stderr.flush; exit 1'",
|
92
|
+
puts_output: :error,
|
93
|
+
input_lines: ['hi']
|
94
|
+
)
|
95
|
+
end.to raise_error(
|
96
|
+
ProcessHelper::UnexpectedExitStatusError,
|
97
|
+
/Command failed/)
|
98
|
+
.and(output(/hi\nhi\n/).to_stdout)
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'pipes input before processing output' do
|
102
|
+
expect do
|
103
|
+
clazz.process(
|
104
|
+
"ruby -e 'i=$stdin.gets; $stdout.puts i; $stdout.flush; exit'",
|
105
|
+
input_lines: ['hi']
|
106
|
+
)
|
107
|
+
end.to output(/hi\n/m).to_stdout
|
108
|
+
.and(not_output.to_stderr)
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'fails if unprocessed input remains when command exits' do
|
112
|
+
# TODO: This fails when run with code coverage instrumentation
|
113
|
+
# enabled (via RubyMine). Why???
|
114
|
+
expect do
|
115
|
+
clazz.process(
|
116
|
+
"ruby -e 'i=$stdin.gets; $stdout.puts i; exit'",
|
117
|
+
input_lines: %w(hi unprocessed)
|
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
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ':expected_exit_status option' do
|
4
|
+
attr_reader :clazz
|
5
|
+
|
6
|
+
before do
|
7
|
+
@clazz = Clazz.new
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '== 0 (default)' do
|
11
|
+
describe 'when exit_status == 0' do
|
12
|
+
it 'succeeds' do
|
13
|
+
expect do
|
14
|
+
clazz.process('echo')
|
15
|
+
end.to not_raise_error
|
16
|
+
.and(output("\n").to_stdout)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'succeeds when :expected_exit_status is explicitly 0' do
|
20
|
+
expect do
|
21
|
+
clazz.process('echo', exp_st: 0)
|
22
|
+
end.to not_raise_error
|
23
|
+
.and(output("\n").to_stdout)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'when exit_status != 0' do
|
28
|
+
it 'fails with message' do
|
29
|
+
cmd_regex = Regexp.escape('`ls /does_not_exist`')
|
30
|
+
expect do
|
31
|
+
clazz.process('ls /does_not_exist', puts_output: :error)
|
32
|
+
end.to raise_error(
|
33
|
+
ProcessHelper::UnexpectedExitStatusError,
|
34
|
+
/Command failed, pid \d+ exit [1,2]\. Command: #{cmd_regex}\./
|
35
|
+
)
|
36
|
+
.and(output(/No such file or directory/).to_stdout)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '!= 0' do
|
42
|
+
describe 'when exit_status == 0' do
|
43
|
+
it 'fails with message' do
|
44
|
+
expected_regex = 'Command succeeded but was expected to fail, ' \
|
45
|
+
'pid \d+ exit 0 \(expected \[1, 2\]\). Command: ' \
|
46
|
+
"#{Regexp.escape('`echo`')}" \
|
47
|
+
'\.'
|
48
|
+
expect do
|
49
|
+
clazz.process('echo', exp_st: [1, 2], puts_output: :error)
|
50
|
+
end.to raise_error(ProcessHelper::UnexpectedExitStatusError, /#{expected_regex}/)
|
51
|
+
.and(output("\n").to_stdout)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe 'when exit_status != 0' do
|
56
|
+
it 'succeeds' do
|
57
|
+
expect do
|
58
|
+
clazz.process('ls /does_not_exist', exp_st: [1, 2], puts_output: :error)
|
59
|
+
end.to_not raise_error
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe 'multiple values' do
|
65
|
+
describe 'when exit_status == 0' do
|
66
|
+
describe 'when 0 is one of the expected values' do
|
67
|
+
it 'succeeds' do
|
68
|
+
expect do
|
69
|
+
clazz.process(
|
70
|
+
'echo',
|
71
|
+
expected_exit_status: [0, 98, 99],
|
72
|
+
)
|
73
|
+
end.to not_raise_error
|
74
|
+
.and(output("\n").to_stdout)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe 'when 0 is not one of the expected values' do
|
79
|
+
it 'fails with message' do
|
80
|
+
expected_regex = 'Command succeeded but was expected to fail, ' \
|
81
|
+
'pid \d+ exit 0 \(expected \[98, 99\]\). Command: ' \
|
82
|
+
"#{Regexp.escape('`echo`')}" \
|
83
|
+
'\.'
|
84
|
+
expect do
|
85
|
+
clazz.process(
|
86
|
+
'echo',
|
87
|
+
expected_exit_status: [98, 99],
|
88
|
+
)
|
89
|
+
end.to raise_error(
|
90
|
+
ProcessHelper::UnexpectedExitStatusError,
|
91
|
+
/#{expected_regex}/
|
92
|
+
)
|
93
|
+
.and(output("\n").to_stdout)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe 'when exit_status != 0' do
|
99
|
+
describe 'when the exit status is one of the expected values' do
|
100
|
+
it 'succeeds' do
|
101
|
+
expect do
|
102
|
+
clazz.process(
|
103
|
+
'ls /does_not_exist',
|
104
|
+
expected_exit_status: [0, 1, 2],
|
105
|
+
)
|
106
|
+
end.to not_raise_error
|
107
|
+
.and(output(/No such file or directory/).to_stdout)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe 'when the exit status is not one of the expected values' do
|
112
|
+
it 'fails with message' do
|
113
|
+
expected_regex = 'Command did not exit with one of the expected exit statuses, ' \
|
114
|
+
'pid \d+ exit [1,2] \(expected \[0, 98, 99\]\). Command: ' \
|
115
|
+
"#{Regexp.escape('`ls /does_not_exist`')}" \
|
116
|
+
'\.'
|
117
|
+
expect do
|
118
|
+
clazz.process(
|
119
|
+
'ls /does_not_exist',
|
120
|
+
expected_exit_status: [0, 98, 99],
|
121
|
+
puts_output: :error)
|
122
|
+
end.to raise_error(
|
123
|
+
ProcessHelper::UnexpectedExitStatusError,
|
124
|
+
/#{expected_regex}/
|
125
|
+
)
|
126
|
+
.and(output(/No such file or directory/).to_stdout)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'supports long form of option' do
|
133
|
+
expect do
|
134
|
+
clazz.process('echo', expected_exit_status: 0)
|
135
|
+
end.to not_raise_error
|
136
|
+
.and(output("\n").to_stdout)
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ':include_output_in_exception option' do
|
4
|
+
attr_reader :clazz
|
5
|
+
|
6
|
+
before do
|
7
|
+
@clazz = Clazz.new
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '== true (default)' do
|
11
|
+
describe 'includes output in exception if exception' do
|
12
|
+
it 'when :expected_exit_status is zero' do
|
13
|
+
expect do
|
14
|
+
clazz.process(
|
15
|
+
'ls /does_not_exist',
|
16
|
+
puts_output: :error,
|
17
|
+
include_output_in_exception: true
|
18
|
+
)
|
19
|
+
end.to raise_error(
|
20
|
+
ProcessHelper::UnexpectedExitStatusError,
|
21
|
+
/Command Output: "ls:.*\/does_not_exist: No such file or directory\n"/)
|
22
|
+
.and(output(/No such file or directory/).to_stdout)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'when :expected_exit_status is nonzero' do
|
26
|
+
expect do
|
27
|
+
clazz.process(
|
28
|
+
'echo stdout > /dev/stdout',
|
29
|
+
puts_output: :error,
|
30
|
+
expected_exit_status: 1,
|
31
|
+
include_output_in_exception: true)
|
32
|
+
end.to raise_error(
|
33
|
+
ProcessHelper::UnexpectedExitStatusError,
|
34
|
+
/Command Output: "stdout\n"/)
|
35
|
+
.and(output("stdout\n").to_stdout)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '== false' do
|
41
|
+
describe 'does not includes output in exception if exception' do
|
42
|
+
it 'when :expected_exit_status is zero' do
|
43
|
+
expect do
|
44
|
+
clazz.process(
|
45
|
+
'ls /does_not_exist',
|
46
|
+
puts_output: :error,
|
47
|
+
include_output_in_exception: false)
|
48
|
+
end
|
49
|
+
.to output(/No such file or directory/).to_stdout
|
50
|
+
.and(
|
51
|
+
raise_error(ProcessHelper::UnexpectedExitStatusError) do |e|
|
52
|
+
expect(e.message).not_to match(/Command Output/)
|
53
|
+
expect(e.message).not_to match(/No such file or directory/)
|
54
|
+
end
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'when :expected_exit_status is nonzero' do
|
59
|
+
expect do
|
60
|
+
clazz.process(
|
61
|
+
'echo stdout > /dev/stdout',
|
62
|
+
expected_exit_status: 1,
|
63
|
+
puts_output: :error,
|
64
|
+
include_output_in_exception: false)
|
65
|
+
end.to output("stdout\n").to_stdout
|
66
|
+
.and(
|
67
|
+
raise_error(ProcessHelper::UnexpectedExitStatusError) do |e|
|
68
|
+
expect(e.message).not_to match(/Command Output:/)
|
69
|
+
expect(e.message).not_to match(/No such file or directory/)
|
70
|
+
end
|
71
|
+
)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ':puts_output option' do
|
4
|
+
attr_reader :clazz
|
5
|
+
|
6
|
+
before do
|
7
|
+
@clazz = Clazz.new
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '== :always (default)' do
|
11
|
+
it 'puts output to stdout' do
|
12
|
+
expect do
|
13
|
+
clazz.process('echo stdout > /dev/stdout', puts_output: :always)
|
14
|
+
end.to output("stdout\n").to_stdout
|
15
|
+
|
16
|
+
expect do
|
17
|
+
clazz.process('echo stdout > /dev/stdout')
|
18
|
+
end.to output("stdout\n").to_stdout
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '== :error' do
|
23
|
+
describe 'when :expected_exit_status is zero' do
|
24
|
+
it 'puts output to stdout on exception' do
|
25
|
+
expect do
|
26
|
+
clazz.process('ls /does_not_exist', puts_output: :error)
|
27
|
+
end.to raise_error(
|
28
|
+
ProcessHelper::UnexpectedExitStatusError,
|
29
|
+
/Command failed/)
|
30
|
+
.and(output(/No such file or directory/).to_stdout)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'suppresses stdout if no exception' do
|
34
|
+
expect do
|
35
|
+
clazz.process('echo stdout > /dev/stdout', puts_output: :error)
|
36
|
+
end.to not_output.to_stdout
|
37
|
+
.and(not_output.to_stderr)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'when :expected_exit_status is nonzero' do
|
42
|
+
it 'puts output to stdout on exception' do
|
43
|
+
expect do
|
44
|
+
clazz.process(
|
45
|
+
'echo stdout > /dev/stdout',
|
46
|
+
expected_exit_status: 1,
|
47
|
+
puts_output: :error)
|
48
|
+
end.to raise_error(
|
49
|
+
ProcessHelper::UnexpectedExitStatusError,
|
50
|
+
/Command succeeded but was expected to fail/)
|
51
|
+
.and(output("stdout\n").to_stdout)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'suppresses stdout if no exception' do
|
55
|
+
expect do
|
56
|
+
clazz.process(
|
57
|
+
'ls /does_not_exist',
|
58
|
+
expected_exit_status: [1, 2],
|
59
|
+
puts_output: :error)
|
60
|
+
end.to not_output.to_stdout
|
61
|
+
.and(not_output.to_stderr)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '== :never' do
|
67
|
+
it 'suppresses stdout' do
|
68
|
+
expect do
|
69
|
+
clazz.process(
|
70
|
+
'echo stdout > /dev/stdout',
|
71
|
+
puts_output: :never)
|
72
|
+
end.to not_output.to_stdout
|
73
|
+
.and(output(/WARNING/).to_stderr)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|