rspec-bash-x 1.0.1 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: '0943308df600188a73abc10314d5ecebb942670b'
4
- data.tar.gz: c86f4a7ff3988a3014fc1a31b3c9053fd1c224da
3
+ metadata.gz: 74cf305ed10730b811f278b17e84579525ca2f75
4
+ data.tar.gz: 69fafdc217a018885b31b568ac0a7f95c0752bdb
5
5
  SHA512:
6
- metadata.gz: c7efe7d2457e5e2d166037b01deaf8f66da11e590c48538aa8b56df5963fe26ab83978c0cf9270f1c0a26c9e6da824434d2c4c4bc5d9f456b2b2d148f09ee203
7
- data.tar.gz: 4ff1bc4e194464cff9b2318343048355f7de467d678ab88b8403b854e83244c298a855947868200ca356bec3994b405f1e769e4a73bdb2ce361ba6f075b20bfe
6
+ metadata.gz: 335ba43a3659066d595d1dd6e085c6ba6f1503e5d24730adf808764c1a0a0394ccebf4d31d07185517b15cf05b577e265019f3b968f1b04252a08ed8143c87b5
7
+ data.tar.gz: 39decb1ca2a1193e0c7eb4046d572e8454dd9e8b289f1bff4db8efbf53bf9bfca8c2492b8b5d2bd8acf0eeaf346aad8d3580e09a39ce9c539e5d70ac620762bf
data/lib/rspec/bash/fd.rb CHANGED
@@ -5,10 +5,10 @@ module RSpec
5
5
  begin
6
6
  !fd.closed? && !fd.eof?
7
7
  rescue IOError => e
8
- if e.to_s == "stream closed"
8
+ if noise? e
9
9
  return false
10
10
  else
11
- throw
11
+ throw e
12
12
  end
13
13
  end
14
14
  end
@@ -22,7 +22,7 @@ module RSpec
22
22
  IO.select([ fd ])
23
23
  retry
24
24
  rescue IOError => e
25
- if e.to_s == "stream closed" || e.to_s == "closed stream"
25
+ if noise? e
26
26
  break
27
27
  else
28
28
  throw e
@@ -32,6 +32,10 @@ module RSpec
32
32
  end
33
33
  end
34
34
  end
35
+
36
+ def self.noise?(error)
37
+ error.to_s == "stream closed" || error.to_s == "closed stream"
38
+ end
35
39
  end
36
40
  end
37
41
  end
@@ -0,0 +1,70 @@
1
+ module RSpec
2
+ module Bash
3
+ class MessageDecoder
4
+ READING_FRAME_COUNT = 1
5
+ READING_FRAME_SZ = 2
6
+ READING_FRAME = 3
7
+ RE_NUMBER = /\d/
8
+
9
+ def self.decode(buffer)
10
+ state = READING_FRAME_COUNT
11
+ buffersz = buffer.length
12
+ frames = []
13
+ frame_buf = ''
14
+ frame_count_buf = ''
15
+ frame_count = Float::INFINITY
16
+ framesz_buf = ''
17
+ framesz = Float::INFINITY
18
+ cursor = 0
19
+
20
+ while cursor < buffersz && frames.count < frame_count
21
+ char = buffer[cursor]
22
+ cursor += 1
23
+
24
+ case state
25
+ when READING_FRAME_COUNT
26
+ case char
27
+ when ';'
28
+ state = READING_FRAME_SZ
29
+ frame_count = frame_count_buf.to_i
30
+ frame_count_buf = ''
31
+ framesz_buf = ''
32
+ when /\d/
33
+ frame_count_buf += char
34
+ else
35
+ return nil, "invalid payload: illegal character in header '#{char}' (#{cursor}/#{buffersz})"
36
+ end
37
+ when READING_FRAME_SZ
38
+ case char
39
+ when ';'
40
+ state = READING_FRAME
41
+ frame_buf = ''
42
+ framesz = framesz_buf.to_i
43
+ framesz_buf = ''
44
+ when /\d/
45
+ framesz_buf += char
46
+ else
47
+ return nil, "invalid payload: illegal character in frame header '#{char}' (#{cursor}/#{buffersz})"
48
+ end
49
+ when READING_FRAME
50
+ frame_buf += char
51
+
52
+ if frame_buf.length == framesz
53
+ state = READING_FRAME_SZ
54
+ frames.push(frame_buf)
55
+ framesz = Float::INFINITY
56
+ end
57
+ end
58
+ end
59
+
60
+ if frames.count != frame_count
61
+ return nil, "invalid payload: expected #{frame_count} frames but got #{frames.count}"
62
+ end
63
+
64
+ [ frames.map do |frame|
65
+ frame_class, frame_content = frame[0..1].to_i, frame.slice(2..-1)
66
+ end ]
67
+ end
68
+ end
69
+ end
70
+ end
@@ -3,15 +3,17 @@ module RSpec
3
3
  module Mocks
