diff options
Diffstat (limited to 'spec/bundler/support')
70 files changed, 1243 insertions, 776 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.rb b/spec/bundler/support/artifice/compact_index.rb index fb068fa9b5..ebc4d0ae5b 100644 --- a/spec/bundler/support/artifice/compact_index.rb +++ b/spec/bundler/support/artifice/compact_index.rb @@ -1,120 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -$LOAD_PATH.unshift Dir[Spec::Path.base_system_gem_path.join("gems/compact_index*/lib")].first.to_s -require "compact_index" - -class CompactIndexAPI < Endpoint - helpers do - include Spec::Path - - def load_spec(name, version, platform, gem_repo) - full_name = "#{name}-#{version}" - full_name += "-#{platform}" if platform != "ruby" - Marshal.load(Bundler.rubygems.inflate(File.binread(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")))) - end - - def etag_response - response_body = yield - checksum = Digest(:MD5).hexdigest(response_body) - return if not_modified?(checksum) - headers "ETag" => quote(checksum) - headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" - content_type "text/plain" - requested_range_for(response_body) - rescue StandardError => e - puts e - puts e.backtrace - raise - end - - def not_modified?(checksum) - etags = parse_etags(request.env["HTTP_IF_NONE_MATCH"]) - - return unless etags.include?(checksum) - headers "ETag" => quote(checksum) - status 304 - body "" - end - - def requested_range_for(response_body) - ranges = Rack::Utils.byte_ranges(env, response_body.bytesize) - - if ranges - status 206 - body ranges.map! {|range| slice_body(response_body, range) }.join - else - status 200 - body response_body - end - end - - def quote(string) - %("#{string}") - end - - def parse_etags(value) - value ? value.split(/, ?/).select {|s| s.sub!(/"(.*)"/, '\1') } : [] - end - - def slice_body(body, range) - body.byteslice(range) - end - - def gems(gem_repo = default_gem_repo) - @gems ||= {} - @gems[gem_repo] ||= begin - specs = Bundler::Deprecate.skip_during do - %w[specs.4.8 prerelease_specs.4.8].map do |filename| - Marshal.load(File.open(gem_repo.join(filename)).read).map do |name, version, platform| - load_spec(name, version, platform, gem_repo) - end - end.flatten - end - - specs.group_by(&:name).map do |name, versions| - gem_versions = versions.map do |spec| - deps = spec.dependencies.select {|d| d.type == :runtime }.map do |d| - reqs = d.requirement.requirements.map {|r| r.join(" ") }.join(", ") - CompactIndex::Dependency.new(d.name, reqs) - end - checksum = begin - Digest(:SHA256).file("#{gem_repo}/gems/#{spec.original_name}.gem").base64digest - rescue StandardError - nil - end - CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, checksum, nil, - deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s) - end - CompactIndex::Gem.new(name, gem_versions) - end - end - end - end - - get "/names" do - etag_response do - CompactIndex.names(gems.map(&:name)) - end - end - - get "/versions" do - etag_response do - file = tmp("versions.list") - FileUtils.rm_f(file) - file = CompactIndex::VersionsFile.new(file.to_s) - file.create(gems) - file.contents - end - end - - get "/info/:name" do - etag_response do - gem = gems.find {|g| g.name == params[:name] } - CompactIndex.info(gem ? gem.versions : []) - end - end -end +require_relative "helpers/compact_index" +require_relative "helpers/artifice" Artifice.activate_with(CompactIndexAPI) diff --git a/spec/bundler/support/artifice/compact_index_api_missing.rb b/spec/bundler/support/artifice/compact_index_api_missing.rb index 6514fde01e..f771f7d1f0 100644 --- a/spec/bundler/support/artifice/compact_index_api_missing.rb +++ b/spec/bundler/support/artifice/compact_index_api_missing.rb @@ -1,18 +1,13 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexApiMissing < CompactIndexAPI get "/fetch/actual/gem/:id" do - warn params[:id] - if params[:id] == "rack-1.0.gemspec.rz" - halt 404 - else - File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") - end + halt 404 end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexApiMissing) diff --git a/spec/bundler/support/artifice/compact_index_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_basic_authentication.rb index 775f1a3977..b9115cdd86 100644 --- a/spec/bundler/support/artifice/compact_index_basic_authentication.rb +++ b/spec/bundler/support/artifice/compact_index_basic_authentication.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexBasicAuthentication < CompactIndexAPI before do @@ -12,4 +10,6 @@ class CompactIndexBasicAuthentication < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexBasicAuthentication) diff --git a/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb index 1abe64236c..83b147d2ae 100644 --- a/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb +++ b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +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 +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexChecksumMismatch) diff --git a/spec/bundler/support/artifice/compact_index_concurrent_download.rb b/spec/bundler/support/artifice/compact_index_concurrent_download.rb index 14c31f35a4..5d55b8a72b 100644 --- a/spec/bundler/support/artifice/compact_index_concurrent_download.rb +++ b/spec/bundler/support/artifice/compact_index_concurrent_download.rb @@ -1,19 +1,18 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexConcurrentDownload < CompactIndexAPI get "/versions" do versions = File.join(Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions") - # Verify the original (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 @@ -29,4 +28,6 @@ class CompactIndexConcurrentDownload < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexConcurrentDownload) 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 cfe22c7f51..401e8a98d8 100644 --- a/spec/bundler/support/artifice/compact_index_creds_diff_host.rb +++ b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexCredsDiffHost < CompactIndexAPI helpers do @@ -36,4 +34,6 @@ class CompactIndexCredsDiffHost < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexCredsDiffHost) diff --git a/spec/bundler/support/artifice/compact_index_etag_match.rb b/spec/bundler/support/artifice/compact_index_etag_match.rb new file mode 100644 index 0000000000..08d7b5ec53 --- /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 "ETag header should be present" unless env["HTTP_IF_NONE_MATCH"] + headers "ETag" => env["HTTP_IF_NONE_MATCH"] + status 304 + body "" + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexEtagMatch) diff --git a/spec/bundler/support/artifice/compact_index_extra.rb b/spec/bundler/support/artifice/compact_index_extra.rb index cec368276a..cd41b3ecca 100644 --- a/spec/bundler/support/artifice/compact_index_extra.rb +++ b/spec/bundler/support/artifice/compact_index_extra.rb @@ -1,37 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate - -class CompactIndexExtra < CompactIndexAPI - get "/extra/versions" do - halt 404 - end - - get "/extra/api/v1/dependencies" do - halt 404 - end - - get "/extra/specs.4.8.gz" do - File.binread("#{gem_repo2}/specs.4.8.gz") - end - - get "/extra/prerelease_specs.4.8.gz" do - File.binread("#{gem_repo2}/prerelease_specs.4.8.gz") - end - - get "/extra/quick/Marshal.4.8/:id" do - redirect "/extra/fetch/actual/gem/#{params[:id]}" - end - - get "/extra/fetch/actual/gem/:id" do - File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") - end - - get "/extra/gems/:id" do - File.binread("#{gem_repo2}/gems/#{params[:id]}") - end -end +require_relative "helpers/compact_index_extra" +require_relative "helpers/artifice" Artifice.activate_with(CompactIndexExtra) diff --git a/spec/bundler/support/artifice/compact_index_extra_api.rb b/spec/bundler/support/artifice/compact_index_extra_api.rb index 5cc13421a8..8b9d304ab4 100644 --- a/spec/bundler/support/artifice/compact_index_extra_api.rb +++ b/spec/bundler/support/artifice/compact_index_extra_api.rb @@ -1,52 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate - -class CompactIndexExtraApi < CompactIndexAPI - get "/extra/names" do - etag_response do - CompactIndex.names(gems(gem_repo4).map(&:name)) - end - end - - get "/extra/versions" do - etag_response do - file = tmp("versions.list") - FileUtils.rm_f(file) - file = CompactIndex::VersionsFile.new(file.to_s) - file.create(gems(gem_repo4)) - file.contents - end - end - - get "/extra/info/:name" do - etag_response do - gem = gems(gem_repo4).find {|g| g.name == params[:name] } - CompactIndex.info(gem ? gem.versions : []) - end - end - - get "/extra/specs.4.8.gz" do - File.binread("#{gem_repo4}/specs.4.8.gz") - end - - get "/extra/prerelease_specs.4.8.gz" do - File.binread("#{gem_repo4}/prerelease_specs.4.8.gz") - end - - get "/extra/quick/Marshal.4.8/:id" do - redirect "/extra/fetch/actual/gem/#{params[:id]}" - end - - get "/extra/fetch/actual/gem/:id" do - File.binread("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}") - end - - get "/extra/gems/:id" do - File.binread("#{gem_repo4}/gems/#{params[:id]}") - end -end +require_relative "helpers/compact_index_extra_api" +require_relative "helpers/artifice" Artifice.activate_with(CompactIndexExtraApi) diff --git a/spec/bundler/support/artifice/compact_index_extra_api_missing.rb b/spec/bundler/support/artifice/compact_index_extra_api_missing.rb index b9d757c266..df6ede584c 100644 --- a/spec/bundler/support/artifice/compact_index_extra_api_missing.rb +++ b/spec/bundler/support/artifice/compact_index_extra_api_missing.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index_extra_api" - -Artifice.deactivate +require_relative "helpers/compact_index_extra_api" class CompactIndexExtraAPIMissing < CompactIndexExtraApi get "/extra/fetch/actual/gem/:id" do @@ -14,4 +12,6 @@ class CompactIndexExtraAPIMissing < CompactIndexExtraApi end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexExtraAPIMissing) diff --git a/spec/bundler/support/artifice/compact_index_extra_missing.rb b/spec/bundler/support/artifice/compact_index_extra_missing.rb index ff1e47a1bb..255c89afdb 100644 --- a/spec/bundler/support/artifice/compact_index_extra_missing.rb +++ b/spec/bundler/support/artifice/compact_index_extra_missing.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index_extra" - -Artifice.deactivate +require_relative "helpers/compact_index_extra" class CompactIndexExtraMissing < CompactIndexExtra get "/extra/fetch/actual/gem/:id" do @@ -14,4 +12,6 @@ class CompactIndexExtraMissing < CompactIndexExtra end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexExtraMissing) diff --git a/spec/bundler/support/artifice/compact_index_forbidden.rb b/spec/bundler/support/artifice/compact_index_forbidden.rb index 3eebe0fbd8..18c30ed9a2 100644 --- a/spec/bundler/support/artifice/compact_index_forbidden.rb +++ b/spec/bundler/support/artifice/compact_index_forbidden.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexForbidden < CompactIndexAPI get "/versions" do @@ -10,4 +8,6 @@ class CompactIndexForbidden < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexForbidden) diff --git a/spec/bundler/support/artifice/compact_index_host_redirect.rb b/spec/bundler/support/artifice/compact_index_host_redirect.rb index 304c897d68..4f82bf3812 100644 --- a/spec/bundler/support/artifice/compact_index_host_redirect.rb +++ b/spec/bundler/support/artifice/compact_index_host_redirect.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +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 @@ -18,4 +16,6 @@ class CompactIndexHostRedirect < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexHostRedirect) diff --git a/spec/bundler/support/artifice/compact_index_no_gem.rb b/spec/bundler/support/artifice/compact_index_no_gem.rb index 0a4be08a46..71f6629688 100644 --- a/spec/bundler/support/artifice/compact_index_no_gem.rb +++ b/spec/bundler/support/artifice/compact_index_no_gem.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexNoGem < CompactIndexAPI get "/gems/:id" do @@ -10,4 +8,6 @@ class CompactIndexNoGem < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexNoGem) diff --git a/spec/bundler/support/artifice/compact_index_partial_update.rb b/spec/bundler/support/artifice/compact_index_partial_update.rb index cb1c7b9481..f111d91ef9 100644 --- a/spec/bundler/support/artifice/compact_index_partial_update.rb +++ b/spec/bundler/support/artifice/compact_index_partial_update.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexPartialUpdate < CompactIndexAPI # Stub the server to never return 304s. This simulates the behaviour of @@ -25,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 @@ -35,4 +33,6 @@ class CompactIndexPartialUpdate < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexPartialUpdate) diff --git a/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb b/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb new file mode 100644 index 0000000000..ac04336636 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +# The purpose of this Artifice is to test that an incremental response is invalidated +# and a second request is issued for the full content. +class CompactIndexPartialUpdateBadDigest < CompactIndexAPI + def partial_update_bad_digest + response_body = yield + if request.env["HTTP_RANGE"] + headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest("wrong digest on ranged request")}:" + else + headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest(response_body)}:" + end + headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" + content_type "text/plain" + requested_range_for(response_body) + end + + get "/versions" do + partial_update_bad_digest do + file = tmp("versions.list") + FileUtils.rm_f(file) + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + file.contents([], calculate_info_checksums: true) + end + end + + get "/info/:name" do + partial_update_bad_digest do + gem = gems.find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexPartialUpdateBadDigest) diff --git a/spec/bundler/support/artifice/compact_index_partial_update_no_etag_not_incremental.rb b/spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb index acf76dfbf0..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 @@ -1,11 +1,11 @@ # frozen_string_literal: true -require_relative "compact_index" +require_relative "helpers/compact_index" -Artifice.deactivate - -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" @@ -13,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 @@ -27,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 +37,6 @@ class CompactIndexPartialUpdateNoEtagNotIncremental < CompactIndexAPI end end -Artifice.activate_with(CompactIndexPartialUpdateNoEtagNotIncremental) +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexPartialUpdateNoDigestNotIncremental) diff --git a/spec/bundler/support/artifice/compact_index_precompiled_before.rb b/spec/bundler/support/artifice/compact_index_precompiled_before.rb new file mode 100644 index 0000000000..b5f72f546a --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_precompiled_before.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexPrecompiledBefore < CompactIndexAPI + get "/info/:name" do + etag_response do + gem = gems.find {|g| g.name == params[:name] } + move_ruby_variant_to_the_end(CompactIndex.info(gem ? gem.versions : [])) + end + end + + private + + def move_ruby_variant_to_the_end(response) + lines = response.split("\n") + ruby = lines.find {|line| /\A\d+\.\d+\.\d* \|/.match(line) } + lines.delete(ruby) + lines.push(ruby).join("\n") + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexPrecompiledBefore) diff --git a/spec/bundler/support/artifice/compact_index_range_ignored.rb b/spec/bundler/support/artifice/compact_index_range_ignored.rb new file mode 100644 index 0000000000..2303682c1f --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_range_ignored.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexRangeIgnored < CompactIndexAPI + # Stub the server to not return 304 so that we don't bypass all the logic + def not_modified?(_checksum) + false + end + + get "/versions" do + cached_versions_path = File.join( + Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions" + ) + + # Verify a cached copy of the versions file exists + unless File.binread(cached_versions_path).size > 0 + raise("Cached versions file should be present and have content") + end + + # Verify that a partial request is made, starting from the index of the + # final byte of the cached file. + unless env.delete("HTTP_RANGE") + raise("Expected client to write the full response on the first try") + end + + etag_response do + file = tmp("versions.list") + FileUtils.rm_f(file) + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + file.contents + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexRangeIgnored) diff --git a/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb b/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb index bb616125bb..8a7c4b79b0 100644 --- a/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb +++ b/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexRangeNotSatisfiable < CompactIndexAPI get "/versions" do @@ -31,4 +29,6 @@ class CompactIndexRangeNotSatisfiable < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexRangeNotSatisfiable) diff --git a/spec/bundler/support/artifice/compact_index_rate_limited.rb b/spec/bundler/support/artifice/compact_index_rate_limited.rb index 570105e2a0..4495491635 100644 --- a/spec/bundler/support/artifice/compact_index_rate_limited.rb +++ b/spec/bundler/support/artifice/compact_index_rate_limited.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexRateLimited < CompactIndexAPI class RequestCounter @@ -45,4 +43,6 @@ class CompactIndexRateLimited < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexRateLimited) diff --git a/spec/bundler/support/artifice/compact_index_redirects.rb b/spec/bundler/support/artifice/compact_index_redirects.rb index 99adc797bf..f7ba393239 100644 --- a/spec/bundler/support/artifice/compact_index_redirects.rb +++ b/spec/bundler/support/artifice/compact_index_redirects.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexRedirect < CompactIndexAPI get "/fetch/actual/gem/:id" do @@ -18,4 +16,6 @@ class CompactIndexRedirect < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexRedirect) diff --git a/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb index 7d427b5382..96259385e7 100644 --- a/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb +++ b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexStrictBasicAuthentication < CompactIndexAPI before do @@ -12,9 +10,11 @@ class CompactIndexStrictBasicAuthentication < CompactIndexAPI # Only accepts password == "password" unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz" - halt 403, "Authentication failed" + halt 401, "Authentication failed" end end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexStrictBasicAuthentication) diff --git a/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb index 036fac70b3..15850599b6 100644 --- a/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb +++ b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexWrongDependencies < CompactIndexAPI get "/info/:name" do @@ -14,4 +12,6 @@ class CompactIndexWrongDependencies < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexWrongDependencies) diff --git a/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb index 8add32b88f..9bd2ca0a9d 100644 --- a/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb +++ b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb @@ -1,15 +1,14 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexWrongGemChecksum < CompactIndexAPI get "/info/:name" do etag_response do name = params[:name] gem = gems.find {|g| g.name == name } - 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) @@ -17,4 +16,6 @@ class CompactIndexWrongGemChecksum < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexWrongGemChecksum) diff --git a/spec/bundler/support/artifice/endpoint.rb b/spec/bundler/support/artifice/endpoint.rb index c00113b28f..15242a7942 100644 --- a/spec/bundler/support/artifice/endpoint.rb +++ b/spec/bundler/support/artifice/endpoint.rb @@ -1,115 +1,6 @@ # frozen_string_literal: true -require_relative "../path" - -$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gem_path.join("gems/{artifice,mustermann,rack,tilt,sinatra,ruby2_keywords}-*/lib")].map(&:to_s)) - -require "artifice" -require "sinatra/base" - -ALL_REQUESTS = [] # rubocop:disable Style/MutableConstant -ALL_REQUESTS_MUTEX = Thread::Mutex.new - -at_exit do - if expected = ENV["BUNDLER_SPEC_ALL_REQUESTS"] - expected = expected.split("\n").sort - actual = ALL_REQUESTS.sort - - unless expected == actual - raise "Unexpected requests!\nExpected:\n\t#{expected.join("\n\t")}\n\nActual:\n\t#{actual.join("\n\t")}" - end - end -end - -class Endpoint < Sinatra::Base - def self.all_requests - @all_requests ||= [] - end - - set :raise_errors, true - set :show_exceptions, false - - def call!(*) - super.tap do - ALL_REQUESTS_MUTEX.synchronize do - ALL_REQUESTS << @request.url - end - end - end - - helpers do - include Spec::Path - - def default_gem_repo - if ENV["BUNDLER_SPEC_GEM_REPO"] - Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"]) - else - case request.host - when "gem.repo1" - Spec::Path.gem_repo1 - when "gem.repo2" - Spec::Path.gem_repo2 - when "gem.repo3" - Spec::Path.gem_repo3 - when "gem.repo4" - Spec::Path.gem_repo4 - else - Spec::Path.gem_repo1 - end - end - end - - def dependencies_for(gem_names, gem_repo = default_gem_repo) - return [] if gem_names.nil? || gem_names.empty? - - all_specs = %w[specs.4.8 prerelease_specs.4.8].map do |filename| - Marshal.load(File.open(gem_repo.join(filename)).read) - end.inject(:+) - - all_specs.map do |name, version, platform| - spec = load_spec(name, version, platform, gem_repo) - next unless gem_names.include?(spec.name) - { - :name => spec.name, - :number => spec.version.version, - :platform => spec.platform.to_s, - :dependencies => spec.dependencies.select {|dep| dep.type == :runtime }.map do |dep| - [dep.name, dep.requirement.requirements.map {|a| a.join(" ") }.join(", ")] - end, - } - end.compact - end - - def load_spec(name, version, platform, gem_repo) - full_name = "#{name}-#{version}" - full_name += "-#{platform}" if platform != "ruby" - Marshal.load(Bundler.rubygems.inflate(File.binread(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")))) - end - end - - get "/quick/Marshal.4.8/:id" do - redirect "/fetch/actual/gem/#{params[:id]}" - end - - get "/fetch/actual/gem/:id" do - File.binread("#{default_gem_repo}/quick/Marshal.4.8/#{params[:id]}") - end - - get "/gems/:id" do - File.binread("#{default_gem_repo}/gems/#{params[:id]}") - end - - get "/api/v1/dependencies" do - Marshal.dump(dependencies_for(params[:gems])) - end - - get "/specs.4.8.gz" do - File.binread("#{default_gem_repo}/specs.4.8.gz") - end - - get "/prerelease_specs.4.8.gz" do - File.binread("#{default_gem_repo}/prerelease_specs.4.8.gz") - end -end +require_relative "helpers/endpoint" +require_relative "helpers/artifice" Artifice.activate_with(Endpoint) diff --git a/spec/bundler/support/artifice/endpoint_500.rb b/spec/bundler/support/artifice/endpoint_500.rb index a0d850a44d..b1ed1964c8 100644 --- a/spec/bundler/support/artifice/endpoint_500.rb +++ b/spec/bundler/support/artifice/endpoint_500.rb @@ -2,17 +2,16 @@ require_relative "../path" -$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gem_path.join("gems/{artifice,mustermann,rack,tilt,sinatra,ruby2_keywords}-*/lib")].map(&:to_s)) +$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gem_path.join("gems/{mustermann,rack,tilt,sinatra,ruby2_keywords,base64}-*/lib")].map(&:to_s)) -require "artifice" require "sinatra/base" -Artifice.deactivate - class Endpoint500 < Sinatra::Base before do halt 500 end end +require_relative "helpers/artifice" + Artifice.activate_with(Endpoint500) diff --git a/spec/bundler/support/artifice/endpoint_api_forbidden.rb b/spec/bundler/support/artifice/endpoint_api_forbidden.rb index edc2463424..6bdc5896d6 100644 --- a/spec/bundler/support/artifice/endpoint_api_forbidden.rb +++ b/spec/bundler/support/artifice/endpoint_api_forbidden.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointApiForbidden < Endpoint get "/api/v1/dependencies" do @@ -10,4 +8,6 @@ class EndpointApiForbidden < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointApiForbidden) diff --git a/spec/bundler/support/artifice/endpoint_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_basic_authentication.rb index ff3d1493d6..e8e3569e63 100644 --- a/spec/bundler/support/artifice/endpoint_basic_authentication.rb +++ b/spec/bundler/support/artifice/endpoint_basic_authentication.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointBasicAuthentication < Endpoint before do @@ -12,4 +10,6 @@ class EndpointBasicAuthentication < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_creds_diff_host.rb b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb index 8b8972cedd..ce30de0a68 100644 --- a/spec/bundler/support/artifice/endpoint_creds_diff_host.rb +++ b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointCredsDiffHost < Endpoint helpers do @@ -36,4 +34,6 @@ class EndpointCredsDiffHost < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointCredsDiffHost) diff --git a/spec/bundler/support/artifice/endpoint_extra.rb b/spec/bundler/support/artifice/endpoint_extra.rb index 942c4352b7..021fd435fe 100644 --- a/spec/bundler/support/artifice/endpoint_extra.rb +++ b/spec/bundler/support/artifice/endpoint_extra.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointExtra < Endpoint get "/extra/api/v1/dependencies" do @@ -30,4 +28,6 @@ class EndpointExtra < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointExtra) diff --git a/spec/bundler/support/artifice/endpoint_extra_api.rb b/spec/bundler/support/artifice/endpoint_extra_api.rb index 1cfef7a7fc..a965af6e73 100644 --- a/spec/bundler/support/artifice/endpoint_extra_api.rb +++ b/spec/bundler/support/artifice/endpoint_extra_api.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointExtraApi < Endpoint get "/extra/api/v1/dependencies" do @@ -31,4 +29,6 @@ class EndpointExtraApi < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointExtraApi) diff --git a/spec/bundler/support/artifice/endpoint_extra_missing.rb b/spec/bundler/support/artifice/endpoint_extra_missing.rb index 5fd9238207..73e2defb32 100644 --- a/spec/bundler/support/artifice/endpoint_extra_missing.rb +++ b/spec/bundler/support/artifice/endpoint_extra_missing.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint_extra" - -Artifice.deactivate +require_relative "helpers/endpoint_extra" class EndpointExtraMissing < EndpointExtra get "/extra/fetch/actual/gem/:id" do @@ -14,4 +12,6 @@ class EndpointExtraMissing < EndpointExtra end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointExtraMissing) diff --git a/spec/bundler/support/artifice/endpoint_fallback.rb b/spec/bundler/support/artifice/endpoint_fallback.rb index 08edf232e3..742e563f07 100644 --- a/spec/bundler/support/artifice/endpoint_fallback.rb +++ b/spec/bundler/support/artifice/endpoint_fallback.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointFallback < Endpoint DEPENDENCY_LIMIT = 60 @@ -16,4 +14,6 @@ class EndpointFallback < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointFallback) diff --git a/spec/bundler/support/artifice/endpoint_host_redirect.rb b/spec/bundler/support/artifice/endpoint_host_redirect.rb index 338cbcad00..6ce51bed93 100644 --- a/spec/bundler/support/artifice/endpoint_host_redirect.rb +++ b/spec/bundler/support/artifice/endpoint_host_redirect.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +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 @@ -14,4 +12,6 @@ class EndpointHostRedirect < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointHostRedirect) diff --git a/spec/bundler/support/artifice/endpoint_marshal_fail.rb b/spec/bundler/support/artifice/endpoint_marshal_fail.rb index 22c13e3e17..74ce321de6 100644 --- a/spec/bundler/support/artifice/endpoint_marshal_fail.rb +++ b/spec/bundler/support/artifice/endpoint_marshal_fail.rb @@ -1,13 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint_fallback" - -Artifice.deactivate - -class EndpointMarshalFail < EndpointFallback - get "/api/v1/dependencies" do - "f0283y01hasf" - end -end +require_relative "helpers/endpoint_marshal_fail" +require_relative "helpers/artifice" Artifice.activate_with(EndpointMarshalFail) diff --git a/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb index c341c3993f..ea4cfbe965 100644 --- a/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb +++ b/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint_marshal_fail" - -Artifice.deactivate +require_relative "helpers/endpoint_marshal_fail" class EndpointMarshalFailBasicAuthentication < EndpointMarshalFail before do @@ -12,4 +10,6 @@ class EndpointMarshalFailBasicAuthentication < EndpointMarshalFail end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointMarshalFailBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_mirror_source.rb b/spec/bundler/support/artifice/endpoint_mirror_source.rb index 788a9027f3..fed7a746b9 100644 --- a/spec/bundler/support/artifice/endpoint_mirror_source.rb +++ b/spec/bundler/support/artifice/endpoint_mirror_source.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require_relative "endpoint" +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 @@ -12,4 +12,6 @@ class EndpointMirrorSource < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointMirrorSource) diff --git a/spec/bundler/support/artifice/endpoint_redirect.rb b/spec/bundler/support/artifice/endpoint_redirect.rb index ee97fccf64..84f546ba9d 100644 --- a/spec/bundler/support/artifice/endpoint_redirect.rb +++ b/spec/bundler/support/artifice/endpoint_redirect.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointRedirect < Endpoint get "/fetch/actual/gem/:id" do @@ -14,4 +12,6 @@ class EndpointRedirect < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointRedirect) diff --git a/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb index 4d4da08770..dff360c5c5 100644 --- a/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb +++ b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointStrictBasicAuthentication < Endpoint before do @@ -12,9 +10,11 @@ class EndpointStrictBasicAuthentication < Endpoint # Only accepts password == "password" unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz" - halt 403, "Authentication failed" + halt 401, "Authentication failed" end end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointStrictBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_timeout.rb b/spec/bundler/support/artifice/endpoint_timeout.rb index c118da1893..86b793e499 100644 --- a/spec/bundler/support/artifice/endpoint_timeout.rb +++ b/spec/bundler/support/artifice/endpoint_timeout.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint_fallback" - -Artifice.deactivate +require_relative "helpers/endpoint_fallback" class EndpointTimeout < EndpointFallback SLEEP_TIMEOUT = 3 @@ -12,4 +10,6 @@ class EndpointTimeout < EndpointFallback end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointTimeout) diff --git a/spec/bundler/support/artifice/fail.rb b/spec/bundler/support/artifice/fail.rb index f69f2eccc6..8822e5b8e2 100644 --- a/spec/bundler/support/artifice/fail.rb +++ b/spec/bundler/support/artifice/fail.rb @@ -1,15 +1,11 @@ # frozen_string_literal: true -require "net/http" +require "bundler/vendored_net_http" -# We can't use artifice here because it uses rack - -module Artifice; end # for < 2.0, Net::HTTP::Persistent::SSLReuse - -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) @@ -21,14 +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 -# Replace Net::HTTP with our failing subclass -::Net.class_eval do - remove_const(:HTTP) - const_set(:HTTP, ::Fail) -end +require_relative "helpers/artifice" + +# Replace Gem::Net::HTTP with our failing subclass +Artifice.replace_net_http(::Fail) diff --git a/spec/bundler/support/artifice/helpers/artifice.rb b/spec/bundler/support/artifice/helpers/artifice.rb new file mode 100644 index 0000000000..788268295c --- /dev/null +++ b/spec/bundler/support/artifice/helpers/artifice.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# This module was initially borrowed from https://github.com/wycats/artifice +module Artifice + # Activate Artifice with a particular Rack endpoint. + # + # Calling this method will replace the Gem::Net::HTTP system + # with a replacement that routes all requests to the + # Rack endpoint. + # + # @param [#call] endpoint A valid Rack endpoint + def self.activate_with(endpoint) + require_relative "rack_request" + + Net::HTTP.endpoint = endpoint + replace_net_http(Artifice::Net::HTTP) + end + + # Deactivate the Artifice replacement. + def self.deactivate + replace_net_http(::Gem::Net::HTTP) + end + + def self.replace_net_http(value) + ::Gem::Net.class_eval do + remove_const(:HTTP) + const_set(:HTTP, value) + end + end +end diff --git a/spec/bundler/support/artifice/helpers/compact_index.rb b/spec/bundler/support/artifice/helpers/compact_index.rb new file mode 100644 index 0000000000..a803a2d30a --- /dev/null +++ b/spec/bundler/support/artifice/helpers/compact_index.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require_relative "endpoint" + +$LOAD_PATH.unshift Dir[Spec::Path.base_system_gem_path.join("gems/compact_index*/lib")].first.to_s +require "compact_index" +require "digest" + +class CompactIndexAPI < Endpoint + helpers do + include Spec::Path + + def load_spec(name, version, platform, gem_repo) + full_name = "#{name}-#{version}" + full_name += "-#{platform}" if platform != "ruby" + Marshal.load(Bundler.rubygems.inflate(File.binread(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")))) + end + + def etag_response + response_body = yield + etag = Digest::MD5.hexdigest(response_body) + headers "ETag" => quote(etag) + return if not_modified?(etag) + headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest(response_body)}:" + headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" + content_type "text/plain" + requested_range_for(response_body) + rescue StandardError => e + puts e + puts e.backtrace + raise + end + + def not_modified?(etag) + etags = parse_etags(request.env["HTTP_IF_NONE_MATCH"]) + + return unless etags.include?(etag) + status 304 + body "" + end + + def requested_range_for(response_body) + ranges = Rack::Utils.byte_ranges(env, response_body.bytesize) + + if ranges + status 206 + body ranges.map! {|range| slice_body(response_body, range) }.join + else + status 200 + body response_body + end + end + + def quote(string) + %("#{string}") + end + + def parse_etags(value) + value ? value.split(/, ?/).select {|s| s.sub!(/"(.*)"/, '\1') } : [] + end + + def slice_body(body, range) + body.byteslice(range) + end + + def gems(gem_repo = default_gem_repo) + @gems ||= {} + @gems[gem_repo] ||= begin + specs = Bundler::Deprecate.skip_during do + %w[specs.4.8 prerelease_specs.4.8].map do |filename| + Marshal.load(File.open(gem_repo.join(filename)).read).map do |name, version, platform| + load_spec(name, version, platform, gem_repo) + end + end.flatten + end + + specs.group_by(&:name).map do |name, versions| + gem_versions = versions.map do |spec| + deps = spec.runtime_dependencies.map do |d| + reqs = d.requirement.requirements.map {|r| r.join(" ") }.join(", ") + CompactIndex::Dependency.new(d.name, reqs) + end + begin + checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") do + Digest(:SHA256).file("#{gem_repo}/gems/#{spec.original_name}.gem").hexdigest + end + rescue StandardError + checksum = nil + end + CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, checksum, nil, + deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s) + end + CompactIndex::Gem.new(name, gem_versions) + end + end + end + end + + get "/names" do + etag_response do + CompactIndex.names(gems.map(&:name)) + end + end + + get "/versions" do + etag_response do + file = tmp("versions.list") + FileUtils.rm_f(file) + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + file.contents + end + end + + get "/info/:name" do + etag_response do + gem = gems.find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end +end diff --git a/spec/bundler/support/artifice/helpers/compact_index_extra.rb b/spec/bundler/support/artifice/helpers/compact_index_extra.rb new file mode 100644 index 0000000000..9e742630dd --- /dev/null +++ b/spec/bundler/support/artifice/helpers/compact_index_extra.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative "compact_index" + +class CompactIndexExtra < CompactIndexAPI + get "/extra/versions" do + halt 404 + end + + get "/extra/api/v1/dependencies" do + halt 404 + end + + get "/extra/specs.4.8.gz" do + File.binread("#{gem_repo2}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.binread("#{gem_repo2}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.binread("#{gem_repo2}/gems/#{params[:id]}") + end +end diff --git a/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb b/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb new file mode 100644 index 0000000000..d9a7d83d23 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require_relative "compact_index" + +class CompactIndexExtraApi < CompactIndexAPI + get "/extra/names" do + etag_response do + CompactIndex.names(gems(gem_repo4).map(&:name)) + end + end + + get "/extra/versions" do + etag_response do + file = tmp("versions.list") + FileUtils.rm_f(file) + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems(gem_repo4)) + file.contents + end + end + + get "/extra/info/:name" do + etag_response do + gem = gems(gem_repo4).find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end + + get "/extra/specs.4.8.gz" do + File.binread("#{gem_repo4}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.binread("#{gem_repo4}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.binread("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.binread("#{gem_repo4}/gems/#{params[:id]}") + end +end diff --git a/spec/bundler/support/artifice/helpers/endpoint.rb b/spec/bundler/support/artifice/helpers/endpoint.rb new file mode 100644 index 0000000000..83ba1be0fc --- /dev/null +++ b/spec/bundler/support/artifice/helpers/endpoint.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require_relative "../../path" + +$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gem_path.join("gems/{mustermann,rack,tilt,sinatra,ruby2_keywords,base64}-*/lib")].map(&:to_s)) + +require "sinatra/base" + +ALL_REQUESTS = [] # rubocop:disable Style/MutableConstant +ALL_REQUESTS_MUTEX = Thread::Mutex.new + +at_exit do + if expected = ENV["BUNDLER_SPEC_ALL_REQUESTS"] + expected = expected.split("\n").sort + actual = ALL_REQUESTS.sort + + unless expected == actual + raise "Unexpected requests!\nExpected:\n\t#{expected.join("\n\t")}\n\nActual:\n\t#{actual.join("\n\t")}" + end + end +end + +class Endpoint < Sinatra::Base + def self.all_requests + @all_requests ||= [] + end + + set :raise_errors, true + set :show_exceptions, false + + def call!(*) + super.tap do + ALL_REQUESTS_MUTEX.synchronize do + ALL_REQUESTS << @request.url + end + end + end + + helpers do + include Spec::Path + + def default_gem_repo + if ENV["BUNDLER_SPEC_GEM_REPO"] + Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"]) + else + case request.host + when "gem.repo1" + Spec::Path.gem_repo1 + when "gem.repo2" + Spec::Path.gem_repo2 + when "gem.repo3" + Spec::Path.gem_repo3 + when "gem.repo4" + Spec::Path.gem_repo4 + else + Spec::Path.gem_repo1 + end + end + end + + def dependencies_for(gem_names, gem_repo = default_gem_repo) + return [] if gem_names.nil? || gem_names.empty? + + all_specs = %w[specs.4.8 prerelease_specs.4.8].map do |filename| + Marshal.load(File.open(gem_repo.join(filename)).read) + end.inject(:+) + + all_specs.map do |name, version, platform| + spec = load_spec(name, version, platform, gem_repo) + next unless gem_names.include?(spec.name) + { + name: spec.name, + number: spec.version.version, + platform: spec.platform.to_s, + dependencies: spec.runtime_dependencies.map do |dep| + [dep.name, dep.requirement.requirements.map {|a| a.join(" ") }.join(", ")] + end, + } + end.compact + end + + def load_spec(name, version, platform, gem_repo) + full_name = "#{name}-#{version}" + full_name += "-#{platform}" if platform != "ruby" + Marshal.load(Bundler.rubygems.inflate(File.binread(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")))) + end + end + + get "/quick/Marshal.4.8/:id" do + redirect "/fetch/actual/gem/#{params[:id]}" + end + + get "/fetch/actual/gem/:id" do + File.binread("#{default_gem_repo}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/gems/:id" do + File.binread("#{default_gem_repo}/gems/#{params[:id]}") + end + + get "/api/v1/dependencies" do + Marshal.dump(dependencies_for(params[:gems])) + end + + get "/specs.4.8.gz" do + File.binread("#{default_gem_repo}/specs.4.8.gz") + end + + get "/prerelease_specs.4.8.gz" do + File.binread("#{default_gem_repo}/prerelease_specs.4.8.gz") + end +end diff --git a/spec/bundler/support/artifice/helpers/endpoint_extra.rb b/spec/bundler/support/artifice/helpers/endpoint_extra.rb new file mode 100644 index 0000000000..ad08495b50 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/endpoint_extra.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative "endpoint" + +class EndpointExtra < Endpoint + get "/extra/api/v1/dependencies" do + halt 404 + end + + get "/extra/specs.4.8.gz" do + File.binread("#{gem_repo2}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.binread("#{gem_repo2}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.binread("#{gem_repo2}/gems/#{params[:id]}") + end +end diff --git a/spec/bundler/support/artifice/helpers/endpoint_fallback.rb b/spec/bundler/support/artifice/helpers/endpoint_fallback.rb new file mode 100644 index 0000000000..a232930b67 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/endpoint_fallback.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative "endpoint" + +class EndpointFallback < Endpoint + DEPENDENCY_LIMIT = 60 + + get "/api/v1/dependencies" do + if params[:gems] && params[:gems].size <= DEPENDENCY_LIMIT + Marshal.dump(dependencies_for(params[:gems])) + else + halt 413, "Too many gems to resolve, please request less than #{DEPENDENCY_LIMIT} gems" + end + end +end diff --git a/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb b/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb new file mode 100644 index 0000000000..c409d39d99 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require_relative "endpoint_fallback" + +class EndpointMarshalFail < EndpointFallback + get "/api/v1/dependencies" do + "f0283y01hasf" + end +end diff --git a/spec/bundler/support/artifice/helpers/rack_request.rb b/spec/bundler/support/artifice/helpers/rack_request.rb new file mode 100644 index 0000000000..f419bacb8c --- /dev/null +++ b/spec/bundler/support/artifice/helpers/rack_request.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require "rack/test" +require "bundler/vendored_net_http" + +module Artifice + module Net + # This is an internal object that can receive Rack requests + # to the application using the Rack::Test API + class RackRequest + include Rack::Test::Methods + attr_reader :app + + def initialize(app) + @app = app + end + end + + class HTTP < ::Gem::Net::HTTP + class << self + attr_accessor :endpoint + end + + # Gem::Net::HTTP uses a @newimpl instance variable to decide whether + # to use a legacy implementation. Since we are subclassing + # Gem::Net::HTTP, we must set it + @newimpl = true + + # We don't need to connect, so blank out this method + def connect + end + + # Replace the Gem::Net::HTTP request method with a method + # that converts the request into a Rack request and + # dispatches it to the Rack endpoint. + # + # @param [Net::HTTPRequest] req A Gem::Net::HTTPRequest + # object, or one if its subclasses + # @param [optional, String, #read] body This should + # be sent as "rack.input". If it's a String, it will + # be converted to a StringIO. + # @return [Net::HTTPResponse] + # + # @yield [Net::HTTPResponse] If a block is provided, + # this method will yield the Gem::Net::HTTPResponse to + # it after the body is read. + def request(req, body = nil, &block) + rack_request = RackRequest.new(self.class.endpoint) + + req.each_header do |header, value| + rack_request.header(header, value) + end + + scheme = use_ssl? ? "https" : "http" + prefix = "#{scheme}://#{addr_port}" + body_stream_contents = req.body_stream.read if req.body_stream + + response = rack_request.request("#{prefix}#{req.path}", + { method: req.method, input: body || req.body || body_stream_contents }) + + make_net_http_response(response, &block) + end + + private + + # This method takes a Rack response and creates a Gem::Net::HTTPResponse + # Instead of trying to mock HTTPResponse directly, we just convert + # the Rack response into a String that looks like a normal HTTP + # response and call Gem::Net::HTTPResponse.read_new + # + # @param [Array(#to_i, Hash, #each)] response a Rack response + # @return [Net::HTTPResponse] + # @yield [Net::HTTPResponse] If a block is provided, yield the + # response to it after the body is read + def make_net_http_response(response) + status = response.status + headers = response.headers + body = response.body + + response_string = [] + response_string << "HTTP/1.1 #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}" + + headers.each do |header, value| + response_string << "#{header}: #{value}" + end + + response_string << "" << body + + response_io = ::Gem::Net::BufferedIO.new(StringIO.new(response_string.join("\n"))) + res = ::Gem::Net::HTTPResponse.read_new(response_io) + + res.reading_body(response_io, true) do + yield res if block_given? + end + + res + end + end + end +end diff --git a/spec/bundler/support/artifice/vcr.rb b/spec/bundler/support/artifice/vcr.rb index ceb133346f..7b9a8bdeaf 100644 --- a/spec/bundler/support/artifice/vcr.rb +++ b/spec/bundler/support/artifice/vcr.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true -require "net/http" +require "bundler/vendored_net_http" require_relative "../path" -CASSETTE_PATH = "#{Spec::Path.spec_dir}/support/artifice/vcr_cassettes" +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) @@ -22,6 +23,10 @@ class BundlerVCRHTTP < Net::HTTP @__vcr_request_handler = handler end + File.open(USED_CASSETTES_PATH, "a+") do |f| + f.puts request_pair_paths.map {|path| Pathname.new(path).relative_path_from(Spec::Path.source_root).to_s }.join("\n") + end + if recorded_response? recorded_response else @@ -36,13 +41,13 @@ 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 response.reading_body(response_io, request.response_body_permitted?) do - response_block.call(response) if response_block + response_block&.call(response) end end end @@ -74,25 +79,8 @@ class BundlerVCRHTTP < Net::HTTP def request_pair_paths %w[request response].map do |kind| - File.join(CASSETTE_PATH, CASSETTE_NAME, file_name_for_key(key + [kind])) - end - end - - def read_stored_request(path) - contents = File.binread(path) - headers = {} - method = nil - path = nil - contents.lines.grep(/^> /).each do |line| - if line =~ /^> (GET|HEAD|POST|PATCH|PUT|DELETE) (.*)/ - method = $1 - path = $2.strip - elsif line =~ /^> (.*?): (.*)/ - headers[$1] = $2 - end + File.join(CASSETTE_PATH, CASSETTE_NAME, file_name_for_key(key), kind) end - body = contents =~ /^([^>].*)/m && $1 - Net::HTTP.const_get(method.capitalize).new(path, headers).tap {|r| r.body = body if body } end def request_to_string(request) @@ -158,8 +146,7 @@ class BundlerVCRHTTP < Net::HTTP alias_method :request, :request_with_vcr end -# Replace Net::HTTP with our VCR subclass -::Net.class_eval do - remove_const(:HTTP) - const_set(:HTTP, BundlerVCRHTTP) -end +require_relative "helpers/artifice" + +# Replace Gem::Net::HTTP with our VCR subclass +Artifice.replace_net_http(BundlerVCRHTTP) diff --git a/spec/bundler/support/artifice/windows.rb b/spec/bundler/support/artifice/windows.rb index 674470688d..fea991c071 100644 --- a/spec/bundler/support/artifice/windows.rb +++ b/spec/bundler/support/artifice/windows.rb @@ -2,13 +2,10 @@ require_relative "../path" -$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gem_path.join("gems/{artifice,mustermann,rack,tilt,sinatra,ruby2_keywords}-*/lib")].map(&:to_s)) +$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gem_path.join("gems/{mustermann,rack,tilt,sinatra,ruby2_keywords,base64}-*/lib")].map(&:to_s)) -require "artifice" require "sinatra/base" -Artifice.deactivate - class Windows < Sinatra::Base set :raise_errors, true set :show_exceptions, false @@ -43,4 +40,6 @@ class Windows < Sinatra::Base end end +require_relative "helpers/artifice" + Artifice.activate_with(Windows) diff --git a/spec/bundler/support/build_metadata.rb b/spec/bundler/support/build_metadata.rb index 98d8ac23c8..5898e7f3bd 100644 --- a/spec/bundler/support/build_metadata.rb +++ b/spec/bundler/support/build_metadata.rb @@ -10,20 +10,20 @@ module Spec def write_build_metadata(dir: source_root) 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: loaded_gemspec.date.utc.strftime("%Y-%m-%d"), + release: true, } - 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, + release: false, } - replace_build_metadata(build_metadata, dir: dir) # rubocop:disable Style/HashSyntax + replace_build_metadata(build_metadata, dir: dir) end private @@ -41,7 +41,7 @@ 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" : sys_exec("git rev-parse --short HEAD", dir: source_root).strip end extend self diff --git a/spec/bundler/support/builders.rb b/spec/bundler/support/builders.rb index aca31638ac..8f646b9358 100644 --- a/spec/bundler/support/builders.rb +++ b/spec/bundler/support/builders.rb @@ -17,16 +17,8 @@ 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 + def rake_version + "13.2.1" end def build_repo1 @@ -61,7 +53,7 @@ module Spec 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,17 +77,17 @@ 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| + 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 = Bundler.local_platform - s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 #{Bundler.local_platform}'" + s.platform = Gem::Platform.local + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 #{Gem::Platform.local}'" end build_gem "platform_specific" do |s| @@ -110,15 +102,27 @@ module Spec build_gem "platform_specific" do |s| s.platform = "x86-mswin32" - s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 MSWIN'" + 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'" + 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'" end build_gem "platform_specific" do |s| @@ -191,27 +195,25 @@ module Spec end end - def build_repo2(&blk) + def build_repo2(**kwargs, &blk) FileUtils.rm_rf gem_repo2 FileUtils.cp_r gem_repo1, gem_repo2 - update_repo2(&blk) if block_given? + 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) + def build_repo4(**kwargs, &blk) FileUtils.rm_rf gem_repo4 - build_repo(gem_repo4, &blk) + build_repo(gem_repo4, **kwargs, &blk) end def update_repo4(&blk) update_repo(gem_repo4, &blk) 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 build_security_repo @@ -229,12 +231,12 @@ module Spec end end - def build_repo(path, &blk) + def build_repo(path, **kwargs, &blk) return if File.directory?(path) FileUtils.mkdir_p("#{path}/gems") - update_repo(path, &blk) + update_repo(path,**kwargs, &blk) end def check_test_gems! @@ -251,7 +253,7 @@ module Spec end end - def update_repo(path) + def update_repo(path, build_compact_index: true) if path == gem_repo1 && caller.first.split(" ").last == "`build_repo`" raise "Updating gem_repo1 is unsupported -- use gem_repo2 instead" end @@ -260,7 +262,12 @@ module Spec @_build_repo = File.basename(path) yield with_gem_path_as Path.base_system_gem_path do - gem_command :generate_index, :dir => path + Dir[Spec::Path.base_system_gem_path.join("gems/rubygems-generate_index*/lib")].first || + raise("Could not find rubygems-generate_index lib directory in #{Spec::Path.base_system_gem_path}") + + command = "generate_index" + command += " --no-compact" if !build_compact_index && gem_command(command + " --help").include?("--[no-]compact") + gem_command command, dir: path end ensure @_build_path = nil @@ -286,14 +293,14 @@ module Spec end end - def build_dep(name, requirements = Gem::Requirement.default, type = :runtime) - Bundler::Dependency.new(name, :version => requirements) - end - def build_lib(name, *args, &blk) build_with(LibBuilder, name, args, &blk) end + def build_bundler(*args, &blk) + build_with(BundlerBuilder, "bundler", args, &blk) + end + def build_gem(name, *args, &blk) build_with(GemBuilder, name, args, &blk) end @@ -399,6 +406,49 @@ module Spec alias_method :dep, :runtime end + class BundlerBuilder + attr_writer :required_ruby_version + + def initialize(context, name, version) + raise "can only build bundler" unless name == "bundler" + + @context = context + @version = version || Bundler::VERSION + end + + def _build(options = {}) + full_name = "bundler-#{@version}" + 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 shipped_file, target_shipped_file, preserve: true + end + + @context.replace_version_file(@version, dir: build_path) + @context.replace_required_ruby_version(@required_ruby_version, dir: build_path) if @required_ruby_version + + Spec::BuildMetadata.write_build_metadata(dir: build_path) + + @context.gem_command "build #{@context.relative_gemspec}", dir: build_path + + if block_given? + yield(bundler_path) + else + FileUtils.mv bundler_path, options[:path] + end + ensure + build_path.rmtree + end + end + class LibBuilder def initialize(context, name, version) @context = context @@ -444,9 +494,6 @@ module Spec write "ext/extconf.rb", <<-RUBY require "mkmf" - - # exit 1 unless with_config("simple") - extension_name = "#{name}_c" if extra_lib_dir = with_config("ext-lib") # add extra libpath if --with-ext-lib is @@ -471,7 +518,6 @@ module Spec if options[:rubygems_version] @spec.rubygems_version = options[:rubygems_version] - def @spec.mark_version; end def @spec.validate(*); end end @@ -484,7 +530,7 @@ module Spec end @spec.authors = ["no one"] - @spec.files = @files.keys + @spec.files += @files.keys case options[:gemspec] when false @@ -525,10 +571,10 @@ module Spec class GitBuilder < LibBuilder def _build(options) - default_branch = options[:default_branch] || "master" + 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) @@ -542,7 +588,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 @@ -567,7 +613,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 @@ -589,7 +635,8 @@ module Spec class GemBuilder < LibBuilder def _build(opts) - lib_path = super(opts.merge(:path => @context.tmp(".tmp/#{@spec.full_name}"), :no_default => opts[:no_default])) + lib_path = opts[:lib_path] || @context.tmp(".tmp/#{@spec.full_name}") + lib_path = super(opts.merge(path: lib_path, no_default: opts[:no_default])) destination = opts[:path] || _default_path FileUtils.mkdir_p(lib_path.join(destination)) @@ -598,16 +645,16 @@ module Spec Bundler.rubygems.build(@spec, opts[:skip_validation]) end elsif opts[:skip_validation] - @context.gem_command "build --force #{@spec.name}", :dir => lib_path + @context.gem_command "build --force #{@spec.name}", dir: lib_path else - @context.gem_command "build #{@spec.name}", :dir => lib_path + @context.gem_command "build #{@spec.name}", dir: lib_path 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 @@ -627,7 +674,7 @@ module Spec end end - TEST_CERT = <<-CERT.gsub(/^\s*/, "") + TEST_CERT = <<~CERT -----BEGIN CERTIFICATE----- MIIDMjCCAhqgAwIBAgIBATANBgkqhkiG9w0BAQUFADAnMQwwCgYDVQQDDAN5b3Ux FzAVBgoJkiaJk/IsZAEZFgdleGFtcGxlMB4XDTE1MDIwODAwMTIyM1oXDTQyMDYy @@ -650,7 +697,7 @@ module Spec -----END CERTIFICATE----- CERT - TEST_PKEY = <<-PKEY.gsub(/^\s*/, "") + TEST_PKEY = <<~PKEY -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA2W8V2k3jdzgMxL0mjTqbRruTdtDcdZDXKtiFkyLvsXUXvc2k GSdgcjMOS1CkafqGz/hAUlPibjM0QEXjtQuMdTmdMrmuORLeeIZhSO+HdkTNV6j3 diff --git a/spec/bundler/support/bundle.rb b/spec/bundler/support/bundle.rb index bb21526d35..5d6d658040 100644 --- a/spec/bundler/support/bundle.rb +++ b/spec/bundler/support/bundle.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require "rubygems" -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) +require_relative "activate" + load File.expand_path("bundle", Spec::Path.bindir) diff --git a/spec/bundler/support/checksums.rb b/spec/bundler/support/checksums.rb new file mode 100644 index 0000000000..f758559b3b --- /dev/null +++ b/spec/bundler/support/checksums.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +module Spec + module Checksums + class ChecksumsBuilder + 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) + name_tuple = Gem::NameTuple.new(name, version, platform) + gem_file = File.join(repo, "gems", "#{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, &block) + ChecksumsBuilder.new(enabled, &block) + end + + def checksums_section_when_existing(&block) + begin + enabled = lockfile.match?(/^CHECKSUMS$/) + rescue Errno::ENOENT + enabled = false + end + checksums_section(enabled, &block) + end + + def checksum_to_lock(*args) + checksums_section 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 + end +end diff --git a/spec/bundler/support/command_execution.rb b/spec/bundler/support/command_execution.rb index 68e5c56c75..5639fda3b6 100644 --- a/spec/bundler/support/command_execution.rb +++ b/spec/bundler/support/command_execution.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Spec - CommandExecution = Struct.new(:command, :working_directory, :exitstatus, :stdout, :stderr) do + CommandExecution = Struct.new(:command, :working_directory, :exitstatus, :original_stdout, :original_stderr) do def to_s "$ #{command}" end @@ -11,6 +11,19 @@ module Spec @stdboth ||= [stderr, stdout].join("\n").strip end + def stdout + original_stdout + end + + # Can be removed once/if https://github.com/oneclick/rubyinstaller2/pull/369 is resolved + def stderr + return original_stderr unless Gem.win_platform? + + original_stderr.split("\n").reject do |l| + l.include?("operating_system_defaults") + end.join("\n") + end + def to_s_verbose [ to_s, diff --git a/spec/bundler/support/filters.rb b/spec/bundler/support/filters.rb index 96f2e2a274..8e164af756 100644 --- a/spec/bundler/support/filters.rb +++ b/spec/bundler/support/filters.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative "sudo" - class RequirementChecker < Proc def self.against(present) provided = Gem::Version.new(present) @@ -21,21 +19,20 @@ class RequirementChecker < Proc end RSpec.configure do |config| - config.filter_run_excluding :sudo => true - config.filter_run_excluding :realworld => true - - git_version = Bundler::Source::Git::GitProxy.new(nil, nil, nil).version - - 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 => RUBY_ENGINE != "jruby" - config.filter_run_excluding :truffleruby => RUBY_ENGINE != "truffleruby" - config.filter_run_excluding :man => Gem.win_platform? + config.filter_run_excluding realworld: true + + git_version = Bundler::Source::Git::GitProxy.new(nil, nil).version + + 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? config.filter_run_when_matching :focus unless ENV["CI"] end diff --git a/spec/bundler/support/hax.rb b/spec/bundler/support/hax.rb index 3d97b93c5a..c7fe3637cc 100644 --- a/spec/bundler/support/hax.rb +++ b/spec/bundler/support/hax.rb @@ -1,5 +1,10 @@ # frozen_string_literal: true +if ENV["BUNDLER_SPEC_RUBY_PLATFORM"] + Object.send(:remove_const, :RUBY_PLATFORM) + RUBY_PLATFORM = ENV["BUNDLER_SPEC_RUBY_PLATFORM"] +end + module Gem def self.ruby=(ruby) @ruby = ruby @@ -14,21 +19,35 @@ module Gem @default_specifications_dir = nil end + if ENV["BUNDLER_SPEC_WINDOWS"] + @@win_platform = true # rubocop:disable Style/ClassVars + end + if ENV["BUNDLER_SPEC_PLATFORM"] + previous_platforms = @platforms + previous_local = Platform.local + class Platform @local = new(ENV["BUNDLER_SPEC_PLATFORM"]) end - @platforms = [Gem::Platform::RUBY, Gem::Platform.local] + @platforms = previous_platforms.map {|platform| platform == previous_local ? Platform.local : platform } end if ENV["BUNDLER_SPEC_GEM_SOURCES"] self.sources = [ENV["BUNDLER_SPEC_GEM_SOURCES"]] end - # We only need this hack for rubygems versions without the BundlerVersionFinder - if Gem.rubygems_version < Gem::Version.new("2.7.0") - @path_to_default_spec_map.delete_if do |_path, spec| - spec.name == "bundler" + if ENV["BUNDLER_IGNORE_DEFAULT_GEM"] + module RemoveDefaultBundlerStub + def default_stubs(pattern = "*") + super.delete_if {|stub| stub.name == "bundler" } + end + end + + class Specification + class << self + prepend RemoveDefaultBundlerStub + end end end end diff --git a/spec/bundler/support/helpers.rb b/spec/bundler/support/helpers.rb index e995418a4e..1ad9cc78ca 100644 --- a/spec/bundler/support/helpers.rb +++ b/spec/bundler/support/helpers.rb @@ -43,7 +43,7 @@ module Spec last_command.stderr end - MAJOR_DEPRECATION = /^\[DEPRECATED\]\s*/.freeze + MAJOR_DEPRECATION = /^\[DEPRECATED\]\s*/ def err_without_deprecations err.gsub(/#{MAJOR_DEPRECATION}.+[\n]?/, "") @@ -60,7 +60,7 @@ module Spec 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 @@ -78,9 +78,6 @@ module Spec end def bundle(cmd, options = {}, &block) - with_sudo = options.delete(:sudo) - sudo = with_sudo == :preserve_env ? "sudo -E --preserve-env=RUBYOPT" : "sudo" if with_sudo - bundle_bin = options.delete(:bundle_bin) bundle_bin ||= installed_bindir.join("bundle") @@ -119,9 +116,9 @@ module Spec end end.join - ruby_cmd = build_ruby_cmd({ :sudo => sudo, :load_path => load_path, :requires => requires }) + ruby_cmd = build_ruby_cmd({ load_path: load_path, requires: requires, env: env }) cmd = "#{ruby_cmd} #{bundle_bin} #{cmd}#{args}" - sys_exec(cmd, { :env => env, :dir => dir, :raise_on_error => raise_on_error }, &block) + sys_exec(cmd, { env: env, dir: dir, raise_on_error: raise_on_error }, &block) end def bundler(cmd, options = {}) @@ -146,16 +143,20 @@ module Spec end def build_ruby_cmd(options = {}) - sudo = options.delete(:sudo) - libs = options.delete(:load_path) lib_option = libs ? "-I#{libs.join(File::PATH_SEPARATOR)}" : [] requires = options.delete(:requires) || [] - requires << "#{Path.spec_dir}/support/hax.rb" + + hax_path = "#{Path.spec_dir}/support/hax.rb" + + # For specs that need to ignore the default Bundler gem, load hax before + # anything else since other stuff may actually load bundler and not skip + # the default version + options[:env]&.include?("BUNDLER_IGNORE_DEFAULT_GEM") ? requires.prepend(hax_path) : requires.append(hax_path) require_option = requires.map {|r| "-r#{r}" } - [sudo, Gem.ruby, *lib_option, *require_option].compact.join(" ") + [Gem.ruby, *lib_option, *require_option].compact.join(" ") end def gembin(cmd, options = {}) @@ -175,7 +176,7 @@ module Spec end def git(cmd, path, options = {}) - sys_exec("git #{cmd}", options.merge(:dir => path)) + sys_exec("git #{cmd}", options.merge(dir: path)) end def sys_exec(cmd, options = {}) @@ -186,14 +187,14 @@ module Spec require "open3" require "shellwords" - Open3.popen3(env, *cmd.shellsplit, :chdir => dir) do |stdin, stdout, stderr, wait_thr| + 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 + command_execution.original_stdout = stdout_read_thread.value.strip + command_execution.original_stderr = stderr_read_thread.value.strip status = wait_thr.value command_execution.exitstatus = if status.exited? @@ -249,7 +250,7 @@ module Spec contents = args.pop if contents.nil? - File.open(bundled_app_gemfile, "r", &:read) + read_gemfile else create_file(args.pop || "Gemfile", contents) end @@ -259,12 +260,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] || "" @@ -286,59 +299,35 @@ module Spec 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 + with_gem_path_as(install_dir) 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 gem_name =~ %r{\A(?:[a-zA-Z]:)?/.*\.gem\z} - install_gem(gem_name, default) + with_built_bundler(version) {|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 - install_gem("#{gem_repo}/gems/#{gem_name}.gem", default) + install_gem("#{gem_repo}/gems/#{gem_name}.gem", install_dir, default) end end end end - def install_gem(path, default = false) + def install_gem(path, install_dir, default = false) raise "OMG `#{path}` does not exist!" unless File.exist?(path) - args = "--no-document --ignore-dependencies" - args += " --default --install-dir #{system_gem_path}" if default + args = "--no-document --ignore-dependencies --verbose --local --install-dir #{install_dir}" + args += " --default" if default gem_command "install #{args} '#{path}'" 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" - - Dir.mkdir build_path - - 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 - - replace_version_file(version, dir: build_path) # rubocop:disable Style/HashSyntax - - Spec::BuildMetadata.write_build_metadata(dir: build_path) # rubocop:disable Style/HashSyntax - - gem_command "build #{relative_gemspec}", :dir => build_path - - yield(bundler_path) - ensure - build_path.rmtree - end + def with_built_bundler(version = nil, &block) + Builders::BundlerBuilder.new(self, "bundler", version)._build(&block) end def with_gem_path_as(path) @@ -419,14 +408,14 @@ module Spec 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" + path = "#{gem_repo}/gems/#{g}.gem" raise "OMG `#{path}` does not exist!" unless File.exist?(path) FileUtils.cp(path, "#{bundled_app}/vendor/cache") end @@ -437,6 +426,14 @@ module Spec pristine_system_gems :bundler end + def simulate_ruby_platform(ruby_platform) + old = ENV["BUNDLER_SPEC_RUBY_PLATFORM"] + ENV["BUNDLER_SPEC_RUBY_PLATFORM"] = ruby_platform.to_s + yield + ensure + ENV["BUNDLER_SPEC_RUBY_PLATFORM"] = old + end + def simulate_platform(platform) old = ENV["BUNDLER_SPEC_PLATFORM"] ENV["BUNDLER_SPEC_PLATFORM"] = platform.to_s @@ -445,48 +442,36 @@ module Spec ENV["BUNDLER_SPEC_PLATFORM"] = old if block_given? end - def simulate_windows(platform = mswin) + 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 + yield end - 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_WINDOWS"] = old end def current_ruby_minor - Gem.ruby_version.segments[0..1].join(".") + Gem.ruby_version.segments.tap {|s| s.delete_at(2) }.join(".") 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 + + def previous_ruby_minor + return "2.7" if ruby_major_minor == [3, 0] + + ruby_major_minor.map.with_index {|s, i| i == 1 ? s - 1 : s }.join(".") end - # versions providing a bundler version finder but 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") && Gem.rubygems_version >= Gem::Version.new("2.7.0") + def ruby_major_minor + Gem.ruby_version.segments[0..1] end def revision_for(path) - sys_exec("git rev-parse HEAD", :dir => path).strip + sys_exec("git rev-parse HEAD", dir: path).strip end def with_read_only(pattern) diff --git a/spec/bundler/support/indexes.rb b/spec/bundler/support/indexes.rb index 638f394e76..086a311551 100644 --- a/spec/bundler/support/indexes.rb +++ b/spec/bundler/support/indexes.rb @@ -14,22 +14,25 @@ module Spec alias_method :platforms, :platform - def resolve(args = []) + def resolve(args = [], dependency_api_available: true) @platforms ||= ["ruby"] - deps = [] - default_source = instance_double("Bundler::Source::Rubygems", :specs => @index, :to_s => "locally install gems") - source_requirements = { :default => default_source } + 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| - source_requirements[d.name] = d.source = default_source - @platforms.each do |p| - deps << Bundler::DepProxy.get_proxy(d, p) - end + name = d.name + source_requirements[name] = d.source = default_source end - args[0] ||= [] # base - args[1] ||= Bundler::GemVersionPromoter.new # gem_version_promoter - args[2] ||= [] # additional_base_requirements - args[3] ||= @platforms # platforms - Bundler::Resolver.resolve(deps, source_requirements, *args) + packages = Bundler::Resolver::Base.new(source_requirements, @deps, base, @platforms, locked_specs: originally_locked, unlock: unlock) + Bundler::Resolver.new(packages, gem_version_promoter).start + end + + def should_not_resolve + expect { resolve }.to raise_error(Bundler::GemNotFound) end def should_resolve_as(specs) @@ -38,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 @@ -46,13 +55,6 @@ module Spec end end - def should_conflict_on(names) - got = resolve - raise "The resolve succeeded with: #{got.map(&:full_name).sort.inspect}" - rescue Bundler::VersionConflict => e - expect(Array(names).sort).to eq(e.conflicts.sort) - end - def gem(*args, &blk) build_spec(*args, &blk).first end @@ -66,12 +68,11 @@ module Spec def should_conservative_resolve_and_include(opts, unlock, specs) # empty unlock means unlock all opts = Array(opts) - search = Bundler::GemVersionPromoter.new(@locked, unlock).tap do |s| + search = Bundler::GemVersionPromoter.new.tap do |s| s.level = opts.first s.strict = opts.include?(:strict) - s.prerelease_specified = Hash[@deps.map {|d| [d.name, d.requirement.prerelease?] }] end - should_resolve_and_include specs, [@base, search] + should_resolve_and_include specs, [@base, search, @locked, unlock] end def an_awesome_index @@ -126,7 +127,7 @@ module Spec next if version == v("1.4.2.1") && platform != pl("x86-mswin32") next if version == v("1.4.2") && platform == pl("x86-mswin32") gem "nokogiri", version, platform do - dep "weakling", ">= 0.0.3" if platform =~ pl("java") + dep "weakling", ">= 0.0.3" if platform =~ pl("java") # rubocop:disable Performance/RegexpMatch end end end @@ -303,7 +304,7 @@ module Spec end end - def a_unresovable_child_index + def a_unresolvable_child_index build_index do gem "json", %w[1.8.0] diff --git a/spec/bundler/support/matchers.rb b/spec/bundler/support/matchers.rb index 3c2a7f9f58..0f027dcf04 100644 --- a/spec/bundler/support/matchers.rb +++ b/spec/bundler/support/matchers.rb @@ -150,7 +150,7 @@ module Spec end if exitstatus == 65 actual_platform = out.split("\n").last - next "#{name} was expected to be of platform #{platform} but was #{actual_platform}" + next "#{name} was expected to be of platform #{platform || "ruby"} but was #{actual_platform || "ruby"}" end if exitstatus == 66 actual_source = out.split("\n").last @@ -178,7 +178,7 @@ module Spec begin require '#{name}' - name_constant = '#{Spec::Builders.constantize(name)}' + name_constant = #{Spec::Builders.constantize(name)} if #{version.nil?} || name_constant == '#{version}' exit 64 else diff --git a/spec/bundler/support/path.rb b/spec/bundler/support/path.rb index 41b36997b2..7352d5a353 100644 --- a/spec/bundler/support/path.rb +++ b/spec/bundler/support/path.rb @@ -71,10 +71,6 @@ module Spec @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") - end - def man_dir @man_dir ||= lib_dir.join("bundler/man") end @@ -84,7 +80,13 @@ module Spec 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 @@ -118,6 +120,14 @@ module Spec end 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 + end + def bundled_app(*path) root = tmp.join("bundled_app") FileUtils.mkdir_p(root) @@ -221,13 +231,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 @@ -247,6 +250,13 @@ module Spec File.open(version_file, "w") {|f| f << contents } end + def replace_required_ruby_version(version, dir:) + gemspec_file = File.expand_path("bundler.gemspec", dir) + contents = File.read(gemspec_file) + contents.sub!(/(^\s+s\.required_ruby_version\s*=\s*)"[^"]+"/, %(\\1"#{version}")) + File.open(gemspec_file, "w") {|f| f << contents } + end + def ruby_core? # avoid to warnings @ruby_core ||= nil @@ -258,16 +268,20 @@ module Spec end end + def git_root + ruby_core? ? source_root : source_root.parent + end + private 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") + sys_exec("git ls-files -z -- #{glob}", dir: 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 spec CHANGELOG.md LICENSE.md README.md bundler.gemspec" end def lib_tracked_files_glob @@ -278,38 +292,24 @@ module Spec ruby_core? ? "man/bundle* man/gemfile*" : "lib/bundler/man/bundle*.1 lib/bundler/man/gemfile*.5" end - def git_root - ruby_core? ? source_root : source_root.parent - end - def ruby_core_tarball? !git_root.join(".git").directory? end def rubocop_gemfile_basename - filename = if RUBY_VERSION.start_with?("2.3") - "rubocop23_gems" - elsif RUBY_VERSION.start_with?("2.4") - "rubocop24_gems" - else - "rubocop_gems" - end - tool_dir.join("#{filename}.rb") + tool_dir.join("rubocop_gems.rb") end def standard_gemfile_basename - filename = if RUBY_VERSION.start_with?("2.3") - "standard23_gems" - elsif RUBY_VERSION.start_with?("2.4") - "standard24_gems" - else - "standard_gems" - end - tool_dir.join("#{filename}.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 + lib_dir.join("bundler", "templates") end extend self diff --git a/spec/bundler/support/platforms.rb b/spec/bundler/support/platforms.rb index 07973fd727..526e1c09a9 100644 --- a/spec/bundler/support/platforms.rb +++ b/spec/bundler/support/platforms.rb @@ -21,31 +21,35 @@ module Spec end def linux - Gem::Platform.new(["x86", "linux", nil]) + Gem::Platform.new("x86_64-linux") end - def mswin + def x86_mswin32 Gem::Platform.new(["x86", "mswin32", nil]) end - def mingw + def x64_mswin64 + Gem::Platform.new(["x64", "mswin64", nil]) + end + + def x86_mingw32 Gem::Platform.new(["x86", "mingw32", nil]) end - def x64_mingw + def x64_mingw32 Gem::Platform.new(["x64", "mingw32", nil]) end - def all_platforms - [rb, java, linux, mswin, mingw, x64_mingw] + def x64_mingw_ucrt + Gem::Platform.new(["x64", "mingw", "ucrt"]) end - def local - generic_local_platform + def windows_platforms + [x86_mswin32, x64_mswin64, x86_mingw32, x64_mingw32, x64_mingw_ucrt] end - def specific_local_platform - Bundler.local_platform + def all_platforms + [rb, java, linux, windows_platforms].flatten end def not_local @@ -55,13 +59,15 @@ module Spec def local_tag if RUBY_PLATFORM == "java" :jruby + elsif ["x64-mingw32", "x64-mingw-ucrt"].include?(RUBY_PLATFORM) + :windows else :ruby end end def not_local_tag - [:ruby, :jruby].find {|tag| tag != local_tag } + [:jruby, :windows, :ruby].find {|tag| tag != local_tag } end def local_ruby_engine @@ -69,12 +75,12 @@ module Spec end def local_engine_version - RUBY_ENGINE_VERSION + RUBY_ENGINE == "ruby" ? Gem.ruby_version : RUBY_ENGINE_VERSION end def not_local_engine_version case not_local_tag - when :ruby + when :ruby, :windows not_local_ruby_version when :jruby "1.6.1" @@ -89,16 +95,17 @@ module Spec 9999 end - def lockfile_platforms - lockfile_platforms_for(local_platforms) + def default_platform_list(*extra, defaults: default_locked_platforms) + defaults.concat(extra).uniq end - def lockfile_platforms_for(platforms) + def lockfile_platforms(*extra, defaults: default_locked_platforms) + platforms = default_platform_list(*extra, defaults: defaults) platforms.map(&:to_s).sort.join("\n ") end - def local_platforms - [specific_local_platform] + def default_locked_platforms + [local_platform, generic_local_platform] end end end diff --git a/spec/bundler/support/rubygems_ext.rb b/spec/bundler/support/rubygems_ext.rb index fd5f06b14f..889ebc90c3 100644 --- a/spec/bundler/support/rubygems_ext.rb +++ b/spec/bundler/support/rubygems_ext.rb @@ -18,9 +18,15 @@ module Spec gem_load_and_activate(gem_name, bin_container) end - def gem_require(gem_name) + def gem_load_and_possibly_install(gem_name, bin_container) + require_relative "switch_rubygems" + + gem_load_activate_and_possibly_install(gem_name, bin_container) + end + + def gem_require(gem_name, entrypoint) gem_activate(gem_name) - require gem_name + require entrypoint end def test_setup @@ -32,6 +38,10 @@ module Spec FileUtils.mkdir_p(Path.tmpdir) ENV["HOME"] = Path.home.to_s + # Remove "RUBY_CODESIGN", which is used by mkmf-generated Makefile to + # sign extension bundles on macOS, to avoid trying to find the specified key + # from the fake $HOME/Library/Keychains directory. + ENV.delete "RUBY_CODESIGN" ENV["TMPDIR"] = Path.tmpdir.to_s require "rubygems/user_interaction" @@ -80,7 +90,7 @@ module Spec puts success_message puts else - system("git status --porcelain") + system("git diff") puts puts error_message @@ -99,9 +109,22 @@ module Spec abort "We couldn't activate #{gem_name} (#{e.requirement}). Run `gem install #{gem_name}:'#{e.requirement}'`" end + def gem_load_activate_and_possibly_install(gem_name, bin_container) + gem_activate_and_possibly_install(gem_name) + load Gem.bin_path(gem_name, bin_container) + end + + def gem_activate_and_possibly_install(gem_name) + gem_activate(gem_name) + rescue Gem::LoadError => e + Gem.install(gem_name, e.requirement) + retry + end + def gem_activate(gem_name) + require_relative "activate" require "bundler" - gem_requirement = Bundler::LockfileParser.new(File.read(dev_lockfile)).dependencies[gem_name]&.requirement + gem_requirement = Bundler::LockfileParser.new(File.read(dev_lockfile)).specs.find {|spec| spec.name == gem_name }.version gem gem_name, gem_requirement end @@ -119,8 +142,8 @@ module Spec ENV["BUNDLE_PATH__SYSTEM"] = "true" end - output = `#{Gem.ruby} #{File.expand_path("support/bundle.rb", Path.spec_dir)} install --verbose` - raise "Error when installing gems in #{gemfile}: #{output}" unless $?.success? + 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 diff --git a/spec/bundler/support/rubygems_version_manager.rb b/spec/bundler/support/rubygems_version_manager.rb index d1b1f8dd03..88da14b67e 100644 --- a/spec/bundler/support/rubygems_version_manager.rb +++ b/spec/bundler/support/rubygems_version_manager.rb @@ -30,11 +30,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? @@ -66,7 +65,7 @@ class RubygemsVersionManager def switch_local_copy_if_needed return unless local_copy_switch_needed? - sys_exec("git checkout #{target_tag}", :dir => local_copy_path) + sys_exec("git checkout #{target_tag}", dir: local_copy_path) ENV["RGV"] = local_copy_path.to_s end @@ -85,7 +84,7 @@ class RubygemsVersionManager end def local_copy_tag - sys_exec("git rev-parse --abbrev-ref HEAD", :dir => local_copy_path) + sys_exec("git rev-parse --abbrev-ref HEAD", dir: local_copy_path) end def local_copy_path @@ -98,7 +97,7 @@ class RubygemsVersionManager rubygems_path = source_root.join("tmp/rubygems") unless rubygems_path.directory? - sys_exec("git clone .. #{rubygems_path}", :dir => source_root) + sys_exec("git clone .. #{rubygems_path}", dir: source_root) end rubygems_path @@ -113,7 +112,7 @@ class RubygemsVersionManager end def resolve_target_tag - return "v#{@source}" if @source.match(/^\d/) + return "v#{@source}" if @source.match?(/^\d/) @source end diff --git a/spec/bundler/support/sudo.rb b/spec/bundler/support/sudo.rb deleted file mode 100644 index 04e9443945..0000000000 --- a/spec/bundler/support/sudo.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Spec - module Sudo - def self.present? - @which_sudo ||= Bundler.which("sudo") - end - - def sudo(cmd) - raise "sudo not present" unless Sudo.present? - sys_exec("sudo #{cmd}") - end - - def chown_system_gems_to_root - sudo "chown -R root #{system_gem_path}" - end - end -end |