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
@@ -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
|