4
4
  module Doubles
5
5
  class AbstractDouble
6
- attr_accessor :body,
6
+ attr_accessor(
7
+ :behaviors,
7
8
  :call_original,
8
9
  :calls,
9
10
  :expected_call_count,
10
11
  :expected_calls,
11
12
  :subshell
13
+ )
12
14
 
13
15
  def initialize(*)
14
- @body = nil
16
+ @behaviors = []
15
17
  @call_original = false
16
18
  @calls = []
17
19
  @expected_call_count = [:at_least, 1]
@@ -3,6 +3,8 @@ module RSpec
3
3
  module Mocks
4
4
  module Doubles
5
5
  class ConditionalDouble < AbstractDouble
6
+ attr_accessor :expr
7
+
6
8
  def initialize(expr)
7
9
  super()
8
10
 
@@ -10,7 +12,9 @@ module RSpec
10
12
  end
11
13
 
12
14
  def apply(script)
13
- script.stub_conditional(@expr, &body)
15
+ script.stub_conditional(@expr,
16
+ behaviors: behaviors
17
+ )
14
18
  end
15
19
 
16
20
  def call_count(script)
@@ -18,7 +22,7 @@ module RSpec
18
22
  end
19
23
 
20
24
  def call_args(script)
21
- script.conditional_calls_for(@expr).map { |x| x[:args] }
25
+ script.conditional_calls_for(@expr)
22
26
  end
23
27
 
24
28
  def to_s
@@ -0,0 +1,35 @@
1
+ module RSpec
2
+ module Bash
3
+ module Mocks
4
+ module Doubles
5
+ class ExactConditionalDouble < AbstractDouble
6
+ attr_accessor :fullexpr
7
+
8
+ def initialize(fullexpr)
9
+ super()
10
+
11
+ @fullexpr = fullexpr
12
+ end
13
+
14
+ def apply(script)
15
+ script.stub_conditional(@fullexpr,
16
+ behaviors: behaviors
17
+ )
18
+ end
19
+
20
+ def call_count(script)
21
+ script.exact_conditional_calls_for(@fullexpr).count
22
+ end
23
+
24
+ def call_args(script)
25
+ script.exact_conditional_calls_for(@fullexpr)
26
+ end
27
+
28
+ def to_s
29
+ @fullexpr.to_s
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -10,7 +10,11 @@ module RSpec
10
10
  end
11
11
 
12
12
  def apply(script)
13
- script.stub(@routine, call_original: call_original, subshell: subshell, &body)
13
+ script.stub(@routine,
14
+ call_original: call_original,
15
+ behaviors: behaviors,
16
+ subshell: subshell,
17
+ )
14
18
  end
15
19
 
16
20
  def call_count(script)
@@ -1,5 +1,6 @@
1
1
  require_relative './doubles/abstract_double'
2
2
  require_relative './doubles/conditional_double'
3
+ require_relative './doubles/exact_conditional_double'
3
4
  require_relative './doubles/function_double'
4
5
 
5
6
  module RSpec
