diff --git a/lib/rubygems/resolver/api_specification.rb b/lib/rubygems/resolver/api_specification.rb index a14bcbfeb1a6..8772c103beee 100644 --- a/lib/rubygems/resolver/api_specification.rb +++ b/lib/rubygems/resolver/api_specification.rb @@ -87,19 +87,24 @@ def pretty_print(q) # :nodoc: # Fetches a Gem::Specification for this APISpecification. def spec # :nodoc: - @spec ||= - begin - tuple = Gem::NameTuple.new @name, @version, @platform - source.fetch_spec tuple - rescue Gem::RemoteFetcher::FetchError - raise if @original_platform == @platform - - tuple = Gem::NameTuple.new @name, @version, @original_platform - source.fetch_spec tuple - end + @spec ||= build_minimal_spec_from_compact_index end def source # :nodoc: @set.source end + + private + + def build_minimal_spec_from_compact_index + spec = Gem::Specification.new + spec.name = @name + spec.version = @version + spec.platform = @platform + spec.dependencies.replace(@dependencies) + spec.required_ruby_version = @required_ruby_version + spec.required_rubygems_version = @required_rubygems_version + + spec + end end diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index dc40f4ecb1f8..4d2f889d1b46 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -1143,6 +1143,25 @@ def write_marshalled_gemspecs(*all_specs) end end + ## + # Sets up Compact Index API endpoints for testing. Does NOT set up old marshal API. + # This causes Source#new_dependency_resolver_set to use APISet. + # + # Usage: + # compact_index do |ci| + # ci.gem "a", 1 do |s| + # s.add_dependency "b", ">= 2.0" + # end + # ci.gem "b", 2 + # end + + def compact_index(&block) + fake_compact_index = Gem::TestCase::CompactIndexSetup.new(self, @gem_repo) + yield fake_compact_index + fake_compact_index.stub + fake_compact_index.specs + end + ## # Deflates +data+ diff --git a/test/rubygems/test_gem_commands_install_command.rb b/test/rubygems/test_gem_commands_install_command.rb index d2ca933a632c..617c678f6c00 100644 --- a/test/rubygems/test_gem_commands_install_command.rb +++ b/test/rubygems/test_gem_commands_install_command.rb @@ -29,9 +29,9 @@ def teardown end def test_execute_exclude_prerelease - spec_fetcher do |fetcher| - fetcher.gem "a", 2 - fetcher.gem "a", "2.pre" + compact_index do |ci| + ci.gem "a", 2 + ci.gem "a", "2.pre" end @cmd.options[:args] = %w[a] @@ -46,9 +46,9 @@ def test_execute_exclude_prerelease end def test_execute_explicit_version_includes_prerelease - specs = spec_fetcher do |fetcher| - fetcher.gem "a", 2 - fetcher.gem "a", "2.a" + specs = compact_index do |ci| + ci.gem "a", 2 + ci.gem "a", "2.a" end a2_pre = specs["a-2.a"] @@ -430,9 +430,9 @@ def test_execute_nonexistent_with_dashes end def test_execute_prerelease_skipped_when_no_flag_set - spec_fetcher do |fetcher| - fetcher.gem "a", 1 - fetcher.gem "a", "3.a" + compact_index do |ci| + ci.gem "a", 1 + ci.gem "a", "3.a" end @cmd.options[:prerelease] = false @@ -483,9 +483,9 @@ def test_execute_with_version_specified_by_colon end def test_execute_prerelease_skipped_when_non_pre_available - spec_fetcher do |fetcher| - fetcher.gem "a", "2.pre" - fetcher.gem "a", 2 + compact_index do |ci| + ci.gem "a", "2.pre" + ci.gem "a", 2 end @cmd.options[:prerelease] = true @@ -532,9 +532,9 @@ def test_execute_required_ruby_version def test_execute_required_ruby_version_upper_bound local = Gem::Platform.local - spec_fetcher do |fetcher| - fetcher.gem "a", 2.0 - fetcher.gem "a", 2.0 do |s| + compact_index do |ci| + ci.gem "a", 2.0 + ci.gem "a", 2.0 do |s| s.required_ruby_version = "< #{RUBY_VERSION}.a" s.platform = local end @@ -552,8 +552,8 @@ def test_execute_required_ruby_version_upper_bound end def test_execute_required_ruby_version_specific_not_met - spec_fetcher do |fetcher| - fetcher.gem "a", "1.0" do |s| + compact_index do |ci| + ci.gem "a", "1.0" do |s| s.required_ruby_version = "= 1.4.6" end end @@ -572,8 +572,8 @@ def test_execute_required_ruby_version_specific_not_met end def test_execute_required_ruby_version_specific_prerelease_met - spec_fetcher do |fetcher| - fetcher.gem "a", "1.0" do |s| + compact_index do |ci| + ci.gem "a", "1.0" do |s| s.required_ruby_version = ">= 1.4.6.preview2" end end @@ -592,8 +592,8 @@ def test_execute_required_ruby_version_specific_prerelease_met def test_execute_required_ruby_version_specific_prerelease_not_met next_ruby_pre = Gem.ruby_version.segments.map.with_index {|n, i| i == 1 ? n + 1 : n }.join(".") + ".a" - spec_fetcher do |fetcher| - fetcher.gem "a", "1.0" do |s| + compact_index do |ci| + ci.gem "a", "1.0" do |s| s.required_ruby_version = "> #{next_ruby_pre}" end end @@ -612,8 +612,8 @@ def test_execute_required_ruby_version_specific_prerelease_not_met end def test_execute_required_rubygems_version_wrong - spec_fetcher do |fetcher| - fetcher.gem "a", "1.0" do |s| + compact_index do |ci| + ci.gem "a", "1.0" do |s| s.required_rubygems_version = "< 0" end end @@ -632,8 +632,8 @@ def test_execute_required_rubygems_version_wrong end def test_execute_rdoc - specs = spec_fetcher do |fetcher| - fetcher.gem "a", 2 + specs = compact_index do |ci| + ci.gem "a", 2 end Gem.done_installing(&Gem::RDoc.method(:generation_hook)) @@ -661,8 +661,8 @@ def test_execute_rdoc end if defined?(Gem::RDoc) && !Gem.rdoc_hooks_defined_via_plugin? def test_execute_rdoc_with_path - specs = spec_fetcher do |fetcher| - fetcher.gem "a", 2 + specs = compact_index do |ci| + ci.gem "a", 2 end Gem.done_installing(&Gem::RDoc.method(:generation_hook)) @@ -690,8 +690,8 @@ def test_execute_rdoc_with_path end if defined?(Gem::RDoc) && !Gem.rdoc_hooks_defined_via_plugin? def test_execute_saves_build_args - specs = spec_fetcher do |fetcher| - fetcher.gem "a", 2 + specs = compact_index do |ci| + ci.gem "a", 2 end args = %w[--with-awesome=true --more-awesome=yes] @@ -720,8 +720,8 @@ def test_execute_saves_build_args end def test_execute_remote - spec_fetcher do |fetcher| - fetcher.gem "a", 2 + compact_index do |ci| + ci.gem "a", 2 end @cmd.options[:args] = %w[a] @@ -740,8 +740,8 @@ def test_execute_remote def test_execute_with_invalid_gem_file FileUtils.touch("a.gem") - spec_fetcher do |fetcher| - fetcher.gem "a", 2 + compact_index do |ci| + ci.gem "a", 2 end @cmd.options[:args] = %w[a] @@ -758,8 +758,8 @@ def test_execute_with_invalid_gem_file end def test_execute_remote_truncates_existing_gemspecs - spec_fetcher do |fetcher| - fetcher.gem "a", 1 + compact_index do |ci| + ci.gem "a", 1 end @cmd.options[:domain] = :remote @@ -791,9 +791,9 @@ def test_execute_remote_truncates_existing_gemspecs end def test_execute_remote_ignores_files - specs = spec_fetcher do |fetcher| - fetcher.gem "a", 1 - fetcher.gem "a", 2 + specs = compact_index do |ci| + ci.gem "a", 1 + ci.gem "a", 2 end @cmd.options[:domain] = :remote @@ -887,11 +887,11 @@ def test_execute_two_version end def test_execute_two_version_specified_by_colon - spec_fetcher do |fetcher| - fetcher.gem "a", 1 - fetcher.gem "a", 2 - fetcher.gem "b", 1 - fetcher.gem "b", 2 + compact_index do |ci| + ci.gem "a", 1 + ci.gem "a", 2 + ci.gem "b", 1 + ci.gem "b", 2 end @cmd.options[:args] = %w[a:1 b:1] @@ -956,8 +956,8 @@ def test_install_gem_ignore_dependencies_both end def test_install_gem_ignore_dependencies_remote - spec_fetcher do |fetcher| - fetcher.gem "a", 2 + compact_index do |ci| + ci.gem "a", 2 end @cmd.options[:ignore_dependencies] = true @@ -969,10 +969,10 @@ def test_install_gem_ignore_dependencies_remote def test_install_gem_ignore_dependencies_remote_platform_local local = Gem::Platform.local - spec_fetcher do |fetcher| - fetcher.gem "a", 3 + compact_index do |ci| + ci.gem "a", 3 - fetcher.gem "a", 3 do |s| + ci.gem "a", 3 do |s| s.platform = local end end @@ -1365,9 +1365,9 @@ def test_execute_with_gemdeps_path_ignores_system end def test_execute_uses_deps_a_gemdeps_with_a_path - specs = spec_fetcher do |fetcher| - fetcher.gem "q", "1.0" - fetcher.gem "r", "2.0", "q" => nil + specs = compact_index do |ci| + ci.gem "q", "1.0" + ci.gem "r", "2.0", "q" => nil end i = Gem::Installer.at specs["q-1.0"].cache_file, install_dir: "gf-path" @@ -1568,8 +1568,8 @@ def test_explain_platform_ruby_ignore_dependencies def test_suggest_update_if_enabled TestUpdateSuggestion.with_eligible_environment(cmd: @cmd) do - spec_fetcher do |fetcher| - fetcher.gem "a", 2 + compact_index do |ci| + ci.gem "a", 2 end @cmd.options[:args] = %w[a] @@ -1642,4 +1642,32 @@ def test_execute_bindir_with_nonexistent_parent_dirs assert_equal %w[a-2], @cmd.installed_specs.map(&:full_name) end + + def test_install_from_compact_index_only_source + compact_index do |ci| + ci.gem "main-gem", "2" do |s| + s.add_dependency "dep-gem", ">= 1.0" + end + ci.gem "dep-gem", "1" + end + + @cmd.options[:args] = %w[main-gem] + + use_ui @ui do + assert_raise Gem::MockGemUi::SystemExitException, @ui.error do + @cmd.execute + end + end + + assert_equal %w[dep-gem-1 main-gem-2], @cmd.installed_specs.map(&:full_name).sort + + installed_gem = @cmd.installed_specs.find {|s| s.name == "main-gem" } + assert_equal "main-gem", installed_gem.name + assert_equal Gem::Version.new("2"), installed_gem.version + assert_equal 1, installed_gem.dependencies.size + assert_equal "dep-gem", installed_gem.dependencies.first.name + + marshal_requests = @fetcher.paths.select {|p| p.include?("/quick/Marshal.4.8/") } + assert_empty marshal_requests, "Should not request marshal gemspecs: #{marshal_requests.inspect}" + end end diff --git a/test/rubygems/test_gem_resolver_api_specification.rb b/test/rubygems/test_gem_resolver_api_specification.rb index 2119d734780b..766623049783 100644 --- a/test/rubygems/test_gem_resolver_api_specification.rb +++ b/test/rubygems/test_gem_resolver_api_specification.rb @@ -164,4 +164,55 @@ def test_spec_jruby_platform assert_kind_of Gem::Specification, spec assert_equal "j-1-java", spec.full_name end + + def test_spec_builds_from_compact_index_without_marshal_gemspec + dep_uri = @gem_repo + "info" + set = Gem::Resolver::APISet.new dep_uri + data = { + name: "rails", + number: "7.0.0", + platform: "ruby", + dependencies: [ + ["activesupport", "= 7.0.0"], + ["bundler", ">= 1.15.0"], + ], + requirements: { + ruby: ">= 3.4.0", + rubygems: ">= 4.0.0", + }, + } + + api_spec = Gem::Resolver::APISpecification.new set, data + spec = api_spec.spec + + assert_kind_of Gem::Specification, spec + assert_equal "rails", spec.name + assert_equal Gem::Version.new("7.0.0"), spec.version + assert_equal Gem::Platform::RUBY, spec.platform + + assert_equal 2, spec.dependencies.size + assert spec.dependencies.any? {|d| d.name == "activesupport" && d.requirement.to_s == "= 7.0.0" } + assert spec.dependencies.any? {|d| d.name == "bundler" && d.requirement.to_s == ">= 1.15.0" } + + assert_equal ">= 3.4.0", spec.required_ruby_version.to_s + assert_equal ">= 4.0.0", spec.required_rubygems_version.to_s + end + + def test_spec_builds_without_requirements + dep_uri = @gem_repo + "info" + set = Gem::Resolver::APISet.new dep_uri + data = { + name: "simple_gem", + number: "1.0.0", + platform: "ruby", + dependencies: [], + } + + api_spec = Gem::Resolver::APISpecification.new set, data + spec = api_spec.spec + + assert_kind_of Gem::Specification, spec + assert_equal "simple_gem", spec.name + assert_equal Gem::Version.new("1.0.0"), spec.version + end end diff --git a/test/rubygems/utilities.rb b/test/rubygems/utilities.rb index bf601f6fac2d..124dbc9579c1 100644 --- a/test/rubygems/utilities.rb +++ b/test/rubygems/utilities.rb @@ -420,6 +420,106 @@ def write_spec(spec) # :nodoc: end end +## +# Minimal CompactIndex implementation for tests. +# This is a simplified version that only implements what's needed for test fixtures. +module CompactIndexBuilder + # Generates the /info/{gem_name} response body + # Format: ---\nVERSION DEPS|METADATA\n + # Where DEPS is: dep_name:requirement,dep_name:requirement + # And METADATA is: checksum:SHA256,ruby:requirement,rubygems:requirement + def self.info(versions) + lines = ["---"] + versions.each do |version| + # Add dependencies (if any) + deps = version.dependencies.map {|d| "#{d.name}:#{d.requirement}" } + deps_string = deps.join(",") + + # Build metadata + metadata = [] + metadata << "checksum:#{version.checksum}" if version.checksum + metadata << "ruby:#{version.ruby_version}" if version.ruby_version && version.ruby_version != ">= 0" + metadata << "rubygems:#{version.rubygems_version}" if version.rubygems_version && version.rubygems_version != ">= 0" + + # Format: "VERSION DEPS|METADATA" or "VERSION |METADATA" (space before | only when no deps) + line = "#{version.version} #{deps_string}|" + metadata.join(",") + lines << line + end + lines.join("\n") << "\n" + end + + GemVersion = Data.define(:version, :platform, :checksum, :info_checksum, :dependencies, :ruby_version, :rubygems_version) do + def initialize(version:, platform:, checksum:, info_checksum: nil, dependencies: [], ruby_version: nil, rubygems_version: nil) + super(version:, platform:, checksum:, info_checksum:, dependencies:, ruby_version:, rubygems_version:) + end + end + + Dependency = Data.define(:name, :requirement) +end + +## +# The CompactIndexSetup allows easy setup of compact index endpoints in tests. +# Unlike SpecFetcherSetup, this only sets up compact index (no marshal API). +# +# compact_index do |ci| +# ci.gem "a", 1 do |s| +# s.add_dependency "b", "~> 2.0" +# end +# ci.gem "b", 2 +# end + +class Gem::TestCase::CompactIndexSetup + attr_reader :specs + + def initialize(test, repository) + @test = test + @repository = repository + @test.fetcher = Gem::FakeFetcher.new + Gem::RemoteFetcher.fetcher = @test.fetcher + @specs = {} + end + + def gem(name, version, dependencies = nil, &block) + spec = @test.util_spec(name, version, dependencies, &block) + @specs[spec.full_name] = spec + end + + def stub + @specs.values.group_by(&:name).each do |name, gem_specs| + versions = gem_specs.map do |spec| + gem_file = Gem::Package.build(spec) + gem_contents = Gem.read_binary(gem_file) + FileUtils.cp gem_file, spec.cache_file + + @test.fetcher.data["#{@repository}gems/#{spec.file_name}"] = gem_contents + + dependencies = spec.dependencies.select(&:runtime?).map do |dep| + CompactIndexBuilder::Dependency.new(dep.name, dep.requirement.to_s) + end + + checksum = Digest::SHA256.hexdigest(gem_contents) + + CompactIndexBuilder::GemVersion.new( + spec.version.to_s, + spec.platform.to_s, + checksum, + nil, + dependencies, + spec.required_ruby_version.to_s, + spec.required_rubygems_version.to_s + ) + end + + @test.fetcher.data["#{@repository}info/#{name}"] = CompactIndexBuilder.info(versions) + end + + # stub only to pass Compact Index API presence check currently + versions_response = Gem::Net::HTTPResponse.new "1.1", 200, "OK" + versions_response.uri = Gem::URI("#{@repository}versions") + @test.fetcher.data["#{@repository}versions"] = versions_response + end +end + ## # A StringIO duck-typed class that uses Tempfile instead of String as the # backing store.