expect-pty 0.5.2 → 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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/README.md +37 -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 -207
  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 +122 -68
  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 +49 -16
  19. data/lib/expect/terminal.rb +78 -78
  20. data/lib/expect/version.rb +1 -1
  21. data/lib/expect.rb +336 -486
  22. data/sig/expect.rbs +355 -0
  23. metadata +12 -100
  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 -133
  36. data/docs/PERFORMANCE.md +0 -165
  37. data/docs/RELEASING.md +0 -84
  38. data/docs/VERIFICATION.md +0 -358
  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 -311
  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/integration/README.md +0 -109
  62. data/test/integration/ssh_scripts.rb +0 -94
  63. data/test/interact_test.rb +0 -253
  64. data/test/interconnect_test.rb +0 -425
  65. data/test/io_test.rb +0 -321
  66. data/test/kibitz_test.rb +0 -45
  67. data/test/lifecycle_contract_test.rb +0 -71
  68. data/test/literal_scan_test.rb +0 -97
  69. data/test/matching_test.rb +0 -211
  70. data/test/multi_session_test.rb +0 -66
  71. data/test/pattern_offset_test.rb +0 -43
  72. data/test/process_test.rb +0 -291
  73. data/test/redactor_test.rb +0 -183
  74. data/test/relay_recovery_test.rb +0 -451
  75. data/test/release_test.rb +0 -328
  76. data/test/ruby_api_test.rb +0 -515
  77. data/test/scan_reuse_test.rb +0 -109
  78. data/test/script_logging_test.rb +0 -124
  79. data/test/support/interact_probe.rb +0 -125
  80. data/test/support/kibitz_probe.rb +0 -177
  81. data/test/support/script_probe.rb +0 -158
  82. data/test/terminal_cleanup_test.rb +0 -349
  83. data/test/test_helper.rb +0 -58
  84. data/test/timeout_test.rb +0 -289