@@ -20,22 +20,47 @@ module RSpec
20
20
  end
21
21
 
22
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}" } }
23
+ tap {
24
+ @double.expected_calls << args
25
+ @double.behaviors << create_behavior({ args: args })
26
+ }
28
27
  end
29
28
 
30
- def and_yield(subshell: true, &block)
29
+ def and_yield(subshell: true, times: 1, &body)
31
30
  tap {
32
- @double.body = block
33
31
  @double.subshell = subshell
32
+
33
+ behavior = find_last_blank_or_create_behavior
34
+ behavior[:body] = body
35
+ behavior[:charges] = behavior[:charges] == 0 ? times : behavior[:charges]
36
+ behavior[:subshell] = subshell
34
37
  }
35
38
  end
36
39
 
40
+ def and_return(code, times: 1)
41
+ and_yield(subshell: false, times: times) { |*| "return #{code}" }
42
+ end
43
+
44
+ def and_always_return(code)
45
+ and_return(code, times: Float::INFINITY)
46
+ end
47
+
48
+ def and_always_yield(subshell: true, &body)
49
+ and_yield(subshell: subshell, times: Float::INFINITY, &body)
50
+ end
51
+
37
52
  def exactly(n)
38
- tap { @double.expected_call_count = [:exactly, n] }
53
+ tap do
54
+ if @double.behaviors.last
55
+ @double.behaviors.last[:charges] = n
56
+
57
+ (n-1).times do
58
+ @double.expected_calls << @double.expected_calls.last
59
+ end
60
+ else
61
+ @double.expected_call_count = [:exactly, n]
62
+ end
63
+ end
39
64
  end
40
65
 
41
66
  def at_least(n)
@@ -89,11 +114,25 @@ module RSpec
89
114
  )
90
115
  end
91
116
 
92
- private
117
+ protected
93
118
 
94
119
  def proxy_for(subject)
95
120
  ::RSpec::Mocks.space.proxy_for(subject)
96
121
  end
122
+
123
+ def find_last_blank_or_create_behavior
124
+ @double.behaviors.detect { |x| x[:body].nil? } || begin
125
+ create_behavior.tap { |x| @double.behaviors << x }
126
+ end
127
+ end
128
+
129
+ def create_behavior(args: nil, body: nil, charges: 0)
130
+ {
131
+ args: args,
132
+ body: body,
133
+ charges: charges
134
+ }
135
+ end
97
136
  end
98
137
  end
99
138
  end
@@ -0,0 +1,29 @@
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 TestBy < BaseMatcher
10
+ def initialize(expr)
11
+ @double = Doubles::ConditionalDouble.new(expr)
12
+ @display_name = "test_by"
13
+
14
+ super()
15
+ end
16
+
17
+ def with_args(args)
18
+ tap {
19
+ fullexpr = "#{@double.expr} #{args}"
20
+
21
+ @double.expected_calls << fullexpr
22
+ @double.behaviors << create_behavior(args: fullexpr)
23
+ }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,30 @@
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 TestFor < BaseMatcher
10
+ def initialize(fullexpr)
11
+ @double = Doubles::ExactConditionalDouble.new(fullexpr)
12
+ @display_name = "test_for"
13
+
14
+ super()
15
+ end
16
+
17
+ def with_args(args)
18
+ fail "#{to_s}: cannot be used with '.with_args', use 'test_by' instead"
19
+ end
20
+
21
+ protected
22
+
23
+ def create_behavior(**rest)
24
+ super(**rest, args: @double.fullexpr)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,5 +1,6 @@
1
1
  require_relative './matchers/receive'
2
- require_relative './matchers/test'
2
+ require_relative './matchers/test_for'
3
+ require_relative './matchers/test_by'
3
4
 
4
5
  module RSpec
5
6
  module Bash
@@ -9,8 +10,16 @@ module RSpec
9
10
  Receive.new(*args)
10
11
  end
11
12
 
