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