expect-pty 0.5.1 → 0.5.3
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 +13 -0
- data/CONTRIBUTING.md +10 -4
- data/README.md +13 -4
- data/docs/INTERNAL_CONTRACTS.md +21 -0
- data/docs/RELEASING.md +26 -12
- data/docs/VERIFICATION.md +98 -0
- data/examples/kibitz/README.md +28 -20
- data/examples/kibitz/kibitz.rb +1 -0
- data/lib/expect/interaction.rb +2 -0
- data/lib/expect/relay.rb +37 -7
- data/lib/expect/session_resources.rb +39 -14
- data/lib/expect/terminal.rb +1 -1
- data/lib/expect/version.rb +1 -1
- data/lib/expect.rb +70 -33
- data/script/release.rb +10 -2
- data/test/initialization_failure_test.rb +92 -0
- data/test/integration/README.md +42 -23
- data/test/ownership_sequence_test.rb +208 -0
- data/test/process_interruption_test.rb +296 -0
- data/test/relay_reentrancy_test.rb +159 -0
- data/test/release_test.rb +47 -0
- data/test/write_contract_test.rb +105 -0
- metadata +7 -2
data/test/release_test.rb
CHANGED
|
@@ -10,6 +10,40 @@ class ReleaseTest < Minitest::Test
|
|
|
10
10
|
assert_equal "- New API.", Release.release_notes(changelog, "0.2.0")
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
def test_dry_run_writes_identical_utf8_notes_in_c_and_utf8_locales
|
|
14
|
+
notes = "- 修复初始化中断,保留原异常。\n"
|
|
15
|
+
with_package do |_release, artifact|
|
|
16
|
+
script = locale_script
|
|
17
|
+
File.write("CHANGELOG.md", "## #{Expect::VERSION}\n\n#{notes}", encoding: "UTF-8")
|
|
18
|
+
rebuild_package
|
|
19
|
+
%w[US-ASCII UTF-8].each do |encoding|
|
|
20
|
+
env = release_environment.merge("LC_ALL" => (encoding == "US-ASCII" ? "C" : "en_US.UTF-8"), "LANG" => "C")
|
|
21
|
+
output, error, status = Open3.capture3(env, RbConfig.ruby, "-E#{encoding}", script,
|
|
22
|
+
"--dry-run", "--artifact", artifact, binmode: true)
|
|
23
|
+
assert status.success?, "#{encoding}: #{output} #{error}"
|
|
24
|
+
assert_includes output, "Dry run complete"
|
|
25
|
+
files = Dir.glob("tmp/release/#{Expect::VERSION}/candidate-*/release-notes.md")
|
|
26
|
+
refute_empty files
|
|
27
|
+
files.each { |file| assert_equal notes.b, File.binread(file) }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_dry_run_rejects_invalid_utf8_without_rewriting_the_changelog
|
|
33
|
+
with_package do |_release, artifact|
|
|
34
|
+
script = locale_script
|
|
35
|
+
bytes = "## #{Expect::VERSION}\n- invalid \xff\n".b
|
|
36
|
+
File.binwrite("CHANGELOG.md", bytes)
|
|
37
|
+
rebuild_package
|
|
38
|
+
_output, error, status = Open3.capture3(release_environment.merge("LC_ALL" => "C", "LANG" => "C"),
|
|
39
|
+
RbConfig.ruby, "-EUS-ASCII", script, "--dry-run", "--artifact", artifact)
|
|
40
|
+
refute status.success?
|
|
41
|
+
assert_includes error, "Invalid UTF-8 in CHANGELOG.md"
|
|
42
|
+
assert_equal bytes, File.binread("CHANGELOG.md")
|
|
43
|
+
assert_empty Dir.glob("tmp/release/#{Expect::VERSION}/candidate-*")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
13
47
|
def test_rejects_unreleased_missing_empty_or_invalid_versions
|
|
14
48
|
["## Unreleased\n- Pending.\n## 0.2.0\n- Ready.", "## 0.1.1\n- Old.", "## 0.2.0\n"].each do |changelog|
|
|
15
49
|
assert_raises(RuntimeError) { Release.release_notes(changelog, "0.2.0") }
|
|
@@ -261,6 +295,19 @@ class ReleaseTest < Minitest::Test
|
|
|
261
295
|
|
|
262
296
|
private
|
|
263
297
|
|
|
298
|
+
# CLI 按自身路径定位项目根目录;复制入口和版本文件,避免误读开发仓库的 CHANGELOG。
|
|
299
|
+
def locale_script
|
|
300
|
+
FileUtils.mkdir_p(["script", "lib/expect"])
|
|
301
|
+
FileUtils.cp(File.expand_path("../script/release.rb", __dir__), "script/release.rb")
|
|
302
|
+
FileUtils.cp(File.expand_path("../lib/expect/version.rb", __dir__), "lib/expect/version.rb")
|
|
303
|
+
File.expand_path("script/release.rb")
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def release_environment
|
|
307
|
+
names = ENV.keys.grep(/\ABUNDLE/) + %w[RUBYOPT RUBYLIB RUBYGEMS_GEMDEPS]
|
|
308
|
+
names.to_h { |name| [name, nil] }
|
|
309
|
+
end
|
|
310
|
+
|
|
264
311
|
def git(*)
|
|
265
312
|
output, error, status = Open3.capture3("git", *)
|
|
266
313
|
assert status.success?, error
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
require "minitest/mock"
|
|
5
|
+
|
|
6
|
+
class WriteContractTest < ExpectTest
|
|
7
|
+
def test_invalid_write_counts_fail_on_the_first_attempt
|
|
8
|
+
[0, -1, nil, "3", 4].each do |invalid|
|
|
9
|
+
session, = writable_session
|
|
10
|
+
attempts = 0
|
|
11
|
+
session.writer.stub(:write_nonblock, lambda { |*, **|
|
|
12
|
+
attempts += 1
|
|
13
|
+
invalid
|
|
14
|
+
}) do
|
|
15
|
+
error = assert_raises(IOError) { bounded(0.2) { session.write("abc") } }
|
|
16
|
+
refute_kind_of Expect::WriteTimeout, error
|
|
17
|
+
assert_match(/accepted bytes/, error.message)
|
|
18
|
+
assert_equal 1, attempts
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_count_is_checked_against_the_attempted_chunk_not_the_total_input
|
|
24
|
+
session, = writable_session
|
|
25
|
+
session.writer.stub(:write_nonblock, lambda { |chunk, **|
|
|
26
|
+
assert_equal Expect::READ_SIZE, chunk.bytesize
|
|
27
|
+
chunk.bytesize + 1
|
|
28
|
+
}) do
|
|
29
|
+
assert_raises(IOError) { session.write("x" * (Expect::READ_SIZE + 1)) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_successful_short_writes_can_outlive_the_backpressure_deadline
|
|
34
|
+
session, sink = writable_session(write_timeout: 0.01)
|
|
35
|
+
now = 0.0
|
|
36
|
+
original = session.writer.method(:write_nonblock)
|
|
37
|
+
Expect.stub(:monotonic, -> { now }) do
|
|
38
|
+
session.writer.stub(:write_nonblock, lambda { |chunk, **options|
|
|
39
|
+
count = original.call(chunk.byteslice(0, 1), **options)
|
|
40
|
+
now += 0.02
|
|
41
|
+
count
|
|
42
|
+
}) do
|
|
43
|
+
assert_equal 3, session.write("abc")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
assert_in_delta 0.06, now
|
|
47
|
+
assert_equal("abc", bounded { sink.read(3) })
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_empty_input_never_attempts_a_write
|
|
51
|
+
session, = writable_session(write_timeout: 0)
|
|
52
|
+
session.writer.stub(:write_nonblock, ->(*, **) { flunk "empty input reached writer" }) do
|
|
53
|
+
assert_equal 0, session.write
|
|
54
|
+
assert_equal 0, session.write("")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_valid_short_writes_and_eintr_preserve_every_byte
|
|
59
|
+
session, sink = writable_session
|
|
60
|
+
original = session.writer.method(:write_nonblock)
|
|
61
|
+
attempts = 0
|
|
62
|
+
session.writer.stub(:write_nonblock, lambda { |chunk, **options|
|
|
63
|
+
attempts += 1
|
|
64
|
+
raise Errno::EINTR if attempts == 2
|
|
65
|
+
|
|
66
|
+
original.call(chunk.byteslice(0, 1), **options)
|
|
67
|
+
}) do
|
|
68
|
+
assert_equal(4, bounded { session.write("a\0\xffb".b) })
|
|
69
|
+
end
|
|
70
|
+
assert_equal 5, attempts
|
|
71
|
+
assert_equal("a\0\xffb".b, bounded { sink.read(4) })
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_backpressure_reports_only_confirmed_progress
|
|
75
|
+
session, sink = writable_session(write_timeout: 0.01)
|
|
76
|
+
original = session.writer.method(:write_nonblock)
|
|
77
|
+
now = 0.0
|
|
78
|
+
attempts = 0
|
|
79
|
+
Expect.stub(:monotonic, -> { now }) do
|
|
80
|
+
session.writer.stub(:write_nonblock, lambda { |chunk, **options|
|
|
81
|
+
attempts += 1
|
|
82
|
+
next :wait_writable if attempts > 1
|
|
83
|
+
|
|
84
|
+
now = 0.02
|
|
85
|
+
original.call(chunk.byteslice(0, 1), **options)
|
|
86
|
+
}) do
|
|
87
|
+
error = assert_raises(Expect::WriteTimeout) { bounded { session.write("abc") } }
|
|
88
|
+
assert_equal 1, error.bytes_written
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
assert_equal 2, attempts
|
|
92
|
+
assert_equal("a", bounded { sink.read(1) })
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def writable_session(**)
|
|
98
|
+
reader, producer = IO.pipe
|
|
99
|
+
sink, writer = IO.pipe
|
|
100
|
+
@ios.push(reader, producer, sink, writer)
|
|
101
|
+
session = Expect.open(reader, writer: writer, **)
|
|
102
|
+
@sessions << session
|
|
103
|
+
[session, sink]
|
|
104
|
+
end
|
|
105
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: expect-pty
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- expect-pty contributors
|
|
@@ -158,6 +158,7 @@ files:
|
|
|
158
158
|
- test/fixtures/ssh_scripts/03_delayed.sh
|
|
159
159
|
- test/fixtures/ssh_scripts/04_failure.sh
|
|
160
160
|
- test/fixtures/ssh_scripts/05_recovery.sh
|
|
161
|
+
- test/initialization_failure_test.rb
|
|
161
162
|
- test/integration/README.md
|
|
162
163
|
- test/integration/ssh_scripts.rb
|
|
163
164
|
- test/interact_test.rb
|
|
@@ -168,10 +169,13 @@ files:
|
|
|
168
169
|
- test/literal_scan_test.rb
|
|
169
170
|
- test/matching_test.rb
|
|
170
171
|
- test/multi_session_test.rb
|
|
172
|
+
- test/ownership_sequence_test.rb
|
|
171
173
|
- test/pattern_offset_test.rb
|
|
174
|
+
- test/process_interruption_test.rb
|
|
172
175
|
- test/process_test.rb
|
|
173
176
|
- test/redactor_test.rb
|
|
174
177
|
- test/relay_recovery_test.rb
|
|
178
|
+
- test/relay_reentrancy_test.rb
|
|
175
179
|
- test/release_test.rb
|
|
176
180
|
- test/ruby_api_test.rb
|
|
177
181
|
- test/scan_reuse_test.rb
|
|
@@ -182,6 +186,7 @@ files:
|
|
|
182
186
|
- test/terminal_cleanup_test.rb
|
|
183
187
|
- test/test_helper.rb
|
|
184
188
|
- test/timeout_test.rb
|
|
189
|
+
- test/write_contract_test.rb
|
|
185
190
|
homepage: https://github.com/gatework/expect-ruby
|
|
186
191
|
licenses:
|
|
187
192
|
- MIT
|
|
@@ -204,7 +209,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
204
209
|
- !ruby/object:Gem::Version
|
|
205
210
|
version: '0'
|
|
206
211
|
requirements: []
|
|
207
|
-
rubygems_version: 4.0.
|
|
212
|
+
rubygems_version: 4.0.12
|
|
208
213
|
specification_version: 4
|
|
209
214
|
summary: Ruby PTY automation with the Expect.pm interaction model
|
|
210
215
|
test_files: []
|