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.
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(:capture, capture) do
62
- release.stub(:get, get) do
63
- release.stub(:system, push) do
64
- release.stub(:github, ->(*) { flunk "RubyGems-only release contacted GitHub" }) do
65
- release.stub(:publish_github, -> { flunk "RubyGems-only release published to GitHub" }) do
66
- [nil, "true"].each do |github_actions|
67
- with_environment("GITHUB_ACTIONS" => github_actions,
68
- "GEM_HOST_API_KEY" => (github_actions ? "test-only-api-key" : nil)) do
69
- published = false
70
- output, = capture_io { release.run }
71
- assert_includes output, "SHA256 verified"
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
- capture = ->(*arguments) { arguments.include?("rev-parse") ? "verified" : statuses.shift }
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.3.2
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-26 00:00:00.000000000 Z
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