expect-pty 0.5.3 → 0.7.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 +55 -1
- data/README.md +122 -130
- data/docs/API.md +113 -0
- data/docs/MIGRATION.md +68 -0
- data/lib/expect/cleanup.rb +34 -0
- data/lib/expect/interaction.rb +228 -210
- data/lib/expect/logging.rb +117 -162
- data/lib/expect/matcher.rb +42 -38
- data/lib/expect/pattern.rb +10 -3
- data/lib/expect/pattern_list.rb +39 -9
- data/lib/expect/redactor.rb +12 -5
- data/lib/expect/relay.rb +106 -81
- data/lib/expect/relay_writer.rb +7 -5
- data/lib/expect/result.rb +33 -9
- data/lib/expect/session.rb +554 -0
- data/lib/expect/session_resources.rb +19 -18
- data/lib/expect/version.rb +2 -2
- data/lib/expect.rb +68 -578
- data/sig/expect.rbs +279 -0
- metadata +14 -123
- 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 -154
- data/docs/PERFORMANCE.md +0 -165
- data/docs/RELEASING.md +0 -84
- data/docs/VERIFICATION.md +0 -456
- 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/lib/expect/configuration.rb +0 -122
- data/lib/expect/terminal.rb +0 -102
- data/script/ci +0 -121
- data/script/release.rb +0 -319
- 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/initialization_failure_test.rb +0 -92
- 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/ownership_sequence_test.rb +0 -208
- data/test/pattern_offset_test.rb +0 -43
- data/test/process_interruption_test.rb +0 -296
- data/test/process_test.rb +0 -291
- data/test/redactor_test.rb +0 -183
- data/test/relay_recovery_test.rb +0 -451
- data/test/relay_reentrancy_test.rb +0 -159
- data/test/release_test.rb +0 -375
- 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/write_contract_test.rb +0 -105
data/test/cleanup_test.rb
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "test_helper"
|
|
4
|
-
|
|
5
|
-
class CleanupTest < ExpectTest
|
|
6
|
-
def test_failed_open_initialization_attempts_all_owned_handles_and_preserves_validation_error
|
|
7
|
-
reader, writer = IO.pipe
|
|
8
|
-
@ios.push(reader, writer)
|
|
9
|
-
reader.stub(:close, -> { raise IOError, "reader close failed" }) do
|
|
10
|
-
error = assert_raises(ArgumentError) { Expect.open(reader, writer: writer, own: true, unknown: true) }
|
|
11
|
-
assert_match(/unknown/, error.message)
|
|
12
|
-
assert writer.closed?
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def test_failed_new_initialization_attempts_all_pty_handles_and_preserves_validation_error
|
|
17
|
-
master, slave = PTY.open
|
|
18
|
-
@ios.push(master, slave)
|
|
19
|
-
PTY.stub(:open, [master, slave]) do
|
|
20
|
-
master.stub(:close, -> { raise IOError, "master close failed" }) do
|
|
21
|
-
error = assert_raises(ArgumentError) { Expect.new(unknown: true) }
|
|
22
|
-
assert_match(/unknown/, error.message)
|
|
23
|
-
assert slave.closed?
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def test_failed_open_closes_real_owned_handles_when_an_argument_is_not_io
|
|
29
|
-
[true, false].each do |invalid_reader|
|
|
30
|
-
reader, writer = IO.pipe
|
|
31
|
-
@ios.push(reader, writer)
|
|
32
|
-
invalid = Object.new
|
|
33
|
-
invalid.define_singleton_method(:close) { raise "invalid IO must not be closed" }
|
|
34
|
-
incoming = invalid_reader ? invalid : reader
|
|
35
|
-
outgoing = invalid_reader ? writer : invalid
|
|
36
|
-
assert_raises(ArgumentError) { Expect.open(incoming, writer: outgoing, own: true) }
|
|
37
|
-
assert(invalid_reader ? writer.closed? : reader.closed?)
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def test_open_block_error_is_preserved_when_owned_cleanup_also_fails
|
|
42
|
-
[ArgumentError.new("block failed"), Interrupt.new("interrupted"), SystemExit.new(17)].each do |failure|
|
|
43
|
-
reader, writer = IO.pipe
|
|
44
|
-
@ios.push(reader, writer)
|
|
45
|
-
reader.stub(:close, -> { raise IOError, "reader close failed" }) do
|
|
46
|
-
error = assert_raises(failure.class) do
|
|
47
|
-
Expect.open(reader, writer: writer, own: true) { raise failure }
|
|
48
|
-
end
|
|
49
|
-
assert_same failure, error
|
|
50
|
-
assert writer.closed?
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def test_open_block_break_still_reports_cleanup_failure
|
|
56
|
-
reader, writer = IO.pipe
|
|
57
|
-
@ios.push(reader, writer)
|
|
58
|
-
failure = IOError.new("reader close failed")
|
|
59
|
-
reader.stub(:close, -> { raise failure }) do
|
|
60
|
-
error = assert_raises(IOError) { Expect.open(reader, writer: writer, own: true) { break :done } }
|
|
61
|
-
assert_same failure, error
|
|
62
|
-
assert writer.closed?
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def test_spawn_block_error_is_preserved_and_child_reaped_when_cleanup_also_fails
|
|
67
|
-
session = stubborn_child
|
|
68
|
-
failure = ArgumentError.new("block failed")
|
|
69
|
-
Expect.stub(:new, session) do
|
|
70
|
-
session.stub(:spawn, session) do
|
|
71
|
-
session.to_io.stub(:close, -> { raise IOError, "reader close failed" }) do
|
|
72
|
-
error = assert_raises(ArgumentError) { Expect.spawn("already started") { raise failure } }
|
|
73
|
-
assert_same failure, error
|
|
74
|
-
assert_nil session.pid
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def test_graceful_close_preserves_logging_error_when_handle_cleanup_also_fails
|
|
81
|
-
session = stubborn_child
|
|
82
|
-
failure = ArgumentError.new("log failed")
|
|
83
|
-
session.log_output = ->(_) { raise failure }
|
|
84
|
-
session.to_io.stub(:wait_readable, true) do
|
|
85
|
-
session.to_io.stub(:read_nonblock, "last output") do
|
|
86
|
-
session.to_io.stub(:close, -> { raise IOError, "reader close failed" }) do
|
|
87
|
-
assert_same failure, assert_raises(ArgumentError) { session.close(graceful: true) }
|
|
88
|
-
assert_nil session.pid
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def test_close_handles_attempts_every_owned_handle_and_preserves_first_error
|
|
95
|
-
reader, writer = IO.pipe
|
|
96
|
-
extra, peer = IO.pipe
|
|
97
|
-
@ios.push(reader, writer, extra, peer)
|
|
98
|
-
resources = Expect::SessionResources.new(reader, writer: writer, slave: extra, own: true)
|
|
99
|
-
failure = IOError.new("reader close failed")
|
|
100
|
-
reader.stub(:close, -> { raise failure }) do
|
|
101
|
-
assert_same failure, assert_raises(IOError) { resources.close_handles }
|
|
102
|
-
assert writer.closed?
|
|
103
|
-
assert extra.closed?
|
|
104
|
-
end
|
|
105
|
-
resources.close_handles
|
|
106
|
-
assert reader.closed?
|
|
107
|
-
resources.close_handles
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def test_hard_close_finishes_wrappers_log_and_child_after_handle_failure
|
|
111
|
-
session = stubborn_child
|
|
112
|
-
pid = session.pid
|
|
113
|
-
first, = pipe_session
|
|
114
|
-
second, = pipe_session
|
|
115
|
-
session.instance_variable_set(:@interact_inputs, { first.to_io => first, second.to_io => second })
|
|
116
|
-
Dir.mktmpdir do |dir|
|
|
117
|
-
log = session.log_to(File.join(dir, "owned.log"))
|
|
118
|
-
failure = IOError.new("handle close failed")
|
|
119
|
-
session.to_io.stub(:close, -> { raise failure }) do
|
|
120
|
-
first.stub(:close, ->(**) { raise IOError, "wrapper close failed" }) do
|
|
121
|
-
error = assert_raises(IOError) { bounded { session.hard_close(timeout: 0.01) } }
|
|
122
|
-
assert_same failure, error
|
|
123
|
-
assert second.closed?
|
|
124
|
-
refute second.to_io.closed?, "borrowed wrapper IO must remain open"
|
|
125
|
-
assert log.closed?
|
|
126
|
-
assert_nil session.log_output
|
|
127
|
-
assert_nil session.pid
|
|
128
|
-
assert_equal Signal.list.fetch("KILL"), session.process_status.termsig
|
|
129
|
-
assert_raises(Errno::ECHILD) { Process.waitpid(pid, Process::WNOHANG) }
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
ensure
|
|
134
|
-
session&.hard_close(timeout: 0)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def test_owned_log_close_failure_is_visible_after_child_reaping_and_retryable
|
|
138
|
-
session = stubborn_child
|
|
139
|
-
Dir.mktmpdir do |dir|
|
|
140
|
-
log = session.log_to(File.join(dir, "owned.log"))
|
|
141
|
-
log.stub(:close, -> { raise IOError, "log close failed" }) do
|
|
142
|
-
assert_raises(IOError) { bounded { session.hard_close(timeout: 0.01) } }
|
|
143
|
-
assert_nil session.pid
|
|
144
|
-
assert_same log, session.log_output
|
|
145
|
-
end
|
|
146
|
-
status = session.hard_close(timeout: 0)
|
|
147
|
-
assert_same session.process_status, status
|
|
148
|
-
assert log.closed?
|
|
149
|
-
assert_nil session.log_output
|
|
150
|
-
end
|
|
151
|
-
ensure
|
|
152
|
-
session&.hard_close(timeout: 0)
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def test_explicit_close_inside_rescue_does_not_hide_a_new_cleanup_error
|
|
156
|
-
session, = pipe_session(own: true)
|
|
157
|
-
failure = IOError.new("close failed inside caller rescue")
|
|
158
|
-
begin
|
|
159
|
-
raise ArgumentError, "already handled"
|
|
160
|
-
rescue ArgumentError
|
|
161
|
-
session.to_io.stub(:close, -> { raise failure }) do
|
|
162
|
-
assert_same failure, assert_raises(IOError) { session.hard_close(timeout: 0) }
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def test_unexpected_process_error_is_not_replaced_by_cleanup_failure
|
|
168
|
-
session = stubborn_child
|
|
169
|
-
failure = RuntimeError.new("process wait failed")
|
|
170
|
-
session.to_io.stub(:close, -> { raise IOError, "close failed" }) do
|
|
171
|
-
session.stub(:wait, ->(**) { raise failure }) do
|
|
172
|
-
assert_same failure, assert_raises(RuntimeError) { session.hard_close(timeout: 0) }
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
ensure
|
|
176
|
-
session&.hard_close(timeout: 0)
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def test_soft_close_handle_failure_still_sends_term_but_never_kill
|
|
180
|
-
session = stubborn_child
|
|
181
|
-
signals = []
|
|
182
|
-
original = Process.method(:kill)
|
|
183
|
-
Process.stub(:kill, lambda { |signal, pid|
|
|
184
|
-
signals << signal
|
|
185
|
-
original.call(signal, pid)
|
|
186
|
-
}) do
|
|
187
|
-
session.to_io.stub(:close, -> { raise Errno::EIO, "close failed" }) do
|
|
188
|
-
assert_raises(Errno::EIO) { bounded { session.soft_close(timeout: 0, term_timeout: 0) } }
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
assert_equal ["TERM"], signals
|
|
192
|
-
assert session.alive?
|
|
193
|
-
assert session.closed?
|
|
194
|
-
ensure
|
|
195
|
-
session&.hard_close(timeout: 0)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def test_finalizer_reaps_after_both_handle_and_log_close_fail
|
|
199
|
-
session = stubborn_child
|
|
200
|
-
pid = session.pid
|
|
201
|
-
resources = session.instance_variable_get(:@resources)
|
|
202
|
-
log_attempted = false
|
|
203
|
-
Dir.mktmpdir do |dir|
|
|
204
|
-
log = session.log_to(File.join(dir, "owned.log"))
|
|
205
|
-
session.to_io.stub(:close, -> { raise IOError, "handle failed" }) do
|
|
206
|
-
log.stub(:close, lambda {
|
|
207
|
-
log_attempted = true
|
|
208
|
-
raise Errno::EIO, "log failed"
|
|
209
|
-
}) do
|
|
210
|
-
bounded { resources.finalize }
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
assert log_attempted
|
|
214
|
-
assert_nil resources.pid
|
|
215
|
-
bounded do
|
|
216
|
-
loop do
|
|
217
|
-
Process.kill(0, pid)
|
|
218
|
-
sleep 0.005
|
|
219
|
-
rescue Errno::ESRCH
|
|
220
|
-
break
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
ensure
|
|
225
|
-
session&.hard_close(timeout: 0)
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def test_non_owner_cleanup_never_signals_and_finalizer_leaves_handles_alone
|
|
229
|
-
session = stubborn_child
|
|
230
|
-
resources = session.instance_variable_get(:@resources)
|
|
231
|
-
Process.stub(:pid, -1) do
|
|
232
|
-
Process.stub(:kill, ->(*) { flunk "non-owner sent a signal" }) do
|
|
233
|
-
resources.finalize
|
|
234
|
-
refute session.to_io.closed?
|
|
235
|
-
assert_nil session.hard_close(timeout: 0)
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
assert session.alive?
|
|
239
|
-
ensure
|
|
240
|
-
session&.hard_close(timeout: 0)
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
private
|
|
244
|
-
|
|
245
|
-
def stubborn_child
|
|
246
|
-
session = child('Signal.trap("HUP", "IGNORE"); Signal.trap("TERM", "IGNORE"); puts "ready"; sleep 60',
|
|
247
|
-
raw_pty: true)
|
|
248
|
-
assert_equal 1, session.expect("ready", timeout: 2)
|
|
249
|
-
session
|
|
250
|
-
end
|
|
251
|
-
end
|
data/test/compare_upstream.rb
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Optional differential check. The ordinary test suite has no Perl dependency.
|
|
4
|
-
require "json"
|
|
5
|
-
require "open3"
|
|
6
|
-
require "rbconfig"
|
|
7
|
-
require_relative "../lib/expect"
|
|
8
|
-
|
|
9
|
-
upstream = ARGV.fetch(0) { abort "usage: ruby test/compare_upstream.rb /path/to/expect.pm" }
|
|
10
|
-
perl = <<~'PERL'
|
|
11
|
-
use strict;
|
|
12
|
-
use warnings;
|
|
13
|
-
use Expect;
|
|
14
|
-
use JSON::PP;
|
|
15
|
-
my %results;
|
|
16
|
-
sub fresh {
|
|
17
|
-
my ($buffer) = @_;
|
|
18
|
-
my $e = Expect->new;
|
|
19
|
-
$e->log_stdout(0);
|
|
20
|
-
$e->set_accum($buffer);
|
|
21
|
-
return $e;
|
|
22
|
-
}
|
|
23
|
-
sub snapshot {
|
|
24
|
-
my ($e) = @_;
|
|
25
|
-
my $captures = $e->exp_matchlist;
|
|
26
|
-
return { number => $e->match_number, before => $e->before,
|
|
27
|
-
match => $e->match, after => $e->after, captures => $captures || [],
|
|
28
|
-
accum => $e->get_accum };
|
|
29
|
-
}
|
|
30
|
-
my $e = fresh('before a.c after');
|
|
31
|
-
$e->expect(0, 'absent', 'a.c');
|
|
32
|
-
$results{literal} = snapshot($e);
|
|
33
|
-
$e = fresh('prefix value=42 tail');
|
|
34
|
-
$e->expect(0, ['value=(\d+)']);
|
|
35
|
-
$results{regexp_array} = snapshot($e);
|
|
36
|
-
$e = fresh("first\nsecond\nthird");
|
|
37
|
-
$e->expect(0, '-re', '^second$');
|
|
38
|
-
$results{multiline} = snapshot($e);
|
|
39
|
-
$e = fresh('second first');
|
|
40
|
-
$e->expect(0, 'first', 'second');
|
|
41
|
-
$results{priority} = snapshot($e);
|
|
42
|
-
$e = fresh('before token tail');
|
|
43
|
-
$e->notransfer(1);
|
|
44
|
-
$e->expect(0, 'token');
|
|
45
|
-
$results{notransfer} = snapshot($e);
|
|
46
|
-
$e = fresh('discard tail');
|
|
47
|
-
$e->max_accum(4);
|
|
48
|
-
$e->expect(0, 'tail');
|
|
49
|
-
$results{max_accum} = snapshot($e);
|
|
50
|
-
$e = fresh('keep this');
|
|
51
|
-
$e->expect(0, 'missing');
|
|
52
|
-
$results{timeout} = { error => $e->exp_error, before => $e->before, accum => $e->get_accum };
|
|
53
|
-
$e = fresh('A B C End');
|
|
54
|
-
my @states;
|
|
55
|
-
$e->expect(1, ['[ABC]', sub { push @states, $_[0]->match; exp_continue }], 'End');
|
|
56
|
-
$results{continue} = { states => \@states, final => $e->match, number => $e->match_number };
|
|
57
|
-
$e = fresh('');
|
|
58
|
-
$e->raw_pty(1);
|
|
59
|
-
$e->spawn($^X, '-e', '$|=1; while (<STDIN>) { chomp; print scalar(reverse($_)), "\n" }');
|
|
60
|
-
$e->send("crate\n");
|
|
61
|
-
$e->expect(3, 'etarc');
|
|
62
|
-
$results{pty_dialogue} = { before => $e->before, match => $e->match, number => $e->match_number };
|
|
63
|
-
$e->hard_close;
|
|
64
|
-
pipe(my $reader, my $writer) or die $!;
|
|
65
|
-
$writer->autoflush(1);
|
|
66
|
-
my $input = Expect->exp_init($reader);
|
|
67
|
-
my $idle = fresh('');
|
|
68
|
-
print $writer 'beforeSTOP42;after';
|
|
69
|
-
$results{readiness} = [Expect::test_handles(1, $idle, $input)];
|
|
70
|
-
my @escapes;
|
|
71
|
-
$input->set_seq('STOP\d+;', sub { push @escapes, 'stopped'; return 0; });
|
|
72
|
-
Expect::interconnect($input);
|
|
73
|
-
$results{regexp_escape} = \@escapes;
|
|
74
|
-
close $writer;
|
|
75
|
-
print JSON::PP->new->canonical->encode(\%results);
|
|
76
|
-
PERL
|
|
77
|
-
|
|
78
|
-
output, errors, status = Open3.capture3("perl", "-I#{File.join(upstream, "lib")}", "-e", perl)
|
|
79
|
-
abort "Perl fixture failed: #{errors}" unless status.success?
|
|
80
|
-
expected = JSON.parse(output)
|
|
81
|
-
actual = {}
|
|
82
|
-
sessions = []
|
|
83
|
-
fresh = lambda do |buffer|
|
|
84
|
-
session = Expect.new(log_stdout: false)
|
|
85
|
-
sessions << session
|
|
86
|
-
session.buffer = buffer
|
|
87
|
-
session
|
|
88
|
-
end
|
|
89
|
-
snapshot = lambda do |s|
|
|
90
|
-
{ "number" => s.match_number, "before" => s.before, "match" => s.match,
|
|
91
|
-
"after" => s.after, "captures" => s.captures, "accum" => s.buffer }
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
begin
|
|
95
|
-
s = fresh.call("before a.c after")
|
|
96
|
-
s.expect("absent", "a.c", timeout: 0)
|
|
97
|
-
actual["literal"] = snapshot.call(s)
|
|
98
|
-
s = fresh.call("prefix value=42 tail")
|
|
99
|
-
s.expect(/value=(\d+)/, timeout: 0)
|
|
100
|
-
actual["regexp_array"] = snapshot.call(s)
|
|
101
|
-
s = fresh.call("first\nsecond\nthird")
|
|
102
|
-
s.expect(/^second$/, timeout: 0)
|
|
103
|
-
actual["multiline"] = snapshot.call(s)
|
|
104
|
-
s = fresh.call("second first")
|
|
105
|
-
s.expect("first", "second", timeout: 0)
|
|
106
|
-
actual["priority"] = snapshot.call(s)
|
|
107
|
-
s = fresh.call("before token tail")
|
|
108
|
-
s.preserve_buffer = true
|
|
109
|
-
s.expect("token", timeout: 0)
|
|
110
|
-
actual["notransfer"] = snapshot.call(s)
|
|
111
|
-
s = fresh.call("discard tail")
|
|
112
|
-
s.buffer_limit = 4
|
|
113
|
-
s.expect("tail", timeout: 0)
|
|
114
|
-
actual["max_accum"] = snapshot.call(s)
|
|
115
|
-
s = fresh.call("keep this")
|
|
116
|
-
s.expect("missing", timeout: 0)
|
|
117
|
-
actual["timeout"] =
|
|
118
|
-
{ "error" => (s.error == :timeout ? "1:TIMEOUT" : s.error), "before" => s.before, "accum" => s.buffer }
|
|
119
|
-
s = fresh.call("A B C End")
|
|
120
|
-
states = []
|
|
121
|
-
s.expect(timeout: 1) do
|
|
122
|
-
on(/[ABC]/) do |object|
|
|
123
|
-
states << object.match
|
|
124
|
-
Expect.continue
|
|
125
|
-
end
|
|
126
|
-
on("End")
|
|
127
|
-
end
|
|
128
|
-
actual["continue"] = { "states" => states, "final" => s.match, "number" => s.match_number }
|
|
129
|
-
s = fresh.call("")
|
|
130
|
-
s.raw_pty = true
|
|
131
|
-
s.spawn(RbConfig.ruby, "--disable-gems", "-e",
|
|
132
|
-
"STDOUT.sync = true; while line = STDIN.gets; puts line.chomp.reverse; end")
|
|
133
|
-
s.write("crate\n")
|
|
134
|
-
s.expect("etarc", timeout: 3)
|
|
135
|
-
actual["pty_dialogue"] = { "before" => s.before, "match" => s.match, "number" => s.match_number }
|
|
136
|
-
reader, writer = IO.pipe
|
|
137
|
-
input = Expect.open(reader, own: true)
|
|
138
|
-
sessions << input
|
|
139
|
-
idle = fresh.call("")
|
|
140
|
-
writer.write("beforeSTOP42;after")
|
|
141
|
-
actual["readiness"] = Expect.readable_sessions(idle, input, timeout: 1).map { |session| [idle, input].index(session) }
|
|
142
|
-
escapes = []
|
|
143
|
-
input.on_sequence(/STOP\d+;/) do
|
|
144
|
-
escapes << "stopped"
|
|
145
|
-
false
|
|
146
|
-
end
|
|
147
|
-
Expect.interconnect(input, timeout: 1)
|
|
148
|
-
actual["regexp_escape"] = escapes
|
|
149
|
-
writer.close
|
|
150
|
-
|
|
151
|
-
failures = expected.keys.reject { |key| actual.fetch(key) == expected.fetch(key) }
|
|
152
|
-
failures.each { |key| warn "#{key}: Perl=#{expected[key].inspect} Ruby=#{actual[key].inspect}" }
|
|
153
|
-
abort "#{failures.length} upstream comparisons failed" unless failures.empty?
|
|
154
|
-
puts "#{expected.length} upstream comparisons passed (Expect.pm #{File.basename(upstream)})"
|
|
155
|
-
ensure
|
|
156
|
-
sessions.reverse_each { |session| session.hard_close(timeout: 0.03) }
|
|
157
|
-
end
|
data/test/configuration_test.rb
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "test_helper"
|
|
4
|
-
|
|
5
|
-
class ConfigurationTest < ExpectTest
|
|
6
|
-
def test_configuration_is_frozen_and_sessions_have_independent_settings
|
|
7
|
-
Expect.configure(timeout: 0.1) do |config|
|
|
8
|
-
config.buffer_limit = 4
|
|
9
|
-
config.log_stdout = false
|
|
10
|
-
end
|
|
11
|
-
first, = pipe_session
|
|
12
|
-
second, = pipe_session
|
|
13
|
-
first.timeout = 1
|
|
14
|
-
first.buffer_limit = nil
|
|
15
|
-
Expect.configure(timeout: 2)
|
|
16
|
-
third, = pipe_session
|
|
17
|
-
|
|
18
|
-
assert_equal 1, first.timeout
|
|
19
|
-
assert_equal 0.1, second.timeout
|
|
20
|
-
assert_equal 2, third.timeout
|
|
21
|
-
assert_equal 4, second.buffer_limit
|
|
22
|
-
assert_nil first.buffer_limit
|
|
23
|
-
assert_equal 2, Expect.configuration.timeout
|
|
24
|
-
assert_raises(FrozenError) { Expect.configuration.timeout = 10 }
|
|
25
|
-
Expect.configuration.to_h.clear
|
|
26
|
-
assert_equal 2, Expect.configuration.timeout
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def test_configuration_errors_do_not_publish_partial_changes
|
|
30
|
-
previous = Expect.configuration
|
|
31
|
-
assert_raises(ArgumentError) { Expect.configure(unknown: true) }
|
|
32
|
-
assert_same previous, Expect.configuration
|
|
33
|
-
assert_raises(ArgumentError) do
|
|
34
|
-
Expect.configure do |config|
|
|
35
|
-
config.timeout = 5
|
|
36
|
-
config.debug_level = 4
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
assert_same previous, Expect.configuration
|
|
40
|
-
assert_raises(RuntimeError) { Expect.configure(timeout: 3) { raise "cancelled" } }
|
|
41
|
-
assert_same previous, Expect.configuration
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def test_subclasses_inherit_configuration_and_can_override_it_independently
|
|
45
|
-
subclass = Class.new(Expect)
|
|
46
|
-
Expect.configure(timeout: 0.5)
|
|
47
|
-
assert_equal 0.5, subclass.configuration.timeout
|
|
48
|
-
subclass.configure(timeout: 1)
|
|
49
|
-
Expect.configure(timeout: 2)
|
|
50
|
-
reader, writer = IO.pipe
|
|
51
|
-
@ios.push(reader, writer)
|
|
52
|
-
subclass.open(reader) do |session|
|
|
53
|
-
assert_equal 1, session.timeout
|
|
54
|
-
assert_instance_of subclass, session
|
|
55
|
-
end
|
|
56
|
-
assert_equal 2, Expect.configuration.timeout
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def test_concurrent_configure_updates_are_serialized
|
|
60
|
-
subclass = Class.new(Expect)
|
|
61
|
-
entered = Queue.new
|
|
62
|
-
second_started = Queue.new
|
|
63
|
-
release = Queue.new
|
|
64
|
-
first = Thread.new do
|
|
65
|
-
subclass.configure do |config|
|
|
66
|
-
entered << true
|
|
67
|
-
release.pop
|
|
68
|
-
config.timeout = 1
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
entered.pop
|
|
72
|
-
second = Thread.new do
|
|
73
|
-
second_started << true
|
|
74
|
-
subclass.configure(debug_level: 2)
|
|
75
|
-
end
|
|
76
|
-
second_started.pop
|
|
77
|
-
# 第一轮尚未发布时,第二轮不能从同一旧快照完成更新。
|
|
78
|
-
refute second.join(0.05), "concurrent configure published before the first update finished"
|
|
79
|
-
release << true
|
|
80
|
-
first.value
|
|
81
|
-
second.value
|
|
82
|
-
assert_equal 1, subclass.configuration.timeout
|
|
83
|
-
assert_equal 2, subclass.configuration.debug_level
|
|
84
|
-
ensure
|
|
85
|
-
release&.push(true)
|
|
86
|
-
first&.join
|
|
87
|
-
second&.join
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def test_nested_configure_fails_without_publishing_partial_changes
|
|
91
|
-
subclass = Class.new(Expect)
|
|
92
|
-
previous = subclass.configuration
|
|
93
|
-
assert_raises(ThreadError) do
|
|
94
|
-
subclass.configure do |config|
|
|
95
|
-
config.timeout = 1
|
|
96
|
-
subclass.configure(debug_level: 2)
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
assert_same previous, subclass.configuration
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def test_attribute_validation_preserves_the_current_value_and_buffer
|
|
103
|
-
session, = pipe_session(buffer_limit: 8, timeout: 1, write_timeout: 2, debug_level: 1)
|
|
104
|
-
session.buffer = "contents"
|
|
105
|
-
[0, -1, 1.5, "4"].each do |limit|
|
|
106
|
-
assert_raises(ArgumentError) { session.buffer_limit = limit }
|
|
107
|
-
end
|
|
108
|
-
assert_equal 8, session.buffer_limit
|
|
109
|
-
assert_equal "contents", session.buffer
|
|
110
|
-
assert_raises(ArgumentError) { session.timeout = Float::INFINITY }
|
|
111
|
-
assert_raises(ArgumentError) { session.write_timeout = -1 }
|
|
112
|
-
assert_raises(ArgumentError) { session.debug_level = 1.5 }
|
|
113
|
-
assert_equal [1, 2, 1], [session.timeout, session.write_timeout, session.debug_level]
|
|
114
|
-
session.buffer_limit = 4
|
|
115
|
-
assert_equal "ents", session.buffer
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def test_predicates_use_ruby_truthiness_and_normalize_boolean_attributes
|
|
119
|
-
Expect.configure(raw_pty: 0, log_stdout: nil)
|
|
120
|
-
config = Expect.configuration
|
|
121
|
-
assert_equal true, config.raw_pty
|
|
122
|
-
assert_equal false, config.log_stdout
|
|
123
|
-
session, = pipe_session
|
|
124
|
-
assert session.raw_pty?
|
|
125
|
-
refute session.log_stdout?
|
|
126
|
-
session.preserve_buffer = 0
|
|
127
|
-
assert session.preserve_buffer?
|
|
128
|
-
session.preserve_buffer = nil
|
|
129
|
-
refute session.preserve_buffer?
|
|
130
|
-
assert_raises(ArgumentError) { session.timeout(1) }
|
|
131
|
-
assert_raises(ArgumentError) { session.log_stdout(true) }
|
|
132
|
-
end
|
|
133
|
-
end
|