12
- def test(*args)
13
- Test.new(*args)
13
+ def receive_function(method_name, &block)
14
+ ::RSpec::Mocks::Matchers::Receive.new(method_name, block)
15
+ end
16
+
17
+ def test_for(*args)
18
+ TestFor.new(*args)
19
+ end
20
+
21
+ def test_by(*args)
22
+ TestBy.new(*args)
14
23
  end
15
24
  end
16
25
  end
@@ -28,12 +28,12 @@ module RSpec
28
28
  def expect_message(double:, display_name:)
29
29
  allow_message(double: double)
30
30
 
31
- @expectations << ScriptMessageExpectation.new(
31
+ ScriptMessageExpectation.new(
32
32
  double: double,
33
33
  display_name: display_name,
34
34
  error_generator: @error_generator,
35
35
  backtrace_line: ::RSpec::CallerFilter.first_non_rspec_line
36
- )
36
+ ).tap { |x| @expectations << x }
37
37
  end
38
38
 
39
39
  def allow_message(double:)
@@ -1,14 +1,18 @@
1
+ require_relative './stub_behavior'
2
+ require_relative './script_generator'
3
+
1
4
  module RSpec
2
5
  module Bash
3
6
  class Script
4
7
  MAIN_SCRIPT_FILE = File.expand_path('../controller.sh', __FILE__)
5
8
  NOOP = lambda { |*| '' }
9
+ NOOP_BEHAVIOR = StubBehavior.new(body: NOOP, charges: Float::INFINITY)
6
10
 
7
11
  def self.load(path)
8
12
  new(File.read(path))
9
13
  end
10
14
 
11
- attr_reader :source, :source_file, :stdout, :stderr, :exit_code
15
+ attr_reader :exit_code, :source, :source_file, :stdout, :stderr, :stubs
12
16
 
13
17
  def initialize(source, path = 'Anonymous')
14
18
  @conditional_stubs = []
@@ -23,41 +27,46 @@ module RSpec
23
27
  end
24
28
 
25
29
  def to_s
26
- to_bash_script
30
+ ScriptGenerator.generate(self)
27
31
  end
28
32
 
29
33
  def inspect
30
34
  "Script(\"#{File.basename(@source_file)}\")"
31
35
  end
32
36
 
33
- def stub(fn, call_original: false, subshell: true, &body)
37
+ def stub(fn, behaviors:, call_original: false, subshell: true)
34
38
  @stubs[fn.to_sym] = {
35
- body: (call_original || !body) ? NOOP : body,
39
+ behaviors: behaviors.map { |x| StubBehavior.new(x) },
36
40
  subshell: subshell,
37
41
  call_original: call_original
38
42
  }
39
43
  end
40
44
 
41
- def stub_conditional(expr, &body)
42
- @conditional_stubs << { expr: expr, body: body || NOOP }
45
+ def stub_conditional(expr, behaviors:)
46
+ @conditional_stubs << {
47
+ behaviors: behaviors.map { |x| StubBehavior.new(x) },
48
+ expr: expr,
49
+ }
43
50
  end
44
51
 
45
52
  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)
53
+ apply_matching_behavior @stubs[name.to_sym], args
49
54
  end
50
55
 
51
- def stubbed_conditional(expr, args)
52
- conditional_stub = @conditional_stubs.detect { |x| x[:expr] == expr }
56
+ def stubbed_conditional(fullexpr)
57
+ conditional_stub = @conditional_stubs.detect { |x| fullexpr.index(x[:expr]) == 0 }
53
58
 
54
59
  if conditional_stub
55
- conditional_stub[:body].call(args)
60
+ apply_matching_behavior conditional_stub, fullexpr
56
61
  else
57
62
  ""
58
63
  end
59
64
  end
60
65
 
66
+ def has_stub?(name)
67
+ @stubs.key?(name.to_sym)
68
+ end
69
+
61
70
  def has_conditional_stubs?
62
71
  @conditional_stubs.any?
63
72
  end
@@ -67,7 +76,11 @@ module RSpec
67
76
  end
