expect-pty 0.5.3 → 0.6.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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +28 -19
  4. data/docs/API.md +88 -0
  5. data/docs/MIGRATION.md +15 -0
  6. data/lib/expect/cleanup.rb +33 -0
  7. data/lib/expect/configuration.rb +87 -66
  8. data/lib/expect/interaction.rb +226 -209
  9. data/lib/expect/logging.rb +157 -156
  10. data/lib/expect/matcher.rb +30 -29
  11. data/lib/expect/pattern.rb +8 -2
  12. data/lib/expect/pattern_list.rb +35 -5
  13. data/lib/expect/redactor.rb +11 -4
  14. data/lib/expect/relay.rb +97 -73
  15. data/lib/expect/relay_writer.rb +2 -1
  16. data/lib/expect/result.rb +32 -8
  17. data/lib/expect/session.rb +508 -0
  18. data/lib/expect/session_resources.rb +10 -2
  19. data/lib/expect/terminal.rb +78 -78
  20. data/lib/expect/version.rb +1 -1
  21. data/lib/expect.rb +333 -520
  22. data/sig/expect.rbs +355 -0
  23. metadata +13 -106
  24. data/.rubocop.yml +0 -66
  25. data/CONTRIBUTING.md +0 -29
  26. data/Gemfile +0 -28
  27. data/Rakefile +0 -38
  28. data/benchmark/matching.rb +0 -98
  29. data/benchmark/redactor.rb +0 -64
  30. data/benchmark/relay.rb +0 -48
  31. data/benchmark/scaling.rb +0 -70
  32. data/benchmark/send_slow.rb +0 -41
  33. data/benchmark/support.rb +0 -114
  34. data/docs/COMPATIBILITY.md +0 -98
  35. data/docs/INTERNAL_CONTRACTS.md +0 -154
  36. data/docs/PERFORMANCE.md +0 -165
  37. data/docs/RELEASING.md +0 -84
  38. data/docs/VERIFICATION.md +0 -456
  39. data/examples/dialogue.rb +0 -30
  40. data/examples/kibitz/README.md +0 -81
  41. data/examples/kibitz/kibitz.rb +0 -142
  42. data/examples/kibitz/test_kibitz.rb +0 -37
  43. data/examples/ssh_auto.rb +0 -94
  44. data/examples/ssh_interact.rb +0 -159
  45. data/examples/ssh_login.rb +0 -64
  46. data/expect-pty.gemspec +0 -33
  47. data/script/ci +0 -121
  48. data/script/release.rb +0 -319
  49. data/test/buffer_accounting_test.rb +0 -83
  50. data/test/cleanup_test.rb +0 -251
  51. data/test/compare_upstream.rb +0 -157
  52. data/test/configuration_test.rb +0 -133
  53. data/test/deadline_test.rb +0 -237
  54. data/test/diagnostics_test.rb +0 -353
  55. data/test/edge_case_test.rb +0 -262
  56. data/test/fixtures/ssh_scripts/01_identity.sh +0 -4
  57. data/test/fixtures/ssh_scripts/02_output.sh +0 -5
  58. data/test/fixtures/ssh_scripts/03_delayed.sh +0 -6
  59. data/test/fixtures/ssh_scripts/04_failure.sh +0 -2
  60. data/test/fixtures/ssh_scripts/05_recovery.sh +0 -3
  61. data/test/initialization_failure_test.rb +0 -92
  62. data/test/integration/README.md +0 -109
  63. data/test/integration/ssh_scripts.rb +0 -94
  64. data/test/interact_test.rb +0 -253
  65. data/test/interconnect_test.rb +0 -425
  66. data/test/io_test.rb +0 -321
  67. data/test/kibitz_test.rb +0 -45
  68. data/test/lifecycle_contract_test.rb +0 -71
  69. data/test/literal_scan_test.rb +0 -97
  70. data/test/matching_test.rb +0 -211
  71. data/test/multi_session_test.rb +0 -66
  72. data/test/ownership_sequence_test.rb +0 -208
  73. data/test/pattern_offset_test.rb +0 -43
  74. data/test/process_interruption_test.rb +0 -296
  75. data/test/process_test.rb +0 -291
  76. data/test/redactor_test.rb +0 -183
  77. data/test/relay_recovery_test.rb +0 -451
  78. data/test/relay_reentrancy_test.rb +0 -159
  79. data/test/release_test.rb +0 -375
  80. data/test/ruby_api_test.rb +0 -515
  81. data/test/scan_reuse_test.rb +0 -109
  82. data/test/script_logging_test.rb +0 -124
  83. data/test/support/interact_probe.rb +0 -125
  84. data/test/support/kibitz_probe.rb +0 -177
  85. data/test/support/script_probe.rb +0 -158
  86. data/test/terminal_cleanup_test.rb +0 -349
  87. data/test/test_helper.rb +0 -58
  88. data/test/timeout_test.rb +0 -289
  89. data/test/write_contract_test.rb +0 -105