@@ -1,211 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "test_helper"
4
-
5
- class MatchingTest < ExpectTest
6
- def test_literal_regexp_number_and_result_tuple
7
- session, = pipe_session
8
- session.buffer = "before a.c between abc:42 after"
9
- assert_equal 2, session.expect("absent", "a.c", timeout: 0)
10
- assert_equal "before ", session.before
11
- assert_equal "a.c", session.match
12
- result = session.expect_result(/abc:(\d+)/, timeout: 0)
13
- assert_equal [1, nil, "abc:42", " between ", " after", session, ["42"]], result.to_a
14
- assert_equal ["42"], session.captures
15
- assert result.matched?
16
- refute result.timeout?
17
- refute result.eof?
18
- end
19
-
20
- def test_regexp_callback_captures_lexical_arguments
21
- session, = pipe_session
22
- session.buffer = "prefix xyz42 suffix"
23
- observed = []
24
- argument = :value
25
- number = session.expect(timeout: 0) do
26
- on(/xyz(\d+)/) { |object| observed << [object.captures, argument] }
27
- end
28
- assert_equal 1, number
29
- assert_equal [[["42"], :value]], observed
30
- end
31
-
32
- def test_native_regexp_and_literal_event_names
33
- session, = pipe_session
34
- session.buffer = "timeout eof -word a.c end99"
35
- assert_equal 1, session.expect("timeout", timeout: 0)
36
- assert_equal 1, session.expect("eof", timeout: 0)
37
- assert_equal 1, session.expect("-word", timeout: 0)
38
- assert_equal 1, session.expect("a.c", timeout: 0)
39
- assert_equal 1, session.expect(/end\d+/, timeout: 0)
40
- end
41
-
42
- def test_patterns_have_priority_over_their_position_in_buffer
43
- session, = pipe_session
44
- session.buffer = "second first"
45
- assert_equal 1, session.expect("first", "second", timeout: 0)
46
- assert_equal "second ", session.before
47
- end
48
-
49
- def test_regexp_uses_actual_offset_with_lookbehind
50
- session, = pipe_session
51
- session.buffer = "same prefix same end"
52
- session.expect(/(?<=prefix )same/, timeout: 0)
53
- assert_equal "same prefix ", session.before
54
- assert_equal " end", session.after
55
- end
56
-
57
- def test_unmatched_capture_and_zero_width_pattern
58
- session, = pipe_session
59
- session.buffer = "b"
60
- assert_equal 1, session.expect(/(a)?(?=b)/, timeout: 0)
61
- assert_equal [nil], session.captures
62
- assert_equal "", session.match
63
- assert_equal "b", session.buffer
64
- end
65
-
66
- def test_capture_list_clears_for_literal_and_timeout
67
- session, = pipe_session
68
- session.buffer = "a1 b"
69
- session.expect(/a(\d)/, timeout: 0)
70
- session.expect("b", timeout: 0)
71
- assert_empty session.captures
72
- assert_nil session.expect(/absent/, timeout: 0)
73
- assert_nil session.match
74
- assert_nil session.after
75
- assert_empty session.captures
76
- end
77
-
78
- def test_buffer_retained_on_timeout_and_manually_replaceable
79
- session, writer = pipe_session
80
- writer.write("some string")
81
- assert_nil session.expect("other", timeout: 0.03)
82
- assert_equal :timeout, session.error
83
- assert_equal "some string", session.before
84
- assert_equal 1, session.expect("some", timeout: 0)
85
- assert_equal " string", session.buffer
86
- session.buffer = "new"
87
- assert_equal "new", session.clear_buffer
88
- assert_empty session.buffer
89
- end
90
-
91
- def test_preserve_buffer_and_buffer_limit
92
- session, = pipe_session
93
- session.preserve_buffer = true
94
- session.buffer = "prefix token tail"
95
- 2.times { assert_equal 1, session.expect("token", timeout: 0) }
96
- assert_equal "prefix token tail", session.buffer
97
- session.buffer_limit = 4
98
- assert_equal "tail", session.buffer
99
- session.buffer_limit = nil
100
- session.buffer = "123456"
101
- assert_equal "123456", session.buffer
102
- end
103
-
104
- def test_split_patterns_across_reads
105
- session, writer = pipe_session
106
- background do
107
- writer.write("pass")
108
- sleep 0.03
109
- writer.write("word: ")
110
- end
111
- assert_equal 1, session.expect(/password: /, timeout: 1)
112
- end
113
-
114
- def test_utf8_regexp_split_character_and_byte_offsets
115
- session, writer = pipe_session
116
- bytes = "前缀密码:结束".b
117
- background do
118
- bytes.each_byte do |byte|
119
- writer.write(byte.chr)
120
- sleep 0.001
121
- end
122
- end
123
- assert_equal 1, session.expect(/(密码):/, timeout: 1)
124
- assert_equal "前缀".b, session.before
125
- assert_equal ["密码".b], session.captures
126
- assert_equal 1, session.expect("结束", timeout: 1)
127
- end
128
-
129
- def test_utf8_regexp_waits_for_incomplete_trailing_character_before_matching
130
- session, writer = pipe_session
131
- session.buffer = "ready\xE4".b
132
-
133
- assert_nil session.expect(/ready\z/u, timeout: 0)
134
- assert_nil session.expect(/ready/u, timeout: 0)
135
- assert_equal "ready\xE4".b, session.buffer
136
-
137
- writer.write("\xB8\xAD".b)
138
- assert_equal 1, session.expect(/ready中\z/u, timeout: 1)
139
- assert_equal "ready中".b, session.match
140
- end
141
-
142
- def test_binary_nul_and_invalid_utf8
143
- session, writer = pipe_session
144
- writer.write("\xff\x00abc\xfe".b)
145
- assert_equal 1, session.expect(/\x00(abc)/n, timeout: 1)
146
- assert_equal "\xff".b, session.before
147
- assert_equal ["abc"], session.captures
148
- end
149
-
150
- def test_utf8_regexp_rejects_impossible_partial_characters
151
- session, = pipe_session
152
- ["\xE0\x80", "\xED\xA0", "\xF0\x80", "\xF4\x90"].each do |bytes|
153
- session.buffer = bytes.b
154
- assert_raises(EncodingError) { session.expect(/ready/u, timeout: 0) }
155
- assert_equal bytes.b, session.buffer
156
- end
157
- end
158
-
159
- def test_utf8_regexp_accepts_valid_partial_characters
160
- session, = pipe_session
161
- ["¢", "中", "😀", "\u{10FFFF}"].each do |character|
162
- 1.upto(character.bytesize - 1) do |length|
163
- session.buffer = character.b.byteslice(0, length)
164
- assert_nil session.expect(/./u, timeout: 0)
165
- assert_equal character.b.byteslice(0, length), session.buffer
166
- end
167
- end
168
- end
169
-
170
- def test_native_regexp_anchors_and_flags
171
- session, = pipe_session
172
- session.buffer = "a\nb\nc"
173
- session.preserve_buffer = true
174
- assert_nil session.expect("^b$", timeout: 0)
175
- assert_nil session.expect(/\Ab\z/, timeout: 0)
176
- assert_equal 1, session.expect(/^b$/, timeout: 0)
177
- assert_equal 1, session.expect(/a.b/m, timeout: 0)
178
- assert_nil session.expect(/a.b/, timeout: 0)
179
- end
180
-
181
- def test_eof_callback_consumes_remaining_buffer
182
- session, writer = pipe_session
183
- writer.write("last bytes")
184
- writer.close
185
- seen = nil
186
- result = session.expect_result(timeout: 1) { eof { |object| seen = object.before } }
187
- assert_nil result.number
188
- assert result.eof?
189
- assert_equal "last bytes", seen
190
- assert_empty session.buffer
191
- end
192
-
193
- def test_read_without_patterns_and_match_before_eof
194
- session, writer = pipe_session
195
- writer.write("END")
196
- writer.close
197
- assert_equal 1, session.expect("END", timeout: nil)
198
- assert session.expect_result(timeout: nil).eof?
199
- end
200
-
201
- def test_argument_validation_does_not_read_io
202
- session, = pipe_session
203
- assert_raises(ArgumentError) { session.expect("x", timeout: -1) }
204
- assert_raises(ArgumentError) { session.expect("x", timeout: Float::INFINITY) }
205
- assert_raises(ArgumentError) { session.expect(["a", 42], timeout: 0) }
206
- assert_raises(ArgumentError) { session.expect("-i", [], timeout: 0) }
207
- assert_raises(ArgumentError) { session.expect(Object.new, timeout: 0) }
208
- assert_raises(ArgumentError) { Expect.expect("x", timeout: 0) }
209
- assert_raises(ArgumentError) { session.buffer_limit = -1 }
210
- end
211
- end
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "test_helper"
4
-
5
- class MultiSessionTest < ExpectTest
6
- def test_multi_session_pattern_groups_and_global_numbering
7
- first, first_writer = pipe_session
8
- second, second_writer = pipe_session
9
- first_writer.write("irrelevant")
10
- second_writer.write("second:42")
11
- result = Expect.expect_result(timeout: 1) do
12
- on("first", from: first)
13
- on(/second:(\d+)/, from: second)
14
- end
15
- assert_equal 2, result.number
16
- assert_same second, result.session
17
- assert_equal ["42"], result.captures
18
- assert_equal "irrelevant", first.buffer
19
- end
20
-
21
- def test_shared_patterns_across_sessions
22
- first, = pipe_session
23
- second, writer = pipe_session
24
- writer.write("ready")
25
- assert_equal 1, Expect.expect("ready", from: [first, second], timeout: 1)
26
- assert_equal "ready", second.match
27
- end
28
-
29
- def test_eof_continue_removes_closed_session_and_waits_for_remaining
30
- first, first_writer = pipe_session
31
- second, second_writer = pipe_session
32
- first_writer.close
33
- ended = []
34
- result = Expect.expect_result(timeout: 1) do
35
- eof(from: first) do |connection|
36
- ended << connection
37
- second_writer.write("done")
38
- Expect.continue(reset_timeout: false)
39
- end
40
- on("done", from: second)
41
- end
42
- assert_equal [first], ended
43
- assert_equal 2, result.number
44
- assert_same second, result.session
45
- end
46
-
47
- def test_all_eof_continuation_returns_without_waiting_forever
48
- first, writer = pipe_session
49
- writer.close
50
- result = bounded { first.expect_result(timeout: nil) { eof { Expect.continue } } }
51
- assert result.eof?
52
- end
53
-
54
- def test_same_session_in_multiple_groups_is_read_once
55
- session, writer = pipe_session
56
- writer.write("right")
57
- other, = pipe_session
58
- result = Expect.expect_result(timeout: 1) do
59
- on("wrong", from: session)
60
- on("missing", from: other)
61
- on("right", from: session)
62
- end
63
- assert_equal 3, result.number
64
- assert_equal "right", result.match
65
- end
66
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "test_helper"
4
-
5
- class PatternOffsetTest < ExpectTest
6
- def test_byte_offsets_captures_and_buffer_consumption
7
- cases = [
8
- ["abENDtail", /END/, 2, 3, []],
9
- ["中文终点尾", /(终)(点)/, 6, 6, ["终".b, "点".b]],
10
- ["😀中😀尾", /(😀)(x)?/, 0, 4, ["😀".b, nil]],
11
- ["😀中END尾", /END/, 7, 3, []],
12
- ["a\0b", /(\0)/n, 1, 1, ["\0".b]],
13
- ["\xFF\x00\xFE".b, /(\xFE)/n, 2, 1, ["\xFE".b]],
14
- ["中文尾", /(?=尾)/, 6, 0, []],
15
- ["😀", /$/u, 4, 0, []]
16
- ]
17
- session, = pipe_session
18
- cases.each do |text, regexp, offset, length, captures|
19
- # 扫描只读调用者输入;省去正则副本后也不能改变编码标记或冻结字符串。
20
- bytes = text.b.freeze
21
- assert_equal [offset, length, captures], Expect::Pattern.new(value: regexp).locate(bytes)
22
- assert_equal Encoding::BINARY, bytes.encoding
23
- session.buffer = bytes
24
- result = session.expect_result(regexp, timeout: 0)
25
- assert_equal bytes.byteslice(0, offset), result.before
26
- assert_equal bytes.byteslice(offset, length), result.match
27
- assert_equal bytes.byteslice((offset + length)..), result.after
28
- assert_equal captures, result.captures
29
- assert_equal result.after, session.buffer
30
- end
31
- end
32
-
33
- def test_incomplete_utf8_defers_matching_and_eof_still_rejects_it
34
- pattern = Expect::Pattern.new(value: /中(?=😀)/)
35
- bytes = "前中😀".b
36
- (1..3).each do |missing|
37
- partial = bytes.byteslice(0, bytes.bytesize - missing)
38
- assert_nil pattern.locate(partial)
39
- assert_raises(EncodingError) { pattern.locate(partial, final: true) }
40
- end
41
- assert_equal [3, 3, []], pattern.locate(bytes)
42
- end
43
- 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