expect-pty 0.5.2 → 0.6.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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/README.md +37 -19
  4. data/docs/API.md +88 -0
  5. data/docs/MIGRATION.md +15 -0
  6. data/lib/expect/cleanup.rb +33 -0
  7. data/lib/expect/configuration.rb +87 -66
  8. data/lib/expect/interaction.rb +226 -207
  9. data/lib/expect/logging.rb +157 -156
  10. data/lib/expect/matcher.rb +30 -29
  11. data/lib/expect/pattern.rb +8 -2
  12. data/lib/expect/pattern_list.rb +35 -5
  13. data/lib/expect/redactor.rb +11 -4
  14. data/lib/expect/relay.rb +122 -68
  15. data/lib/expect/relay_writer.rb +2 -1
  16. data/lib/expect/result.rb +32 -8
  17. data/lib/expect/session.rb +508 -0
  18. data/lib/expect/session_resources.rb +49 -16
  19. data/lib/expect/terminal.rb +78 -78
  20. data/lib/expect/version.rb +1 -1
  21. data/lib/expect.rb +336 -486
  22. data/sig/expect.rbs +355 -0
  23. metadata +12 -100
  24. data/.rubocop.yml +0 -66
  25. data/CONTRIBUTING.md +0 -29
  26. data/Gemfile +0 -28
  27. data/Rakefile +0 -38
  28. data/benchmark/matching.rb +0 -98
  29. data/benchmark/redactor.rb +0 -64
  30. data/benchmark/relay.rb +0 -48
  31. data/benchmark/scaling.rb +0 -70
  32. data/benchmark/send_slow.rb +0 -41
  33. data/benchmark/support.rb +0 -114
  34. data/docs/COMPATIBILITY.md +0 -98
  35. data/docs/INTERNAL_CONTRACTS.md +0 -133
  36. data/docs/PERFORMANCE.md +0 -165
  37. data/docs/RELEASING.md +0 -84
  38. data/docs/VERIFICATION.md +0 -358
  39. data/examples/dialogue.rb +0 -30
  40. data/examples/kibitz/README.md +0 -81
  41. data/examples/kibitz/kibitz.rb +0 -142
  42. data/examples/kibitz/test_kibitz.rb +0 -37
  43. data/examples/ssh_auto.rb +0 -94
  44. data/examples/ssh_interact.rb +0 -159
  45. data/examples/ssh_login.rb +0 -64
  46. data/expect-pty.gemspec +0 -33
  47. data/script/ci +0 -121
  48. data/script/release.rb +0 -311
  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/integration/README.md +0 -109
  62. data/test/integration/ssh_scripts.rb +0 -94
  63. data/test/interact_test.rb +0 -253
  64. data/test/interconnect_test.rb +0 -425
  65. data/test/io_test.rb +0 -321
  66. data/test/kibitz_test.rb +0 -45
  67. data/test/lifecycle_contract_test.rb +0 -71
  68. data/test/literal_scan_test.rb +0 -97
  69. data/test/matching_test.rb +0 -211
  70. data/test/multi_session_test.rb +0 -66
  71. data/test/pattern_offset_test.rb +0 -43
  72. data/test/process_test.rb +0 -291
  73. data/test/redactor_test.rb +0 -183
  74. data/test/relay_recovery_test.rb +0 -451
  75. data/test/release_test.rb +0 -328
  76. data/test/ruby_api_test.rb +0 -515
  77. data/test/scan_reuse_test.rb +0 -109
  78. data/test/script_logging_test.rb +0 -124
  79. data/test/support/interact_probe.rb +0 -125
  80. data/test/support/kibitz_probe.rb +0 -177
  81. data/test/support/script_probe.rb +0 -158
  82. data/test/terminal_cleanup_test.rb +0 -349
  83. data/test/test_helper.rb +0 -58
  84. data/test/timeout_test.rb +0 -289
