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 +7 -0
- data/CHANGELOG.md +0 -0
- data/LICENSE.md +0 -0
- data/README.md +0 -0
- data/ext/rspec-mocks/space.rb +24 -0
- data/lib/rspec/bash.rb +25 -0
- data/lib/rspec/bash/configuration.rb +19 -0
- data/lib/rspec/bash/controller.sh +49 -0
- data/lib/rspec/bash/fd.rb +37 -0
- data/lib/rspec/bash/mocks/doubles.rb +12 -0
- data/lib/rspec/bash/mocks/doubles/abstract_double.rb +33 -0
- data/lib/rspec/bash/mocks/doubles/conditional_double.rb +31 -0
- data/lib/rspec/bash/mocks/doubles/function_double.rb +31 -0
- data/lib/rspec/bash/mocks/matchers.rb +18 -0
- data/lib/rspec/bash/mocks/matchers/base_matcher.rb +101 -0
- data/lib/rspec/bash/mocks/matchers/receive.rb +20 -0
- data/lib/rspec/bash/mocks/matchers/test.rb +20 -0
- data/lib/rspec/bash/mocks/script_message_expectation.rb +78 -0
- data/lib/rspec/bash/mocks/script_proxy.rb +45 -0
- data/lib/rspec/bash/noisy_thread.rb +11 -0
- data/lib/rspec/bash/open3.rb +41 -0
- data/lib/rspec/bash/script.rb +122 -0
- data/lib/rspec/bash/script_evaluator.rb +189 -0
- data/lib/rspec/bash/support.rb +9 -0
- data/lib/rspec/bash/version.rb +5 -0
- metadata +96 -0
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,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,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
|
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:
|