rspec-bash-x 1.0.0

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