expect-pty 0.3.2 → 0.4.0
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/.rubocop.yml +18 -10
- data/CHANGELOG.md +42 -12
- data/CONTRIBUTING.md +3 -1
- data/Gemfile +1 -0
- data/README.md +194 -80
- data/benchmark/matching.rb +26 -0
- data/benchmark/scaling.rb +70 -0
- data/benchmark/support.rb +23 -3
- data/docs/COMPATIBILITY.md +68 -57
- data/docs/INTERNAL_CONTRACTS.md +95 -24
- data/docs/PERFORMANCE.md +71 -9
- data/docs/RELEASING.md +8 -6
- data/docs/VERIFICATION.md +213 -81
- data/lib/expect/configuration.rb +3 -1
- data/lib/expect/interaction.rb +18 -3
- data/lib/expect/logging.rb +200 -0
- data/lib/expect/matcher.rb +70 -8
- data/lib/expect/pattern.rb +11 -4
- data/lib/expect/pattern_list.rb +3 -0
- data/lib/expect/redactor.rb +81 -0
- data/lib/expect/relay.rb +12 -0
- data/lib/expect/relay_writer.rb +9 -0
- data/lib/expect/result.rb +2 -0
- data/lib/expect/session_resources.rb +6 -2
- data/lib/expect/terminal.rb +36 -0
- data/lib/expect/version.rb +1 -1
- data/lib/expect.rb +84 -152
- data/script/ci +30 -3
- data/script/release.rb +37 -5
- data/test/buffer_accounting_test.rb +83 -0
- data/test/cleanup_test.rb +88 -0
- data/test/deadline_test.rb +237 -0
- data/test/diagnostics_test.rb +353 -0
- data/test/lifecycle_contract_test.rb +71 -0
- data/test/literal_scan_test.rb +97 -0
- data/test/pattern_offset_test.rb +3 -1
- data/test/process_test.rb +46 -0
- data/test/release_test.rb +94 -16
- data/test/timeout_test.rb +119 -0
- metadata +11 -2
data/test/release_test.rb
CHANGED
|
@@ -36,11 +36,11 @@ class ReleaseTest < Minitest::Test
|
|
|
36
36
|
|
|
37
37
|
def test_rubygems_only_publishes_the_verified_copy_without_github
|
|
38
38
|
with_package do |_release, artifact|
|
|
39
|
+
commit_package_source
|
|
39
40
|
release = Release.new(artifact: artifact, rubygems_only: true)
|
|
40
41
|
bytes = File.binread(artifact)
|
|
41
42
|
checksum = Digest::SHA256.hexdigest(bytes)
|
|
42
43
|
published = false
|
|
43
|
-
capture = ->(*arguments) { arguments.include?("rev-parse") ? "verified" : "" }
|
|
44
44
|
get = lambda do |path|
|
|
45
45
|
if path.start_with?("/downloads/")
|
|
46
46
|
Struct.new(:code, :body).new("200", bytes)
|
|
@@ -58,19 +58,17 @@ class ReleaseTest < Minitest::Test
|
|
|
58
58
|
refute_equal File.expand_path(artifact), candidate
|
|
59
59
|
published = true
|
|
60
60
|
end
|
|
61
|
-
release.stub(:
|
|
62
|
-
release.stub(:
|
|
63
|
-
release.stub(:
|
|
64
|
-
release.stub(:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
assert published
|
|
73
|
-
end
|
|
61
|
+
release.stub(:get, get) do
|
|
62
|
+
release.stub(:system, push) do
|
|
63
|
+
release.stub(:github, ->(*) { flunk "RubyGems-only release contacted GitHub" }) do
|
|
64
|
+
release.stub(:publish_github, -> { flunk "RubyGems-only release published to GitHub" }) do
|
|
65
|
+
[nil, "true"].each do |github_actions|
|
|
66
|
+
with_environment("GITHUB_ACTIONS" => github_actions,
|
|
67
|
+
"GEM_HOST_API_KEY" => (github_actions ? "test-only-api-key" : nil)) do
|
|
68
|
+
published = false
|
|
69
|
+
output, = capture_io { release.run }
|
|
70
|
+
assert_includes output, "SHA256 verified"
|
|
71
|
+
assert published
|
|
74
72
|
end
|
|
75
73
|
end
|
|
76
74
|
end
|
|
@@ -107,9 +105,11 @@ class ReleaseTest < Minitest::Test
|
|
|
107
105
|
|
|
108
106
|
def test_rubygems_only_rechecks_source_after_verification
|
|
109
107
|
with_package do |_release, artifact|
|
|
108
|
+
commit_package_source
|
|
110
109
|
release = Release.new(artifact: artifact, rubygems_only: true)
|
|
111
110
|
statuses = ["", " M payload.rb"]
|
|
112
|
-
|
|
111
|
+
original = release.method(:capture)
|
|
112
|
+
capture = ->(*arguments) { arguments.include?("rev-parse") ? original.call(*arguments) : statuses.shift }
|
|
113
113
|
release.stub(:capture, capture) do
|
|
114
114
|
release.stub(:get, ->(*) { flunk "changed source contacted RubyGems" }) do
|
|
115
115
|
capture_io do
|
|
@@ -127,6 +127,53 @@ class ReleaseTest < Minitest::Test
|
|
|
127
127
|
end
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
def test_rejects_ignored_source_collected_by_the_package_glob
|
|
131
|
+
with_package do |_release, artifact|
|
|
132
|
+
commit_package_source
|
|
133
|
+
FileUtils.mkdir_p("lib")
|
|
134
|
+
File.write("lib/local_only.rb", "puts :local_only\n")
|
|
135
|
+
File.write(".git/info/exclude", "lib/local_only.rb\n", mode: "a")
|
|
136
|
+
rebuild_package
|
|
137
|
+
assert_empty git("status", "--porcelain")
|
|
138
|
+
|
|
139
|
+
error = publishing_source_error(artifact)
|
|
140
|
+
assert_match "not in release commit: lib/local_only.rb", error.message
|
|
141
|
+
# 提交前的 dry-run 仍可检验候选包,但不能以此证明发布来源。
|
|
142
|
+
capture_io { Release.new(artifact: artifact, dry_run: true).run }
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def test_rejects_source_changes_hidden_from_git_status
|
|
147
|
+
with_package do |_release, artifact|
|
|
148
|
+
commit_package_source
|
|
149
|
+
git("update-index", "--assume-unchanged", "payload.rb")
|
|
150
|
+
File.binwrite("payload.rb", "puts :changed\n\n")
|
|
151
|
+
rebuild_package
|
|
152
|
+
assert_empty git("status", "--porcelain")
|
|
153
|
+
assert_match "differs from release commit: payload.rb", publishing_source_error(artifact).message
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def test_rejects_executable_mode_changes_hidden_from_git_status
|
|
158
|
+
with_package do |_release, artifact|
|
|
159
|
+
commit_package_source
|
|
160
|
+
git("config", "core.filemode", "false")
|
|
161
|
+
File.chmod(0o755, "payload.rb")
|
|
162
|
+
rebuild_package
|
|
163
|
+
assert_empty git("status", "--porcelain")
|
|
164
|
+
assert_match "permissions differ from release commit: payload.rb", publishing_source_error(artifact).message
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def test_rejects_altered_homepage_metadata
|
|
169
|
+
with_package do |release, _artifact|
|
|
170
|
+
spec = Gem::Specification.load(File.expand_path("expect-pty.gemspec")).dup
|
|
171
|
+
spec.homepage = "https://example.invalid/altered"
|
|
172
|
+
capture_io { Gem::Package.build(spec) }
|
|
173
|
+
assert_match "metadata", assert_raises(RuntimeError) { release.send(:verify_package) }.message
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
130
177
|
def test_rejects_different_installation_metadata_even_with_identical_source_files
|
|
131
178
|
with_package do |release, _artifact|
|
|
132
179
|
expected = Gem::Specification.load(File.expand_path("expect-pty.gemspec")).dup
|
|
@@ -214,6 +261,37 @@ class ReleaseTest < Minitest::Test
|
|
|
214
261
|
|
|
215
262
|
private
|
|
216
263
|
|
|
264
|
+
def git(*)
|
|
265
|
+
output, error, status = Open3.capture3("git", *)
|
|
266
|
+
assert status.success?, error
|
|
267
|
+
output
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def commit_package_source
|
|
271
|
+
git("init", "--quiet")
|
|
272
|
+
git("config", "core.hooksPath", File::NULL)
|
|
273
|
+
File.write(".git/info/exclude", "*.gem\ntmp/\n")
|
|
274
|
+
git("add", "--", "payload.rb", "CHANGELOG.md", "expect-pty.gemspec")
|
|
275
|
+
git("-c", "user.name=Release Test", "-c", "user.email=release-test@example.invalid",
|
|
276
|
+
"-c", "commit.gpgsign=false", "commit", "--quiet", "-m", "Release fixture")
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def rebuild_package
|
|
280
|
+
# Gem::Specification.load 会缓存已加载的 gemspec;重新执行才能看到新的 glob 文件。
|
|
281
|
+
Gem::Specification.reset
|
|
282
|
+
spec = Gem::Specification.load(File.expand_path("expect-pty.gemspec"))
|
|
283
|
+
capture_io { Gem::Package.build(spec) }
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def publishing_source_error(artifact)
|
|
287
|
+
release = Release.new(artifact: artifact, rubygems_only: true)
|
|
288
|
+
release.stub(:get, ->(*) { flunk "uncommitted package content reached RubyGems" }) do
|
|
289
|
+
error = nil
|
|
290
|
+
capture_io { error = assert_raises(RuntimeError) { release.run } }
|
|
291
|
+
error
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
217
295
|
# 发布测试显式控制凭据环境,避免本机登录状态或 CI 标记影响用例结果。
|
|
218
296
|
def with_environment(values)
|
|
219
297
|
previous = values.to_h { |name, _value| [name, ENV.fetch(name, nil)] }
|
|
@@ -238,7 +316,7 @@ class ReleaseTest < Minitest::Test
|
|
|
238
316
|
spec.authors = ["Test"]
|
|
239
317
|
spec.license = "MIT"
|
|
240
318
|
spec.homepage = "https://github.com/gatework/expect-ruby"
|
|
241
|
-
spec.files = ["payload.rb", "CHANGELOG.md", "expect-pty.gemspec"]
|
|
319
|
+
spec.files = ["payload.rb", "CHANGELOG.md", "expect-pty.gemspec"] + Dir["lib/**/*.rb"]
|
|
242
320
|
end
|
|
243
321
|
RUBY
|
|
244
322
|
artifact = nil
|
data/test/timeout_test.rb
CHANGED
|
@@ -48,6 +48,125 @@ class TimeoutTest < ExpectTest
|
|
|
48
48
|
assert_equal :timeout, session.error
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
def test_eof_continuation_without_reset_observes_the_deadline_before_matching_again
|
|
52
|
+
first, = pipe_session
|
|
53
|
+
second, = pipe_session
|
|
54
|
+
first.close
|
|
55
|
+
now = 0.0
|
|
56
|
+
result = Expect.stub(:monotonic, -> { now }) do
|
|
57
|
+
Expect.expect_result(timeout: 1) do
|
|
58
|
+
eof(from: first) do
|
|
59
|
+
now = 2.0
|
|
60
|
+
second.buffer = "ready"
|
|
61
|
+
Expect.continue(reset_timeout: false)
|
|
62
|
+
end
|
|
63
|
+
on("ready", from: second)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
assert result.timeout?
|
|
68
|
+
assert_same second, result.session
|
|
69
|
+
assert_equal "ready", second.buffer
|
|
70
|
+
assert first.last_result.eof?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_eof_continuation_can_reset_an_expired_deadline
|
|
74
|
+
first, = pipe_session
|
|
75
|
+
second, = pipe_session
|
|
76
|
+
first.close
|
|
77
|
+
now = 0.0
|
|
78
|
+
result = Expect.stub(:monotonic, -> { now }) do
|
|
79
|
+
Expect.expect_result(timeout: 1) do
|
|
80
|
+
eof(from: first) do
|
|
81
|
+
now = 2.0
|
|
82
|
+
second.buffer = "ready"
|
|
83
|
+
Expect.continue
|
|
84
|
+
end
|
|
85
|
+
on("ready", from: second)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
assert_equal "ready", result.match
|
|
90
|
+
assert_same second, result.session
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_last_eof_returns_even_when_continuation_deadline_has_expired
|
|
94
|
+
session, = pipe_session
|
|
95
|
+
session.close
|
|
96
|
+
now = 0.0
|
|
97
|
+
result = Expect.stub(:monotonic, -> { now }) do
|
|
98
|
+
session.expect_result(timeout: 1) do
|
|
99
|
+
eof do
|
|
100
|
+
now = 2.0
|
|
101
|
+
Expect.continue(reset_timeout: false)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
assert result.eof?
|
|
107
|
+
assert_same session, result.session
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def test_expired_eof_continuation_delivers_all_known_eof_events
|
|
111
|
+
sessions = Array.new(3) { pipe_session.first }
|
|
112
|
+
sessions.each(&:close)
|
|
113
|
+
seen = []
|
|
114
|
+
result = Expect.expect_result(from: sessions, timeout: 0) do
|
|
115
|
+
eof do |session|
|
|
116
|
+
seen << session
|
|
117
|
+
Expect.continue(reset_timeout: false)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
assert result.eof?
|
|
122
|
+
assert_equal sessions, seen
|
|
123
|
+
assert(sessions.all? { |session| session.last_result.eof? })
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_expired_eof_continuation_handles_known_eof_before_timing_out_live_sources
|
|
127
|
+
first, = pipe_session
|
|
128
|
+
second, = pipe_session
|
|
129
|
+
live, = pipe_session
|
|
130
|
+
[first, second].each(&:close)
|
|
131
|
+
seen = []
|
|
132
|
+
timed_out = nil
|
|
133
|
+
result = Expect.expect_result(timeout: 0) do
|
|
134
|
+
eof(from: [first, second]) do |session|
|
|
135
|
+
seen << session
|
|
136
|
+
live.buffer = "ready"
|
|
137
|
+
Expect.continue(reset_timeout: false)
|
|
138
|
+
end
|
|
139
|
+
on("ready", from: live)
|
|
140
|
+
timeout { |sessions| timed_out = sessions }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
assert result.timeout?
|
|
144
|
+
assert_equal [first, second], seen
|
|
145
|
+
assert_equal [live], timed_out
|
|
146
|
+
assert_equal "ready", live.buffer
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def test_reset_after_expired_eof_continuation_resumes_text_matching
|
|
150
|
+
%i[eof timeout].each do |reset_event|
|
|
151
|
+
first, = pipe_session
|
|
152
|
+
second, = pipe_session
|
|
153
|
+
live, = pipe_session
|
|
154
|
+
[first, second].each(&:close)
|
|
155
|
+
result = Expect.expect_result(timeout: 0) do
|
|
156
|
+
eof(from: first) do
|
|
157
|
+
live.buffer = "ready"
|
|
158
|
+
Expect.continue(reset_timeout: false)
|
|
159
|
+
end
|
|
160
|
+
eof(from: second) { Expect.continue(reset_timeout: reset_event == :eof) }
|
|
161
|
+
on("ready", from: live)
|
|
162
|
+
timeout { Expect.continue }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
assert_equal "ready", result.match
|
|
166
|
+
assert_same live, result.session
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
51
170
|
def test_restart_timeout_on_receive
|
|
52
171
|
session, writer = pipe_session
|
|
53
172
|
session.reset_timeout_on_read = true
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: expect-pty
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- expect-pty contributors
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-09-
|
|
10
|
+
date: 2026-09-27 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: forwardable
|
|
@@ -112,6 +112,7 @@ files:
|
|
|
112
112
|
- Rakefile
|
|
113
113
|
- benchmark/matching.rb
|
|
114
114
|
- benchmark/relay.rb
|
|
115
|
+
- benchmark/scaling.rb
|
|
115
116
|
- benchmark/send_slow.rb
|
|
116
117
|
- benchmark/support.rb
|
|
117
118
|
- docs/COMPATIBILITY.md
|
|
@@ -130,20 +131,26 @@ files:
|
|
|
130
131
|
- lib/expect.rb
|
|
131
132
|
- lib/expect/configuration.rb
|
|
132
133
|
- lib/expect/interaction.rb
|
|
134
|
+
- lib/expect/logging.rb
|
|
133
135
|
- lib/expect/matcher.rb
|
|
134
136
|
- lib/expect/pattern.rb
|
|
135
137
|
- lib/expect/pattern_list.rb
|
|
136
138
|
- lib/expect/pty.rb
|
|
139
|
+
- lib/expect/redactor.rb
|
|
137
140
|
- lib/expect/relay.rb
|
|
138
141
|
- lib/expect/relay_writer.rb
|
|
139
142
|
- lib/expect/result.rb
|
|
140
143
|
- lib/expect/session_resources.rb
|
|
144
|
+
- lib/expect/terminal.rb
|
|
141
145
|
- lib/expect/version.rb
|
|
142
146
|
- script/ci
|
|
143
147
|
- script/release.rb
|
|
148
|
+
- test/buffer_accounting_test.rb
|
|
144
149
|
- test/cleanup_test.rb
|
|
145
150
|
- test/compare_upstream.rb
|
|
146
151
|
- test/configuration_test.rb
|
|
152
|
+
- test/deadline_test.rb
|
|
153
|
+
- test/diagnostics_test.rb
|
|
147
154
|
- test/edge_case_test.rb
|
|
148
155
|
- test/fixtures/ssh_scripts/01_identity.sh
|
|
149
156
|
- test/fixtures/ssh_scripts/02_output.sh
|
|
@@ -156,6 +163,8 @@ files:
|
|
|
156
163
|
- test/interconnect_test.rb
|
|
157
164
|
- test/io_test.rb
|
|
158
165
|
- test/kibitz_test.rb
|
|
166
|
+
- test/lifecycle_contract_test.rb
|
|
167
|
+
- test/literal_scan_test.rb
|
|
159
168
|
- test/matching_test.rb
|
|
160
169
|
- test/multi_session_test.rb
|
|
161
170
|
- test/pattern_offset_test.rb
|