data/test/release_test.rb DELETED
@@ -1,328 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "minitest/autorun"
4
- require "minitest/mock"
5
- require_relative "../script/release"
6
-
7
- class ReleaseTest < Minitest::Test
8
- def test_extracts_only_the_selected_version
9
- changelog = "# Changelog\n\n## Unreleased\n\n## 0.2.0 - 2026-09-12\n\n- New API.\n\n## 0.1.1\n\n- Old API.\n"
10
- assert_equal "- New API.", Release.release_notes(changelog, "0.2.0")
11
- end
12
-
13
- def test_rejects_unreleased_missing_empty_or_invalid_versions
14
- ["## Unreleased\n- Pending.\n## 0.2.0\n- Ready.", "## 0.1.1\n- Old.", "## 0.2.0\n"].each do |changelog|
15
- assert_raises(RuntimeError) { Release.release_notes(changelog, "0.2.0") }
16
- end
17
- assert_raises(RuntimeError) { Release.release_notes("## 0.2.0.rc1\n- Preview.", "0.2.0.rc1") }
18
- assert_raises(RuntimeError) { Release.release_notes("## Unreleased \t\n- Pending.\n## 0.2.0\n- Ready.", "0.2.0") }
19
- end
20
-
21
- def test_dry_run_verifies_the_artifact_without_remote_calls
22
- with_package do |release, artifact|
23
- release.stub(:github, ->(*) { flunk "dry run contacted GitHub" }) do
24
- release.stub(:get, ->(*) { flunk "dry run contacted RubyGems" }) do
25
- capture_io { release.run }
26
- end
27
- end
28
- directory = Dir.glob(File.join("tmp", "release", Expect::VERSION, "candidate-*")).fetch(0)
29
- checksum = File.read(File.join(directory, "SHA256SUMS"))
30
- assert_equal "#{Digest::SHA256.file(artifact).hexdigest} #{File.basename(artifact)}\n", checksum
31
- assert_equal "- Release fixture.\n", File.read(File.join(directory, "release-notes.md"))
32
- File.write(artifact, "another build")
33
- assert_equal checksum.split.first, Digest::SHA256.file(File.join(directory, File.basename(artifact))).hexdigest
34
- end
35
- end
36
-
37
- def test_rubygems_only_publishes_the_verified_copy_without_github
38
- with_package do |_release, artifact|
39
- commit_package_source
40
- release = Release.new(artifact: artifact, rubygems_only: true)
41
- bytes = File.binread(artifact)
42
- checksum = Digest::SHA256.hexdigest(bytes)
43
- published = false
44
- get = lambda do |path|
45
- if path.start_with?("/downloads/")
46
- Struct.new(:code, :body).new("200", bytes)
47
- elsif published
48
- Struct.new(:code, :body).new("200", JSON.generate("sha" => checksum, "yanked" => false))
49
- else
50
- Struct.new(:code, :body).new("404", "")
51
- end
52
- end
53
- push = lambda do |*arguments|
54
- candidate = arguments.fetch(2)
55
- assert_equal ["gem", "push", candidate, "--host", "https://rubygems.org"], arguments
56
- assert_match %r{/tmp/release/#{Regexp.escape(Expect::VERSION)}/candidate-[^/]+/}, candidate
57
- assert_equal bytes, File.binread(candidate)
58
- refute_equal File.expand_path(artifact), candidate
59
- published = true
60
- 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
72
- end
73
- end
74
- end
75
- end
76
- end
77
- end
78
- end
79
- end
80
-
81
- def test_ci_publish_requires_an_api_key_before_pushing
82
- release = Release.new(rubygems_only: true)
83
- release.stub(:registry_version, nil) do
84
- release.stub(:system, ->(*) { flunk "pushed without a CI API key" }) do
85
- [nil, ""].each do |api_key|
86
- with_environment("GITHUB_ACTIONS" => "true", "GEM_HOST_API_KEY" => api_key) do
87
- error = assert_raises(RuntimeError) { release.send(:publish_rubygems) }
88
- assert_includes error.message, "RUBYGEMS_API_KEY"
89
- end
90
- end
91
- end
92
- end
93
- end
94
-
95
- def test_rubygems_only_rejects_uncommitted_source_before_publishing
96
- with_package do |_release, artifact|
97
- release = Release.new(artifact: artifact, rubygems_only: true)
98
- release.stub(:capture, " M payload.rb") do
99
- release.stub(:get, ->(*) { flunk "uncommitted source contacted RubyGems" }) do
100
- assert_match "Commit all source changes", assert_raises(RuntimeError) { release.run }.message
101
- end
102
- end
103
- end
104
- end
105
-
106
- def test_rubygems_only_rechecks_source_after_verification
107
- with_package do |_release, artifact|
108
- commit_package_source
109
- release = Release.new(artifact: artifact, rubygems_only: true)
110
- statuses = ["", " M payload.rb"]
111
- original = release.method(:capture)
112
- capture = ->(*arguments) { arguments.include?("rev-parse") ? original.call(*arguments) : statuses.shift }
113
- release.stub(:capture, capture) do
114
- release.stub(:get, ->(*) { flunk "changed source contacted RubyGems" }) do
115
- capture_io do
116
- assert_match "Source changed", assert_raises(RuntimeError) { release.run }.message
117
- end
118
- end
119
- end
120
- end
121
- end
122
-
123
- def test_rejects_a_package_from_different_source
124
- with_package do |release, _artifact|
125
- File.write("payload.rb", "puts :changed\n")
126
- assert_match "Artifact differs from source", assert_raises(RuntimeError) { release.send(:verify_package) }.message
127
- end
128
- end
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
-
177
- def test_rejects_different_installation_metadata_even_with_identical_source_files
178
- with_package do |release, _artifact|
179
- expected = Gem::Specification.load(File.expand_path("expect-pty.gemspec")).dup
180
- expected.add_runtime_dependency "unexpected-dependency", "= 1.0.0"
181
- Gem::Specification.stub(:load, expected) do
182
- assert_match "metadata", assert_raises(RuntimeError) { release.send(:verify_package) }.message
183
- end
184
- expected = Gem::Specification.load(File.expand_path("expect-pty.gemspec")).dup
185
- expected.extensions = ["payload.rb"]
186
- Gem::Specification.stub(:load, expected) do
187
- assert_match "metadata", assert_raises(RuntimeError) { release.send(:verify_package) }.message
188
- end
189
- end
190
- end
191
-
192
- def test_registry_errors_are_not_treated_as_an_unpublished_version
193
- release = Release.new
194
- response = Struct.new(:code, :body).new("503", "unavailable")
195
- release.stub(:get, response) { assert_raises(RuntimeError) { release.send(:registry_version) } }
196
- end
197
-
198
- def test_rejects_overwriting_a_published_version_or_reusing_a_yanked_version
199
- release = Release.new
200
- release.instance_variable_set(:@sha256, "expected")
201
- [{ "sha" => "different", "yanked" => false }, { "sha" => "expected", "yanked" => true }].each do |version|
202
- assert_raises(RuntimeError) { release.send(:verify_registry_checksum, version) }
203
- end
204
- end
205
-
206
- def test_existing_registry_version_is_downloaded_and_verified_without_pushing_again
207
- release = Release.new
208
- bytes = "published gem"
209
- checksum = Digest::SHA256.hexdigest(bytes)
210
- release.instance_variable_set(:@sha256, checksum)
211
- response = Struct.new(:code, :body).new("200", bytes)
212
- release.stub(:registry_version, { "sha" => checksum, "yanked" => false }) do
213
- release.stub(:system, ->(*) { flunk "repushed an existing gem" }) do
214
- release.stub(:get, response) { capture_io { release.send(:publish_rubygems) } }
215
- response.body = "different bytes"
216
- release.stub(:get, response) { assert_raises(RuntimeError) { release.send(:publish_rubygems) } }
217
- end
218
- end
219
- end
220
-
221
- def test_remote_tag_must_point_to_the_verified_commit
222
- release = Release.new
223
- release.instance_variable_set(:@commit, "verified")
224
- capture = ->(*arguments) { arguments.include?("rev-parse") ? "verified" : "" }
225
- github = lambda do |path, **|
226
- { "status" => (path.end_with?("...main") ? "ahead" : "behind") }
227
- end
228
- release.stub(:capture, capture) do
229
- release.stub(:github, github) do
230
- assert_match "another commit", assert_raises(RuntimeError) { release.send(:verify_remote_source) }.message
231
- end
232
- end
233
- end
234
-
235
- def test_package_validation_uses_archive_modes_regardless_of_umask
236
- with_package do |release, artifact|
237
- File.chmod(0o755, "payload.rb")
238
- capture_io { Gem::Package.build(Gem::Specification.load(File.expand_path("expect-pty.gemspec"))) }
239
- previous_umask = File.umask(0o077)
240
- begin
241
- release.send(:verify_package)
242
- assert File.file?(artifact)
243
- File.chmod(0o644, "payload.rb")
244
- assert_match "permissions differ", assert_raises(RuntimeError) { release.send(:verify_package) }.message
245
- ensure
246
- File.umask(previous_umask)
247
- end
248
- end
249
- end
250
-
251
- def test_incomplete_github_asset_has_an_explicit_recovery_message
252
- release = Release.new
253
- release.instance_variable_set(:@checksum_file, "SHA256SUMS")
254
- remote = { "assets" => [{ "name" => "SHA256SUMS", "state" => "starter" }] }
255
- release.stub(:github_release, remote) do
256
- error = assert_raises(RuntimeError) { release.send(:publish_github) }
257
- assert_match "Incomplete GitHub asset: SHA256SUMS", error.message
258
- assert_match "stop any active upload", error.message
259
- end
260
- end
261
-
262
- private
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
-
295
- # 发布测试显式控制凭据环境,避免本机登录状态或 CI 标记影响用例结果。
296
- def with_environment(values)
297
- previous = values.to_h { |name, _value| [name, ENV.fetch(name, nil)] }
298
- begin
299
- values.each { |name, value| ENV[name] = value }
300
- yield
301
- ensure
302
- previous.each { |name, value| ENV[name] = value }
303
- end
304
- end
305
-
306
- def with_package
307
- Dir.mktmpdir("expect-release-test-") do |directory|
308
- Dir.chdir(directory) do
309
- File.write("payload.rb", "puts :original\n")
310
- File.write("CHANGELOG.md", "## #{Expect::VERSION}\n\n- Release fixture.\n")
311
- File.write("expect-pty.gemspec", <<~RUBY)
312
- Gem::Specification.new do |spec|
313
- spec.name = "expect-pty"
314
- spec.version = "#{Expect::VERSION}"
315
- spec.summary = "Release test fixture"
316
- spec.authors = ["Test"]
317
- spec.license = "MIT"
318
- spec.homepage = "https://github.com/gatework/expect-ruby"
319
- spec.files = ["payload.rb", "CHANGELOG.md", "expect-pty.gemspec"] + Dir["lib/**/*.rb"]
320
- end
321
- RUBY
322
- artifact = nil
323
- capture_io { artifact = Gem::Package.build(Gem::Specification.load(File.expand_path("expect-pty.gemspec"))) }
324
- yield Release.new(artifact: artifact, dry_run: true), artifact
325
- end
326
- end
327
- end
328
- end