expect-pty 0.2.0 → 0.3.1

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.
@@ -0,0 +1,414 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class RelayRecoveryTest < ExpectTest
6
+ def sink_session(**)
7
+ reader, peer = IO.pipe
8
+ sink, writer = IO.pipe
9
+ @ios.push(reader, peer, sink, writer)
10
+ session = Expect.open(reader, writer: writer, **)
11
+ @sessions << session
12
+ [session, sink, peer]
13
+ end
14
+
15
+ def fill(writer)
16
+ total = 0
17
+ loop do
18
+ count = writer.write_nonblock("x" * 4096, exception: false)
19
+ return total if count == :wait_writable
20
+
21
+ total += count
22
+ end
23
+ end
24
+
25
+ def drain(reader)
26
+ data = "".b
27
+ loop do
28
+ chunk = reader.read_nonblock(65_536, exception: false)
29
+ return data if chunk == :wait_readable || chunk.nil?
30
+
31
+ data << chunk
32
+ end
33
+ end
34
+
35
+ def test_outer_deadline_bounds_raw_io_and_expect_listeners
36
+ [false, true].each do |wrapped|
37
+ source, = pipe_session
38
+ target, = sink_session
39
+ fill(target.writer)
40
+ source.listeners = [wrapped ? target : target.writer]
41
+ source.buffer = "request"
42
+ started = Expect.monotonic
43
+ assert_nil bounded(0.5) { Expect.interconnect(source, target, timeout: 0.02) }
44
+ assert_operator Expect.monotonic - started, :<, 0.3
45
+ end
46
+ end
47
+
48
+ def test_partial_timeout_and_retry_deliver_exactly_once_to_each_listener
49
+ source, source_writer = pipe_session
50
+ target, sink = sink_session(write_timeout: 0.02)
51
+ first = StringIO.new
52
+ source.listeners = [first, target]
53
+ payload = (0..255).to_a.pack("C*") * 2048
54
+ source.buffer = payload
55
+ assert_raises(Expect::WriteTimeout) { bounded { Expect.interconnect(source, timeout: 1) } }
56
+ received = drain(sink)
57
+ assert_operator received.bytesize, :>, 0
58
+ assert_operator received.bytesize, :<, payload.bytesize
59
+ target.write_timeout = 2
60
+ receiver = background do
61
+ loop { received << sink.readpartial(65_536) }
62
+ rescue EOFError
63
+ received
64
+ end
65
+ source_writer.close
66
+ assert_same(source, bounded { Expect.interconnect(source, timeout: 2) })
67
+ target.writer.close
68
+ assert receiver.join(2), "receiver did not finish"
69
+ assert_equal payload.bytesize, receiver.value.bytesize
70
+ assert payload == receiver.value, "resumed pipe output differs from the original bytes"
71
+ assert_equal payload.bytesize, first.string.bytesize
72
+ assert payload == first.string.b, "first listener output differs from the original bytes"
73
+ end
74
+
75
+ def test_short_custom_writes_preserve_suffix
76
+ source, = pipe_session
77
+ received = "".b
78
+ target = Object.new
79
+ target.define_singleton_method(:write) do |data|
80
+ count = [2, data.bytesize].min
81
+ received << data.byteslice(0, count)
82
+ count
83
+ end
84
+ source.listeners = [target]
85
+ source.buffer = "abcdef"
86
+ Expect.interconnect(source, timeout: 0.02)
87
+ assert_equal "abcdef", received
88
+ end
89
+
90
+ def test_failed_second_listener_does_not_replay_first_listener
91
+ source, = pipe_session
92
+ first = StringIO.new
93
+ second = StringIO.new
94
+ failing = true
95
+ original = second.method(:write)
96
+ second.define_singleton_method(:write) do |data|
97
+ raise IOError, "listener failed" if failing
98
+
99
+ original.call(data)
100
+ end
101
+ source.listeners = [first, second]
102
+ source.buffer = "payload"
103
+ assert_raises(IOError) { Expect.interconnect(source, timeout: 0.02) }
104
+ failing = false
105
+ Expect.interconnect(source, timeout: 0.02)
106
+ assert_equal "payload", first.string
107
+ assert_equal "payload", second.string
108
+ end
109
+
110
+ def test_repeated_interact_with_raw_input_keeps_read_ahead
111
+ session, sink = sink_session
112
+ input, keyboard = IO.pipe
113
+ @ios.push(input, keyboard)
114
+ keyboard.write("one!two!")
115
+ first = session.interact(input: input, output: StringIO.new, escape: "!", timeout: 0.05)
116
+ second = session.interact(input: input, output: StringIO.new, escape: "!", timeout: 0.05)
117
+ assert_same first, second
118
+ assert_equal "onetwo", drain(sink)
119
+ refute input.closed?
120
+ end
121
+
122
+ def test_write_retries_read_eintr_during_backpressure
123
+ session, sink, peer = sink_session(write_timeout: 1)
124
+ fill(session.writer)
125
+ peer.write("reply")
126
+ original = session.to_io.method(:read_nonblock)
127
+ interrupted = false
128
+ session.to_io.define_singleton_method(:read_nonblock) do |*args, **kwargs|
129
+ unless interrupted
130
+ interrupted = true
131
+ raise Errno::EINTR
132
+ end
133
+ original.call(*args, **kwargs)
134
+ end
135
+ session.log_to { drain(sink) }
136
+ assert_equal(5, bounded { session.write("hello") })
137
+ assert interrupted
138
+ assert_equal "reply", session.buffer
139
+ assert_equal "hello", drain(sink)
140
+ end
141
+
142
+ def test_write_timeout_reports_accepted_bytes
143
+ session, sink = sink_session(write_timeout: 0.02)
144
+ error = assert_raises(Expect::WriteTimeout) { session.write("x" * 524_288) }
145
+ received = drain(sink)
146
+ assert_operator received.bytesize, :>, 0
147
+ assert_equal received.bytesize, error.bytes_written
148
+ end
149
+
150
+ def test_nested_output_timeout_preserves_the_callers_write_progress
151
+ %i[listeners log_output].each do |destination|
152
+ session, sink, peer = sink_session(write_timeout: 1)
153
+ target, = sink_session(write_timeout: 0)
154
+ fill(target.writer)
155
+ session.public_send(:"#{destination}=", destination == :listeners ? [target] : target)
156
+ peer.write("reply")
157
+
158
+ error = assert_raises(Expect::WriteTimeout) { bounded { session.write("x" * 1_048_576) } }
159
+ received = drain(sink)
160
+ assert_operator received.bytesize, :>, 0
161
+ assert_equal received.bytesize, error.bytes_written
162
+ assert_instance_of Expect::WriteTimeout, error.cause
163
+ assert_equal 0, error.cause.bytes_written
164
+ assert_equal "reply", session.buffer
165
+ end
166
+ end
167
+
168
+ def test_truncated_utf8_at_eof_raises_without_losing_bytes
169
+ session, writer = pipe_session
170
+ writer.write("\xe4".b)
171
+ writer.close
172
+ assert_raises(EncodingError) { session.expect(/中/u, timeout: 1) }
173
+ assert_equal "\xe4".b, session.buffer
174
+ assert_equal 1, session.expect("\xe4".b, timeout: 0)
175
+ end
176
+
177
+ def test_blocked_target_does_not_starve_another_source
178
+ slow, = pipe_session
179
+ fast, writer = pipe_session
180
+ target, = sink_session
181
+ fill(target.writer)
182
+ slow.listeners = [target]
183
+ slow.buffer = "blocked"
184
+ output = StringIO.new
185
+ fast.listeners = [output]
186
+ writer.write("ready")
187
+ assert_nil bounded(0.5) { Expect.interconnect(slow, fast, timeout: 0.02) }
188
+ assert_equal "ready", output.string
189
+ assert slow.pending_output?
190
+ end
191
+
192
+ def test_outer_timeout_keeps_output_for_original_targets
193
+ source, = pipe_session
194
+ target, sink = sink_session
195
+ fill(target.writer)
196
+ source.listeners = [target.writer]
197
+ source.buffer = "request"
198
+ Expect.interconnect(source, timeout: 0.01)
199
+ assert source.pending_output?
200
+ assert_empty source.buffer
201
+ drain(sink)
202
+ replacement = StringIO.new
203
+ source.listeners = [replacement]
204
+ source.buffer = "new"
205
+ Expect.interconnect(source, timeout: 0.02)
206
+ refute source.pending_output?
207
+ assert_equal "request", drain(sink)
208
+ assert_equal "new", replacement.string
209
+ end
210
+
211
+ def test_every_short_write_failure_position_resumes_without_replay
212
+ 0.upto(5) do |failure_offset|
213
+ source, = pipe_session
214
+ first = StringIO.new
215
+ received = "".b
216
+ failed = false
217
+ target = Object.new
218
+ target.define_singleton_method(:write) do |data|
219
+ if !failed && received.bytesize == failure_offset
220
+ failed = true
221
+ raise IOError, "interrupted delivery"
222
+ end
223
+ received << data.byteslice(0, 1)
224
+ 1
225
+ end
226
+ source.listeners = [first, target]
227
+ source.buffer = "abcdef"
228
+ assert_raises(IOError) { Expect.interconnect(source, timeout: 1) }
229
+ assert source.pending_output?
230
+ Expect.interconnect(source, timeout: 0.01)
231
+ assert_equal "abcdef", received
232
+ assert_equal "abcdef", first.string
233
+ refute source.pending_output?
234
+ end
235
+ end
236
+
237
+ def test_flush_failure_retries_flush_without_replaying_data
238
+ source, = pipe_session
239
+ output = StringIO.new
240
+ failed = false
241
+ output.define_singleton_method(:flush) do
242
+ return if failed
243
+
244
+ failed = true
245
+ raise IOError, "flush failed"
246
+ end
247
+ source.listeners = [output]
248
+ source.buffer = "once"
249
+ assert_raises(IOError) { Expect.interconnect(source, timeout: 1) }
250
+ Expect.interconnect(source, timeout: 0.01)
251
+ assert_equal "once", output.string
252
+ refute source.pending_output?
253
+ end
254
+
255
+ def test_invalid_write_count_raises_and_can_recover
256
+ [0, nil, -1, 7, "6"].each do |invalid|
257
+ source, = pipe_session
258
+ output = StringIO.new
259
+ source.listeners = [output]
260
+ source.buffer = "abcdef"
261
+ output.stub(:write, invalid) do
262
+ assert_raises(IOError) { bounded { Expect.interconnect(source, timeout: 1) } }
263
+ end
264
+ Expect.interconnect(source, timeout: 0.01)
265
+ assert_equal "abcdef", output.string
266
+ end
267
+ end
268
+
269
+ def test_escape_callback_waits_for_prefix_delivery_across_timeout
270
+ source, = pipe_session
271
+ target, sink = sink_session
272
+ fill(target.writer)
273
+ source.listeners = [target]
274
+ called = 0
275
+ source.on_sequence(/END\z/) do
276
+ called += 1
277
+ false
278
+ end
279
+ source.buffer = "prefixEND"
280
+ assert_nil Expect.interconnect(source, timeout: 0.01)
281
+ assert_equal 0, called
282
+ source.buffer = "tail"
283
+ drain(sink)
284
+ assert_same source, Expect.interconnect(source, timeout: 0.1)
285
+ assert_equal 1, called
286
+ assert_equal "prefix", drain(sink)
287
+ assert_equal "tail", source.buffer
288
+ end
289
+
290
+ def test_matching_window_does_not_truncate_undelivered_relay_input
291
+ source, writer = pipe_session(buffer_limit: 2)
292
+ target, sink = sink_session
293
+ fill(target.writer)
294
+ source.listeners = [target]
295
+ source.on_sequence("!")
296
+ writer.write("prefix!tail")
297
+ Expect.interconnect(source, timeout: 0.01)
298
+ assert_equal "tail", source.buffer
299
+ drain(sink)
300
+ assert_same source, Expect.interconnect(source, timeout: 0.1)
301
+ assert_equal "prefix", drain(sink)
302
+ assert_equal "tail", source.buffer
303
+ end
304
+
305
+ def test_unlisted_target_output_is_drained_without_losing_bytes
306
+ source, writer = pipe_session
307
+ target = child('puts "ready"; data = STDIN.read(131072); print "reply" if data.bytesize == 131072',
308
+ raw_pty: true, write_timeout: 1)
309
+ source.listeners = [target]
310
+ source.buffer = "x" * 131_072
311
+ writer.close
312
+ assert_same(source, bounded { Expect.interconnect(source, timeout: 2) })
313
+ refute source.pending_output?
314
+ assert_empty source.buffer
315
+ assert_equal 1, target.expect("ready", timeout: 1)
316
+ assert_equal 1, target.expect("reply", timeout: 1)
317
+ end
318
+
319
+ def test_eof_validates_truncated_utf8_in_escape_regex
320
+ source, writer = pipe_session
321
+ source.on_sequence(/中/u)
322
+ writer.write("\xe4".b)
323
+ writer.close
324
+ assert_raises(EncodingError) { Expect.interconnect(source, timeout: 1) }
325
+ end
326
+
327
+ def test_instance_regexp_timeout_propagates_and_keeps_buffer
328
+ source, = pipe_session
329
+ source.buffer = "#{"a" * 30}!"
330
+ regexp = Regexp.new('\\A(a+)+\\1\\z', timeout: 0.01)
331
+ global_timeout = Regexp.timeout
332
+ assert_raises(Regexp::TimeoutError) { bounded { source.expect(regexp, timeout: 0) } }
333
+ assert_equal "#{"a" * 30}!", source.buffer
334
+ assert Regexp.timeout == global_timeout, "matching must not change the process regexp timeout"
335
+ assert_equal 0.01, regexp.timeout
336
+ end
337
+
338
+ def test_closed_cached_input_wrapper_can_be_reopened_without_closing_raw_io
339
+ session, sink = sink_session
340
+ input, keyboard = IO.pipe
341
+ @ios.push(input, keyboard)
342
+ keyboard.write("one!")
343
+ source = session.interact(input: input, output: StringIO.new, escape: "!", timeout: 0.1)
344
+ source.close
345
+ keyboard.write("two!")
346
+ resumed = session.interact(input: input, output: StringIO.new, escape: "!", timeout: 0.1)
347
+ refute_same source, resumed
348
+ assert_equal "onetwo", drain(sink)
349
+ resumed.graceful_close = true
350
+ bounded(0.5) { session.close }
351
+ assert resumed.closed?
352
+ refute input.closed?
353
+ end
354
+
355
+ def test_repeated_read_interruptions_do_not_extend_write_timeout
356
+ session, = sink_session(write_timeout: 0.02)
357
+ fill(session.writer)
358
+ reads = 0
359
+ session.to_io.stub(:read_nonblock, lambda { |*|
360
+ reads += 1
361
+ raise Errno::EINTR
362
+ }) do
363
+ IO.stub(:select, ->(*) { [[session.to_io], [], []] }) do
364
+ error = assert_raises(Expect::WriteTimeout) { bounded(0.5) { session.write("request") } }
365
+ assert_equal 0, error.bytes_written
366
+ end
367
+ end
368
+ assert_operator reads, :>, 1
369
+ end
370
+
371
+ def test_zero_timeout_retains_a_blocked_escape_prefix_for_retry
372
+ source, = pipe_session
373
+ target, sink = sink_session
374
+ fill(target.writer)
375
+ source.on_sequence("STOP")
376
+ source.listeners = [target]
377
+ source.buffer = "ST"
378
+ assert_nil bounded(0.5) { Expect.interconnect(source, timeout: 0) }
379
+ assert source.pending_output?
380
+ drain(sink)
381
+ Expect.interconnect(source, timeout: 0.01)
382
+ assert_equal "ST", drain(sink)
383
+ refute source.pending_output?
384
+ end
385
+
386
+ def test_short_log_writes_keep_all_bytes
387
+ source, = pipe_session
388
+ output = StringIO.new
389
+ original = output.method(:write)
390
+ output.define_singleton_method(:write) { |data| original.call(data.byteslice(0, 1)) }
391
+ source.log_to(output)
392
+ source.write_log("complete")
393
+ assert_equal "complete", output.string
394
+ end
395
+
396
+ def test_interrupted_custom_output_is_retried_without_waiting_for_new_input
397
+ source, = pipe_session
398
+ output = StringIO.new
399
+ interrupted = false
400
+ original = output.method(:write)
401
+ output.define_singleton_method(:write) do |data|
402
+ unless interrupted
403
+ interrupted = true
404
+ raise Errno::EINTR
405
+ end
406
+ original.call(data)
407
+ end
408
+ source.listeners = [output]
409
+ source.on_sequence("!")
410
+ source.buffer = "ready!"
411
+ assert_same source, bounded(0.5) { Expect.interconnect(source, timeout: 0.05) }
412
+ assert_equal "ready", output.string
413
+ end
414
+ end
data/test/release_test.rb CHANGED
@@ -25,7 +25,7 @@ class ReleaseTest < Minitest::Test
25
25
  capture_io { release.run }