68
77
 
69
78
  def conditional_calls_for(expr)
70
- @conditional_stub_calls.select { |x| x[:expr] == expr }
79
+ @conditional_stub_calls.select { |x| x.index(expr) == 0 }
80
+ end
81
+
82
+ def exact_conditional_calls_for(fullexpr)
83
+ @conditional_stub_calls.select { |x| x == fullexpr }
71
84
  end
72
85
 
73
86
  def track_call(name, args)
@@ -76,8 +89,8 @@ module RSpec
76
89
  @stub_calls[name.to_sym].push({ args: args })
77
90
  end
78
91
 
79
- def track_conditional_call(expr, args)
80
- @conditional_stub_calls.push({ expr: expr, args: args })
92
+ def track_conditional_call(fullexpr)
93
+ @conditional_stub_calls.push(fullexpr)
81
94
  end
82
95
 
83
96
  def track_exit_code(code)
@@ -86,36 +99,11 @@ module RSpec
86
99
 
87
100
  private
88
101
 
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
102
+ def apply_matching_behavior(stub, args)
103
+ behavior = stub[:behaviors].detect { |x| x.usable? && x.applicable?(args) }
104
+ behavior ||= stub[:behaviors].detect { |x| x.usable? && x.context_free? }
105
+ behavior ||= NOOP_BEHAVIOR
106
+ behavior.apply!(args)
119
107
  end
120
108
  end
121
109
  end
@@ -4,12 +4,18 @@ require 'tempfile'
4
4
  require_relative './fd'
5
5
  require_relative './open3'
6
6
  require_relative './noisy_thread'
7
+ require_relative './message_decoder'
7
8
 
8
9
  module RSpec
9
10
  module Bash
10
11
  class ScriptEvaluator
12
+ BLOCK_SIZE = 1024
11
13
  CONDITIONAL_EXPR_STUB = 'conditional_expr'.freeze
12
- BLOCK_SIZE = 4096
14
+ FRAME_NAME = 1
15
+ FRAME_ARG = 2
16
+ FRAME_TRACE = 3
17
+ MESSAGE_REQ = '<rspec-bash::req>'.freeze
18
+ MESSAGE_ACK = '<rspec-bash::ack>'.freeze
13
19
 
14
20
  # (String, Object?): Boolean
15
21
  #
@@ -63,8 +69,8 @@ module RSpec
63
69
 
64
70
  # accept & respond to prompts
65
71
  workers << NoisyThread.new do
66
- FD.poll(b2r, throttle: Bash.configuration.throttle) do
67
- respond_to_prompts(r2b, b2r, script, bus_file)
72
+ FD.poll(b2r, throttle: 0) do
73
+ respond_to_prompts(r2b, b2r, bus_file, script)
68
74
  end
69
75
  end
70
76
 
@@ -87,92 +93,96 @@ module RSpec
87
93
 
88
94
  private
89
95
 
90
- def respond_to_prompts(fd_in, fd_out, script, bus_file)
91
- fd_out.expect("</rspec_bash::stub>", 1) do |result|
96
+ def respond_to_prompts(fd_in, fd_out, bus_file, script)
97
+ fd_out.expect(MESSAGE_REQ, 1) do |result|
92
98
  break if result.nil?
93
99
 
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
100
+ lines = result[0].split("\n").reject(&:empty?)
104
101
 
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
102
+ stubs = lines.each_with_index.reduce([]) do |acc, (line, index)|
103
+ next acc unless line == MESSAGE_REQ
114
104
 
115
- File.write(bus_file, script.stubbed_conditional(stub[:expr], stub[:args]))
105
+ message = lines[index-1]
106
+ frames, err = MessageDecoder.decode(message)
116
107
 
117
- fd_in.puts bus_file.path
118
- fd_in.flush
108
+ if err
109
+ STDERR.puts <<-EOF
110
+ bash-rspec: communication between Ruby and Bash failed, this is
111
+ most likely an internal error.
119
112
 
