Skip to content

Commit aa7f984

Browse files
authored
Verify content address test coverage (#186)
* Add edge case tests for Gem::ContentAddress - test_match: add realistic SHA-256 prefix '78be552b', empty string, non-String types (Integer, Symbol), and whitespace-padded hex - test_content_addressed_with_eligible_spec_and_invalid_address: eligible spec with non-hex content_address should not be considered content-addressed Assisted-By: devx/25d3c4be-88ab-425e-a76b-08a00d8e9d71 * Add edge case tests for gem build --ruby-abi and rename test_handle_options - test_ruby_abi_rejects_invalid_format: '3', '3.4.1', 'abc', '3.x' raise - test_ruby_abi_rejects_ruby_platform: no platform set raises - test_ruby_abi_rejects_nil_platform: nil platform raises - test_ruby_abi_rejects_mismatched_required_ruby_version: spec with ~> 3.3.0 and --ruby-abi 3.4 raises - test_ruby_abi_defaults_required_ruby_version_when_unset: no RRV set defaults to ~> 3.4.0, verified from built gem spec - test_ruby_abi_with_output_raises: both --output and --ruby-abi raises - test_ruby_abi_hash_is_deterministic: two builds produce same SHA - Rename test_handle_options to test_handle_options_force_strict_platform Assisted-By: devx/25d3c4be-88ab-425e-a76b-08a00d8e9d71 * Add edge case tests for gem push selectors - test_execute_with_both_selectors_raises_when_multiple_gems_match_without_suggestion: both selectors set, multiple matches, verifies no suggestion is appended - test_execute_with_both_selectors_selects_single_matching_gem: single gem matching both --platform and --ruby-abi pushes successfully - test_execute_with_platform_selector_selects_single_non_content_addressable_gem: single non-content-addressable gem with --platform only pushes - test_execute_with_ruby_abi_selector_matches_source_gem: RUBY-platform gem with ~> 3.4.0 matched by --ruby-abi, documenting ruby_matches? doesn't check platform - Rename fat/skinny/broad references in existing tests to non_content_addressable and content_addressable Assisted-By: devx/25d3c4be-88ab-425e-a76b-08a00d8e9d71 * Add tests for built CA gem spec metadata and RRV stability - test_ruby_abi_built_gem_preserves_spec_metadata: reads back built CA gem with Gem::Package.new(file).spec, verifies name, version, platform, required_ruby_version, and ruby_abi are preserved - test_required_ruby_version_unchanged_after_successful_matching_build: builds with matching ~> 3.4.0 and --ruby-abi 3.4, asserts RRV unchanged Assisted-By: devx/25d3c4be-88ab-425e-a76b-08a00d8e9d71 * Add cache file verification to CA installer test - test_install_assigns_content_address_from_filename: assert the gem is cached under the CA filename (cache/a-2-<address>.gem) in addition to gems/ and specifications/ paths Assisted-By: devx/25d3c4be-88ab-425e-a76b-08a00d8e9d71 * Add source gem to mixed CA and platform display test - test_execute_remote_content_addressable_and_platform_gems_display_together: add a source gem (version 4, RUBY platform) alongside CA and platform gems to verify it displays as bare version without platform info Assisted-By: devx/25d3c4be-88ab-425e-a76b-08a00d8e9d71 * Add missing CA display test cases for gem search - test_execute_content_addressable_gems_displays_ruby_abis_next_to_their_platforms: same version, different platforms/ABIs on separate lines - test_execute_content_addressable_gems_displays_multiple_ruby_abis_on_the_same_line: same version+platform, different ABIs grouped - test_execute_content_addressable_gems_displays_multiple_versions_on_separate_lines: multiple versions each on own line - test_execute_platform_gem_displays_version_once_for_multiple_platforms: platform gem with multiple platforms shows version once - test_execute_content_addressable_platform_and_source_gems_display_together: mixed CA, platform, and source gems display together Assisted-By: devx/25d3c4be-88ab-425e-a76b-08a00d8e9d71 * Add missing CA display test cases for gem info - test_execute_remote_content_addressable_gem_displays_multiple_ruby_abis_on_same_platform: same version+platform, different ABIs grouped on one line - test_execute_remote_content_addressable_platform_and_source_gems_display_together: mixed CA, platform, and source gems; source gem appears in header but not in Platforms section - test_execute_remote_platform_gem_displays_version_once_for_multiple_platforms: platform gem with multiple platforms uses comma-separated display format Assisted-By: devx/25d3c4be-88ab-425e-a76b-08a00d8e9d71 * Add tests for mixed CA/platform entries and missing ruby field - test_load_specs_compact_index_decodes_mixed_content_addressable_and_platform_entries: versions list with both CA hash and platform suffix; verifies both returned, CA entry has content_address and ruby_abi, platform entry does not - test_load_specs_compact_index_content_addressable_without_ruby_field: /info/ metadata with platform:= but no ruby: field; verifies ruby_abi is nil while platform and content_address are still set Assisted-By: devx/25d3c4be-88ab-425e-a76b-08a00d8e9d71 * Add CA resolver tests and rename fat to non_content_addressable - test_prefers_compatible_content_addressed_gem_when_multiple_abis_available: two CA gems with different ABIs, one incompatible, verifies compatible one is selected - test_raises_when_only_content_addressed_gem_is_incompatible: only CA gem with incompatible required_ruby_version, no fallback, verifies DependencyResolutionError is raised - Rename test_falls_back_to_fat_* to test_falls_back_to_non_content_addressable_* and fat_spec to non_content_addressable_spec Assisted-By: devx/25d3c4be-88ab-425e-a76b-08a00d8e9d71 * Add test for ruby_abi with default required_ruby_version - test_ruby_abi_returns_nil_for_default_required_ruby_version: a fresh spec with default required_ruby_version (>= 0) returns nil for ruby_abi, covering the most common case of gems without RRV Assisted-By: devx/25d3c4be-88ab-425e-a76b-08a00d8e9d71 * Add CA spec_name and equality tests, rename fat to non_content_addressable - test_content_addressable_spec_name: verifies spec_name uses CA suffix (a-1-abcdef12.gemspec) - test_content_addressable_tuples_with_different_addresses_are_distinct: verifies == includes content_address but hash does not (based on to_a), documenting hash collision behavior - Rename test_fat_tuple_* to test_non_content_addressable_tuple_* and fat/skinny variables to non_content_addressable/content_addressable Assisted-By: devx/25d3c4be-88ab-425e-a76b-08a00d8e9d71 * Add test for all CA gems incompatible and rename fat in description - Add test for two CA gems with different mismatched ABIs plus non-CA platform fallback, verifies non-CA gem is installed - Rename 'fat' to remove it from test #7 description for consistency Assisted-By: devx/25d3c4be-88ab-425e-a76b-08a00d8e9d71 * Add test for CA gem yank with no matching ABI - test_execute_with_ruby_abi_and_platform_no_matching_gem_displays_error: verifies yank with both --platform and --ruby-abi sends all params to API and displays the 404 error message from rubygems.org Assisted-By: devx/2658d4ad-be37-4929-8dda-b1d772952c7b * Add tests for full SHA256 content address and CA gem coexistence - test_install_assigns_content_address_from_filename_with_full_sha: verifies installer accepts full 64-char SHA256 as content address - test_two_content_addressed_gems_with_same_name_version_coexist: verifies two gems with same name/version/platform but different content addresses install to distinct directories and gemspecs without overwriting Assisted-By: devx/2658d4ad-be37-4929-8dda-b1d772952c7b
1 parent 2f3b567 commit aa7f984

16 files changed

Lines changed: 845 additions & 35 deletions

lib/rubygems/commands/push_command.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,9 @@ def platform_matches?(spec)
170170
end
171171

172172
def ruby_matches?(spec)
173-
!options[:ruby_abi] || spec.ruby_abi == options[:ruby_abi]
173+
return true unless options[:ruby_abi]
174+
175+
Gem::ContentAddress.applicable?(spec) && spec.ruby_abi == options[:ruby_abi]
174176
end
175177

176178
def send_push_request(name, args)

lib/rubygems/source.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ def content_addressable_metadata(name, rows)
406406
requirements = compact_index_requirements(info_row)
407407
platform = required_platform_from(requirements[:platform])
408408
next unless platform
409+
next unless requirements[:ruby]
409410

410411
ContentAddressableInfo.new(version, suffix, ruby_abi_from(requirements[:ruby]), platform)
411412
end

spec/install/gemfile/content_addressable_spec.rb

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@
168168
end
169169
end
170170

171-
it "falls back to the pure-ruby gem when the content-addressed fat gem requires a different Ruby ABI" do
171+
it "falls back to the pure-ruby gem when the content-addressed gem requires a different Ruby ABI" do
172172
simulate_platform "x86_64-linux" do
173173
build_repo2 do
174174
build_gem "mygem", "1.0" do |s|
@@ -242,4 +242,96 @@
242242
expect(the_bundle).to include_gems "mygem 2.0 not_content_addressed"
243243
end
244244
end
245+
246+
it "falls back to the non-content-addressed gem when all content-addressed gems require a different Ruby ABI" do
247+
simulate_platform "x86_64-linux" do
248+
build_repo2 do
249+
build_gem "mygem", "1.0" do |s|
250+
s.platform = Gem::Platform.new("x86_64-linux")
251+
s.write "lib/mygem.rb", "MYGEM = '1.0 not_content_addressed'"
252+
end
253+
end
254+
255+
build_gem "mygem", "1.0", ruby_abi: mismatched_abi, path: gem_repo2("gems") do |s|
256+
s.platform = Gem::Platform.new("x86_64-linux")
257+
s.required_ruby_version = "~> #{mismatched_abi}.0"
258+
s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed_mismatched_abi_1'"
259+
end
260+
261+
second_mismatched_abi = "#{Gem.ruby_version.segments[0] + 2}.0"
262+
build_gem "mygem", "1.0", ruby_abi: second_mismatched_abi, path: gem_repo2("gems") do |s|
263+
s.platform = Gem::Platform.new("x86_64-linux")
264+
s.required_ruby_version = "~> #{second_mismatched_abi}.0"
265+
s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed_mismatched_abi_2'"
266+
end
267+
268+
install_gemfile <<~G, artifice: "compact_index_v2", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
269+
source "https://gem.repo2"
270+
271+
gem "mygem"
272+
G
273+
274+
expect(the_bundle).to include_gems "mygem 1.0 not_content_addressed"
275+
end
276+
end
277+
278+
it "installs a locked content-addressed gem in frozen mode" do
279+
simulate_platform "x86_64-linux" do
280+
build_repo2 do
281+
build_gem "mygem", "1.0" do |s|
282+
s.platform = Gem::Platform.new("x86_64-linux")
283+
s.write "lib/mygem.rb", "MYGEM = '1.0 not_content_addressed'"
284+
end
285+
end
286+
287+
build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s|
288+
s.platform = Gem::Platform.new("x86_64-linux")
289+
s.required_ruby_version = "~> #{current_abi}.0"
290+
s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'"
291+
end
292+
293+
install_gemfile <<~G, artifice: "compact_index_v2", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
294+
source "https://gem.repo2"
295+
296+
gem "mygem"
297+
G
298+
299+
expect(the_bundle).to include_gems "mygem 1.0 content_addressed"
300+
301+
pristine_system_gems
302+
bundle_config "frozen true"
303+
bundle "install", artifice: "compact_index_v2", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
304+
305+
expect(the_bundle).to include_gems "mygem 1.0 content_addressed"
306+
end
307+
end
308+
309+
it "fails when the downloaded content-addressed gem hash does not match the filename" do
310+
simulate_platform "x86_64-linux" do
311+
build_repo2 do
312+
build_gem "mygem", "1.0" do |s|
313+
s.platform = Gem::Platform.new("x86_64-linux")
314+
s.write "lib/mygem.rb", "MYGEM = '1.0 not_content_addressed'"
315+
end
316+
end
317+
318+
build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s|
319+
s.platform = Gem::Platform.new("x86_64-linux")
320+
s.required_ruby_version = "~> #{current_abi}.0"
321+
s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'"
322+
end
323+
324+
ca_gem = Dir[gem_repo2("gems", "mygem-1.0-[0-9a-f]*.gem")].first
325+
non_ca_gem = gem_repo2("gems", "mygem-1.0-x86_64-linux.gem")
326+
FileUtils.cp non_ca_gem, ca_gem
327+
328+
install_gemfile <<~G, artifice: "compact_index_v2", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }, raise_on_error: false
329+
source "https://gem.repo2"
330+
331+
gem "mygem"
332+
G
333+
334+
expect(err).to include("content address mismatch")
335+
end
336+
end
245337
end

test/rubygems/test_gem_commands_build_command.rb

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def setup
2828
@cmd = Gem::Commands::BuildCommand.new
2929
end
3030

31-
def test_handle_options
31+
def test_handle_options_force_strict_platform
3232
@cmd.handle_options %w[--force --strict]
3333

3434
assert @cmd.options[:force]
@@ -120,6 +120,156 @@ def test_execute
120120
util_test_build_gem @gem
121121
end
122122

123+
def test_ruby_abi_rejects_invalid_format
124+
gem = util_spec "platformed_gem" do |s|
125+
s.license = "AGPL-3.0-only"
126+
s.files = ["README.md"]
127+
s.platform = "arm64-darwin"
128+
end
129+
130+
gemspec_file = File.join(@tempdir, gem.spec_name)
131+
File.open gemspec_file, "w" do |gs|
132+
gs.write gem.to_ruby
133+
end
134+
135+
["3", "3.4.1", "abc", "3.x"].each do |invalid|
136+
@cmd.handle_options [gemspec_file, "--ruby-abi", invalid]
137+
error = assert_raise(ArgumentError) do
138+
use_ui @ui do
139+
Dir.chdir @tempdir do
140+
@cmd.execute
141+
end
142+
end
143+
end
144+
assert_match(/Ruby ABI must be in X\.Y format/, error.message)
145+
end
146+
end
147+
148+
def test_ruby_abi_rejects_ruby_platform
149+
gem = util_spec "some_gem" do |s|
150+
s.license = "AGPL-3.0-only"
151+
s.files = ["README.md"]
152+
end
153+
154+
gemspec_file = File.join(@tempdir, gem.spec_name)
155+
File.open gemspec_file, "w" do |gs|
156+
gs.write gem.to_ruby
157+
end
158+
159+
@cmd.handle_options [gemspec_file, "--ruby-abi", "3.4"]
160+
error = assert_raise(ArgumentError) do
161+
use_ui @ui do
162+
Dir.chdir @tempdir do
163+
@cmd.execute
164+
end
165+
end
166+
end
167+
assert_match(/no platform or a Ruby platform has been set/, error.message)
168+
end
169+
170+
def test_ruby_abi_rejects_mismatched_required_ruby_version
171+
gem = util_spec "platformed_gem" do |s|
172+
s.license = "AGPL-3.0-only"
173+
s.files = ["README.md"]
174+
s.platform = "arm64-darwin"
175+
s.required_ruby_version = "~> 3.3.0"
176+
end
177+
178+
gemspec_file = File.join(@tempdir, gem.spec_name)
179+
File.open gemspec_file, "w" do |gs|
180+
gs.write gem.to_ruby
181+
end
182+
183+
@cmd.handle_options [gemspec_file, "--ruby-abi", "3.4"]
184+
error = assert_raise(ArgumentError) do
185+
use_ui @ui do
186+
Dir.chdir @tempdir do
187+
@cmd.execute
188+
end
189+
end
190+
end
191+
assert_match(/Cannot build gem for Ruby ABI 3\.4 because required_ruby_version/, error.message)
192+
end
193+
194+
def test_ruby_abi_defaults_required_ruby_version_when_unset
195+
gem = util_spec "platformed_gem" do |s|
196+
s.license = "AGPL-3.0-only"
197+
s.files = ["README.md"]
198+
s.platform = "arm64-darwin"
199+
end
200+
201+
gemspec_file = File.join(@tempdir, gem.spec_name)
202+
File.open gemspec_file, "w" do |gs|
203+
gs.write gem.to_ruby
204+
end
205+
206+
@cmd.handle_options [gemspec_file, "--ruby-abi", "3.4"]
207+
use_ui @ui do
208+
Dir.chdir @tempdir do
209+
@cmd.execute
210+
end
211+
end
212+
213+
files = Dir[File.join(@tempdir, "platformed_gem-2-*.gem")]
214+
assert_equal 1, files.size
215+
spec = Gem::Package.new(files.first).spec
216+
assert_equal Gem::Requirement.new("~> 3.4.0"), spec.required_ruby_version
217+
end
218+
219+
def test_ruby_abi_produces_deterministic_content_address
220+
gemspec = lambda do
221+
gem = util_spec "platformed_gem" do |s|
222+
s.license = "AGPL-3.0-only"
223+
s.files = ["README.md"]
224+
s.platform = "arm64-darwin"
225+
s.required_ruby_version = "~> 3.4.0"
226+
end
227+
228+
gemspec_file = File.join(@tempdir, gem.spec_name)
229+
File.open gemspec_file, "w" do |gs|
230+
gs.write gem.to_ruby
231+
end
232+
233+
@cmd.handle_options [gemspec_file, "--ruby-abi", "3.4"]
234+
use_ui @ui do
235+
Dir.chdir @tempdir do
236+
@cmd.execute
237+
end
238+
end
239+
240+
Dir[File.join(@tempdir, "platformed_gem-2-*.gem")].first
241+
end
242+
243+
first_build = gemspec.call
244+
second_build = gemspec.call
245+
246+
assert_equal File.basename(first_build), File.basename(second_build)
247+
end
248+
249+
def test_ruby_abi_with_output_raises
250+
gem = util_spec "platformed_gem" do |s|
251+
s.license = "AGPL-3.0-only"
252+
s.files = ["README.md"]
253+
s.platform = "arm64-darwin"
254+
s.required_ruby_version = "~> 3.4.0"
255+
end
256+
257+
gemspec_file = File.join(@tempdir, gem.spec_name)
258+
File.open gemspec_file, "w" do |gs|
259+
gs.write gem.to_ruby
260+
end
261+
262+
@cmd.handle_options [gemspec_file, "--ruby-abi", "3.4", "--output", "test.gem"]
263+
error = assert_raise(ArgumentError) do
264+
use_ui @ui do
265+
Dir.chdir @tempdir do
266+
@cmd.execute
267+
end
268+
end
269+
end
270+
assert_match(/Cannot specify both a Ruby ABI and an output file name/, error.message)
271+
end
272+
123273
def test_execute_platform
124274
gemspec_file = File.join(@tempdir, @gem.spec_name)
125275

0 commit comments

Comments
 (0)