26
26
  end
27
27
  end
28
- directory = Dir.glob(File.join("pkg", "release", Expect::VERSION, "candidate-*")).fetch(0)
28
+ directory = Dir.glob(File.join("tmp", "release", Expect::VERSION, "candidate-*")).fetch(0)
29
29
  checksum = File.read(File.join(directory, "SHA256SUMS"))
30
30
  assert_equal "#{Digest::SHA256.file(artifact).hexdigest} #{File.basename(artifact)}\n", checksum
31
31
  assert_equal "- Release fixture.\n", File.read(File.join(directory, "release-notes.md"))
@@ -34,6 +34,92 @@ class ReleaseTest < Minitest::Test
34
34
  end
35
35
  end
36
36
 
37
+ def test_rubygems_only_publishes_the_verified_copy_without_github
38
+ with_package do |_release, artifact|
39
+ release = Release.new(artifact: artifact, rubygems_only: true)
40
+ bytes = File.binread(artifact)
41
+ checksum = Digest::SHA256.hexdigest(bytes)
42
+ published = false
43
+ capture = ->(*arguments) { arguments.include?("rev-parse") ? "verified" : "" }
44
+ get = lambda do |path|
45
+ if path.start_with?("/downloads/")
46
+ Struct.new(:code, :body).new("200", bytes)
47
+ elsif published
48
+ Struct.new(:code, :body).new("200", JSON.generate("sha" => checksum, "yanked" => false))
49
+ else
50
+ Struct.new(:code, :body).new("404", "")
51
+ end
52
+ end
53
+ push = lambda do |*arguments|
54
+ candidate = arguments.fetch(2)
55
+ assert_equal ["gem", "push", candidate, "--host", "https://rubygems.org"], arguments
56
+ assert_match %r{/tmp/release/#{Regexp.escape(Expect::VERSION)}/candidate-[^/]+/}, candidate
57
+ assert_equal bytes, File.binread(candidate)
58
+ refute_equal File.expand_path(artifact), candidate
59
+ published = true
60
+ end
61
+ release.stub(:capture, capture) do
62
+ release.stub(:get, get) do
63
+ release.stub(:system, push) do
64
+ release.stub(:github, ->(*) { flunk "RubyGems-only release contacted GitHub" }) do
65
+ release.stub(:publish_github, -> { flunk "RubyGems-only release published to GitHub" }) do
66
+ [nil, "true"].each do |github_actions|
67
+ with_environment("GITHUB_ACTIONS" => github_actions,
68
+ "GEM_HOST_API_KEY" => (github_actions ? "test-only-api-key" : nil)) do
69
+ published = false
70
+ output, = capture_io { release.run }
71
+ assert_includes output, "SHA256 verified"
72
+ assert published
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ def test_ci_publish_requires_an_api_key_before_pushing
84
+ release = Release.new(rubygems_only: true)
85
+ release.stub(:registry_version, nil) do
86
+ release.stub(:system, ->(*) { flunk "pushed without a CI API key" }) do
87
+ [nil, ""].each do |api_key|
88
+ with_environment("GITHUB_ACTIONS" => "true", "GEM_HOST_API_KEY" => api_key) do
89
+ error = assert_raises(RuntimeError) { release.send(:publish_rubygems) }
90
+ assert_includes error.message, "RUBYGEMS_API_KEY"
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ def test_rubygems_only_rejects_uncommitted_source_before_publishing
98
+ with_package do |_release, artifact|
99
+ release = Release.new(artifact: artifact, rubygems_only: true)
100
+ release.stub(:capture, " M payload.rb") do
101
+ release.stub(:get, ->(*) { flunk "uncommitted source contacted RubyGems" }) do
102
+ assert_match "Commit all source changes", assert_raises(RuntimeError) { release.run }.message
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def test_rubygems_only_rechecks_source_after_verification
109
+ with_package do |_release, artifact|
110
+ release = Release.new(artifact: artifact, rubygems_only: true)
111
+ statuses = ["", " M payload.rb"]
112
+ capture = ->(*arguments) { arguments.include?("rev-parse") ? "verified" : statuses.shift }
113
+ release.stub(:capture, capture) do
114
+ release.stub(:get, ->(*) { flunk "changed source contacted RubyGems" }) do
115
+ capture_io do
116
+ assert_match "Source changed", assert_raises(RuntimeError) { release.run }.message
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+
37
123
  def test_rejects_a_package_from_different_source
38
124
  with_package do |release, _artifact|
39
125
  File.write("payload.rb", "puts :changed\n")
@@ -128,6 +214,17 @@ class ReleaseTest < Minitest::Test
128
214
 
129
215
  private
130
216
 
217
+ # 发布测试显式控制凭据环境,避免本机登录状态或 CI 标记影响用例结果。
218
+ def with_environment(values)
219
+ previous = values.to_h { |name, _value| [name, ENV.fetch(name, nil)] }
220
+ begin
221
+ values.each { |name, value| ENV[name] = value }
222
+ yield
223
+ ensure
224
+ previous.each { |name, value| ENV[name] = value }
225
+ end
226
+ end
227
+
131
228
  def with_package
132
229
  Dir.mktmpdir("expect-release-test-") do |directory|
133
230
  Dir.chdir(directory) do
@@ -10,6 +10,7 @@ require_relative "../../lib/expect/pty"
10
10
  module ScriptProbe
11
11
  PROMPT = "EXPECT_SCRIPT_PROMPT> "
12
12
  FIXTURES = File.expand_path("../fixtures/ssh_scripts", __dir__)
13
+
13
14
  class Failure < StandardError; end
14
15
 
15
16
  def self.check(condition, message)
metadata CHANGED
@@ -1,14 +1,102 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: expect-pty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - expect-pty contributors
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-09-13 00:00:00.000000000 Z
11
- dependencies: []
10
+ date: 2026-09-26 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: forwardable
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.3'
26
+ - !ruby/object:Gem::Dependency
27
+ name: io-console
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0.6'
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '1.0'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0.6'
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '1.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: io-wait
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0.3'
53
+ - - "<"
54
+ - !ruby/object:Gem::Version
55
+ version: '1.0'
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0.3'
63
+ - - "<"
64
+ - !ruby/object:Gem::Version
65
+ version: '1.0'
66
+ - !ruby/object:Gem::Dependency
67
+ name: shellwords
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0.1'
73
+ - - "<"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0.1'
83
+ - - "<"
84
+ - !ruby/object:Gem::Version
85
+ version: '1.0'
86
+ - !ruby/object:Gem::Dependency
87
+ name: stringio
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - "~>"
91
+ - !ruby/object:Gem::Version
92
+ version: '3.0'
93
+ type: :runtime
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '3.0'
12
100
  description: Automate interactive programs with exact and regexp matching, Ruby blocks,
13
101
  multi-session waits, logging and terminal interaction.
14
102
  executables: []
@@ -34,13 +122,15 @@ files:
34
122
  - expect-pty.gemspec
35
123
  - lib/expect.rb
36
124
  - lib/expect/configuration.rb
37
- - lib/expect/engine.rb
38
- - lib/expect/interconnect.rb
125
+ - lib/expect/interaction.rb
126
+ - lib/expect/matcher.rb
39
127
  - lib/expect/pattern.rb
40
128
  - lib/expect/pattern_list.rb
41
129
  - lib/expect/pty.rb
42
- - lib/expect/resources.rb
130
+ - lib/expect/relay.rb
131
+ - lib/expect/relay_writer.rb
43
132
  - lib/expect/result.rb
133
+ - lib/expect/session_resources.rb
44
134
  - lib/expect/version.rb
45
135
  - script/ci
46
136
  - script/release.rb
@@ -61,6 +151,7 @@ files:
61
151
  - test/matching_test.rb
62
152
  - test/multi_session_test.rb
63
153
  - test/process_test.rb
154
+ - test/relay_recovery_test.rb
64
155
  - test/release_test.rb
65
156
  - test/ruby_api_test.rb
66
157
  - test/script_logging_test.rb
@@ -91,7 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
182
  - !ruby/object:Gem::Version
92
183
  version: '0'
93
184
  requirements: []
94
- rubygems_version: 4.0.16
185
+ rubygems_version: 4.0.12
95
186
  specification_version: 4
96
187
  summary: Ruby PTY automation with the Expect.pm interaction model
97
188
  test_files: []