120
- fd_out.expect('</rspec_bash::stub-body>', 1)
113
+ #{err}
114
+ EOF
121
115
 
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)
116
+ next acc
117
+ end
127
118
 
128
- File.write(bus_file, body)
119
+ stub = frames.reduce({ expr: nil, type: nil, args: [], stacktrace: [] }) do |x, (type, content)|
120
+ case type
121
+ when FRAME_NAME
122
+ x[:expr] = content
123
+ x[:type] = content == CONDITIONAL_EXPR_STUB ? :conditional : :function
124
+ when FRAME_ARG
125
+ x[:args] << content
126
+ when FRAME_TRACE
127
+ x[:stacktrace] << content unless content.empty?
128
+ else
129
+ STDERR.puts "rspec-bash: unrecognized frame '#{type}' => '#{content}'"
130
+ STDERR.puts "rspec-bash: source:\n#{message}"
131
+ end
129
132
 
130
- fd_in.puts bus_file.path
131
- fd_in.flush
133
+ x
134
+ end.tap do |stub|
135
+ stub[:args] = stub[:args].join(' ')
136
+ end
132
137
 
133
- fd_out.expect('</rspec_bash::stub-body>', 1)
138
+ acc.push(stub)
139
+ end
134
140
 
135
- script.track_call(routine, args)
141
+ stubs.each do |stub|
142
+ case stub[:type]
143
+ when :conditional
144
+ relay_conditional_stub(fd_in, fd_out, bus_file, script: script, stub: stub)
145
+ when :function
146
+ relay_command_stub(fd_in, fd_out, bus_file, script: script, stub: stub)
136
147
  when :unknown
137
- STDERR.write "[err] unexpected message from bash: #{buffer}"
148
+ STDERR.puts "[err] unexpected message from bash: #{stub.inspect}"
138
149
  end
139
150
  end
140
151
  end
141
152
  end
142
153
 
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
- }
154
+ def relay_command_stub(fd_in, fd_out, bus_file, script:, stub:)
155
+ if !script.has_stub?(stub[:expr])
156
+ fail(
157
+ "#{stub[:expr]} is not stubbed!\n\n" +
158
+ "Call stack:\n" +
159
+ stub[:stacktrace].map { |x| "- #{x}" }.join("\n")
160
+ )
161
161
  end
162
- end
163
162
 
164
- def split_by_first_space(string)
165
- delim = string.index(' ')
163
+ File.write(bus_file, script.stubbed(stub[:expr], stub[:args]))
164
+
165
+ fd_in.puts bus_file.path
166
+ fd_in.flush
167
+
168
+ fd_out.expect(MESSAGE_ACK, 1)
166
169
 
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, '' ]
170
+ script.track_call(stub[:expr], stub[:args])
171
+ end
172
+
173
+ def relay_conditional_stub(fd_in, fd_out, bus_file, script:, stub:)
174
+ if !script.has_conditional_stubs? && !Bash.configuration.allow_unstubbed_conditionals
175
+ fail "conditional expressions are not stubbed!\n#{stub[:stacktrace]}"
173
176
  end
174
177
 
175
- [ string[0..delim - 1], string[delim + 1..-1] ]
178
+ File.write(bus_file, script.stubbed_conditional(stub[:args]))
179
+
180
+ fd_in.puts bus_file.path
181
+ fd_in.flush
182
+
183
+ fd_out.expect(MESSAGE_ACK, 1)
184
+
185
+ script.track_conditional_call(stub[:args])
176
186
  end
177
187
 
178
188
  def try_hard(what)
