diff options
Diffstat (limited to 'spec/bundler/support')
51 files changed, 3372 insertions, 0 deletions
diff --git a/spec/bundler/support/artifice/compact_index.rb b/spec/bundler/support/artifice/compact_index.rb new file mode 100644 index 0000000000..9111ed8211 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +$LOAD_PATH.unshift Dir[base_system_gems.join("gems/compact_index*/lib")].first.to_s +require "compact_index" + +class CompactIndexAPI < Endpoint + helpers do + def load_spec(name, version, platform, gem_repo) + full_name = "#{name}-#{version}" + full_name += "-#{platform}" if platform != "ruby" + Marshal.load(Gem.inflate(File.open(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")).read)) + 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 => 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) + if body.respond_to?(:byteslice) + body.byteslice(range) + else # pre-1.9.3 + body.unpack("@#{range.first}a#{range.end + 1}").first + end + end + + def gems(gem_repo = 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 + nil + end + CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, checksum, nil, + deps, spec.required_ruby_version, spec.required_rubygems_version) + 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") + file.delete if file.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 + +Artifice.activate_with(CompactIndexAPI) diff --git a/spec/bundler/support/artifice/compact_index_api_missing.rb b/spec/bundler/support/artifice/compact_index_api_missing.rb new file mode 100644 index 0000000000..6d15b54b85 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_api_missing.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexApiMissing < CompactIndexAPI + get "/fetch/actual/gem/:id" do + $stderr.puts params[:id] + if params[:id] == "rack-1.0.gemspec.rz" + halt 404 + else + File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + end +end + +Artifice.activate_with(CompactIndexApiMissing) diff --git a/spec/bundler/support/artifice/compact_index_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_basic_authentication.rb new file mode 100644 index 0000000000..bffb5b9e2b --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_basic_authentication.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexBasicAuthentication < CompactIndexAPI + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + end +end + +Artifice.activate_with(CompactIndexBasicAuthentication) diff --git a/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb new file mode 100644 index 0000000000..4ac328736c --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexChecksumMismatch < CompactIndexAPI + get "/versions" do + headers "ETag" => quote("123") + headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" + content_type "text/plain" + body "" + end +end + +Artifice.activate_with(CompactIndexChecksumMismatch) diff --git a/spec/bundler/support/artifice/compact_index_concurrent_download.rb b/spec/bundler/support/artifice/compact_index_concurrent_download.rb new file mode 100644 index 0000000000..b788a852cf --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_concurrent_download.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +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.read(versions) == "" || raise("Original file should be present and empty") + + # Verify this is only requested once for a partial download + env["HTTP_RANGE"] || raise("Missing Range header for expected partial download") + + # Overwrite the file in parallel, which should be then overwritten + # after a successful download to prevent corruption + File.open(versions, "w") {|f| f.puts "another process" } + + etag_response do + file = tmp("versions.list") + file.delete if file.file? + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + file.contents + end + end +end + +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 new file mode 100644 index 0000000000..0c417f0580 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexCredsDiffHost < CompactIndexAPI + helpers do + def auth + @auth ||= Rack::Auth::Basic::Request.new(request.env) + end + + def authorized? + auth.provided? && auth.basic? && auth.credentials && auth.credentials == %w(user pass) + end + + def protected! + return if authorized? + response["WWW-Authenticate"] = %(Basic realm="Testing HTTP Auth") + throw(:halt, [401, "Not authorized\n"]) + end + end + + before do + protected! unless request.path_info.include?("/no/creds/") + end + + get "/gems/:id" do + redirect "http://diffhost.com/no/creds/#{params[:id]}" + end + + get "/no/creds/:id" do + if request.host.include?("diffhost") && !auth.provided? + File.read("#{gem_repo1}/gems/#{params[:id]}") + end + end +end + +Artifice.activate_with(CompactIndexCredsDiffHost) diff --git a/spec/bundler/support/artifice/compact_index_extra.rb b/spec/bundler/support/artifice/compact_index_extra.rb new file mode 100644 index 0000000000..8a87fc4343 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_extra.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +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.read("#{gem_repo2}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.read("#{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.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.read("#{gem_repo2}/gems/#{params[:id]}") + end +end + +Artifice.activate_with(CompactIndexExtra) diff --git a/spec/bundler/support/artifice/compact_index_extra_api.rb b/spec/bundler/support/artifice/compact_index_extra_api.rb new file mode 100644 index 0000000000..844a9ca9f2 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_extra_api.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +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") + file.delete if file.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.read("#{gem_repo4}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.read("#{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.read("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.read("#{gem_repo4}/gems/#{params[:id]}") + end +end + +Artifice.activate_with(CompactIndexExtraApi) diff --git a/spec/bundler/support/artifice/compact_index_extra_missing.rb b/spec/bundler/support/artifice/compact_index_extra_missing.rb new file mode 100644 index 0000000000..2af5ce9c27 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_extra_missing.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index_extra", __FILE__) + +Artifice.deactivate + +class CompactIndexExtraMissing < CompactIndexExtra + get "/extra/fetch/actual/gem/:id" do + if params[:id] == "missing-1.0.gemspec.rz" + halt 404 + else + File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + end +end + +Artifice.activate_with(CompactIndexExtraMissing) diff --git a/spec/bundler/support/artifice/compact_index_forbidden.rb b/spec/bundler/support/artifice/compact_index_forbidden.rb new file mode 100644 index 0000000000..b25eea94e7 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_forbidden.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexForbidden < CompactIndexAPI + get "/versions" do + halt 403 + end +end + +Artifice.activate_with(CompactIndexForbidden) diff --git a/spec/bundler/support/artifice/compact_index_host_redirect.rb b/spec/bundler/support/artifice/compact_index_host_redirect.rb new file mode 100644 index 0000000000..6c1ab2def6 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_host_redirect.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexHostRedirect < CompactIndexAPI + get "/fetch/actual/gem/:id", :host_name => "localgemserver.test" do + redirect "http://bundler.localgemserver.test#{request.path_info}" + end + + get "/versions" do + status 404 + end + + get "/api/v1/dependencies" do + status 404 + end +end + +Artifice.activate_with(CompactIndexHostRedirect) diff --git a/spec/bundler/support/artifice/compact_index_partial_update.rb b/spec/bundler/support/artifice/compact_index_partial_update.rb new file mode 100644 index 0000000000..bf6feab877 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_partial_update.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexPartialUpdate < CompactIndexAPI + # Stub the server to never return 304s. This simulates the behaviour of + # Fastly / Rubygems ignoring ETag headers. + def not_modified?(_checksum) + false + end + + get "/versions" do + cached_versions_path = File.join( + Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions" + ) + + # Verify a cached copy of the versions file exists + unless File.read(cached_versions_path).start_with?("created_at: ") + raise("Cached versions file should be present and have content") + end + + # Verify that a partial request is made, starting from the index of the + # final byte of the cached file. + unless env["HTTP_RANGE"] == "bytes=#{File.read(cached_versions_path).bytesize - 1}-" + raise("Range header should be present, and start from the index of the final byte of the cache.") + end + + etag_response do + # Return the exact contents of the cache. + File.read(cached_versions_path) + end + end +end + +Artifice.activate_with(CompactIndexPartialUpdate) diff --git a/spec/bundler/support/artifice/compact_index_redirects.rb b/spec/bundler/support/artifice/compact_index_redirects.rb new file mode 100644 index 0000000000..ff1d3e43bc --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_redirects.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexRedirect < CompactIndexAPI + get "/fetch/actual/gem/:id" do + redirect "/fetch/actual/gem/#{params[:id]}" + end + + get "/versions" do + status 404 + end + + get "/api/v1/dependencies" do + status 404 + end +end + +Artifice.activate_with(CompactIndexRedirect) diff --git a/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb new file mode 100644 index 0000000000..49a072d2b9 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexStrictBasicAuthentication < CompactIndexAPI + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + + # Only accepts password == "password" + unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz" + halt 403, "Authentication failed" + end + end +end + +Artifice.activate_with(CompactIndexStrictBasicAuthentication) diff --git a/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb new file mode 100644 index 0000000000..25935f5e5d --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexWrongDependencies < CompactIndexAPI + get "/info/:name" do + etag_response do + gem = gems.find {|g| g.name == params[:name] } + gem.versions.each {|gv| gv.dependencies.clear } if gem + CompactIndex.info(gem ? gem.versions : []) + end + end +end + +Artifice.activate_with(CompactIndexWrongDependencies) diff --git a/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb new file mode 100644 index 0000000000..3a12a59ae7 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +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 } + versions = gem ? gem.versions : [] + versions.each {|v| v.checksum = checksum } + CompactIndex.info(versions) + end + end +end + +Artifice.activate_with(CompactIndexWrongGemChecksum) diff --git a/spec/bundler/support/artifice/endopint_marshal_fail_basic_authentication.rb b/spec/bundler/support/artifice/endopint_marshal_fail_basic_authentication.rb new file mode 100644 index 0000000000..f1f8dc5700 --- /dev/null +++ b/spec/bundler/support/artifice/endopint_marshal_fail_basic_authentication.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint_marshal_fail", __FILE__) + +Artifice.deactivate + +class EndpointMarshalFailBasicAuthentication < EndpointMarshalFail + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + end +end + +Artifice.activate_with(EndpointMarshalFailBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint.rb b/spec/bundler/support/artifice/endpoint.rb new file mode 100644 index 0000000000..771d431f22 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true +require File.expand_path("../../path.rb", __FILE__) +require Spec::Path.root.join("lib/bundler/deprecate") +include Spec::Path + +$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gems.join("gems/{artifice,rack,tilt,sinatra}-*/lib")].map(&:to_s)) +require "artifice" +require "sinatra/base" + +class Endpoint < Sinatra::Base + GEM_REPO = Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"] || Spec::Path.gem_repo1) + set :raise_errors, true + set :show_exceptions, false + + helpers do + def dependencies_for(gem_names, gem_repo = GEM_REPO) + return [] if gem_names.nil? || gem_names.empty? + + require "rubygems" + require "bundler" + Bundler::Deprecate.skip_during do + 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 + end + + def load_spec(name, version, platform, gem_repo) + full_name = "#{name}-#{version}" + full_name += "-#{platform}" if platform != "ruby" + Marshal.load(Gem.inflate(File.open(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")).read)) + end + end + + get "/quick/Marshal.4.8/:id" do + redirect "/fetch/actual/gem/#{params[:id]}" + end + + get "/fetch/actual/gem/:id" do + File.read("#{GEM_REPO}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/gems/:id" do + File.read("#{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.read("#{GEM_REPO}/specs.4.8.gz") + end + + get "/prerelease_specs.4.8.gz" do + File.read("#{GEM_REPO}/prerelease_specs.4.8.gz") + end +end + +Artifice.activate_with(Endpoint) diff --git a/spec/bundler/support/artifice/endpoint_500.rb b/spec/bundler/support/artifice/endpoint_500.rb new file mode 100644 index 0000000000..993630b79e --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_500.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +require File.expand_path("../../path.rb", __FILE__) +include Spec::Path + +$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gems.join("gems/{artifice,rack,tilt,sinatra}-*/lib")].map(&:to_s)) + +require "artifice" +require "sinatra/base" + +Artifice.deactivate + +class Endpoint500 < Sinatra::Base + before do + halt 500 + end +end + +Artifice.activate_with(Endpoint500) diff --git a/spec/bundler/support/artifice/endpoint_api_forbidden.rb b/spec/bundler/support/artifice/endpoint_api_forbidden.rb new file mode 100644 index 0000000000..21ad9117ed --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_api_forbidden.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointApiForbidden < Endpoint + get "/api/v1/dependencies" do + halt 403 + end +end + +Artifice.activate_with(EndpointApiForbidden) diff --git a/spec/bundler/support/artifice/endpoint_api_missing.rb b/spec/bundler/support/artifice/endpoint_api_missing.rb new file mode 100644 index 0000000000..6f5b5f1323 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_api_missing.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointApiMissing < Endpoint + get "/fetch/actual/gem/:id" do + $stderr.puts params[:id] + if params[:id] == "rack-1.0.gemspec.rz" + halt 404 + else + File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + end +end + +Artifice.activate_with(EndpointApiMissing) diff --git a/spec/bundler/support/artifice/endpoint_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_basic_authentication.rb new file mode 100644 index 0000000000..9fafd51a3d --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_basic_authentication.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointBasicAuthentication < Endpoint + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + end +end + +Artifice.activate_with(EndpointBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_creds_diff_host.rb b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb new file mode 100644 index 0000000000..cd152848fe --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointCredsDiffHost < Endpoint + helpers do + def auth + @auth ||= Rack::Auth::Basic::Request.new(request.env) + end + + def authorized? + auth.provided? && auth.basic? && auth.credentials && auth.credentials == %w(user pass) + end + + def protected! + return if authorized? + response["WWW-Authenticate"] = %(Basic realm="Testing HTTP Auth") + throw(:halt, [401, "Not authorized\n"]) + end + end + + before do + protected! unless request.path_info.include?("/no/creds/") + end + + get "/gems/:id" do + redirect "http://diffhost.com/no/creds/#{params[:id]}" + end + + get "/no/creds/:id" do + if request.host.include?("diffhost") && !auth.provided? + File.read("#{gem_repo1}/gems/#{params[:id]}") + end + end +end + +Artifice.activate_with(EndpointCredsDiffHost) diff --git a/spec/bundler/support/artifice/endpoint_extra.rb b/spec/bundler/support/artifice/endpoint_extra.rb new file mode 100644 index 0000000000..ed4e87e65f --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_extra.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointExtra < Endpoint + get "/extra/api/v1/dependencies" do + halt 404 + end + + get "/extra/specs.4.8.gz" do + File.read("#{gem_repo2}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.read("#{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.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.read("#{gem_repo2}/gems/#{params[:id]}") + end +end + +Artifice.activate_with(EndpointExtra) diff --git a/spec/bundler/support/artifice/endpoint_extra_api.rb b/spec/bundler/support/artifice/endpoint_extra_api.rb new file mode 100644 index 0000000000..1e9e1dc60d --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_extra_api.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointExtraApi < Endpoint + get "/extra/api/v1/dependencies" do + deps = dependencies_for(params[:gems], gem_repo4) + Marshal.dump(deps) + end + + get "/extra/specs.4.8.gz" do + File.read("#{gem_repo4}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.read("#{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.read("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.read("#{gem_repo4}/gems/#{params[:id]}") + end +end + +Artifice.activate_with(EndpointExtraApi) diff --git a/spec/bundler/support/artifice/endpoint_extra_missing.rb b/spec/bundler/support/artifice/endpoint_extra_missing.rb new file mode 100644 index 0000000000..dc79705a26 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_extra_missing.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint_extra", __FILE__) + +Artifice.deactivate + +class EndpointExtraMissing < EndpointExtra + get "/extra/fetch/actual/gem/:id" do + if params[:id] == "missing-1.0.gemspec.rz" + halt 404 + else + File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + end +end + +Artifice.activate_with(EndpointExtraMissing) diff --git a/spec/bundler/support/artifice/endpoint_fallback.rb b/spec/bundler/support/artifice/endpoint_fallback.rb new file mode 100644 index 0000000000..8a85a41784 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_fallback.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +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 + +Artifice.activate_with(EndpointFallback) diff --git a/spec/bundler/support/artifice/endpoint_host_redirect.rb b/spec/bundler/support/artifice/endpoint_host_redirect.rb new file mode 100644 index 0000000000..250416d8cc --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_host_redirect.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointHostRedirect < Endpoint + get "/fetch/actual/gem/:id", :host_name => "localgemserver.test" do + redirect "http://bundler.localgemserver.test#{request.path_info}" + end + + get "/api/v1/dependencies" do + status 404 + end +end + +Artifice.activate_with(EndpointHostRedirect) diff --git a/spec/bundler/support/artifice/endpoint_marshal_fail.rb b/spec/bundler/support/artifice/endpoint_marshal_fail.rb new file mode 100644 index 0000000000..0fb75ebf31 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_marshal_fail.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint_fallback", __FILE__) + +Artifice.deactivate + +class EndpointMarshalFail < EndpointFallback + get "/api/v1/dependencies" do + "f0283y01hasf" + end +end + +Artifice.activate_with(EndpointMarshalFail) diff --git a/spec/bundler/support/artifice/endpoint_mirror_source.rb b/spec/bundler/support/artifice/endpoint_mirror_source.rb new file mode 100644 index 0000000000..9fb58ecb29 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_mirror_source.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +class EndpointMirrorSource < Endpoint + get "/gems/:id" do + if request.env["HTTP_X_GEMFILE_SOURCE"] == "https://server.example.org/" + File.read("#{gem_repo1}/gems/#{params[:id]}") + else + halt 500 + end + end +end + +Artifice.activate_with(EndpointMirrorSource) diff --git a/spec/bundler/support/artifice/endpoint_redirect.rb b/spec/bundler/support/artifice/endpoint_redirect.rb new file mode 100644 index 0000000000..f80d7600c2 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_redirect.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointRedirect < Endpoint + get "/fetch/actual/gem/:id" do + redirect "/fetch/actual/gem/#{params[:id]}" + end + + get "/api/v1/dependencies" do + status 404 + end +end + +Artifice.activate_with(EndpointRedirect) diff --git a/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb new file mode 100644 index 0000000000..4b32cbbf5b --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointStrictBasicAuthentication < Endpoint + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + + # Only accepts password == "password" + unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz" + halt 403, "Authentication failed" + end + end +end + +Artifice.activate_with(EndpointStrictBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_timeout.rb b/spec/bundler/support/artifice/endpoint_timeout.rb new file mode 100644 index 0000000000..b15650f226 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_timeout.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint_fallback", __FILE__) + +Artifice.deactivate + +class EndpointTimeout < EndpointFallback + SLEEP_TIMEOUT = 15 + + get "/api/v1/dependencies" do + sleep(SLEEP_TIMEOUT) + end +end + +Artifice.activate_with(EndpointTimeout) diff --git a/spec/bundler/support/artifice/fail.rb b/spec/bundler/support/artifice/fail.rb new file mode 100644 index 0000000000..1059c6df4e --- /dev/null +++ b/spec/bundler/support/artifice/fail.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "net/http" +begin + require "net/https" +rescue LoadError + nil # net/https or openssl +end + +# 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 + # to use a legacy implementation. Since we are subclassing + # Net::HTTP, we must set it + @newimpl = true + + def request(req, body = nil, &block) + raise(exception(req)) + end + + # Ensure we don't start a connect here + def connect + end + + def exception(req) + 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"]}") + end +end + +# Replace Net::HTTP with our failing subclass +::Net.class_eval do + remove_const(:HTTP) + const_set(:HTTP, ::Fail) +end diff --git a/spec/bundler/support/artifice/windows.rb b/spec/bundler/support/artifice/windows.rb new file mode 100644 index 0000000000..c18ca454ec --- /dev/null +++ b/spec/bundler/support/artifice/windows.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true +require File.expand_path("../../path.rb", __FILE__) +include Spec::Path + +$LOAD_PATH.unshift Dir[base_system_gems.join("gems/artifice*/lib")].first.to_s +$LOAD_PATH.unshift(*Dir[base_system_gems.join("gems/rack-*/lib")]) +$LOAD_PATH.unshift Dir[base_system_gems.join("gems/tilt*/lib")].first.to_s +$LOAD_PATH.unshift Dir[base_system_gems.join("gems/sinatra*/lib")].first.to_s +require "artifice" +require "sinatra/base" + +Artifice.deactivate + +class Windows < Sinatra::Base + set :raise_errors, true + set :show_exceptions, false + + helpers do + def gem_repo + Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"] || Spec::Path.gem_repo1) + end + end + + files = ["specs.4.8.gz", + "prerelease_specs.4.8.gz", + "quick/Marshal.4.8/rcov-1.0-mswin32.gemspec.rz", + "gems/rcov-1.0-mswin32.gem"] + + files.each do |file| + get "/#{file}" do + File.read gem_repo.join(file) + end + end + + get "/gems/rcov-1.0-x86-mswin32.gem" do + halt 404 + end + + get "/api/v1/dependencies" do + halt 404 + end + + get "/versions" do + halt 500 + end +end + +Artifice.activate_with(Windows) diff --git a/spec/bundler/support/builders.rb b/spec/bundler/support/builders.rb new file mode 100644 index 0000000000..db128d497b --- /dev/null +++ b/spec/bundler/support/builders.rb @@ -0,0 +1,806 @@ +# frozen_string_literal: true +require "bundler/shared_helpers" +require "shellwords" + +module Spec + module Builders + def self.constantize(name) + name.delete("-").upcase + end + + def v(version) + Gem::Version.new(version) + end + + def pl(platform) + Gem::Platform.new(platform) + end + + def build_repo1 + build_repo gem_repo1 do + build_gem "rack", %w(0.9.1 1.0.0) do |s| + s.executables = "rackup" + s.post_install_message = "Rack's post install message" + end + + build_gem "thin" do |s| + s.add_dependency "rack" + s.post_install_message = "Thin's post install message" + end + + build_gem "rack-obama" do |s| + s.add_dependency "rack" + s.post_install_message = "Rack-obama's post install message" + end + + build_gem "rack_middleware", "1.0" do |s| + s.add_dependency "rack", "0.9.1" + end + + build_gem "rails", "2.3.2" do |s| + s.executables = "rails" + s.add_dependency "rake", "10.0.2" + s.add_dependency "actionpack", "2.3.2" + s.add_dependency "activerecord", "2.3.2" + s.add_dependency "actionmailer", "2.3.2" + s.add_dependency "activeresource", "2.3.2" + end + build_gem "actionpack", "2.3.2" do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "activerecord", ["2.3.1", "2.3.2"] do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "actionmailer", "2.3.2" do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "activeresource", "2.3.2" do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "activesupport", %w(1.2.3 2.3.2 2.3.5) + + build_gem "activemerchant" do |s| + s.add_dependency "activesupport", ">= 2.0.0" + end + + build_gem "rails_fail" do |s| + s.add_dependency "activesupport", "= 1.2.3" + end + + build_gem "missing_dep" do |s| + s.add_dependency "not_here" + end + + build_gem "rspec", "1.2.7", :no_default => true do |s| + s.write "lib/spec.rb", "SPEC = '1.2.7'" + end + + build_gem "rack-test", :no_default => true do |s| + s.write "lib/rack/test.rb", "RACK_TEST = '1.0'" + end + + build_gem "platform_specific" do |s| + s.platform = Bundler.local_platform + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 #{Bundler.local_platform}'" + end + + build_gem "platform_specific" do |s| + s.platform = "java" + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 JAVA'" + end + + build_gem "platform_specific" do |s| + s.platform = "ruby" + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 RUBY'" + end + + build_gem "platform_specific" do |s| + s.platform = "x86-mswin32" + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 MSWIN'" + end + + build_gem "platform_specific" do |s| + s.platform = "x86-darwin-100" + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 x86-darwin-100'" + end + + build_gem "only_java", "1.0" do |s| + s.platform = "java" + s.write "lib/only_java.rb", "ONLY_JAVA = '1.0.0 JAVA'" + end + + build_gem "only_java", "1.1" do |s| + s.platform = "java" + s.write "lib/only_java.rb", "ONLY_JAVA = '1.1.0 JAVA'" + end + + build_gem "nokogiri", "1.4.2" + build_gem "nokogiri", "1.4.2" do |s| + s.platform = "java" + s.write "lib/nokogiri.rb", "NOKOGIRI = '1.4.2 JAVA'" + s.add_dependency "weakling", ">= 0.0.3" + end + + build_gem "laduradura", "5.15.2" + build_gem "laduradura", "5.15.2" do |s| + s.platform = "java" + s.write "lib/laduradura.rb", "LADURADURA = '5.15.2 JAVA'" + end + build_gem "laduradura", "5.15.3" do |s| + s.platform = "java" + s.write "lib/laduradura.rb", "LADURADURA = '5.15.2 JAVA'" + end + + build_gem "weakling", "0.0.3" + + build_gem "terranova", "8" + + build_gem "duradura", "7.0" + + build_gem "multiple_versioned_deps" do |s| + s.add_dependency "weakling", ">= 0.0.1", "< 0.1" + end + + build_gem "not_released", "1.0.pre" + + build_gem "has_prerelease", "1.0" + build_gem "has_prerelease", "1.1.pre" + + build_gem "with_development_dependency" do |s| + s.add_development_dependency "activesupport", "= 2.3.5" + end + + build_gem "with_license" do |s| + s.license = "MIT" + end + + build_gem "with_implicit_rake_dep" do |s| + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("../lib", __FILE__) + FileUtils.mkdir_p(path) + File.open("\#{path}/implicit_rake_dep.rb", "w") do |f| + f.puts "IMPLICIT_RAKE_DEP = 'YES'" + end + end + RUBY + end + + build_gem "another_implicit_rake_dep" do |s| + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("../lib", __FILE__) + FileUtils.mkdir_p(path) + File.open("\#{path}/another_implicit_rake_dep.rb", "w") do |f| + f.puts "ANOTHER_IMPLICIT_RAKE_DEP = 'YES'" + end + end + RUBY + end + + build_gem "very_simple_binary", &:add_c_extension + + build_gem "bundler", "0.9" do |s| + s.executables = "bundle" + s.write "bin/bundle", "puts 'FAIL'" + end + + # The bundler 0.8 gem has a rubygems plugin that always loads :( + build_gem "bundler", "0.8.1" do |s| + s.write "lib/bundler/omg.rb", "" + s.write "lib/rubygems_plugin.rb", "require 'bundler/omg' ; puts 'FAIL'" + end + + build_gem "bundler_dep" do |s| + s.add_dependency "bundler" + end + + # The yard gem iterates over Gem.source_index looking for plugins + build_gem "yard" do |s| + s.write "lib/yard.rb", <<-Y + if Gem::Version.new(Gem::VERSION) >= Gem::Version.new("1.8.10") + specs = Gem::Specification + else + specs = Gem.source_index.find_name('') + end + specs.sort_by(&:name).each do |gem| + puts gem.full_name + end + Y + end + + # The rcov gem is platform mswin32, but has no arch + build_gem "rcov" do |s| + s.platform = Gem::Platform.new([nil, "mswin32", nil]) + s.write "lib/rcov.rb", "RCOV = '1.0.0'" + end + + build_gem "net-ssh" + build_gem "net-sftp", "1.1.1" do |s| + s.add_dependency "net-ssh", ">= 1.0.0", "< 1.99.0" + end + + # Test complicated gem dependencies for install + build_gem "net_a" do |s| + s.add_dependency "net_b" + s.add_dependency "net_build_extensions" + end + + build_gem "net_b" + + build_gem "net_build_extensions" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("../lib", __FILE__) + FileUtils.mkdir_p(path) + File.open("\#{path}/net_build_extensions.rb", "w") do |f| + f.puts "NET_BUILD_EXTENSIONS = 'YES'" + end + end + RUBY + end + + build_gem "net_c" do |s| + s.add_dependency "net_a" + s.add_dependency "net_d" + end + + build_gem "net_d" + + build_gem "net_e" do |s| + s.add_dependency "net_d" + end + + # Capistrano did this (at least until version 2.5.10) + # Rubygems 2.2 doesn't allow the specifying of a dependency twice + # See https://github.com/rubygems/rubygems/commit/03dbac93a3396a80db258d9bc63500333c25bd2f + build_gem "double_deps", "1.0", :skip_validation => true do |s| + s.add_dependency "net-ssh", ">= 1.0.0" + s.add_dependency "net-ssh" + end + + build_gem "foo" + + # A minimal fake pry console + build_gem "pry" do |s| + s.write "lib/pry.rb", <<-RUBY + class Pry + class << self + def toplevel_binding + unless defined?(@toplevel_binding) && @toplevel_binding + TOPLEVEL_BINDING.eval %{ + def self.__pry__; binding; end + Pry.instance_variable_set(:@toplevel_binding, __pry__) + class << self; undef __pry__; end + } + end + @toplevel_binding.eval('private') + @toplevel_binding + end + + def __pry__ + while line = gets + begin + puts eval(line, toplevel_binding).inspect.sub(/^"(.*)"$/, '=> \\1') + rescue Exception => e + puts "\#{e.class}: \#{e.message}" + puts e.backtrace.first + end + end + end + alias start __pry__ + end + end + RUBY + end + end + end + + def build_repo2(&blk) + FileUtils.rm_rf gem_repo2 + FileUtils.cp_r gem_repo1, gem_repo2 + update_repo2(&blk) if block_given? + end + + def build_repo3 + build_repo gem_repo3 do + build_gem "rack" + end + FileUtils.rm_rf Dir[gem_repo3("prerelease*")] + end + + # A repo that has no pre-installed gems included. (The caller completely + # determines the contents with the block.) + def build_repo4(&blk) + FileUtils.rm_rf gem_repo4 + build_repo(gem_repo4, &blk) + end + + def update_repo4(&blk) + update_repo(gem_repo4, &blk) + end + + def update_repo2 + update_repo gem_repo2 do + build_gem "rack", "1.2" do |s| + s.executables = "rackup" + end + yield if block_given? + end + end + + def build_security_repo + build_repo security_repo do + build_gem "rack" + + build_gem "signed_gem" do |s| + cert = "signing-cert.pem" + pkey = "signing-pkey.pem" + s.write cert, TEST_CERT + s.write pkey, TEST_PKEY + s.signing_key = pkey + s.cert_chain = [cert] + end + end + end + + def build_repo(path, &blk) + return if File.directory?(path) + rake_path = Dir["#{Path.base_system_gems}/**/rake*.gem"].first + + if rake_path.nil? + Spec::Path.base_system_gems.rmtree + Spec::Rubygems.setup + rake_path = Dir["#{Path.base_system_gems}/**/rake*.gem"].first + end + + if rake_path + FileUtils.mkdir_p("#{path}/gems") + FileUtils.cp rake_path, "#{path}/gems/" + else + abort "Your test gems are missing! Run `rm -rf #{tmp}` and try again." + end + + update_repo(path, &blk) + end + + def update_repo(path) + if path == gem_repo1 && caller.first.split(" ").last == "`build_repo`" + raise "Updating gem_repo1 is unsupported -- use gem_repo2 instead" + end + return unless block_given? + @_build_path = "#{path}/gems" + @_build_repo = File.basename(path) + yield + with_gem_path_as Path.base_system_gems do + Dir.chdir(path) { gem_command! :generate_index } + end + ensure + @_build_path = nil + @_build_repo = nil + end + + def build_index(&block) + index = Bundler::Index.new + IndexBuilder.run(index, &block) if block_given? + index + end + + def build_spec(name, version, platform = nil, &block) + Array(version).map do |v| + Gem::Specification.new do |s| + s.name = name + s.version = Gem::Version.new(v) + s.platform = platform + s.authors = ["no one in particular"] + s.summary = "a gemspec used only for testing" + DepBuilder.run(s, &block) if block_given? + end + end + end + + def build_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_gem(name, *args, &blk) + build_with(GemBuilder, name, args, &blk) + end + + def build_git(name, *args, &block) + opts = args.last.is_a?(Hash) ? args.last : {} + builder = opts[:bare] ? GitBareBuilder : GitBuilder + spec = build_with(builder, name, args, &block) + GitReader.new(opts[:path] || lib_path(spec.full_name)) + end + + def update_git(name, *args, &block) + opts = args.last.is_a?(Hash) ? args.last : {} + spec = build_with(GitUpdater, name, args, &block) + GitReader.new(opts[:path] || lib_path(spec.full_name)) + end + + def build_plugin(name, *args, &blk) + build_with(PluginBuilder, name, args, &blk) + end + + private + + def build_with(builder, name, args, &blk) + @_build_path ||= nil + @_build_repo ||= nil + options = args.last.is_a?(Hash) ? args.pop : {} + versions = args.last || "1.0" + spec = nil + + options[:path] ||= @_build_path + options[:source] ||= @_build_repo + + Array(versions).each do |version| + spec = builder.new(self, name, version) + spec.authors = ["no one"] if !spec.authors || spec.authors.empty? + yield spec if block_given? + spec._build(options) + end + + spec + end + + class IndexBuilder + include Builders + + def self.run(index, &block) + new(index).run(&block) + end + + def initialize(index) + @index = index + end + + def run(&block) + instance_eval(&block) + end + + def gem(*args, &block) + build_spec(*args, &block).each do |s| + @index << s + end + end + + def platforms(platforms) + platforms.split(/\s+/).each do |platform| + platform.gsub!(/^(mswin32)$/, 'x86-\1') + yield Gem::Platform.new(platform) + end + end + + def versions(versions) + versions.split(/\s+/).each {|version| yield v(version) } + end + end + + class DepBuilder + include Builders + + def self.run(spec, &block) + new(spec).run(&block) + end + + def initialize(spec) + @spec = spec + end + + def run(&block) + instance_eval(&block) + end + + def runtime(name, requirements) + @spec.add_runtime_dependency(name, requirements) + end + + def development(name, requirements) + @spec.add_development_dependency(name, requirements) + end + + def required_ruby_version=(*reqs) + @spec.required_ruby_version = *reqs + end + + alias_method :dep, :runtime + end + + class LibBuilder + def initialize(context, name, version) + @context = context + @name = name + @spec = Gem::Specification.new do |s| + s.name = name + s.version = version + s.summary = "This is just a fake gem for testing" + s.description = "This is a completely fake gem, for testing purposes." + s.author = "no one" + s.email = "foo@bar.baz" + s.homepage = "http://example.com" + s.license = "MIT" + end + @files = {} + end + + def method_missing(*args, &blk) + @spec.send(*args, &blk) + end + + def write(file, source = "") + @files[file] = source + end + + def executables=(val) + @spec.executables = Array(val) + @spec.executables.each do |file| + executable = "#{@spec.bindir}/#{file}" + shebang = if Bundler.current_ruby.jruby? + "#!/usr/bin/env jruby\n" + else + "#!/usr/bin/env ruby\n" + end + @spec.files << executable + write executable, "#{shebang}require '#{@name}' ; puts #{Builders.constantize(@name)}" + end + end + + def add_c_extension + require_paths << "ext" + extensions << "ext/extconf.rb" + write "ext/extconf.rb", <<-RUBY + require "mkmf" + + + # exit 1 unless with_config("simple") + + extension_name = "very_simple_binary_c" + if extra_lib_dir = with_config("ext-lib") + # add extra libpath if --with-ext-lib is + # passed in as a build_arg + dir_config extension_name, nil, extra_lib_dir + else + dir_config extension_name + end + create_makefile extension_name + RUBY + write "ext/very_simple_binary.c", <<-C + #include "ruby.h" + + void Init_very_simple_binary_c() { + rb_define_module("VerySimpleBinaryInC"); + } + C + end + + def _build(options) + path = options[:path] || _default_path + + if options[:rubygems_version] + @spec.rubygems_version = options[:rubygems_version] + def @spec.mark_version; end + + def @spec.validate; end + end + + case options[:gemspec] + when false + # do nothing + when :yaml + @files["#{name}.gemspec"] = @spec.to_yaml + else + @files["#{name}.gemspec"] = @spec.to_ruby + end + + unless options[:no_default] + gem_source = options[:source] || "path@#{path}" + @files = _default_files. + merge("lib/#{name}/source.rb" => "#{Builders.constantize(name)}_SOURCE = #{gem_source.to_s.dump}"). + merge(@files) + end + + @spec.authors = ["no one"] + + @files.each do |file, source| + file = Pathname.new(path).join(file) + FileUtils.mkdir_p(file.dirname) + File.open(file, "w") {|f| f.puts source } + end + @spec.files = @files.keys + path + end + + def _default_files + @_default_files ||= begin + platform_string = " #{@spec.platform}" unless @spec.platform == Gem::Platform::RUBY + { "lib/#{name}.rb" => "#{Builders.constantize(name)} = '#{version}#{platform_string}'" } + end + end + + def _default_path + @context.tmp("libs", @spec.full_name) + end + end + + class GitBuilder < LibBuilder + def _build(options) + path = options[:path] || _default_path + source = options[:source] || "git@#{path}" + super(options.merge(:path => path, :source => source)) + Dir.chdir(path) do + `git init` + `git add *` + `git config user.email "lol@wut.com"` + `git config user.name "lolwut"` + `git commit -m 'OMG INITIAL COMMIT'` + end + end + end + + class GitBareBuilder < LibBuilder + def _build(options) + path = options[:path] || _default_path + super(options.merge(:path => path)) + Dir.chdir(path) do + `git init --bare` + end + end + end + + class GitUpdater < LibBuilder + def silently(str) + `#{str} 2>#{Bundler::NULL}` + end + + def _build(options) + libpath = options[:path] || _default_path + update_gemspec = options[:gemspec] || false + source = options[:source] || "git@#{libpath}" + + Dir.chdir(libpath) do + silently "git checkout master" + + if branch = options[:branch] + raise "You can't specify `master` as the branch" if branch == "master" + escaped_branch = Shellwords.shellescape(branch) + + if `git branch | grep #{escaped_branch}`.empty? + silently("git branch #{escaped_branch}") + end + + silently("git checkout #{escaped_branch}") + elsif tag = options[:tag] + `git tag #{Shellwords.shellescape(tag)}` + elsif options[:remote] + silently("git remote add origin file://#{options[:remote]}") + elsif options[:push] + silently("git push origin #{options[:push]}") + end + + current_ref = `git rev-parse HEAD`.strip + _default_files.keys.each do |path| + _default_files[path] += "\n#{Builders.constantize(name)}_PREV_REF = '#{current_ref}'" + end + super(options.merge(:path => libpath, :gemspec => update_gemspec, :source => source)) + `git add *` + `git commit -m "BUMP"` + end + end + end + + class GitReader + attr_reader :path + + def initialize(path) + @path = path + end + + def ref_for(ref, len = nil) + ref = git "rev-parse #{ref}" + ref = ref[0..len] if len + ref + end + + private + + def git(cmd) + Bundler::SharedHelpers.with_clean_git_env do + Dir.chdir(@path) { `git #{cmd}`.strip } + end + end + end + + class GemBuilder < LibBuilder + def _build(opts) + lib_path = super(opts.merge(:path => @context.tmp(".tmp/#{@spec.full_name}"), :no_default => opts[:no_default])) + Dir.chdir(lib_path) do + destination = opts[:path] || _default_path + FileUtils.mkdir_p(destination) + + @spec.authors = ["that guy"] if !@spec.authors || @spec.authors.empty? + + Bundler.rubygems.build(@spec, opts[:skip_validation]) + if opts[:to_system] + `gem install --ignore-dependencies --no-ri --no-rdoc #{@spec.full_name}.gem` + else + FileUtils.mv("#{@spec.full_name}.gem", opts[:path] || _default_path) + end + end + end + + def _default_path + @context.gem_repo1("gems") + end + end + + class PluginBuilder < GemBuilder + def _default_files + @_default_files ||= super.merge("plugins.rb" => "") + end + end + + TEST_CERT = <<-CERT.gsub(/^\s*/, "") + -----BEGIN CERTIFICATE----- + MIIDMjCCAhqgAwIBAgIBATANBgkqhkiG9w0BAQUFADAnMQwwCgYDVQQDDAN5b3Ux + FzAVBgoJkiaJk/IsZAEZFgdleGFtcGxlMB4XDTE1MDIwODAwMTIyM1oXDTQyMDYy + NTAwMTIyM1owJzEMMAoGA1UEAwwDeW91MRcwFQYKCZImiZPyLGQBGRYHZXhhbXBs + ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANlvFdpN43c4DMS9Jo06 + m0a7k3bQ3HWQ1yrYhZMi77F1F73NpBknYHIzDktQpGn6hs/4QFJT4m4zNEBF47UL + jHU5nTK5rjkS3niGYUjvh3ZEzVeo9zHUlD/UwflDo4ALl3TSo2KY/KdPS/UTdLXL + ajkQvaVJtEDgBPE3DPhlj5whp+Ik3mDHej7qpV6F502leAwYaFyOtlEG/ZGNG+nZ + L0clH0j77HpP42AylHDi+vakEM3xcjo9BeWQ6Vkboic93c9RTt6CWBWxMQP7Nol1 + MOebz9XOSQclxpxWteXNfPRtMdAhmRl76SMI8ywzThNPpa4EH/yz34ftebVOgKyM + nd0CAwEAAaNpMGcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFA7D + n9qo0np23qi3aOYuAAPn/5IdMBYGA1UdEQQPMA2BC3lvdUBleGFtcGxlMBYGA1Ud + EgQPMA2BC3lvdUBleGFtcGxlMA0GCSqGSIb3DQEBBQUAA4IBAQA7Gyk62sWOUX/N + vk4tJrgKESph6Ns8+E36A7n3jt8zCep8ldzMvwTWquf9iqhsC68FilEoaDnUlWw7 + d6oNuaFkv7zfrWGLlvqQJC+cu2X5EpcCksg5oRp8VNbwJysJ6JgwosxzROII8eXc + R+j1j6mDvQYqig2QOnzf480pjaqbP+tspfDFZbhKPrgM3Blrb3ZYuFpv4zkqI7aB + 6fuk2DUhNO1CuwrJA84TqC+jGo73bDKaT5hrIDiaJRrN5+zcWja2uEWrj5jSbep4 + oXdEdyH73hOHMBP40uds3PqnUsxEJhzjB2sCCe1geV24kw9J4m7EQXPVkUKDgKrt + LlpDmOoo + -----END CERTIFICATE----- + CERT + + TEST_PKEY = <<-PKEY.gsub(/^\s*/, "") + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEA2W8V2k3jdzgMxL0mjTqbRruTdtDcdZDXKtiFkyLvsXUXvc2k + GSdgcjMOS1CkafqGz/hAUlPibjM0QEXjtQuMdTmdMrmuORLeeIZhSO+HdkTNV6j3 + MdSUP9TB+UOjgAuXdNKjYpj8p09L9RN0tctqORC9pUm0QOAE8TcM+GWPnCGn4iTe + YMd6PuqlXoXnTaV4DBhoXI62UQb9kY0b6dkvRyUfSPvsek/jYDKUcOL69qQQzfFy + Oj0F5ZDpWRuiJz3dz1FO3oJYFbExA/s2iXUw55vP1c5JByXGnFa15c189G0x0CGZ + GXvpIwjzLDNOE0+lrgQf/LPfh+15tU6ArIyd3QIDAQABAoIBACbDqz20TS1gDMa2 + gj0DidNedbflHKjJHdNBru7Ad8NHgOgR1YO2hXdWquG6itVqGMbTF4SV9/R1pIcg + 7qvEV1I+50u31tvOBWOvcYCzU48+TO2n7gowQA3xPHPYHzog1uu48fAOHl0lwgD7 + av9OOK3b0jO5pC08wyTOD73pPWU0NrkTh2+N364leIi1pNuI1z4V+nEuIIm7XpVd + 5V4sXidMTiEMJwE6baEDfTjHKaoRndXrrPo3ryIXmcX7Ag1SwAQwF5fBCRToCgIx + dszEZB1bJD5gA6r+eGnJLB/F60nK607az5o3EdguoB2LKa6q6krpaRCmZU5svvoF + J7xgBPECgYEA8RIzHAQ3zbaibKdnllBLIgsqGdSzebTLKheFuigRotEV3Or/z5Lg + k/nVnThWVkTOSRqXTNpJAME6a4KTdcVSxYP+SdZVO1esazHrGb7xPVb7MWSE1cqp + WEk3Yy8OUOPoPQMc4dyGzd30Mi8IBB6gnFIYOTrpUo0XtkBv8rGGhfsCgYEA5uYn + 6QgL4NqNT84IXylmMb5ia3iBt6lhxI/A28CDtQvfScl4eYK0IjBwdfG6E1vJgyzg + nJzv3xEVo9bz+Kq7CcThWpK5JQaPnsV0Q74Wjk0ShHet15txOdJuKImnh5F6lylC + GTLR9gnptytfMH/uuw4ws0Q2kcg4l5NHKOWOnAcCgYEAvAwIVkhsB0n59Wu4gCZu + FUZENxYWUk/XUyQ6KnZrG2ih90xQ8+iMyqFOIm/52R2fFKNrdoWoALC6E3ct8+ZS + pMRLrelFXx8K3it4SwMJR2H8XBEfFW4bH0UtsW7Zafv+AunUs9LETP5gKG1LgXsq + qgXX43yy2LQ61O365YPZfdUCgYBVbTvA3MhARbvYldrFEnUL3GtfZbNgdxuD9Mee + xig0eJMBIrgfBLuOlqtVB70XYnM4xAbKCso4loKSHnofO1N99siFkRlM2JOUY2tz + kMWZmmxKdFjuF0WZ5f/5oYxI/QsFGC+rUQEbbWl56mMKd5qkvEhKWudxoklF0yiV + ufC8SwKBgDWb8iWqWN5a/kfvKoxFcDM74UHk/SeKMGAL+ujKLf58F+CbweM5pX9C + EUsxeoUEraVWTiyFVNqD81rCdceus9TdBj0ZIK1vUttaRZyrMAwF0uQSfjtxsOpd + l69BkyvzjgDPkmOHVGiSZDLi3YDvypbUpo6LOy4v5rVg5U2F/A0v + -----END RSA PRIVATE KEY----- + PKEY + end +end diff --git a/spec/bundler/support/code_climate.rb b/spec/bundler/support/code_climate.rb new file mode 100644 index 0000000000..8f1fb35bcd --- /dev/null +++ b/spec/bundler/support/code_climate.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +module Spec + module CodeClimate + def self.setup + require "codeclimate-test-reporter" + ::CodeClimate::TestReporter.start + configure_exclusions + rescue LoadError + # it's fine if CodeClimate isn't set up + nil + end + + def self.configure_exclusions + SimpleCov.start do + add_filter "/bin/" + add_filter "/lib/bundler/man/" + add_filter "/lib/bundler/vendor/" + add_filter "/man/" + add_filter "/pkg/" + add_filter "/spec/" + add_filter "/tmp/" + end + end + end +end diff --git a/spec/bundler/support/hax.rb b/spec/bundler/support/hax.rb new file mode 100644 index 0000000000..663d3527c5 --- /dev/null +++ b/spec/bundler/support/hax.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true +require "rubygems" + +module Gem + class Platform + @local = new(ENV["BUNDLER_SPEC_PLATFORM"]) if ENV["BUNDLER_SPEC_PLATFORM"] + end + @platforms = [Gem::Platform::RUBY, Gem::Platform.local] +end + +if ENV["BUNDLER_SPEC_VERSION"] + module Bundler + remove_const(:VERSION) if const_defined?(:VERSION) + VERSION = ENV["BUNDLER_SPEC_VERSION"].dup + end +end + +if ENV["BUNDLER_SPEC_WINDOWS"] == "true" + require "bundler/constants" + + module Bundler + remove_const :WINDOWS if defined?(WINDOWS) + WINDOWS = true + end +end + +class Object + if ENV["BUNDLER_SPEC_RUBY_ENGINE"] + if defined?(RUBY_ENGINE) && RUBY_ENGINE != "jruby" && ENV["BUNDLER_SPEC_RUBY_ENGINE"] == "jruby" + begin + # this has to be done up front because psych will try to load a .jar + # if it thinks its on jruby + require "psych" + rescue LoadError + nil + end + end + + remove_const :RUBY_ENGINE if defined?(RUBY_ENGINE) + RUBY_ENGINE = ENV["BUNDLER_SPEC_RUBY_ENGINE"] + + if RUBY_ENGINE == "jruby" + remove_const :JRUBY_VERSION if defined?(JRUBY_VERSION) + JRUBY_VERSION = ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] + end + end +end diff --git a/spec/bundler/support/helpers.rb b/spec/bundler/support/helpers.rb new file mode 100644 index 0000000000..1a3fec1960 --- /dev/null +++ b/spec/bundler/support/helpers.rb @@ -0,0 +1,504 @@ +# frozen_string_literal: true + +module Spec + module Helpers + def reset! + Dir.glob("#{tmp}/{gems/*,*}", File::FNM_DOTMATCH).each do |dir| + next if %w(base remote1 gems rubygems . ..).include?(File.basename(dir)) + if ENV["BUNDLER_SUDO_TESTS"] + `sudo rm -rf "#{dir}"` + else + FileUtils.rm_rf(dir) + end + end + FileUtils.mkdir_p(home) + FileUtils.mkdir_p(tmpdir) + Bundler.reset! + Bundler.ui = nil + Bundler.ui # force it to initialize + end + + def self.bang(method) + define_method("#{method}!") do |*args, &blk| + send(method, *args, &blk).tap do + if exitstatus && exitstatus != 0 + error = out + "\n" + err + error.strip! + raise RuntimeError, + "Invoking #{method}!(#{args.map(&:inspect).join(", ")}) failed:\n#{error}", + caller.drop_while {|bt| bt.start_with?(__FILE__) } + end + end + end + end + + attr_reader :out, :err, :exitstatus + + def the_bundle(*args) + TheBundle.new(*args) + end + + def in_app_root(&blk) + Dir.chdir(bundled_app, &blk) + end + + def in_app_root2(&blk) + Dir.chdir(bundled_app2, &blk) + end + + def in_app_root_custom(root, &blk) + Dir.chdir(root, &blk) + end + + def run(cmd, *args) + opts = args.last.is_a?(Hash) ? args.pop : {} + groups = args.map(&:inspect).join(", ") + setup = "require 'rubygems' ; require 'bundler' ; Bundler.setup(#{groups})\n" + @out = ruby(setup + cmd, opts) + end + bang :run + + def load_error_run(ruby, name, *args) + cmd = <<-RUBY + begin + #{ruby} + rescue LoadError => e + $stderr.puts "ZOMG LOAD ERROR" if e.message.include?("-- #{name}") + end + RUBY + opts = args.last.is_a?(Hash) ? args.pop : {} + args += [opts] + run(cmd, *args) + end + + def lib + root.join("lib") + end + + def spec + spec_dir.to_s + end + + def bundle(cmd, options = {}) + with_sudo = options.delete(:sudo) + sudo = with_sudo == :preserve_env ? "sudo -E" : "sudo" if with_sudo + + options["no-color"] = true unless options.key?("no-color") || cmd.to_s =~ /\A(e|ex|exe|exec|conf|confi|config)(\s|\z)/ + + bundle_bin = options.delete("bundle_bin") || bindir.join("bundle") + + if system_bundler = options.delete(:system_bundler) + bundle_bin = "-S bundle" + end + + requires = options.delete(:requires) || [] + if artifice = options.delete(:artifice) { "fail" unless RSpec.current_example.metadata[:realworld] } + requires << File.expand_path("../artifice/#{artifice}.rb", __FILE__) + end + requires << "support/hax" + requires_str = requires.map {|r| "-r#{r}" }.join(" ") + + load_path = [] + load_path << lib unless system_bundler + load_path << spec + load_path_str = "-I#{load_path.join(File::PATH_SEPARATOR)}" + + env = (options.delete(:env) || {}).map {|k, v| "#{k}='#{v}'" }.join(" ") + env["PATH"].gsub!("#{Path.root}/exe", "") if env["PATH"] && system_bundler + args = options.map do |k, v| + v == true ? " --#{k}" : " --#{k} #{v}" if v + end.join + + cmd = "#{env} #{sudo} #{Gem.ruby} #{load_path_str} #{requires_str} #{bundle_bin} #{cmd}#{args}" + sys_exec(cmd) {|i, o, thr| yield i, o, thr if block_given? } + end + bang :bundle + + def bundler(cmd, options = {}) + options["bundle_bin"] = bindir.join("bundler") + bundle(cmd, options) + end + + def bundle_ruby(options = {}) + options["bundle_bin"] = bindir.join("bundle_ruby") + bundle("", options) + end + + def ruby(ruby, options = {}) + env = (options.delete(:env) || {}).map {|k, v| "#{k}='#{v}' " }.join + ruby = ruby.gsub(/["`\$]/) {|m| "\\#{m}" } + lib_option = options[:no_lib] ? "" : " -I#{lib}" + sys_exec(%(#{env}#{Gem.ruby}#{lib_option} -e "#{ruby}")) + end + bang :ruby + + def load_error_ruby(ruby, name, opts = {}) + ruby(<<-R) + begin + #{ruby} + rescue LoadError => e + $stderr.puts "ZOMG LOAD ERROR"# if e.message.include?("-- #{name}") + end + R + end + + def gembin(cmd) + lib = File.expand_path("../../../lib", __FILE__) + old = ENV["RUBYOPT"] + ENV["RUBYOPT"] = "#{ENV["RUBYOPT"]} -I#{lib}" + cmd = bundled_app("bin/#{cmd}") unless cmd.to_s.include?("/") + sys_exec(cmd.to_s) + ensure + ENV["RUBYOPT"] = old + end + + def gem_command(command, args = "", options = {}) + if command == :exec && !options[:no_quote] + args = args.gsub(/(?=")/, "\\") + args = %("#{args}") + end + gem = ENV['BUNDLE_GEM'] || "#{Gem.ruby} -rubygems -S gem --backtrace" + sys_exec("#{gem} #{command} #{args}") + end + bang :gem_command + + def rake + if ENV['BUNDLE_RUBY'] && ENV['BUNDLE_GEM'] + "#{ENV['BUNDLE_RUBY']} #{ENV['GEM_PATH']}/bin/rake" + else + 'rake' + end + end + + def sys_exec(cmd) + Open3.popen3(cmd.to_s) do |stdin, stdout, stderr, wait_thr| + yield stdin, stdout, wait_thr if block_given? + stdin.close + + @exitstatus = wait_thr && wait_thr.value.exitstatus + @out = Thread.new { stdout.read }.value.strip + @err = Thread.new { stderr.read }.value.strip + end + + (@all_output ||= String.new) << [ + "$ #{cmd.to_s.strip}", + out, + err, + @exitstatus ? "# $? => #{@exitstatus}" : "", + "\n", + ].reject(&:empty?).join("\n") + + @out + end + bang :sys_exec + + def config(config = nil, path = bundled_app(".bundle/config")) + return YAML.load_file(path) unless config + FileUtils.mkdir_p(File.dirname(path)) + File.open(path, "w") do |f| + f.puts config.to_yaml + end + config + end + + def global_config(config = nil) + config(config, home(".bundle/config")) + end + + def create_file(*args) + path = bundled_app(args.shift) + path = args.shift if args.first.is_a?(Pathname) + str = args.shift || "" + path.dirname.mkpath + File.open(path.to_s, "w") do |f| + f.puts strip_whitespace(str) + end + end + + def gemfile(*args) + if args.empty? + File.open("Gemfile", "r", &:read) + else + create_file("Gemfile", *args) + end + end + + def lockfile(*args) + if args.empty? + File.open("Gemfile.lock", "r", &:read) + else + create_file("Gemfile.lock", *args) + end + end + + def strip_whitespace(str) + # Trim the leading spaces + spaces = str[/\A\s+/, 0] || "" + str.gsub(/^#{spaces}/, "") + end + + def install_gemfile(*args) + gemfile(*args) + opts = args.last.is_a?(Hash) ? args.last : {} + opts[:retry] ||= 0 + bundle :install, opts + end + bang :install_gemfile + + def lock_gemfile(*args) + gemfile(*args) + opts = args.last.is_a?(Hash) ? args.last : {} + opts[:retry] ||= 0 + bundle :lock, opts + end + + def install_gems(*gems) + options = gems.last.is_a?(Hash) ? gems.pop : {} + gem_repo = options.fetch(:gem_repo) { gem_repo1 } + gems.each do |g| + path = if g == :bundler + Dir.chdir(root) { gem_command! :build, gemspec.to_s } + bundler_path = root + "bundler-#{Bundler::VERSION}.gem" + elsif g.to_s =~ %r{\A/.*\.gem\z} + g + else + "#{gem_repo}/gems/#{g}.gem" + end + + raise "OMG `#{path}` does not exist!" unless File.exist?(path) + + gem_command! :install, "--no-rdoc --no-ri --ignore-dependencies '#{path}'" + bundler_path && bundler_path.rmtree + end + end + + alias_method :install_gem, :install_gems + + def with_gem_path_as(path) + backup = ENV.to_hash + ENV["GEM_HOME"] = path.to_s + ENV["GEM_PATH"] = path.to_s + ENV["BUNDLER_ORIG_GEM_PATH"] = nil + yield + ensure + ENV.replace(backup) + end + + def with_path_as(path) + backup = ENV.to_hash + ENV["PATH"] = path.to_s + ENV["BUNDLER_ORIG_PATH"] = nil + yield + ensure + ENV.replace(backup) + end + + def with_path_added(path) + with_path_as(path.to_s + ":" + ENV["PATH"]) do + yield + end + end + + def break_git! + FileUtils.mkdir_p(tmp("broken_path")) + File.open(tmp("broken_path/git"), "w", 0o755) do |f| + f.puts "#!/usr/bin/env ruby\nSTDERR.puts 'This is not the git you are looking for'\nexit 1" + end + + ENV["PATH"] = "#{tmp("broken_path")}:#{ENV["PATH"]}" + end + + def with_fake_man + FileUtils.mkdir_p(tmp("fake_man")) + File.open(tmp("fake_man/man"), "w", 0o755) do |f| + f.puts "#!/usr/bin/env ruby\nputs ARGV.inspect\n" + end + with_path_added(tmp("fake_man")) { yield } + end + + def system_gems(*gems) + gems = gems.flatten + + FileUtils.rm_rf(system_gem_path) + FileUtils.mkdir_p(system_gem_path) + + Gem.clear_paths + + env_backup = ENV.to_hash + ENV["GEM_HOME"] = system_gem_path.to_s + ENV["GEM_PATH"] = system_gem_path.to_s + ENV["BUNDLER_ORIG_GEM_PATH"] = nil + + install_gems(*gems) + return unless block_given? + begin + yield + ensure + ENV.replace(env_backup) + end + end + + def realworld_system_gems(*gems) + gems = gems.flatten + + FileUtils.rm_rf(system_gem_path) + FileUtils.mkdir_p(system_gem_path) + + Gem.clear_paths + + gem_home = ENV["GEM_HOME"] + gem_path = ENV["GEM_PATH"] + path = ENV["PATH"] + ENV["GEM_HOME"] = system_gem_path.to_s + ENV["GEM_PATH"] = system_gem_path.to_s + + gems.each do |gem| + gem_command :install, "--no-rdoc --no-ri #{gem}" + end + return unless block_given? + begin + yield + ensure + ENV["GEM_HOME"] = gem_home + ENV["GEM_PATH"] = gem_path + ENV["PATH"] = path + end + end + + def cache_gems(*gems) + gems = gems.flatten + + FileUtils.rm_rf("#{bundled_app}/vendor/cache") + FileUtils.mkdir_p("#{bundled_app}/vendor/cache") + + gems.each do |g| + path = "#{gem_repo1}/gems/#{g}.gem" + raise "OMG `#{path}` does not exist!" unless File.exist?(path) + FileUtils.cp(path, "#{bundled_app}/vendor/cache") + end + end + + def simulate_new_machine + system_gems [] + FileUtils.rm_rf default_bundle_path + FileUtils.rm_rf bundled_app(".bundle") + end + + def simulate_platform(platform) + old = ENV["BUNDLER_SPEC_PLATFORM"] + ENV["BUNDLER_SPEC_PLATFORM"] = platform.to_s + yield if block_given? + ensure + ENV["BUNDLER_SPEC_PLATFORM"] = old if block_given? + end + + def simulate_ruby_version(version) + return if version == RUBY_VERSION + old = ENV["BUNDLER_SPEC_RUBY_VERSION"] + ENV["BUNDLER_SPEC_RUBY_VERSION"] = version + yield if block_given? + ensure + ENV["BUNDLER_SPEC_RUBY_VERSION"] = old if block_given? + end + + def simulate_ruby_engine(engine, version = "1.6.0") + return if engine == local_ruby_engine + + old = ENV["BUNDLER_SPEC_RUBY_ENGINE"] + ENV["BUNDLER_SPEC_RUBY_ENGINE"] = engine + old_version = ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] + ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] = version + yield if block_given? + ensure + ENV["BUNDLER_SPEC_RUBY_ENGINE"] = old if block_given? + ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] = old_version if block_given? + end + + def simulate_bundler_version(version) + old = ENV["BUNDLER_SPEC_VERSION"] + ENV["BUNDLER_SPEC_VERSION"] = version.to_s + yield if block_given? + ensure + ENV["BUNDLER_SPEC_VERSION"] = old if block_given? + end + + def simulate_windows + old = ENV["BUNDLER_SPEC_WINDOWS"] + ENV["BUNDLER_SPEC_WINDOWS"] = "true" + simulate_platform mswin do + yield + end + ensure + ENV["BUNDLER_SPEC_WINDOWS"] = old + end + + def revision_for(path) + Dir.chdir(path) { `git rev-parse HEAD`.strip } + end + + def capture_output + capture(:stdout) + end + + def with_read_only(pattern) + chmod = lambda do |dirmode, filemode| + lambda do |f| + mode = File.directory?(f) ? dirmode : filemode + File.chmod(mode, f) + end + end + + Dir[pattern].each(&chmod[0o555, 0o444]) + yield + ensure + Dir[pattern].each(&chmod[0o755, 0o644]) + end + + def process_file(pathname) + changed_lines = pathname.readlines.map do |line| + yield line + end + File.open(pathname, "w") {|file| file.puts(changed_lines.join) } + end + + def with_env_vars(env_hash, &block) + current_values = {} + env_hash.each do |k, v| + current_values[k] = ENV[k] + ENV[k] = v + end + block.call if block_given? + env_hash.each do |k, _| + ENV[k] = current_values[k] + end + end + + def require_rack + # need to hack, so we can require rack + old_gem_home = ENV["GEM_HOME"] + ENV["GEM_HOME"] = Spec::Path.base_system_gems.to_s + require "rack" + ENV["GEM_HOME"] = old_gem_home + end + + def wait_for_server(host, port, seconds = 15) + tries = 0 + sleep 0.5 + TCPSocket.new(host, port) + rescue => e + raise(e) if tries > (seconds * 2) + tries += 1 + retry + end + + def find_unused_port + port = 21_453 + begin + port += 1 while TCPSocket.new("127.0.0.1", port) + rescue + false + end + port + end + end +end diff --git a/spec/bundler/support/indexes.rb b/spec/bundler/support/indexes.rb new file mode 100644 index 0000000000..29780014fc --- /dev/null +++ b/spec/bundler/support/indexes.rb @@ -0,0 +1,365 @@ +# frozen_string_literal: true +module Spec + module Indexes + def dep(name, reqs = nil) + @deps ||= [] + @deps << Bundler::Dependency.new(name, reqs) + end + + def platform(*args) + @platforms ||= [] + @platforms.concat args.map {|p| Gem::Platform.new(p) } + end + + alias_method :platforms, :platform + + def resolve(args = []) + @platforms ||= ["ruby"] + deps = [] + @deps.each do |d| + @platforms.each do |p| + deps << Bundler::DepProxy.new(d, p) + end + end + Bundler::Resolver.resolve(deps, @index, *args) + end + + def should_resolve_as(specs) + got = resolve + got = got.map(&:full_name).sort + expect(got).to eq(specs.sort) + end + + def should_resolve_and_include(specs, args = []) + got = resolve(args) + got = got.map(&:full_name).sort + specs.each do |s| + expect(got).to include(s) + end + end + + def should_conflict_on(names) + got = resolve + flunk "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 + + def locked(*args) + Bundler::SpecSet.new(args.map do |name, version| + gem(name, version) + end) + end + + def should_conservative_resolve_and_include(opts, unlock, specs) + # empty unlock means unlock all + opts = Array(opts) + search = Bundler::GemVersionPromoter.new(@locked, unlock).tap do |s| + s.level = opts.first + s.strict = opts.include?(:strict) + end + should_resolve_and_include specs, [{}, @base, search] + end + + def an_awesome_index + build_index do + gem "rack", %w(0.8 0.9 0.9.1 0.9.2 1.0 1.1) + gem "rack-mount", %w(0.4 0.5 0.5.1 0.5.2 0.6) + + # --- Rails + versions "1.2.3 2.2.3 2.3.5 3.0.0.beta 3.0.0.beta1" do |version| + gem "activesupport", version + gem "actionpack", version do + dep "activesupport", version + if version >= v("3.0.0.beta") + dep "rack", "~> 1.1" + dep "rack-mount", ">= 0.5" + elsif version > v("2.3") then dep "rack", "~> 1.0.0" + elsif version > v("2.0.0") then dep "rack", "~> 0.9.0" + end + end + gem "activerecord", version do + dep "activesupport", version + dep "arel", ">= 0.2" if version >= v("3.0.0.beta") + end + gem "actionmailer", version do + dep "activesupport", version + dep "actionmailer", version + end + if version < v("3.0.0.beta") + gem "railties", version do + dep "activerecord", version + dep "actionpack", version + dep "actionmailer", version + dep "activesupport", version + end + else + gem "railties", version + gem "rails", version do + dep "activerecord", version + dep "actionpack", version + dep "actionmailer", version + dep "activesupport", version + dep "railties", version + end + end + end + + versions "1.0 1.2 1.2.1 1.2.2 1.3 1.3.0.1 1.3.5 1.4.0 1.4.2 1.4.2.1" do |version| + platforms "ruby java mswin32 mingw32 x64-mingw32" do |platform| + next if version == v("1.4.2.1") && platform != pl("x86-mswin32") + next if version == v("1.4.2") && platform == pl("x86-mswin32") + gem "nokogiri", version, platform do + dep "weakling", ">= 0.0.3" if platform =~ pl("java") + end + end + end + + versions "0.0.1 0.0.2 0.0.3" do |version| + gem "weakling", version + end + + # --- Rails related + versions "1.2.3 2.2.3 2.3.5" do |version| + gem "activemerchant", version do + dep "activesupport", ">= #{version}" + end + end + end + end + + # Builder 3.1.4 will activate first, but if all + # goes well, it should resolve to 3.0.4 + def a_conflict_index + build_index do + gem "builder", %w(3.0.4 3.1.4) + gem("grape", "0.2.6") do + dep "builder", ">= 0" + end + + versions "3.2.8 3.2.9 3.2.10 3.2.11" do |version| + gem("activemodel", version) do + dep "builder", "~> 3.0.0" + end + end + + gem("my_app", "1.0.0") do + dep "activemodel", ">= 0" + dep "grape", ">= 0" + end + end + end + + def a_complex_conflict_index + build_index do + gem("a", %w(1.0.2 1.1.4 1.2.0 1.4.0)) do + dep "d", ">= 0" + end + + gem("d", %w(1.3.0 1.4.1)) do + dep "x", ">= 0" + end + + gem "d", "0.9.8" + + gem("b", "0.3.4") do + dep "a", ">= 1.5.0" + end + + gem("b", "0.3.5") do + dep "a", ">= 1.2" + end + + gem("b", "0.3.3") do + dep "a", "> 1.0" + end + + versions "3.2 3.3" do |version| + gem("c", version) do + dep "a", "~> 1.0" + end + end + + gem("my_app", "1.3.0") do + dep "c", ">= 4.0" + dep "b", ">= 0" + end + + gem("my_app", "1.2.0") do + dep "c", "~> 3.3.0" + dep "b", "0.3.4" + end + + gem("my_app", "1.1.0") do + dep "c", "~> 3.2.0" + dep "b", "0.3.5" + end + end + end + + def index_with_conflict_on_child + build_index do + gem "json", %w(1.6.5 1.7.7 1.8.0) + + gem("chef", "10.26") do + dep "json", [">= 1.4.4", "<= 1.7.7"] + end + + gem("berkshelf", "2.0.7") do + dep "json", ">= 1.7.7" + end + + gem("chef_app", "1.0.0") do + dep "berkshelf", "~> 2.0" + dep "chef", "~> 10.26" + end + end + end + + # Issue #3459 + def a_complicated_index + build_index do + gem "foo", %w(3.0.0 3.0.5) do + dep "qux", ["~> 3.1"] + dep "baz", ["< 9.0", ">= 5.0"] + dep "bar", ["~> 1.0"] + dep "grault", ["~> 3.1"] + end + + gem "foo", "1.2.1" do + dep "baz", ["~> 4.2"] + dep "bar", ["~> 1.0"] + dep "qux", ["~> 3.1"] + dep "grault", ["~> 2.0"] + end + + gem "bar", "1.0.5" do + dep "grault", ["~> 3.1"] + dep "baz", ["< 9", ">= 4.2"] + end + + gem "bar", "1.0.3" do + dep "baz", ["< 9", ">= 4.2"] + dep "grault", ["~> 2.0"] + end + + gem "baz", "8.2.10" do + dep "grault", ["~> 3.0"] + dep "garply", [">= 0.5.1", "~> 0.5"] + end + + gem "baz", "5.0.2" do + dep "grault", ["~> 2.0"] + dep "garply", [">= 0.3.1"] + end + + gem "baz", "4.2.0" do + dep "grault", ["~> 2.0"] + dep "garply", [">= 0.3.1"] + end + + gem "grault", %w(2.6.3 3.1.1) + + gem "garply", "0.5.1" do + dep "waldo", ["~> 0.1.3"] + end + + gem "waldo", "0.1.5" do + dep "plugh", ["~> 0.6.0"] + end + + gem "plugh", %w(0.6.3 0.6.11 0.7.0) + + gem "qux", "3.2.21" do + dep "plugh", [">= 0.6.4", "~> 0.6"] + dep "corge", ["~> 1.0"] + end + + gem "corge", "1.10.1" + end + end + + def a_unresovable_child_index + build_index do + gem "json", %w(1.8.0) + + gem("chef", "10.26") do + dep "json", [">= 1.4.4", "<= 1.7.7"] + end + + gem("berkshelf", "2.0.7") do + dep "json", ">= 1.7.7" + end + + gem("chef_app_error", "1.0.0") do + dep "berkshelf", "~> 2.0" + dep "chef", "~> 10.26" + end + end + end + + def a_index_with_root_conflict_on_child + build_index do + gem "builder", %w(2.1.2 3.0.1 3.1.3) + gem "i18n", %w(0.4.1 0.4.2) + + gem "activesupport", %w(3.0.0 3.0.1 3.0.5 3.1.7) + + gem("activemodel", "3.0.5") do + dep "activesupport", "= 3.0.5" + dep "builder", "~> 2.1.2" + dep "i18n", "~> 0.4" + end + + gem("activemodel", "3.0.0") do + dep "activesupport", "= 3.0.0" + dep "builder", "~> 2.1.2" + dep "i18n", "~> 0.4.1" + end + + gem("activemodel", "3.1.3") do + dep "activesupport", "= 3.1.3" + dep "builder", "~> 2.1.2" + dep "i18n", "~> 0.5" + end + + gem("activerecord", "3.0.0") do + dep "activesupport", "= 3.0.0" + dep "activemodel", "= 3.0.0" + end + + gem("activerecord", "3.0.5") do + dep "activesupport", "= 3.0.5" + dep "activemodel", "= 3.0.5" + end + + gem("activerecord", "3.0.9") do + dep "activesupport", "= 3.1.5" + dep "activemodel", "= 3.1.5" + end + end + end + + def a_circular_index + build_index do + gem "rack", "1.0.1" + gem("foo", "0.2.6") do + dep "bar", ">= 0" + end + + gem("bar", "1.0.0") do + dep "foo", ">= 0" + end + + gem("circular_app", "1.0.0") do + dep "foo", ">= 0" + dep "bar", ">= 0" + end + end + end + end +end diff --git a/spec/bundler/support/less_than_proc.rb b/spec/bundler/support/less_than_proc.rb new file mode 100644 index 0000000000..27966aa6ed --- /dev/null +++ b/spec/bundler/support/less_than_proc.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +class LessThanProc < Proc + attr_accessor :present + + def self.with(present) + provided = Gem::Version.new(present.dup) + new do |required| + if required =~ /[=><~]/ + !Gem::Requirement.new(required).satisfied_by?(provided) + else + provided < Gem::Version.new(required) + end + end.tap {|l| l.present = present } + end + + def inspect + "\"=< #{present}\"" + end +end diff --git a/spec/bundler/support/matchers.rb b/spec/bundler/support/matchers.rb new file mode 100644 index 0000000000..9248360639 --- /dev/null +++ b/spec/bundler/support/matchers.rb @@ -0,0 +1,222 @@ +# frozen_string_literal: true +require "forwardable" +require "support/the_bundle" +module Spec + module Matchers + extend RSpec::Matchers + + class Precondition + include RSpec::Matchers::Composable + extend Forwardable + def_delegators :failing_matcher, + :failure_message, + :actual, + :description, + :diffable?, + :expected, + :failure_message_when_negated + + def initialize(matcher, preconditions) + @matcher = with_matchers_cloned(matcher) + @preconditions = with_matchers_cloned(preconditions) + @failure_index = nil + end + + def matches?(target, &blk) + return false if @failure_index = @preconditions.index {|pc| !pc.matches?(target, &blk) } + @matcher.matches?(target, &blk) + end + + def does_not_match?(target, &blk) + return false if @failure_index = @preconditions.index {|pc| !pc.matches?(target, &blk) } + if @matcher.respond_to?(:does_not_match?) + @matcher.does_not_match?(target, &blk) + else + !@matcher.matches?(target, &blk) + end + end + + def expects_call_stack_jump? + @matcher.expects_call_stack_jump? || @preconditions.any?(&:expects_call_stack_jump) + end + + def supports_block_expectations? + @matcher.supports_block_expectations? || @preconditions.any?(&:supports_block_expectations) + end + + def failing_matcher + @failure_index ? @preconditions[@failure_index] : @matcher + end + end + + def self.define_compound_matcher(matcher, preconditions, &declarations) + raise "Must have preconditions to define a compound matcher" if preconditions.empty? + define_method(matcher) do |*expected, &block_arg| + Precondition.new( + RSpec::Matchers::DSL::Matcher.new(matcher, declarations, self, *expected, &block_arg), + preconditions + ) + end + end + + MAJOR_DEPRECATION = /^\[DEPRECATED FOR 2\.0\]\s*/ + + RSpec::Matchers.define :lack_errors do + diffable + match do |actual| + actual.gsub(/#{MAJOR_DEPRECATION}.+[\n]?/, "") == "" + end + end + + RSpec::Matchers.define :eq_err do |expected| + diffable + match do |actual| + actual.gsub(/#{MAJOR_DEPRECATION}.+[\n]?/, "") == expected + end + end + + RSpec::Matchers.define :have_major_deprecation do |expected| + diffable + match do |actual| + actual.split(MAJOR_DEPRECATION).any? do |d| + !d.empty? && values_match?(expected, d.strip) + end + end + end + + RSpec::Matchers.define :have_dep do |*args| + dep = Bundler::Dependency.new(*args) + + match do |actual| + actual.length == 1 && actual.all? {|d| d == dep } + end + end + + RSpec::Matchers.define :have_gem do |*args| + match do |actual| + actual.length == args.length && actual.all? {|a| args.include?(a.full_name) } + end + end + + RSpec::Matchers.define :have_rubyopts do |*args| + args = args.flatten + args = args.first.split(/\s+/) if args.size == 1 + + match do |actual| + actual = actual.split(/\s+/) if actual.is_a?(String) + args.all? {|arg| actual.include?(arg) } && actual.uniq.size == actual.size + end + end + + define_compound_matcher :read_as, [exist] do |file_contents| + diffable + + match do |actual| + @actual = Bundler.read_file(actual) + values_match?(file_contents, @actual) + end + end + + def indent(string, padding = 4, indent_character = " ") + string.to_s.gsub(/^/, indent_character * padding).gsub("\t", " ") + end + + define_compound_matcher :include_gems, [be_an_instance_of(Spec::TheBundle)] do |*names| + match do + opts = names.last.is_a?(Hash) ? names.pop : {} + source = opts.delete(:source) + groups = Array(opts[:groups]) + groups << opts + @errors = names.map do |name| + name, version, platform = name.split(/\s+/) + version_const = name == "bundler" ? "Bundler::VERSION" : Spec::Builders.constantize(name) + begin + run! "require '#{name}.rb'; puts #{version_const}", *groups + rescue => e + next "#{name} is not installed:\n#{indent(e)}" + end + out.gsub!(/#{MAJOR_DEPRECATION}.*$/, "") + actual_version, actual_platform = out.strip.split(/\s+/, 2) + unless Gem::Version.new(actual_version) == Gem::Version.new(version) + next "#{name} was expected to be at version #{version} but was #{actual_version}" + end + unless actual_platform == platform + next "#{name} was expected to be of platform #{platform} but was #{actual_platform}" + end + next unless source + begin + source_const = "#{Spec::Builders.constantize(name)}_SOURCE" + run! "require '#{name}/source'; puts #{source_const}", *groups + rescue + next "#{name} does not have a source defined:\n#{indent(e)}" + end + out.gsub!(/#{MAJOR_DEPRECATION}.*$/, "") + unless out.strip == source + next "Expected #{name} (#{version}) to be installed from `#{source}`, was actually from `#{out}`" + end + end.compact + + @errors.empty? + end + + match_when_negated do + opts = names.last.is_a?(Hash) ? names.pop : {} + groups = Array(opts[:groups]) || [] + @errors = names.map do |name| + name, version = name.split(/\s+/, 2) + begin + run <<-R, *(groups + [opts]) + begin + require '#{name}' + puts #{Spec::Builders.constantize(name)} + rescue LoadError, NameError + puts "WIN" + end + R + rescue => e + next "checking for #{name} failed:\n#{e}" + end + next if out == "WIN" + next "expected #{name} to not be installed, but it was" if version.nil? + if Gem::Version.new(out) == Gem::Version.new(version) + next "expected #{name} (#{version}) not to be installed, but it was" + end + end.compact + + @errors.empty? + end + + failure_message do + super() + " but:\n" + @errors.map {|e| indent(e) }.join("\n") + end + + failure_message_when_negated do + super() + " but:\n" + @errors.map {|e| indent(e) }.join("\n") + end + end + RSpec::Matchers.define_negated_matcher :not_include_gems, :include_gems + RSpec::Matchers.alias_matcher :include_gem, :include_gems + + def have_lockfile(expected) + read_as(strip_whitespace(expected)) + end + + def plugin_should_be_installed(*names) + names.each do |name| + expect(Bundler::Plugin).to be_installed(name) + path = Pathname.new(Bundler::Plugin.installed?(name)) + expect(path + "plugins.rb").to exist + end + end + + def plugin_should_not_be_installed(*names) + names.each do |name| + expect(Bundler::Plugin).not_to be_installed(name) + end + end + + def lockfile_should_be(expected) + expect(bundled_app("Gemfile.lock")).to read_as(strip_whitespace(expected)) + end + end +end diff --git a/spec/bundler/support/path.rb b/spec/bundler/support/path.rb new file mode 100644 index 0000000000..f28d660e83 --- /dev/null +++ b/spec/bundler/support/path.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true +require "pathname" + +module Spec + module Path + def root + if !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + # for Ruby Core + root_path = File.expand_path("../../../..", __FILE__) + else + root_path = File.expand_path("../../..", __FILE__) + end + @root ||= Pathname.new(root_path) + end + + def gemspec + if !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + # for Ruby Core + gemspec_path = File.expand_path(root.join("lib/bundler.gemspec"), __FILE__) + else + gemspec_path = File.expand_path(root.join("bundler.gemspec"), __FILE__) + end + @gemspec ||= Pathname.new(gemspec_path) + end + + def bindir + if !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + # for Ruby Core + bin_path = File.expand_path(root.join("bin"), __FILE__) + else + bin_path = File.expand_path(root.join("exe"), __FILE__) + end + @bindir ||= Pathname.new(bin_path) + end + + def spec_dir + if !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + # for Ruby Core + spec_path = File.expand_path(root.join("spec/bundler"), __FILE__) + else + spec_path = File.expand_path(root.join("spec"), __FILE__) + end + @spec_dir ||= Pathname.new(spec_path) + end + + def tmp(*path) + root.join("tmp", *path) + end + + def home(*path) + tmp.join("home", *path) + end + + def default_bundle_path(*path) + system_gem_path(*path) + end + + def bundled_app(*path) + root = tmp.join("bundled_app") + FileUtils.mkdir_p(root) + root.join(*path) + end + + alias_method :bundled_app1, :bundled_app + + def bundled_app2(*path) + root = tmp.join("bundled_app2") + FileUtils.mkdir_p(root) + root.join(*path) + end + + def vendored_gems(path = nil) + bundled_app(*["vendor/bundle", Gem.ruby_engine, Gem::ConfigMap[:ruby_version], path].compact) + end + + def cached_gem(path) + bundled_app("vendor/cache/#{path}.gem") + end + + def base_system_gems + tmp.join("gems/base") + end + + def gem_repo1(*args) + tmp("gems/remote1", *args) + end + + def gem_repo_missing(*args) + tmp("gems/missing", *args) + end + + def gem_repo2(*args) + tmp("gems/remote2", *args) + end + + def gem_repo3(*args) + tmp("gems/remote3", *args) + end + + def gem_repo4(*args) + tmp("gems/remote4", *args) + end + + def security_repo(*args) + tmp("gems/security_repo", *args) + end + + def system_gem_path(*path) + tmp("gems/system", *path) + end + + def lib_path(*args) + tmp("libs", *args) + end + + def bundler_path + Pathname.new(File.expand_path(root.join("lib"), __FILE__)) + end + + def global_plugin_gem(*args) + home ".bundle", "plugin", "gems", *args + end + + def local_plugin_gem(*args) + bundled_app ".bundle", "plugin", "gems", *args + end + + def tmpdir(*args) + tmp "tmpdir", *args + end + + extend self + end +end diff --git a/spec/bundler/support/permissions.rb b/spec/bundler/support/permissions.rb new file mode 100644 index 0000000000..f5636dd70a --- /dev/null +++ b/spec/bundler/support/permissions.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +module Spec + module Permissions + def with_umask(new_umask) + old_umask = File.umask(new_umask) + yield if block_given? + ensure + File.umask(old_umask) + end + end +end diff --git a/spec/bundler/support/platforms.rb b/spec/bundler/support/platforms.rb new file mode 100644 index 0000000000..a2a3afba00 --- /dev/null +++ b/spec/bundler/support/platforms.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true +module Spec + module Platforms + include Bundler::GemHelpers + + def rb + Gem::Platform::RUBY + end + + def mac + Gem::Platform.new("x86-darwin-10") + end + + def x64_mac + Gem::Platform.new("x86_64-darwin-15") + end + + def java + Gem::Platform.new([nil, "java", nil]) + end + + def linux + Gem::Platform.new(["x86", "linux", nil]) + end + + def mswin + Gem::Platform.new(["x86", "mswin32", nil]) + end + + def mingw + Gem::Platform.new(["x86", "mingw32", nil]) + end + + def x64_mingw + Gem::Platform.new(["x64", "mingw32", nil]) + end + + def all_platforms + [rb, java, linux, mswin, mingw, x64_mingw] + end + + def local + generic_local_platform + end + + def not_local + all_platforms.find {|p| p != generic_local_platform } + end + + def local_tag + if RUBY_PLATFORM == "java" + :jruby + else + :ruby + end + end + + def not_local_tag + [:ruby, :jruby].find {|tag| tag != local_tag } + end + + def local_ruby_engine + ENV["BUNDLER_SPEC_RUBY_ENGINE"] || (defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby") + end + + def local_engine_version + return ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] if ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] + + case local_ruby_engine + when "ruby" + RUBY_VERSION + when "rbx" + Rubinius::VERSION + when "jruby" + JRUBY_VERSION + else + raise BundlerError, "That RUBY_ENGINE is not recognized" + end + end + + def not_local_engine_version + case not_local_tag + when :ruby + not_local_ruby_version + when :jruby + "1.6.1" + end + end + + def not_local_ruby_version + "1.12" + end + + def not_local_patchlevel + 9999 + end + end +end diff --git a/spec/bundler/support/rubygems_ext.rb b/spec/bundler/support/rubygems_ext.rb new file mode 100644 index 0000000000..b484d63eab --- /dev/null +++ b/spec/bundler/support/rubygems_ext.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true +require "rubygems/user_interaction" +require "support/path" unless defined?(Spec::Path) + +module Spec + module Rubygems + DEPS = begin + deps = { + # rack 2.x requires Ruby version >= 2.2.2. + # artifice doesn't support rack 2.x now. + "rack" => "< 2", + # rack-test 0.7.0 dropped 1.8.7 support + # https://github.com/rack-test/rack-test/issues/193#issuecomment-314230318 + "rack-test" => "< 0.7.0", + "artifice" => "~> 0.6.0", + "compact_index" => "~> 0.11.0", + "sinatra" => "~> 1.4.7", + # Rake version has to be consistent for tests to pass + "rake" => "10.0.2", + # 3.0.0 breaks 1.9.2 specs + "builder" => "2.1.2", + "bundler" => "1.12.0", + } + # ruby-graphviz is used by the viz tests + deps["ruby-graphviz"] = nil if RUBY_VERSION >= "1.9.3" + deps + end + + def self.setup + Gem.clear_paths + + ENV["BUNDLE_PATH"] = nil + ENV["GEM_HOME"] = ENV["GEM_PATH"] = Path.base_system_gems.to_s + ENV["PATH"] = ["#{Path.root}/exe", "#{Path.system_gem_path}/bin", ENV["PATH"]].join(File::PATH_SEPARATOR) + + manifest = DEPS.to_a.sort_by(&:first).map {|k, v| "#{k} => #{v}\n" } + manifest_path = "#{Path.base_system_gems}/manifest.txt" + # it's OK if there are extra gems + if !File.exist?(manifest_path) || !(manifest - File.readlines(manifest_path)).empty? + FileUtils.rm_rf(Path.base_system_gems) + FileUtils.mkdir_p(Path.base_system_gems) + puts "installing gems for the tests to use..." + install_gems(DEPS) + File.open(manifest_path, "w") {|f| f << manifest.join } + end + + ENV["HOME"] = Path.home.to_s + ENV["TMPDIR"] = Path.tmpdir.to_s + + Gem::DefaultUserInteraction.ui = Gem::SilentUI.new + end + + def self.install_gems(gems) + reqs, no_reqs = gems.partition {|_, req| !req.nil? && !req.split(" ").empty? } + reqs = reqs.sort_by {|name, _| name == "rack" ? 0 : 1 } # TODO: remove when we drop ruby 1.8.7 support + no_reqs.map!(&:first) + reqs.map! {|name, req| "'#{name}:#{req}'" } + deps = reqs.concat(no_reqs).join(" ") + cmd = "gem install #{deps} --no-rdoc --no-ri --conservative" + puts cmd + system(cmd) || raise("Installing gems #{deps} for the tests to use failed!") + end + end +end diff --git a/spec/bundler/support/silent_logger.rb b/spec/bundler/support/silent_logger.rb new file mode 100644 index 0000000000..1a8f91b3ba --- /dev/null +++ b/spec/bundler/support/silent_logger.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +require "logger" +module Spec + class SilentLogger + (::Logger.instance_methods - Object.instance_methods).each do |logger_instance_method| + define_method(logger_instance_method) {|*args, &blk| } + end + end +end diff --git a/spec/bundler/support/sometimes.rb b/spec/bundler/support/sometimes.rb new file mode 100644 index 0000000000..6a50f5ff4c --- /dev/null +++ b/spec/bundler/support/sometimes.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true +module Sometimes + def run_with_retries(example_to_run, retries) + example = RSpec.current_example + example.metadata[:retries] ||= retries + + retries.times do |t| + example.metadata[:retried] = t + 1 + example.instance_variable_set(:@exception, nil) + example_to_run.run + break unless example.exception + end + + if e = example.exception + new_exception = e.exception(e.message + "[Retried #{retries} times]") + new_exception.set_backtrace e.backtrace + example.instance_variable_set(:@exception, new_exception) + end + end +end + +RSpec.configure do |config| + config.include Sometimes + config.alias_example_to :sometimes, :sometimes => true + config.add_setting :sometimes_retry_count, :default => 5 + + config.around(:each, :sometimes => true) do |example| + retries = example.metadata[:retries] || RSpec.configuration.sometimes_retry_count + run_with_retries(example, retries) + end + + config.after(:suite) do + message = proc do |color, text| + colored = RSpec::Core::Formatters::ConsoleCodes.wrap(text, color) + notification = RSpec::Core::Notifications::MessageNotification.new(colored) + formatter = RSpec.configuration.formatters.first + formatter.message(notification) if formatter.respond_to?(:message) + end + + retried_examples = RSpec.world.example_groups.map do |g| + g.descendants.map do |d| + d.filtered_examples.select do |e| + e.metadata[:sometimes] && e.metadata.fetch(:retried, 1) > 1 + end + end + end.flatten + + message.call(retried_examples.empty? ? :green : :yellow, "\n\nRetried examples: #{retried_examples.count}") + + retried_examples.each do |e| + message.call(:cyan, " #{e.full_description}") + path = RSpec::Core::Metadata.relative_path(e.location) + message.call(:cyan, " [#{e.metadata[:retried]}/#{e.metadata[:retries]}] " + path) + end + end +end diff --git a/spec/bundler/support/streams.rb b/spec/bundler/support/streams.rb new file mode 100644 index 0000000000..561b29092b --- /dev/null +++ b/spec/bundler/support/streams.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require "stringio" + +def capture(*streams) + streams.map!(&:to_s) + begin + result = StringIO.new + streams.each {|stream| eval "$#{stream} = result" } + yield + ensure + streams.each {|stream| eval("$#{stream} = #{stream.upcase}") } + end + result.string +end diff --git a/spec/bundler/support/sudo.rb b/spec/bundler/support/sudo.rb new file mode 100644 index 0000000000..8c82bb8c0f --- /dev/null +++ b/spec/bundler/support/sudo.rb @@ -0,0 +1,17 @@ +# 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 diff --git a/spec/bundler/support/the_bundle.rb b/spec/bundler/support/the_bundle.rb new file mode 100644 index 0000000000..742d393425 --- /dev/null +++ b/spec/bundler/support/the_bundle.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true +require "support/helpers" +require "support/path" + +module Spec + class TheBundle + include Spec::Helpers + include Spec::Path + + attr_accessor :bundle_dir + + def initialize(opts = {}) + opts = opts.dup + @bundle_dir = Pathname.new(opts.delete(:bundle_dir) { bundled_app }) + raise "Too many options! #{opts}" unless opts.empty? + end + + def to_s + "the bundle" + end + alias_method :inspect, :to_s + + def locked? + lockfile.file? + end + + def lockfile + bundle_dir.join("Gemfile.lock") + end + + def locked_gems + raise "Cannot read lockfile if it doesn't exist" unless locked? + Bundler::LockfileParser.new(lockfile.read) + end + end +end |