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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +28 -19
- data/docs/API.md +88 -0
- data/docs/MIGRATION.md +15 -0
- data/lib/expect/cleanup.rb +33 -0
- data/lib/expect/configuration.rb +87 -66
- data/lib/expect/interaction.rb +226 -209
- data/lib/expect/logging.rb +157 -156
- data/lib/expect/matcher.rb +30 -29
- data/lib/expect/pattern.rb +8 -2
- data/lib/expect/pattern_list.rb +35 -5
- data/lib/expect/redactor.rb +11 -4
- data/lib/expect/relay.rb +97 -73
- data/lib/expect/relay_writer.rb +2 -1
- data/lib/expect/result.rb +32 -8
- data/lib/expect/session.rb +508 -0
- data/lib/expect/session_resources.rb +10 -2
- data/lib/expect/terminal.rb +78 -78
- data/lib/expect/version.rb +1 -1
- data/lib/expect.rb +333 -520
- data/sig/expect.rbs +355 -0
- metadata +13 -106
- data/.rubocop.yml +0 -66
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -28
- data/Rakefile +0 -38
- data/benchmark/matching.rb +0 -98
- data/benchmark/redactor.rb +0 -64
- data/benchmark/relay.rb +0 -48
- data/benchmark/scaling.rb +0 -70
- data/benchmark/send_slow.rb +0 -41
- data/benchmark/support.rb +0 -114
- data/docs/COMPATIBILITY.md +0 -98
- data/docs/INTERNAL_CONTRACTS.md +0 -154
- data/docs/PERFORMANCE.md +0 -165
- data/docs/RELEASING.md +0 -84
- data/docs/VERIFICATION.md +0 -456
- data/examples/dialogue.rb +0 -30
- data/examples/kibitz/README.md +0 -81
- data/examples/kibitz/kibitz.rb +0 -142
- data/examples/kibitz/test_kibitz.rb +0 -37
- data/examples/ssh_auto.rb +0 -94
- data/examples/ssh_interact.rb +0 -159
- data/examples/ssh_login.rb +0 -64
- data/expect-pty.gemspec +0 -33
- data/script/ci +0 -121
- data/script/release.rb +0 -319
- data/test/buffer_accounting_test.rb +0 -83
- data/test/cleanup_test.rb +0 -251
- data/test/compare_upstream.rb +0 -157
- data/test/configuration_test.rb +0 -133
- data/test/deadline_test.rb +0 -237
- data/test/diagnostics_test.rb +0 -353
- data/test/edge_case_test.rb +0 -262
- data/test/fixtures/ssh_scripts/01_identity.sh +0 -4
- data/test/fixtures/ssh_scripts/02_output.sh +0 -5
- data/test/fixtures/ssh_scripts/03_delayed.sh +0 -6
- data/test/fixtures/ssh_scripts/04_failure.sh +0 -2
- data/test/fixtures/ssh_scripts/05_recovery.sh +0 -3
- data/test/initialization_failure_test.rb +0 -92
- data/test/integration/README.md +0 -109
- data/test/integration/ssh_scripts.rb +0 -94
- data/test/interact_test.rb +0 -253
- data/test/interconnect_test.rb +0 -425
- data/test/io_test.rb +0 -321
- data/test/kibitz_test.rb +0 -45
- data/test/lifecycle_contract_test.rb +0 -71
- data/test/literal_scan_test.rb +0 -97
- data/test/matching_test.rb +0 -211
- data/test/multi_session_test.rb +0 -66
- data/test/ownership_sequence_test.rb +0 -208
- data/test/pattern_offset_test.rb +0 -43
- data/test/process_interruption_test.rb +0 -296
- data/test/process_test.rb +0 -291
- data/test/redactor_test.rb +0 -183
- data/test/relay_recovery_test.rb +0 -451
- data/test/relay_reentrancy_test.rb +0 -159
- data/test/release_test.rb +0 -375
- data/test/ruby_api_test.rb +0 -515
- data/test/scan_reuse_test.rb +0 -109
- data/test/script_logging_test.rb +0 -124
- data/test/support/interact_probe.rb +0 -125
- data/test/support/kibitz_probe.rb +0 -177
- data/test/support/script_probe.rb +0 -158
- data/test/terminal_cleanup_test.rb +0 -349
- data/test/test_helper.rb +0 -58
- data/test/timeout_test.rb +0 -289
- 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
|