@@ -0,0 +1,17 @@
1
+ function test() {
2
+ if __rspec_bash_load_stub 'conditional_expr' "${@}"; then
3
+ builtin . "${__rspec_bash_stub_body}" "${@}"
4
+ else
5
+ builtin test "${@}"
6
+ fi
7
+ }
8
+
9
+ function [() {
10
+ local without_bracket="${@:1:$(($#-1))}"
11
+
12
+ if __rspec_bash_load_stub 'conditional_expr' "${without_bracket[@]}"; then
13
+ builtin . "${__rspec_bash_stub_body}" "${without_bracket[@]}"
14
+ else
15
+ builtin [ "${@}"
16
+ fi
17
+ }
@@ -0,0 +1,53 @@
1
+ export __rspec_bash_stub_body=""
2
+
3
+ function __rspec_bash_read() {
4
+ local fd=${BASHIT_R_FD:-4}
5
+
6
+ builtin read -u $fd "${@}"
7
+ }
8
+
9
+ function __rspec_bash_write() {
10
+ local fd=${BASHIT_W_FD:-5}
11
+
12
+ builtin echo 1>&$fd "${@}"
13
+ }
14
+
15
+ function __rspec_bash_load_stub() {
16
+ local name="${1}"
17
+ local arg
18
+ local message
19
+ local fragments=(
20
+ "1 ${name}"
21
+ "3 $(caller 1)"
22
+ "3 $(caller 2)"
23
+ "3 $(caller 3)"
24
+ )
25
+
26
+ builtin shift 1
27
+
28
+ for arg in "${@}"; do
29
+ fragments+=("2 ${arg}")
30
+ done
31
+
32
+ message="${#fragments[@]};"
33
+
34
+ for fragment in "${fragments[@]}"; do
35
+ message="${message}${#fragment};${fragment}"
36
+ done
37
+
38
+ __rspec_bash_write "${message}"
39
+ __rspec_bash_write "<rspec-bash::req>"
40
+
41
+ __rspec_bash_read __rspec_bash_stub_body
42
+ __rspec_bash_write "<rspec-bash::ack>"
43
+
44
+ builtin test -s "${__rspec_bash_stub_body}"
45
+ }
46
+
47
+ function __rspec_bash_call_stubbed() {
48
+ __rspec_bash_load_stub "${@}"
49
+
50
+ builtin shift 1
51
+
52
+ builtin . "${__rspec_bash_stub_body}" "${@}"
53
+ }
@@ -0,0 +1,58 @@
1
+ module RSpec
2
+ module Bash
3
+ class ScriptGenerator
4
+ NOOP = lambda { |*| '' }
5
+ SCRIPTS = {
6
+ conditionals: File.expand_path('../script_generator/conditional.sh', __FILE__),
7
+ controller: File.expand_path('../script_generator/controller.sh', __FILE__)
8
+ }
9
+ SPIES = {
10
+ builtin: lambda { |name|
11
+ <<-EOF
12
+ #{name}() {
13
+ __rspec_bash_call_stubbed '#{name}' "${@}"
14
+
15
+ builtin #{name} $@
16
+ }
17
+ EOF
18
+ },
19
+ }
20
+ STUBS = {
21
+ function: lambda { |name|
22
+ <<-EOF
23
+ #{name}() {
24
+ __rspec_bash_call_stubbed '#{name}' "${@}"
25
+ }
26
+ EOF
27
+ },
28
+
29
+ function_in_subshell: lambda { |name|
30
+ "#{name}()(__rspec_bash_call_stubbed '#{name}' \"${@}\")\n"
31
+ }
32
+ }
33
+
34
+ def self.generate(script)
35
+ buffer = ""
36
+ buffer << "builtin . '#{SCRIPTS[:controller]}'\n"
37
+ buffer << "builtin . '#{SCRIPTS[:conditionals]}'\n" if script.has_conditional_stubs?
38
+ buffer << "\n"
39
+
40
+ script.stubs.keys.each do |name|
41
+ stub_def = script.stubs[name]
42
+
43
+ if stub_def[:call_original] then
44
+ buffer << SPIES[:builtin].call(name)
45
+ elsif stub_def[:subshell] == false then
46
+ buffer << STUBS[:function].call(name)
47
+ else
48
+ buffer << STUBS[:function_in_subshell].call(name)
49
+ end
50
+ end
51
+
52
+ buffer << "\n"
53
+ buffer << script.source
54
+ buffer
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,35 @@
1
+ module RSpec
2
+ module Bash
3
+ class StubBehavior
4
+ attr_accessor :args, :body
5
+
6
+ def initialize(args: nil, body:, charges: 1, subshell: true)
7
+ @args = args
8
+ @body = body
9
+ @subshell = subshell
10
+ @charges = charges
11
+ end
12
+
13
+ def usable?
14
+ @charges > 0
15
+ end
16
+
17
+ def applicable?(args)
18
+ @args == args
19
+ end
20
+
21
+ def context_free?
22
+ @args.nil?
23
+ end
24
+
25
+ def requires_subshell?
26
+ @subshell
27
+ end
28
+
29
+ def apply!(args)
30
+ @charges -= 1
31
+ @body.call(args)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,6 +1,10 @@
1
1
  module RSpec
