diff options
Diffstat (limited to 'spec/bundler/support')
82 files changed, 5332 insertions, 0 deletions
diff --git a/spec/bundler/support/activate.rb b/spec/bundler/support/activate.rb new file mode 100644 index 0000000000..143b77833d --- /dev/null +++ b/spec/bundler/support/activate.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "rubygems" +Gem.instance_variable_set(:@ruby, ENV["RUBY"]) if ENV["RUBY"] + +require_relative "path" +bundler_gemspec = Spec::Path.loaded_gemspec +bundler_gemspec.instance_variable_set(:@full_gem_path, Spec::Path.source_root.to_s) +bundler_gemspec.activate if bundler_gemspec.respond_to?(:activate) diff --git a/spec/bundler/support/artifice/compact_index.rb b/spec/bundler/support/artifice/compact_index.rb new file mode 100644 index 0000000000..ebc4d0ae5b --- /dev/null +++ b/spec/bundler/support/artifice/compact_index.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexAPI) diff --git a/spec/bundler/support/artifice/compact_index_api_missing.rb b/spec/bundler/support/artifice/compact_index_api_missing.rb new file mode 100644 index 0000000000..f771f7d1f0 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_api_missing.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexApiMissing < CompactIndexAPI + get "/fetch/actual/gem/:id" do + halt 404 + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexApiMissing) diff --git a/spec/bundler/support/artifice/compact_index_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_basic_authentication.rb new file mode 100644 index 0000000000..b9115cdd86 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_basic_authentication.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexBasicAuthentication < CompactIndexAPI + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexBasicAuthentication) diff --git a/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb new file mode 100644 index 0000000000..83b147d2ae --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexChecksumMismatch < CompactIndexAPI + get "/versions" do + headers "Repr-Digest" => "sha-256=:ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=:" + headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" + content_type "text/plain" + body "content does not match the checksum" + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexChecksumMismatch) diff --git a/spec/bundler/support/artifice/compact_index_concurrent_download.rb b/spec/bundler/support/artifice/compact_index_concurrent_download.rb new file mode 100644 index 0000000000..5d55b8a72b --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_concurrent_download.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexConcurrentDownload < CompactIndexAPI + get "/versions" do + versions = File.join(Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions") + + # Verify the original content hasn't been deleted, e.g. on a retry + data = File.binread(versions) + data == "created_at" || raise("Original file should be present with expected content") + + # Verify this is only requested once for a partial download + env["HTTP_RANGE"] == "bytes=#{data.bytesize - 1}-" || raise("Missing Range header for expected partial download") + + # Overwrite the file in parallel, which should be then overwritten + # after a successful download to prevent corruption + File.open(versions, "w") {|f| f.puts "another process" } + + 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 +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexConcurrentDownload) diff --git a/spec/bundler/support/artifice/compact_index_cooldown.rb b/spec/bundler/support/artifice/compact_index_cooldown.rb new file mode 100644 index 0000000000..85e3173c98 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_cooldown.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index_cooldown" +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexCooldownAPI) diff --git a/spec/bundler/support/artifice/compact_index_creds_diff_host.rb b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb new file mode 100644 index 0000000000..282e9c8961 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexCredsDiffHost < CompactIndexAPI + helpers do + def auth + @auth ||= Rack::Auth::Basic::Request.new(request.env) + end + + def authorized? + auth.provided? && auth.basic? && auth.credentials && auth.credentials == %w[user pass] + end + + def protected! + return if authorized? + response["WWW-Authenticate"] = %(Basic realm="Testing HTTP Auth") + throw(:halt, [401, "Not authorized\n"]) + end + end + + before do + protected! unless request.path_info.include?("/no/creds/") + end + + get "/gems/:id" do + redirect "http://diffhost.test/no/creds/#{params[:id]}" + end + + get "/no/creds/:id" do + if request.host.include?("diffhost") && !auth.provided? + File.binread("#{gem_repo1}/gems/#{params[:id]}") + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexCredsDiffHost) diff --git a/spec/bundler/support/artifice/compact_index_etag_match.rb b/spec/bundler/support/artifice/compact_index_etag_match.rb new file mode 100644 index 0000000000..6c62166051 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_etag_match.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexEtagMatch < CompactIndexAPI + get "/versions" do + raise ArgumentError, "ETag header should be present" unless env["HTTP_IF_NONE_MATCH"] + headers "ETag" => env["HTTP_IF_NONE_MATCH"] + status 304 + body "" + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexEtagMatch) diff --git a/spec/bundler/support/artifice/compact_index_extra.rb b/spec/bundler/support/artifice/compact_index_extra.rb new file mode 100644 index 0000000000..cd41b3ecca --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_extra.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index_extra" +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexExtra) diff --git a/spec/bundler/support/artifice/compact_index_extra_api.rb b/spec/bundler/support/artifice/compact_index_extra_api.rb new file mode 100644 index 0000000000..8b9d304ab4 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_extra_api.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index_extra_api" +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexExtraApi) diff --git a/spec/bundler/support/artifice/compact_index_extra_api_missing.rb b/spec/bundler/support/artifice/compact_index_extra_api_missing.rb new file mode 100644 index 0000000000..df6ede584c --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_extra_api_missing.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index_extra_api" + +class CompactIndexExtraAPIMissing < CompactIndexExtraApi + get "/extra/fetch/actual/gem/:id" do + if params[:id] == "missing-1.0.gemspec.rz" + halt 404 + else + File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexExtraAPIMissing) diff --git a/spec/bundler/support/artifice/compact_index_extra_missing.rb b/spec/bundler/support/artifice/compact_index_extra_missing.rb new file mode 100644 index 0000000000..255c89afdb --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_extra_missing.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index_extra" + +class CompactIndexExtraMissing < CompactIndexExtra + get "/extra/fetch/actual/gem/:id" do + if params[:id] == "missing-1.0.gemspec.rz" + halt 404 + else + File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexExtraMissing) diff --git a/spec/bundler/support/artifice/compact_index_forbidden.rb b/spec/bundler/support/artifice/compact_index_forbidden.rb new file mode 100644 index 0000000000..18c30ed9a2 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_forbidden.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexForbidden < CompactIndexAPI + get "/versions" do + halt 403 + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexForbidden) diff --git a/spec/bundler/support/artifice/compact_index_host_redirect.rb b/spec/bundler/support/artifice/compact_index_host_redirect.rb new file mode 100644 index 0000000000..4f82bf3812 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_host_redirect.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexHostRedirect < CompactIndexAPI + get "/fetch/actual/gem/:id", host_name: "localgemserver.test" do + redirect "http://bundler.localgemserver.test#{request.path_info}" + end + + get "/versions" do + status 404 + end + + get "/api/v1/dependencies" do + status 404 + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexHostRedirect) diff --git a/spec/bundler/support/artifice/compact_index_mirror_down.rb b/spec/bundler/support/artifice/compact_index_mirror_down.rb new file mode 100644 index 0000000000..88983c715d --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_mirror_down.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" +require_relative "helpers/artifice" +require_relative "helpers/rack_request" + +module Artifice + module Net + class HTTPMirrorDown < HTTP + def connect + raise SocketError if address == "gem.mirror" + + super + end + end + + HTTP.endpoint = CompactIndexAPI + end + + replace_net_http(Net::HTTPMirrorDown) +end diff --git a/spec/bundler/support/artifice/compact_index_no_checksums.rb b/spec/bundler/support/artifice/compact_index_no_checksums.rb new file mode 100644 index 0000000000..ecb7fc7d7c --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_no_checksums.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexNoChecksums < CompactIndexAPI + get "/info/:name" do + etag_response do + gem = gems.find {|g| g.name == params[:name] } + gem.versions.map(&:number).join("\n") + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexNoChecksums) diff --git a/spec/bundler/support/artifice/compact_index_no_gem.rb b/spec/bundler/support/artifice/compact_index_no_gem.rb new file mode 100644 index 0000000000..71f6629688 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_no_gem.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexNoGem < CompactIndexAPI + get "/gems/:id" do + halt 500 + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexNoGem) diff --git a/spec/bundler/support/artifice/compact_index_partial_update.rb b/spec/bundler/support/artifice/compact_index_partial_update.rb new file mode 100644 index 0000000000..f111d91ef9 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_partial_update.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexPartialUpdate < CompactIndexAPI + # Stub the server to never return 304s. This simulates the behaviour of + # Fastly / Rubygems ignoring ETag headers. + def not_modified?(_checksum) + false + end + + get "/versions" do + cached_versions_path = File.join( + Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions" + ) + + # Verify a cached copy of the versions file exists + unless File.binread(cached_versions_path).start_with?("created_at: ") + raise("Cached versions file should be present and have content") + end + + # Verify that a partial request is made, starting from the index of the + # final byte of the cached file. + unless env["HTTP_RANGE"] == "bytes=#{File.binread(cached_versions_path).bytesize - 1}-" + raise("Range header should be present, and start from the index of the final byte of the cache. #{env["HTTP_RANGE"].inspect}") + end + + etag_response do + # Return the exact contents of the cache. + File.binread(cached_versions_path) + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexPartialUpdate) diff --git a/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb b/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb new file mode 100644 index 0000000000..ac04336636 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +# The purpose of this Artifice is to test that an incremental response is invalidated +# and a second request is issued for the full content. +class CompactIndexPartialUpdateBadDigest < CompactIndexAPI + def partial_update_bad_digest + response_body = yield + if request.env["HTTP_RANGE"] + headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest("wrong digest on ranged request")}:" + else + headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest(response_body)}:" + end + headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" + content_type "text/plain" + requested_range_for(response_body) + end + + get "/versions" do + partial_update_bad_digest do + file = tmp("versions.list") + FileUtils.rm_f(file) + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + file.contents([], calculate_info_checksums: true) + end + end + + get "/info/:name" do + partial_update_bad_digest do + gem = gems.find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexPartialUpdateBadDigest) diff --git a/spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb b/spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb new file mode 100644 index 0000000000..99bae039f0 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +# The purpose of this Artifice is to test that an incremental response is ignored +# when the digest is not present to verify that the partial response is valid. +class CompactIndexPartialUpdateNoDigestNotIncremental < CompactIndexAPI + def partial_update_no_digest + response_body = yield + headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" + content_type "text/plain" + requested_range_for(response_body) + end + + get "/versions" do + partial_update_no_digest do + file = tmp("versions.list") + FileUtils.rm_f(file) + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + lines = file.contents([], calculate_info_checksums: true).split("\n") + name, versions, checksum = lines.last.split(" ") + + # shuffle versions so new versions are not appended to the end + [*lines[0..-2], [name, versions.split(",").reverse.join(","), checksum].join(" ")].join("\n") + end + end + + get "/info/:name" do + partial_update_no_digest do + gem = gems.find {|g| g.name == params[:name] } + lines = CompactIndex.info(gem ? gem.versions : []).split("\n") + + # shuffle versions so new versions are not appended to the end + [lines.first, lines.last, *lines[1..-2]].join("\n") + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexPartialUpdateNoDigestNotIncremental) diff --git a/spec/bundler/support/artifice/compact_index_precompiled_before.rb b/spec/bundler/support/artifice/compact_index_precompiled_before.rb new file mode 100644 index 0000000000..b5f72f546a --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_precompiled_before.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexPrecompiledBefore < CompactIndexAPI + get "/info/:name" do + etag_response do + gem = gems.find {|g| g.name == params[:name] } + move_ruby_variant_to_the_end(CompactIndex.info(gem ? gem.versions : [])) + end + end + + private + + def move_ruby_variant_to_the_end(response) + lines = response.split("\n") + ruby = lines.find {|line| /\A\d+\.\d+\.\d* \|/.match(line) } + lines.delete(ruby) + lines.push(ruby).join("\n") + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexPrecompiledBefore) diff --git a/spec/bundler/support/artifice/compact_index_range_ignored.rb b/spec/bundler/support/artifice/compact_index_range_ignored.rb new file mode 100644 index 0000000000..2303682c1f --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_range_ignored.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexRangeIgnored < CompactIndexAPI + # Stub the server to not return 304 so that we don't bypass all the logic + def not_modified?(_checksum) + false + end + + get "/versions" do + cached_versions_path = File.join( + Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions" + ) + + # Verify a cached copy of the versions file exists + unless File.binread(cached_versions_path).size > 0 + raise("Cached versions file should be present and have content") + end + + # Verify that a partial request is made, starting from the index of the + # final byte of the cached file. + unless env.delete("HTTP_RANGE") + raise("Expected client to write the full response on the first try") + end + + 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 +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexRangeIgnored) diff --git a/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb b/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb new file mode 100644 index 0000000000..8a7c4b79b0 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexRangeNotSatisfiable < CompactIndexAPI + get "/versions" do + if env["HTTP_RANGE"] + status 416 + else + 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 + end + + get "/info/:name" do + if env["HTTP_RANGE"] + status 416 + else + etag_response do + gem = gems.find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexRangeNotSatisfiable) diff --git a/spec/bundler/support/artifice/compact_index_rate_limited.rb b/spec/bundler/support/artifice/compact_index_rate_limited.rb new file mode 100644 index 0000000000..4495491635 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_rate_limited.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexRateLimited < CompactIndexAPI + class RequestCounter + def self.queue + @queue ||= Thread::Queue.new + end + + def self.size + @queue.size + end + + def self.enq(name) + @queue.enq(name) + end + + def self.deq + @queue.deq + end + end + + configure do + RequestCounter.queue + end + + get "/info/:name" do + RequestCounter.enq(params[:name]) + + begin + if RequestCounter.size == 1 + etag_response do + gem = gems.find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + else + status 429 + end + ensure + RequestCounter.deq + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexRateLimited) diff --git a/spec/bundler/support/artifice/compact_index_redirects.rb b/spec/bundler/support/artifice/compact_index_redirects.rb new file mode 100644 index 0000000000..f7ba393239 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_redirects.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexRedirect < CompactIndexAPI + get "/fetch/actual/gem/:id" do + redirect "/fetch/actual/gem/#{params[:id]}" + end + + get "/versions" do + status 404 + end + + get "/api/v1/dependencies" do + status 404 + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexRedirect) diff --git a/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb new file mode 100644 index 0000000000..96259385e7 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexStrictBasicAuthentication < CompactIndexAPI + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + + # Only accepts password == "password" + unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz" + halt 401, "Authentication failed" + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexStrictBasicAuthentication) diff --git a/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb new file mode 100644 index 0000000000..15850599b6 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexWrongDependencies < CompactIndexAPI + get "/info/:name" do + etag_response do + gem = gems.find {|g| g.name == params[:name] } + gem.versions.each {|gv| gv.dependencies.clear } if gem + CompactIndex.info(gem ? gem.versions : []) + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexWrongDependencies) diff --git a/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb new file mode 100644 index 0000000000..9bd2ca0a9d --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexWrongGemChecksum < CompactIndexAPI + get "/info/:name" do + etag_response do + name = params[:name] + gem = gems.find {|g| g.name == name } + # This generates the hexdigest "2222222222222222222222222222222222222222222222222222222222222222" + checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") { "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI=" } + versions = gem ? gem.versions : [] + versions.each {|v| v.checksum = checksum } + CompactIndex.info(versions) + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexWrongGemChecksum) diff --git a/spec/bundler/support/artifice/endpoint.rb b/spec/bundler/support/artifice/endpoint.rb new file mode 100644 index 0000000000..15242a7942 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require_relative "helpers/endpoint" +require_relative "helpers/artifice" + +Artifice.activate_with(Endpoint) diff --git a/spec/bundler/support/artifice/endpoint_500.rb b/spec/bundler/support/artifice/endpoint_500.rb new file mode 100644 index 0000000000..9dd373bbf6 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_500.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "../path" + +$LOAD_PATH.unshift(*Spec::Path.sinatra_dependency_paths) + +require "sinatra/base" + +class Endpoint500 < Sinatra::Base + before do + halt 500 + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(Endpoint500) diff --git a/spec/bundler/support/artifice/endpoint_api_forbidden.rb b/spec/bundler/support/artifice/endpoint_api_forbidden.rb new file mode 100644 index 0000000000..6bdc5896d6 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_api_forbidden.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "helpers/endpoint" + +class EndpointApiForbidden < Endpoint + get "/api/v1/dependencies" do + halt 403 + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(EndpointApiForbidden) diff --git a/spec/bundler/support/artifice/endpoint_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_basic_authentication.rb new file mode 100644 index 0000000000..e8e3569e63 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_basic_authentication.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative "helpers/endpoint" + +class EndpointBasicAuthentication < Endpoint + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(EndpointBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_creds_diff_host.rb b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb new file mode 100644 index 0000000000..9cbb4de61a --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require_relative "helpers/endpoint" + +class EndpointCredsDiffHost < Endpoint + helpers do + def auth + @auth ||= Rack::Auth::Basic::Request.new(request.env) + end + + def authorized? + auth.provided? && auth.basic? && auth.credentials && auth.credentials == %w[user pass] + end + + def protected! + return if authorized? + response["WWW-Authenticate"] = %(Basic realm="Testing HTTP Auth") + throw(:halt, [401, "Not authorized\n"]) + end + end + + before do + protected! unless request.path_info.include?("/no/creds/") + end + + get "/gems/:id" do + redirect "http://diffhost.test/no/creds/#{params[:id]}" + end + + get "/no/creds/:id" do + if request.host.include?("diffhost") && !auth.provided? + File.binread("#{gem_repo1}/gems/#{params[:id]}") + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(EndpointCredsDiffHost) diff --git a/spec/bundler/support/artifice/endpoint_extra.rb b/spec/bundler/support/artifice/endpoint_extra.rb new file mode 100644 index 0000000000..021fd435fe --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_extra.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative "helpers/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 + +require_relative "helpers/artifice" + +Artifice.activate_with(EndpointExtra) diff --git a/spec/bundler/support/artifice/endpoint_extra_api.rb b/spec/bundler/support/artifice/endpoint_extra_api.rb new file mode 100644 index 0000000000..a965af6e73 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_extra_api.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require_relative "helpers/endpoint" + +class EndpointExtraApi < Endpoint + get "/extra/api/v1/dependencies" do + deps = dependencies_for(params[:gems], gem_repo4) + Marshal.dump(deps) + 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 + +require_relative "helpers/artifice" + +Artifice.activate_with(EndpointExtraApi) diff --git a/spec/bundler/support/artifice/endpoint_extra_missing.rb b/spec/bundler/support/artifice/endpoint_extra_missing.rb new file mode 100644 index 0000000000..73e2defb32 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_extra_missing.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "helpers/endpoint_extra" + +class EndpointExtraMissing < EndpointExtra + get "/extra/fetch/actual/gem/:id" do + if params[:id] == "missing-1.0.gemspec.rz" + halt 404 + else + File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(EndpointExtraMissing) diff --git a/spec/bundler/support/artifice/endpoint_fallback.rb b/spec/bundler/support/artifice/endpoint_fallback.rb new file mode 100644 index 0000000000..742e563f07 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_fallback.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require_relative "helpers/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 + +require_relative "helpers/artifice" + +Artifice.activate_with(EndpointFallback) diff --git a/spec/bundler/support/artifice/endpoint_host_redirect.rb b/spec/bundler/support/artifice/endpoint_host_redirect.rb new file mode 100644 index 0000000000..6ce51bed93 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_host_redirect.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "helpers/endpoint" + +class EndpointHostRedirect < Endpoint + get "/fetch/actual/gem/:id", host_name: "localgemserver.test" do + redirect "http://bundler.localgemserver.test#{request.path_info}" + end + + get "/api/v1/dependencies" do + status 404 + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(EndpointHostRedirect) diff --git a/spec/bundler/support/artifice/endpoint_marshal_fail.rb b/spec/bundler/support/artifice/endpoint_marshal_fail.rb new file mode 100644 index 0000000000..74ce321de6 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_marshal_fail.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require_relative "helpers/endpoint_marshal_fail" +require_relative "helpers/artifice" + +Artifice.activate_with(EndpointMarshalFail) diff --git a/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb new file mode 100644 index 0000000000..ea4cfbe965 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative "helpers/endpoint_marshal_fail" + +class EndpointMarshalFailBasicAuthentication < EndpointMarshalFail + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(EndpointMarshalFailBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_mirror_source.rb b/spec/bundler/support/artifice/endpoint_mirror_source.rb new file mode 100644 index 0000000000..fed7a746b9 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_mirror_source.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "helpers/endpoint" + +class EndpointMirrorSource < Endpoint + get "/gems/:id" do + if request.env["HTTP_X_GEMFILE_SOURCE"] == "https://server.example.org/" && request.env["HTTP_USER_AGENT"].start_with?("bundler") + File.binread("#{gem_repo1}/gems/#{params[:id]}") + else + halt 500 + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(EndpointMirrorSource) diff --git a/spec/bundler/support/artifice/endpoint_redirect.rb b/spec/bundler/support/artifice/endpoint_redirect.rb new file mode 100644 index 0000000000..84f546ba9d --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_redirect.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "helpers/endpoint" + +class EndpointRedirect < Endpoint + get "/fetch/actual/gem/:id" do + redirect "/fetch/actual/gem/#{params[:id]}" + end + + get "/api/v1/dependencies" do + status 404 + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(EndpointRedirect) diff --git a/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb new file mode 100644 index 0000000000..dff360c5c5 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require_relative "helpers/endpoint" + +class EndpointStrictBasicAuthentication < Endpoint + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + + # Only accepts password == "password" + unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz" + halt 401, "Authentication failed" + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(EndpointStrictBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_timeout.rb b/spec/bundler/support/artifice/endpoint_timeout.rb new file mode 100644 index 0000000000..86b793e499 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_timeout.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative "helpers/endpoint_fallback" + +class EndpointTimeout < EndpointFallback + SLEEP_TIMEOUT = 3 + + get "/api/v1/dependencies" do + sleep(SLEEP_TIMEOUT) + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(EndpointTimeout) diff --git a/spec/bundler/support/artifice/fail.rb b/spec/bundler/support/artifice/fail.rb new file mode 100644 index 0000000000..5ddbc4e590 --- /dev/null +++ b/spec/bundler/support/artifice/fail.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require_relative "../vendored_net_http" + +class Fail < Gem::Net::HTTP + # Gem::Net::HTTP uses a @newimpl instance variable to decide whether + # to use a legacy implementation. Since we are subclassing + # Gem::Net::HTTP, we must set it + @newimpl = true + + def request(req, body = nil, &block) + raise(exception(req)) + end + + # Ensure we don't start a connect here + def connect + end + + def exception(req) + Errno::ENETUNREACH.new("host down: Bundler spec artifice fail! #{req["PATH_INFO"]}") + end +end + +require_relative "helpers/artifice" + +# Replace Gem::Net::HTTP with our failing subclass +Artifice.replace_net_http(::Fail) diff --git a/spec/bundler/support/artifice/helpers/artifice.rb b/spec/bundler/support/artifice/helpers/artifice.rb new file mode 100644 index 0000000000..788268295c --- /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 Gem::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(::Gem::Net::HTTP) + end + + def self.replace_net_http(value) + ::Gem::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..e684aa8628 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/compact_index.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require_relative "endpoint" + +$LOAD_PATH.unshift Spec::Path.tmp_root.join("compact_index/lib").to_s +require "compact_index" +require "digest" + +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 + etag = Digest::MD5.hexdigest(response_body) + headers "ETag" => quote(etag) + return if not_modified?(etag) + headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest(response_body)}:" + 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?(etag) + etags = parse_etags(request.env["HTTP_IF_NONE_MATCH"]) + + return unless etags.include?(etag) + status 304 + body "" + end + + def requested_range_for(response_body) + ranges = Rack::Utils.get_byte_ranges(env["HTTP_RANGE"], 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].flat_map do |filename| + spec_index = gem_repo.join(filename) + next [] unless File.exist?(spec_index) + + Marshal.load(File.binread(spec_index)).map do |name, version, platform| + load_spec(name, version, platform, gem_repo) + end + end + end + + specs.group_by(&:name).map do |name, versions| + gem_versions = versions.map do |spec| + deps = spec.runtime_dependencies.map do |d| + reqs = d.requirement.requirements.map {|r| r.join(" ") }.join(", ") + CompactIndex::Dependency.new(d.name, reqs) + end + begin + checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") do + Digest(:SHA256).file("#{gem_repo}/gems/#{spec.original_name}.gem").hexdigest + end + rescue StandardError + checksum = nil + end + build_gem_version(spec, deps, checksum) + end + CompactIndex::Gem.new(name, gem_versions) + end + end + end + + def build_gem_version(spec, deps, checksum) + 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 + 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_cooldown.rb b/spec/bundler/support/artifice/helpers/compact_index_cooldown.rb new file mode 100644 index 0000000000..9920fd2c95 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/compact_index_cooldown.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "compact_index" + +class CompactIndexCooldownAPI < CompactIndexAPI + helpers do + def build_gem_version(spec, deps, checksum) + created_at = spec.date&.utc&.iso8601 + CompactIndex::GemVersionV2.new(spec.version.version, spec.platform.to_s, checksum, nil, + deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s, created_at) + 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..9590611dfe --- /dev/null +++ b/spec/bundler/support/artifice/helpers/endpoint.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require_relative "../../path" + +$LOAD_PATH.unshift(*Spec::Path.sinatra_dependency_paths) + +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 + set :host_authorization, permitted_hosts: [".example.org", ".local", ".mirror", ".repo", ".repo1", ".repo2", ".repo3", ".repo4", ".rubygems.org", ".security", ".source", ".test", "127.0.0.1"] + + 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.binread(gem_repo.join(filename))) + end.inject(:+) + + all_specs.filter_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.runtime_dependencies.map do |dep| + [dep.name, dep.requirement.requirements.map {|a| a.join(" ") }.join(", ")] + end, + } + end + 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..05ff034463 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/rack_request.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require "rack/test" +require_relative "../../vendored_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 < ::Gem::Net::HTTP + class << self + attr_accessor :endpoint + end + + # Gem::Net::HTTP uses a @newimpl instance variable to decide whether + # to use a legacy implementation. Since we are subclassing + # Gem::Net::HTTP, we must set it + @newimpl = true + + # We don't need to connect, so blank out this method + def connect + end + + # Replace the Gem::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 Gem::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 Gem::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 Gem::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 Gem::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 = ::Gem::Net::BufferedIO.new(StringIO.new(response_string.join("\n"))) + res = ::Gem::Net::HTTPResponse.read_new(response_io) + + res.reading_body(response_io, true) do + yield res if block_given? + end + + res + end + end + end +end diff --git a/spec/bundler/support/artifice/vcr.rb b/spec/bundler/support/artifice/vcr.rb new file mode 100644 index 0000000000..0bf5ade8f6 --- /dev/null +++ b/spec/bundler/support/artifice/vcr.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +require_relative "../vendored_net_http" +require_relative "../path" + +CASSETTE_PATH = "#{Spec::Path.spec_dir}/support/artifice/vcr_cassettes".freeze +USED_CASSETTES_PATH = "#{Spec::Path.spec_dir}/support/artifice/used_cassettes.txt".freeze +CASSETTE_NAME = ENV.fetch("BUNDLER_SPEC_VCR_CASSETTE_NAME") { "realworld" } + +class BundlerVCRHTTP < Gem::Net::HTTP + class RequestHandler + attr_reader :http, :request, :body, :response_block + def initialize(http, request, body = nil, &response_block) + @http = http + @request = request + @body = body + @response_block = response_block + end + + def handle_request + handler = self + request.instance_eval do + @__vcr_request_handler = handler + end + + File.open(USED_CASSETTES_PATH, "a+") do |f| + f.puts request_pair_paths.map {|path| Pathname.new(path).relative_path_from(Spec::Path.git_root).to_s }.join("\n") + end + + if recorded_response? + recorded_response + else + record_response + end + end + + def recorded_response? + return true if ENV["BUNDLER_SPEC_PRE_RECORDED"] + request_pair_paths.all? {|f| File.exist?(f) } + end + + def recorded_response + File.open(request_pair_paths.last, "rb:ASCII-8BIT") do |response_file| + response_io = ::Gem::Net::BufferedIO.new(response_file) + ::Gem::Net::HTTPResponse.read_new(response_io).tap do |response| + response.decode_content = request.decode_content if request.respond_to?(:decode_content) + response.uri = request.uri + + response.reading_body(response_io, request.response_body_permitted?) do + response_block&.call(response) + end + end + end + end + + def record_response + request_path, response_path = *request_pair_paths + + @recording = true + + response = http.request_without_vcr(request, body, &response_block) + @recording = false + unless @recording + require "fileutils" + FileUtils.mkdir_p(File.dirname(request_path)) + binwrite(request_path, request_to_string(request)) + binwrite(response_path, response_to_string(response)) + end + response + end + + def key + [request["host"] || http.address, request.path, request.method].compact + end + + def file_name_for_key(key) + File.join(*key).gsub(/[\:*?"<>|]/, "-") + end + + def request_pair_paths + %w[request response].map do |kind| + File.join(CASSETTE_PATH, CASSETTE_NAME, file_name_for_key(key), kind) + end + end + + def request_to_string(request) + request_string = [] + request_string << "> #{request.method.upcase} #{request.path}" + request.to_hash.each do |key, value| + request_string << "> #{key}: #{Array(value).first}" + end + request << "" << request.body if request.body + request_string.join("\n") + end + + def response_to_string(response) + headers = response.to_hash + body = response.body + + response_string = [] + response_string << "HTTP/1.1 #{response.code} #{response.message}" + + headers["content-length"] = [body.bytesize.to_s] if body + + headers.each do |header, value| + response_string << "#{header}: #{value.join(", ")}" + end + + response_string << "" << body + + response_string = response_string.join("\n") + if response_string.respond_to?(:force_encoding) + response_string.force_encoding("ASCII-8BIT") + else + response_string + end + end + + def binwrite(path, contents) + File.open(path, "wb:ASCII-8BIT") {|f| f.write(contents) } + end + end + + def start_with_vcr + if ENV["BUNDLER_SPEC_PRE_RECORDED"] + raise IOError, "HTTP session already opened" if @started + @socket = nil + @started = true + else + start_without_vcr + end + end + + alias_method :start_without_vcr, :start + alias_method :start, :start_with_vcr + + def request_with_vcr(request, *args, &block) + handler = request.instance_eval do + remove_instance_variable(:@__vcr_request_handler) if defined?(@__vcr_request_handler) + end || RequestHandler.new(self, request, *args, &block) + + handler.handle_request + end + + alias_method :request_without_vcr, :request + alias_method :request, :request_with_vcr +end + +require_relative "helpers/artifice" + +# Replace Gem::Net::HTTP with our VCR subclass +Artifice.replace_net_http(BundlerVCRHTTP) diff --git a/spec/bundler/support/artifice/windows.rb b/spec/bundler/support/artifice/windows.rb new file mode 100644 index 0000000000..3056540beb --- /dev/null +++ b/spec/bundler/support/artifice/windows.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require_relative "../path" + +$LOAD_PATH.unshift(*Spec::Path.sinatra_dependency_paths) + +require "sinatra/base" + +class Windows < Sinatra::Base + set :raise_errors, true + set :show_exceptions, false + + helpers do + def default_gem_repo + Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"] || Spec::Path.gem_repo1) + end + end + + files = ["specs.4.8.gz", + "prerelease_specs.4.8.gz", + "quick/Marshal.4.8/rcov-1.0-mswin32.gemspec.rz", + "gems/rcov-1.0-mswin32.gem"] + + files.each do |file| + get "/#{file}" do + File.binread default_gem_repo.join(file) + end + end + + get "/gems/rcov-1.0-x86-mswin32.gem" do + halt 404 + end + + get "/api/v1/dependencies" do + halt 404 + end + + get "/versions" do + halt 500 + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(Windows) diff --git a/spec/bundler/support/build_metadata.rb b/spec/bundler/support/build_metadata.rb new file mode 100644 index 0000000000..2eade4137b --- /dev/null +++ b/spec/bundler/support/build_metadata.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require_relative "path" +require_relative "helpers" + +module Spec + module BuildMetadata + include Spec::Path + include Spec::Helpers + + def write_build_metadata(dir: source_root, version: Bundler::VERSION) + build_metadata = { + git_commit_sha: git_commit_sha, + built_at: release_date_for(version, dir: dir), + } + + replace_build_metadata(build_metadata, dir: dir) + end + + def reset_build_metadata(dir: source_root) + build_metadata = { + built_at: nil, + } + + replace_build_metadata(build_metadata, dir: dir) + end + + private + + def replace_build_metadata(build_metadata, dir:) + build_metadata_file = File.expand_path("lib/bundler/build_metadata.rb", dir) + + ivars = build_metadata.sort.map do |k, v| + " @#{k} = #{loaded_gemspec.send(:ruby_code, v)}" + end.join("\n") + + contents = File.read(build_metadata_file) + contents.sub!(/^(\s+# begin ivars).+(^\s+# end ivars)/m, "\\1\n#{ivars}\n\\2") + File.open(build_metadata_file, "w") {|f| f << contents } + end + + def git_commit_sha + ruby_core_tarball? ? "unknown" : git("rev-parse --short HEAD", source_root).strip + end + + def release_date_for(version, dir:) + changelog = File.expand_path("CHANGELOG.md", dir) + File.readlines(changelog)[2].scan(/^## #{Regexp.escape(version)} \((.*)\)/).first&.first if File.exist?(changelog) + end + + extend self + end +end diff --git a/spec/bundler/support/builders.rb b/spec/bundler/support/builders.rb new file mode 100644 index 0000000000..43ab7e053d --- /dev/null +++ b/spec/bundler/support/builders.rb @@ -0,0 +1,749 @@ +# frozen_string_literal: true + +require "bundler/shared_helpers" +require "shellwords" +require "fileutils" +require "rubygems/package" + +require_relative "build_metadata" + +module Spec + module Builders + def self.extended(mod) + mod.extend Path + mod.extend Helpers + end + + def self.constantize(name) + name.delete("-").upcase + end + + def v(version) + Gem::Version.new(version) + end + + def pl(platform) + Gem::Platform.new(platform) + end + + def build_repo1 + build_repo gem_repo1 do + FileUtils.cp rake_path, "#{gem_repo1}/gems/" + + build_gem "coffee-script-source" + build_gem "git" + build_gem "puma" + build_gem "minitest" + + build_gem "myrack", %w[0.9.1 1.0.0] do |s| + s.executables = "myrackup" + s.post_install_message = "Myrack's post install message" + end + + build_gem "thin" do |s| + s.add_dependency "myrack" + s.post_install_message = "Thin's post install message" + end + + build_gem "myrack-obama" do |s| + s.add_dependency "myrack" + s.post_install_message = "Myrack-obama's post install message" + end + + build_gem "myrack_middleware", "1.0" do |s| + s.add_dependency "myrack", "0.9.1" + end + + build_gem "rails", "2.3.2" do |s| + s.executables = "rails" + s.add_dependency "rake", rake_version + s.add_dependency "actionpack", "2.3.2" + s.add_dependency "activerecord", "2.3.2" + s.add_dependency "actionmailer", "2.3.2" + s.add_dependency "activeresource", "2.3.2" + end + build_gem "actionpack", "2.3.2" do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "activerecord", ["2.3.1", "2.3.2"] do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "actionmailer", "2.3.2" do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "activeresource", "2.3.2" do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "activesupport", %w[1.2.3 2.3.2 2.3.5] + + build_gem "activemerchant" do |s| + s.add_dependency "activesupport", ">= 2.0.0" + end + + build_gem "rspec", "1.2.7", no_default: true do |s| + s.write "lib/spec.rb", "SPEC = '1.2.7'" + end + + build_gem "myrack-test", no_default: true do |s| + s.write "lib/myrack/test.rb", "MYRACK_TEST = '1.0'" + end + + build_gem "platform_specific" do |s| + s.platform = "java" + end + + build_gem "platform_specific" do |s| + s.platform = "ruby" + end + + build_gem "platform_specific" do |s| + s.platform = "x86-mswin32" + end + + build_gem "platform_specific" do |s| + s.platform = "x64-mswin64" + end + + build_gem "platform_specific" do |s| + s.platform = "x86-mingw32" + end + + build_gem "platform_specific" do |s| + s.platform = "x64-mingw-ucrt" + end + + build_gem "platform_specific" do |s| + s.platform = "aarch64-mingw-ucrt" + end + + build_gem "platform_specific" do |s| + s.platform = "x86-darwin-100" + end + + build_gem "only_java", "1.0" do |s| + s.platform = "java" + end + + build_gem "only_java", "1.1" do |s| + s.platform = "java" + end + + build_gem "nokogiri", "1.4.2" + build_gem "nokogiri", "1.4.2" do |s| + s.platform = "java" + s.add_dependency "weakling", ">= 0.0.3" + end + + build_gem "laduradura", "5.15.2" + build_gem "laduradura", "5.15.2" do |s| + s.platform = "java" + end + build_gem "laduradura", "5.15.3" do |s| + s.platform = "java" + end + + build_gem "weakling", "0.0.3" + + build_gem "terranova", "8" + + build_gem "duradura", "7.0" + + build_gem "very_simple_binary", &:add_c_extension + build_gem "simple_binary", &:add_c_extension + + build_gem "bundler", "0.9" do |s| + s.executables = "bundle" + s.write "bin/bundle", "#!/usr/bin/env ruby\nputs 'FAIL'" + end + + # The bundler 0.8 gem has a rubygems plugin that always loads :( + build_gem "bundler", "0.8.1" do |s| + s.write "lib/bundler/omg.rb", "" + s.write "lib/rubygems_plugin.rb", "require 'bundler/omg' ; puts 'FAIL'" + end + + # The yard gem iterates over Gem.source_index looking for plugins + build_gem "yard" do |s| + s.write "lib/yard.rb", <<-Y + Gem::Specification.sort_by(&:name).each do |gem| + puts gem.full_name + end + Y + end + + build_gem "net-ssh" + build_gem "net-sftp", "1.1.1" do |s| + s.add_dependency "net-ssh", ">= 1.0.0", "< 1.99.0" + end + + build_gem "foo" + end + end + + def build_repo2(**kwargs, &blk) + FileUtils.cp_r gem_repo1, gem_repo2, remove_destination: true + update_repo2(**kwargs, &blk) if block_given? + end + + # A repo that has no pre-installed gems included. (The caller completely + # determines the contents with the block.) + # + # If the repo already exists, `#update_repo` will be called. + def build_repo3(**kwargs, &blk) + if File.exist?(gem_repo3) + update_repo(gem_repo3, &blk) + else + build_repo gem_repo3, **kwargs, &blk + end + end + + # Like build_repo3, this is a repo that has no pre-installed gems included. + # + # If the repo already exists, `#udpate_repo` will be called + def build_repo4(**kwargs, &blk) + if File.exist?(gem_repo4) + update_repo gem_repo4, &blk + else + build_repo gem_repo4, **kwargs, &blk + end + end + + def update_repo2(**kwargs, &blk) + update_repo(gem_repo2, **kwargs, &blk) + end + + def update_repo3(&blk) + update_repo(gem_repo3, &blk) + end + + def build_security_repo + build_repo security_repo do + build_gem "myrack" + + build_gem "signed_gem" do |s| + cert = "signing-cert.pem" + pkey = "signing-pkey.pem" + s.write cert, TEST_CERT + s.write pkey, TEST_PKEY + s.signing_key = pkey + s.cert_chain = [cert] + end + end + end + + # A minimal fake irb console + def build_dummy_irb(version = "9.9.9") + build_gem "irb", version do |s| + s.write "lib/irb.rb", <<-RUBY + class IRB + class << self + def toplevel_binding + unless defined?(@toplevel_binding) && @toplevel_binding + TOPLEVEL_BINDING.eval %{ + def self.__irb__; binding; end + IRB.instance_variable_set(:@toplevel_binding, __irb__) + class << self; undef __irb__; end + } + end + @toplevel_binding.eval('private') + @toplevel_binding + end + + def __irb__ + while line = gets + begin + puts eval(line, toplevel_binding).inspect.sub(/^"(.*)"$/, '=> \\1') + rescue Exception => e + puts "\#{e.class}: \#{e.message}" + puts e.backtrace.first + end + end + end + alias start __irb__ + end + end + RUBY + end + end + + def build_repo(path, **kwargs, &blk) + return if File.directory?(path) + + FileUtils.mkdir_p("#{path}/gems") + + update_repo(path,**kwargs, &blk) + end + + def update_repo(path, build_compact_index: true) + exempted_caller = Gem.ruby_version >= Gem::Version.new("3.4.0.dev") && RUBY_ENGINE != "jruby" ? "#{Module.nesting.first}#build_repo" : "build_repo" + if path == gem_repo1 && caller_locations(1, 1).first.label != exempted_caller + raise "Updating gem_repo1 is unsupported -- use gem_repo2 instead" + end + return unless block_given? + @_build_path = "#{path}/gems" + @_build_repo = File.basename(path) + yield + options = { build_compact: build_compact_index } + Gem::Indexer.new(path, options).generate_index + ensure + @_build_path = nil + @_build_repo = nil + end + + def build_index(&block) + index = Bundler::Index.new + IndexBuilder.run(index, &block) if block_given? + index + end + + def build_spec(name, version = "0.0.1", platform = nil, &block) + Array(version).map do |v| + Gem::Specification.new do |s| + s.name = name + s.version = Gem::Version.new(v) + s.platform = platform + s.authors = ["no one in particular"] + s.summary = "a gemspec used only for testing" + DepBuilder.run(s, &block) if block_given? + end + end + end + + def build_lib(name, *args, &blk) + build_with(LibBuilder, name, args, &blk) + end + + def build_bundler(*args, &blk) + build_with(BundlerBuilder, "bundler", args, &blk) + end + + def build_gem(name, *args, &blk) + build_with(GemBuilder, name, args, &blk) + end + + def build_git(name, *args, &block) + opts = args.last.is_a?(Hash) ? args.last : {} + builder = opts[:bare] ? GitBareBuilder : GitBuilder + spec = build_with(builder, name, args, &block) + GitReader.new(self, opts[:path] || lib_path(spec.full_name)) + end + + def update_git(name, *args, &block) + opts = args.last.is_a?(Hash) ? args.last : {} + spec = build_with(GitUpdater, name, args, &block) + GitReader.new(self, opts[:path] || lib_path(spec.full_name)) + end + + def build_plugin(name, *args, &blk) + build_with(PluginBuilder, name, args, &blk) + end + + private + + def build_with(builder, name, args, &blk) + @_build_path ||= nil + @_build_repo ||= nil + options = args.last.is_a?(Hash) ? args.pop : {} + versions = args.last || "1.0" + spec = nil + + options[:path] ||= @_build_path + options[:source] ||= @_build_repo + + Array(versions).each do |version| + spec = builder.new(self, name, version) + yield spec if block_given? + spec._build(options) + end + + spec + end + + class IndexBuilder + include Builders + + def self.run(index, &block) + new(index).run(&block) + end + + def initialize(index) + @index = index + end + + def run(&block) + instance_eval(&block) + end + + def gem(*args, &block) + build_spec(*args, &block).each do |s| + @index << s + end + end + + def platforms(platforms) + platforms.split(/\s+/).each do |platform| + platform.gsub!(/^(mswin32)$/, 'x86-\1') + yield Gem::Platform.new(platform) + end + end + + def versions(versions) + versions.split(/\s+/).each {|version| yield v(version) } + end + end + + class DepBuilder + include Builders + + def self.run(spec, &block) + new(spec).run(&block) + end + + def initialize(spec) + @spec = spec + end + + def run(&block) + instance_eval(&block) + end + + def runtime(name, requirements) + @spec.add_runtime_dependency(name, requirements) + end + + def development(name, requirements) + @spec.add_development_dependency(name, requirements) + end + + def required_ruby_version=(*reqs) + @spec.required_ruby_version = *reqs + end + + alias_method :dep, :runtime + end + + class BundlerBuilder + def initialize(context, name, version) + @context = context + @spec = Spec::Path.loaded_gemspec.dup + @spec.version = version || Bundler::VERSION + end + + def required_ruby_version + @spec.required_ruby_version + end + + def required_ruby_version=(x) + @spec.required_ruby_version = x + end + + def _build(options = {}) + full_name = "bundler-#{@spec.version}" + build_path = (options[:build_path] || @context.tmp) + full_name + bundler_path = build_path + "#{full_name}.gem" + + FileUtils.mkdir_p build_path + + @context.shipped_files.each do |shipped_file| + target_shipped_file = shipped_file + target_shipped_file = shipped_file.sub(/\Alibexec/, "exe") if @context.ruby_core? + target_shipped_file = build_path + target_shipped_file + target_shipped_dir = File.dirname(target_shipped_file) + FileUtils.mkdir_p target_shipped_dir unless File.directory?(target_shipped_dir) + FileUtils.cp File.expand_path(shipped_file, @context.source_root), target_shipped_file, preserve: true + end + + @context.replace_version_file(@spec.version, dir: build_path) + @context.replace_changelog(@spec.version, dir: build_path) if options[:released] + + Spec::BuildMetadata.write_build_metadata(dir: build_path, version: @spec.version.to_s) + + Dir.chdir build_path do + Gem::DefaultUserInteraction.use_ui(Gem::SilentUI.new) do + Gem::Package.build(@spec) + end + end + + if block_given? + yield(bundler_path) + else + FileUtils.mv bundler_path, options[:path] + end + ensure + FileUtils.rm_rf build_path + end + end + + class LibBuilder + def initialize(context, name, version) + @context = context + @name = name + @spec = Gem::Specification.new do |s| + s.name = name + s.version = version + s.summary = "This is just a fake gem for testing" + s.description = "This is a completely fake gem, for testing purposes." + s.author = "no one" + s.email = "foo@bar.baz" + s.homepage = "http://example.com" + s.license = "MIT" + s.required_ruby_version = ">= 3.0" + end + @files = {} + end + + def method_missing(*args, &blk) + @spec.send(*args, &blk) + end + + def write(file, source = "") + @files[file] = source + end + + def executables=(val) + @spec.executables = Array(val) + @spec.executables.each do |file| + executable = "#{@spec.bindir}/#{file}" + shebang = "#!/usr/bin/env ruby\n" + @spec.files << executable + write executable, "#{shebang}require_relative '../lib/#{@name}' ; puts #{Builders.constantize(@name)}" + end + end + + def add_c_extension + extensions << "ext/extconf.rb" + write "ext/extconf.rb", <<-RUBY + require "mkmf" + + extension_name = "#{name}_c" + if extra_lib_dir = with_config("ext-lib") + # add extra libpath if --with-ext-lib is + # passed in as a build_arg + dir_config extension_name, nil, extra_lib_dir + else + dir_config extension_name + end + create_makefile extension_name + RUBY + write "ext/#{name}.c", <<-C + #include "ruby.h" + + void Init_#{name}_c(void) { + rb_define_module("#{Builders.constantize(name)}_IN_C"); + } + C + end + + def _build(options) + path = options[:path] || _default_path + + if options[:rubygems_version] + @spec.rubygems_version = options[:rubygems_version] + + def @spec.validate(*); end + end + + unless options[:no_default] + gem_source = options[:source] || "path@#{path}" + @files = _default_files. + merge("lib/#{entrypoint}/source.rb" => "#{Builders.constantize(name)}_SOURCE = #{gem_source.to_s.dump}"). + merge(@files) + end + + @spec.authors = ["no one"] + @spec.files += @files.keys + + case options[:gemspec] + when false + # do nothing + when :yaml + @files["#{name}.gemspec"] = @spec.to_yaml + else + @files["#{name}.gemspec"] = @spec.to_ruby + end + + @files.each do |file, source| + full_path = Pathname.new(path).join(file) + FileUtils.mkdir_p(full_path.dirname) + File.open(full_path, "w") {|f| f.puts source } + FileUtils.chmod("+x", full_path) if @spec.executables.map {|exe| "#{@spec.bindir}/#{exe}" }.include?(file) + end + path + end + + def _default_files + @_default_files ||= { "lib/#{entrypoint}.rb" => "#{Builders.constantize(name)} = '#{version}#{platform_string}'" } + end + + def entrypoint + name.tr("-", "/") + end + + def _default_path + @context.tmp("libs", @spec.full_name) + end + + def platform_string + " #{@spec.platform}" unless @spec.platform == Gem::Platform::RUBY + end + end + + class GitBuilder < LibBuilder + def _build(options) + default_branch = options[:default_branch] || "main" + path = options[:path] || _default_path + source = options[:source] || "git@#{path}" + super(options.merge(path: path, source: source)) + @context.git("config --global init.defaultBranch #{default_branch}", path) + @context.git("init", path) + @context.git("add *", path) + @context.git("config user.email lol@wut.com", path) + @context.git("config user.name lolwut", path) + @context.git("config commit.gpgsign false", path) + @context.git("commit -m OMG_INITIAL_COMMIT", path) + end + end + + class GitBareBuilder < LibBuilder + def _build(options) + path = options[:path] || _default_path + super(options.merge(path: path)) + @context.git("init --bare", path) + end + end + + class GitUpdater < LibBuilder + def _build(options) + libpath = options[:path] || _default_path + update_gemspec = options[:gemspec] || false + source = options[:source] || "git@#{libpath}" + + if branch = options[:branch] + @context.git("checkout -b #{Shellwords.shellescape(branch)}", libpath) + elsif tag = options[:tag] + @context.git("tag #{Shellwords.shellescape(tag)}", libpath) + elsif options[:remote] + @context.git("remote add origin #{options[:remote]}", libpath) + elsif options[:push] + @context.git("push origin #{options[:push]}", libpath) + end + + current_ref = @context.git("rev-parse HEAD", libpath).strip + _default_files.keys.each do |path| + _default_files[path] += "\n#{Builders.constantize(name)}_PREV_REF = '#{current_ref}'" + end + super(options.merge(path: libpath, gemspec: update_gemspec, source: source)) + @context.git("commit -am BUMP", libpath) + end + end + + class GitReader + attr_reader :context, :path + + def initialize(context, path) + @context = context + @path = path + end + + def ref_for(ref, len = nil) + ref = context.git "rev-parse #{ref}", path + ref = ref[0..len] if len + ref + end + end + + class GemBuilder < LibBuilder + def _build(opts) + lib_path = opts[:lib_path] || @context.tmp(".tmp/#{@spec.full_name}") + lib_path = super(opts.merge(path: lib_path, no_default: opts[:no_default])) + destination = opts[:path] || _default_path + FileUtils.mkdir_p(lib_path.join(destination)) + + if [:yaml, false].include?(opts[:gemspec]) + Dir.chdir(lib_path) do + Bundler.rubygems.build(@spec, opts[:skip_validation]) + end + elsif opts[:skip_validation] + Dir.chdir(lib_path) { Gem::Package.build(@spec, true) } + else + Dir.chdir(lib_path) { Gem::Package.build(@spec) } + end + + gem_path = File.expand_path("#{@spec.full_name}.gem", lib_path) + if opts[:to_system] + @context.system_gems gem_path, default: opts[:default] + elsif opts[:to_bundle] + @context.system_gems gem_path, path: @context.default_bundle_path + else + FileUtils.mv(gem_path, destination) + end + end + + def _default_path + @context.gem_repo1("gems") + end + end + + class PluginBuilder < GemBuilder + def _default_files + @_default_files ||= { + "lib/#{name}.rb" => "#{Builders.constantize(name)} = '#{version}#{platform_string}'", + "plugins.rb" => "", + } + end + end + + TEST_CERT = <<~CERT + -----BEGIN CERTIFICATE----- + MIIDNTCCAh2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAnMQwwCgYDVQQDDAN5b3Ux + FzAVBgoJkiaJk/IsZAEZFgdleGFtcGxlMB4XDTE1MDIwODAwMTIyM1oXDTQyMDYy + NTAwMTIyM1owJzEMMAoGA1UEAwwDeW91MRcwFQYKCZImiZPyLGQBGRYHZXhhbXBs + ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMkupYkg3Nd1oXM3fo0d + mVJBWNrni88lKDuIIQXwcKe6XCgiloZG708ecLTOws9+o9MkTl9Wtpf/WGXT98NK + EPUYakd2Fv1SuD1jWYlP7iDR6hB3RkWBm5ziujYftVJ4ZrPD42PLjDASvlh75Tvr + MeM7yq/qkcgNsd9dQyUvMNPks3tla9je7Dt7Auli2IN3CNXys7gIOfwJH0Bb/M6t + y7oUfpoUKAfLzwe61abztgDu1lSNgdFBM1kcxYflyh/FkX5TlAcWeAXzLrnxAXGR + UxXrxW4oPC+kZi/pDRBd7X4zQDx7bCmr1+FsS3M05i3w5E08Tt9iKRk4V8nCmE4i + k6UCAwEAAaNsMGowCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYE + FOOOFw5TNAqt/TcRRZEU3Dg/58XuMBYGA1UdEQQPMA2BC3lvdUBleGFtcGxlMBYG + A1UdEgQPMA2BC3lvdUBleGFtcGxlMA0GCSqGSIb3DQEBCwUAA4IBAQAy3xnmobxU + 1SyhHvoIXTJmG0wt1DQ/Dqwjy362LpEf1UHt29wtg1Mph58eVtl93z5Vd2t4/O77 + E2BHpSu9ujc6/Br4+2uA/Qk/xRyLBtZAwty6J4uFvOOg985HonN+RCUZbKSUTmtA + TZvNtIDAZFQ8Tu75K4gIBxDcz7biGi4i1VJ3F3GNCNeossr9IQwKvb+UWFq14U5R + IzUnGgMIzcjUG2kKQvddRD1CjS+egtcLvShbOfm5bs4w4rfQ2FPF+Aaf9v7fxa/c + Jrf3K+cB19eAy7O4nlPG1xurvnZd0QpqRk++werrBuKe1Pgga7YBLePfJhzwqcZv + wVOSsB870yeO + -----END CERTIFICATE----- + CERT + + TEST_PKEY = <<~PKEY + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEAyS6liSDc13Whczd+jR2ZUkFY2ueLzyUoO4ghBfBwp7pcKCKW + hkbvTx5wtM7Cz36j0yROX1a2l/9YZdP3w0oQ9RhqR3YW/VK4PWNZiU/uINHqEHdG + RYGbnOK6Nh+1Unhms8PjY8uMMBK+WHvlO+sx4zvKr+qRyA2x311DJS8w0+Sze2Vr + 2N7sO3sC6WLYg3cI1fKzuAg5/AkfQFv8zq3LuhR+mhQoB8vPB7rVpvO2AO7WVI2B + 0UEzWRzFh+XKH8WRflOUBxZ4BfMuufEBcZFTFevFbig8L6RmL+kNEF3tfjNAPHts + KavX4WxLczTmLfDkTTxO32IpGThXycKYTiKTpQIDAQABAoIBABpyrHEWRed5X7aN + kXCBzKSN/LLChT8VNnB6bppLnV501yVbmV2hDlg2EJZkfCMvwIptwnPcKs2uqZ4G + u2gMC6X9Bgkg/YK4u4nZJBiIzoMNYEUL48wYGYS1dcokaapO3nQ8M1+XjyAexrFL + 5btL1IIisScRTQWiGe6FtzcN43sSNkBISyDF5zG4Kodynqi0ekITmMl2q5XLWcsM + KBnmZcRFEmFae2YYczVy8SXNApkZEvN69znvAX1iDNnZ3sJFchXo1nRPt4stOOKw + mydgIYqaNQ22aF3OkblvoA4Y4m+X2Qt1sfkryKa5xTT7DSE81GmmazNI64EWqtES + 6Xde6P0CgYEA+V1vuSnE5fWX188abWMbVwNMC71WfHbntFmI+qwWYPEpickm+RGX + DDfXs5unlVX4KUmjfplgavO29op1GZTuD9TlRnUAV0+0aJnNq4DY6XsHfD84qsBr + gQGEHeJ1cMGNDnZR/EV3eudMalj9Qjpx9NoXNzMykb0/SUYZQemiqwcCgYEAzokC + s0GoHVJqan4dfU0h0G5QPncrajW9DGG1ySxK/A2eqbVB8W2ZQx39OS26/Gydb31p + cR7zm8PZpNbzLqlIMEbD4F6q22xxvYVtDx/HHPjxHMi87yxwQ9uLDUHoMa/LciTO + djv3D1xTDDGxbpjmsdmINetunAs3htxku7JY5PMCgYBs3/TVvXzwgmhHm28Ib4sS + VKgxP/uw4CGORsFd4SDsNp9SP3c6rAltFjyheMaUlzKApFwz/DdyuvIZdp5mCvZe + BzALsS3y8SPtv6lixiDu3/6GqvvM4bKOYuESQzvPfVJfDB4DrTjben2MuUnqTqZO + p6IXQc1EgIJPNcH1W1LgpQKBgAKZlPAevngIBpDqn4JpSyititMOevxuSr/yJvCu + Xw9HOJ0YTAk3APvoT7y9h6IP1/eEU6R56EUotP+vOQZ4WRFKgsK7TllOxyvElzfe + hYom1BoxqLc2Dv+7rsdu8fZWKTB5qCOy44xM9DquEXa79AN/IojTOuQ5++v1sErw + ls/jAoGBANneGe9ogN51mYkrLyg1fhU1i24gFRq+sPGEvsCUoE6Vjw/lawQQ80T8 + v45TFqvhoGpgznqy3qxDJyguquZg6HN2yW6HE2Dvk7uk3XogcjdXgNDmWqb2j0eE + z9pKzHCqfwNVPuYf44Znyo2YeyZ2kHn42MU73oXuFshUs3QHcH+P + -----END RSA PRIVATE KEY----- + PKEY + end +end diff --git a/spec/bundler/support/bundle b/spec/bundler/support/bundle new file mode 100755 index 0000000000..8f8b535295 --- /dev/null +++ b/spec/bundler/support/bundle @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative "../bundler/support/activate" + +load File.expand_path("bundle", Spec::Path.exedir) diff --git a/spec/bundler/support/bundle.rb b/spec/bundler/support/bundle.rb new file mode 100644 index 0000000000..aa7b121706 --- /dev/null +++ b/spec/bundler/support/bundle.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require_relative "path" + +warn "#{__FILE__} is deprecated. Please use #{Spec::Path.dev_binstub} instead" + +load Spec::Path.dev_binstub diff --git a/spec/bundler/support/checksums.rb b/spec/bundler/support/checksums.rb new file mode 100644 index 0000000000..7b69bba668 --- /dev/null +++ b/spec/bundler/support/checksums.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +module Spec + module Checksums + class ChecksumsBuilder + attr_reader :bundler_registered + + def initialize(enabled = true, &block) + @enabled = enabled + @checksums = {} + yield self if block_given? + end + + def initialize_copy(original) + super + @checksums = @checksums.dup + end + + def checksum(repo, name, version, platform = Gem::Platform::RUBY, folder = "gems") + @bundler_registered = true if name == "bundler" + + name_tuple = Gem::NameTuple.new(name, version, platform) + gem_file = File.join(repo, folder, "#{name_tuple.full_name}.gem") + File.open(gem_file, "rb") do |f| + register(name_tuple, Bundler::Checksum.from_gem(f, "#{gem_file} (via ChecksumsBuilder#checksum)")) + end + end + + def no_checksum(name, version, platform = Gem::Platform::RUBY) + name_tuple = Gem::NameTuple.new(name, version, platform) + register(name_tuple, nil) + end + + def delete(name, platform = nil) + @checksums.reject! {|k, _| k.name == name && (platform.nil? || k.platform == platform) } + end + + def to_s + return "" unless @enabled + + locked_checksums = @checksums.map do |name_tuple, checksum| + checksum &&= " #{checksum.to_lock}" + " #{name_tuple.lock_name}#{checksum}\n" + end + + "\nCHECKSUMS\n#{locked_checksums.sort.join}" + end + + private + + def register(name_tuple, checksum) + delete(name_tuple.name, name_tuple.platform) + @checksums[name_tuple] = checksum + end + end + + def checksums_section(enabled = true, bundler_checksum: true, &block) + ChecksumsBuilder.new(enabled, &block).tap do |builder| + next if builder.bundler_registered || !bundler_checksum + + next if Bundler::VERSION.to_s.end_with?(".dev") + builder.checksum(system_gem_path, "bundler", Bundler::VERSION, Gem::Platform::RUBY, "cache") + end + end + + def checksums_section_when_enabled(target_lockfile = nil, &block) + begin + enabled = (target_lockfile || lockfile).match?(/^CHECKSUMS$/) + rescue Errno::ENOENT + enabled = true + end + checksums_section(enabled, &block) + end + + def checksum_to_lock(*args) + checksums_section(true, bundler_checksum: false) do |c| + c.checksum(*args) + end.to_s.sub(/^CHECKSUMS\n/, "").strip + end + + def checksum_digest(*args) + checksum_to_lock(*args).split(Bundler::Checksum::ALGO_SEPARATOR, 2).last + end + + # if prefixes is given, removes all checksums where the line + # has any of the prefixes on the line before the checksum + # otherwise, removes all checksums from the lockfile + def remove_checksums_from_lockfile(lockfile, *prefixes) + head, remaining = lockfile.split(/^CHECKSUMS$/, 2) + return lockfile unless remaining + checksums, tail = remaining.split("\n\n", 2) + + prefixes = + if prefixes.empty? + nil + else + /(#{prefixes.map {|p| Regexp.escape(p) }.join("|")})/ + end + + checksums = checksums.each_line.map do |line| + if prefixes.nil? || line.match?(prefixes) + line.gsub(/ sha256=[a-f0-9]{64}/i, "") + else + line + end + end + + head.concat( + "CHECKSUMS", + checksums.join, + "\n\n", + tail + ) + end + + def remove_checksums_section_from_lockfile(lockfile) + head, remaining = lockfile.split(/^CHECKSUMS$/, 2) + return lockfile unless remaining + _checksums, tail = remaining.split("\n\n", 2) + head.concat(tail) + end + + def checksum_from_package(gem_file, name, version) + name_tuple = Gem::NameTuple.new(name, version) + + checksum = nil + + File.open(gem_file, "rb") do |f| + checksum = Bundler::Checksum.from_gem(f, gemfile) + end + + "#{name_tuple.lock_name} #{checksum.to_lock}" + end + end +end diff --git a/spec/bundler/support/command_execution.rb b/spec/bundler/support/command_execution.rb new file mode 100644 index 0000000000..e2915b996d --- /dev/null +++ b/spec/bundler/support/command_execution.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module Spec + class CommandExecution + def initialize(command, timeout:) + @command = command + @timeout = timeout + @original_stdout = String.new + @original_stderr = String.new + end + + attr_accessor :exitstatus, :command, :original_stdout, :original_stderr + attr_reader :timeout + attr_writer :failure_reason + + def raise_error! + return unless failure? + + error_header = if failure_reason == :timeout + "Invoking `#{command}` was aborted after #{timeout} seconds with output:" + else + "Invoking `#{command}` failed with output:" + end + + raise <<~ERROR + #{error_header} + + ---------------------------------------------------------------------- + #{stdboth} + ---------------------------------------------------------------------- + ERROR + end + + def to_s + "$ #{command}" + end + alias_method :inspect, :to_s + + def stdboth + @stdboth ||= [stderr, stdout].join("\n").strip + end + + def stdout + normalize(original_stdout) + end + + def stderr + normalize(original_stderr) + end + + def to_s_verbose + [ + to_s, + stdout, + stderr, + exitstatus ? "# $? => #{exitstatus}" : "", + ].reject(&:empty?).join("\n") + end + + def success? + return true unless exitstatus + exitstatus == 0 + end + + def failure? + return true unless exitstatus + exitstatus > 0 + end + + private + + attr_reader :failure_reason + + def normalize(string) + string.dup.force_encoding(Encoding::UTF_8).scrub.strip.gsub("\r\n", "\n") + end + end +end diff --git a/spec/bundler/support/env.rb b/spec/bundler/support/env.rb new file mode 100644 index 0000000000..0899bd82a3 --- /dev/null +++ b/spec/bundler/support/env.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Spec + module Env + def ruby_core? + File.exist?(File.expand_path("../../../lib/bundler/bundler.gemspec", __dir__)) + end + + def rubylib + ENV["RUBYLIB"].to_s.split(File::PATH_SEPARATOR) + end + end +end diff --git a/spec/bundler/support/filters.rb b/spec/bundler/support/filters.rb new file mode 100644 index 0000000000..2be25b4a78 --- /dev/null +++ b/spec/bundler/support/filters.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class RequirementChecker < Proc + def self.against(provided) + new do |required| + requirement = Gem::Requirement.new(required) + + !requirement.satisfied_by?(provided) + end.tap do |checker| + checker.provided = provided + end + end + + attr_accessor :provided + + def inspect + "\"#{provided}\"" + end +end + +git_version = Gem::Version.new(`git --version`[/(\d+\.\d+\.\d+)/, 1]) + +RSpec.configure do |config| + config.filter_run_excluding realworld: true + + config.filter_run_excluding rubygems: RequirementChecker.against(Gem.rubygems_version) + config.filter_run_excluding git: RequirementChecker.against(git_version) + config.filter_run_excluding ruby_repo: !ENV["GEM_COMMAND"].nil? + config.filter_run_excluding no_color_tty: Gem.win_platform? || !ENV["GITHUB_ACTION"].nil? + config.filter_run_excluding permissions: Gem.win_platform? + config.filter_run_excluding readline: Gem.win_platform? + config.filter_run_excluding jruby_only: RUBY_ENGINE != "jruby" + config.filter_run_excluding truffleruby_only: RUBY_ENGINE != "truffleruby" + config.filter_run_excluding man: Gem.win_platform? + config.filter_run_excluding mri_only: RUBY_ENGINE != "ruby" + + config.filter_run_when_matching :focus unless ENV["CI"] + + config.before(:each, :bundler) do |example| + bundle_config "simulate_version #{example.metadata[:bundler]}" + end +end diff --git a/spec/bundler/support/hax.rb b/spec/bundler/support/hax.rb new file mode 100644 index 0000000000..46718f5fa4 --- /dev/null +++ b/spec/bundler/support/hax.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +if ENV["BUNDLER_SPEC_RUBY_PLATFORM"] + Object.send(:remove_const, :RUBY_PLATFORM) + RUBY_PLATFORM = ENV["BUNDLER_SPEC_RUBY_PLATFORM"] +end + +module Gem + def self.ruby=(ruby) + @ruby = ruby + end + + if ENV["RUBY"] + Gem.ruby = ENV["RUBY"] + end + + if ENV["BUNDLER_GEM_DEFAULT_DIR"] + @default_dir = ENV["BUNDLER_GEM_DEFAULT_DIR"] + @default_specifications_dir = nil + end + + spec_platform = ENV["BUNDLER_SPEC_PLATFORM"] + if spec_platform + if /mingw|mswin/.match?(spec_platform) + @@win_platform = nil # rubocop:disable Style/ClassVars + RbConfig::CONFIG["host_os"] = spec_platform.gsub(/^[^-]+-/, "").tr("-", "_") + end + + RbConfig::CONFIG["arch"] = spec_platform + + class Platform + @local = nil + end + @platforms = [] + end + + if ENV["BUNDLER_SPEC_GEM_SOURCES"] + self.sources = [ENV["BUNDLER_SPEC_GEM_SOURCES"]] + end + + if ENV["BUNDLER_SPEC_READ_ONLY"] + module ReadOnly + def open(file, mode) + if file != IO::NULL && mode == "wb" + raise Errno::EROFS + else + super + end + end + end + + File.singleton_class.prepend ReadOnly + end + + if ENV["BUNDLER_SPEC_FAKE_RESOLVE"] + module FakeResolv + def getaddrinfo(host, port) + if host == ENV["BUNDLER_SPEC_FAKE_RESOLVE"] + [["AF_INET", port, "127.0.0.1", "127.0.0.1", 2, 2, 17]] + else + super + end + end + end + + Socket.singleton_class.prepend FakeResolv + end +end + +# mise installed rubygems_plugin.rb to system wide `site_ruby` directory. +# This empty module avoid to call `mise` command. +module ReshimInstaller + def self.reshim; end +end diff --git a/spec/bundler/support/helpers.rb b/spec/bundler/support/helpers.rb new file mode 100644 index 0000000000..b0d4b5008b --- /dev/null +++ b/spec/bundler/support/helpers.rb @@ -0,0 +1,631 @@ +# frozen_string_literal: true + +require_relative "the_bundle" +require_relative "path" +require_relative "options" +require_relative "subprocess" + +module Spec + module Helpers + include Spec::Path + include Spec::Options + include Spec::Subprocess + + def self.extended(mod) + mod.extend Spec::Path + mod.extend Spec::Options + mod.extend Spec::Subprocess + end + + def reset! + Dir.glob("#{tmp}/{gems/*,*}", File::FNM_DOTMATCH).each do |dir| + next if %w[base base_system remote1 rubocop standard gems rubygems . ..].include?(File.basename(dir)) + FileUtils.rm_r(dir) + end + FileUtils.mkdir_p(home) + FileUtils.mkdir_p(tmpdir) + Bundler.reset! + Bundler::Source::Git::GitProxy.reset + Gem.clear_paths + end + + def the_bundle + TheBundle.new + end + + MAJOR_DEPRECATION = /^\[DEPRECATED\]\s*/ + + def err_without_deprecations + err.gsub(/#{MAJOR_DEPRECATION}.+[\n]?/, "") + end + + def deprecations + err.split("\n").filter_map {|l| l.sub(MAJOR_DEPRECATION, "") if l.match?(MAJOR_DEPRECATION) } + end + + def run(cmd, *args) + opts = args.last.is_a?(Hash) ? args.pop : {} + groups = args.map(&:inspect).join(", ") + setup = "require 'bundler' ; Bundler.ui.silence { Bundler.setup(#{groups}) }" + ruby([setup, cmd].join(" ; "), opts) + end + + def load_error_run(ruby, name, *args) + cmd = <<-RUBY + begin + #{ruby} + rescue LoadError => e + warn e.message if e.message.include?("-- #{name}") + end + RUBY + opts = args.last.is_a?(Hash) ? args.pop : {} + args += [opts] + run(cmd, *args) + end + + def in_bundled_app(cmd, options = {}) + sys_exec(cmd, dir: bundled_app, raise_on_error: options[:raise_on_error]) + end + + def bundle(cmd, options = {}, &block) + bundle_bin = options.delete(:bundle_bin) + bundle_bin ||= installed_bindir.join("bundle") + + env = options.delete(:env) || {} + + requires = options.delete(:requires) || [] + + dir = options.delete(:dir) || bundled_app + custom_load_path = options.delete(:load_path) + + load_path = [] + load_path << custom_load_path if custom_load_path + + build_env_options = { load_path: load_path, requires: requires, env: env } + build_env_options.merge!(artifice: options.delete(:artifice)) if options.key?(:artifice) || cmd.start_with?("exec") + + match_source(cmd) + + env = build_env(build_env_options) + + raise_on_error = options.delete(:raise_on_error) + + args = options.map do |k, v| + case v + when true + " --#{k}" + when false + " --no-#{k}" + else + " --#{k} #{v}" + end + end.join + + cmd = "#{Gem.ruby} #{bundle_bin} #{cmd}#{args}" + sys_exec(cmd, { env: env, dir: dir, raise_on_error: raise_on_error }, &block) + end + + def main_source(dir) + gemfile = File.expand_path("Gemfile", dir) + return unless File.exist?(gemfile) + + match = File.readlines(gemfile).first.match(/source ["'](?<source>[^"']+)["']/) + return unless match + + match[:source] + end + + def bundler(cmd, options = {}) + options[:bundle_bin] = system_gem_path("bin/bundler") + bundle(cmd, options) + end + + def ruby(ruby, options = {}) + env = build_env({ artifice: nil }.merge(options)) + escaped_ruby = ruby.shellescape + options[:env] = env if env + options[:dir] ||= bundled_app + sys_exec(%(#{Gem.ruby} -w -e #{escaped_ruby}), options) + end + + def load_error_ruby(ruby, name, opts = {}) + ruby(<<-R) + begin + #{ruby} + rescue LoadError => e + warn e.message if e.message.include?("-- #{name}") + end + R + end + + def build_env(options = {}) + env = options.delete(:env) || {} + libs = options.delete(:load_path) || [] + env["RUBYOPT"] = opt_add("-I#{libs.join(File::PATH_SEPARATOR)}", env["RUBYOPT"]) if libs.any? + + current_example = RSpec.current_example + + main_source = @gemfile_source if defined?(@gemfile_source) + compact_index_main_source = main_source&.start_with?("https://gem.repo", "https://gems.security") + + requires = options.delete(:requires) || [] + requires << hax + + artifice = options.delete(:artifice) do + if current_example && current_example.metadata[:realworld] + "vcr" + elsif compact_index_main_source + env["BUNDLER_SPEC_GEM_REPO"] ||= + case main_source + when "https://gem.repo1" then gem_repo1.to_s + when "https://gem.repo2" then gem_repo2.to_s + when "https://gem.repo3" then gem_repo3.to_s + when "https://gem.repo4" then gem_repo4.to_s + when "https://gems.security" then security_repo.to_s + end + + "compact_index" + else + "fail" + end + end + if artifice + requires << "#{Path.spec_dir}/support/artifice/#{artifice}.rb" + end + + requires.each {|r| env["RUBYOPT"] = opt_add("-r#{r}", env["RUBYOPT"]) } + + env + end + + def gembin(cmd, options = {}) + cmd = bundled_app("bin/#{cmd}") unless cmd.to_s.include?("/") + sys_exec(cmd.to_s, options) + end + + def sys_exec(cmd, options = {}, &block) + env = options[:env] || {} + env["RUBYOPT"] = opt_add(opt_add("-r#{spec_dir}/support/switch_rubygems.rb", env["RUBYOPT"]), ENV["RUBYOPT"]) + options[:env] = env + + sh(cmd, options, &block) + end + + def bundle_config(config = nil, path = bundled_app(".bundle/config")) + if config.is_a?(String) + key, value = config.split(" ", 2) + config = { Bundler::Settings.key_for(key) => value } + end + + current = File.exist?(path) ? Psych.load_file(path) : {} + return current unless config + + current = {} if current.empty? + + FileUtils.mkdir_p(File.dirname(path)) + + new_config = current.merge(config).compact + + File.open(path, "w+") do |f| + f.puts new_config.to_yaml + end + + new_config + end + + def bundle_config_global(config = nil) + bundle_config(config, home(".bundle/config")) + end + + def create_file(path, contents = "") + contents = strip_whitespace(contents) + path = Pathname.new(path).expand_path(bundled_app) unless path.is_a?(Pathname) + path.dirname.mkpath + path.write(contents) + + # if the file is a script, create respective bat file on Windows + if contents.start_with?("#!") + path.chmod(0o755) + if Gem.win_platform? + path.sub_ext(".bat").write <<~SCRIPT + @ECHO OFF + @"ruby.exe" "%~dpn0" %* + SCRIPT + end + end + end + + def gemfile(*args) + contents = args.pop + + if contents.nil? + read_gemfile + else + match_source(contents) + create_file(args.pop || "Gemfile", contents) + end + end + + def lockfile(*args) + contents = args.pop + + if contents.nil? + read_lockfile + else + create_file(args.pop || "Gemfile.lock", contents) + end + end + + def read_gemfile(file = "Gemfile") + read_bundled_app_file(file) + end + + def read_lockfile(file = "Gemfile.lock") + read_bundled_app_file(file) + end + + def read_bundled_app_file(file) + bundled_app(file).read + end + + def strip_whitespace(str) + # Trim the leading spaces + spaces = str[/\A\s+/, 0] || "" + str.gsub(/^#{spaces}/, "") + end + + def install_gemfile(*args) + opts = args.last.is_a?(Hash) ? args.pop : {} + gemfile(*args) + bundle :install, opts + end + + def lock_gemfile(*args) + gemfile(*args) + opts = args.last.is_a?(Hash) ? args.last : {} + bundle :lock, opts + end + + def base_system_gems(*names, **options) + system_gems names.map {|name| find_base_path(name) }, **options + end + + def system_gems(*gems) + gems = gems.flatten + options = gems.last.is_a?(Hash) ? gems.pop : {} + install_dir = options.fetch(:path, system_gem_path) + default = options.fetch(:default, false) + gems.each do |g| + gem_name = g.to_s + bundler = gem_name.match(/\Abundler-(?<version>.*)\z/) + + if bundler + with_built_bundler(bundler[:version], released: options.fetch(:released, false)) {|gem_path| install_gem(gem_path, install_dir, default) } + elsif %r{\A(?:[a-zA-Z]:)?/.*\.gem\z}.match?(gem_name) + install_gem(gem_name, install_dir, default) + else + gem_repo = options.fetch(:gem_repo, gem_repo1) + install_gem("#{gem_repo}/gems/#{gem_name}.gem", install_dir, default) + end + end + end + + def self.install_dev_bundler + extend self + + with_built_bundler(nil, build_path: tmp_root) {|gem_path| install_gem(gem_path, pristine_system_gem_path) } + end + + def install_gem(path, install_dir, default = false) + raise ArgumentError, "`#{path}` does not exist!" unless File.exist?(path) + + require "rubygems/installer" + + with_simulated_platform do + installer = Gem::Installer.at( + path.to_s, + install_dir: install_dir.to_s, + document: [], + ignore_dependencies: true, + wrappers: true, + env_shebang: true, + force: true + ) + installer.install + end + + if default + gem = Pathname.new(path).basename.to_s.match(/(.*)\.gem/)[1] + + # Revert Gem::Installer#write_spec and apply Gem::Installer#write_default_spec + FileUtils.mkdir_p File.join(install_dir, "specifications", "default") + File.rename File.join(install_dir, "specifications", gem + ".gemspec"), + File.join(install_dir, "specifications", "default", gem + ".gemspec") + + # Revert Gem::Installer#write_cache_file + File.delete File.join(install_dir, "cache", gem + ".gem") + end + end + + def uninstall_gem(name, options = {}) + require "rubygems/uninstaller" + + gem_home = options.dig(:env, "GEM_HOME") || system_gem_path.to_s + + with_env_vars("GEM_HOME" => gem_home) do + Gem.clear_paths + + uninstaller = Gem::Uninstaller.new( + name, + ignore: true, + executables: true, + all: true + ) + uninstaller.uninstall + ensure + Gem.clear_paths + end + end + + def installed_gems_list(options = {}) + gem_home = options.dig(:env, "GEM_HOME") || system_gem_path.to_s + + # Temporarily set GEM_HOME for the command + old_gem_home = ENV["GEM_HOME"] + ENV["GEM_HOME"] = gem_home + Gem.clear_paths + + begin + require "rubygems/commands/list_command" + + # Capture output from the list command + require "stringio" + output_io = StringIO.new + cmd = Gem::Commands::ListCommand.new + cmd.ui = Gem::StreamUI.new(StringIO.new, output_io, StringIO.new, false) + cmd.invoke + output = output_io.string.strip + ensure + ENV["GEM_HOME"] = old_gem_home + Gem.clear_paths + end + + # Create a fake command execution so `out` helper works + command_execution = Spec::CommandExecution.new("gem list", timeout: 60) + command_execution.original_stdout << output + command_execution.exitstatus = 0 + command_executions << command_execution + + output + end + + def with_built_bundler(version = nil, opts = {}, &block) + require_relative "builders" + + Builders::BundlerBuilder.new(self, "bundler", version)._build(opts, &block) + end + + def with_gem_path_as(path) + without_env_side_effects do + ENV["GEM_HOME"] = path.to_s + ENV["GEM_PATH"] = path.to_s + ENV["BUNDLER_ORIG_GEM_HOME"] = nil + ENV["BUNDLER_ORIG_GEM_PATH"] = nil + yield + end + end + + def with_path_as(path) + without_env_side_effects do + ENV["PATH"] = path.to_s + ENV["BUNDLER_ORIG_PATH"] = nil + yield + end + end + + def without_env_side_effects + backup = ENV.to_hash + yield + ensure + ENV.replace(backup) + end + + # Simulate the platform set by BUNDLER_SPEC_PLATFORM for in-process + # operations, mirroring what hax.rb does for subprocesses. + def with_simulated_platform + spec_platform = ENV["BUNDLER_SPEC_PLATFORM"] + unless spec_platform + return yield + end + + old_arch = RbConfig::CONFIG["arch"] + old_host_os = RbConfig::CONFIG["host_os"] + + if /mingw|mswin/.match?(spec_platform) + Gem.class_variable_set(:@@win_platform, nil) # rubocop:disable Style/ClassVars + RbConfig::CONFIG["host_os"] = spec_platform.gsub(/^[^-]+-/, "").tr("-", "_") + end + + RbConfig::CONFIG["arch"] = spec_platform + Gem::Platform.instance_variable_set(:@local, nil) + Gem.instance_variable_set(:@platforms, []) + + yield + ensure + if spec_platform + RbConfig::CONFIG["arch"] = old_arch + RbConfig::CONFIG["host_os"] = old_host_os + Gem::Platform.instance_variable_set(:@local, nil) + Gem.instance_variable_set(:@platforms, []) + end + end + + def with_path_added(path) + with_path_as([path.to_s, ENV["PATH"]].join(File::PATH_SEPARATOR)) do + yield + end + end + + def break_git! + FileUtils.mkdir_p(tmp("broken_path")) + File.open(tmp("broken_path/git"), "w", 0o755) do |f| + f.puts "#!/usr/bin/env ruby\nSTDERR.puts 'This is not the git you are looking for'\nexit 1" + end + + ENV["PATH"] = "#{tmp("broken_path")}:#{ENV["PATH"]}" + end + + def with_fake_man + FileUtils.mkdir_p(tmp("fake_man")) + create_file(tmp("fake_man/man"), <<~SCRIPT) + #!/usr/bin/env ruby + puts ARGV.inspect + SCRIPT + with_path_added(tmp("fake_man")) { yield } + end + + def pristine_system_gems(*gems) + FileUtils.rm_r(system_gem_path) + + if gems.any? + system_gems(*gems) + else + default_system_gems + end + end + + def cache_gems(*gems, gem_repo: gem_repo1) + gems = gems.flatten + + FileUtils.mkdir_p("#{bundled_app}/vendor/cache") + + gems.each do |g| + path = "#{gem_repo}/gems/#{g}.gem" + raise ArgumentError, "`#{path}` does not exist!" unless File.exist?(path) + FileUtils.cp(path, "#{bundled_app}/vendor/cache") + end + end + + def simulate_new_machine + FileUtils.rm_r bundled_app(".bundle") + pristine_system_gems + end + + def default_system_gems + FileUtils.cp_r pristine_system_gem_path, system_gem_path + end + + def simulate_ruby_platform(ruby_platform) + old = ENV["BUNDLER_SPEC_RUBY_PLATFORM"] + ENV["BUNDLER_SPEC_RUBY_PLATFORM"] = ruby_platform.to_s + yield + ensure + ENV["BUNDLER_SPEC_RUBY_PLATFORM"] = old + end + + def simulate_platform(platform) + old = ENV["BUNDLER_SPEC_PLATFORM"] + ENV["BUNDLER_SPEC_PLATFORM"] = platform.to_s + yield + ensure + ENV["BUNDLER_SPEC_PLATFORM"] = old if block_given? + end + + def current_ruby_minor + Gem.ruby_version.segments.tap {|s| s.delete_at(2) }.join(".") + end + + def next_ruby_minor + ruby_major_minor.map.with_index {|s, i| i == 1 ? s + 1 : s }.join(".") + end + + def ruby_major_minor + Gem.ruby_version.segments[0..1] + end + + def revision_for(path) + git("rev-parse HEAD", path).strip + end + + def with_read_only(pattern) + chmod = lambda do |dirmode, filemode| + lambda do |f| + mode = File.directory?(f) ? dirmode : filemode + File.chmod(mode, f) + end + end + + Dir[pattern].each(&chmod[0o555, 0o444]) + yield + ensure + Dir[pattern].each(&chmod[0o755, 0o644]) + end + + # Simulate replacing TODOs with real values + def prepare_gemspec(pathname) + process_file(pathname) do |line| + case line + when /spec\.metadata\["(?:allowed_push_host|homepage_uri|source_code_uri|changelog_uri)"\]/, /spec\.homepage/ + line.gsub(/\=.*$/, '= "http://example.org"') + when /spec\.summary/ + line.gsub(/\=.*$/, '= "A short summary of my new gem."') + when /spec\.description/ + line.gsub(/\=.*$/, '= "A longer description of my new gem."') + else + line + end + end + end + + def process_file(pathname) + changed_lines = pathname.readlines.map do |line| + yield line + end + File.open(pathname, "w") {|file| file.puts(changed_lines.join) } + end + + def with_env_vars(env_hash, &block) + current_values = {} + env_hash.each do |k, v| + current_values[k] = ENV[k] + ENV[k] = v + end + block.call if block_given? + env_hash.each do |k, _| + ENV[k] = current_values[k] + end + end + + def require_rack_test + # need to hack, so we can require rack for testing + old_gem_home = ENV["GEM_HOME"] + ENV["GEM_HOME"] = Spec::Path.scoped_base_system_gem_path.to_s + require "rack/test" + ENV["GEM_HOME"] = old_gem_home + end + + def exit_status_for_signal(signal_number) + # For details see: https://en.wikipedia.org/wiki/Exit_status#Shell_and_scripts + 128 + signal_number + end + + def empty_repo4 + FileUtils.rm_r gem_repo4 + + build_repo4 {} + end + + private + + def match_source(contents) + match = /source ["']?(?<source>http[^"']+)["']?/.match(contents) + return unless match + + @gemfile_source = match[:source] + end + + def git_root_dir? + root.to_s == `git rev-parse --show-toplevel`.chomp + end + end +end diff --git a/spec/bundler/support/indexes.rb b/spec/bundler/support/indexes.rb new file mode 100644 index 0000000000..1fbdd49abe --- /dev/null +++ b/spec/bundler/support/indexes.rb @@ -0,0 +1,424 @@ +# frozen_string_literal: true + +module Spec + module Indexes + def dep(name, reqs = nil) + @deps ||= [] + @deps << Bundler::Dependency.new(name, reqs) + end + + def platform(*args) + @platforms ||= [] + @platforms.concat args.map {|p| Gem::Platform.new(p) } + end + + alias_method :platforms, :platform + + def resolve(args = [], dependency_api_available: true) + @platforms ||= ["ruby"] + default_source = instance_double("Bundler::Source::Rubygems", specs: @index, to_s: "locally install gems", dependency_api_available?: dependency_api_available) + source_requirements = { default: default_source } + base = args[0] || Bundler::SpecSet.new([]) + base.each {|ls| ls.source = default_source } + gem_version_promoter = args[1] || Bundler::GemVersionPromoter.new + originally_locked = args[2] || Bundler::SpecSet.new([]) + unlock = args[3] || [] + @deps.each do |d| + name = d.name + source_requirements[name] = d.source = default_source + end + packages = Bundler::Resolver::Base.new(source_requirements, @deps, base, @platforms, locked_specs: originally_locked, unlock: unlock) + Bundler::Resolver.new(packages, gem_version_promoter).start + end + + def should_not_resolve + expect { resolve }.to raise_error(Bundler::GemNotFound) + end + + def should_resolve_as(specs) + got = resolve + got = got.map(&:full_name).sort + expect(got).to eq(specs.sort) + end + + def should_resolve_without_dependency_api(specs) + got = resolve(dependency_api_available: false) + got = got.map(&:full_name).sort + expect(got).to eq(specs.sort) + end + + def should_resolve_and_include(specs, args = []) + got = resolve(args) + got = got.map(&:full_name).sort + specs.each do |s| + expect(got).to include(s) + end + end + + def gem(*args, &blk) + build_spec(*args, &blk).first + end + + def locked(*args) + Bundler::SpecSet.new(args.map do |name, version| + gem(name, version) + end) + end + + def should_conservative_resolve_and_include(opts, unlock, specs) + opts = Array(opts) + search = Bundler::GemVersionPromoter.new.tap do |s| + s.level = opts.first + s.strict = opts.include?(:strict) + end + should_resolve_and_include specs, [@base, search, @locked, unlock] + end + + def an_awesome_index + build_index do + gem "myrack", %w[0.8 0.9 0.9.1 0.9.2 1.0 1.1] + gem "myrack-mount", %w[0.4 0.5 0.5.1 0.5.2 0.6] + + # --- Pre-release support + gem "RubyGems\0", ["1.3.2"] + + # --- Rails + versions "1.2.3 2.2.3 2.3.5 3.0.0.beta 3.0.0.beta1" do |version| + gem "activesupport", version + gem "actionpack", version do + dep "activesupport", version + if version >= v("3.0.0.beta") + dep "myrack", "~> 1.1" + dep "myrack-mount", ">= 0.5" + elsif version > v("2.3") then dep "myrack", "~> 1.0.0" + elsif version > v("2.0.0") then dep "myrack", "~> 0.9.0" + end + end + gem "activerecord", version do + dep "activesupport", version + dep "arel", ">= 0.2" if version >= v("3.0.0.beta") + end + gem "actionmailer", version do + dep "activesupport", version + dep "actionmailer", version + end + if version < v("3.0.0.beta") + gem "railties", version do + dep "activerecord", version + dep "actionpack", version + dep "actionmailer", version + dep "activesupport", version + end + else + gem "railties", version + gem "rails", version do + dep "activerecord", version + dep "actionpack", version + dep "actionmailer", version + dep "activesupport", version + dep "railties", version + end + end + end + + versions "1.0 1.2 1.2.1 1.2.2 1.3 1.3.0.1 1.3.5 1.4.0 1.4.2 1.4.2.1" do |version| + platforms "ruby java mswin32 mingw32 x64-mingw-ucrt" do |platform| + next if version == v("1.4.2.1") && platform != pl("x86-mswin32") + next if version == v("1.4.2") && platform == pl("x86-mswin32") + gem "nokogiri", version, platform do + dep "weakling", ">= 0.0.3" if platform =~ pl("java") # rubocop:disable Performance/RegexpMatch + end + end + end + + versions "0.0.1 0.0.2 0.0.3" do |version| + gem "weakling", version + end + + # --- Rails related + versions "1.2.3 2.2.3 2.3.5" do |version| + gem "activemerchant", version do + dep "activesupport", ">= #{version}" + end + end + + gem "reform", ["1.0.0"] do + dep "activesupport", ">= 1.0.0.beta1" + end + + gem "need-pre", ["1.0.0"] do + dep "activesupport", "~> 3.0.0.beta1" + end + end + end + + # Builder 3.1.4 will activate first, but if all + # goes well, it should resolve to 3.0.4 + def a_conflict_index + build_index do + gem "builder", %w[3.0.4 3.1.4] + gem("grape", "0.2.6") do + dep "builder", ">= 0" + end + + versions "3.2.8 3.2.9 3.2.10 3.2.11" do |version| + gem("activemodel", version) do + dep "builder", "~> 3.0.0" + end + end + + gem("my_app", "1.0.0") do + dep "activemodel", ">= 0" + dep "grape", ">= 0" + end + end + end + + def a_complex_conflict_index + build_index do + gem("a", %w[1.0.2 1.1.4 1.2.0 1.4.0]) do + dep "d", ">= 0" + end + + gem("d", %w[1.3.0 1.4.1]) do + dep "x", ">= 0" + end + + gem "d", "0.9.8" + + gem("b", "0.3.4") do + dep "a", ">= 1.5.0" + end + + gem("b", "0.3.5") do + dep "a", ">= 1.2" + end + + gem("b", "0.3.3") do + dep "a", "> 1.0" + end + + versions "3.2 3.3" do |version| + gem("c", version) do + dep "a", "~> 1.0" + end + end + + gem("my_app", "1.3.0") do + dep "c", ">= 4.0" + dep "b", ">= 0" + end + + gem("my_app", "1.2.0") do + dep "c", "~> 3.3.0" + dep "b", "0.3.4" + end + + gem("my_app", "1.1.0") do + dep "c", "~> 3.2.0" + dep "b", "0.3.5" + end + end + end + + def index_with_conflict_on_child + build_index do + gem "json", %w[1.6.5 1.7.7 1.8.0] + + gem("chef", "10.26") do + dep "json", [">= 1.4.4", "<= 1.7.7"] + end + + gem("berkshelf", "2.0.7") do + dep "json", ">= 1.7.7" + end + + gem("chef_app", "1.0.0") do + dep "berkshelf", "~> 2.0" + dep "chef", "~> 10.26" + end + end + end + + # Issue #3459 + def a_complicated_index + build_index do + gem "foo", %w[3.0.0 3.0.5] do + dep "qux", ["~> 3.1"] + dep "baz", ["< 9.0", ">= 5.0"] + dep "bar", ["~> 1.0"] + dep "grault", ["~> 3.1"] + end + + gem "foo", "1.2.1" do + dep "baz", ["~> 4.2"] + dep "bar", ["~> 1.0"] + dep "qux", ["~> 3.1"] + dep "grault", ["~> 2.0"] + end + + gem "bar", "1.0.5" do + dep "grault", ["~> 3.1"] + dep "baz", ["< 9", ">= 4.2"] + end + + gem "bar", "1.0.3" do + dep "baz", ["< 9", ">= 4.2"] + dep "grault", ["~> 2.0"] + end + + gem "baz", "8.2.10" do + dep "grault", ["~> 3.0"] + dep "garply", [">= 0.5.1", "~> 0.5"] + end + + gem "baz", "5.0.2" do + dep "grault", ["~> 2.0"] + dep "garply", [">= 0.3.1"] + end + + gem "baz", "4.2.0" do + dep "grault", ["~> 2.0"] + dep "garply", [">= 0.3.1"] + end + + gem "grault", %w[2.6.3 3.1.1] + + gem "garply", "0.5.1" do + dep "waldo", ["~> 0.1.3"] + end + + gem "waldo", "0.1.5" do + dep "plugh", ["~> 0.6.0"] + end + + gem "plugh", %w[0.6.3 0.6.11 0.7.0] + + gem "qux", "3.2.21" do + dep "plugh", [">= 0.6.4", "~> 0.6"] + dep "corge", ["~> 1.0"] + end + + gem "corge", "1.10.1" + end + end + + def a_unresolvable_child_index + build_index do + gem "json", %w[1.8.0] + + gem("chef", "10.26") do + dep "json", [">= 1.4.4", "<= 1.7.7"] + end + + gem("berkshelf", "2.0.7") do + dep "json", ">= 1.7.7" + end + + gem("chef_app_error", "1.0.0") do + dep "berkshelf", "~> 2.0" + dep "chef", "~> 10.26" + end + end + end + + def a_index_with_root_conflict_on_child + build_index do + gem "builder", %w[2.1.2 3.0.1 3.1.3] + gem "i18n", %w[0.4.1 0.4.2] + + gem "activesupport", %w[3.0.0 3.0.1 3.0.5 3.1.7] + + gem("activemodel", "3.0.5") do + dep "activesupport", "= 3.0.5" + dep "builder", "~> 2.1.2" + dep "i18n", "~> 0.4" + end + + gem("activemodel", "3.0.0") do + dep "activesupport", "= 3.0.0" + dep "builder", "~> 2.1.2" + dep "i18n", "~> 0.4.1" + end + + gem("activemodel", "3.1.3") do + dep "activesupport", "= 3.1.3" + dep "builder", "~> 2.1.2" + dep "i18n", "~> 0.5" + end + + gem("activerecord", "3.0.0") do + dep "activesupport", "= 3.0.0" + dep "activemodel", "= 3.0.0" + end + + gem("activerecord", "3.0.5") do + dep "activesupport", "= 3.0.5" + dep "activemodel", "= 3.0.5" + end + + gem("activerecord", "3.0.9") do + dep "activesupport", "= 3.1.5" + dep "activemodel", "= 3.1.5" + end + end + end + + def a_circular_index + build_index do + gem "myrack", "1.0.1" + gem("foo", "0.2.6") do + dep "bar", ">= 0" + end + + gem("bar", "1.0.0") do + dep "foo", ">= 0" + end + + gem("circular_app", "1.0.0") do + dep "foo", ">= 0" + dep "bar", ">= 0" + end + end + end + + def an_ambiguous_index + build_index do + gem("a", "1.0.0") do + dep "c", ">= 0" + end + + gem("b", %w[0.5.0 1.0.0]) + + gem("b", "2.0.0") do + dep "c", "< 2.0.0" + end + + gem("c", "1.0.0") do + dep "d", "1.0.0" + end + + gem("c", "2.0.0") do + dep "d", "2.0.0" + end + + gem("d", %w[1.0.0 2.0.0]) + end + end + + def optional_prereleases_index + build_index do + gem("a", %w[1.0.0]) + + gem("a", "2.0.0") do + dep "b", ">= 2.0.0.pre" + end + + gem("b", %w[0.9.0 1.5.0 2.0.0.pre]) + + # --- Pre-release support + gem "RubyGems\0", ["1.3.2"] + end + end + end +end diff --git a/spec/bundler/support/matchers.rb b/spec/bundler/support/matchers.rb new file mode 100644 index 0000000000..5a3c38a4db --- /dev/null +++ b/spec/bundler/support/matchers.rb @@ -0,0 +1,227 @@ +# frozen_string_literal: true + +require "forwardable" +require_relative "the_bundle" + +module Spec + module Matchers + extend RSpec::Matchers + + class Precondition + include RSpec::Matchers::Composable + extend Forwardable + def_delegators :failing_matcher, + :failure_message, + :actual, + :description, + :diffable?, + :expected, + :failure_message_when_negated + + def initialize(matcher, preconditions) + @matcher = with_matchers_cloned(matcher) + @preconditions = with_matchers_cloned(preconditions) + @failure_index = nil + end + + def matches?(target, &blk) + return false if @failure_index = @preconditions.index {|pc| !pc.matches?(target, &blk) } + @matcher.matches?(target, &blk) + end + + def does_not_match?(target, &blk) + return false if @failure_index = @preconditions.index {|pc| !pc.matches?(target, &blk) } + if @matcher.respond_to?(:does_not_match?) + @matcher.does_not_match?(target, &blk) + else + !@matcher.matches?(target, &blk) + end + end + + def expects_call_stack_jump? + @matcher.expects_call_stack_jump? || @preconditions.any?(&:expects_call_stack_jump) + end + + def supports_block_expectations? + @matcher.supports_block_expectations? || @preconditions.any?(&:supports_block_expectations) + end + + def failing_matcher + @failure_index ? @preconditions[@failure_index] : @matcher + end + end + + def self.define_compound_matcher(matcher, preconditions, &declarations) + raise ArgumentError, "Must have preconditions to define a compound matcher" if preconditions.empty? + define_method(matcher) do |*expected, &block_arg| + Precondition.new( + RSpec::Matchers::DSL::Matcher.new(matcher, declarations, self, *expected, &block_arg), + preconditions + ) + end + end + + RSpec::Matchers.define :have_dep do |*args| + dep = Bundler::Dependency.new(*args) + + match do |actual| + actual.length == 1 && actual.all? {|d| d == dep } + end + end + + RSpec::Matchers.define :have_gem do |*args| + match do |actual| + actual.length == args.length && actual.all? {|a| args.include?(a.full_name) } + end + end + + RSpec::Matchers.define :be_sorted do + diffable + attr_reader :expected + match do |actual| + expected = block_arg ? actual.sort_by(&block_arg) : actual.sort + actual.==(expected).tap do + # HACK: since rspec won't show a diff when everything is a string + differ = RSpec::Support::Differ.new + @actual = differ.send(:object_to_string, actual) + @expected = differ.send(:object_to_string, expected) + end + end + end + + RSpec::Matchers.define :be_well_formed do + match(&:empty?) + + failure_message do |actual| + actual.join("\n") + end + end + + define_compound_matcher :read_as, [exist] do |file_contents| + diffable + + match do |actual| + @actual = Bundler.read_file(actual) + values_match?(file_contents, @actual) + end + end + + def indent(string, padding = 4, indent_character = " ") + string.to_s.gsub(/^/, indent_character * padding).gsub("\t", " ") + end + + define_compound_matcher :include_gems, [be_an_instance_of(Spec::TheBundle)] do |*names| + match do + opts = names.last.is_a?(Hash) ? names.pop : {} + source = opts.delete(:source) + groups = Array(opts.delete(:groups)).map(&:inspect).join(", ") + opts[:raise_on_error] = false + @errors = names.filter_map do |full_name| + name, version, platform = full_name.split(/\s+/) + platform ||= "ruby" + require_path = name.tr("-", "/") + version_const = name == "bundler" ? "Bundler::VERSION" : Spec::Builders.constantize(name) + source_const = "#{Spec::Builders.constantize(name)}_SOURCE" + ruby <<~R, opts + require 'bundler' + Bundler.setup(#{groups}) + + require '#{require_path}' + actual_version, actual_platform = #{version_const}.split(/\s+/, 2) + actual_platform ||= "ruby" + unless Gem::Version.new(actual_version) == Gem::Version.new('#{version}') + puts actual_version + exit 64 + end + unless actual_platform.to_s == '#{platform}' + puts actual_platform + exit 65 + end + require '#{require_path}/source' + exit 0 if #{source.nil?} + actual_source = #{source_const} + unless actual_source == '#{source}' + puts actual_source + exit 66 + end + R + next if exitstatus == 0 + if exitstatus == 64 + actual_version = out.split("\n").last + next "#{name} was expected to be at version #{version} but was #{actual_version}" + end + if exitstatus == 65 + actual_platform = out.split("\n").last + next "#{name} was expected to be of platform #{platform} but was #{actual_platform}" + end + if exitstatus == 66 + actual_source = out.split("\n").last + next "Expected #{name} (#{version}) to be installed from `#{source}`, was actually from `#{actual_source}`" + end + next "Command to check for inclusion of gem #{full_name} failed" + end + + @errors.empty? + end + + match_when_negated do + opts = names.last.is_a?(Hash) ? names.pop : {} + groups = Array(opts.delete(:groups)).map(&:inspect).join(", ") + opts[:raise_on_error] = false + @errors = names.filter_map do |name| + name, version = name.split(/\s+/, 2) + ruby <<-R, opts + begin + require 'bundler' + Bundler.setup(#{groups}) + rescue Bundler::GemNotFound, Bundler::GitError + exit 0 + end + + begin + require '#{name}' + name_constant = #{Spec::Builders.constantize(name)} + if #{version.nil?} || name_constant == '#{version}' + exit 64 + else + exit 0 + end + rescue LoadError, NameError + exit 0 + end + R + next if exitstatus == 0 + next "command to check version of #{name} installed failed" unless exitstatus == 64 + next "expected #{name} to not be installed, but it was" if version.nil? + next "expected #{name} (#{version}) not to be installed, but it was" + end + + @errors.empty? + end + + failure_message do + super() + " but:\n" + @errors.map {|e| indent(e) }.join("\n") + end + + failure_message_when_negated do + super() + " but:\n" + @errors.map {|e| indent(e) }.join("\n") + end + end + RSpec::Matchers.define_negated_matcher :not_include_gems, :include_gems + RSpec::Matchers.alias_matcher :include_gem, :include_gems + + def plugin_should_be_installed(*names) + names.each do |name| + expect(Bundler::Plugin).to be_installed(name) + path = Pathname.new(Bundler::Plugin.installed?(name)) + expect(path + "plugins.rb").to exist + end + end + + def plugin_should_not_be_installed(*names) + names.each do |name| + expect(Bundler::Plugin).not_to be_installed(name) + end + end + end +end diff --git a/spec/bundler/support/options.rb b/spec/bundler/support/options.rb new file mode 100644 index 0000000000..551fa1acd8 --- /dev/null +++ b/spec/bundler/support/options.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Spec + module Options + def opt_add(option, options) + [option.strip, options].compact.reject(&:empty?).join(" ") + end + + def opt_remove(option, options) + return unless options + + options.split(" ").reject {|opt| opt.strip == option.strip }.join(" ") + end + end +end diff --git a/spec/bundler/support/path.rb b/spec/bundler/support/path.rb new file mode 100644 index 0000000000..2e6486412f --- /dev/null +++ b/spec/bundler/support/path.rb @@ -0,0 +1,381 @@ +# frozen_string_literal: true + +require "pathname" unless defined?(Pathname) +require "rbconfig" + +require_relative "env" + +module Spec + module Path + include Spec::Env + + def source_root + @source_root ||= Pathname.new(ruby_core? ? "../../.." : "../../bundler").expand_path(__dir__) + end + + def root + @root ||= system_gem_path("gems/bundler-#{Bundler::VERSION}") + end + + def gemspec + @gemspec ||= source_root.join(relative_gemspec) + end + + def relative_gemspec + @relative_gemspec ||= ruby_core? ? "lib/bundler/bundler.gemspec" : "bundler.gemspec" + end + + def loaded_gemspec + @loaded_gemspec ||= Dir.chdir(source_root) { Gem::Specification.load(gemspec.to_s) } + end + + def test_gemfile + @test_gemfile ||= tool_dir.join("test_gems.rb") + end + + def rubocop_gemfile + @rubocop_gemfile ||= source_root.join(rubocop_gemfile_basename) + end + + def standard_gemfile + @standard_gemfile ||= source_root.join(standard_gemfile_basename) + end + + def dev_gemfile + @dev_gemfile ||= tool_dir.join("dev_gems.rb") + end + + def dev_binstub + @dev_binstub ||= bindir.join("bundle") + end + + def bindir + @bindir ||= source_root.join(ruby_core? ? "spec/bin" : "../bin") + end + + def exedir + @exedir ||= source_root.join(ruby_core? ? "libexec" : "exe") + end + + def installed_bindir + @installed_bindir ||= system_gem_path("bin") + end + + def gem_cmd + @gem_cmd ||= ruby_core? ? source_root.join("bin/gem") : "gem" + end + + def gem_bin + @gem_bin ||= ENV["GEM_COMMAND"] || "gem" + end + + def path + env_path = ENV["PATH"] + env_path = env_path.split(File::PATH_SEPARATOR).reject {|path| path == exedir.to_s }.join(File::PATH_SEPARATOR) if ruby_core? + env_path + end + + def spec_dir + @spec_dir ||= source_root.join(ruby_core? ? "spec/bundler" : "../spec") + end + + def man_dir + @man_dir ||= lib_dir.join("bundler/man") + end + + def hax + @hax ||= spec_dir.join("support/hax.rb") + end + + def tracked_files + @tracked_files ||= git_ls_files(tracked_files_glob) + end + + def shipped_files + @shipped_files ||= if ruby_core_tarball? + loaded_gemspec.files.map {|f| f.gsub(%r{^exe/}, "libexec/") } + elsif ruby_core? + tracked_files + else + loaded_gemspec.files + end + end + + def lib_tracked_files + @lib_tracked_files ||= git_ls_files(lib_tracked_files_glob) + end + + def man_tracked_files + @man_tracked_files ||= git_ls_files(man_tracked_files_glob) + end + + def tmp(*path) + tmp_root.join("#{test_env_version}.#{scope}").join(*path) + end + + def tmp_root + if ruby_core? && (tmpdir = ENV["TMPDIR"]) + # Use realpath to resolve any symlinks in TMPDIR (e.g., on macOS /var -> /private/var) + real = begin + File.realpath(tmpdir) + rescue Errno::ENOENT, Errno::EACCES + tmpdir + end + Pathname(real) + else + (ruby_core? ? source_root : source_root.parent).join("tmp") + end + end + + # Bump this version whenever you make a breaking change to the spec setup + # that requires regenerating tmp/. + + def test_env_version + 2 + end + + def scope + test_number = ENV["TEST_ENV_NUMBER"] + return "1" if test_number.nil? + + test_number.empty? ? "1" : test_number + end + + def home(*path) + tmp("home", *path) + end + + def default_bundle_path(*path) + system_gem_path(*path) + end + + def default_cache_path(*path) + default_bundle_path("cache/bundler", *path) + end + + def compact_index_cache_path + home(".bundle/cache/compact_index") + end + + def bundled_app(*path) + root = tmp("bundled_app") + FileUtils.mkdir_p(root) + root.join(*path) + end + + def bundled_app2(*path) + root = tmp("bundled_app2") + FileUtils.mkdir_p(root) + root.join(*path) + end + + def vendored_gems(path = nil) + scoped_gem_path(bundled_app("vendor/bundle")).join(*[path].compact) + end + + def cached_gem(path) + bundled_app("vendor/cache/#{path}.gem") + end + + def bundled_app_gemfile + bundled_app("Gemfile") + end + + def bundled_app_lock + bundled_app("Gemfile.lock") + end + + def scoped_base_system_gem_path + scoped_gem_path(base_system_gem_path) + end + + def base_system_gem_path + tmp_root.join("gems/base") + end + + def rubocop_gem_path + tmp_root.join("gems/rubocop") + end + + def standard_gem_path + tmp_root.join("gems/standard") + end + + def file_uri_for(path) + protocol = "file://" + root = Gem.win_platform? ? "/" : "" + + protocol + root + path.to_s + end + + def gem_repo1(*args) + gem_path("remote1", *args) + end + + def gem_repo_missing(*args) + gem_path("missing", *args) + end + + def gem_repo2(*args) + gem_path("remote2", *args) + end + + def gem_repo3(*args) + gem_path("remote3", *args) + end + + def gem_repo4(*args) + gem_path("remote4", *args) + end + + def security_repo(*args) + gem_path("security_repo", *args) + end + + def system_gem_path(*path) + gem_path("system", *path) + end + + def pristine_system_gem_path + tmp_root.join("gems/pristine_system") + end + + def local_gem_path(*path, base: bundled_app) + scoped_gem_path(base.join(".bundle")).join(*path) + end + + def scoped_gem_path(base) + base.join(Gem.ruby_engine, RbConfig::CONFIG["ruby_version"]) + end + + def gem_path(*args) + tmp("gems", *args) + end + + def lib_path(*args) + tmp("libs", *args) + end + + def source_lib_dir + source_root.join("lib") + end + + def lib_dir + root.join("lib") + end + + def global_plugin_gem(*args) + home ".bundle", "plugin", "gems", *args + end + + def local_plugin_gem(*args) + bundled_app ".bundle", "plugin", "gems", *args + end + + def tmpdir(*args) + tmp "tmpdir", *args + end + + def replace_version_file(version, dir: source_root) + version_file = File.expand_path("lib/bundler/version.rb", dir) + contents = File.read(version_file) + contents.sub!(/(^\s+VERSION\s*=\s*).*$/, %(\\1"#{version}")) + File.open(version_file, "w") {|f| f << contents } + end + + def replace_required_ruby_version(version, dir:) + gemspec_file = File.expand_path("bundler.gemspec", dir) + contents = File.read(gemspec_file) + contents.sub!(/(^\s+s\.required_ruby_version\s*=\s*)"[^"]+"/, %(\\1"#{version}")) + File.open(gemspec_file, "w") {|f| f << contents } + end + + def replace_changelog(version, dir:) + changelog = File.expand_path("CHANGELOG.md", dir) + contents = File.readlines(changelog) + contents = [contents[0], contents[1], "## #{version} (2100-01-01)\n", *contents[3..-1]].join + File.open(changelog, "w") {|f| f << contents } + end + + def git_root + ruby_core? ? source_root : source_root.parent + end + + def rake_path + find_base_path("rake") + end + + def rake_version + File.basename(rake_path).delete_prefix("rake-").delete_suffix(".gem") + end + + def sinatra_dependency_paths + deps = %w[ + mustermann + rack + rack-protection + rack-session + tilt + sinatra + base64 + logger + compact_index + ] + path = if deps.all? {|dep| !Dir[scoped_base_system_gem_path.join("gems/#{dep}-*")].empty? } + scoped_base_system_gem_path + elsif ruby_core? && deps.all? {|dep| !Dir[source_root.join(".bundle/gems/#{dep}-*")].empty? } + source_root.join(".bundle") + else + scoped_base_system_gem_path + end + + Dir[path.join("gems/{#{deps.join(",")}}-*/lib")].map(&:to_s) + end + + private + + def find_base_path(name) + Dir["#{scoped_base_system_gem_path}/**/#{name}-*.gem"].first + end + + def git_ls_files(glob) + skip "Not running on a git context, since running tests from a tarball" if ruby_core_tarball? + + git("ls-files -z -- #{glob}", source_root).split("\x0") + end + + def tracked_files_glob + ruby_core? ? "libexec/bundle* lib/bundler lib/bundler.rb spec/bundler man/bundle*" : "lib exe CHANGELOG.md LICENSE.md README.md bundler.gemspec" + end + + def lib_tracked_files_glob + ruby_core? ? "lib/bundler lib/bundler.rb" : "lib" + end + + def man_tracked_files_glob + "lib/bundler/man/bundle*.1.ronn lib/bundler/man/gemfile*.5.ronn" + end + + def ruby_core_tarball? + !git_root.join(".git").directory? + end + + def rubocop_gemfile_basename + tool_dir.join("rubocop_gems.rb") + end + + def standard_gemfile_basename + tool_dir.join("standard_gems.rb") + end + + def tool_dir + ruby_core? ? source_root.join("tool/bundler") : source_root.join("../tool/bundler") + end + + def templates_dir + lib_dir.join("bundler", "templates") + end + + extend self + end +end diff --git a/spec/bundler/support/permissions.rb b/spec/bundler/support/permissions.rb new file mode 100644 index 0000000000..b21ce3848d --- /dev/null +++ b/spec/bundler/support/permissions.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Spec + module Permissions + def with_umask(new_umask) + old_umask = File.umask(new_umask) + yield if block_given? + ensure + File.umask(old_umask) + end + end +end diff --git a/spec/bundler/support/platforms.rb b/spec/bundler/support/platforms.rb new file mode 100644 index 0000000000..56a0843005 --- /dev/null +++ b/spec/bundler/support/platforms.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Spec + module Platforms + def not_local + generic_local_platform == Gem::Platform::RUBY ? "java" : Gem::Platform::RUBY + end + + def local_platform + Bundler.local_platform + end + + def generic_local_platform + Gem::Platform.generic(local_platform) + end + + def local_tag + if Gem.java_platform? + :jruby + elsif Gem.win_platform? + :windows + else + :ruby + end + end + + def not_local_tag + [:jruby, :windows, :ruby].find {|tag| tag != local_tag } + end + + def local_ruby_engine + RUBY_ENGINE + end + + def local_engine_version + RUBY_ENGINE == "ruby" ? Gem.ruby_version : RUBY_ENGINE_VERSION + end + + def not_local_engine_version + case not_local_tag + when :ruby, :windows + not_local_ruby_version + when :jruby + "1.6.1" + end + end + + def not_local_ruby_version + "1.12" + end + + def not_local_patchlevel + 9999 + end + + def default_platform_list(*extra, defaults: default_locked_platforms) + defaults.concat(extra).map(&:to_s).uniq + end + + def lockfile_platforms(*extra, defaults: default_locked_platforms) + platforms = default_platform_list(*extra, defaults: defaults) + platforms.sort.join("\n ") + end + + def default_locked_platforms + [local_platform, generic_default_locked_platform].compact + end + + def generic_default_locked_platform + return unless Bundler::MatchPlatform.generic_local_platform_is_ruby? + + Gem::Platform::RUBY + end + end +end diff --git a/spec/bundler/support/rubygems_ext.rb b/spec/bundler/support/rubygems_ext.rb new file mode 100644 index 0000000000..812dc4deaa --- /dev/null +++ b/spec/bundler/support/rubygems_ext.rb @@ -0,0 +1,222 @@ +# frozen_string_literal: true + +abort "RubyGems only supports Ruby 3.2 or higher" if RUBY_VERSION < "3.2.0" + +require_relative "path" + +$LOAD_PATH.unshift(Spec::Path.source_lib_dir.to_s) + +module Spec + module Rubygems + extend self + + def gem_load(gem_name, bin_container) + require_relative "switch_rubygems" + + gem_load_and_activate(gem_name, bin_container) + end + + def gem_load_and_possibly_install(gem_name, bin_container) + require_relative "switch_rubygems" + + gem_load_activate_and_possibly_install(gem_name, bin_container) + end + + def gem_require(gem_name, entrypoint) + gem_activate(gem_name) + require entrypoint + end + + def test_setup + # Install test dependencies unless parallel-rspec is being used, since in that case they should be setup already + install_test_deps unless ENV["RSPEC_FORMATTER_OUTPUT_ID"] + + setup_test_paths + + require "fileutils" + + FileUtils.mkdir_p(Path.home) + FileUtils.mkdir_p(Path.tmpdir) + + ENV["HOME"] = Path.home.to_s + # Remove "RUBY_CODESIGN", which is used by mkmf-generated Makefile to + # sign extension bundles on macOS, to avoid trying to find the specified key + # from the fake $HOME/Library/Keychains directory. + ENV.delete "RUBY_CODESIGN" + if Path.ruby_core? + if (tmpdir = ENV["TMPDIR"]) + tmpdir_real = begin + File.realpath(tmpdir) + rescue Errno::ENOENT, Errno::EACCES + tmpdir + end + ENV["TMPDIR"] = tmpdir_real if tmpdir_real != tmpdir + end + else + ENV["TMPDIR"] = Path.tmpdir.to_s + end + + require "rubygems/user_interaction" + Gem::DefaultUserInteraction.ui = Gem::SilentUI.new + end + + def setup_test_paths + ENV["BUNDLE_PATH"] = nil + ENV["PATH"] = [Path.system_gem_path("bin"), ENV["PATH"]].join(File::PATH_SEPARATOR) + ENV["PATH"] = [Path.exedir, ENV["PATH"]].join(File::PATH_SEPARATOR) if Path.ruby_core? + end + + def install_test_deps + dev_bundle("install", gemfile: test_gemfile, path: Path.base_system_gem_path.to_s) + dev_bundle("install", gemfile: rubocop_gemfile, path: Path.rubocop_gem_path.to_s) + dev_bundle("install", gemfile: standard_gemfile, path: Path.standard_gem_path.to_s) + + require_relative "helpers" + Helpers.install_dev_bundler + + install_vendored_compact_index + end + + # Vendor `rubygems/rubygems.org#lib/compact_index/` under `tmp/compact_index/` + # so the artifice can serve compact-index responses without a runtime gem + # dependency. Pinned to a reviewed commit; override with COMPACT_INDEX_REF + # to refresh against another ref (the existing vendor copy is discarded). + def install_vendored_compact_index + target_root = Path.tmp_root.join("compact_index") + require "fileutils" + FileUtils.mkdir_p(Path.tmp_root) + + files = %w[ + lib/compact_index.rb + lib/compact_index/dependency.rb + lib/compact_index/gem.rb + lib/compact_index/gem_version.rb + lib/compact_index/versions_file.rb + ] + + # Serialize installs so parallel test setups don't race on the same + # vendor tree, and only skip the download when every file is present so + # an interrupted run can't leave a partial copy behind. + File.open(Path.tmp_root.join("compact_index.lock"), File::CREAT | File::RDWR) do |lock| + lock.flock(File::LOCK_EX) + + FileUtils.rm_rf(target_root) if ENV["COMPACT_INDEX_REF"] + + next if files.all? {|path| File.exist?(target_root.join(path)) } + + require "open-uri" + ref = ENV["COMPACT_INDEX_REF"] || "7c68a7b39761c61a66f9299f85b889ec39afc02c" + files.each do |path| + url = "https://raw.githubusercontent.com/rubygems/rubygems.org/#{ref}/#{path}" + target = target_root.join(path) + FileUtils.mkdir_p(File.dirname(target)) + tmp = "#{target}.tmp" + File.write(tmp, URI.parse(url).open(&:read)) + File.rename(tmp, target) + end + end + end + + def check_source_control_changes(success_message:, error_message:) + require "open3" + + output, status = Open3.capture2e("git status --porcelain") + + if status.success? && output.empty? + puts + puts success_message + puts + else + system("git diff") + + puts + puts error_message + puts + + exit(1) + end + end + + def dev_bundle(*args, gemfile: dev_gemfile, path: nil) + old_gemfile = ENV["BUNDLE_GEMFILE"] + old_orig_gemfile = ENV["BUNDLER_ORIG_BUNDLE_GEMFILE"] + ENV["BUNDLE_GEMFILE"] = gemfile.to_s + ENV["BUNDLER_ORIG_BUNDLE_GEMFILE"] = nil + + if path + old_path = ENV["BUNDLE_PATH"] + ENV["BUNDLE_PATH"] = path + else + old_path__system = ENV["BUNDLE_PATH__SYSTEM"] + ENV["BUNDLE_PATH__SYSTEM"] = "true" + end + + require "shellwords" + # We don't use `Open3` here because it does not work on JRuby + Windows + output = `ruby #{Path.dev_binstub} #{args.shelljoin}` + raise output unless $?.success? + output + ensure + if path + ENV["BUNDLE_PATH"] = old_path + else + ENV["BUNDLE_PATH__SYSTEM"] = old_path__system + end + + ENV["BUNDLER_ORIG_BUNDLE_GEMFILE"] = old_orig_gemfile + ENV["BUNDLE_GEMFILE"] = old_gemfile + end + + private + + def gem_load_and_activate(gem_name, bin_container) + gem_activate(gem_name) + load Gem.bin_path(gem_name, bin_container) + rescue Gem::LoadError => e + abort "We couldn't activate #{gem_name} (#{e.requirement}). Run `gem install #{gem_name}:'#{e.requirement}'`" + end + + def gem_load_activate_and_possibly_install(gem_name, bin_container) + gem_activate_and_possibly_install(gem_name) + load Gem.bin_path(gem_name, bin_container) + end + + def gem_activate_and_possibly_install(gem_name) + gem_activate(gem_name) + rescue Gem::LoadError => e + Gem.install(gem_name, e.requirement) + retry + end + + def gem_activate(gem_name) + require_relative "activate" + require "bundler" + gem_requirement = Bundler::LockfileParser.new(File.read(dev_lockfile)).specs.find {|spec| spec.name == gem_name }.version + gem gem_name, gem_requirement + end + + def test_gemfile + Path.test_gemfile + end + + def rubocop_gemfile + Path.rubocop_gemfile + end + + def standard_gemfile + Path.standard_gemfile + end + + def dev_gemfile + Path.dev_gemfile + end + + def dev_lockfile + lockfile_for(dev_gemfile) + end + + def lockfile_for(gemfile) + Pathname.new("#{gemfile.expand_path}.lock") + end + end +end diff --git a/spec/bundler/support/rubygems_version_manager.rb b/spec/bundler/support/rubygems_version_manager.rb new file mode 100644 index 0000000000..c174c461f0 --- /dev/null +++ b/spec/bundler/support/rubygems_version_manager.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require_relative "options" +require_relative "env" +require_relative "subprocess" + +class RubygemsVersionManager + include Spec::Options + include Spec::Env + include Spec::Subprocess + + def initialize(source) + @source = source + end + + def switch + return if use_system? + + assert_system_features_not_loaded! + + switch_local_copy_if_needed + + reexec_if_needed + end + + def assert_system_features_not_loaded! + at_exit do + rubylibdir = RbConfig::CONFIG["rubylibdir"] + + rubygems_path = rubylibdir + "/rubygems" + rubygems_default_path = rubygems_path + "/defaults" + + bundler_path = rubylibdir + "/bundler" + + bad_loaded_features = $LOADED_FEATURES.select do |loaded_feature| + (loaded_feature.start_with?(rubygems_path) && !loaded_feature.start_with?(rubygems_default_path)) || + loaded_feature.start_with?(bundler_path) + end + + errors = if bad_loaded_features.any? + all_commands_output + "the following features were incorrectly loaded:\n#{bad_loaded_features.join("\n")}" + end + + raise errors if errors + end + end + + private + + def use_system? + @source.nil? + end + + def reexec_if_needed + return unless rubygems_unrequire_needed? + + require "rbconfig" + + cmd = [RbConfig.ruby, $0, *ARGV].compact + + ENV["RUBYOPT"] = opt_add("-I#{File.join(local_copy_path, "lib")}", opt_remove("--disable-gems", ENV["RUBYOPT"])) + + exec(ENV, *cmd) + end + + def switch_local_copy_if_needed + return unless local_copy_switch_needed? + + git("checkout #{target_tag}", local_copy_path) + + ENV["RGV"] = local_copy_path + end + + def rubygems_unrequire_needed? + require "rubygems" + !$LOADED_FEATURES.include?(File.join(local_copy_path, "lib/rubygems.rb")) + end + + def local_copy_switch_needed? + !source_is_path? && target_tag != local_copy_tag + end + + def target_tag + @target_tag ||= resolve_target_tag + end + + def local_copy_tag + git("rev-parse --abbrev-ref HEAD", local_copy_path) + end + + def local_copy_path + @local_copy_path ||= resolve_local_copy_path + end + + def resolve_local_copy_path + return expanded_source if source_is_path? + + rubygems_path = File.join(source_root, "tmp/rubygems") + + unless File.directory?(rubygems_path) + git("clone .. #{rubygems_path}", source_root) + end + + rubygems_path + end + + def source_is_path? + File.directory?(expanded_source) + end + + def expanded_source + @expanded_source ||= File.expand_path(@source, source_root) + end + + def source_root + @source_root ||= File.expand_path(ruby_core? ? "../../.." : "../..", __dir__) + end + + def resolve_target_tag + return "v#{@source}" if @source.match?(/^\d/) + + @source + end +end diff --git a/spec/bundler/support/setup.rb b/spec/bundler/support/setup.rb new file mode 100644 index 0000000000..4ac2e5b472 --- /dev/null +++ b/spec/bundler/support/setup.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require_relative "switch_rubygems" + +require_relative "rubygems_ext" +Spec::Rubygems.install_test_deps + +require_relative "path" +$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__)) if Spec::Path.ruby_core? diff --git a/spec/bundler/support/shards.rb b/spec/bundler/support/shards.rb new file mode 100644 index 0000000000..ce33896539 --- /dev/null +++ b/spec/bundler/support/shards.rb @@ -0,0 +1,200 @@ +# frozen_string_literal: true + +# This classifies test files into 4 shards by running `bin/rspec --profile 10000` +# to ensure balanced execution times. When adding new test files, it is recommended to +# re-aggregate and adjust the shards to keep them balanced. +# For now, please add new files to shard 'shard_d'. + +module Spec + module Shards + EXAMPLE_MAPPINGS = { + shard_a: [ + "spec/runtime/setup_spec.rb", + "spec/commands/install_spec.rb", + "spec/commands/add_spec.rb", + "spec/install/gems/compact_index_spec.rb", + "spec/commands/config_spec.rb", + "spec/commands/pristine_spec.rb", + "spec/install/gemfile/path_spec.rb", + "spec/update/git_spec.rb", + "spec/commands/open_spec.rb", + "spec/commands/remove_spec.rb", + "spec/commands/show_spec.rb", + "spec/plugins/source/example_spec.rb", + "spec/commands/console_spec.rb", + "spec/runtime/require_spec.rb", + "spec/runtime/env_helpers_spec.rb", + "spec/runtime/gem_tasks_spec.rb", + "spec/install/gemfile_spec.rb", + "spec/commands/fund_spec.rb", + "spec/commands/init_spec.rb", + "spec/bundler/ruby_dsl_spec.rb", + "spec/bundler/mirror_spec.rb", + "spec/bundler/source/git/git_proxy_spec.rb", + "spec/bundler/source_list_spec.rb", + "spec/bundler/plugin/installer_spec.rb", + "spec/bundler/errors_spec.rb", + "spec/bundler/friendly_errors_spec.rb", + "spec/resolver/platform_spec.rb", + "spec/bundler/fetcher/downloader_spec.rb", + "spec/update/force_spec.rb", + "spec/bundler/env_spec.rb", + "spec/install/gems/mirror_spec.rb", + "spec/install/failure_spec.rb", + "spec/bundler/yaml_serializer_spec.rb", + "spec/bundler/environment_preserver_spec.rb", + "spec/install/gemfile/install_if_spec.rb", + "spec/install/gems/gemfile_source_header_spec.rb", + "spec/bundler/fetcher/base_spec.rb", + "spec/bundler/rubygems_integration_spec.rb", + "spec/bundler/worker_spec.rb", + "spec/bundler/dependency_spec.rb", + "spec/bundler/ui_spec.rb", + "spec/bundler/plugin/source_list_spec.rb", + "spec/bundler/source/path_spec.rb", + ], + shard_b: [ + "spec/install/gemfile/git_spec.rb", + "spec/install/gems/standalone_spec.rb", + "spec/commands/lock_spec.rb", + "spec/cache/gems_spec.rb", + "spec/other/major_deprecation_spec.rb", + "spec/install/gems/dependency_api_spec.rb", + "spec/install/gemfile/gemspec_spec.rb", + "spec/plugins/install_spec.rb", + "spec/commands/binstubs_spec.rb", + "spec/install/gems/flex_spec.rb", + "spec/runtime/inline_spec.rb", + "spec/commands/post_bundle_message_spec.rb", + "spec/runtime/executable_spec.rb", + "spec/lock/git_spec.rb", + "spec/plugins/hook_spec.rb", + "spec/install/allow_offline_install_spec.rb", + "spec/install/gems/post_install_spec.rb", + "spec/install/gemfile/ruby_spec.rb", + "spec/install/security_policy_spec.rb", + "spec/install/yanked_spec.rb", + "spec/update/gemfile_spec.rb", + "spec/runtime/load_spec.rb", + "spec/plugins/command_spec.rb", + "spec/commands/version_spec.rb", + "spec/install/prereleases_spec.rb", + "spec/bundler/uri_credentials_filter_spec.rb", + "spec/bundler/plugin_spec.rb", + "spec/install/gems/mirror_probe_spec.rb", + "spec/plugins/list_spec.rb", + "spec/bundler/compact_index_client/parser_spec.rb", + "spec/bundler/gem_version_promoter_spec.rb", + "spec/other/cli_dispatch_spec.rb", + "spec/bundler/source/rubygems_spec.rb", + "spec/cache/platform_spec.rb", + "spec/update/gems/fund_spec.rb", + "spec/bundler/stub_specification_spec.rb", + "spec/bundler/retry_spec.rb", + "spec/bundler/installer/spec_installation_spec.rb", + "spec/bundler/spec_set_spec.rb", + "spec/quality_es_spec.rb", + "spec/bundler/index_spec.rb", + "spec/other/cli_man_pages_spec.rb", + ], + shard_c: [ + "spec/commands/newgem_spec.rb", + "spec/commands/exec_spec.rb", + "spec/commands/clean_spec.rb", + "spec/commands/platform_spec.rb", + "spec/cache/git_spec.rb", + "spec/install/gemfile/groups_spec.rb", + "spec/commands/cache_spec.rb", + "spec/commands/check_spec.rb", + "spec/commands/list_spec.rb", + "spec/install/path_spec.rb", + "spec/bundler/cli_spec.rb", + "spec/install/bundler_spec.rb", + "spec/install/git_spec.rb", + "spec/commands/doctor_spec.rb", + "spec/bundler/dsl_spec.rb", + "spec/install/gems/fund_spec.rb", + "spec/install/gems/env_spec.rb", + "spec/bundler/ruby_version_spec.rb", + "spec/bundler/definition_spec.rb", + "spec/install/gemfile/eval_gemfile_spec.rb", + "spec/plugins/source_spec.rb", + "spec/install/gems/dependency_api_fallback_spec.rb", + "spec/plugins/uninstall_spec.rb", + "spec/bundler/plugin/index_spec.rb", + "spec/bundler/bundler_spec.rb", + "spec/bundler/fetcher_spec.rb", + "spec/bundler/source/rubygems/remote_spec.rb", + "spec/bundler/lockfile_parser_spec.rb", + "spec/cache/cache_path_spec.rb", + "spec/bundler/source/git_spec.rb", + "spec/bundler/source_spec.rb", + "spec/commands/ssl_spec.rb", + "spec/bundler/fetcher/compact_index_spec.rb", + "spec/bundler/plugin/api_spec.rb", + "spec/bundler/endpoint_specification_spec.rb", + "spec/bundler/fetcher/index_spec.rb", + "spec/bundler/settings/validator_spec.rb", + "spec/bundler/build_metadata_spec.rb", + "spec/bundler/current_ruby_spec.rb", + "spec/bundler/installer/gem_installer_spec.rb", + "spec/bundler/installer/parallel_installer_spec.rb", + "spec/bundler/cli_common_spec.rb", + "spec/bundler/ci_detector_spec.rb", + ], + shard_d: [ + "spec/bundler/rubygems_ext_spec.rb", + "spec/bundler/resolver/cooldown_spec.rb", + "spec/install/cooldown_spec.rb", + "spec/commands/outdated_spec.rb", + "spec/commands/update_spec.rb", + "spec/lock/lockfile_spec.rb", + "spec/install/deploy_spec.rb", + "spec/install/gemfile/sources_spec.rb", + "spec/runtime/self_management_spec.rb", + "spec/install/gemfile/specific_platform_spec.rb", + "spec/commands/info_spec.rb", + "spec/install/gems/resolving_spec.rb", + "spec/install/gemfile/platform_spec.rb", + "spec/bundler/gem_helper_spec.rb", + "spec/install/global_cache_spec.rb", + "spec/runtime/platform_spec.rb", + "spec/update/gems/post_install_spec.rb", + "spec/install/gems/native_extensions_spec.rb", + "spec/install/force_spec.rb", + "spec/cache/path_spec.rb", + "spec/install/gemspecs_spec.rb", + "spec/commands/help_spec.rb", + "spec/bundler/shared_helpers_spec.rb", + "spec/bundler/settings_spec.rb", + "spec/resolver/basic_spec.rb", + "spec/install/gemfile/force_ruby_platform_spec.rb", + "spec/commands/licenses_spec.rb", + "spec/install/gemfile/lockfile_spec.rb", + "spec/bundler/fetcher/dependency_spec.rb", + "spec/quality_spec.rb", + "spec/bundler/remote_specification_spec.rb", + "spec/install/process_lock_spec.rb", + "spec/install/binstubs_spec.rb", + "spec/bundler/compact_index_client/updater_spec.rb", + "spec/bundler/ui/shell_spec.rb", + "spec/other/ext_spec.rb", + "spec/commands/issue_spec.rb", + "spec/update/path_spec.rb", + "spec/bundler/plugin/api/source_spec.rb", + "spec/install/gems/win32_spec.rb", + "spec/bundler/plugin/dsl_spec.rb", + "spec/runtime/requiring_spec.rb", + "spec/bundler/plugin/events_spec.rb", + "spec/bundler/resolver/candidate_spec.rb", + "spec/bundler/digest_spec.rb", + "spec/bundler/fetcher/gem_remote_fetcher_spec.rb", + "spec/bundler/uri_normalizer_spec.rb", + "spec/install/gems/no_build_extension_spec.rb", + "spec/install/gems/no_install_plugin_spec.rb", + "spec/bundler/override_spec.rb", + "spec/install/gemfile/override_spec.rb", + ], + }.freeze + end +end diff --git a/spec/bundler/support/subprocess.rb b/spec/bundler/support/subprocess.rb new file mode 100644 index 0000000000..91db80da48 --- /dev/null +++ b/spec/bundler/support/subprocess.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require_relative "command_execution" + +module Spec + module Subprocess + class TimeoutExceeded < StandardError; end + + def command_executions + @command_executions ||= [] + end + + def last_command + command_executions.last || raise("There is no last command") + end + + def out + last_command.stdout + end + + def err + last_command.stderr + end + + def stdboth + last_command.stdboth + end + + def exitstatus + last_command.exitstatus + end + + def git(cmd, path = Dir.pwd, options = {}) + sh("git #{cmd}", options.merge(dir: path)) + end + + def sh(cmd, options = {}) + dir = options[:dir] + env = options[:env] || {} + + command_execution = CommandExecution.new(cmd.to_s, timeout: options[:timeout] || 60) + + open3_opts = {} + open3_opts[:chdir] = dir if dir + + require "open3" + require "shellwords" + Open3.popen3(env, *cmd.shellsplit, **open3_opts) do |stdin, stdout, stderr, wait_thr| + yield stdin, stdout, wait_thr if block_given? + stdin.close + + stdout_handler = ->(data) { command_execution.original_stdout << data } + stderr_handler = ->(data) { command_execution.original_stderr << data } + + stdout_thread = read_stream(stdout, stdout_handler, timeout: command_execution.timeout) + stderr_thread = read_stream(stderr, stderr_handler, timeout: command_execution.timeout) + + stdout_thread.join + stderr_thread.join + + status = wait_thr.value + command_execution.exitstatus = if status.exited? + status.exitstatus + elsif status.signaled? + exit_status_for_signal(status.termsig) + end + rescue TimeoutExceeded + command_execution.failure_reason = :timeout + command_execution.exitstatus = exit_status_for_signal(Signal.list["INT"]) + end + + unless options[:raise_on_error] == false || command_execution.success? + command_execution.raise_error! + end + + command_executions << command_execution + + command_execution.stdout + end + + # Mostly copied from https://github.com/piotrmurach/tty-command/blob/49c37a895ccea107e8b78d20e4cb29de6a1a53c8/lib/tty/command/process_runner.rb#L165-L193 + def read_stream(stream, handler, timeout:) + Thread.new do + Thread.current.report_on_exception = false + cmd_start = Time.now + readers = [stream] + + while readers.any? + ready = IO.select(readers, nil, readers, timeout) + raise TimeoutExceeded if ready.nil? + + ready[0].each do |reader| + chunk = reader.readpartial(16 * 1024) + handler.call(chunk) + + # control total time spent reading + runtime = Time.now - cmd_start + time_left = timeout - runtime + raise TimeoutExceeded if time_left < 0.0 + rescue Errno::EAGAIN, Errno::EINTR + rescue EOFError, Errno::EPIPE, Errno::EIO + readers.delete(reader) + reader.close + end + end + end + end + + def all_commands_output + return "" if command_executions.empty? + + "\n\nCommands:\n#{command_executions.map(&:to_s_verbose).join("\n\n")}" + end + end +end diff --git a/spec/bundler/support/switch_rubygems.rb b/spec/bundler/support/switch_rubygems.rb new file mode 100644 index 0000000000..640b9f83b7 --- /dev/null +++ b/spec/bundler/support/switch_rubygems.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require_relative "rubygems_version_manager" +ENV["RGV"] ||= "." +RubygemsVersionManager.new(ENV["RGV"]).switch diff --git a/spec/bundler/support/the_bundle.rb b/spec/bundler/support/the_bundle.rb new file mode 100644 index 0000000000..452abd7d41 --- /dev/null +++ b/spec/bundler/support/the_bundle.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require_relative "path" + +module Spec + class TheBundle + include Spec::Path + + attr_accessor :bundle_dir + + def initialize + @bundle_dir = Pathname.new(bundled_app) + end + + def to_s + "the bundle" + end + alias_method :inspect, :to_s + + def locked? + lockfile.file? + end + + def lockfile + bundle_dir.join("Gemfile.lock") + end + + def locked_gems + raise ArgumentError, "Cannot read lockfile if it doesn't exist" unless locked? + Bundler::LockfileParser.new(lockfile.read) + end + + def locked_specs + locked_gems.specs.map(&:full_name) + end + + def locked_platforms + locked_gems.platforms.map(&:to_s) + end + end +end diff --git a/spec/bundler/support/vendored_net_http.rb b/spec/bundler/support/vendored_net_http.rb new file mode 100644 index 0000000000..8ff2ccd1fe --- /dev/null +++ b/spec/bundler/support/vendored_net_http.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# This defined? guard can be removed once RubyGems 3.4 support is dropped. +# +# Bundler specs load this code from `spec/support/vendored_net_http.rb` to avoid +# activating the Bundler gem too early. Without this guard, we get redefinition +# warnings once Bundler is actually activated and +# `lib/bundler/vendored_net_http.rb` is required. This is not an issue in +# RubyGems versions including `rubygems/vendored_net_http` since `require` takes +# care of avoiding the double load. +# +unless defined?(Gem::Net) + begin + require "rubygems/vendored_net_http" + rescue LoadError + begin + require "rubygems/net/http" + rescue LoadError + require "net/http" + Gem::Net = Net + end + end +end |
