diff options
author | Hiroshi SHIBATA <hsbt@ruby-lang.org> | 2022-12-15 18:00:04 +0900 |
---|---|---|
committer | Hiroshi SHIBATA <hsbt@ruby-lang.org> | 2022-12-15 19:06:40 +0900 |
commit | 49b0f3b024855efad66a386595eabd103058826d (patch) | |
tree | 629182a46cb9e8b4bc9661878e07e1e9a539ac32 /spec/bundler/support/artifice/helpers | |
parent | 2581de112c1957dc4b5852e54337551dc8972c99 (diff) |
Merge RubyGems/Bundler master
Pick from https://github.com/rubygems/rubygems/commit/084f7d1f21f6fc3e2bb685b7bda3653fb2891c6e
Notes
Notes:
Merged: https://github.com/ruby/ruby/pull/6936
Diffstat (limited to 'spec/bundler/support/artifice/helpers')
9 files changed, 494 insertions, 0 deletions
diff --git a/spec/bundler/support/artifice/helpers/artifice.rb b/spec/bundler/support/artifice/helpers/artifice.rb new file mode 100644 index 0000000000..b8c78614fb --- /dev/null +++ b/spec/bundler/support/artifice/helpers/artifice.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# This module was initially borrowed from https://github.com/wycats/artifice +module Artifice + # Activate Artifice with a particular Rack endpoint. + # + # Calling this method will replace the Net::HTTP system + # with a replacement that routes all requests to the + # Rack endpoint. + # + # @param [#call] endpoint A valid Rack endpoint + def self.activate_with(endpoint) + require_relative "rack_request" + + Net::HTTP.endpoint = endpoint + replace_net_http(Artifice::Net::HTTP) + end + + # Deactivate the Artifice replacement. + def self.deactivate + replace_net_http(::Net::HTTP) + end + + def self.replace_net_http(value) + ::Net.class_eval do + remove_const(:HTTP) + const_set(:HTTP, value) + end + end +end diff --git a/spec/bundler/support/artifice/helpers/compact_index.rb b/spec/bundler/support/artifice/helpers/compact_index.rb new file mode 100644 index 0000000000..4df47a9659 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/compact_index.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require_relative "endpoint" + +$LOAD_PATH.unshift Dir[Spec::Path.base_system_gem_path.join("gems/compact_index*/lib")].first.to_s +require "compact_index" + +class CompactIndexAPI < Endpoint + helpers do + include Spec::Path + + def load_spec(name, version, platform, gem_repo) + full_name = "#{name}-#{version}" + full_name += "-#{platform}" if platform != "ruby" + Marshal.load(Bundler.rubygems.inflate(File.binread(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")))) + end + + def etag_response + response_body = yield + checksum = Digest(:MD5).hexdigest(response_body) + return if not_modified?(checksum) + headers "ETag" => quote(checksum) + headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" + content_type "text/plain" + requested_range_for(response_body) + rescue StandardError => e + puts e + puts e.backtrace + raise + end + + def not_modified?(checksum) + etags = parse_etags(request.env["HTTP_IF_NONE_MATCH"]) + + return unless etags.include?(checksum) + headers "ETag" => quote(checksum) + status 304 + body "" + end + + def requested_range_for(response_body) + ranges = Rack::Utils.byte_ranges(env, response_body.bytesize) + + if ranges + status 206 + body ranges.map! {|range| slice_body(response_body, range) }.join + else + status 200 + body response_body + end + end + + def quote(string) + %("#{string}") + end + + def parse_etags(value) + value ? value.split(/, ?/).select {|s| s.sub!(/"(.*)"/, '\1') } : [] + end + + def slice_body(body, range) + body.byteslice(range) + end + + def gems(gem_repo = default_gem_repo) + @gems ||= {} + @gems[gem_repo] ||= begin + specs = Bundler::Deprecate.skip_during do + %w[specs.4.8 prerelease_specs.4.8].map do |filename| + Marshal.load(File.open(gem_repo.join(filename)).read).map do |name, version, platform| + load_spec(name, version, platform, gem_repo) + end + end.flatten + end + + specs.group_by(&:name).map do |name, versions| + gem_versions = versions.map do |spec| + deps = spec.dependencies.select {|d| d.type == :runtime }.map do |d| + reqs = d.requirement.requirements.map {|r| r.join(" ") }.join(", ") + CompactIndex::Dependency.new(d.name, reqs) + end + checksum = begin + Digest(:SHA256).file("#{gem_repo}/gems/#{spec.original_name}.gem").base64digest + rescue StandardError + nil + end + CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, checksum, nil, + deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s) + end + CompactIndex::Gem.new(name, gem_versions) + end + end + end + end + + get "/names" do + etag_response do + CompactIndex.names(gems.map(&:name)) + end + end + + get "/versions" do + etag_response do + file = tmp("versions.list") + FileUtils.rm_f(file) + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + file.contents + end + end + + get "/info/:name" do + etag_response do + gem = gems.find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end +end diff --git a/spec/bundler/support/artifice/helpers/compact_index_extra.rb b/spec/bundler/support/artifice/helpers/compact_index_extra.rb new file mode 100644 index 0000000000..9e742630dd --- /dev/null +++ b/spec/bundler/support/artifice/helpers/compact_index_extra.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative "compact_index" + +class CompactIndexExtra < CompactIndexAPI + get "/extra/versions" do + halt 404 + end + + get "/extra/api/v1/dependencies" do + halt 404 + end + + get "/extra/specs.4.8.gz" do + File.binread("#{gem_repo2}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.binread("#{gem_repo2}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.binread("#{gem_repo2}/gems/#{params[:id]}") + end +end diff --git a/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb b/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb new file mode 100644 index 0000000000..d9a7d83d23 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require_relative "compact_index" + +class CompactIndexExtraApi < CompactIndexAPI + get "/extra/names" do + etag_response do + CompactIndex.names(gems(gem_repo4).map(&:name)) + end + end + + get "/extra/versions" do + etag_response do + file = tmp("versions.list") + FileUtils.rm_f(file) + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems(gem_repo4)) + file.contents + end + end + + get "/extra/info/:name" do + etag_response do + gem = gems(gem_repo4).find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end + + get "/extra/specs.4.8.gz" do + File.binread("#{gem_repo4}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.binread("#{gem_repo4}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.binread("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.binread("#{gem_repo4}/gems/#{params[:id]}") + end +end diff --git a/spec/bundler/support/artifice/helpers/endpoint.rb b/spec/bundler/support/artifice/helpers/endpoint.rb new file mode 100644 index 0000000000..fc0381dc38 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/endpoint.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require_relative "../../path" + +$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gem_path.join("gems/{mustermann,rack,tilt,sinatra,ruby2_keywords}-*/lib")].map(&:to_s)) + +require "sinatra/base" + +ALL_REQUESTS = [] # rubocop:disable Style/MutableConstant +ALL_REQUESTS_MUTEX = Thread::Mutex.new + +at_exit do + if expected = ENV["BUNDLER_SPEC_ALL_REQUESTS"] + expected = expected.split("\n").sort + actual = ALL_REQUESTS.sort + + unless expected == actual + raise "Unexpected requests!\nExpected:\n\t#{expected.join("\n\t")}\n\nActual:\n\t#{actual.join("\n\t")}" + end + end +end + +class Endpoint < Sinatra::Base + def self.all_requests + @all_requests ||= [] + end + + set :raise_errors, true + set :show_exceptions, false + + def call!(*) + super.tap do + ALL_REQUESTS_MUTEX.synchronize do + ALL_REQUESTS << @request.url + end + end + end + + helpers do + include Spec::Path + + def default_gem_repo + if ENV["BUNDLER_SPEC_GEM_REPO"] + Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"]) + else + case request.host + when "gem.repo1" + Spec::Path.gem_repo1 + when "gem.repo2" + Spec::Path.gem_repo2 + when "gem.repo3" + Spec::Path.gem_repo3 + when "gem.repo4" + Spec::Path.gem_repo4 + else + Spec::Path.gem_repo1 + end + end + end + + def dependencies_for(gem_names, gem_repo = default_gem_repo) + return [] if gem_names.nil? || gem_names.empty? + + all_specs = %w[specs.4.8 prerelease_specs.4.8].map do |filename| + Marshal.load(File.open(gem_repo.join(filename)).read) + end.inject(:+) + + all_specs.map do |name, version, platform| + spec = load_spec(name, version, platform, gem_repo) + next unless gem_names.include?(spec.name) + { + :name => spec.name, + :number => spec.version.version, + :platform => spec.platform.to_s, + :dependencies => spec.dependencies.select {|dep| dep.type == :runtime }.map do |dep| + [dep.name, dep.requirement.requirements.map {|a| a.join(" ") }.join(", ")] + end, + } + end.compact + end + + def load_spec(name, version, platform, gem_repo) + full_name = "#{name}-#{version}" + full_name += "-#{platform}" if platform != "ruby" + Marshal.load(Bundler.rubygems.inflate(File.binread(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")))) + end + end + + get "/quick/Marshal.4.8/:id" do + redirect "/fetch/actual/gem/#{params[:id]}" + end + + get "/fetch/actual/gem/:id" do + File.binread("#{default_gem_repo}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/gems/:id" do + File.binread("#{default_gem_repo}/gems/#{params[:id]}") + end + + get "/api/v1/dependencies" do + Marshal.dump(dependencies_for(params[:gems])) + end + + get "/specs.4.8.gz" do + File.binread("#{default_gem_repo}/specs.4.8.gz") + end + + get "/prerelease_specs.4.8.gz" do + File.binread("#{default_gem_repo}/prerelease_specs.4.8.gz") + end +end diff --git a/spec/bundler/support/artifice/helpers/endpoint_extra.rb b/spec/bundler/support/artifice/helpers/endpoint_extra.rb new file mode 100644 index 0000000000..ad08495b50 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/endpoint_extra.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative "endpoint" + +class EndpointExtra < Endpoint + get "/extra/api/v1/dependencies" do + halt 404 + end + + get "/extra/specs.4.8.gz" do + File.binread("#{gem_repo2}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.binread("#{gem_repo2}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.binread("#{gem_repo2}/gems/#{params[:id]}") + end +end diff --git a/spec/bundler/support/artifice/helpers/endpoint_fallback.rb b/spec/bundler/support/artifice/helpers/endpoint_fallback.rb new file mode 100644 index 0000000000..a232930b67 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/endpoint_fallback.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative "endpoint" + +class EndpointFallback < Endpoint + DEPENDENCY_LIMIT = 60 + + get "/api/v1/dependencies" do + if params[:gems] && params[:gems].size <= DEPENDENCY_LIMIT + Marshal.dump(dependencies_for(params[:gems])) + else + halt 413, "Too many gems to resolve, please request less than #{DEPENDENCY_LIMIT} gems" + end + end +end diff --git a/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb b/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb new file mode 100644 index 0000000000..c409d39d99 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require_relative "endpoint_fallback" + +class EndpointMarshalFail < EndpointFallback + get "/api/v1/dependencies" do + "f0283y01hasf" + end +end diff --git a/spec/bundler/support/artifice/helpers/rack_request.rb b/spec/bundler/support/artifice/helpers/rack_request.rb new file mode 100644 index 0000000000..c4a07812a6 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/rack_request.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require "rack/test" +require "net/http" + +module Artifice + module Net + # This is an internal object that can receive Rack requests + # to the application using the Rack::Test API + class RackRequest + include Rack::Test::Methods + attr_reader :app + + def initialize(app) + @app = app + end + end + + class HTTP < ::Net::HTTP + class << self + attr_accessor :endpoint + end + + # Net::HTTP uses a @newimpl instance variable to decide whether + # to use a legacy implementation. Since we are subclassing + # Net::HTTP, we must set it + @newimpl = true + + # We don't need to connect, so blank out this method + def connect + end + + # Replace the Net::HTTP request method with a method + # that converts the request into a Rack request and + # dispatches it to the Rack endpoint. + # + # @param [Net::HTTPRequest] req A Net::HTTPRequest + # object, or one if its subclasses + # @param [optional, String, #read] body This should + # be sent as "rack.input". If it's a String, it will + # be converted to a StringIO. + # @return [Net::HTTPResponse] + # + # @yield [Net::HTTPResponse] If a block is provided, + # this method will yield the Net::HTTPResponse to + # it after the body is read. + def request(req, body = nil, &block) + rack_request = RackRequest.new(self.class.endpoint) + + req.each_header do |header, value| + rack_request.header(header, value) + end + + scheme = use_ssl? ? "https" : "http" + prefix = "#{scheme}://#{addr_port}" + body_stream_contents = req.body_stream.read if req.body_stream + + response = rack_request.request("#{prefix}#{req.path}", + { :method => req.method, :input => body || req.body || body_stream_contents }) + + make_net_http_response(response, &block) + end + + private + + # This method takes a Rack response and creates a Net::HTTPResponse + # Instead of trying to mock HTTPResponse directly, we just convert + # the Rack response into a String that looks like a normal HTTP + # response and call Net::HTTPResponse.read_new + # + # @param [Array(#to_i, Hash, #each)] response a Rack response + # @return [Net::HTTPResponse] + # @yield [Net::HTTPResponse] If a block is provided, yield the + # response to it after the body is read + def make_net_http_response(response) + status = response.status + headers = response.headers + body = response.body + + response_string = [] + response_string << "HTTP/1.1 #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}" + + headers.each do |header, value| + response_string << "#{header}: #{value}" + end + + response_string << "" << body + + response_io = ::Net::BufferedIO.new(StringIO.new(response_string.join("\n"))) + res = ::Net::HTTPResponse.read_new(response_io) + + res.reading_body(response_io, true) do + yield res if block_given? + end + + res + end + end + end +end |