@@ -1,296 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "test_helper"
4
- require "minitest/mock"
5
-
6
- class ProcessInterruptionTest < ExpectTest
7
- def test_hard_close_reaps_a_real_child_after_one_wait_interruption
8
- session = child('Signal.trap("HUP", "IGNORE"); Signal.trap("TERM", "IGNORE"); puts "ready"; sleep 60',
9
- raw_pty: true)
10
- assert_equal 1, session.expect("ready", timeout: 2)
11
- pid = session.pid
12
- original = Process.method(:waitpid2)
13
- interrupted = false
14
- Process.stub(:waitpid2, lambda { |*args|
15
- if args.first == pid && !interrupted
16
- interrupted = true
17
- raise Errno::EINTR
18
- end
19
- original.call(*args)
20
- }) do
21
- status = bounded { session.hard_close(timeout: 0) }
22
- assert_instance_of Process::Status, status
23
- assert_equal Signal.list.fetch("KILL"), status.termsig
24
- assert_same status, session.process_status
25
- end
26
- assert interrupted
27
- assert_nil session.pid
28
- assert_raises(Errno::ECHILD) { Process.waitpid(pid, Process::WNOHANG) }
29
- ensure
30
- session&.hard_close(timeout: 0)
31
- end
32
-
33
- def test_continuous_wait_interruptions_keep_the_original_deadline_and_pid
34
- with_fake_child do |session, resources, calls|
35
- with_clock(session) do |now|
36
- Process.stub(:waitpid2, lambda { |*|
37
- calls << :wait
38
- raise Errno::EINTR
39
- }) do
40
- assert_nil(bounded { session.wait(timeout: 0.03) })
41
- assert_equal 0.03, now.call
42
- assert_operator calls.size, :<=, 5
43
- assert_operator calls.size, :>=, 3
44
- assert_equal 123_456, resources.pid
45
- assert_nil resources.status
46
- end
47
- end
48
- end
49
- end
50
-
51
- def test_continuous_wait_and_signal_interruptions_are_bounded_during_hard_close
52
- with_fake_child do |session, resources, calls|
53
- with_clock(session) do |now|
54
- Process.stub(:waitpid2, lambda { |*|
55
- calls << :wait
56
- raise Errno::EINTR
57
- }) do
58
- Process.stub(:kill, lambda { |signal, *|
59
- calls << signal
60
- raise Errno::EINTR
61
- }) do
62
- assert_nil(bounded { session.hard_close(timeout: 0.02) })
63
- assert_in_delta 1.04, now.call, 0.000001
64
- assert_operator calls.count("TERM"), :>, 1
65
- assert_operator calls.count("KILL"), :>, 1
66
- assert_operator calls.size, :<, 230
67
- assert_equal 123_456, resources.pid
68
- assert_nil resources.status
69
- end
70
- end
71
- end
72
- end
73
- end
74
-
75
- def test_signal_interruption_retries_without_extending_the_soft_close_budget
76
- with_fake_child do |session, resources, calls|
77
- with_clock(session) do |now|
78
- Process.stub(:kill, lambda { |signal, _pid|
79
- calls << signal
80
- raise Errno::EINTR if calls.count(signal) == 1
81
- }) do
82
- assert_nil session.soft_close(timeout: 0, term_timeout: 0.03)
83
- assert_equal 0.03, now.call
84
- assert_equal %w[TERM TERM], calls.grep(String)
85
- assert_equal 123_456, resources.pid
86
- assert_nil resources.status
87
- end
88
- end
89
- end
90
- end
91
-
92
- def test_echild_clears_ownership_before_any_signal_or_detach
93
- with_fake_child do |session, resources, calls|
94
- Process.stub(:waitpid2, ->(*) { raise Errno::ECHILD }) do
95
- assert_nil session.hard_close(timeout: 0)
96
- resources.finalize
97
- assert_nil resources.pid
98
- assert_nil resources.status
99
- assert_empty calls
100
- end
101
- end
102
- end
103
-
104
- def test_finalizer_continues_to_kill_and_detach_after_interrupted_reap
105
- with_fake_child do |_session, resources, calls|
106
- Process.stub(:waitpid2, lambda { |*|
107
- calls << :wait
108
- raise Errno::EINTR
109
- }) do
110
- bounded { resources.finalize }
111
- end
112
- assert_equal [:wait, "KILL", :detach], calls
113
- assert_nil resources.pid
114
- assert_nil resources.status
115
- end
116
- end
117
-
118
- def test_finalizer_detaches_even_if_kill_is_continuously_interrupted
119
- with_fake_child do |session, resources, calls|
120
- session.log_to { flunk "finalizer invoked a user callback" }
121
- Process.stub(:kill, lambda { |signal, *|
122
- calls << signal
123
- raise Errno::EINTR
124
- }) do
125
- bounded { resources.finalize }
126
- end
127
- assert_equal :detach, calls.last
128
- assert_operator calls.count("KILL"), :>=, 1
129
- assert_operator calls.count("KILL"), :<=, 2
130
- assert_nil resources.pid
131
- assert_nil resources.status
132
- end
133
- end
134
-
135
- def test_finalizer_retains_unknown_pid_if_detach_also_fails
136
- with_fake_child do |_session, resources, _calls|
137
- Process.stub(:detach, ->(*) { raise Errno::EINTR }) { bounded { resources.finalize } }
138
- assert_equal 123_456, resources.pid
139
- assert_nil resources.status
140
- end
141
- end
142
-
143
- def test_non_owner_never_waits_signals_or_detaches
144
- with_fake_child do |session, resources, calls|
145
- Process.stub(:pid, resources.owner + 1) do
146
- resources.finalize
147
- assert_nil session.wait(timeout: 0)
148
- assert_nil session.hard_close(timeout: 0)
149
- end
150
- assert_empty calls
151
- assert_equal 123_456, resources.pid
152
- end
153
- end
154
-
155
- def test_forked_copy_cannot_reap_signal_or_detach_the_parent_child
156
- session = child('puts "ready"; sleep 60', raw_pty: true)
157
- assert_equal 1, session.expect("ready", timeout: 2)
158
- reader, writer = IO.pipe
159
- @ios.push(reader, writer)
160
- probe = fork do
161
- reader.close
162
- forbidden = ->(*) { raise "non-owner attempted process cleanup" }
163
- Process.stub(:waitpid2, forbidden) do
164
- Process.stub(:kill, forbidden) do
165
- Process.stub(:detach, forbidden) do
166
- session.instance_variable_get(:@resources).finalize
167
- session.hard_close(timeout: 0)
168
- writer.write("ok")
169
- end
170
- end
171
- end
172
- exit! 0
173
- rescue Exception # rubocop:disable Lint/RescueException -- 子进程只回传验证失败,父进程负责回收。
174
- exit! 1
175
- end
176
- writer.close
177
- assert_equal("ok", bounded { reader.read })
178
- status = bounded { Process.waitpid2(probe).last }
179
- probe = nil
180
- assert status.success?
181
- assert session.alive?
182
- ensure
183
- if probe
184
- begin
185
- Process.kill("KILL", probe)
186
- rescue Errno::ESRCH
187
- nil
188
- end
189
- begin
190
- Process.waitpid(probe)
191
- rescue Errno::ECHILD
192
- nil
193
- end
194
- end
195
- session&.hard_close(timeout: 0)
196
- end
197
-
198
- def test_finalizer_rechecks_ownership_before_detaching_after_a_signal
199
- with_fake_child do |_session, resources, calls|
200
- owner = resources.owner
201
- Process.stub(:pid, -> { owner }) do
202
- Process.stub(:kill, lambda { |signal, *|
203
- calls << signal
204
- owner += 1
205
- }) do
206
- resources.finalize
207
- end
208
- end
209
- assert_equal [:wait, "KILL"], calls
210
- assert_equal 123_456, resources.pid
211
- assert_nil resources.status
212
- end
213
- end
214
-
215
- def test_esrch_during_signal_still_reaps_without_inventing_status
216
- with_fake_child do |session, resources, calls|
217
- gone = false
218
- Process.stub(:waitpid2, lambda { |*|
219
- calls << :wait
220
- raise Errno::ECHILD if gone
221
- }) do
222
- Process.stub(:kill, lambda { |signal, *|
223
- calls << signal
224
- gone = true
225
- raise Errno::ESRCH
226
- }) do
227
- assert_nil session.hard_close(timeout: 0)
228
- end
229
- end
230
- assert_equal [:wait, :wait, "TERM", :wait], calls
231
- assert_nil resources.pid
232
- assert_nil resources.status
233
- end
234
- end
235
-
236
- def test_zero_budget_close_returns_status_reaped_after_esrch
237
- status = child("exit 7").wait(timeout: 2)
238
- assert_instance_of Process::Status, status
239
- %i[soft_close hard_close].each do |method|
240
- with_fake_child do |session, resources, calls|
241
- gone = false
242
- Process.stub(:waitpid2, lambda { |pid, *|
243
- calls << :wait
244
- [pid, status] if gone
245
- }) do
246
- Process.stub(:kill, lambda { |signal, *|
247
- calls << signal
248
- gone = true
249
- raise Errno::ESRCH
250
- }) do
251
- options = { timeout: 0 }
252
- options[:term_timeout] = 0 if method == :soft_close
253
- assert_same status, session.public_send(method, **options)
254
- end
255
- end
256
- assert_nil resources.pid
257
- assert_same status, session.process_status
258
- assert_equal [:wait, :wait, "TERM", :wait], calls
259
- end
260
- end
261
- end
262
-
263
- private
264
-
265
- # 假 PID 只存在于系统调用全部被替换的作用域;ensure 先撤销 PID,再交给通用 teardown。
266
- def with_fake_child
267
- session, = pipe_session
268
- resources = session.instance_variable_get(:@resources)
269
- calls = []
270
- originals = %i[waitpid2 kill detach].to_h { |name| [name, Process.method(name)] }
271
- Process.define_singleton_method(:waitpid2) do |*|
272
- calls << :wait
273
- nil
274
- end
275
- Process.define_singleton_method(:kill) do |signal, *|
276
- calls << signal
277
- 1
278
- end
279
- Process.define_singleton_method(:detach) do |*|
280
- calls << :detach
281
- nil
282
- end
283
- resources.pid = 123_456
284
- yield session, resources, calls
285
- ensure
286
- resources.pid = nil if resources
287
- originals&.each { |name, method| Process.define_singleton_method(name, method) }
288
- end
289
-
290
- def with_clock(session)
291
- now = 0.0
292
- Expect.stub(:monotonic, -> { now }) do
293
- session.stub(:sleep, ->(seconds) { now += seconds }) { yield -> { now } }
294
- end
295
- end
296
- end
data/test/process_test.rb DELETED
@@ -1,291 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "test_helper"
4
-
5
- class ProcessTest < ExpectTest
6
- def test_spawn_has_controlling_terminal_and_merged_stderr
7
- session = child(<<~RUBY)
8
- terminal = File.open("/dev/tty", &:tty?)
9
- puts [STDIN.tty?, STDOUT.tty?, STDERR.tty?, terminal].join(":")
10
- STDERR.puts "error-stream"
11
- RUBY
12
- assert_equal 1, session.expect("true:true:true:true", timeout: 2)
13
- assert_equal 1, session.expect("error-stream", timeout: 2)
14
- session.soft_close(timeout: 1)
15
- assert_equal 0, session.exit_code
16
- assert_equal 0, session.process_status.to_i
17
- assert session.closed?
18
- refute session.alive?
19
- end
20
-
21
- def test_raw_mode_is_applied_before_child_starts
22
- session = child(<<~'RUBY', raw_pty: true)
23
- print "name: "
24
- value = STDIN.gets
25
- puts "reply=#{value.strip.reverse}"
26
- RUBY
27
- session.expect("name: ", timeout: 2)
28
- session.write("crate\n")
29
- assert_equal 1, session.expect("reply=etarc\n", timeout: 2)
30
- assert_equal "", session.before
31
- end
32
-
33
- def test_new_pty_supports_slave_configuration_before_spawn
34
- session = Expect.new(log_stdout: false)
35
- @sessions << session
36
- assert session.slave.tty?
37
- session.slave.echo = false
38
- session.slave.winsize = [37, 111]
39
- # 子进程需要 io-console gem,保留 RubyGems 以使用 Bundler 选定的版本。
40
- session.spawn(RbConfig.ruby, "-rio/console", "-e",
41
- 'STDOUT.sync = true; puts STDIN.winsize.join(":"); puts STDIN.gets')
42
- assert_equal 1, session.expect("37:111", timeout: 2), session.before.inspect
43
- assert_equal [37, 111], session.winsize
44
- session.winsize = [24, 80]
45
- assert_equal [24, 80], session.winsize
46
- session.clear_buffer
47
- session.write("line\n")
48
- session.expect("line", timeout: 2)
49
- session.expect(timeout: 2)
50
- assert_equal "\r\n", session.before
51
- end
52
-
53
- def test_command_arguments_environment_and_directory
54
- Dir.mktmpdir do |dir|
55
- session = Expect.spawn(RbConfig.ruby, "--disable-gems", "-e",
56
- 'puts ARGV.first; puts ENV.fetch("EXPECT_TEST_VALUE"); puts Dir.pwd', "a;$(exit) b",
57
- env: { "EXPECT_TEST_VALUE" => "environment" }, chdir: dir,
58
- log_stdout: false, raw_pty: true)
59
- @sessions << session
60
- assert_equal 1, session.expect("a;$(exit) b\nenvironment\n#{File.realpath(dir)}\n", timeout: 2)
61
- end
62
- end
63
-
64
- def test_string_command_supports_shell_semantics
65
- session = Expect.new("printf 'shell-one'; printf 'shell-two'", log_stdout: false)
66
- @sessions << session
67
- assert_equal 1, session.expect("shell-oneshell-two", timeout: 2)
68
- end
69
-
70
- def test_exec_failure_is_synchronous_and_reaped
71
- session = Expect.new(log_stdout: false)
72
- @sessions << session
73
- error = assert_raises(Expect::SpawnError) { session.spawn("/no/such/expect-test-command") }
74
- assert_match(/ENOENT/, error.message)
75
- assert session.closed?
76
- assert_nil session.pid
77
- assert_raises(Expect::SpawnError) { session.spawn("cat") }
78
- end
79
-
80
- def test_spawn_cleans_child_when_post_exec_diagnostics_fail
81
- assert_failed_construction_cleans_child(:spawn)
82
- end
83
-
84
- def test_new_cleans_child_when_post_exec_diagnostics_fail
85
- assert_failed_construction_cleans_child(:new)
86
- end
87
-
88
- def test_block_closes_child_on_exception
89
- pid = nil
90
- assert_raises(RuntimeError) do
91
- Expect.spawn(RbConfig.ruby, "--disable-gems", "-e", "sleep 60", log_stdout: false) do |session|
92
- pid = session.pid
93
- raise "stop"
94
- end
95
- end
96
- assert_raises(Errno::ECHILD) { Process.waitpid(pid, Process::WNOHANG) }
97
- assert_raises(Errno::ESRCH) { Process.kill(0, pid) }
98
- end
99
-
100
- def test_soft_close_drains_output_before_exit_and_returns_process_status
101
- session = child('sleep 0.03; print "last output"; exit 7', raw_pty: true)
102
- log = StringIO.new
103
- session.log_to(log)
104
- assert_equal 7, session.soft_close(timeout: 1).exitstatus
105
- assert_equal "last output", log.string
106
- assert_equal "last output", session.buffer
107
- assert_equal 7, session.exit_code
108
- refute log.closed?
109
- end
110
-
111
- def test_hard_close_escalates_uncooperative_child_and_reaps
112
- session = child('Signal.trap("HUP", "IGNORE"); Signal.trap("TERM", "IGNORE"); puts "ready"; loop { sleep 1 }')
113
- session.expect("ready", timeout: 2)
114
- pid = session.pid
115
- bounded(2) { session.hard_close(timeout: 0.03) }
116
- assert_equal Signal.list.fetch("KILL"), session.process_status.termsig
117
- assert_raises(Errno::ECHILD) { Process.waitpid(pid, Process::WNOHANG) }
118
- assert_equal session.process_status, session.hard_close
119
- end
120
-
121
- def test_soft_close_sends_term_and_returns_the_exit_status
122
- session = child(<<~RUBY, raw_pty: true)
123
- Signal.trap("HUP", "IGNORE")
124
- Signal.trap("TERM") { exit 7 }
125
- puts "ready"
126
- sleep 60
127
- RUBY
128
- session.expect("ready", timeout: 2)
129
- status = bounded { session.soft_close(timeout: 0, term_timeout: 1) }
130
- assert_equal 7, status.exitstatus
131
- assert session.closed?
132
- refute session.alive?
133
- assert_same status, session.soft_close(timeout: 0)
134
- end
135
-
136
- def test_soft_close_never_sends_kill_and_retains_a_live_pid_for_hard_close
137
- session = child(<<~RUBY)
138
- Signal.trap("HUP", "IGNORE")
139
- Signal.trap("TERM", "IGNORE")
140
- puts "ready"
141
- sleep 60
142
- RUBY
143
- session.expect("ready", timeout: 2)
144
- pid = session.pid
145
- assert_nil(bounded { session.soft_close(timeout: 0, term_timeout: 0.03) })
146
- assert session.closed?
147
- assert session.alive?
148
- assert_equal pid, session.pid
149
- assert_nil session.wait(timeout: 0)
150
- status = bounded { session.hard_close(timeout: 0.03) }
151
- assert_equal Signal.list.fetch("KILL"), status.termsig
152
- assert_nil session.pid
153
- assert_raises(Errno::ECHILD) { Process.waitpid(pid, Process::WNOHANG) }
154
- end
155
-
156
- def test_block_cleanup_uses_the_configured_graceful_close_and_drains_output
157
- log = StringIO.new
158
- connection = nil
159
- value = Expect.spawn(RbConfig.ruby, "--disable-gems", "-e", 'sleep 0.03; puts "tail"',
160
- raw_pty: true, graceful_close: true) do |session|
161
- connection = session
162
- session.log_output = log
163
- :finished
164
- end
165
- assert_equal :finished, value
166
- assert_equal "tail\n", log.string
167
- assert connection.closed?
168
- assert_equal 0, connection.exit_code
169
- refute log.closed?
170
- end
171
-
172
- def test_explicit_graceful_close_returns_nil_like_io_close
173
- session = child('sleep 0.03; print "tail"', raw_pty: true)
174
- assert_nil session.close(graceful: true)
175
- assert_equal "tail", session.buffer
176
- assert_equal 0, session.exit_code
177
- assert_nil session.close
178
- end
179
-
180
- def test_close_still_reaps_the_child_when_graceful_logging_fails
181
- session = child('puts "ready"; sleep 60', raw_pty: true)
182
- pid = session.pid
183
- session.log_to { raise IOError, "log failed" }
184
- assert_raises(IOError) { bounded { session.close(graceful: true) } }
185
- assert session.closed?
186
- refute session.alive?
187
- assert_raises(Errno::ECHILD) { Process.waitpid(pid, Process::WNOHANG) }
188
- end
189
-
190
- def test_invalid_close_timeout_does_not_close_or_signal_the_child
191
- session = child('puts "ready"; sleep 60')
192
- session.expect("ready", timeout: 2)
193
- assert_raises(ArgumentError) { session.soft_close(timeout: 0, term_timeout: nil) }
194
- assert_raises(ArgumentError) { session.soft_close(timeout: 0, term_timeout: -1) }
195
- assert_raises(ArgumentError) { session.hard_close(timeout: nil) }
196
- refute session.closed?
197
- assert session.alive?
198
- end
199
-
200
- def test_eof_does_not_prevent_buffered_match
201
- session = child('print "final"; exit 4', raw_pty: true)
202
- assert_equal 1, session.expect("final", timeout: 2)
203
- assert session.expect_result(:eof, timeout: 2).eof?
204
- assert_equal 4, session.wait(timeout: 1).exitstatus
205
- end
206
-
207
- def test_wait_timeout_and_explicit_close
208
- session = child("sleep 0.15; exit 0")
209
- assert_nil session.wait(timeout: 0)
210
- assert session.alive?
211
- assert_equal 0, session.wait(timeout: 1).exitstatus
212
- refute session.alive?
213
- session.close
214
- assert_raises(IOError) { session.write("x") }
215
- end
216
-
217
- def test_borrowed_and_owned_io_lifetimes
218
- session, writer = pipe_session
219
- reader = session.to_io
220
- session.close
221
- refute reader.closed?
222
- writer.write("x")
223
- assert_equal "x", reader.read(1)
224
- owned = Expect.open(reader, own: true)
225
- owned.close
226
- assert reader.closed?
227
- end
228
-
229
- def test_stty_roundtrip
230
- session = child("sleep 30")
231
- initial = session.stty
232
- configuration = terminal_configuration(session)
233
- session.stty("raw -echo")
234
- refute session.to_io.echo?
235
- session.stty(initial)
236
- assert_equal configuration, terminal_configuration(session)
237
- end
238
-
239
- def test_stty_reports_a_missing_system_command_and_preserves_the_cause
240
- previous_path = ENV.fetch("PATH", nil)
241
- session = child("sleep 60")
242
- ENV["PATH"] = ""
243
- error = assert_raises(IOError) { session.stty }
244
- assert_match "stty executable not found", error.message
245
- assert_instance_of Errno::ENOENT, error.cause
246
- ensure
247
- ENV["PATH"] = previous_path
248
- end
249
-
250
- def test_configuration_and_session_attributes
251
- Expect.configure(timeout: 0.01, log_stdout: false)
252
- session = Expect.new
253
- @sessions << session
254
- refute session.log_stdout?
255
- assert_equal 0.01, session.timeout
256
- session.log_stdout = true
257
- assert session.log_stdout
258
- session.log_stdout = false
259
- refute session.log_stdout
260
- session.buffer_limit = 30
261
- assert_equal 30, session.buffer_limit
262
- assert_match(/\A#<Expect .*closed=false>\z/, session.inspect)
263
- end
264
-
265
- private
266
-
267
- def assert_failed_construction_cleans_child(factory)
268
- connection = nil
269
- session_class = Class.new(Expect)
270
- session_class.define_method(:initialize) do |*command, **options|
271
- connection = self
272
- super(*command, **options)
273
- end
274
- reader, diagnostic_output = IO.pipe
275
- @ios.push(reader, diagnostic_output)
276
- reader.close
277
- previous_stderr = $stderr
278
- begin
279
- $stderr = diagnostic_output
280
- assert_raises(Errno::EPIPE) do
281
- session_class.public_send(factory, RbConfig.ruby, "--disable-gems", "-e", "sleep 60", debug_level: 1)
282
- end
283
- ensure
284
- $stderr = previous_stderr
285
- @sessions << connection if connection
286
- end
287
- assert connection.closed?
288
- assert_nil connection.pid
289
- assert_instance_of Process::Status, connection.process_status
290
- end
291
- end