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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -1
  3. data/README.md +122 -130
  4. data/docs/API.md +113 -0
  5. data/docs/MIGRATION.md +68 -0
  6. data/lib/expect/cleanup.rb +34 -0
  7. data/lib/expect/interaction.rb +228 -210
  8. data/lib/expect/logging.rb +117 -162
  9. data/lib/expect/matcher.rb +42 -38
  10. data/lib/expect/pattern.rb +10 -3
  11. data/lib/expect/pattern_list.rb +39 -9
  12. data/lib/expect/redactor.rb +12 -5
  13. data/lib/expect/relay.rb +106 -81
  14. data/lib/expect/relay_writer.rb +7 -5
  15. data/lib/expect/result.rb +33 -9
  16. data/lib/expect/session.rb +554 -0
  17. data/lib/expect/session_resources.rb +19 -18
  18. data/lib/expect/version.rb +2 -2
  19. data/lib/expect.rb +68 -578
  20. data/sig/expect.rbs +279 -0
  21. metadata +14 -123
  22. data/.rubocop.yml +0 -66
  23. data/CONTRIBUTING.md +0 -29
  24. data/Gemfile +0 -28
  25. data/Rakefile +0 -38
  26. data/benchmark/matching.rb +0 -98
  27. data/benchmark/redactor.rb +0 -64
  28. data/benchmark/relay.rb +0 -48
  29. data/benchmark/scaling.rb +0 -70
  30. data/benchmark/send_slow.rb +0 -41
  31. data/benchmark/support.rb +0 -114
  32. data/docs/COMPATIBILITY.md +0 -98
  33. data/docs/INTERNAL_CONTRACTS.md +0 -154
  34. data/docs/PERFORMANCE.md +0 -165
  35. data/docs/RELEASING.md +0 -84
  36. data/docs/VERIFICATION.md +0 -456
  37. data/examples/dialogue.rb +0 -30
  38. data/examples/kibitz/README.md +0 -81
  39. data/examples/kibitz/kibitz.rb +0 -142
  40. data/examples/kibitz/test_kibitz.rb +0 -37
  41. data/examples/ssh_auto.rb +0 -94
  42. data/examples/ssh_interact.rb +0 -159
  43. data/examples/ssh_login.rb +0 -64
  44. data/expect-pty.gemspec +0 -33
  45. data/lib/expect/configuration.rb +0 -122
  46. data/lib/expect/terminal.rb +0 -102
  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
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
@@ -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
@@ -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