diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb index 90d2d57b1ab0..42005315168b 100644 --- a/lib/rubygems/commands/push_command.rb +++ b/lib/rubygems/commands/push_command.rb @@ -170,7 +170,9 @@ def platform_matches?(spec) end def ruby_matches?(spec) - !options[:ruby_abi] || spec.ruby_abi == options[:ruby_abi] + return true unless options[:ruby_abi] + + Gem::ContentAddress.applicable?(spec) && spec.ruby_abi == options[:ruby_abi] end def send_push_request(name, args) diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb index bf86bef09856..90255c227032 100644 --- a/lib/rubygems/source.rb +++ b/lib/rubygems/source.rb @@ -406,6 +406,7 @@ def content_addressable_metadata(name, rows) requirements = compact_index_requirements(info_row) platform = required_platform_from(requirements[:platform]) next unless platform + next unless requirements[:ruby] ContentAddressableInfo.new(version, suffix, ruby_abi_from(requirements[:ruby]), platform) end diff --git a/spec/install/gemfile/content_addressable_spec.rb b/spec/install/gemfile/content_addressable_spec.rb index a2a09bbe250a..c8fe3ee168fd 100644 --- a/spec/install/gemfile/content_addressable_spec.rb +++ b/spec/install/gemfile/content_addressable_spec.rb @@ -168,7 +168,7 @@ end end - it "falls back to the pure-ruby gem when the content-addressed fat gem requires a different Ruby ABI" do + it "falls back to the pure-ruby gem when the content-addressed gem requires a different Ruby ABI" do simulate_platform "x86_64-linux" do build_repo2 do build_gem "mygem", "1.0" do |s| @@ -242,4 +242,96 @@ expect(the_bundle).to include_gems "mygem 2.0 not_content_addressed" end end + + it "falls back to the non-content-addressed gem when all content-addressed gems require a different Ruby ABI" do + simulate_platform "x86_64-linux" do + build_repo2 do + build_gem "mygem", "1.0" do |s| + s.platform = Gem::Platform.new("x86_64-linux") + s.write "lib/mygem.rb", "MYGEM = '1.0 not_content_addressed'" + end + end + + build_gem "mygem", "1.0", ruby_abi: mismatched_abi, path: gem_repo2("gems") do |s| + s.platform = Gem::Platform.new("x86_64-linux") + s.required_ruby_version = "~> #{mismatched_abi}.0" + s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed_mismatched_abi_1'" + end + + second_mismatched_abi = "#{Gem.ruby_version.segments[0] + 2}.0" + build_gem "mygem", "1.0", ruby_abi: second_mismatched_abi, path: gem_repo2("gems") do |s| + s.platform = Gem::Platform.new("x86_64-linux") + s.required_ruby_version = "~> #{second_mismatched_abi}.0" + s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed_mismatched_abi_2'" + end + + install_gemfile <<~G, artifice: "compact_index_v2", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s } + source "https://gem.repo2" + + gem "mygem" + G + + expect(the_bundle).to include_gems "mygem 1.0 not_content_addressed" + end + end + + it "installs a locked content-addressed gem in frozen mode" do + simulate_platform "x86_64-linux" do + build_repo2 do + build_gem "mygem", "1.0" do |s| + s.platform = Gem::Platform.new("x86_64-linux") + s.write "lib/mygem.rb", "MYGEM = '1.0 not_content_addressed'" + end + end + + build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s| + s.platform = Gem::Platform.new("x86_64-linux") + s.required_ruby_version = "~> #{current_abi}.0" + s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'" + end + + install_gemfile <<~G, artifice: "compact_index_v2", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s } + source "https://gem.repo2" + + gem "mygem" + G + + expect(the_bundle).to include_gems "mygem 1.0 content_addressed" + + pristine_system_gems + bundle_config "frozen true" + bundle "install", artifice: "compact_index_v2", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s } + + expect(the_bundle).to include_gems "mygem 1.0 content_addressed" + end + end + + it "fails when the downloaded content-addressed gem hash does not match the filename" do + simulate_platform "x86_64-linux" do + build_repo2 do + build_gem "mygem", "1.0" do |s| + s.platform = Gem::Platform.new("x86_64-linux") + s.write "lib/mygem.rb", "MYGEM = '1.0 not_content_addressed'" + end + end + + build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s| + s.platform = Gem::Platform.new("x86_64-linux") + s.required_ruby_version = "~> #{current_abi}.0" + s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'" + end + + ca_gem = Dir[gem_repo2("gems", "mygem-1.0-[0-9a-f]*.gem")].first + non_ca_gem = gem_repo2("gems", "mygem-1.0-x86_64-linux.gem") + FileUtils.cp non_ca_gem, ca_gem + + install_gemfile <<~G, artifice: "compact_index_v2", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }, raise_on_error: false + source "https://gem.repo2" + + gem "mygem" + G + + expect(err).to include("content address mismatch") + end + end end diff --git a/test/rubygems/test_gem_commands_build_command.rb b/test/rubygems/test_gem_commands_build_command.rb index 65c90e5bc76a..aef8444a5f84 100644 --- a/test/rubygems/test_gem_commands_build_command.rb +++ b/test/rubygems/test_gem_commands_build_command.rb @@ -28,7 +28,7 @@ def setup @cmd = Gem::Commands::BuildCommand.new end - def test_handle_options + def test_handle_options_force_strict_platform @cmd.handle_options %w[--force --strict] assert @cmd.options[:force] @@ -120,6 +120,156 @@ def test_execute util_test_build_gem @gem end + def test_ruby_abi_rejects_invalid_format + gem = util_spec "platformed_gem" do |s| + s.license = "AGPL-3.0-only" + s.files = ["README.md"] + s.platform = "arm64-darwin" + end + + gemspec_file = File.join(@tempdir, gem.spec_name) + File.open gemspec_file, "w" do |gs| + gs.write gem.to_ruby + end + + ["3", "3.4.1", "abc", "3.x"].each do |invalid| + @cmd.handle_options [gemspec_file, "--ruby-abi", invalid] + error = assert_raise(ArgumentError) do + use_ui @ui do + Dir.chdir @tempdir do + @cmd.execute + end + end + end + assert_match(/Ruby ABI must be in X\.Y format/, error.message) + end + end + + def test_ruby_abi_rejects_ruby_platform + gem = util_spec "some_gem" do |s| + s.license = "AGPL-3.0-only" + s.files = ["README.md"] + end + + gemspec_file = File.join(@tempdir, gem.spec_name) + File.open gemspec_file, "w" do |gs| + gs.write gem.to_ruby + end + + @cmd.handle_options [gemspec_file, "--ruby-abi", "3.4"] + error = assert_raise(ArgumentError) do + use_ui @ui do + Dir.chdir @tempdir do + @cmd.execute + end + end + end + assert_match(/no platform or a Ruby platform has been set/, error.message) + end + + def test_ruby_abi_rejects_mismatched_required_ruby_version + gem = util_spec "platformed_gem" do |s| + s.license = "AGPL-3.0-only" + s.files = ["README.md"] + s.platform = "arm64-darwin" + s.required_ruby_version = "~> 3.3.0" + end + + gemspec_file = File.join(@tempdir, gem.spec_name) + File.open gemspec_file, "w" do |gs| + gs.write gem.to_ruby + end + + @cmd.handle_options [gemspec_file, "--ruby-abi", "3.4"] + error = assert_raise(ArgumentError) do + use_ui @ui do + Dir.chdir @tempdir do + @cmd.execute + end + end + end + assert_match(/Cannot build gem for Ruby ABI 3\.4 because required_ruby_version/, error.message) + end + + def test_ruby_abi_defaults_required_ruby_version_when_unset + gem = util_spec "platformed_gem" do |s| + s.license = "AGPL-3.0-only" + s.files = ["README.md"] + s.platform = "arm64-darwin" + end + + gemspec_file = File.join(@tempdir, gem.spec_name) + File.open gemspec_file, "w" do |gs| + gs.write gem.to_ruby + end + + @cmd.handle_options [gemspec_file, "--ruby-abi", "3.4"] + use_ui @ui do + Dir.chdir @tempdir do + @cmd.execute + end + end + + files = Dir[File.join(@tempdir, "platformed_gem-2-*.gem")] + assert_equal 1, files.size + spec = Gem::Package.new(files.first).spec + assert_equal Gem::Requirement.new("~> 3.4.0"), spec.required_ruby_version + end + + def test_ruby_abi_produces_deterministic_content_address + gemspec = lambda do + gem = util_spec "platformed_gem" do |s| + s.license = "AGPL-3.0-only" + s.files = ["README.md"] + s.platform = "arm64-darwin" + s.required_ruby_version = "~> 3.4.0" + end + + gemspec_file = File.join(@tempdir, gem.spec_name) + File.open gemspec_file, "w" do |gs| + gs.write gem.to_ruby + end + + @cmd.handle_options [gemspec_file, "--ruby-abi", "3.4"] + use_ui @ui do + Dir.chdir @tempdir do + @cmd.execute + end + end + + Dir[File.join(@tempdir, "platformed_gem-2-*.gem")].first + end + + first_build = gemspec.call + second_build = gemspec.call + + assert_equal File.basename(first_build), File.basename(second_build) + end + + def test_ruby_abi_with_output_raises + gem = util_spec "platformed_gem" do |s| + s.license = "AGPL-3.0-only" + s.files = ["README.md"] + s.platform = "arm64-darwin" + s.required_ruby_version = "~> 3.4.0" + end + + gemspec_file = File.join(@tempdir, gem.spec_name) + File.open gemspec_file, "w" do |gs| + gs.write gem.to_ruby + end + + @cmd.handle_options [gemspec_file, "--ruby-abi", "3.4", "--output", "test.gem"] + error = assert_raise(ArgumentError) do + use_ui @ui do + Dir.chdir @tempdir do + @cmd.execute + end + end + end + assert_match(/Cannot specify both a Ruby ABI and an output file name/, error.message) + end + def test_execute_platform gemspec_file = File.join(@tempdir, @gem.spec_name) diff --git a/test/rubygems/test_gem_commands_info_command.rb b/test/rubygems/test_gem_commands_info_command.rb index 9e2f5238bd61..4f2ee5084e36 100644 --- a/test/rubygems/test_gem_commands_info_command.rb +++ b/test/rubygems/test_gem_commands_info_command.rb @@ -198,6 +198,132 @@ def test_execute_remote_content_addressable_and_platform_gems_display_together refute_match "fedcba98", @ui.output end + def test_execute_remote_content_addressable_gem_displays_multiple_ruby_abis_on_same_platform + spec_fetcher {} + + spec = util_spec "a", "1" do |s| + s.platform = "x86_64-linux" + s.summary = "this is a summary" + s.homepage = "http://example.com" + s.authors = ["A User"] + end + spec.content_address = "abcdef12" + + versions_body = +"created_at: 2026-01-01T00:00:00Z\n---\na 1-abcdef12,1-fedcba98 0000\n" + versions_response = util_compact_index_response(versions_body) + versions_response.uri = Gem::URI("#{@gem_repo}versions") + @fetcher.data["#{@gem_repo}versions"] = versions_response + @fetcher.data["#{@gem_repo}info/a"] = util_compact_index_response(<<~INFO) + --- + 1-abcdef12 |checksum:123,ruby:~> 3.3.0,platform:= x86_64-linux + 1-fedcba98 |checksum:456,ruby:~> 3.4.0,platform:= x86_64-linux + INFO + + path = "#{@gem_repo}quick/Marshal.#{Gem.marshal_version}/a-1-abcdef12.gemspec.rz" + @fetcher.data[path] = Zlib::Deflate.deflate(Marshal.dump(spec)) + path = "#{@gem_repo}quick/Marshal.#{Gem.marshal_version}/a-1-fedcba98.gemspec.rz" + @fetcher.data[path] = Zlib::Deflate.deflate(Marshal.dump(spec)) + Gem::SpecFetcher.fetcher = nil + + @cmd.handle_options %w[a --remote] + + use_ui @ui do + @cmd.execute + end + + assert_include @ui.output, "Platforms:\n" + assert_include @ui.output, "x86_64-linux Ruby ABI: 3.3, 3.4\n" + refute_match "abcdef12", @ui.output + refute_match "fedcba98", @ui.output + end + + def test_execute_remote_content_addressable_platform_and_source_gems_display_together + spec_fetcher {} + + spec = util_spec "a", "3" do |s| + s.platform = "arm64-darwin" + s.summary = "this is a summary" + s.homepage = "http://example.com" + s.authors = ["A User"] + end + spec.content_address = "fedcba98" + + spec_source = util_spec "a", "4" do |s| + s.summary = "this is a summary" + s.homepage = "http://example.com" + s.authors = ["A User"] + end + + versions_body = +"created_at: 2026-01-01T00:00:00Z\n---\na 1-x86_64-linux,2-abcdef12,3-fedcba98,4 0000\n" + versions_response = util_compact_index_response(versions_body) + versions_response.uri = Gem::URI("#{@gem_repo}versions") + @fetcher.data["#{@gem_repo}versions"] = versions_response + @fetcher.data["#{@gem_repo}info/a"] = util_compact_index_response(<<~INFO) + --- + 2-abcdef12 |checksum:123,ruby:~> 3.3.0,platform:= x86_64-linux + 3-fedcba98 |checksum:456,ruby:~> 3.4.0,platform:= arm64-darwin + INFO + + path = "#{@gem_repo}quick/Marshal.#{Gem.marshal_version}/a-3-fedcba98.gemspec.rz" + @fetcher.data[path] = Zlib::Deflate.deflate(Marshal.dump(spec)) + path = "#{@gem_repo}quick/Marshal.#{Gem.marshal_version}/a-4.gemspec.rz" + @fetcher.data[path] = Zlib::Deflate.deflate(Marshal.dump(spec_source)) + Gem::SpecFetcher.fetcher = nil + + @cmd.handle_options %w[a --remote --all] + + use_ui @ui do + @cmd.execute + end + + assert_include @ui.output, "a (4, 3, 2, 1)" + assert_include @ui.output, "Platforms:\n" + assert_include @ui.output, " 1: x86_64-linux\n" + assert_include @ui.output, " 2: x86_64-linux Ruby ABI: 3.3\n" + assert_include @ui.output, " 3: arm64-darwin Ruby ABI: 3.4\n" + refute_match " 4:", @ui.output + refute_match "abcdef12", @ui.output + refute_match "fedcba98", @ui.output + end + + def test_execute_remote_platform_gem_displays_version_once_for_multiple_platforms + spec_fetcher {} + + spec_e1 = util_spec "e", "1" do |s| + s.platform = "x86_64-linux" + s.summary = "summary e" + s.homepage = "http://example.com" + s.authors = ["E User"] + end + + spec_e2 = util_spec "e", "1" do |s| + s.platform = "arm64-darwin" + s.summary = "summary e" + s.homepage = "http://example.com" + s.authors = ["E User"] + end + + versions_body = +"created_at: 2026-01-01T00:00:00Z\n---\ne 1-x86_64-linux,1-arm64-darwin 0000\n" + versions_response = util_compact_index_response(versions_body) + versions_response.uri = Gem::URI("#{@gem_repo}versions") + @fetcher.data["#{@gem_repo}versions"] = versions_response + + path = "#{@gem_repo}quick/Marshal.#{Gem.marshal_version}/e-1-x86_64-linux.gemspec.rz" + @fetcher.data[path] = Zlib::Deflate.deflate(Marshal.dump(spec_e1)) + path = "#{@gem_repo}quick/Marshal.#{Gem.marshal_version}/e-1-arm64-darwin.gemspec.rz" + @fetcher.data[path] = Zlib::Deflate.deflate(Marshal.dump(spec_e2)) + Gem::SpecFetcher.fetcher = nil + + @cmd.handle_options %w[e --remote] + + use_ui @ui do + @cmd.execute + end + + assert_include @ui.output, "e (1)" + assert_include @ui.output, "Platforms: arm64-darwin, x86_64-linux\n" + end + def test_execute_with_version_flag spec_fetcher do |fetcher| fetcher.spec "coolgem", "1.0" diff --git a/test/rubygems/test_gem_commands_list_command.rb b/test/rubygems/test_gem_commands_list_command.rb index 2ee58cdf28ef..d35bc7021e2f 100644 --- a/test/rubygems/test_gem_commands_list_command.rb +++ b/test/rubygems/test_gem_commands_list_command.rb @@ -192,7 +192,7 @@ def test_execute_remote_content_addressable_gems_displays_multiple_versions_on_s def test_execute_remote_content_addressable_and_platform_gems_display_together spec_fetcher {} - versions_body = +"created_at: 2026-01-01T00:00:00Z\n---\na 1-x86_64-linux,2-abcdef12,3-fedcba98 0000\n" + versions_body = +"created_at: 2026-01-01T00:00:00Z\n---\na 1-x86_64-linux,2-abcdef12,3-fedcba98,4 0000\n" versions_response = util_compact_index_response(versions_body) versions_response.uri = Gem::URI("#{@gem_repo}versions") @fetcher.data["#{@gem_repo}versions"] = versions_response @@ -210,7 +210,8 @@ def test_execute_remote_content_addressable_and_platform_gems_display_together end expected = <<~OUTPUT.chomp - a (3 Platform: arm64-darwin, Ruby ABI: 3.4 + a (4 + 3 Platform: arm64-darwin, Ruby ABI: 3.4 2 Platform: x86_64-linux, Ruby ABI: 3.3 1 Platform: x86_64-linux) OUTPUT diff --git a/test/rubygems/test_gem_commands_push_command.rb b/test/rubygems/test_gem_commands_push_command.rb index 768edb32f373..48cff376c8b1 100644 --- a/test/rubygems/test_gem_commands_push_command.rb +++ b/test/rubygems/test_gem_commands_push_command.rb @@ -238,12 +238,12 @@ def test_execute_with_same_name_version_platform_selects_matching_ruby_abi assert_equal Gem.read_binary(ruby_34_path), @fetcher.last_request.body end - def test_execute_with_fat_and_skinny_candidates_selects_skinny_for_ruby_abi - _, fat_path = util_gem "mixed-target", "1.0.0" do |spec| + def test_execute_with_non_and_content_addressable_candidates_selects_content_addressable_for_ruby_abi + _, non_content_addressable_path = util_gem "mixed-target", "1.0.0" do |spec| spec.platform = "arm64-darwin" spec.required_ruby_version = ">= 3.1" end - _, skinny_path = util_gem "mixed-target", "1.0.0", ruby_abi: "3.4" do |spec| + _, content_addressable_path = util_gem "mixed-target", "1.0.0", ruby_abi: "3.4" do |spec| spec.files = ["lib/mixed-target.rb"] spec.platform = "arm64-darwin" end @@ -251,18 +251,18 @@ def test_execute_with_fat_and_skinny_candidates_selects_skinny_for_ruby_abi @response = "Successfully registered gem: mixed-target (1.0.0)" @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") - @cmd.options[:args] = [fat_path, skinny_path] + @cmd.options[:args] = [non_content_addressable_path, content_addressable_path] @cmd.options[:platform] = "arm64-darwin" @cmd.options[:ruby_abi] = "3.4" @cmd.execute assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class - assert_equal Gem.read_binary(skinny_path), @fetcher.last_request.body + assert_equal Gem.read_binary(content_addressable_path), @fetcher.last_request.body end - def test_execute_with_ruby_abi_selector_does_not_match_broad_ruby_requirement - _, gem_path = util_gem "broad-ruby", "1.0.0" do |spec| + def test_execute_with_ruby_abi_selector_does_not_match_non_content_addressable_ruby_requirement + _, gem_path = util_gem "non-content-addressable-ruby", "1.0.0" do |spec| spec.platform = "arm64-darwin" spec.required_ruby_version = ">= 3.1" end @@ -365,16 +365,16 @@ def test_execute_with_ruby_abi_selector_raises_when_multiple_platforms_match end def test_execute_with_platform_selector_suggests_exact_filename_for_gem_without_ruby_abi - _, fat_path = util_gem "ambiguous-fat", "1.0.0" do |spec| + _, non_content_addressable_path = util_gem "ambiguous-non-content-addressable", "1.0.0" do |spec| spec.platform = "arm64-darwin" spec.required_ruby_version = ">= 3.1" end - _, skinny_path = util_gem "ambiguous-fat", "1.0.0", ruby_abi: "3.4" do |spec| - spec.files = ["lib/ambiguous-fat.rb"] + _, content_addressable_path = util_gem "ambiguous-non-content-addressable", "1.0.0", ruby_abi: "3.4" do |spec| + spec.files = ["lib/ambiguous-non-content-addressable.rb"] spec.platform = "arm64-darwin" end - @cmd.options[:args] = [fat_path, skinny_path] + @cmd.options[:args] = [non_content_addressable_path, content_addressable_path] @cmd.options[:platform] = "arm64-darwin" error = assert_raise Gem::CommandLineError do @@ -382,8 +382,8 @@ def test_execute_with_platform_selector_suggests_exact_filename_for_gem_without_ end assert_match "Multiple gems matched platform arm64-darwin", error.message - assert_match fat_path, error.message - assert_match skinny_path, error.message + assert_match non_content_addressable_path, error.message + assert_match content_addressable_path, error.message assert_match "Specify --ruby-abi with one of: 3.4", error.message assert_match "To push a gem without a Ruby ABI, pass the exact filename.", error.message end @@ -403,6 +403,86 @@ def test_execute_without_selectors_still_rejects_multiple_gems assert_match "Too many gem names", error.message end + def test_execute_with_both_selectors_raises_when_multiple_gems_match_without_suggestion + _, first_path = util_gem "dual-ambiguous-one", "1.0.0", ruby_abi: "3.4" do |spec| + spec.files = ["lib/dual-ambiguous-one.rb"] + spec.platform = "arm64-darwin" + end + _, second_path = util_gem "dual-ambiguous-two", "1.0.0", ruby_abi: "3.4" do |spec| + spec.files = ["lib/dual-ambiguous-two.rb"] + spec.platform = "arm64-darwin" + end + + @cmd.options[:args] = [first_path, second_path] + @cmd.options[:platform] = "arm64-darwin" + @cmd.options[:ruby_abi] = "3.4" + + error = assert_raise Gem::CommandLineError do + @cmd.execute + end + + assert_match "Multiple gems matched platform arm64-darwin and Ruby ABI 3.4", error.message + assert_match first_path, error.message + assert_match second_path, error.message + refute_match(/Specify/, error.message) + end + + def test_execute_with_both_selectors_selects_single_matching_gem + _, matching_path = util_gem "dual-match", "1.0.0", ruby_abi: "3.4" do |spec| + spec.files = ["lib/dual-match.rb"] + spec.platform = "arm64-darwin" + end + + @response = "Successfully registered gem: dual-match (1.0.0)" + @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") + + @cmd.options[:args] = [matching_path] + @cmd.options[:platform] = "arm64-darwin" + @cmd.options[:ruby_abi] = "3.4" + + @cmd.execute + + assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class + assert_equal Gem.read_binary(matching_path), @fetcher.last_request.body + end + + def test_execute_with_platform_selector_selects_single_non_content_addressable_gem + _, non_content_addressable_path = util_gem "non-content-addressable-only", "1.0.0" do |spec| + spec.platform = "arm64-darwin" + spec.required_ruby_version = ">= 3.1" + end + + @response = "Successfully registered gem: non-content-addressable-only (1.0.0)" + @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") + + @cmd.options[:args] = [non_content_addressable_path] + @cmd.options[:platform] = "arm64-darwin" + + @cmd.execute + + assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class + assert_equal Gem.read_binary(non_content_addressable_path), @fetcher.last_request.body + end + + def test_execute_with_ruby_abi_selector_rejects_source_gem + _, source_path = util_gem "source-ruby", "1.0.0" do |spec| + spec.files = ["lib/source-ruby.rb"] + spec.required_ruby_version = "~> 3.4.0" + end + + @response = "Successfully registered gem: source-ruby (1.0.0)" + @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") + + @cmd.options[:args] = [source_path] + @cmd.options[:ruby_abi] = "3.4" + + error = assert_raise(Gem::CommandLineError) do + @cmd.execute + end + + assert_match(/No gem matched/, error.message) + end + def test_execute_attestation @response = "Successfully registered gem: freewill (1.0.0)" @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") diff --git a/test/rubygems/test_gem_commands_search_command.rb b/test/rubygems/test_gem_commands_search_command.rb index cb7bdbc83d40..2bbe7f4a5578 100644 --- a/test/rubygems/test_gem_commands_search_command.rb +++ b/test/rubygems/test_gem_commands_search_command.rb @@ -112,4 +112,142 @@ def test_execute_content_addressable_and_platform_gems_display_together refute_match "abcdef12", @ui.output refute_match "fedcba98", @ui.output end + + def test_execute_content_addressable_gems_displays_ruby_abis_next_to_their_platforms + spec_fetcher {} + + versions_body = +"created_at: 2026-01-01T00:00:00Z\n---\na 1-abcdef12,1-fedcba98 0000\n" + versions_response = util_compact_index_response(versions_body) + versions_response.uri = Gem::URI("#{@gem_repo}versions") + @fetcher.data["#{@gem_repo}versions"] = versions_response + @fetcher.data["#{@gem_repo}info/a"] = util_compact_index_response(<<~INFO) + --- + 1-abcdef12 |checksum:123,ruby:~> 3.3.0,platform:= x86_64-linux + 1-fedcba98 |checksum:456,ruby:~> 3.4.0,platform:= arm64-darwin + INFO + Gem::SpecFetcher.fetcher = nil + + @cmd.handle_options %w[a] + + use_ui @ui do + @cmd.execute + end + + expected = <<~OUTPUT.chomp + a (1 Platform: arm64-darwin, Ruby ABI: 3.4 + 1 Platform: x86_64-linux, Ruby ABI: 3.3) + OUTPUT + + assert_include @ui.output, expected + refute_match "abcdef12", @ui.output + refute_match "fedcba98", @ui.output + end + + def test_execute_content_addressable_gems_displays_multiple_ruby_abis_on_the_same_line + spec_fetcher {} + + versions_body = +"created_at: 2026-01-01T00:00:00Z\n---\na 1-abcdef12,1-fedcba98 0000\n" + versions_response = util_compact_index_response(versions_body) + versions_response.uri = Gem::URI("#{@gem_repo}versions") + @fetcher.data["#{@gem_repo}versions"] = versions_response + @fetcher.data["#{@gem_repo}info/a"] = util_compact_index_response(<<~INFO) + --- + 1-abcdef12 |checksum:123,ruby:~> 3.3.0,platform:= x86_64-linux + 1-fedcba98 |checksum:456,ruby:~> 3.4.0,platform:= x86_64-linux + INFO + Gem::SpecFetcher.fetcher = nil + + @cmd.handle_options %w[a] + + use_ui @ui do + @cmd.execute + end + + assert_include @ui.output, "a (1 Platform: x86_64-linux, Ruby ABI: 3.3, 3.4)" + refute_match "abcdef12", @ui.output + refute_match "fedcba98", @ui.output + end + + def test_execute_content_addressable_gems_displays_multiple_versions_on_separate_lines + spec_fetcher {} + + versions_body = +"created_at: 2026-01-01T00:00:00Z\n---\na 1-abcdef12,2-fedcba98,3-12345678 0000\n" + versions_response = util_compact_index_response(versions_body) + versions_response.uri = Gem::URI("#{@gem_repo}versions") + @fetcher.data["#{@gem_repo}versions"] = versions_response + @fetcher.data["#{@gem_repo}info/a"] = util_compact_index_response(<<~INFO) + --- + 1-abcdef12 |checksum:123,ruby:~> 3.3.0,platform:= x86_64-linux + 2-fedcba98 |checksum:456,ruby:~> 3.4.0,platform:= x86_64-linux + 3-12345678 |checksum:789,ruby:~> 3.4.0,platform:= arm64-darwin + INFO + Gem::SpecFetcher.fetcher = nil + + @cmd.handle_options %w[a] + + use_ui @ui do + @cmd.execute + end + + expected = <<~OUTPUT.chomp + a (3 Platform: arm64-darwin, Ruby ABI: 3.4 + 2 Platform: x86_64-linux, Ruby ABI: 3.4 + 1 Platform: x86_64-linux, Ruby ABI: 3.3) + OUTPUT + + assert_include @ui.output, expected + refute_match "abcdef12", @ui.output + refute_match "fedcba98", @ui.output + refute_match "12345678", @ui.output + end + + def test_execute_platform_gem_displays_version_once_for_multiple_platforms + spec_fetcher {} + + versions_body = +"created_at: 2026-01-01T00:00:00Z\n---\ne 1-x86_64-linux,1-arm64-darwin 0000\n" + versions_response = util_compact_index_response(versions_body) + versions_response.uri = Gem::URI("#{@gem_repo}versions") + @fetcher.data["#{@gem_repo}versions"] = versions_response + Gem::SpecFetcher.fetcher = nil + + @cmd.handle_options %w[e] + + use_ui @ui do + @cmd.execute + end + + assert_include @ui.output, "e (1 arm64-darwin x86_64-linux)" + end + + def test_execute_content_addressable_platform_and_source_gems_display_together + spec_fetcher {} + + versions_body = +"created_at: 2026-01-01T00:00:00Z\n---\na 1-x86_64-linux,2-abcdef12,3-fedcba98,4 0000\n" + versions_response = util_compact_index_response(versions_body) + versions_response.uri = Gem::URI("#{@gem_repo}versions") + @fetcher.data["#{@gem_repo}versions"] = versions_response + @fetcher.data["#{@gem_repo}info/a"] = util_compact_index_response(<<~INFO) + --- + 2-abcdef12 |checksum:123,ruby:~> 3.3.0,platform:= x86_64-linux + 3-fedcba98 |checksum:456,ruby:~> 3.4.0,platform:= arm64-darwin + INFO + Gem::SpecFetcher.fetcher = nil + + @cmd.handle_options %w[a --all] + + use_ui @ui do + @cmd.execute + end + + expected = <<~OUTPUT.chomp + a (4 + 3 Platform: arm64-darwin, Ruby ABI: 3.4 + 2 Platform: x86_64-linux, Ruby ABI: 3.3 + 1 Platform: x86_64-linux) + OUTPUT + + assert_include @ui.output, expected + refute_match "abcdef12", @ui.output + refute_match "fedcba98", @ui.output + end end diff --git a/test/rubygems/test_gem_commands_yank_command.rb b/test/rubygems/test_gem_commands_yank_command.rb index ce7db6585c8a..5fbfdeeaaa3d 100644 --- a/test/rubygems/test_gem_commands_yank_command.rb +++ b/test/rubygems/test_gem_commands_yank_command.rb @@ -122,6 +122,33 @@ def test_execute_with_ruby_abi_without_platform_sends_ruby_abi_to_yank_api assert_equal [yank_uri], @fetcher.paths end + def test_execute_with_ruby_abi_and_platform_no_matching_gem_displays_error + original_platforms = Gem.platforms.dup + yank_uri = "http://example/api/v1/gems/yank" + @fetcher.data[yank_uri] = HTTPResponseFactory.create( + body: "The version 1.0 (x86_64-linux) (Ruby ABI 3.9) does not exist.", + code: 404, + msg: "Not Found" + ) + + @cmd.options[:args] = %w[a] + @cmd.options[:version] = req("= 1.0") + @cmd.options[:ruby_abi] = "3.9" + Gem.platforms = [Gem::Platform::RUBY, Gem::Platform.new("x86_64-linux")] + @cmd.options[:added_platform] = true + + use_ui @ui do + @cmd.execute + end + + body = @fetcher.last_request.body.split("&").sort + assert_equal %w[gem_name=a platform=x86_64-linux ruby_abi=3.9 version=1.0], body + assert_match(/The version 1\.0 \(x86_64-linux\) \(Ruby ABI 3\.9\) does not exist/, @ui.output) + assert_equal [yank_uri], @fetcher.paths + ensure + Gem.platforms = original_platforms + end + def test_execute_with_otp_success response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." yank_uri = "http://example/api/v1/gems/yank" diff --git a/test/rubygems/test_gem_content_address.rb b/test/rubygems/test_gem_content_address.rb index 3b98d9024e88..d89355049ce4 100644 --- a/test/rubygems/test_gem_content_address.rb +++ b/test/rubygems/test_gem_content_address.rb @@ -5,10 +5,14 @@ class TestGemContentAddress < Gem::TestCase def test_match - assert Gem::ContentAddress.match?("abcdef12") + assert Gem::ContentAddress.match?("78be552b") refute Gem::ContentAddress.match?("x86_64-linux") refute Gem::ContentAddress.match?(nil) - refute Gem::ContentAddress.match?("abc") + refute Gem::ContentAddress.match?("") + refute Gem::ContentAddress.match?(0xabcdef12) + refute Gem::ContentAddress.match?(:abcdef12) + refute Gem::ContentAddress.match?("abcdef12 ") + refute Gem::ContentAddress.match?(" abcdef12") end def test_match_boundary_lengths @@ -63,6 +67,14 @@ def test_content_addressed_with_eligible_spec_and_no_address refute Gem::ContentAddress.content_addressed?(spec) end + def test_content_addressed_with_eligible_spec_and_invalid_address + spec = Gem::Specification.new "a", 1 + spec.required_ruby_version = ">= 3.0" + spec.platform = "x86_64-linux" + spec.content_address = "x86_64-linux" + refute Gem::ContentAddress.content_addressed?(spec) + end + def test_content_addressed_with_ineligible_spec_and_valid_address spec = Gem::Specification.new "a", 1 spec.content_address = "abcdef12" diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index 83258ed4d2a2..741eed1ea1d5 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -1050,6 +1050,7 @@ def test_install_assigns_content_address_from_filename assert_equal address, spec.content_address assert_equal "a-2-#{address}", spec.full_name assert_path_exist File.join(@gemhome, "gems", "a-2-#{address}") + assert_path_exist File.join(@gemhome, "cache", "a-2-#{address}.gem") assert_path_exist File.join(@gemhome, "specifications", "a-2-#{address}.gemspec") end @@ -1209,6 +1210,68 @@ def test_reinstalling_content_addressed_gem_is_idempotent assert_equal 1, Dir[File.join(@gemhome, "specifications", "a-2*.gemspec")].size end + def test_install_assigns_content_address_from_filename_with_full_sha + _, a_gem = util_gem("a", 2) do |spec| + spec.required_ruby_version = ">= 3.0" + spec.platform = "x86_64-linux" + end + + digest = Digest::SHA256.file(a_gem).hexdigest + dir = File.dirname(a_gem) + filename = File.join(dir, "a-2-#{digest}.gem") + FileUtils.cp a_gem, filename + installer = Gem::Installer.at filename, install_dir: @gemhome, force: true + spec = installer.install + + assert_equal digest, spec.content_address + assert_equal "a-2-#{digest}", spec.full_name + assert_path_exist File.join(@gemhome, "gems", "a-2-#{digest}") + assert_path_exist File.join(@gemhome, "cache", "a-2-#{digest}.gem") + assert_path_exist File.join(@gemhome, "specifications", "a-2-#{digest}.gemspec") + end + + def test_two_content_addressed_gems_with_same_name_version_coexist + _, gem1 = util_gem("a", 2) do |spec| + spec.required_ruby_version = ">= 3.0" + spec.platform = "x86_64-linux" + spec.summary = "variant 1" + end + gem1_backup = File.join(@tempdir, "gem1_backup.gem") + FileUtils.cp gem1, gem1_backup + + _, gem2 = util_gem("a", 2) do |spec| + spec.required_ruby_version = ">= 3.0" + spec.platform = "x86_64-linux" + spec.summary = "variant 2" + end + + FileUtils.rm_rf File.join(@gemhome, "gems", "a-2-x86_64-linux") + FileUtils.rm_rf File.join(@gemhome, "specifications", "a-2-x86_64-linux.gemspec") + Gem::Specification.reset + + dir = File.dirname(gem1) + digest1 = Digest::SHA256.file(gem1_backup).hexdigest + digest2 = Digest::SHA256.file(gem2).hexdigest + refute_equal digest1, digest2 + + address1 = digest1[0, 8] + address2 = digest2[0, 8] + file1 = File.join(dir, "a-2-#{address1}.gem") + file2 = File.join(dir, "a-2-#{address2}.gem") + FileUtils.cp gem1_backup, file1 + FileUtils.cp gem2, file2 + + Gem::Installer.at(file1, install_dir: @gemhome, force: true).install + Gem::Installer.at(file2, install_dir: @gemhome, force: true).install + + assert_path_exist File.join(@gemhome, "gems", "a-2-#{address1}") + assert_path_exist File.join(@gemhome, "gems", "a-2-#{address2}") + assert_path_exist File.join(@gemhome, "specifications", "a-2-#{address1}.gemspec") + assert_path_exist File.join(@gemhome, "specifications", "a-2-#{address2}.gemspec") + assert_equal 2, Dir[File.join(@gemhome, "gems", "a-2-*")].size + assert_equal 2, Dir[File.join(@gemhome, "specifications", "a-2-*.gemspec")].size + end + def test_install installer = util_setup_installer diff --git a/test/rubygems/test_gem_name_tuple.rb b/test/rubygems/test_gem_name_tuple.rb index ec4d48ac98db..276a002dfb3f 100644 --- a/test/rubygems/test_gem_name_tuple.rb +++ b/test/rubygems/test_gem_name_tuple.rb @@ -54,7 +54,7 @@ def test_content_addressable_metadata assert_equal "a-1-abcdef12", n.full_name end - def test_fat_tuple_does_not_store_nil_content_addressable_metadata_ivars + def test_non_content_addressable_tuple_does_not_store_nil_content_addressable_metadata_ivars n = Gem::NameTuple.new "a", Gem::Version.new(1), "x86_64-linux" refute_includes n.instance_variables, :@content_address @@ -63,11 +63,11 @@ def test_fat_tuple_does_not_store_nil_content_addressable_metadata_ivars assert_nil n.ruby_abi end - def test_sort_mixed_fat_and_content_addressable_tuples - fat = Gem::NameTuple.new "a", Gem::Version.new(1), "x86_64-linux" - skinny = Gem::NameTuple.new "a", Gem::Version.new(1), "x86_64-linux", content_address: "abcdef12", ruby_abi: "3.3" + def test_sort_mixed_non_content_addressable_and_content_addressable_tuples + non_content_addressable = Gem::NameTuple.new "a", Gem::Version.new(1), "x86_64-linux" + content_addressable = Gem::NameTuple.new "a", Gem::Version.new(1), "x86_64-linux", content_address: "abcdef12", ruby_abi: "3.3" - assert_equal [fat, skinny], [skinny, fat].sort + assert_equal [non_content_addressable, content_addressable], [content_addressable, non_content_addressable].sort end def test_spec_name diff --git a/test/rubygems/test_gem_package.rb b/test/rubygems/test_gem_package.rb index e89155aeef4b..fe5505a70afb 100644 --- a/test/rubygems/test_gem_package.rb +++ b/test/rubygems/test_gem_package.rb @@ -257,6 +257,52 @@ def test_ruby_abi_creates_content_addressed_file assert_match(/\Aplatformed-1-[0-9a-f]{8}\.gem\z/, built_file) end + def test_ruby_abi_built_gem_preserves_derived_metadata + spec = Gem::Specification.new "platformed", "1" + spec.summary = "platformed" + spec.authors = "platformed" + spec.files = ["lib/code.rb"] + spec.platform = "arm64-darwin" + spec.required_ruby_version = Gem::Requirement.new("~> 3.4.0") + + FileUtils.mkdir "lib" + + File.open "lib/code.rb", "w" do |io| + io.write "# lib/code.rb" + end + + built_file = Gem::Package.build(spec, false, false, nil, "3.4") + + loaded_spec = Gem::Package.new(built_file).spec + assert_equal "platformed", loaded_spec.name + assert_equal Gem::Version.new("1"), loaded_spec.version + assert_equal Gem::Platform.new("arm64-darwin"), loaded_spec.platform + assert_equal Gem::Requirement.new("~> 3.4.0"), loaded_spec.required_ruby_version + assert_equal "3.4", loaded_spec.ruby_abi + end + + def test_required_ruby_version_unchanged_after_successful_matching_build + spec = Gem::Specification.new "platformed", "1" + spec.summary = "platformed" + spec.authors = "platformed" + spec.files = ["lib/code.rb"] + spec.platform = "arm64-darwin" + spec.required_ruby_version = Gem::Requirement.new("~> 3.4.0") + + FileUtils.mkdir "lib" + + File.open "lib/code.rb", "w" do |io| + io.write "# lib/code.rb" + end + + original_rrv = spec.required_ruby_version + + Gem::Package.build(spec, false, false, nil, "3.4") + + assert_equal original_rrv, spec.required_ruby_version + assert_equal Gem::Requirement.new("~> 3.4.0"), spec.required_ruby_version + end + def test_ruby_abi_not_passed_does_not_create_content_addressed_file spec = Gem::Specification.new "platformed", "1" spec.summary = "platformed" diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index 269869cd4c96..c251899b488e 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -336,19 +336,19 @@ def test_prefers_content_addressed_gem_for_same_platform assert_resolves_to [ca_spec], resolver end - def test_falls_back_to_fat_when_content_addressed_gem_requires_other_rubygems_version + def test_falls_back_to_non_content_addressable_when_content_addressed_gem_requires_other_rubygems_version ca_spec = util_spec "a", "1" - fat_spec = util_spec "a", "1" + non_content_addressable_spec = util_spec "a", "1" ca_spec.platform = Gem::Platform.local ca_spec.content_address = "abc1234567" ca_spec.required_rubygems_version = ">= 999" - fat_spec.platform = Gem::Platform.local + non_content_addressable_spec.platform = Gem::Platform.local - s = set(fat_spec, ca_spec) + s = set(non_content_addressable_spec, ca_spec) dependency = make_dep "a" resolver = Gem::Resolver.new([dependency], s) - assert_resolves_to [fat_spec], resolver + assert_resolves_to [non_content_addressable_spec], resolver end def test_falls_back_to_source_when_content_addressed_gem_requires_other_ruby @@ -366,21 +366,56 @@ def test_falls_back_to_source_when_content_addressed_gem_requires_other_ruby assert_resolves_to [source_spec], resolver end - def test_falls_back_to_fat_before_source_when_content_addressed_gem_requires_other_ruby + def test_falls_back_to_non_content_addressable_before_source_when_content_addressed_gem_requires_other_ruby ca_spec = util_spec "a", "1" - fat_spec = util_spec "a", "1" + non_content_addressable_spec = util_spec "a", "1" source_spec = util_spec "a", "1" ca_spec.platform = Gem::Platform.local ca_spec.content_address = "abc1234567" ca_spec.required_ruby_version = ">= 999" - fat_spec.platform = Gem::Platform.local + non_content_addressable_spec.platform = Gem::Platform.local source_spec.platform = Gem::Platform::RUBY - s = set(source_spec, fat_spec, ca_spec) + s = set(source_spec, non_content_addressable_spec, ca_spec) dependency = make_dep "a" resolver = Gem::Resolver.new([dependency], s) - assert_resolves_to [fat_spec], resolver + assert_resolves_to [non_content_addressable_spec], resolver + end + + def test_prefers_compatible_content_addressed_gem_when_multiple_abis_available + current_abi = "#{Gem.ruby_version.segments[0]}.#{Gem.ruby_version.segments[1]}" + + ca_compatible = util_spec "a", "1" + ca_incompatible = util_spec "a", "1" + + ca_compatible.platform = Gem::Platform.local + ca_compatible.content_address = "abc1234567" + ca_compatible.required_ruby_version = "~> #{current_abi}.0" + ca_incompatible.platform = Gem::Platform.local + ca_incompatible.content_address = "def1234567" + ca_incompatible.required_ruby_version = "~> 999.0.0" + + s = set(ca_incompatible, ca_compatible) + dependency = make_dep "a" + resolver = Gem::Resolver.new([dependency], s) + assert_resolves_to [ca_compatible], resolver + end + + def test_raises_when_only_content_addressed_gem_is_incompatible + ca_spec = util_spec "a", "1" + + ca_spec.platform = Gem::Platform.local + ca_spec.content_address = "abc1234567" + ca_spec.required_ruby_version = "~> 999.0.0" + + s = set(ca_spec) + dependency = make_dep "a" + resolver = Gem::Resolver.new([dependency], s) + + assert_raise Gem::DependencyResolutionError do + resolver.resolve + end end def test_does_not_pick_musl_variants_on_non_musl_linux diff --git a/test/rubygems/test_gem_source.rb b/test/rubygems/test_gem_source.rb index 2bde66d2e771..d72bf05315a1 100644 --- a/test/rubygems/test_gem_source.rb +++ b/test/rubygems/test_gem_source.rb @@ -263,6 +263,38 @@ def test_decode_content_addressable_tuples_latest_groups_by_gem_name_before_plat assert_equal %w[aa-7.1-aaaa1111 ab-1.5-bbbb2222 ac-6.4-cccc3333 ad-1.16-dddd4444], specs.map(&:full_name).sort end + def test_load_specs_compact_index_decodes_mixed_content_addressable_and_platform_entries + versions_body = +"created_at: 2026-01-01T00:00:00Z\n---\na 1-abcdef12,1-x86_64-linux 0000\n" + versions_response = util_compact_index_response(versions_body) + versions_response.uri = Gem::URI("#{@gem_repo}versions") + @fetcher.data["#{@gem_repo}versions"] = versions_response + @fetcher.data["#{@gem_repo}info/a"] = util_compact_index_response("---\n1-abcdef12 |checksum:123,ruby:~> 3.3.0,platform:= x86_64-linux\n") + + specs = @source.load_specs(:released) + decoded = @source.decode_content_addressable_tuples(specs) + + assert_equal 2, decoded.size + ca_spec = decoded.find {|s| s.content_address == "abcdef12" } + platform_spec = decoded.find {|s| s.content_address.nil? } + assert_equal "a-1-abcdef12", ca_spec.full_name + assert_equal "x86_64-linux", ca_spec.platform + assert_equal "3.3", ca_spec.ruby_abi + assert_equal "a-1-x86_64-linux", platform_spec.full_name + assert_nil platform_spec.ruby_abi + end + + def test_load_specs_compact_index_skips_content_addressable_rows_without_ruby_field + versions_body = +"created_at: 2026-01-01T00:00:00Z\n---\na 1-abcdef12 0000\n" + versions_response = util_compact_index_response(versions_body) + versions_response.uri = Gem::URI("#{@gem_repo}versions") + @fetcher.data["#{@gem_repo}versions"] = versions_response + @fetcher.data["#{@gem_repo}info/a"] = util_compact_index_response("---\n1-abcdef12 |checksum:123,platform:= x86_64-linux\n") + + specs = @source.decode_content_addressable_tuples(@source.load_specs(:released)) + + assert_empty specs + end + def test_load_specs_compact_index_latest_per_platform a1 = util_spec "a", "1" a2_java = util_spec "a", "2" do |s| diff --git a/test/rubygems/test_gem_specification.rb b/test/rubygems/test_gem_specification.rb index 0f2126f4c914..d0c5f1eb2466 100644 --- a/test/rubygems/test_gem_specification.rb +++ b/test/rubygems/test_gem_specification.rb @@ -1943,6 +1943,11 @@ def test_ruby_abi_returns_nil_for_non_pessimistic_operator assert_nil spec.ruby_abi end + def test_ruby_abi_returns_nil_for_default_required_ruby_version + spec = Gem::Specification.new + assert_nil spec.ruby_abi + end + def test_full_name assert_equal "a-1", @a1.full_name