rspec-bash-x 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3c43c7fbf3a490d9c1de3f55d518a1dfe6a368f3
4
+ data.tar.gz: 5825ea963d9d70acddb25193711f215260a715ea
5
+ SHA512:
6
+ metadata.gz: 6feba6cddfa852151ab182b599bc33f77269b8caffb835b6c50d3fd44c7f06a6cadef43729f5956411979b3d3bc043031bcea386717922688efc65e29384d1fd
7
+ data.tar.gz: 9f5724c6fee93438e390ff151b6a34d0af3a13464f828721fca0e79dae967f3a77b6aff3898212083165a918ba8c5820dacc2fc881ed08536fd5b31016d93f7f
data/CHANGELOG.md ADDED
File without changes
data/LICENSE.md ADDED
File without changes
data/README.md ADDED
File without changes
@@ -0,0 +1,24 @@
1
+ require 'rspec/mocks'
2
+ require 'rspec/mocks/space'
3
+
4
+ require_relative '../../lib/rspec/bash/mocks/script_proxy'
5
+ require_relative '../../lib/rspec/bash/script'
6
+
7
+ module RSpec
8
+ module Mocks
9
+ class Space
10
+ alias __rspec_bash_proxy_for proxy_for
11
+
12
+ def proxy_for(object)
13
+ return __rspec_bash_proxy_for(object) unless object.is_a?(RSpec::Bash::Script)
14
+
15
+ proxy_mutex.synchronize do
16
+ id = id_for(object)
17
+ proxies.fetch(id) do
18
+ proxies[id] = RSpec::Bash::Mocks::ScriptProxy.new(object, @expectation_ordering)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
data/lib/rspec/bash.rb ADDED
@@ -0,0 +1,25 @@
1
+ require_relative './bash/configuration'
2
+ require_relative './bash/fd'
3
+ require_relative './bash/noisy_thread'
4
+ require_relative './bash/open3'
5
+ require_relative './bash/script'
6
+ require_relative './bash/script_evaluator'
7
+ require_relative './bash/support'
8
+ require_relative './bash/version'
9
+ require_relative './bash/mocks/doubles'
10
+ require_relative './bash/mocks/matchers'
11
+ require_relative './bash/mocks/script_message_expectation'
12
+ require_relative './bash/mocks/script_proxy'
13
+ require_relative '../../ext/rspec-mocks/space'
14
+
15
+ module RSpec
16
+ module Bash
17
+ def self.configuration
18
+ @configuration ||= Configuration.new
19
+ end
20
+
21
+ def self.configure(&block)
22
+ yield configuration
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ module RSpec
2
+ module Bash
3
+ class Configuration
4
+ attr_accessor :allow_unstubbed_conditionals
5
+ attr_accessor :read_fd
6
+ attr_accessor :throttle
7
+ attr_accessor :verbose
8
+ attr_accessor :write_fd
9
+
10
+ def initialize()
11
+ @allow_unstubbed_conditionals = true
12
+ @throttle = 25 / 1000.0
13
+ @read_fd = 62
14
+ @write_fd = 63
15
+ @verbose = false
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,49 @@
1
+ __rspec_bash_stub_body=""
2
+
3
+ let r_fd=${BASHIT_R_FD:-4}
4
+ let w_fd=${BASHIT_W_FD:-5}
5
+
6
+ function __rspec_bash_write() {
7
+ builtin echo 1>&$w_fd $@
8
+ }
9
+
10
+ function __rspec_bash_read() {
11
+ builtin read -u $r_fd $@
12
+ }
13
+
14
+ function __rspec_bash_retrieve_stub() {
15
+ local name=$1
16
+
17
+ builtin shift 1
18
+
19
+ __rspec_bash_write $name $@
20
+ __rspec_bash_write "</rspec_bash::stub>"
21
+ __rspec_bash_read __rspec_bash_stub_body
22
+ __rspec_bash_write "</rspec_bash::stub-body>"
23
+
24
+ builtin test -s "${__rspec_bash_stub_body}"
25
+ }
26
+
27
+ function __rspec_bash_run_stub() {
28
+ __rspec_bash_retrieve_stub $@
29
+
30
+ builtin . "${__rspec_bash_stub_body}" $@
31
+ }
32
+
33
+ function test() {
34
+ if __rspec_bash_retrieve_stub "conditional_expr" $@; then
35
+ builtin . "${__rspec_bash_stub_body}" $@
36
+ else
37
+ builtin test $@
38
+ fi
39
+ }
40
+
41
+ function [()(
42
+ local without_bracket="${@:1:$(($#-1))}"
43
+
44
+ if __rspec_bash_retrieve_stub "conditional_expr" "${without_bracket[@]}"; then
45
+ builtin . "${__rspec_bash_stub_body}" "${without_bracket[@]}"
46
+ else
47
+ builtin [ $@
48
+ fi
49
+ )
@@ -0,0 +1,37 @@
1
+ module RSpec
2
+ module Bash
3
+ module FD
4
+ def self.readable?(fd)
5
+ begin
6
+ !fd.closed? && !fd.eof?
7
+ rescue IOError => e
8
+ if e.to_s == "stream closed"
9
+ return false
10
+ else
11
+ throw
12
+ end
13
+ end
14
+ end
15
+
16
+ def self.poll(fd, throttle: 25 / 1000, &block)
17
+ while readable?(fd) do
18
+ begin
19
+ yield fd
20
+ sleep throttle if throttle > 0
21
+ rescue IO::WaitReadable
22
+ IO.select([ fd ])
23
+ retry
24
+ rescue IOError => e
25
+ if e.to_s == "stream closed" || e.to_s == "closed stream"
26
+ break
27
+ else
28
+ throw e
29
+ end
30
+ rescue EOFError
31
+ break
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,12 @@
1
+ require_relative './doubles/abstract_double'
2
+ require_relative './doubles/conditional_double'
3
+ require_relative './doubles/function_double'
4
+
5
+ module RSpec
6
+ module Bash
7
+ module Mocks
8
+ module Doubles
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,33 @@
1
+ module RSpec
2
+ module Bash
3
+ module Mocks
4
+ module Doubles
5
+ class AbstractDouble
6
+ attr_accessor :body,
7
+ :call_original,
8
+ :calls,
9
+ :expected_call_count,
10
+ :expected_calls,
11
+ :subshell
12
+
13
+ def initialize(*)
14
+ @body = nil
15
+ @call_original = false
16
+ @calls = []
17
+ @expected_call_count = [:at_least, 1]
18
+ @expected_calls = []
19
+ @subshell = true
20
+ end
21
+
22
+ def apply
23
+ fail "NotImplemented"
24
+ end
25
+
26
+ def call_count
27
+ fail "NotImplemented"
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,31 @@
1
+ module RSpec
2
+ module Bash
3
+ module Mocks
4
+ module Doubles
5
+ class ConditionalDouble < AbstractDouble
6
+ def initialize(expr)
7
+ super()
8
+
9
+ @expr = expr
10
+ end
11
+
12
+ def apply(script)
13
+ script.stub_conditional(@expr, &body)
14
+ end
15
+
16
+ def call_count(script)
17
+ script.conditional_calls_for(@expr).count
18
+ end
19
+
20
+ def call_args(script)
21
+ script.conditional_calls_for(@expr).map { |x| x[:args] }
22
+ end
23
+
24
+ def to_s
25
+ @expr.to_s
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ module RSpec
2
+ module Bash
3
+ module Mocks
4
+ module Doubles
5
+ class FunctionDouble < AbstractDouble
6
+ def initialize(routine)
7
+ super()
8
+
9
+ @routine = routine
10
+ end
11
+
12
+ def apply(script)
13
+ script.stub(@routine, call_original: call_original, subshell: subshell, &body)
14
+ end
15
+
16
+ def call_count(script)
17
+ script.calls_for(@routine).count
18
+ end
19
+
20
+ def call_args(script)
21
+ script.calls_for(@routine).map { |x| x[:args] }
22
+ end
23
+
24
+ def to_s
25
+ @routine.to_s
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ require_relative './matchers/receive'
2
+ require_relative './matchers/test'
3
+
4
+ module RSpec
5
+ module Bash
6
+ module Mocks
7
+ module Matchers
8
+ def receive(*args)
9
+ Receive.new(*args)
10
+ end
11
+
12
+ def test(*args)
13
+ Test.new(*args)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,101 @@
1
+ require 'rspec/mocks'
2
+
3
+ module RSpec
4
+ module Bash
5
+ module Mocks
6
+ module Matchers
7
+ # @private
8
+ class BaseMatcher
9
+ include ::RSpec::Mocks::Matchers::Matcher
10
+
11
+ attr_reader :double
12
+
13
+ def initialize()
14
+ fail "@double must be created by implementation" if @double.nil?
15
+ fail "@display_name must be specified by implementation" if @display_name.nil?
16
+ end
17
+
18
+ def name
19
+ @display_name
20
+ end
21
+
22
+ def with_args(args)
23
+ tap { @double.expected_calls << args }
24
+ end
25
+
26
+ def and_return(code)
27
+ tap { @double.body = lambda { |*| "return #{code}" } }
28
+ end
29
+
30
+ def and_yield(subshell: true, &block)
31
+ tap {
32
+ @double.body = block
33
+ @double.subshell = subshell
34
+ }
35
+ end
36
+
37
+ def exactly(n)
38
+ tap { @double.expected_call_count = [:exactly, n] }
39
+ end
40
+
41
+ def at_least(n)
42
+ tap { @double.expected_call_count = [:at_least, n] }
43
+ end
44
+
45
+ def at_most(n)
46
+ tap { @double.expected_call_count = [:at_most, n] }
47
+ end
48
+
49
+ def and_call_original
50
+ tap { @double.call_original = true }
51
+ end
52
+
53
+ def never
54
+ exactly(0)
55
+ end
56
+
57
+ def once
58
+ exactly(1)
59
+ end
60
+
61
+ def twice
62
+ exactly(2)
63
+ end
64
+
65
+ def thrice
66
+ exactly(3)
67
+ end
68
+
69
+ def times
70
+ self
71
+ end
72
+
73
+ # @private
74
+ #
75
+ # (RSpec::Bash::Script): RSpec::Bash::Mocks::ScriptMessageExpectation
76
+ def matches?(subject, &block)
77
+ proxy_for(subject).expect_message(
78
+ double: @double,
79
+ display_name: @display_name
80
+ )
81
+ end
82
+
83
+ # @private
84
+ #
85
+ # (RSpec::Bash::Script): RSpec::Bash::Mocks::ScriptMessageExpectation
86
+ def setup_allowance(subject, &block)
87
+ proxy_for(subject).allow_message(
88
+ double: @double
89
+ )
90
+ end
91
+
92
+ private
93
+
94
+ def proxy_for(subject)
95
+ ::RSpec::Mocks.space.proxy_for(subject)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,20 @@
1
+ require_relative '../doubles'
2
+ require_relative './base_matcher'
3
+
4
+ module RSpec
5
+ module Bash
6
+ module Mocks
7
+ module Matchers
8
+ # @private
9
+ class Receive < BaseMatcher
10
+ def initialize(routine)
11
+ @double = Doubles::FunctionDouble.new(routine)
12
+ @display_name = "receive"
13
+
14
+ super()
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ require_relative '../doubles'
2
+ require_relative './base_matcher'
3
+
4
+ module RSpec
5
+ module Bash
6
+ module Mocks
7
+ module Matchers
8
+ # @private
9
+ class Test < BaseMatcher
10
+ def initialize(expr)
11
+ @double = Doubles::ConditionalDouble.new(expr)
12
+ @display_name = "test(#{expr})"
13
+
14
+ super()
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,78 @@
1
+ require 'rspec/mocks'
2
+ require 'rspec/mocks/argument_list_matcher'
3
+ require 'rspec/mocks/matchers/expectation_customization'
4
+
5
+ module RSpec
6
+ module Bash
7
+ module Mocks
8
+ class ScriptMessageExpectation
9
+ attr_reader :expected_args, :message
10
+
11
+ def initialize(double:, display_name:, error_generator:, backtrace_line: nil)
12
+ @double = double
13
+ @display_name = display_name
14
+ @error_generator = error_generator
15
+ @backtrace_line = backtrace_line
16
+ @expected_args = double.expected_calls
17
+ @message = @display_name
18
+ end
19
+
20
+ def invoke(*)
21
+ end
22
+
23
+ def matches?(*)
24
+ end
25
+
26
+ def called_max_times?(*)
27
+ false
28
+ end
29
+
30
+ def verify_messages_received(script)
31
+ type, expected_count = *@double.expected_call_count
32
+ actual_count = @double.call_count(script)
33
+
34
+ report = lambda {
35
+ @error_generator.raise_expectation_error(
36
+ @display_name,
37
+ expected_count,
38
+ ::RSpec::Mocks::ArgumentListMatcher::MATCH_ALL,
39
+ actual_count,
40
+ nil,
41
+ Array(@double.to_s),
42
+ @backtrace_line
43
+ )
44
+ }
45
+
46
+ case type
47
+ when :at_least
48
+ report[] if actual_count < expected_count
49
+ when :at_most
50
+ report[] if actual_count > expected_count
51
+ when :exactly
52
+ report[] if actual_count != expected_count
53
+ else
54
+ fail "Unrecognized call-count quantifier \"#{type}\""
55
+ end
56
+
57
+ @double.call_args(script).tap do |actual_args|
58
+ @double.expected_calls.each_with_index do |args, index|
59
+ expected = args
60
+ actual = actual_args[index]
61
+
62
+ if actual != expected
63
+ @error_generator.raise_unexpected_message_args_error(
64
+ self,
65
+ actual_args.map { |x| Array(x) },
66
+ )
67
+ break
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ def unadvise(*)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,45 @@
1
+ require 'rspec/support'
2
+ require 'rspec/support/caller_filter'
3
+ require 'rspec/mocks'
4
+ require 'rspec/mocks/proxy'
5
+
6
+ require_relative './script_message_expectation'
7
+
8
+ module RSpec
9
+ module Bash
10
+ module Mocks
11
+ class ScriptProxy < ::RSpec::Mocks::Proxy
12
+ def initialize(*)
13
+ @expectations = []
14
+ super
15
+ end
16
+
17
+ def reset
18
+ @expectations.clear
19
+ super
20
+ end
21
+
22
+ def verify
23
+ @expectations.each do |expectation|
24
+ expectation.verify_messages_received(@object)
25
+ end
26
+ end
27
+
28
+ def expect_message(double:, display_name:)
29
+ allow_message(double: double)
30
+
31
+ @expectations << ScriptMessageExpectation.new(
32
+ double: double,
33
+ display_name: display_name,
34
+ error_generator: @error_generator,
35
+ backtrace_line: ::RSpec::CallerFilter.first_non_rspec_line
36
+ )
37
+ end
38
+
39
+ def allow_message(double:)
40
+ double.apply(object)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,11 @@
1
+ module RSpec
2
+ module Bash
3
+ class NoisyThread < Thread
4
+ def initialize(**)
5
+ super.tap do
6
+ self.abort_on_exception = true
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,41 @@
1
+ require 'open3'
2
+
3
+ module RSpec
4
+ module Bash
5
+ module Open3
6
+ # an extended version of Open3.popen3 which exposes two pipes for internal
7
+ # communication between the spawned process and ruby
8
+ #
9
+ # the descriptors are available as 62 (child read) and 63 (child write)
10
+ #
11
+ # @param [Array<String>] cmd
12
+ def self.popen3X(cmd, read_fd: 62, write_fd: 63, &block)
13
+ in_r, in_w = IO.pipe
14
+ out_r, out_w = IO.pipe
15
+ err_r, err_w = IO.pipe
16
+ b2r_r, b2r_w = IO.pipe
17
+ r2b_r, r2b_w = IO.pipe
18
+
19
+ opts = {}
20
+ opts[:in] = in_r
21
+ opts[:out] = out_w
22
+ opts[:err] = err_w
23
+ opts[read_fd] = r2b_r
24
+ opts[write_fd] = b2r_w
25
+
26
+ env = {
27
+ "BASHIT_R_FD" => "#{read_fd}",
28
+ "BASHIT_W_FD" => "#{write_fd}"
29
+ }
30
+
31
+ ::Open3.send(:popen_run,
32
+ [env] + cmd,
33
+ opts,
34
+ [in_r, out_w, err_w, r2b_r, b2r_w], # child_io
35
+ [in_w, out_r, err_r, r2b_w, b2r_r], # parent_io
36
+ &block
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,122 @@
1
+ module RSpec
2
+ module Bash
3
+ class Script
4
+ MAIN_SCRIPT_FILE = File.expand_path('../controller.sh', __FILE__)
5
+ NOOP = lambda { |*| '' }
6
+
7
+ def self.load(path)
8
+ new(File.read(path))
9
+ end
10
+
11
+ attr_reader :source, :source_file, :stdout, :stderr, :exit_code
12
+
13
+ def initialize(source, path = 'Anonymous')
14
+ @conditional_stubs = []
15
+ @conditional_stub_calls = []
16
+ @source = source
17
+ @source_file = path
18
+ @stubs = {}
19
+ @stub_calls = Hash.new { |h, k| h[k] = [] }
20
+ @stdout = ""
21
+ @stderr = ""
22
+ @exit_code = nil
23
+ end
24
+
25
+ def to_s
26
+ to_bash_script
27
+ end
28
+
29
+ def inspect
30
+ "Script(\"#{File.basename(@source_file)}\")"
31
+ end
32
+
33
+ def stub(fn, call_original: false, subshell: true, &body)
34
+ @stubs[fn.to_sym] = {
35
+ body: (call_original || !body) ? NOOP : body,
36
+ subshell: subshell,
37
+ call_original: call_original
38
+ }
39
+ end
40
+
41
+ def stub_conditional(expr, &body)
42
+ @conditional_stubs << { expr: expr, body: body || NOOP }
43
+ end
44
+
45
+ def stubbed(name, args)
46
+ fail "#{name} is not stubbed" unless @stubs.key?(name.to_sym)
47
+
48
+ @stubs[name.to_sym][:body].call(args)
49
+ end
50
+
51
+ def stubbed_conditional(expr, args)
52
+ conditional_stub = @conditional_stubs.detect { |x| x[:expr] == expr }
53
+
54
+ if conditional_stub
55
+ conditional_stub[:body].call(args)
56
+ else
57
+ ""
58
+ end
59
+ end
60
+
61
+ def has_conditional_stubs?
62
+ @conditional_stubs.any?
63
+ end
64
+
65
+ def calls_for(name)
66
+ @stub_calls[name.to_sym]
67
+ end
68
+
69
+ def conditional_calls_for(expr)
70
+ @conditional_stub_calls.select { |x| x[:expr] == expr }
71
+ end
72
+
73
+ def track_call(name, args)
74
+ fail "#{name} is not stubbed" unless @stubs.key?(name.to_sym)
75
+
76
+ @stub_calls[name.to_sym].push({ args: args })
77
+ end
78
+
79
+ def track_conditional_call(expr, args)
80
+ @conditional_stub_calls.push({ expr: expr, args: args })
81
+ end
82
+
83
+ def track_exit_code(code)
84
+ @exit_code = code
85
+ end
86
+
87
+ private
88
+
89
+ def to_bash_script
90
+ buffer = ""
91
+ buffer << "builtin source '#{Script::MAIN_SCRIPT_FILE}'"
92
+ buffer << "\n"
93
+
94
+ @stubs.keys.each do |name|
95
+ stub_def = @stubs[name]
96
+
97
+ if stub_def[:call_original] then
98
+ buffer << <<-EOF
99
+ #{name}() {
100
+ __rspec_bash_run_stub '#{name}' $@
101
+
102
+ builtin #{name} $@
103
+ }
104
+ EOF
105
+ elsif stub_def[:subshell] == false then
106
+ buffer << <<-EOF
107
+ #{name}() {
108
+ __rspec_bash_run_stub '#{name}' $@
109
+ }
110
+ EOF
111
+ else
112
+ buffer << "#{name}()(__rspec_bash_run_stub '#{name}' $@)\n"
113
+ end
114
+ end
115
+
116
+ buffer << "\n"
117
+ buffer << @source
118
+ buffer
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,189 @@
1
+ require 'open3'
2
+ require 'expect'
3
+ require 'tempfile'
4
+ require_relative './fd'
5
+ require_relative './open3'
6
+ require_relative './noisy_thread'
7
+
8
+ module RSpec
9
+ module Bash
10
+ class ScriptEvaluator
11
+ CONDITIONAL_EXPR_STUB = 'conditional_expr'.freeze
12
+ BLOCK_SIZE = 4096
13
+
14
+ # (String, Object?): Boolean
15
+ #
16
+ # @param [String] script
17
+ # @param [Hash?] options
18
+ # @param [Number?] options.read_fd
19
+ # @param [Number?] options.write_fd
20
+ # @param [Number?] options.throttle
21
+ def eval(script, args = [], **opts)
22
+ file = Tempfile.new('rspec_bash')
23
+ file.write(script.to_s)
24
+ file.close
25
+ verbose = opts.fetch(:verbose, Bash.configuration.verbose)
26
+
27
+ bus_file = Tempfile.new("rspec_bash#{File.basename(script.source_file).gsub(/\W+/, '_')}")
28
+ bus_file.close
29
+
30
+ Bash::Open3.popen3X([ '/usr/bin/env', 'bash', file.path ].concat(args), {
31
+ read_fd: opts.fetch(:read_fd, Bash.configuration.read_fd),
32
+ write_fd: opts.fetch(:write_fd, Bash.configuration.write_fd)
33
+ }) do |input, stdout, stderr, r2b, b2r, wait_thr|
34
+ workers = []
35
+
36
+ # transmit stdout
37
+ workers << NoisyThread.new do
38
+ FD.poll(stdout, throttle: Bash.configuration.throttle) do
39
+ buffer = stdout.read_nonblock(BLOCK_SIZE)
40
+
41
+ script.stdout << buffer
42
+
43
+ if verbose
44
+ STDOUT.write buffer
45
+ STDOUT.flush
46
+ end
47
+ end
48
+ end
49
+
50
+ # transmit stderr
51
+ workers << NoisyThread.new do
52
+ FD.poll(stderr, throttle: Bash.configuration.throttle) do
53
+ buffer = stderr.read_nonblock(BLOCK_SIZE)
54
+
55
+ script.stderr << buffer
56
+
57
+ if verbose
58
+ STDERR.write buffer
59
+ STDERR.flush
60
+ end
61
+ end
62
+ end
63
+
64
+ # accept & respond to prompts
65
+ workers << NoisyThread.new do
66
+ FD.poll(b2r, throttle: Bash.configuration.throttle) do
67
+ respond_to_prompts(r2b, b2r, script, bus_file)
68
+ end
69
+ end
70
+
71
+ # wait for the script to finish executing
72
+ wait_thr.join
73
+
74
+ # clean up
75
+ try_hard "close r2b" do r2b.close end
76
+ try_hard "close b2r" do b2r.close end
77
+ try_hard "shut off workers" do workers.map(&:join) end
78
+ try_hard "kill them all" do workers.map(&:kill) end
79
+ try_hard "clean up temp bus file" do bus_file.unlink end
80
+ try_hard "clean up source file" do file.unlink end
81
+
82
+ script.track_exit_code wait_thr.value.exitstatus
83
+
84
+ wait_thr.value.success?
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def respond_to_prompts(fd_in, fd_out, script, bus_file)
91
+ fd_out.expect("</rspec_bash::stub>", 1) do |result|
92
+ break if result.nil?
93
+
94
+ prompts = result[0].split("\n").reject(&:empty?).reduce([]) do |acc, line|
95
+ if line == "</rspec_bash::stub>"
96
+ if acc[-1]
97
+ acc[-1].merge!(classify_stub(acc[-1][:buffer]))
98
+ else
99
+ puts "[WARN] cannot match stub entry: #{line} => #{acc}"
100
+ end
101
+ else
102
+ acc.push({ type: :unknown, buffer: line })
103
+ end
104
+
105
+ acc
106
+ end
107
+
108
+ prompts.each do |type:, buffer:, **stub|
109
+ case type
110
+ when :conditional
111
+ if !script.has_conditional_stubs? && !Bash.configuration.allow_unstubbed_conditionals
112
+ fail "conditional expressions are not stubbed!"
113
+ end
114
+
115
+ File.write(bus_file, script.stubbed_conditional(stub[:expr], stub[:args]))
116
+
117
+ fd_in.puts bus_file.path
118
+ fd_in.flush
119
+
120
+ fd_out.expect('</rspec_bash::stub-body>', 1)
121
+
122
+ script.track_conditional_call(stub[:expr], stub[:args])
123
+ when :function
124
+ routine = stub[:name]
125
+ args = stub[:args]
126
+ body = script.stubbed(routine, args)
127
+
128
+ File.write(bus_file, body)
129
+
130
+ fd_in.puts bus_file.path
131
+ fd_in.flush
132
+
133
+ fd_out.expect('</rspec_bash::stub-body>', 1)
134
+
135
+ script.track_call(routine, args)
136
+ when :unknown
137
+ STDERR.write "[err] unexpected message from bash: #{buffer}"
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ def classify_stub(command)
144
+ identifier, args = split_by_first_space(command)
145
+
146
+ case identifier
147
+ when CONDITIONAL_EXPR_STUB
148
+ expr, expr_args = split_by_first_space(args)
149
+
150
+ {
151
+ type: :conditional,
152
+ expr: expr,
153
+ args: expr_args,
154
+ }
155
+ else
156
+ {
157
+ type: :function,
158
+ name: identifier,
159
+ args: args,
160
+ }
161
+ end
162
+ end
163
+
164
+ def split_by_first_space(string)
165
+ delim = string.index(' ')
166
+
167
+ # single-argument expressions, this usually happens in unary tests
168
+ # where the argument evaluates to an empty string, a la:
169
+ #
170
+ # test -z "${string}" => "-z"
171
+ if delim.nil?
172
+ return [ string, '' ]
173
+ end
174
+
175
+ [ string[0..delim - 1], string[delim + 1..-1] ]
176
+ end
177
+
178
+ def try_hard(what)
179
+ begin
180
+ yield
181
+ rescue StandardError => e
182
+ puts what
183
+ puts e
184
+ puts e.backtrace
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,9 @@
1
+ module RSpec
2
+ module Bash
3
+ module Support
4
+ def run_script(script, args = [], **opts)
5
+ ScriptEvaluator.new.eval(script, args, opts)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module RSpec
2
+ module Bash
3
+ VERSION = '1.0.0'
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-bash-x
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Ahmad Amireh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-03-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec-support
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec-mocks
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.6'
41
+ description:
42
+ email: ahmad@instructure.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - CHANGELOG.md
48
+ - LICENSE.md
49
+ - README.md
50
+ - ext/rspec-mocks/space.rb
51
+ - lib/rspec/bash.rb
52
+ - lib/rspec/bash/configuration.rb
53
+ - lib/rspec/bash/controller.sh
54
+ - lib/rspec/bash/fd.rb
55
+ - lib/rspec/bash/mocks/doubles.rb
56
+ - lib/rspec/bash/mocks/doubles/abstract_double.rb
57
+ - lib/rspec/bash/mocks/doubles/conditional_double.rb
58
+ - lib/rspec/bash/mocks/doubles/function_double.rb
59
+ - lib/rspec/bash/mocks/matchers.rb
60
+ - lib/rspec/bash/mocks/matchers/base_matcher.rb
61
+ - lib/rspec/bash/mocks/matchers/receive.rb
62
+ - lib/rspec/bash/mocks/matchers/test.rb
63
+ - lib/rspec/bash/mocks/script_message_expectation.rb
64
+ - lib/rspec/bash/mocks/script_proxy.rb
65
+ - lib/rspec/bash/noisy_thread.rb
66
+ - lib/rspec/bash/open3.rb
67
+ - lib/rspec/bash/script.rb
68
+ - lib/rspec/bash/script_evaluator.rb
69
+ - lib/rspec/bash/support.rb
70
+ - lib/rspec/bash/version.rb
71
+ homepage:
72
+ licenses:
73
+ - AGPL-3.0
74
+ metadata: {}
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 2.1.0
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubyforge_project:
91
+ rubygems_version: 2.5.2
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: Test Bash scripts with RSpec.
95
+ test_files: []
96
+ has_rdoc: