expect-pty 0.5.3 → 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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +28 -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 -209
  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 +97 -73
  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 +10 -2
  19. data/lib/expect/terminal.rb +78 -78
  20. data/lib/expect/version.rb +1 -1
  21. data/lib/expect.rb +333 -520
  22. data/sig/expect.rbs +355 -0
  23. metadata +13 -106
  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 -154
  36. data/docs/PERFORMANCE.md +0 -165
  37. data/docs/RELEASING.md +0 -84
  38. data/docs/VERIFICATION.md +0 -456
  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 -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
@@ -1,159 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "test_helper"
4
- require "minitest/mock"
5
-
6
- class RelayReentrancyTest < ExpectTest
7
- def test_recursive_write_and_flush_cannot_duplicate_delivery_or_change_cursors
8
- %i[write flush].each do |entry|
9
- source, = pipe_session
10
- output = StringIO.new
11
- errors = []
12
- snapshots = []
13
- entered = false
14
- original = output.method(entry)
15
- output.define_singleton_method(entry) do |*arguments|
16
- result = original.call(*arguments)
17
- unless entered
18
- entered = true
19
- cursors = source.__send__(:relay_outputs)
20
- snapshot = -> { [source.buffer, cursors.map { |c| c.instance_variable_get(:@offset) }] }
21
- snapshots << snapshot.call
22
- begin
23
- Expect.interconnect(source, timeout: 0)
24
- rescue StandardError => error
25
- errors << error
26
- end
27
- snapshots << snapshot.call
28
- end
29
- result
30
- end
31
- source.listeners = [output]
32
- source.buffer = "abc"
33
- bounded { Expect.interconnect(source, timeout: 0) }
34
- assert_equal "abc", output.string, "recursive #{entry} replayed bytes"
35
- assert_equal(["Expect::ReentrancyError"], errors.map { |error| error.class.name })
36
- assert_equal snapshots.first, snapshots.last
37
- refute source.pending_output?
38
- end
39
- end
40
-
41
- def test_escape_callback_cannot_enter_the_same_source_but_can_use_a_matcher
42
- source, = pipe_session
43
- output = StringIO.new
44
- source.listeners = [output]
45
- calls = 0
46
- source.on_sequence("!") do
47
- calls += 1
48
- error = assert_raises(StandardError) { Expect.interconnect(source, timeout: 0) }
49
- assert_equal "Expect::ReentrancyError", error.class.name
50
- assert_equal "inside", source.expect_result("inside", timeout: 0).match
51
- false
52
- end
53
- source.buffer = "prefix!insidetail"
54
- assert_same source, Expect.interconnect(source, timeout: 1)
55
- assert_equal 1, calls
56
- assert_equal "prefix", output.string
57
- assert_equal "tail", source.buffer
58
- Expect.interconnect(source, timeout: 0)
59
- assert_equal "prefixtail", output.string
60
- end
61
-
62
- def test_partial_overlap_does_not_steal_buffers_or_release_the_outer_owner
63
- outer, = pipe_session
64
- other, = pipe_session
65
- other.buffer = "other"
66
- output = StringIO.new
67
- other.listeners = [output]
68
- outer.on_sequence("!") do
69
- 2.times do
70
- error = assert_raises(StandardError) { Expect.interconnect(other, outer, timeout: 0) }
71
- assert_equal "Expect::ReentrancyError", error.class.name
72
- assert_equal "other", other.buffer
73
- end
74
- Expect.interconnect(other, timeout: 0)
75
- false
76
- end
77
- outer.buffer = "!tail"
78
- assert_same outer, Expect.interconnect(outer, timeout: 1)
79
- assert_equal "tail", outer.buffer
80
- assert_equal "other", output.string
81
- end
82
-
83
- def test_disjoint_sources_and_duplicate_arguments_are_supported
84
- outer, = pipe_session
85
- inner, = pipe_session
86
- sink = StringIO.new
87
- inner.listeners = [sink]
88
- inner.buffer = "independent"
89
- outer.buffer = "!tail"
90
- outer.on_sequence("!") do
91
- Expect.interconnect(inner, inner, timeout: 0)
92
- false
93
- end
94
- assert_same outer, Expect.interconnect(outer, outer, timeout: 1)
95
- assert_equal "independent", sink.string
96
- assert_equal "tail", outer.buffer
97
- end
98
-
99
- def test_uncaught_reentry_propagates_and_sequential_retry_releases_the_owner
100
- source, = pipe_session
101
- sink = StringIO.new
102
- entered = false
103
- original = sink.method(:write)
104
- sink.define_singleton_method(:write) do |data|
105
- unless entered
106
- entered = true
107
- Expect.interconnect(source, timeout: 0)
108
- end
109
- original.call(data)
110
- end
111
- source.listeners = [sink]
112
- source.buffer = "once"
113
- error = assert_raises(StandardError) { Expect.interconnect(source, timeout: 0) }
114
- assert_equal "Expect::ReentrancyError", error.class.name
115
- assert source.pending_output?
116
- Expect.interconnect(source, timeout: 0)
117
- assert_equal "once", sink.string
118
- refute source.pending_output?
119
- end
120
-
121
- def test_preparation_failure_restores_all_moved_buffers_and_releases_ownership
122
- first, = pipe_session
123
- second, = pipe_session
124
- first.buffer = "first"
125
- second.buffer = "second"
126
- error = IOError.new("injected preparation failure")
127
- second.stub(:clear_buffer, -> { raise error }) do
128
- assert_same error, assert_raises(IOError) { Expect.interconnect(first, second, timeout: 0) }
129
- end
130
- assert_equal "first", first.buffer
131
- assert_equal "second", second.buffer
132
- sink = StringIO.new
133
- first.listeners = second.listeners = [sink]
134
- Expect.interconnect(first, second, timeout: 0)
135
- assert_equal "firstsecond", sink.string
136
- end
137
-
138
- def test_invalid_arguments_leave_buffers_unchanged
139
- source, = pipe_session
140
- source.buffer = "retained"
141
- assert_raises(ArgumentError) { Expect.interconnect(source, timeout: -1) }
142
- assert_raises(ArgumentError) { Expect.interconnect(source, Object.new, timeout: 0) }
143
- assert_equal "retained", source.buffer
144
- refute source.pending_output?
145
- end
146
-
147
- def test_eof_and_callback_errors_release_ownership
148
- source, producer = pipe_session
149
- failure = Interrupt.new("callback interrupted")
150
- source.on_sequence("!") { raise failure }
151
- source.buffer = "!tail"
152
- assert_same failure, assert_raises(Interrupt) { Expect.interconnect(source, timeout: 1) }
153
- producer.close
154
- sink = StringIO.new
155
- source.listeners = [sink]
156
- 2.times { assert_same source, Expect.interconnect(source, timeout: 1) }
157
- assert_equal "tail", sink.string
158
- end
159
- end
data/test/release_test.rb DELETED
@@ -1,375 +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_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
-
47
- def test_rejects_unreleased_missing_empty_or_invalid_versions
48
- ["## Unreleased\n- Pending.\n## 0.2.0\n- Ready.", "## 0.1.1\n- Old.", "## 0.2.0\n"].each do |changelog|
49
- assert_raises(RuntimeError) { Release.release_notes(changelog, "0.2.0") }
50
- end
51
- assert_raises(RuntimeError) { Release.release_notes("## 0.2.0.rc1\n- Preview.", "0.2.0.rc1") }
52
- assert_raises(RuntimeError) { Release.release_notes("## Unreleased \t\n- Pending.\n## 0.2.0\n- Ready.", "0.2.0") }
53
- end
54
-
55
- def test_dry_run_verifies_the_artifact_without_remote_calls
56
- with_package do |release, artifact|
57
- release.stub(:github, ->(*) { flunk "dry run contacted GitHub" }) do
58
- release.stub(:get, ->(*) { flunk "dry run contacted RubyGems" }) do
59
- capture_io { release.run }
60
- end
61
- end
62
- directory = Dir.glob(File.join("tmp", "release", Expect::VERSION, "candidate-*")).fetch(0)
63
- checksum = File.read(File.join(directory, "SHA256SUMS"))
64
- assert_equal "#{Digest::SHA256.file(artifact).hexdigest} #{File.basename(artifact)}\n", checksum
65
- assert_equal "- Release fixture.\n", File.read(File.join(directory, "release-notes.md"))
66
- File.write(artifact, "another build")
67
- assert_equal checksum.split.first, Digest::SHA256.file(File.join(directory, File.basename(artifact))).hexdigest
68
- end
69
- end
70
-
71
- def test_rubygems_only_publishes_the_verified_copy_without_github
72
- with_package do |_release, artifact|
73
- commit_package_source
74
- release = Release.new(artifact: artifact, rubygems_only: true)
75
- bytes = File.binread(artifact)
76
- checksum = Digest::SHA256.hexdigest(bytes)
77
- published = false
78
- get = lambda do |path|
79
- if path.start_with?("/downloads/")
80
- Struct.new(:code, :body).new("200", bytes)
81
- elsif published
82
- Struct.new(:code, :body).new("200", JSON.generate("sha" => checksum, "yanked" => false))
83
- else
84
- Struct.new(:code, :body).new("404", "")
85
- end
86
- end
87
- push = lambda do |*arguments|
88
- candidate = arguments.fetch(2)
89
- assert_equal ["gem", "push", candidate, "--host", "https://rubygems.org"], arguments
90
- assert_match %r{/tmp/release/#{Regexp.escape(Expect::VERSION)}/candidate-[^/]+/}, candidate
91
- assert_equal bytes, File.binread(candidate)
92
- refute_equal File.expand_path(artifact), candidate
93
- published = true
94
- end
95
- release.stub(:get, get) do
96
- release.stub(:system, push) do
97
- release.stub(:github, ->(*) { flunk "RubyGems-only release contacted GitHub" }) do
98
- release.stub(:publish_github, -> { flunk "RubyGems-only release published to GitHub" }) do
99
- [nil, "true"].each do |github_actions|
100
- with_environment("GITHUB_ACTIONS" => github_actions,
101
- "GEM_HOST_API_KEY" => (github_actions ? "test-only-api-key" : nil)) do
102
- published = false
103
- output, = capture_io { release.run }
104
- assert_includes output, "SHA256 verified"
105
- assert published
106
- end
107
- end
108
- end
109
- end
110
- end
111
- end
112
- end
113
- end
114
-
115
- def test_ci_publish_requires_an_api_key_before_pushing
116
- release = Release.new(rubygems_only: true)
117
- release.stub(:registry_version, nil) do
118
- release.stub(:system, ->(*) { flunk "pushed without a CI API key" }) do
119
- [nil, ""].each do |api_key|
120
- with_environment("GITHUB_ACTIONS" => "true", "GEM_HOST_API_KEY" => api_key) do
121
- error = assert_raises(RuntimeError) { release.send(:publish_rubygems) }
122
- assert_includes error.message, "RUBYGEMS_API_KEY"
123
- end
124
- end
125
- end
126
- end
127
- end
128
-
129
- def test_rubygems_only_rejects_uncommitted_source_before_publishing
130
- with_package do |_release, artifact|
131
- release = Release.new(artifact: artifact, rubygems_only: true)
132
- release.stub(:capture, " M payload.rb") do
133
- release.stub(:get, ->(*) { flunk "uncommitted source contacted RubyGems" }) do
134
- assert_match "Commit all source changes", assert_raises(RuntimeError) { release.run }.message
135
- end
136
- end
137
- end
138
- end
139
-
140
- def test_rubygems_only_rechecks_source_after_verification
141
- with_package do |_release, artifact|
142
- commit_package_source
143
- release = Release.new(artifact: artifact, rubygems_only: true)
144
- statuses = ["", " M payload.rb"]
145
- original = release.method(:capture)
146
- capture = ->(*arguments) { arguments.include?("rev-parse") ? original.call(*arguments) : statuses.shift }
147
- release.stub(:capture, capture) do
148
- release.stub(:get, ->(*) { flunk "changed source contacted RubyGems" }) do
149
- capture_io do
150
- assert_match "Source changed", assert_raises(RuntimeError) { release.run }.message
151
- end
152
- end
153
- end
154
- end
155
- end
156
-
157
- def test_rejects_a_package_from_different_source
158
- with_package do |release, _artifact|
159
- File.write("payload.rb", "puts :changed\n")
160
- assert_match "Artifact differs from source", assert_raises(RuntimeError) { release.send(:verify_package) }.message
161
- end
162
- end
163
-
164
- def test_rejects_ignored_source_collected_by_the_package_glob
165
- with_package do |_release, artifact|
166
- commit_package_source
167
- FileUtils.mkdir_p("lib")
168
- File.write("lib/local_only.rb", "puts :local_only\n")
169
- File.write(".git/info/exclude", "lib/local_only.rb\n", mode: "a")
170
- rebuild_package
171
- assert_empty git("status", "--porcelain")
172
-
173
- error = publishing_source_error(artifact)
174
- assert_match "not in release commit: lib/local_only.rb", error.message
175
- # 提交前的 dry-run 仍可检验候选包,但不能以此证明发布来源。
176
- capture_io { Release.new(artifact: artifact, dry_run: true).run }
177
- end
178
- end
179
-
180
- def test_rejects_source_changes_hidden_from_git_status
181
- with_package do |_release, artifact|
182
- commit_package_source
183
- git("update-index", "--assume-unchanged", "payload.rb")
184
- File.binwrite("payload.rb", "puts :changed\n\n")
185
- rebuild_package
186
- assert_empty git("status", "--porcelain")
187
- assert_match "differs from release commit: payload.rb", publishing_source_error(artifact).message
188
- end
189
- end
190
-
191
- def test_rejects_executable_mode_changes_hidden_from_git_status
192
- with_package do |_release, artifact|
193
- commit_package_source
194
- git("config", "core.filemode", "false")
195
- File.chmod(0o755, "payload.rb")
196
- rebuild_package
197
- assert_empty git("status", "--porcelain")
198
- assert_match "permissions differ from release commit: payload.rb", publishing_source_error(artifact).message
199
- end
200
- end
201
-
202
- def test_rejects_altered_homepage_metadata
203
- with_package do |release, _artifact|
204
- spec = Gem::Specification.load(File.expand_path("expect-pty.gemspec")).dup
205
- spec.homepage = "https://example.invalid/altered"
206
- capture_io { Gem::Package.build(spec) }
207
- assert_match "metadata", assert_raises(RuntimeError) { release.send(:verify_package) }.message
208
- end
209
- end
210
-
211
- def test_rejects_different_installation_metadata_even_with_identical_source_files
212
- with_package do |release, _artifact|
213
- expected = Gem::Specification.load(File.expand_path("expect-pty.gemspec")).dup
214
- expected.add_runtime_dependency "unexpected-dependency", "= 1.0.0"
215
- Gem::Specification.stub(:load, expected) do
216
- assert_match "metadata", assert_raises(RuntimeError) { release.send(:verify_package) }.message
217
- end
218
- expected = Gem::Specification.load(File.expand_path("expect-pty.gemspec")).dup
219
- expected.extensions = ["payload.rb"]
220
- Gem::Specification.stub(:load, expected) do
221
- assert_match "metadata", assert_raises(RuntimeError) { release.send(:verify_package) }.message
222
- end
223
- end
224
- end
225
-
226
- def test_registry_errors_are_not_treated_as_an_unpublished_version
227
- release = Release.new
228
- response = Struct.new(:code, :body).new("503", "unavailable")
229
- release.stub(:get, response) { assert_raises(RuntimeError) { release.send(:registry_version) } }
230
- end
231
-
232
- def test_rejects_overwriting_a_published_version_or_reusing_a_yanked_version
233
- release = Release.new
234
- release.instance_variable_set(:@sha256, "expected")
235
- [{ "sha" => "different", "yanked" => false }, { "sha" => "expected", "yanked" => true }].each do |version|
236
- assert_raises(RuntimeError) { release.send(:verify_registry_checksum, version) }
237
- end
238
- end
239
-
240
- def test_existing_registry_version_is_downloaded_and_verified_without_pushing_again
241
- release = Release.new
242
- bytes = "published gem"
243
- checksum = Digest::SHA256.hexdigest(bytes)
244
- release.instance_variable_set(:@sha256, checksum)
245
- response = Struct.new(:code, :body).new("200", bytes)
246
- release.stub(:registry_version, { "sha" => checksum, "yanked" => false }) do
247
- release.stub(:system, ->(*) { flunk "repushed an existing gem" }) do
248
- release.stub(:get, response) { capture_io { release.send(:publish_rubygems) } }
249
- response.body = "different bytes"
250
- release.stub(:get, response) { assert_raises(RuntimeError) { release.send(:publish_rubygems) } }
251
- end
252
- end
253
- end
254
-
255
- def test_remote_tag_must_point_to_the_verified_commit
256
- release = Release.new
257
- release.instance_variable_set(:@commit, "verified")
258
- capture = ->(*arguments) { arguments.include?("rev-parse") ? "verified" : "" }
259
- github = lambda do |path, **|
260
- { "status" => (path.end_with?("...main") ? "ahead" : "behind") }
261
- end
262
- release.stub(:capture, capture) do
263
- release.stub(:github, github) do
264
- assert_match "another commit", assert_raises(RuntimeError) { release.send(:verify_remote_source) }.message
265
- end
266
- end
267
- end
268
-
269
- def test_package_validation_uses_archive_modes_regardless_of_umask
270
- with_package do |release, artifact|
271
- File.chmod(0o755, "payload.rb")
272
- capture_io { Gem::Package.build(Gem::Specification.load(File.expand_path("expect-pty.gemspec"))) }
273
- previous_umask = File.umask(0o077)
274
- begin
275
- release.send(:verify_package)
276
- assert File.file?(artifact)
277
- File.chmod(0o644, "payload.rb")
278
- assert_match "permissions differ", assert_raises(RuntimeError) { release.send(:verify_package) }.message
279
- ensure
280
- File.umask(previous_umask)
281
- end
282
- end
283
- end
284
-
285
- def test_incomplete_github_asset_has_an_explicit_recovery_message
286
- release = Release.new
287
- release.instance_variable_set(:@checksum_file, "SHA256SUMS")
288
- remote = { "assets" => [{ "name" => "SHA256SUMS", "state" => "starter" }] }
289
- release.stub(:github_release, remote) do
290
- error = assert_raises(RuntimeError) { release.send(:publish_github) }
291
- assert_match "Incomplete GitHub asset: SHA256SUMS", error.message
292
- assert_match "stop any active upload", error.message
293
- end
294
- end
295
-
296
- private
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
-
311
- def git(*)
312
- output, error, status = Open3.capture3("git", *)
313
- assert status.success?, error
314
- output
315
- end
316
-
317
- def commit_package_source
318
- git("init", "--quiet")
319
- git("config", "core.hooksPath", File::NULL)
320
- File.write(".git/info/exclude", "*.gem\ntmp/\n")
321
- git("add", "--", "payload.rb", "CHANGELOG.md", "expect-pty.gemspec")
322
- git("-c", "user.name=Release Test", "-c", "user.email=release-test@example.invalid",
323
- "-c", "commit.gpgsign=false", "commit", "--quiet", "-m", "Release fixture")
324
- end
325
-
326
- def rebuild_package
327
- # Gem::Specification.load 会缓存已加载的 gemspec;重新执行才能看到新的 glob 文件。
328
- Gem::Specification.reset
329
- spec = Gem::Specification.load(File.expand_path("expect-pty.gemspec"))
330
- capture_io { Gem::Package.build(spec) }
331
- end
332
-
333
- def publishing_source_error(artifact)
334
- release = Release.new(artifact: artifact, rubygems_only: true)
335
- release.stub(:get, ->(*) { flunk "uncommitted package content reached RubyGems" }) do
336
- error = nil
337
- capture_io { error = assert_raises(RuntimeError) { release.run } }
338
- error
339
- end
340
- end
341
-
342
- # 发布测试显式控制凭据环境,避免本机登录状态或 CI 标记影响用例结果。
343
- def with_environment(values)
344
- previous = values.to_h { |name, _value| [name, ENV.fetch(name, nil)] }
345
- begin
346
- values.each { |name, value| ENV[name] = value }
347
- yield
348
- ensure
349
- previous.each { |name, value| ENV[name] = value }
350
- end
351
- end
352
-
353
- def with_package
354
- Dir.mktmpdir("expect-release-test-") do |directory|
355
- Dir.chdir(directory) do
356
- File.write("payload.rb", "puts :original\n")
357
- File.write("CHANGELOG.md", "## #{Expect::VERSION}\n\n- Release fixture.\n")
358
- File.write("expect-pty.gemspec", <<~RUBY)
359
- Gem::Specification.new do |spec|
360
- spec.name = "expect-pty"
361
- spec.version = "#{Expect::VERSION}"
362
- spec.summary = "Release test fixture"
363
- spec.authors = ["Test"]
364
- spec.license = "MIT"
365
- spec.homepage = "https://github.com/gatework/expect-ruby"
366
- spec.files = ["payload.rb", "CHANGELOG.md", "expect-pty.gemspec"] + Dir["lib/**/*.rb"]
367
- end
368
- RUBY
369
- artifact = nil
370
- capture_io { artifact = Gem::Package.build(Gem::Specification.load(File.expand_path("expect-pty.gemspec"))) }
371
- yield Release.new(artifact: artifact, dry_run: true), artifact
372
- end
373
- end
374
- end
375
- end