2
2
  module Bash
3
3
  module Support
4
+ def a_script(*args)
5
+ Script.new(*args)
6
+ end
7
+
4
8
  def run_script(script, args = [], **opts)
5
9
  ScriptEvaluator.new.eval(script, args, opts)
6
10
  end
@@ -1,5 +1,5 @@
1
1
  module RSpec
2
2
  module Bash
3
- VERSION = '1.0.1'
3
+ VERSION = '1.1.0'
4
4
  end
5
5
  end
data/lib/rspec/bash.rb CHANGED
@@ -4,6 +4,7 @@ require_relative './bash/noisy_thread'
4
4
  require_relative './bash/open3'
5
5
  require_relative './bash/script'
6
6
  require_relative './bash/script_evaluator'
7
+ require_relative './bash/script_generator'
7
8
  require_relative './bash/support'
8
9
  require_relative './bash/version'
9
10
  require_relative './bash/mocks/doubles'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-bash-x
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ahmad Amireh
@@ -50,22 +50,28 @@ files:
50
50
  - ext/rspec-mocks/space.rb
51
51
  - lib/rspec/bash.rb
52
52
  - lib/rspec/bash/configuration.rb
53
- - lib/rspec/bash/controller.sh
54
53
  - lib/rspec/bash/fd.rb
54
+ - lib/rspec/bash/message_decoder.rb
55
55
  - lib/rspec/bash/mocks/doubles.rb
56
56
  - lib/rspec/bash/mocks/doubles/abstract_double.rb
57
57
  - lib/rspec/bash/mocks/doubles/conditional_double.rb
58
+ - lib/rspec/bash/mocks/doubles/exact_conditional_double.rb
58
59
  - lib/rspec/bash/mocks/doubles/function_double.rb
59
60
  - lib/rspec/bash/mocks/matchers.rb
60
61
  - lib/rspec/bash/mocks/matchers/base_matcher.rb
61
62
  - lib/rspec/bash/mocks/matchers/receive.rb
62
- - lib/rspec/bash/mocks/matchers/test.rb
63
+ - lib/rspec/bash/mocks/matchers/test_by.rb
64
+ - lib/rspec/bash/mocks/matchers/test_for.rb
63
65
  - lib/rspec/bash/mocks/script_message_expectation.rb
64
66
  - lib/rspec/bash/mocks/script_proxy.rb
65
67
  - lib/rspec/bash/noisy_thread.rb
66
68
  - lib/rspec/bash/open3.rb
67
69
  - lib/rspec/bash/script.rb
68
70
  - lib/rspec/bash/script_evaluator.rb
71
+ - lib/rspec/bash/script_generator.rb
72
+ - lib/rspec/bash/script_generator/conditional.sh
73
+ - lib/rspec/bash/script_generator/controller.sh
74
+ - lib/rspec/bash/stub_behavior.rb
69
75
  - lib/rspec/bash/support.rb
70
76
  - lib/rspec/bash/version.rb
71
77
  homepage:
@@ -1,49 +0,0 @@
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
- )
@@ -1,20 +0,0 @@
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