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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +37 -19
- data/docs/API.md +88 -0
- data/docs/MIGRATION.md +15 -0
- data/lib/expect/cleanup.rb +33 -0
- data/lib/expect/configuration.rb +87 -66
- data/lib/expect/interaction.rb +226 -207
- data/lib/expect/logging.rb +157 -156
- data/lib/expect/matcher.rb +30 -29
- data/lib/expect/pattern.rb +8 -2
- data/lib/expect/pattern_list.rb +35 -5
- data/lib/expect/redactor.rb +11 -4
- data/lib/expect/relay.rb +122 -68
- data/lib/expect/relay_writer.rb +2 -1
- data/lib/expect/result.rb +32 -8
- data/lib/expect/session.rb +508 -0
- data/lib/expect/session_resources.rb +49 -16
- data/lib/expect/terminal.rb +78 -78
- data/lib/expect/version.rb +1 -1
- data/lib/expect.rb +336 -486
- data/sig/expect.rbs +355 -0
- metadata +12 -100
- data/.rubocop.yml +0 -66
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -28
- data/Rakefile +0 -38
- data/benchmark/matching.rb +0 -98
- data/benchmark/redactor.rb +0 -64
- data/benchmark/relay.rb +0 -48
- data/benchmark/scaling.rb +0 -70
- data/benchmark/send_slow.rb +0 -41
- data/benchmark/support.rb +0 -114
- data/docs/COMPATIBILITY.md +0 -98
- data/docs/INTERNAL_CONTRACTS.md +0 -133
- data/docs/PERFORMANCE.md +0 -165
- data/docs/RELEASING.md +0 -84
- data/docs/VERIFICATION.md +0 -358
- data/examples/dialogue.rb +0 -30
- data/examples/kibitz/README.md +0 -81
- data/examples/kibitz/kibitz.rb +0 -142
- data/examples/kibitz/test_kibitz.rb +0 -37
- data/examples/ssh_auto.rb +0 -94
- data/examples/ssh_interact.rb +0 -159
- data/examples/ssh_login.rb +0 -64
- data/expect-pty.gemspec +0 -33
- data/script/ci +0 -121
- data/script/release.rb +0 -311
- data/test/buffer_accounting_test.rb +0 -83
- data/test/cleanup_test.rb +0 -251
- data/test/compare_upstream.rb +0 -157
- data/test/configuration_test.rb +0 -133
- data/test/deadline_test.rb +0 -237
- data/test/diagnostics_test.rb +0 -353
- data/test/edge_case_test.rb +0 -262
- data/test/fixtures/ssh_scripts/01_identity.sh +0 -4
- data/test/fixtures/ssh_scripts/02_output.sh +0 -5
- data/test/fixtures/ssh_scripts/03_delayed.sh +0 -6
- data/test/fixtures/ssh_scripts/04_failure.sh +0 -2
- data/test/fixtures/ssh_scripts/05_recovery.sh +0 -3
- data/test/integration/README.md +0 -109
- data/test/integration/ssh_scripts.rb +0 -94
- data/test/interact_test.rb +0 -253
- data/test/interconnect_test.rb +0 -425
- data/test/io_test.rb +0 -321
- data/test/kibitz_test.rb +0 -45
- data/test/lifecycle_contract_test.rb +0 -71
- data/test/literal_scan_test.rb +0 -97
- data/test/matching_test.rb +0 -211
- data/test/multi_session_test.rb +0 -66
- data/test/pattern_offset_test.rb +0 -43
- data/test/process_test.rb +0 -291
- data/test/redactor_test.rb +0 -183
- data/test/relay_recovery_test.rb +0 -451
- data/test/release_test.rb +0 -328
- data/test/ruby_api_test.rb +0 -515
- data/test/scan_reuse_test.rb +0 -109
- data/test/script_logging_test.rb +0 -124
- data/test/support/interact_probe.rb +0 -125
- data/test/support/kibitz_probe.rb +0 -177
- data/test/support/script_probe.rb +0 -158
- data/test/terminal_cleanup_test.rb +0 -349
- data/test/test_helper.rb +0 -58
- data/test/timeout_test.rb +0 -289
data/test/matching_test.rb
DELETED
|
@@ -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
|
data/test/multi_session_test.rb
DELETED
|
@@ -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
|
data/test/pattern_offset_test.rb
DELETED
|
@@ -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
|