diff options
Diffstat (limited to 'spec/bundler/support')
53 files changed, 1725 insertions, 815 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/api_request_limit_hax.rb b/spec/bundler/support/api_request_limit_hax.rb deleted file mode 100644 index 37ff0203b3..0000000000 --- a/spec/bundler/support/api_request_limit_hax.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -if ENV["BUNDLER_SPEC_API_REQUEST_LIMIT"] - require_relative "path" - require "bundler/source" - require "bundler/source/rubygems" - - module Bundler - class Source - class Rubygems < Source - remove_const :API_REQUEST_LIMIT - API_REQUEST_LIMIT = ENV["BUNDLER_SPEC_API_REQUEST_LIMIT"].to_i - end - end - end -end diff --git a/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb index a6545b9ee4..83b147d2ae 100644 --- a/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb +++ b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb @@ -4,10 +4,10 @@ require_relative "helpers/compact_index" class CompactIndexChecksumMismatch < CompactIndexAPI get "/versions" do - headers "ETag" => quote("123") + headers "Repr-Digest" => "sha-256=:ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=:" headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" content_type "text/plain" - body "" + body "content does not match the checksum" end end diff --git a/spec/bundler/support/artifice/compact_index_concurrent_download.rb b/spec/bundler/support/artifice/compact_index_concurrent_download.rb index 35548f278c..5d55b8a72b 100644 --- a/spec/bundler/support/artifice/compact_index_concurrent_download.rb +++ b/spec/bundler/support/artifice/compact_index_concurrent_download.rb @@ -7,11 +7,12 @@ class CompactIndexConcurrentDownload < CompactIndexAPI versions = File.join(Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions") - # Verify the original (empty) content hasn't been deleted, e.g. on a retry - File.binread(versions) == "" || raise("Original file should be present and empty") + # 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"] || raise("Missing Range header for expected 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 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 index 401e8a98d8..282e9c8961 100644 --- a/spec/bundler/support/artifice/compact_index_creds_diff_host.rb +++ b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb @@ -24,7 +24,7 @@ class CompactIndexCredsDiffHost < CompactIndexAPI end get "/gems/:id" do - redirect "http://diffhost.com/no/creds/#{params[:id]}" + redirect "http://diffhost.test/no/creds/#{params[:id]}" end get "/no/creds/:id" do 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_host_redirect.rb b/spec/bundler/support/artifice/compact_index_host_redirect.rb index 9a711186db..4f82bf3812 100644 --- a/spec/bundler/support/artifice/compact_index_host_redirect.rb +++ b/spec/bundler/support/artifice/compact_index_host_redirect.rb @@ -3,7 +3,7 @@ require_relative "helpers/compact_index" class CompactIndexHostRedirect < CompactIndexAPI - get "/fetch/actual/gem/:id", :host_name => "localgemserver.test" do + get "/fetch/actual/gem/:id", host_name: "localgemserver.test" do redirect "http://bundler.localgemserver.test#{request.path_info}" end 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_partial_update.rb b/spec/bundler/support/artifice/compact_index_partial_update.rb index 8c73011346..f111d91ef9 100644 --- a/spec/bundler/support/artifice/compact_index_partial_update.rb +++ b/spec/bundler/support/artifice/compact_index_partial_update.rb @@ -23,7 +23,7 @@ class CompactIndexPartialUpdate < CompactIndexAPI # 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.") + 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 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_etag_not_incremental.rb b/spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb index 20546ba4c3..99bae039f0 100644 --- a/spec/bundler/support/artifice/compact_index_partial_update_no_etag_not_incremental.rb +++ b/spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb @@ -2,8 +2,10 @@ require_relative "helpers/compact_index" -class CompactIndexPartialUpdateNoEtagNotIncremental < CompactIndexAPI - def partial_update_no_etag +# 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" @@ -11,12 +13,12 @@ class CompactIndexPartialUpdateNoEtagNotIncremental < CompactIndexAPI end get "/versions" do - partial_update_no_etag 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") + 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 @@ -25,7 +27,7 @@ class CompactIndexPartialUpdateNoEtagNotIncremental < CompactIndexAPI end get "/info/:name" do - partial_update_no_etag do + partial_update_no_digest do gem = gems.find {|g| g.name == params[:name] } lines = CompactIndex.info(gem ? gem.versions : []).split("\n") @@ -37,4 +39,4 @@ end require_relative "helpers/artifice" -Artifice.activate_with(CompactIndexPartialUpdateNoEtagNotIncremental) +Artifice.activate_with(CompactIndexPartialUpdateNoDigestNotIncremental) 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_strict_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb index fa25c4eca1..96259385e7 100644 --- a/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb +++ b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb @@ -10,7 +10,7 @@ class CompactIndexStrictBasicAuthentication < CompactIndexAPI # Only accepts password == "password" unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz" - halt 403, "Authentication failed" + halt 401, "Authentication failed" end end end diff --git a/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb index acc13a56ff..9bd2ca0a9d 100644 --- a/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb +++ b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb @@ -7,7 +7,8 @@ class CompactIndexWrongGemChecksum < CompactIndexAPI etag_response do name = params[:name] gem = gems.find {|g| g.name == name } - checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") { "ab" * 22 } + # 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) diff --git a/spec/bundler/support/artifice/endpoint_500.rb b/spec/bundler/support/artifice/endpoint_500.rb index d8ab6b65bc..9dd373bbf6 100644 --- a/spec/bundler/support/artifice/endpoint_500.rb +++ b/spec/bundler/support/artifice/endpoint_500.rb @@ -2,7 +2,7 @@ require_relative "../path" -$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gem_path.join("gems/{mustermann,rack,tilt,sinatra,ruby2_keywords}-*/lib")].map(&:to_s)) +$LOAD_PATH.unshift(*Spec::Path.sinatra_dependency_paths) require "sinatra/base" diff --git a/spec/bundler/support/artifice/endpoint_creds_diff_host.rb b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb index ce30de0a68..9cbb4de61a 100644 --- a/spec/bundler/support/artifice/endpoint_creds_diff_host.rb +++ b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb @@ -24,7 +24,7 @@ class EndpointCredsDiffHost < Endpoint end get "/gems/:id" do - redirect "http://diffhost.com/no/creds/#{params[:id]}" + redirect "http://diffhost.test/no/creds/#{params[:id]}" end get "/no/creds/:id" do diff --git a/spec/bundler/support/artifice/endpoint_host_redirect.rb b/spec/bundler/support/artifice/endpoint_host_redirect.rb index 0efb6cda02..6ce51bed93 100644 --- a/spec/bundler/support/artifice/endpoint_host_redirect.rb +++ b/spec/bundler/support/artifice/endpoint_host_redirect.rb @@ -3,7 +3,7 @@ require_relative "helpers/endpoint" class EndpointHostRedirect < Endpoint - get "/fetch/actual/gem/:id", :host_name => "localgemserver.test" do + get "/fetch/actual/gem/:id", host_name: "localgemserver.test" do redirect "http://bundler.localgemserver.test#{request.path_info}" end diff --git a/spec/bundler/support/artifice/endpoint_mirror_source.rb b/spec/bundler/support/artifice/endpoint_mirror_source.rb index 6ea1a77eca..fed7a746b9 100644 --- a/spec/bundler/support/artifice/endpoint_mirror_source.rb +++ b/spec/bundler/support/artifice/endpoint_mirror_source.rb @@ -4,7 +4,7 @@ require_relative "helpers/endpoint" class EndpointMirrorSource < Endpoint get "/gems/:id" do - if request.env["HTTP_X_GEMFILE_SOURCE"] == "https://server.example.org/" + 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 diff --git a/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb index 8ce1bdd4ad..dff360c5c5 100644 --- a/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb +++ b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb @@ -10,7 +10,7 @@ class EndpointStrictBasicAuthentication < Endpoint # Only accepts password == "password" unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz" - halt 403, "Authentication failed" + halt 401, "Authentication failed" end end end diff --git a/spec/bundler/support/artifice/fail.rb b/spec/bundler/support/artifice/fail.rb index 6286e43fbd..5ddbc4e590 100644 --- a/spec/bundler/support/artifice/fail.rb +++ b/spec/bundler/support/artifice/fail.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require "net/http" +require_relative "../vendored_net_http" -class Fail < Net::HTTP - # Net::HTTP uses a @newimpl instance variable to decide whether +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 - # Net::HTTP, we must set it + # Gem::Net::HTTP, we must set it @newimpl = true def request(req, body = nil, &block) @@ -17,13 +17,11 @@ class Fail < Net::HTTP end def exception(req) - name = ENV.fetch("BUNDLER_SPEC_EXCEPTION") { "Errno::ENETUNREACH" } - const = name.split("::").reduce(Object) {|mod, sym| mod.const_get(sym) } - const.new("host down: Bundler spec artifice fail! #{req["PATH_INFO"]}") + Errno::ENETUNREACH.new("host down: Bundler spec artifice fail! #{req["PATH_INFO"]}") end end require_relative "helpers/artifice" -# Replace Net::HTTP with our failing subclass +# 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 index b8c78614fb..788268295c 100644 --- a/spec/bundler/support/artifice/helpers/artifice.rb +++ b/spec/bundler/support/artifice/helpers/artifice.rb @@ -4,7 +4,7 @@ module Artifice # Activate Artifice with a particular Rack endpoint. # - # Calling this method will replace the Net::HTTP system + # Calling this method will replace the Gem::Net::HTTP system # with a replacement that routes all requests to the # Rack endpoint. # @@ -18,11 +18,11 @@ module Artifice # Deactivate the Artifice replacement. def self.deactivate - replace_net_http(::Net::HTTP) + replace_net_http(::Gem::Net::HTTP) end def self.replace_net_http(value) - ::Net.class_eval do + ::Gem::Net.class_eval do remove_const(:HTTP) const_set(:HTTP, value) end diff --git a/spec/bundler/support/artifice/helpers/compact_index.rb b/spec/bundler/support/artifice/helpers/compact_index.rb index 4df47a9659..e684aa8628 100644 --- a/spec/bundler/support/artifice/helpers/compact_index.rb +++ b/spec/bundler/support/artifice/helpers/compact_index.rb @@ -2,8 +2,9 @@ require_relative "endpoint" -$LOAD_PATH.unshift Dir[Spec::Path.base_system_gem_path.join("gems/compact_index*/lib")].first.to_s +$LOAD_PATH.unshift Spec::Path.tmp_root.join("compact_index/lib").to_s require "compact_index" +require "digest" class CompactIndexAPI < Endpoint helpers do @@ -17,9 +18,10 @@ class CompactIndexAPI < Endpoint def etag_response response_body = yield - checksum = Digest(:MD5).hexdigest(response_body) - return if not_modified?(checksum) - headers "ETag" => quote(checksum) + 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) @@ -29,17 +31,16 @@ class CompactIndexAPI < Endpoint raise end - def not_modified?(checksum) + def not_modified?(etag) etags = parse_etags(request.env["HTTP_IF_NONE_MATCH"]) - return unless etags.include?(checksum) - headers "ETag" => quote(checksum) + return unless etags.include?(etag) status 304 body "" end def requested_range_for(response_body) - ranges = Rack::Utils.byte_ranges(env, response_body.bytesize) + ranges = Rack::Utils.get_byte_ranges(env["HTTP_RANGE"], response_body.bytesize) if ranges status 206 @@ -66,31 +67,40 @@ class CompactIndexAPI < Endpoint @gems ||= {} @gems[gem_repo] ||= begin specs = Bundler::Deprecate.skip_during do - %w[specs.4.8 prerelease_specs.4.8].map do |filename| - Marshal.load(File.open(gem_repo.join(filename)).read).map do |name, version, platform| + %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.flatten + end end specs.group_by(&:name).map do |name, versions| gem_versions = versions.map do |spec| - deps = spec.dependencies.select {|d| d.type == :runtime }.map do |d| + deps = spec.runtime_dependencies.map do |d| reqs = d.requirement.requirements.map {|r| r.join(" ") }.join(", ") CompactIndex::Dependency.new(d.name, reqs) end - checksum = begin - Digest(:SHA256).file("#{gem_repo}/gems/#{spec.original_name}.gem").base64digest - rescue StandardError - nil - end - CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, checksum, nil, - deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s) + 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 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/endpoint.rb b/spec/bundler/support/artifice/helpers/endpoint.rb index fc0381dc38..9590611dfe 100644 --- a/spec/bundler/support/artifice/helpers/endpoint.rb +++ b/spec/bundler/support/artifice/helpers/endpoint.rb @@ -2,7 +2,7 @@ require_relative "../../path" -$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gem_path.join("gems/{mustermann,rack,tilt,sinatra,ruby2_keywords}-*/lib")].map(&:to_s)) +$LOAD_PATH.unshift(*Spec::Path.sinatra_dependency_paths) require "sinatra/base" @@ -27,6 +27,7 @@ class Endpoint < Sinatra::Base 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 @@ -62,21 +63,21 @@ class Endpoint < Sinatra::Base return [] if gem_names.nil? || gem_names.empty? all_specs = %w[specs.4.8 prerelease_specs.4.8].map do |filename| - Marshal.load(File.open(gem_repo.join(filename)).read) + Marshal.load(File.binread(gem_repo.join(filename))) end.inject(:+) - all_specs.map do |name, version, platform| + 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.dependencies.select {|dep| dep.type == :runtime }.map do |dep| + 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.compact + end end def load_spec(name, version, platform, gem_repo) diff --git a/spec/bundler/support/artifice/helpers/rack_request.rb b/spec/bundler/support/artifice/helpers/rack_request.rb index c4a07812a6..05ff034463 100644 --- a/spec/bundler/support/artifice/helpers/rack_request.rb +++ b/spec/bundler/support/artifice/helpers/rack_request.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "rack/test" -require "net/http" +require_relative "../../vendored_net_http" module Artifice module Net @@ -16,25 +16,25 @@ module Artifice end end - class HTTP < ::Net::HTTP + class HTTP < ::Gem::Net::HTTP class << self attr_accessor :endpoint end - # Net::HTTP uses a @newimpl instance variable to decide whether + # Gem::Net::HTTP uses a @newimpl instance variable to decide whether # to use a legacy implementation. Since we are subclassing - # Net::HTTP, we must set it + # 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 Net::HTTP request method with a method + # 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 Net::HTTPRequest + # @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 @@ -42,7 +42,7 @@ module Artifice # @return [Net::HTTPResponse] # # @yield [Net::HTTPResponse] If a block is provided, - # this method will yield the Net::HTTPResponse to + # 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) @@ -56,17 +56,17 @@ module Artifice 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 }) + { method: req.method, input: body || req.body || body_stream_contents }) make_net_http_response(response, &block) end private - # This method takes a Rack response and creates a Net::HTTPResponse + # 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 Net::HTTPResponse.read_new + # response and call Gem::Net::HTTPResponse.read_new # # @param [Array(#to_i, Hash, #each)] response a Rack response # @return [Net::HTTPResponse] @@ -86,8 +86,8 @@ module Artifice response_string << "" << body - response_io = ::Net::BufferedIO.new(StringIO.new(response_string.join("\n"))) - res = ::Net::HTTPResponse.read_new(response_io) + 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? diff --git a/spec/bundler/support/artifice/vcr.rb b/spec/bundler/support/artifice/vcr.rb index 6a346f1ff9..0bf5ade8f6 100644 --- a/spec/bundler/support/artifice/vcr.rb +++ b/spec/bundler/support/artifice/vcr.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -require "net/http" +require_relative "../vendored_net_http" require_relative "../path" -CASSETTE_PATH = "#{Spec::Path.spec_dir}/support/artifice/vcr_cassettes" -USED_CASSETTES_PATH = "#{Spec::Path.spec_dir}/support/artifice/used_cassettes.txt" +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 < Net::HTTP +class BundlerVCRHTTP < Gem::Net::HTTP class RequestHandler attr_reader :http, :request, :body, :response_block def initialize(http, request, body = nil, &response_block) @@ -24,7 +24,7 @@ class BundlerVCRHTTP < Net::HTTP end File.open(USED_CASSETTES_PATH, "a+") do |f| - f.puts request_pair_paths.map {|path| Pathname.new(path).relative_path_from(Spec::Path.source_root).to_s }.join("\n") + 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? @@ -41,8 +41,8 @@ class BundlerVCRHTTP < Net::HTTP def recorded_response File.open(request_pair_paths.last, "rb:ASCII-8BIT") do |response_file| - response_io = ::Net::BufferedIO.new(response_file) - ::Net::HTTPResponse.read_new(response_io).tap do |response| + 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 @@ -148,5 +148,5 @@ end require_relative "helpers/artifice" -# Replace Net::HTTP with our VCR subclass +# 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 index 4d90e0a426..3056540beb 100644 --- a/spec/bundler/support/artifice/windows.rb +++ b/spec/bundler/support/artifice/windows.rb @@ -2,7 +2,7 @@ require_relative "../path" -$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gem_path.join("gems/{mustermann,rack,tilt,sinatra,ruby2_keywords}-*/lib")].map(&:to_s)) +$LOAD_PATH.unshift(*Spec::Path.sinatra_dependency_paths) require "sinatra/base" diff --git a/spec/bundler/support/build_metadata.rb b/spec/bundler/support/build_metadata.rb index 98d8ac23c8..2eade4137b 100644 --- a/spec/bundler/support/build_metadata.rb +++ b/spec/bundler/support/build_metadata.rb @@ -8,22 +8,21 @@ module Spec include Spec::Path include Spec::Helpers - def write_build_metadata(dir: source_root) + def write_build_metadata(dir: source_root, version: Bundler::VERSION) build_metadata = { - :git_commit_sha => git_commit_sha, - :built_at => loaded_gemspec.date.utc.strftime("%Y-%m-%d"), - :release => true, + git_commit_sha: git_commit_sha, + built_at: release_date_for(version, dir: dir), } - replace_build_metadata(build_metadata, dir: dir) # rubocop:disable Style/HashSyntax + replace_build_metadata(build_metadata, dir: dir) end def reset_build_metadata(dir: source_root) build_metadata = { - :release => false, + built_at: nil, } - replace_build_metadata(build_metadata, dir: dir) # rubocop:disable Style/HashSyntax + replace_build_metadata(build_metadata, dir: dir) end private @@ -41,7 +40,12 @@ module Spec end def git_commit_sha - ruby_core_tarball? ? "unknown" : sys_exec("git rev-parse --short HEAD", :dir => source_root).strip + 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 diff --git a/spec/bundler/support/builders.rb b/spec/bundler/support/builders.rb index 3aa5454b6a..43ab7e053d 100644 --- a/spec/bundler/support/builders.rb +++ b/spec/bundler/support/builders.rb @@ -2,9 +2,18 @@ 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 @@ -17,21 +26,7 @@ module Spec Gem::Platform.new(platform) end - # Returns a number smaller than the size of the index. Useful for specs that - # need the API request limit to be reached for some reason. - def low_api_request_limit_for(gem_repo) - all_gems = Dir[gem_repo.join("gems/*.gem")] - - all_gem_names = all_gems.map do |file| - File.basename(file, ".gem").match(/\A(?<gem_name>[^-]+)-.*\z/)[:gem_name] - end.uniq - - (all_gem_names - ["bundler"]).size - end - def build_repo1 - rake_path = Dir["#{Path.base_system_gems}/**/rake*.gem"].first - build_repo gem_repo1 do FileUtils.cp rake_path, "#{gem_repo1}/gems/" @@ -40,28 +35,28 @@ module Spec build_gem "puma" build_gem "minitest" - build_gem "rack", %w[0.9.1 1.0.0] do |s| - s.executables = "rackup" - s.post_install_message = "Rack's post install message" + 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 "rack" + s.add_dependency "myrack" s.post_install_message = "Thin's post install message" end - build_gem "rack-obama" do |s| - s.add_dependency "rack" - s.post_install_message = "Rack-obama's post install message" + build_gem "myrack-obama" do |s| + s.add_dependency "myrack" + s.post_install_message = "Myrack-obama's post install message" end - build_gem "rack_middleware", "1.0" do |s| - s.add_dependency "rack", "0.9.1" + 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", "13.0.1" + 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" @@ -85,84 +80,66 @@ module Spec s.add_dependency "activesupport", ">= 2.0.0" end - build_gem "rspec", "1.2.7", :no_default => true do |s| + build_gem "rspec", "1.2.7", no_default: true do |s| s.write "lib/spec.rb", "SPEC = '1.2.7'" end - build_gem "rack-test", :no_default => true do |s| - s.write "lib/rack/test.rb", "RACK_TEST = '1.0'" - end - - build_gem "platform_specific" do |s| - s.platform = Gem::Platform.local - s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 #{Gem::Platform.local}'" + 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" - s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 JAVA'" end build_gem "platform_specific" do |s| s.platform = "ruby" - s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 RUBY'" end build_gem "platform_specific" do |s| s.platform = "x86-mswin32" - s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0 x86-mswin32'" end build_gem "platform_specific" do |s| s.platform = "x64-mswin64" - s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0 x64-mswin64'" end build_gem "platform_specific" do |s| s.platform = "x86-mingw32" - s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0 x86-mingw32'" end build_gem "platform_specific" do |s| - s.platform = "x64-mingw32" - s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0 x64-mingw32'" + s.platform = "x64-mingw-ucrt" end build_gem "platform_specific" do |s| - s.platform = "x64-mingw-ucrt" - s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0 x64-mingw-ucrt'" + s.platform = "aarch64-mingw-ucrt" end build_gem "platform_specific" do |s| s.platform = "x86-darwin-100" - s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 x86-darwin-100'" end build_gem "only_java", "1.0" do |s| s.platform = "java" - s.write "lib/only_java.rb", "ONLY_JAVA = '1.0.0 JAVA'" end build_gem "only_java", "1.1" do |s| s.platform = "java" - s.write "lib/only_java.rb", "ONLY_JAVA = '1.1.0 JAVA'" end build_gem "nokogiri", "1.4.2" build_gem "nokogiri", "1.4.2" do |s| s.platform = "java" - s.write "lib/nokogiri.rb", "NOKOGIRI = '1.4.2 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" - s.write "lib/laduradura.rb", "LADURADURA = '5.15.2 JAVA'" end build_gem "laduradura", "5.15.3" do |s| s.platform = "java" - s.write "lib/laduradura.rb", "LADURADURA = '5.15.2 JAVA'" end build_gem "weakling", "0.0.3" @@ -176,7 +153,7 @@ module Spec build_gem "bundler", "0.9" do |s| s.executables = "bundle" - s.write "bin/bundle", "puts 'FAIL'" + s.write "bin/bundle", "#!/usr/bin/env ruby\nputs 'FAIL'" end # The bundler 0.8 gem has a rubygems plugin that always loads :( @@ -203,32 +180,45 @@ module Spec end end - def build_repo2(&blk) - FileUtils.rm_rf gem_repo2 - FileUtils.cp_r gem_repo1, gem_repo2 - update_repo2(&blk) if block_given? + 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.) - def build_repo4(&blk) - FileUtils.rm_rf gem_repo4 - build_repo(gem_repo4, &blk) + # + # 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 - def update_repo4(&blk) - update_repo(gem_repo4, &blk) + # 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 - update_repo gem_repo2 do - yield if block_given? - 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 "rack" + build_gem "myrack" build_gem "signed_gem" do |s| cert = "signing-cert.pem" @@ -241,39 +231,60 @@ module Spec end end - def build_repo(path, &blk) - return if File.directory?(path) - - FileUtils.mkdir_p("#{path}/gems") - - update_repo(path, &blk) + # 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 check_test_gems! - rake_path = Dir["#{Path.base_system_gems}/**/rake*.gem"].first + def build_repo(path, **kwargs, &blk) + return if File.directory?(path) - if rake_path.nil? - FileUtils.rm_rf(Path.base_system_gems) - Spec::Rubygems.install_test_deps - rake_path = Dir["#{Path.base_system_gems}/**/rake*.gem"].first - end + FileUtils.mkdir_p("#{path}/gems") - if rake_path.nil? - abort "Your test gems are missing! Run `rm -rf #{tmp}` and try again." - end + update_repo(path,**kwargs, &blk) end - def update_repo(path) - if path == gem_repo1 && caller.first.split(" ").last == "`build_repo`" + 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 - with_gem_path_as Path.base_system_gem_path do - gem_command :generate_index, :dir => path - end + options = { build_compact: build_compact_index } + Gem::Indexer.new(path, options).generate_index ensure @_build_path = nil @_build_repo = nil @@ -302,6 +313,10 @@ module Spec 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 @@ -407,6 +422,58 @@ module Spec 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 @@ -420,6 +487,7 @@ module Spec s.email = "foo@bar.baz" s.homepage = "http://example.com" s.license = "MIT" + s.required_ruby_version = ">= 3.0" end @files = {} end @@ -436,24 +504,17 @@ module Spec @spec.executables = Array(val) @spec.executables.each do |file| executable = "#{@spec.bindir}/#{file}" - shebang = if Bundler.current_ruby.jruby? - "#!/usr/bin/env jruby\n" - else - "#!/usr/bin/env ruby\n" - end + 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 - require_paths << "ext" extensions << "ext/extconf.rb" write "ext/extconf.rb", <<-RUBY require "mkmf" - $extout = "$(topdir)/" + RbConfig::CONFIG["EXTOUT"] - extension_name = "#{name}_c" if extra_lib_dir = with_config("ext-lib") # add extra libpath if --with-ext-lib is @@ -467,7 +528,7 @@ module Spec write "ext/#{name}.c", <<-C #include "ruby.h" - void Init_#{name}_c() { + void Init_#{name}_c(void) { rb_define_module("#{Builders.constantize(name)}_IN_C"); } C @@ -478,7 +539,6 @@ module Spec if options[:rubygems_version] @spec.rubygems_version = options[:rubygems_version] - def @spec.mark_version; end def @spec.validate(*); end end @@ -497,18 +557,16 @@ module Spec when false # do nothing when :yaml - @spec.files << "#{name}.gemspec" @files["#{name}.gemspec"] = @spec.to_yaml else - @spec.files << "#{name}.gemspec" @files["#{name}.gemspec"] = @spec.to_ruby end @files.each do |file, source| - file = Pathname.new(path).join(file) - FileUtils.mkdir_p(file.dirname) - File.open(file, "w") {|f| f.puts source } - File.chmod("+x", file) if @spec.executables.map {|exe| "#{@spec.bindir}/#{exe}" }.include?(file) + 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 @@ -535,7 +593,7 @@ module Spec default_branch = options[:default_branch] || "main" path = options[:path] || _default_path source = options[:source] || "git@#{path}" - super(options.merge(:path => path, :source => source)) + super(options.merge(path: path, source: source)) @context.git("config --global init.defaultBranch #{default_branch}", path) @context.git("init", path) @context.git("add *", path) @@ -549,7 +607,7 @@ module Spec class GitBareBuilder < LibBuilder def _build(options) path = options[:path] || _default_path - super(options.merge(:path => path)) + super(options.merge(path: path)) @context.git("init --bare", path) end end @@ -574,7 +632,7 @@ module Spec _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)) + super(options.merge(path: libpath, gemspec: update_gemspec, source: source)) @context.git("commit -am BUMP", libpath) end end @@ -597,25 +655,25 @@ module Spec 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])) + 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 opts[:gemspec] == :yaml || opts[:gemspec] == false + if [:yaml, false].include?(opts[:gemspec]) Dir.chdir(lib_path) do Bundler.rubygems.build(@spec, opts[:skip_validation]) end elsif opts[:skip_validation] - @context.gem_command "build --force #{@spec.name}", :dir => lib_path + Dir.chdir(lib_path) { Gem::Package.build(@spec, true) } else - @context.gem_command "build #{@spec.name}", :dir => lib_path + 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] + @context.system_gems gem_path, default: opts[:default] elsif opts[:to_bundle] - @context.system_gems gem_path, :path => @context.default_bundle_path + @context.system_gems gem_path, path: @context.default_bundle_path else FileUtils.mv(gem_path, destination) end @@ -635,56 +693,56 @@ module Spec end end - TEST_CERT = <<-CERT.gsub(/^\s*/, "") + TEST_CERT = <<~CERT -----BEGIN CERTIFICATE----- - MIIDMjCCAhqgAwIBAgIBATANBgkqhkiG9w0BAQUFADAnMQwwCgYDVQQDDAN5b3Ux + MIIDNTCCAh2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAnMQwwCgYDVQQDDAN5b3Ux FzAVBgoJkiaJk/IsZAEZFgdleGFtcGxlMB4XDTE1MDIwODAwMTIyM1oXDTQyMDYy NTAwMTIyM1owJzEMMAoGA1UEAwwDeW91MRcwFQYKCZImiZPyLGQBGRYHZXhhbXBs - ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANlvFdpN43c4DMS9Jo06 - m0a7k3bQ3HWQ1yrYhZMi77F1F73NpBknYHIzDktQpGn6hs/4QFJT4m4zNEBF47UL - jHU5nTK5rjkS3niGYUjvh3ZEzVeo9zHUlD/UwflDo4ALl3TSo2KY/KdPS/UTdLXL - ajkQvaVJtEDgBPE3DPhlj5whp+Ik3mDHej7qpV6F502leAwYaFyOtlEG/ZGNG+nZ - L0clH0j77HpP42AylHDi+vakEM3xcjo9BeWQ6Vkboic93c9RTt6CWBWxMQP7Nol1 - MOebz9XOSQclxpxWteXNfPRtMdAhmRl76SMI8ywzThNPpa4EH/yz34ftebVOgKyM - nd0CAwEAAaNpMGcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFA7D - n9qo0np23qi3aOYuAAPn/5IdMBYGA1UdEQQPMA2BC3lvdUBleGFtcGxlMBYGA1Ud - EgQPMA2BC3lvdUBleGFtcGxlMA0GCSqGSIb3DQEBBQUAA4IBAQA7Gyk62sWOUX/N - vk4tJrgKESph6Ns8+E36A7n3jt8zCep8ldzMvwTWquf9iqhsC68FilEoaDnUlWw7 - d6oNuaFkv7zfrWGLlvqQJC+cu2X5EpcCksg5oRp8VNbwJysJ6JgwosxzROII8eXc - R+j1j6mDvQYqig2QOnzf480pjaqbP+tspfDFZbhKPrgM3Blrb3ZYuFpv4zkqI7aB - 6fuk2DUhNO1CuwrJA84TqC+jGo73bDKaT5hrIDiaJRrN5+zcWja2uEWrj5jSbep4 - oXdEdyH73hOHMBP40uds3PqnUsxEJhzjB2sCCe1geV24kw9J4m7EQXPVkUKDgKrt - LlpDmOoo + 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.gsub(/^\s*/, "") + TEST_PKEY = <<~PKEY -----BEGIN RSA PRIVATE KEY----- - MIIEowIBAAKCAQEA2W8V2k3jdzgMxL0mjTqbRruTdtDcdZDXKtiFkyLvsXUXvc2k - GSdgcjMOS1CkafqGz/hAUlPibjM0QEXjtQuMdTmdMrmuORLeeIZhSO+HdkTNV6j3 - MdSUP9TB+UOjgAuXdNKjYpj8p09L9RN0tctqORC9pUm0QOAE8TcM+GWPnCGn4iTe - YMd6PuqlXoXnTaV4DBhoXI62UQb9kY0b6dkvRyUfSPvsek/jYDKUcOL69qQQzfFy - Oj0F5ZDpWRuiJz3dz1FO3oJYFbExA/s2iXUw55vP1c5JByXGnFa15c189G0x0CGZ - GXvpIwjzLDNOE0+lrgQf/LPfh+15tU6ArIyd3QIDAQABAoIBACbDqz20TS1gDMa2 - gj0DidNedbflHKjJHdNBru7Ad8NHgOgR1YO2hXdWquG6itVqGMbTF4SV9/R1pIcg - 7qvEV1I+50u31tvOBWOvcYCzU48+TO2n7gowQA3xPHPYHzog1uu48fAOHl0lwgD7 - av9OOK3b0jO5pC08wyTOD73pPWU0NrkTh2+N364leIi1pNuI1z4V+nEuIIm7XpVd - 5V4sXidMTiEMJwE6baEDfTjHKaoRndXrrPo3ryIXmcX7Ag1SwAQwF5fBCRToCgIx - dszEZB1bJD5gA6r+eGnJLB/F60nK607az5o3EdguoB2LKa6q6krpaRCmZU5svvoF - J7xgBPECgYEA8RIzHAQ3zbaibKdnllBLIgsqGdSzebTLKheFuigRotEV3Or/z5Lg - k/nVnThWVkTOSRqXTNpJAME6a4KTdcVSxYP+SdZVO1esazHrGb7xPVb7MWSE1cqp - WEk3Yy8OUOPoPQMc4dyGzd30Mi8IBB6gnFIYOTrpUo0XtkBv8rGGhfsCgYEA5uYn - 6QgL4NqNT84IXylmMb5ia3iBt6lhxI/A28CDtQvfScl4eYK0IjBwdfG6E1vJgyzg - nJzv3xEVo9bz+Kq7CcThWpK5JQaPnsV0Q74Wjk0ShHet15txOdJuKImnh5F6lylC - GTLR9gnptytfMH/uuw4ws0Q2kcg4l5NHKOWOnAcCgYEAvAwIVkhsB0n59Wu4gCZu - FUZENxYWUk/XUyQ6KnZrG2ih90xQ8+iMyqFOIm/52R2fFKNrdoWoALC6E3ct8+ZS - pMRLrelFXx8K3it4SwMJR2H8XBEfFW4bH0UtsW7Zafv+AunUs9LETP5gKG1LgXsq - qgXX43yy2LQ61O365YPZfdUCgYBVbTvA3MhARbvYldrFEnUL3GtfZbNgdxuD9Mee - xig0eJMBIrgfBLuOlqtVB70XYnM4xAbKCso4loKSHnofO1N99siFkRlM2JOUY2tz - kMWZmmxKdFjuF0WZ5f/5oYxI/QsFGC+rUQEbbWl56mMKd5qkvEhKWudxoklF0yiV - ufC8SwKBgDWb8iWqWN5a/kfvKoxFcDM74UHk/SeKMGAL+ujKLf58F+CbweM5pX9C - EUsxeoUEraVWTiyFVNqD81rCdceus9TdBj0ZIK1vUttaRZyrMAwF0uQSfjtxsOpd - l69BkyvzjgDPkmOHVGiSZDLi3YDvypbUpo6LOy4v5rVg5U2F/A0v + 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 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 index 5f808531ff..aa7b121706 100644 --- a/spec/bundler/support/bundle.rb +++ b/spec/bundler/support/bundle.rb @@ -1,10 +1,7 @@ # 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) -bundler_gemspec.activate if bundler_gemspec.respond_to?(:activate) -load File.expand_path("bundle", Spec::Path.bindir) + +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 index 68e5c56c75..e2915b996d 100644 --- a/spec/bundler/support/command_execution.rb +++ b/spec/bundler/support/command_execution.rb @@ -1,7 +1,36 @@ # frozen_string_literal: true module Spec - CommandExecution = Struct.new(:command, :working_directory, :exitstatus, :stdout, :stderr) do + 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 @@ -11,6 +40,14 @@ module Spec @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, @@ -29,5 +66,13 @@ module Spec 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 index 78545d2e64..2be25b4a78 100644 --- a/spec/bundler/support/filters.rb +++ b/spec/bundler/support/filters.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true class RequirementChecker < Proc - def self.against(present) - provided = Gem::Version.new(present) - + def self.against(provided) new do |required| - !Gem::Requirement.new(required).satisfied_by?(provided) + requirement = Gem::Requirement.new(required) + + !requirement.satisfied_by?(provided) end.tap do |checker| checker.provided = provided end @@ -14,25 +14,29 @@ class RequirementChecker < Proc attr_accessor :provided def inspect - "\"!= #{provided}\"" + "\"#{provided}\"" end end -RSpec.configure do |config| - config.filter_run_excluding :realworld => true - - git_version = Bundler::Source::Git::GitProxy.new(nil, nil).version +git_version = Gem::Version.new(`git --version`[/(\d+\.\d+\.\d+)/, 1]) - config.filter_run_excluding :git => RequirementChecker.against(git_version) - config.filter_run_excluding :bundler => RequirementChecker.against(Bundler::VERSION.split(".")[0]) - config.filter_run_excluding :rubygems => RequirementChecker.against(Gem::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? +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 index c7fe3637cc..46718f5fa4 100644 --- a/spec/bundler/support/hax.rb +++ b/spec/bundler/support/hax.rb @@ -19,35 +19,56 @@ module Gem @default_specifications_dir = nil end - if ENV["BUNDLER_SPEC_WINDOWS"] - @@win_platform = true # rubocop:disable Style/ClassVars - 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 - if ENV["BUNDLER_SPEC_PLATFORM"] - previous_platforms = @platforms - previous_local = Platform.local + RbConfig::CONFIG["arch"] = spec_platform class Platform - @local = new(ENV["BUNDLER_SPEC_PLATFORM"]) + @local = nil end - @platforms = previous_platforms.map {|platform| platform == previous_local ? Platform.local : platform } + @platforms = [] end if ENV["BUNDLER_SPEC_GEM_SOURCES"] self.sources = [ENV["BUNDLER_SPEC_GEM_SOURCES"]] end - if ENV["BUNDLER_IGNORE_DEFAULT_GEM"] - module RemoveDefaultBundlerStub - def default_stubs(pattern = "*") - super.delete_if {|stub| stub.name == "bundler" } + 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 - class Specification - class << self - prepend RemoveDefaultBundlerStub + 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 index 703c98eca3..b0d4b5008b 100644 --- a/spec/bundler/support/helpers.rb +++ b/spec/bundler/support/helpers.rb @@ -1,66 +1,52 @@ # frozen_string_literal: true -require_relative "command_execution" 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_rf(dir) + FileUtils.rm_r(dir) end FileUtils.mkdir_p(home) FileUtils.mkdir_p(tmpdir) - reset_paths! - end - - def reset_paths! Bundler.reset! + Bundler::Source::Git::GitProxy.reset Gem.clear_paths end - def the_bundle(*args) - TheBundle.new(*args) - end - - def command_executions - @command_executions ||= [] + def the_bundle + TheBundle.new 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 - - MAJOR_DEPRECATION = /^\[DEPRECATED\]\s*/.freeze + MAJOR_DEPRECATION = /^\[DEPRECATED\]\s*/ def err_without_deprecations err.gsub(/#{MAJOR_DEPRECATION}.+[\n]?/, "") end def deprecations - err.split("\n").select {|l| l =~ MAJOR_DEPRECATION }.join("\n").split(MAJOR_DEPRECATION) - end - - def exitstatus - last_command.exitstatus + 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 '#{entrypoint}' ; Bundler.ui.silence { Bundler.setup(#{groups}) }" + setup = "require 'bundler' ; Bundler.ui.silence { Bundler.setup(#{groups}) }" ruby([setup, cmd].join(" ; "), opts) end @@ -69,7 +55,7 @@ module Spec begin #{ruby} rescue LoadError => e - warn "ZOMG LOAD ERROR" if e.message.include?("-- #{name}") + warn e.message if e.message.include?("-- #{name}") end RUBY opts = args.last.is_a?(Hash) ? args.pop : {} @@ -77,6 +63,10 @@ module Spec 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") @@ -84,29 +74,24 @@ module Spec env = options.delete(:env) || {} requires = options.delete(:requires) || [] - realworld = RSpec.current_example.metadata[:realworld] - artifice = options.delete(:artifice) do - if realworld - "vcr" - else - "fail" - end - end - if artifice - requires << "#{Path.spec_dir}/support/artifice/#{artifice}.rb" - end + dir = options.delete(:dir) || bundled_app + custom_load_path = options.delete(:load_path) load_path = [] - load_path << spec_dir + 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) - dir = options.delete(:dir) || bundled_app raise_on_error = options.delete(:raise_on_error) args = options.map do |k, v| case v - when nil - next when true " --#{k}" when false @@ -116,20 +101,31 @@ module Spec end end.join - ruby_cmd = build_ruby_cmd({ :load_path => load_path, :requires => requires }) - cmd = "#{ruby_cmd} #{bundle_bin} #{cmd}#{args}" - sys_exec(cmd, { :env => env, :dir => dir, :raise_on_error => raise_on_error }, &block) + 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.join("bin/bundler") + options[:bundle_bin] = system_gem_path("bin/bundler") bundle(cmd, options) end def ruby(ruby, options = {}) - ruby_cmd = build_ruby_cmd + env = build_env({ artifice: nil }.merge(options)) escaped_ruby = ruby.shellescape - sys_exec(%(#{ruby_cmd} -w -e #{escaped_ruby}), options) + 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 = {}) @@ -137,20 +133,49 @@ module Spec begin #{ruby} rescue LoadError => e - warn "ZOMG LOAD ERROR" if e.message.include?("-- #{name}") + warn e.message if e.message.include?("-- #{name}") end R end - def build_ruby_cmd(options = {}) - libs = options.delete(:load_path) - lib_option = libs ? "-I#{libs.join(File::PATH_SEPARATOR)}" : [] + 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 << "#{Path.spec_dir}/support/hax.rb" - require_option = requires.map {|r| "-r#{r}" } + 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"]) } - [Gem.ruby, *lib_option, *require_option].compact.join(" ") + env end def gembin(cmd, options = {}) @@ -158,85 +183,55 @@ module Spec sys_exec(cmd.to_s, options) end - def gem_command(command, options = {}) + def sys_exec(cmd, options = {}, &block) env = options[:env] || {} - env["RUBYOPT"] = opt_add(opt_add("-r#{spec_dir}/support/hax.rb", env["RUBYOPT"]), ENV["RUBYOPT"]) + env["RUBYOPT"] = opt_add(opt_add("-r#{spec_dir}/support/switch_rubygems.rb", env["RUBYOPT"]), ENV["RUBYOPT"]) options[:env] = env - sys_exec("#{Path.gem_bin} #{command}", options) - end - def rake - "#{Gem.ruby} -S #{ENV["GEM_PATH"]}/bin/rake" + sh(cmd, options, &block) end - def git(cmd, path, options = {}) - sys_exec("git #{cmd}", options.merge(:dir => path)) - end - - def sys_exec(cmd, options = {}) - env = options[:env] || {} - env["RUBYOPT"] = opt_add(opt_add("-r#{spec_dir}/support/switch_rubygems.rb", env["RUBYOPT"]), ENV["RUBYOPT"]) - dir = options[:dir] || bundled_app - command_execution = CommandExecution.new(cmd.to_s, dir) - - require "open3" - require "shellwords" - Open3.popen3(env, *cmd.shellsplit, :chdir => dir) do |stdin, stdout, stderr, wait_thr| - yield stdin, stdout, wait_thr if block_given? - stdin.close - - stdout_read_thread = Thread.new { stdout.read } - stderr_read_thread = Thread.new { stderr.read } - command_execution.stdout = stdout_read_thread.value.strip - command_execution.stderr = stderr_read_thread.value.strip - - status = wait_thr.value - command_execution.exitstatus = if status.exited? - status.exitstatus - elsif status.signaled? - exit_status_for_signal(status.termsig) - end - end - - unless options[:raise_on_error] == false || command_execution.success? - raise <<~ERROR - - Invoking `#{cmd}` failed with output: - ---------------------------------------------------------------------- - #{command_execution.stdboth} - ---------------------------------------------------------------------- - ERROR + 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 - command_executions << command_execution + current = File.exist?(path) ? Psych.load_file(path) : {} + return current unless config - command_execution.stdout - end + current = {} if current.empty? - def all_commands_output - return "" if command_executions.empty? + FileUtils.mkdir_p(File.dirname(path)) - "\n\nCommands:\n#{command_executions.map(&:to_s_verbose).join("\n\n")}" - end + new_config = current.merge(config).compact - def config(config = nil, path = bundled_app(".bundle/config")) - return Psych.load_file(path) unless config - FileUtils.mkdir_p(File.dirname(path)) - File.open(path, "w") do |f| - f.puts config.to_yaml + File.open(path, "w+") do |f| + f.puts new_config.to_yaml end - config + + new_config end - def global_config(config = nil) - config(config, home(".bundle/config")) + 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 - File.open(path.to_s, "w") do |f| - f.puts strip_whitespace(contents) + 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 @@ -244,8 +239,9 @@ module Spec contents = args.pop if contents.nil? - File.open(bundled_app_gemfile, "r", &:read) + read_gemfile else + match_source(contents) create_file(args.pop || "Gemfile", contents) end end @@ -254,12 +250,24 @@ module Spec contents = args.pop if contents.nil? - File.open(bundled_app_lock, "r", &:read) + 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] || "" @@ -278,62 +286,123 @@ module Spec 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 : {} - path = options.fetch(:path, system_gem_path) + install_dir = options.fetch(:path, system_gem_path) default = options.fetch(:default, false) - with_gem_path_as(path) do - gem_repo = options.fetch(:gem_repo, gem_repo1) - gems.each do |g| - gem_name = g.to_s - if gem_name.start_with?("bundler") - version = gem_name.match(/\Abundler-(?<version>.*)\z/)[:version] if gem_name != "bundler" - with_built_bundler(version) {|gem_path| install_gem(gem_path, default) } - elsif %r{\A(?:[a-zA-Z]:)?/.*\.gem\z}.match?(gem_name) - install_gem(gem_name, default) - else - install_gem("#{gem_repo}/gems/#{gem_name}.gem", default) - end + 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 install_gem(path, default = false) - raise "OMG `#{path}` does not exist!" unless File.exist?(path) + def self.install_dev_bundler + extend self - args = "--no-document --ignore-dependencies" - args += " --default --install-dir #{system_gem_path}" if default + 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 - gem_command "install #{args} '#{path}'" + 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 with_built_bundler(version = nil) - version ||= Bundler::VERSION - full_name = "bundler-#{version}" - build_path = tmp + full_name - bundler_path = build_path + "#{full_name}.gem" + def uninstall_gem(name, options = {}) + require "rubygems/uninstaller" - Dir.mkdir build_path + gem_home = options.dig(:env, "GEM_HOME") || system_gem_path.to_s - begin - shipped_files.each do |shipped_file| - target_shipped_file = build_path + shipped_file - target_shipped_dir = File.dirname(target_shipped_file) - FileUtils.mkdir_p target_shipped_dir unless File.directory?(target_shipped_dir) - FileUtils.cp shipped_file, target_shipped_file, :preserve => true - end + with_env_vars("GEM_HOME" => gem_home) do + Gem.clear_paths - replace_version_file(version, dir: build_path) # rubocop:disable Style/HashSyntax + uninstaller = Gem::Uninstaller.new( + name, + ignore: true, + executables: true, + all: true + ) + uninstaller.uninstall + ensure + Gem.clear_paths + end + end - Spec::BuildMetadata.write_build_metadata(dir: build_path) # rubocop:disable Style/HashSyntax + def installed_gems_list(options = {}) + gem_home = options.dig(:env, "GEM_HOME") || system_gem_path.to_s - gem_command "build #{relative_gemspec}", :dir => build_path + # Temporarily set GEM_HOME for the command + old_gem_home = ENV["GEM_HOME"] + ENV["GEM_HOME"] = gem_home + Gem.clear_paths - yield(bundler_path) + 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 - build_path.rmtree + 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) @@ -361,20 +430,40 @@ module Spec ENV.replace(backup) end - def with_path_added(path) - with_path_as([path.to_s, ENV["PATH"]].join(File::PATH_SEPARATOR)) do - yield + # 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 - end - def opt_add(option, options) - [option.strip, options].compact.reject(&:empty?).join(" ") - 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 - def opt_remove(option, options) - return unless options + RbConfig::CONFIG["arch"] = spec_platform + Gem::Platform.instance_variable_set(:@local, nil) + Gem.instance_variable_set(:@platforms, []) - options.split(" ").reject {|opt| opt.strip == option.strip }.join(" ") + 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! @@ -387,49 +476,43 @@ module Spec end def with_fake_man - skip "fake_man is not a Windows friendly binstub" if Gem.win_platform? - FileUtils.mkdir_p(tmp("fake_man")) - File.open(tmp("fake_man/man"), "w", 0o755) do |f| - f.puts "#!/usr/bin/env ruby\nputs ARGV.inspect\n" - end + 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_rf(system_gem_path) - - system_gems(*gems) - end - - def realworld_system_gems(*gems) - gems = gems.flatten - opts = gems.last.is_a?(Hash) ? gems.pop : {} - path = opts.fetch(:path, system_gem_path) + FileUtils.rm_r(system_gem_path) - with_gem_path_as(path) do - gems.each do |gem| - gem_command "install --no-document #{gem}" - end + if gems.any? + system_gems(*gems) + else + default_system_gems end end - def cache_gems(*gems) + def cache_gems(*gems, gem_repo: gem_repo1) gems = gems.flatten - FileUtils.rm_rf("#{bundled_app}/vendor/cache") FileUtils.mkdir_p("#{bundled_app}/vendor/cache") gems.each do |g| - path = "#{gem_repo1}/gems/#{g}.gem" - raise "OMG `#{path}` does not exist!" unless File.exist?(path) + 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_rf bundled_app(".bundle") - pristine_system_gems :bundler + 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) @@ -443,39 +526,9 @@ module Spec def simulate_platform(platform) old = ENV["BUNDLER_SPEC_PLATFORM"] ENV["BUNDLER_SPEC_PLATFORM"] = platform.to_s - yield if block_given? - ensure - ENV["BUNDLER_SPEC_PLATFORM"] = old if block_given? - end - - def simulate_windows(platform = x86_mswin32) - old = ENV["BUNDLER_SPEC_WINDOWS"] - ENV["BUNDLER_SPEC_WINDOWS"] = "true" - simulate_platform platform do - simulate_bundler_version_when_missing_prerelease_default_gem_activation do - yield - end - end - ensure - ENV["BUNDLER_SPEC_WINDOWS"] = old - end - - def simulate_bundler_version_when_missing_prerelease_default_gem_activation - return yield unless rubygems_version_failing_to_activate_bundler_prereleases - - old = ENV["BUNDLER_VERSION"] - ENV["BUNDLER_VERSION"] = Bundler::VERSION yield ensure - ENV["BUNDLER_VERSION"] = old - end - - def env_for_missing_prerelease_default_gem_activation - if rubygems_version_failing_to_activate_bundler_prereleases - { "BUNDLER_VERSION" => Bundler::VERSION } - else - {} - end + ENV["BUNDLER_SPEC_PLATFORM"] = old if block_given? end def current_ruby_minor @@ -483,17 +536,15 @@ module Spec end def next_ruby_minor - Gem.ruby_version.segments[0..1].map.with_index {|s, i| i == 1 ? s + 1 : s }.join(".") + ruby_major_minor.map.with_index {|s, i| i == 1 ? s + 1 : s }.join(".") end - # versions not including - # https://github.com/rubygems/rubygems/commit/929e92d752baad3a08f3ac92eaec162cb96aedd1 - def rubygems_version_failing_to_activate_bundler_prereleases - Gem.rubygems_version < Gem::Version.new("3.1.0.pre.1") + def ruby_major_minor + Gem.ruby_version.segments[0..1] end def revision_for(path) - sys_exec("git rev-parse HEAD", :dir => path).strip + git("rev-parse HEAD", path).strip end def with_read_only(pattern) @@ -545,41 +596,34 @@ module Spec end end - def require_rack - # need to hack, so we can require rack + 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.base_system_gem_path.to_s - require "rack" + ENV["GEM_HOME"] = Spec::Path.scoped_base_system_gem_path.to_s + require "rack/test" ENV["GEM_HOME"] = old_gem_home end - def wait_for_server(host, port, seconds = 15) - tries = 0 - sleep 0.5 - TCPSocket.new(host, port) - rescue StandardError => e - raise(e) if tries > (seconds * 2) - tries += 1 - retry - end - - def find_unused_port - port = 21_453 - begin - port += 1 while TCPSocket.new("127.0.0.1", port) - rescue StandardError - false - end - port - 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 diff --git a/spec/bundler/support/indexes.rb b/spec/bundler/support/indexes.rb index 670f3b0230..1fbdd49abe 100644 --- a/spec/bundler/support/indexes.rb +++ b/spec/bundler/support/indexes.rb @@ -14,26 +14,21 @@ module Spec alias_method :platforms, :platform - def resolve(args = []) + def resolve(args = [], dependency_api_available: true) @platforms ||= ["ruby"] - default_source = instance_double("Bundler::Source::Rubygems", :specs => @index, :to_s => "locally install gems") - source_requirements = { :default => default_source } - args[0] ||= Bundler::SpecSet.new([]) # base - args[0].each {|ls| ls.source = default_source } - args[1] ||= Bundler::GemVersionPromoter.new # gem_version_promoter - args[2] ||= [] # additional_base_requirements - originally_locked = args[3] || Bundler::SpecSet.new([]) - unlock = args[4] || [] - packages = Hash.new do |h, k| - h[k] = Bundler::Resolver::Package.new(k, @platforms, originally_locked, unlock) - end + 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 - platforms = d.gem_platforms(@platforms) source_requirements[name] = d.source = default_source - packages[name] = Bundler::Resolver::Package.new(name, platforms, originally_locked, unlock, :dependency => d) end - Bundler::Resolver.new(source_requirements, *args[0..2]).start(@deps, packages) + 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 @@ -46,6 +41,12 @@ module Spec 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 @@ -65,19 +66,18 @@ module Spec end def should_conservative_resolve_and_include(opts, unlock, specs) - # empty unlock means unlock all 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] + should_resolve_and_include specs, [@base, search, @locked, unlock] end def an_awesome_index build_index do - gem "rack", %w[0.8 0.9 0.9.1 0.9.2 1.0 1.1] - gem "rack-mount", %w[0.4 0.5 0.5.1 0.5.2 0.6] + 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"] @@ -88,10 +88,10 @@ module Spec gem "actionpack", version do dep "activesupport", version if version >= v("3.0.0.beta") - dep "rack", "~> 1.1" - dep "rack-mount", ">= 0.5" - elsif version > v("2.3") then dep "rack", "~> 1.0.0" - elsif version > v("2.0.0") then dep "rack", "~> 0.9.0" + 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 @@ -122,7 +122,7 @@ module Spec 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-mingw32" do |platform| + 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 @@ -303,7 +303,7 @@ module Spec end end - def a_unresovable_child_index + def a_unresolvable_child_index build_index do gem "json", %w[1.8.0] @@ -366,7 +366,7 @@ module Spec def a_circular_index build_index do - gem "rack", "1.0.1" + gem "myrack", "1.0.1" gem("foo", "0.2.6") do dep "bar", ">= 0" end diff --git a/spec/bundler/support/matchers.rb b/spec/bundler/support/matchers.rb index ea7c784683..5a3c38a4db 100644 --- a/spec/bundler/support/matchers.rb +++ b/spec/bundler/support/matchers.rb @@ -52,7 +52,7 @@ module Spec end def self.define_compound_matcher(matcher, preconditions, &declarations) - raise "Must have preconditions to define a compound matcher" if preconditions.empty? + 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), @@ -97,18 +97,6 @@ module Spec end end - RSpec::Matchers.define :take_less_than do |seconds| - match do |actual| - start_time = Time.now - - actual.call - - (Time.now - start_time).to_f < seconds - end - - supports_block_expectations - end - define_compound_matcher :read_as, [exist] do |file_contents| diffable @@ -128,8 +116,9 @@ module Spec source = opts.delete(:source) groups = Array(opts.delete(:groups)).map(&:inspect).join(", ") opts[:raise_on_error] = false - @errors = names.map do |full_name| + @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" @@ -139,6 +128,7 @@ module Spec 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 @@ -162,14 +152,14 @@ module Spec end if exitstatus == 65 actual_platform = out.split("\n").last - next "#{name} was expected to be of platform #{platform || "ruby"} but was #{actual_platform || "ruby"}" + 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.compact + end @errors.empty? end @@ -178,7 +168,7 @@ module Spec opts = names.last.is_a?(Hash) ? names.pop : {} groups = Array(opts.delete(:groups)).map(&:inspect).join(", ") opts[:raise_on_error] = false - @errors = names.map do |name| + @errors = names.filter_map do |name| name, version = name.split(/\s+/, 2) ruby <<-R, opts begin @@ -204,7 +194,7 @@ module Spec 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.compact + end @errors.empty? 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 index de03b2746e..2e6486412f 100644 --- a/spec/bundler/support/path.rb +++ b/spec/bundler/support/path.rb @@ -1,12 +1,16 @@ # frozen_string_literal: true -require "pathname" +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? ? "../../.." : "../..").expand_path(__dir__) + @source_root ||= Pathname.new(ruby_core? ? "../../.." : "../../bundler").expand_path(__dir__) end def root @@ -21,12 +25,8 @@ module Spec @relative_gemspec ||= ruby_core? ? "lib/bundler/bundler.gemspec" : "bundler.gemspec" end - def gemspec_dir - @gemspec_dir ||= gemspec.parent - end - def loaded_gemspec - @loaded_gemspec ||= Gem::Specification.load(gemspec.to_s) + @loaded_gemspec ||= Dir.chdir(source_root) { Gem::Specification.load(gemspec.to_s) } end def test_gemfile @@ -45,8 +45,16 @@ module Spec @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? ? "libexec" : "exe") + @bindir ||= source_root.join(ruby_core? ? "spec/bin" : "../bin") + end + + def exedir + @exedir ||= source_root.join(ruby_core? ? "libexec" : "exe") end def installed_bindir @@ -58,33 +66,39 @@ module Spec end def gem_bin - @gem_bin ||= ruby_core? ? ENV["GEM_COMMAND"] : "gem" + @gem_bin ||= ENV["GEM_COMMAND"] || "gem" end def path env_path = ENV["PATH"] - env_path = env_path.split(File::PATH_SEPARATOR).reject {|path| path == bindir.to_s }.join(File::PATH_SEPARATOR) if ruby_core? + 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 api_request_limit_hack_file - spec_dir.join("support/api_request_limit_hax.rb") + @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 ||= loaded_gemspec.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 @@ -96,7 +110,28 @@ module Spec end def tmp(*path) - source_root.join("tmp", scope, *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 @@ -107,33 +142,29 @@ module Spec end def home(*path) - tmp.join("home", *path) + tmp("home", *path) end def default_bundle_path(*path) - if Bundler.feature_flag.default_install_uses_path? - local_gem_path(*path) - else - system_gem_path(*path) - end + system_gem_path(*path) end def default_cache_path(*path) - if Bundler.feature_flag.global_gem_cache? - home(".bundle/cache", *path) - else - default_bundle_path("cache/bundler", *path) - end + default_bundle_path("cache/bundler", *path) + end + + def compact_index_cache_path + home(".bundle/cache/compact_index") end def bundled_app(*path) - root = tmp.join("bundled_app") + root = tmp("bundled_app") FileUtils.mkdir_p(root) root.join(*path) end def bundled_app2(*path) - root = tmp.join("bundled_app2") + root = tmp("bundled_app2") FileUtils.mkdir_p(root) root.join(*path) end @@ -154,20 +185,20 @@ module Spec bundled_app("Gemfile.lock") end - def base_system_gem_path - scoped_gem_path(base_system_gems) + def scoped_base_system_gem_path + scoped_gem_path(base_system_gem_path) end - def base_system_gems - tmp.join("gems/base") + def base_system_gem_path + tmp_root.join("gems/base") end - def rubocop_gems - tmp.join("gems/rubocop") + def rubocop_gem_path + tmp_root.join("gems/rubocop") end - def standard_gems - tmp.join("gems/standard") + def standard_gem_path + tmp_root.join("gems/standard") end def file_uri_for(path) @@ -178,35 +209,35 @@ module Spec end def gem_repo1(*args) - tmp("gems/remote1", *args) + gem_path("remote1", *args) end def gem_repo_missing(*args) - tmp("gems/missing", *args) + gem_path("missing", *args) end def gem_repo2(*args) - tmp("gems/remote2", *args) + gem_path("remote2", *args) end def gem_repo3(*args) - tmp("gems/remote3", *args) + gem_path("remote3", *args) end def gem_repo4(*args) - tmp("gems/remote4", *args) + gem_path("remote4", *args) end def security_repo(*args) - tmp("gems/security_repo", *args) + gem_path("security_repo", *args) end def system_gem_path(*path) - tmp("gems/system", *path) + gem_path("system", *path) end def pristine_system_gem_path - tmp("gems/base_system") + tmp_root.join("gems/pristine_system") end def local_gem_path(*path, base: bundled_app) @@ -217,6 +248,10 @@ module Spec 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 @@ -229,13 +264,6 @@ module Spec root.join("lib") end - # Sometimes rubygems version under test does not include - # https://github.com/rubygems/rubygems/pull/2728 and will not always end up - # activating the current bundler. In that case, require bundler absolutely. - def entrypoint - Gem.rubygems_version < Gem::Version.new("3.1.a") ? "#{lib_dir}/bundler" : "bundler" - end - def global_plugin_gem(*args) home ".bundle", "plugin", "gems", *args end @@ -251,35 +279,73 @@ module Spec 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*)"#{Gem::Version::VERSION_PATTERN}"/, %(\\1"#{version}")) + contents.sub!(/(^\s+VERSION\s*=\s*).*$/, %(\\1"#{version}")) File.open(version_file, "w") {|f| f << contents } end - def ruby_core? - # avoid to warnings - @ruby_core ||= nil + 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 - if @ruby_core.nil? - @ruby_core = true & ENV["GEM_COMMAND"] - else - @ruby_core - 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? - sys_exec("git ls-files -z -- #{glob}", :dir => source_root).split("\x0") + git("ls-files -z -- #{glob}", source_root).split("\x0") end def tracked_files_glob - ruby_core? ? "lib/bundler lib/bundler.rb spec/bundler man/bundle*" : "" + 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 @@ -287,7 +353,7 @@ module Spec end def man_tracked_files_glob - ruby_core? ? "man/bundle* man/gemfile*" : "lib/bundler/man/bundle*.1 lib/bundler/man/gemfile*.5" + "lib/bundler/man/bundle*.1.ronn lib/bundler/man/gemfile*.5.ronn" end def ruby_core_tarball? @@ -295,15 +361,15 @@ module Spec end def rubocop_gemfile_basename - source_root.join("tool/bundler/rubocop_gems.rb") + tool_dir.join("rubocop_gems.rb") end def standard_gemfile_basename - source_root.join("tool/bundler/standard_gems.rb") + tool_dir.join("standard_gems.rb") end def tool_dir - source_root.join("tool/bundler") + ruby_core? ? source_root.join("tool/bundler") : source_root.join("../tool/bundler") end def templates_dir diff --git a/spec/bundler/support/platforms.rb b/spec/bundler/support/platforms.rb index 3776901ce3..56a0843005 100644 --- a/spec/bundler/support/platforms.rb +++ b/spec/bundler/support/platforms.rb @@ -2,72 +2,22 @@ module Spec module Platforms - include Bundler::GemHelpers - - def rb - Gem::Platform::RUBY - end - - def mac - Gem::Platform.new("x86-darwin-10") - end - - def x64_mac - Gem::Platform.new("x86_64-darwin-15") - end - - def java - Gem::Platform.new([nil, "java", nil]) - end - - def linux - Gem::Platform.new(["x86", "linux", nil]) - end - - def x86_mswin32 - Gem::Platform.new(["x86", "mswin32", nil]) - end - - def x64_mswin64 - Gem::Platform.new(["x64", "mswin64", nil]) - end - - def x86_mingw32 - Gem::Platform.new(["x86", "mingw32", nil]) - end - - def x64_mingw32 - Gem::Platform.new(["x64", "mingw32", nil]) - end - - def x64_mingw_ucrt - Gem::Platform.new(["x64", "mingw", "ucrt"]) - end - - def windows_platforms - [x86_mswin32, x64_mswin64, x86_mingw32, x64_mingw32, x64_mingw_ucrt] - end - - def all_platforms - [rb, java, linux, windows_platforms].flatten - end - - def local - generic_local_platform + def not_local + generic_local_platform == Gem::Platform::RUBY ? "java" : Gem::Platform::RUBY end - def specific_local_platform + def local_platform Bundler.local_platform end - def not_local - all_platforms.find {|p| p != generic_local_platform } + def generic_local_platform + Gem::Platform.generic(local_platform) end def local_tag - if RUBY_PLATFORM == "java" + if Gem.java_platform? :jruby - elsif ["x64-mingw32", "x64-mingw-ucrt"].include?(RUBY_PLATFORM) + elsif Gem.win_platform? :windows else :ruby @@ -103,12 +53,23 @@ module Spec 9999 end - def lockfile_platforms - lockfile_platforms_for([specific_local_platform]) + 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 lockfile_platforms_for(platforms) - platforms.map(&:to_s).sort.join("\n ") + 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 index 4553c0606e..812dc4deaa 100644 --- a/spec/bundler/support/rubygems_ext.rb +++ b/spec/bundler/support/rubygems_ext.rb @@ -1,5 +1,7 @@ # 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) @@ -8,10 +10,6 @@ module Spec module Rubygems extend self - def dev_setup - install_gems(dev_gemfile) - end - def gem_load(gem_name, bin_container) require_relative "switch_rubygems" @@ -24,12 +22,15 @@ module Spec gem_load_activate_and_possibly_install(gem_name, bin_container) end - def gem_require(gem_name) + def gem_require(gem_name, entrypoint) gem_activate(gem_name) - require 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" @@ -38,42 +39,82 @@ module Spec FileUtils.mkdir_p(Path.tmpdir) ENV["HOME"] = Path.home.to_s - ENV["TMPDIR"] = Path.tmpdir.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 install_parallel_test_deps - Gem.clear_paths - - require "parallel" - require "fileutils" - - install_test_deps - - (2..Parallel.processor_count).each do |n| - source = Path.source_root.join("tmp", "1") - destination = Path.source_root.join("tmp", n.to_s) - - FileUtils.rm_rf destination - FileUtils.cp_r source, destination - end - end - def setup_test_paths - Gem.clear_paths - ENV["BUNDLE_PATH"] = nil - ENV["GEM_HOME"] = ENV["GEM_PATH"] = Path.base_system_gem_path.to_s - ENV["PATH"] = [Path.system_gem_path.join("bin"), ENV["PATH"]].join(File::PATH_SEPARATOR) - ENV["PATH"] = [Path.bindir, ENV["PATH"]].join(File::PATH_SEPARATOR) if Path.ruby_core? + 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 - install_gems(test_gemfile, Path.base_system_gems.to_s) - install_gems(rubocop_gemfile, Path.rubocop_gems.to_s) - install_gems(standard_gemfile, Path.standard_gems.to_s) + 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:) @@ -86,7 +127,7 @@ module Spec puts success_message puts else - system("git status --porcelain") + system("git diff") puts puts error_message @@ -96,6 +137,36 @@ module Spec 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) @@ -118,38 +189,12 @@ module Spec 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 install_gems(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 - - puts `#{Gem.ruby} #{File.expand_path("support/bundle.rb", Path.spec_dir)} install --verbose` - raise unless $?.success? - 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 - def test_gemfile Path.test_gemfile end diff --git a/spec/bundler/support/rubygems_version_manager.rb b/spec/bundler/support/rubygems_version_manager.rb index 5653601ae8..c174c461f0 100644 --- a/spec/bundler/support/rubygems_version_manager.rb +++ b/spec/bundler/support/rubygems_version_manager.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true -require "pathname" -require_relative "helpers" -require_relative "path" +require_relative "options" +require_relative "env" +require_relative "subprocess" class RubygemsVersionManager - include Spec::Helpers - include Spec::Path + include Spec::Options + include Spec::Env + include Spec::Subprocess def initialize(source) @source = source @@ -30,11 +31,10 @@ class RubygemsVersionManager rubygems_default_path = rubygems_path + "/defaults" bundler_path = rubylibdir + "/bundler" - bundler_exemptions = Gem.rubygems_version < Gem::Version.new("3.2.0") ? [bundler_path + "/errors.rb"] : [] 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) && !bundler_exemptions.any? {|bundler_exemption| loaded_feature.start_with?(bundler_exemption) }) + loaded_feature.start_with?(bundler_path) end errors = if bad_loaded_features.any? @@ -58,7 +58,7 @@ class RubygemsVersionManager cmd = [RbConfig.ruby, $0, *ARGV].compact - ENV["RUBYOPT"] = opt_add("-I#{local_copy_path.join("lib")}", opt_remove("--disable-gems", ENV["RUBYOPT"])) + ENV["RUBYOPT"] = opt_add("-I#{File.join(local_copy_path, "lib")}", opt_remove("--disable-gems", ENV["RUBYOPT"])) exec(ENV, *cmd) end @@ -66,14 +66,14 @@ class RubygemsVersionManager def switch_local_copy_if_needed return unless local_copy_switch_needed? - sys_exec("git checkout #{target_tag}", :dir => local_copy_path) + git("checkout #{target_tag}", local_copy_path) - ENV["RGV"] = local_copy_path.to_s + ENV["RGV"] = local_copy_path end def rubygems_unrequire_needed? require "rubygems" - !$LOADED_FEATURES.include?(local_copy_path.join("lib/rubygems.rb").to_s) + !$LOADED_FEATURES.include?(File.join(local_copy_path, "lib/rubygems.rb")) end def local_copy_switch_needed? @@ -85,7 +85,7 @@ class RubygemsVersionManager end def local_copy_tag - sys_exec("git rev-parse --abbrev-ref HEAD", :dir => local_copy_path) + git("rev-parse --abbrev-ref HEAD", local_copy_path) end def local_copy_path @@ -95,21 +95,25 @@ class RubygemsVersionManager def resolve_local_copy_path return expanded_source if source_is_path? - rubygems_path = source_root.join("tmp/rubygems") + rubygems_path = File.join(source_root, "tmp/rubygems") - unless rubygems_path.directory? - sys_exec("git clone .. #{rubygems_path}", :dir => source_root) + unless File.directory?(rubygems_path) + git("clone .. #{rubygems_path}", source_root) end rubygems_path end def source_is_path? - expanded_source.directory? + File.directory?(expanded_source) end def expanded_source - @expanded_source ||= Pathname.new(@source).expand_path(source_root) + @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 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/silent_logger.rb b/spec/bundler/support/silent_logger.rb deleted file mode 100644 index 8665beb2c9..0000000000 --- a/spec/bundler/support/silent_logger.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -require "logger" -module Spec - class SilentLogger - (::Logger.instance_methods - Object.instance_methods).each do |logger_instance_method| - define_method(logger_instance_method) {|*args, &blk| } - end - 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 index a138d22333..640b9f83b7 100644 --- a/spec/bundler/support/switch_rubygems.rb +++ b/spec/bundler/support/switch_rubygems.rb @@ -1,4 +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 index f252a4515b..452abd7d41 100644 --- a/spec/bundler/support/the_bundle.rb +++ b/spec/bundler/support/the_bundle.rb @@ -8,10 +8,8 @@ module Spec attr_accessor :bundle_dir - def initialize(opts = {}) - opts = opts.dup - @bundle_dir = Pathname.new(opts.delete(:bundle_dir) { bundled_app }) - raise "Too many options! #{opts}" unless opts.empty? + def initialize + @bundle_dir = Pathname.new(bundled_app) end def to_s @@ -28,8 +26,16 @@ module Spec end def locked_gems - raise "Cannot read lockfile if it doesn't exist" unless locked? + 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 |
