diff options
Diffstat (limited to 'spec/bundler/bundler')
51 files changed, 1783 insertions, 1105 deletions
diff --git a/spec/bundler/bundler/bundler_spec.rb b/spec/bundler/bundler/bundler_spec.rb index aeadcf9720..6a2e435e54 100644 --- a/spec/bundler/bundler/bundler_spec.rb +++ b/spec/bundler/bundler/bundler_spec.rb @@ -4,6 +4,71 @@ require "bundler" require "tmpdir" RSpec.describe Bundler do + describe "#load_marshal" do + it "is a private method and raises an error" do + data = Marshal.dump(Bundler) + expect { Bundler.load_marshal(data) }.to raise_error(NoMethodError, /private method [`']load_marshal' called/) + end + + it "loads any data" do + data = Marshal.dump(Bundler) + expect(Bundler.send(:load_marshal, data)).to eq(Bundler) + end + end + + describe "#safe_load_marshal" do + it "fails on unexpected class" do + data = Marshal.dump(Bundler) + expect { Bundler.safe_load_marshal(data) }.to raise_error(Bundler::MarshalError) + end + + it "loads simple structure" do + simple_structure = { "name" => [:development] } + data = Marshal.dump(simple_structure) + expect(Bundler.safe_load_marshal(data)).to eq(simple_structure) + end + + it "loads Gem::Specification" do + gem_spec = Gem::Specification.new do |s| + s.name = "bundler" + s.version = Gem::Version.new("2.4.7") + s.installed_by_version = Gem::Version.new("0") + s.authors = ["André Arko", + "Samuel Giddins", + "Colby Swandale", + "Hiroshi Shibata", + "David Rodríguez", + "Grey Baker", + "Stephanie Morillo", + "Chris Morris", + "James Wen", + "Tim Moore", + "André Medeiros", + "Jessica Lynn Suttles", + "Terence Lee", + "Carl Lerche", + "Yehuda Katz"] + s.date = Time.utc(2023, 2, 15) + s.description = "Bundler manages an application's dependencies through its entire life, across many machines, systematically and repeatably" + s.email = ["team@bundler.io"] + s.homepage = "https://bundler.io" + s.metadata = { "bug_tracker_uri" => "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3ABundler", + "changelog_uri" => "https://github.com/rubygems/rubygems/blob/master/bundler/CHANGELOG.md", + "homepage_uri" => "https://bundler.io/", + "source_code_uri" => "https://github.com/rubygems/rubygems/tree/master/bundler" } + s.require_paths = ["lib"] + s.required_ruby_version = Gem::Requirement.new([">= 2.6.0"]) + s.required_rubygems_version = Gem::Requirement.new([">= 3.0.1"]) + s.rubygems_version = "3.4.7" + s.specification_version = 4 + s.summary = "The best way to manage your application's dependencies" + s.license = false + end + data = Marshal.dump(gem_spec) + expect(Bundler.safe_load_marshal(data)).to eq(gem_spec) + end + end + describe "#load_gemspec_uncached" do let(:app_gemspec_path) { tmp("test.gemspec") } subject { Bundler.load_gemspec_uncached(app_gemspec_path) } @@ -11,7 +76,7 @@ RSpec.describe Bundler do context "with incorrect YAML file" do before do File.open(app_gemspec_path, "wb") do |f| - f.write strip_whitespace(<<-GEMSPEC) + f.write <<~GEMSPEC --- {:!00 ao=gu\g1= 7~f GEMSPEC @@ -23,7 +88,7 @@ RSpec.describe Bundler do end end - context "with correct YAML file", :if => defined?(Encoding) do + context "with correct YAML file", if: defined?(Encoding) do it "can load a gemspec with unicode characters with default ruby encoding" do # spec_helper forces the external encoding to UTF-8 but that's not the # default until Ruby 2.0 @@ -34,7 +99,7 @@ RSpec.describe Bundler do $VERBOSE = verbose File.open(app_gemspec_path, "wb") do |file| - file.puts <<-GEMSPEC.gsub(/^\s+/, "") + file.puts <<~GEMSPEC # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.author = "André the Giant" @@ -74,7 +139,7 @@ RSpec.describe Bundler do end GEMSPEC end - expect(Bundler.rubygems).to receive(:validate).with have_attributes(:name => "validated") + expect(Bundler.rubygems).to receive(:validate).with have_attributes(name: "validated") subject end end @@ -82,7 +147,7 @@ RSpec.describe Bundler do context "with gemspec containing local variables" do before do File.open(app_gemspec_path, "wb") do |f| - f.write strip_whitespace(<<-GEMSPEC) + f.write <<~GEMSPEC must_not_leak = true Gem::Specification.new do |gem| gem.name = "leak check" @@ -159,24 +224,6 @@ RSpec.describe Bundler do end end - describe "#rm_rf" do - context "the directory is world writable" do - let(:bundler_ui) { Bundler.ui } - it "should raise a friendly error" do - allow(File).to receive(:exist?).and_return(true) - allow(::Bundler::FileUtils).to receive(:remove_entry_secure).and_raise(ArgumentError) - allow(File).to receive(:world_writable?).and_return(true) - message = <<EOF -It is a security vulnerability to allow your home directory to be world-writable, and bundler can not continue. -You should probably consider fixing this issue by running `chmod o-w ~` on *nix. -Please refer to https://ruby-doc.org/stdlib-2.1.2/libdoc/fileutils/rdoc/FileUtils.html#method-c-remove_entry_secure for details. -EOF - expect(bundler_ui).to receive(:warn).with(message) - expect { Bundler.send(:rm_rf, bundled_app) }.to raise_error(Bundler::PathError) - end - end - end - describe "#mkdir_p" do it "creates a folder at the given path" do install_gemfile <<-G @@ -189,22 +236,6 @@ EOF Bundler.mkdir_p(bundled_app.join("foo", "bar")) expect(bundled_app.join("foo", "bar")).to exist end - - context "when mkdir_p requires sudo" do - it "creates a new folder using sudo" do - expect(Bundler).to receive(:requires_sudo?).and_return(true) - expect(Bundler).to receive(:sudo).and_return true - Bundler.mkdir_p(bundled_app.join("foo")) - end - end - - context "with :no_sudo option" do - it "forces mkdir_p to not use sudo" do - expect(Bundler).to receive(:requires_sudo?).and_return(true) - expect(Bundler).to_not receive(:sudo) - Bundler.mkdir_p(bundled_app.join("foo"), :no_sudo => true) - end - end end describe "#user_home" do @@ -268,118 +299,6 @@ EOF end end - describe "#requires_sudo?" do - let!(:tmpdir) { Dir.mktmpdir } - let(:bundle_path) { Pathname("#{tmpdir}/bundle") } - - def clear_cached_requires_sudo - return unless Bundler.instance_variable_defined?(:@requires_sudo_ran) - Bundler.remove_instance_variable(:@requires_sudo_ran) - Bundler.remove_instance_variable(:@requires_sudo) - end - - before do - clear_cached_requires_sudo - allow(Bundler).to receive(:which).with("sudo").and_return("/usr/bin/sudo") - allow(Bundler).to receive(:bundle_path).and_return(bundle_path) - end - - after do - FileUtils.rm_rf(tmpdir) - clear_cached_requires_sudo - end - - subject { Bundler.requires_sudo? } - - context "bundle_path doesn't exist" do - it { should be false } - - context "and parent dir can't be written" do - before do - FileUtils.chmod(0o500, tmpdir) - end - - it { should be true } - end - - context "with unwritable files in a parent dir" do - # Regression test for https://github.com/rubygems/bundler/pull/6316 - # It doesn't matter if there are other unwritable files so long as - # bundle_path can be created - before do - file = File.join(tmpdir, "unrelated_file") - FileUtils.touch(file) - FileUtils.chmod(0o400, file) - end - - it { should be false } - end - end - - context "bundle_path exists" do - before do - FileUtils.mkdir_p(bundle_path) - end - - it { should be false } - - context "and is unwritable" do - before do - FileUtils.chmod(0o500, bundle_path) - end - - it { should be true } - end - end - - context "path writability" do - before do - FileUtils.mkdir_p("tmp/vendor/bundle") - FileUtils.mkdir_p("tmp/vendor/bin_dir") - end - after do - FileUtils.rm_rf("tmp/vendor/bundle") - FileUtils.rm_rf("tmp/vendor/bin_dir") - end - context "writable paths" do - it "should return false and display nothing" do - allow(Bundler).to receive(:bundle_path).and_return(Pathname("tmp/vendor/bundle")) - expect(Bundler.ui).to_not receive(:warn) - expect(Bundler.requires_sudo?).to eq(false) - end - end - context "unwritable paths" do - before do - FileUtils.touch("tmp/vendor/bundle/unwritable1.txt") - FileUtils.touch("tmp/vendor/bundle/unwritable2.txt") - FileUtils.touch("tmp/vendor/bin_dir/unwritable3.txt") - FileUtils.chmod(0o400, "tmp/vendor/bundle/unwritable1.txt") - FileUtils.chmod(0o400, "tmp/vendor/bundle/unwritable2.txt") - FileUtils.chmod(0o400, "tmp/vendor/bin_dir/unwritable3.txt") - end - it "should return true and display warn message" do - allow(Bundler).to receive(:bundle_path).and_return(Pathname("tmp/vendor/bundle")) - bin_dir = Pathname("tmp/vendor/bin_dir/") - - # allow File#writable? to be called with args other than the stubbed on below - allow(File).to receive(:writable?).and_call_original - - # fake make the directory unwritable - allow(File).to receive(:writable?).with(bin_dir).and_return(false) - allow(Bundler).to receive(:system_bindir).and_return(Pathname("tmp/vendor/bin_dir/")) - message = <<-MESSAGE.chomp -Following files may not be writable, so sudo is needed: - tmp/vendor/bin_dir/ - tmp/vendor/bundle/unwritable1.txt - tmp/vendor/bundle/unwritable2.txt -MESSAGE - expect(Bundler.ui).to receive(:warn).with(message) - expect(Bundler.requires_sudo?).to eq(true) - end - end - end - end - context "user cache dir" do let(:home_path) { Pathname.new(ENV["HOME"]) } diff --git a/spec/bundler/bundler/ci_detector_spec.rb b/spec/bundler/bundler/ci_detector_spec.rb new file mode 100644 index 0000000000..299d8005e8 --- /dev/null +++ b/spec/bundler/bundler/ci_detector_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::CIDetector do + # This is properly tested in rubygems, under the name Gem::CIDetector + # But the test that confirms that our version _stays in sync_ with that version + # will live here. + + it "stays in sync with the rubygems implementation" do + rubygems_implementation_path = File.join(git_root, "lib", "rubygems", "ci_detector.rb") + expect(File.exist?(rubygems_implementation_path)).to be_truthy + rubygems_code = File.read(rubygems_implementation_path) + denamespaced_rubygems_code = rubygems_code.sub("Gem", "NAMESPACE") + + bundler_implementation_path = File.join(source_lib_dir, "bundler", "ci_detector.rb") + expect(File.exist?(bundler_implementation_path)).to be_truthy + bundler_code = File.read(bundler_implementation_path) + denamespaced_bundler_code = bundler_code.sub("Bundler", "NAMESPACE") + + expect(denamespaced_bundler_code).to eq(denamespaced_rubygems_code) + end +end diff --git a/spec/bundler/bundler/cli_spec.rb b/spec/bundler/bundler/cli_spec.rb index c5de12c211..c71fc8e9e7 100644 --- a/spec/bundler/bundler/cli_spec.rb +++ b/spec/bundler/bundler/cli_spec.rb @@ -4,12 +4,12 @@ require "bundler/cli" RSpec.describe "bundle executable" do it "returns non-zero exit status when passed unrecognized options" do - bundle "--invalid_argument", :raise_on_error => false + bundle "--invalid_argument", raise_on_error: false expect(exitstatus).to_not be_zero end it "returns non-zero exit status when passed unrecognized task" do - bundle "unrecognized-task", :raise_on_error => false + bundle "unrecognized-task", raise_on_error: false expect(exitstatus).to_not be_zero end @@ -87,7 +87,7 @@ RSpec.describe "bundle executable" do end context "with no arguments" do - it "prints a concise help message", :bundler => "3" do + it "prints a concise help message", bundler: "3" do bundle "" expect(err).to be_empty expect(out).to include("Bundler version #{Bundler::VERSION}"). @@ -105,7 +105,7 @@ RSpec.describe "bundle executable" do gem 'rack' G - bundle :install, :env => { "BUNDLE_GEMFILE" => "" } + bundle :install, env: { "BUNDLE_GEMFILE" => "" } expect(the_bundle).to include_gems "rack 1.0.0" end @@ -114,25 +114,71 @@ RSpec.describe "bundle executable" do context "with --verbose" do it "prints the running command" do gemfile "source \"#{file_uri_for(gem_repo1)}\"" - bundle "info bundler", :verbose => true + bundle "info bundler", verbose: true expect(out).to start_with("Running `bundle info bundler --verbose` with bundler #{Bundler::VERSION}") end it "doesn't print defaults" do - install_gemfile "source \"#{file_uri_for(gem_repo1)}\"", :verbose => true + install_gemfile "source \"#{file_uri_for(gem_repo1)}\"", verbose: true expect(out).to start_with("Running `bundle install --verbose` with bundler #{Bundler::VERSION}") end it "doesn't print defaults" do - install_gemfile "source \"#{file_uri_for(gem_repo1)}\"", :verbose => true + install_gemfile "source \"#{file_uri_for(gem_repo1)}\"", verbose: true expect(out).to start_with("Running `bundle install --verbose` with bundler #{Bundler::VERSION}") end end + describe "bundle outdated" do + let(:run_command) do + bundle "install" + + bundle "outdated #{flags}", raise_on_error: false + end + + before do + gemfile <<-G + source "#{file_uri_for(gem_repo1)}" + gem "rack", '0.9.1' + G + end + + context "with --groups flag" do + let(:flags) { "--groups" } + + it "prints a message when there are outdated gems" do + run_command + + expect(out).to include("Gem Current Latest Requested Groups") + expect(out).to include("rack 0.9.1 1.0.0 = 0.9.1 default") + end + end + + context "with --parseable" do + let(:flags) { "--parseable" } + + it "prints a message when there are outdated gems" do + run_command + + expect(out).to include("rack (newest 1.0.0, installed 0.9.1, requested = 0.9.1)") + end + end + + context "with --groups and --parseable" do + let(:flags) { "--groups --parseable" } + + it "prints a simplified message when there are outdated gems" do + run_command + + expect(out).to include("rack (newest 1.0.0, installed 0.9.1, requested = 0.9.1)") + end + end + end + describe "printing the outdated warning" do shared_examples_for "no warning" do it "prints no warning" do - bundle "fail", :env => { "BUNDLER_VERSION" => bundler_version }, :raise_on_error => false + bundle "fail", env: { "BUNDLER_VERSION" => bundler_version }, raise_on_error: false expect(last_command.stdboth).to eq("Could not find command \"fail\".") end end @@ -167,24 +213,24 @@ RSpec.describe "bundle executable" do context "when the latest version is greater than the current version" do let(:latest_version) { "222.0" } it "prints the version warning" do - bundle "fail", :env => { "BUNDLER_VERSION" => bundler_version }, :raise_on_error => false + bundle "fail", env: { "BUNDLER_VERSION" => bundler_version }, raise_on_error: false expect(err).to start_with(<<-EOS.strip) The latest bundler is #{latest_version}, but you are currently running #{bundler_version}. -To install the latest version, run `gem install bundler` +To update to the most recent version, run `bundle update --bundler` EOS end context "and disable_version_check is set" do - before { bundle "config set disable_version_check true", :env => { "BUNDLER_VERSION" => bundler_version } } + before { bundle "config set disable_version_check true", env: { "BUNDLER_VERSION" => bundler_version } } include_examples "no warning" end context "running a parseable command" do it "prints no warning" do - bundle "config get --parseable foo", :env => { "BUNDLER_VERSION" => bundler_version } + bundle "config get --parseable foo", env: { "BUNDLER_VERSION" => bundler_version } expect(last_command.stdboth).to eq "" - bundle "platform --ruby", :env => { "BUNDLER_VERSION" => bundler_version }, :raise_on_error => false + bundle "platform --ruby", env: { "BUNDLER_VERSION" => bundler_version }, raise_on_error: false expect(last_command.stdboth).to eq "Could not locate Gemfile" end end @@ -192,10 +238,10 @@ To install the latest version, run `gem install bundler` context "and is a pre-release" do let(:latest_version) { "222.0.0.pre.4" } it "prints the version warning" do - bundle "fail", :env => { "BUNDLER_VERSION" => bundler_version }, :raise_on_error => false + bundle "fail", env: { "BUNDLER_VERSION" => bundler_version }, raise_on_error: false expect(err).to start_with(<<-EOS.strip) The latest bundler is #{latest_version}, but you are currently running #{bundler_version}. -To install the latest version, run `gem install bundler --pre` +To update to the most recent version, run `bundle update --bundler` EOS end end @@ -204,12 +250,12 @@ To install the latest version, run `gem install bundler --pre` end RSpec.describe "bundler executable" do - it "shows the bundler version just as the `bundle` executable does", :bundler => "< 3" do + it "shows the bundler version just as the `bundle` executable does", bundler: "< 3" do bundler "--version" expect(out).to eq("Bundler version #{Bundler::VERSION}") end - it "shows the bundler version just as the `bundle` executable does", :bundler => "3" do + it "shows the bundler version just as the `bundle` executable does", bundler: "3" do bundler "--version" expect(out).to eq(Bundler::VERSION) end diff --git a/spec/bundler/bundler/compact_index_client/updater_spec.rb b/spec/bundler/bundler/compact_index_client/updater_spec.rb index 4acd7dbc63..6eed88ca9e 100644 --- a/spec/bundler/bundler/compact_index_client/updater_spec.rb +++ b/spec/bundler/bundler/compact_index_client/updater_spec.rb @@ -1,53 +1,224 @@ # frozen_string_literal: true -require "net/http" +require "bundler/vendored_net_http" require "bundler/compact_index_client" require "bundler/compact_index_client/updater" require "tmpdir" RSpec.describe Bundler::CompactIndexClient::Updater do + subject(:updater) { described_class.new(fetcher) } + let(:fetcher) { double(:fetcher) } - let(:local_path) { Pathname.new Dir.mktmpdir("localpath") } + let(:local_path) { Pathname.new(Dir.mktmpdir("localpath")).join("versions") } + let(:etag_path) { Pathname.new(Dir.mktmpdir("localpath-etags")).join("versions.etag") } let(:remote_path) { double(:remote_path) } - let!(:updater) { described_class.new(fetcher) } + let(:full_body) { "abc123" } + let(:response) { double(:response, body: full_body, is_a?: false) } + let(:digest) { Digest::SHA256.base64digest(full_body) } + + context "when the local path does not exist" do + before do + allow(response).to receive(:[]).with("Repr-Digest") { nil } + allow(response).to receive(:[]).with("Digest") { nil } + allow(response).to receive(:[]).with("ETag") { '"thisisanetag"' } + end + + it "downloads the file without attempting append" do + expect(fetcher).to receive(:call).once.with(remote_path, {}) { response } + + updater.update(remote_path, local_path, etag_path) + + expect(local_path.read).to eq(full_body) + expect(etag_path.read).to eq("thisisanetag") + end + + it "fails immediately on bad checksum" do + expect(fetcher).to receive(:call).once.with(remote_path, {}) { response } + allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:baddigest:" } + + expect do + updater.update(remote_path, local_path, etag_path) + end.to raise_error(Bundler::CompactIndexClient::Updater::MismatchedChecksumError) + end + end + + context "when the local path exists" do + let(:local_body) { "abc" } + + before do + local_path.open("w") {|f| f.write(local_body) } + end + + context "with an etag" do + before do + etag_path.open("w") {|f| f.write("LocalEtag") } + end + + let(:headers) do + { + "If-None-Match" => '"LocalEtag"', + "Range" => "bytes=2-", + } + end + + it "does nothing if etags match" do + expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response) + allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { false } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified) { true } + + updater.update(remote_path, local_path, etag_path) + + expect(local_path.read).to eq("abc") + expect(etag_path.read).to eq("LocalEtag") + end + + it "appends the file if etags do not match" do + expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response) + allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" } + allow(response).to receive(:[]).with("ETag") { '"NewEtag"' } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified) { false } + allow(response).to receive(:body) { "c123" } + + updater.update(remote_path, local_path, etag_path) + + expect(local_path.read).to eq(full_body) + expect(etag_path.read).to eq("NewEtag") + end + + it "replaces the file if response ignores range" do + expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response) + allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" } + allow(response).to receive(:[]).with("ETag") { '"NewEtag"' } + allow(response).to receive(:body) { full_body } + + updater.update(remote_path, local_path, etag_path) + + expect(local_path.read).to eq(full_body) + expect(etag_path.read).to eq("NewEtag") + end + + it "tries the request again if the partial response fails digest check" do + allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:baddigest:" } + allow(response).to receive(:body) { "the beginning of the file changed" } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true } + expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response) + + full_response = double(:full_response, body: full_body, is_a?: false) + allow(full_response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" } + allow(full_response).to receive(:[]).with("ETag") { '"NewEtag"' } + expect(fetcher).to receive(:call).once.with(remote_path, { "If-None-Match" => '"LocalEtag"' }).and_return(full_response) + + updater.update(remote_path, local_path, etag_path) + + expect(local_path.read).to eq(full_body) + expect(etag_path.read).to eq("NewEtag") + end + end + + context "without an etag file" do + let(:headers) do + { + "Range" => "bytes=2-", + # This MD5 feature should be deleted after sufficient time has passed since release. + # From then on, requests that still don't have a saved etag will be made without this header. + "If-None-Match" => %("#{Digest::MD5.hexdigest(local_body)}"), + } + end + + it "saves only the etag_path if generated etag matches" do + expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response) + allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { false } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified) { true } + + updater.update(remote_path, local_path, etag_path) + + expect(local_path.read).to eq("abc") + expect(%("#{etag_path.read}")).to eq(headers["If-None-Match"]) + end + + it "appends the file" do + expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response) + allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" } + allow(response).to receive(:[]).with("ETag") { '"OpaqueEtag"' } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified) { false } + allow(response).to receive(:body) { "c123" } + + updater.update(remote_path, local_path, etag_path) + + expect(local_path.read).to eq(full_body) + expect(etag_path.read).to eq("OpaqueEtag") + end + + it "replaces the file on full file response that ignores range request" do + expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response) + allow(response).to receive(:[]).with("Repr-Digest") { nil } + allow(response).to receive(:[]).with("Digest") { nil } + allow(response).to receive(:[]).with("ETag") { '"OpaqueEtag"' } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { false } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified) { false } + allow(response).to receive(:body) { full_body } + + updater.update(remote_path, local_path, etag_path) + + expect(local_path.read).to eq(full_body) + expect(etag_path.read).to eq("OpaqueEtag") + end + + it "tries the request again if the partial response fails digest check" do + allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:baddigest:" } + allow(response).to receive(:body) { "the beginning of the file changed" } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true } + expect(fetcher).to receive(:call).once.with(remote_path, headers) do + # During the failed first request, we simulate another process writing the etag. + # This ensures the second request doesn't generate the md5 etag again but just uses whatever is written. + etag_path.open("w") {|f| f.write("LocalEtag") } + response + end + + full_response = double(:full_response, body: full_body, is_a?: false) + allow(full_response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" } + allow(full_response).to receive(:[]).with("ETag") { '"NewEtag"' } + expect(fetcher).to receive(:call).once.with(remote_path, { "If-None-Match" => '"LocalEtag"' }).and_return(full_response) + + updater.update(remote_path, local_path, etag_path) + + expect(local_path.read).to eq(full_body) + expect(etag_path.read).to eq("NewEtag") + end + end + end context "when the ETag header is missing" do # Regression test for https://github.com/rubygems/bundler/issues/5463 - let(:response) { double(:response, :body => "abc123") } + let(:response) { double(:response, body: full_body) } it "treats the response as an update" do - expect(response).to receive(:[]).with("ETag") { nil } + allow(response).to receive(:[]).with("Repr-Digest") { nil } + allow(response).to receive(:[]).with("Digest") { nil } + allow(response).to receive(:[]).with("ETag") { nil } expect(fetcher).to receive(:call) { response } - updater.update(local_path, remote_path) + updater.update(remote_path, local_path, etag_path) end end context "when the download is corrupt" do - let(:response) { double(:response, :body => "") } + let(:response) { double(:response, body: "") } it "raises HTTPError" do expect(fetcher).to receive(:call).and_raise(Zlib::GzipFile::Error) expect do - updater.update(local_path, remote_path) + updater.update(remote_path, local_path, etag_path) end.to raise_error(Bundler::HTTPError) end end - context "when bundler doesn't have permissions on Dir.tmpdir" do - it "Errno::EACCES is raised" do - allow(Bundler::Dir).to receive(:mktmpdir) { raise Errno::EACCES } - - expect do - updater.update(local_path, remote_path) - end.to raise_error(Bundler::PermissionError) - end - end - context "when receiving non UTF-8 data and default internal encoding set to ASCII" do - let(:response) { double(:response, :body => "\x8B".b) } + let(:response) { double(:response, body: "\x8B".b) } it "works just fine" do old_verbose = $VERBOSE @@ -56,10 +227,12 @@ RSpec.describe Bundler::CompactIndexClient::Updater do begin $VERBOSE = false Encoding.default_internal = "ASCII" - expect(response).to receive(:[]).with("ETag") { nil } + allow(response).to receive(:[]).with("Repr-Digest") { nil } + allow(response).to receive(:[]).with("Digest") { nil } + allow(response).to receive(:[]).with("ETag") { nil } expect(fetcher).to receive(:call) { response } - updater.update(local_path, remote_path) + updater.update(remote_path, local_path, etag_path) ensure Encoding.default_internal = previous_internal_encoding $VERBOSE = old_verbose diff --git a/spec/bundler/bundler/definition_spec.rb b/spec/bundler/bundler/definition_spec.rb index bf000c468a..28c04e0860 100644 --- a/spec/bundler/bundler/definition_spec.rb +++ b/spec/bundler/bundler/definition_spec.rb @@ -5,61 +5,63 @@ require "bundler/definition" RSpec.describe Bundler::Definition do describe "#lock" do before do - allow(Bundler).to receive(:settings) { Bundler::Settings.new(".") } - allow(Bundler::SharedHelpers).to receive(:find_gemfile) { Pathname.new("Gemfile") } - allow(Bundler).to receive(:ui) { double("UI", :info => "", :debug => "") } + allow(Bundler::SharedHelpers).to receive(:find_gemfile) { bundled_app_gemfile } + allow(Bundler).to receive(:ui) { double("UI", info: "", debug: "") } end - context "when it's not possible to write to the file" do - subject { Bundler::Definition.new(nil, [], Bundler::SourceList.new, []) } + subject { Bundler::Definition.new(bundled_app_lock, [], Bundler::SourceList.new, {}) } + + context "when it's not possible to write to the file" do it "raises an PermissionError with explanation" do allow(File).to receive(:open).and_call_original - expect(File).to receive(:open).with("Gemfile.lock", "wb"). + expect(File).to receive(:open).with(bundled_app_lock, "wb"). and_raise(Errno::EACCES) - expect { subject.lock("Gemfile.lock") }. + expect { subject.lock }. to raise_error(Bundler::PermissionError, /Gemfile\.lock/) end end context "when a temporary resource access issue occurs" do - subject { Bundler::Definition.new(nil, [], Bundler::SourceList.new, []) } - it "raises a TemporaryResourceError with explanation" do allow(File).to receive(:open).and_call_original - expect(File).to receive(:open).with("Gemfile.lock", "wb"). + expect(File).to receive(:open).with(bundled_app_lock, "wb"). and_raise(Errno::EAGAIN) - expect { subject.lock("Gemfile.lock") }. + expect { subject.lock }. to raise_error(Bundler::TemporaryResourceError, /temporarily unavailable/) end end context "when Bundler::Definition.no_lock is set to true" do - subject { Bundler::Definition.new(nil, [], Bundler::SourceList.new, []) } before { Bundler::Definition.no_lock = true } after { Bundler::Definition.no_lock = false } it "does not create a lock file" do - subject.lock("Gemfile.lock") - expect(File.file?("Gemfile.lock")).to eq false + subject.lock + expect(bundled_app_lock).not_to be_file end end end describe "detects changes" do it "for a path gem with changes" do - build_lib "foo", "1.0", :path => lib_path("foo") + build_lib "foo", "1.0", path: lib_path("foo") install_gemfile <<-G source "#{file_uri_for(gem_repo1)}" gem "foo", :path => "#{lib_path("foo")}" G - build_lib "foo", "1.0", :path => lib_path("foo") do |s| + build_lib "foo", "1.0", path: lib_path("foo") do |s| s.add_dependency "rack", "1.0" end - bundle :install, :env => { "DEBUG" => "1" } + checksums = checksums_section_when_existing do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo1, "rack", "1.0.0" + end + + bundle :install, env: { "DEBUG" => "1" } expect(out).to match(/re-resolving dependencies/) - lockfile_should_be <<-G + expect(lockfile).to eq <<~G PATH remote: #{lib_path("foo")} specs: @@ -76,27 +78,47 @@ RSpec.describe Bundler::Definition do DEPENDENCIES foo! - + #{checksums} BUNDLED WITH #{Bundler::VERSION} G end + it "with an explicit update" do + build_repo4 do + build_gem("ffi", "1.9.23") {|s| s.platform = "java" } + build_gem("ffi", "1.9.23") + end + + gemfile <<-G + source "#{file_uri_for(gem_repo4)}" + gem "ffi" + G + + bundle "lock --add-platform java" + + bundle "update ffi", env: { "DEBUG" => "1" } + + expect(out).to match(/because bundler is unlocking gems: \(ffi\)/) + end + it "for a path gem with deps and no changes" do - build_lib "foo", "1.0", :path => lib_path("foo") do |s| + build_lib "foo", "1.0", path: lib_path("foo") do |s| s.add_dependency "rack", "1.0" s.add_development_dependency "net-ssh", "1.0" end + checksums = checksums_section_when_existing do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo1, "rack", "1.0.0" + end + install_gemfile <<-G source "#{file_uri_for(gem_repo1)}" gem "foo", :path => "#{lib_path("foo")}" G - bundle :check, :env => { "DEBUG" => "1" } - - expect(out).to match(/using resolution from the lockfile/) - lockfile_should_be <<-G + expected_lockfile = <<~G PATH remote: #{lib_path("foo")} specs: @@ -113,50 +135,64 @@ RSpec.describe Bundler::Definition do DEPENDENCIES foo! - + #{checksums} BUNDLED WITH #{Bundler::VERSION} G + + expect(lockfile).to eq(expected_lockfile) + + bundle :check, env: { "DEBUG" => "1" } + + expect(out).to match(/using resolution from the lockfile/) + expect(lockfile).to eq(expected_lockfile) end it "for a locked gem for another platform" do + checksums = checksums_section_when_existing do |c| + c.no_checksum "only_java", "1.1", "java" + end + install_gemfile <<-G source "#{file_uri_for(gem_repo1)}" gem "only_java", platform: :jruby G bundle "lock --add-platform java" - bundle :check, :env => { "DEBUG" => "1" } + bundle :check, env: { "DEBUG" => "1" } expect(out).to match(/using resolution from the lockfile/) - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GEM remote: #{file_uri_for(gem_repo1)}/ specs: only_java (1.1-java) PLATFORMS - java - #{lockfile_platforms} + #{lockfile_platforms("java")} DEPENDENCIES only_java - + #{checksums} BUNDLED WITH #{Bundler::VERSION} G end it "for a rubygems gem" do + checksums = checksums_section_when_existing do |c| + c.checksum gem_repo1, "foo", "1.0" + end + install_gemfile <<-G source "#{file_uri_for(gem_repo1)}" gem "foo" G - bundle :check, :env => { "DEBUG" => "1" } + bundle :check, env: { "DEBUG" => "1" } expect(out).to match(/using resolution from the lockfile/) - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GEM remote: #{file_uri_for(gem_repo1)}/ specs: @@ -167,7 +203,7 @@ RSpec.describe Bundler::Definition do DEPENDENCIES foo - + #{checksums} BUNDLED WITH #{Bundler::VERSION} G @@ -176,31 +212,6 @@ RSpec.describe Bundler::Definition do describe "initialize" do context "gem version promoter" do - context "with lockfile" do - before do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "foo" - G - - allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) - end - - it "should get a locked specs list when updating all" do - definition = Bundler::Definition.new(bundled_app_lock, [], Bundler::SourceList.new, true) - locked_specs = definition.gem_version_promoter.locked_specs - expect(locked_specs.to_a.map(&:name)).to eq ["foo"] - expect(definition.instance_variable_get("@locked_specs").empty?).to eq true - end - end - - context "without gemfile or lockfile" do - it "should not attempt to parse empty lockfile contents" do - definition = Bundler::Definition.new(nil, [], mock_source_list, true) - expect(definition.gem_version_promoter.locked_specs.to_a).to eq [] - end - end - context "eager unlock" do let(:source_list) do Bundler::SourceList.new.tap do |source_list| @@ -268,7 +279,7 @@ RSpec.describe Bundler::Definition do bundled_app_lock, updated_deps_in_gemfile, source_list, - :gems => ["shared_owner_a"], :conservative => true + gems: ["shared_owner_a"], conservative: true ) locked = definition.send(:converge_locked_specs).map(&:name) expect(locked).to eq %w[isolated_dep isolated_owner shared_dep shared_owner_b] diff --git a/spec/bundler/bundler/dep_proxy_spec.rb b/spec/bundler/bundler/dep_proxy_spec.rb deleted file mode 100644 index 8d02a33725..0000000000 --- a/spec/bundler/bundler/dep_proxy_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Bundler::DepProxy do - let(:dep) { Bundler::Dependency.new("rake", ">= 0") } - subject { described_class.get_proxy(dep, Gem::Platform::RUBY) } - let(:same) { subject } - let(:other) { described_class.get_proxy(dep, Gem::Platform::RUBY) } - let(:different) { described_class.get_proxy(dep, Gem::Platform::JAVA) } - - describe "#eql?" do - it { expect(subject.eql?(same)).to be true } - it { expect(subject.eql?(other)).to be true } - it { expect(subject.eql?(different)).to be false } - it { expect(subject.eql?(nil)).to be false } - it { expect(subject.eql?("foobar")).to be false } - end - - describe "must use factory methods" do - it { expect { described_class.new(dep, Gem::Platform::RUBY) }.to raise_error NoMethodError } - it { expect { subject.dup }.to raise_error NoMethodError } - it { expect { subject.clone }.to raise_error NoMethodError } - end - - describe "frozen" do - if Gem.ruby_version >= Gem::Version.new("2.5.0") - error = Object.const_get("FrozenError") - else - error = RuntimeError - end - it { expect { subject.instance_variable_set(:@__platform, {}) }.to raise_error error } - end -end diff --git a/spec/bundler/bundler/dependency_spec.rb b/spec/bundler/bundler/dependency_spec.rb new file mode 100644 index 0000000000..a953372742 --- /dev/null +++ b/spec/bundler/bundler/dependency_spec.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Dependency do + let(:options) do + {} + end + let(:dependency) do + described_class.new( + "test_gem", + "1.0.0", + options + ) + end + + describe "to_lock" do + it "returns formatted string" do + expect(dependency.to_lock).to eq(" test_gem (= 1.0.0)") + end + + it "matches format of Gem::Dependency#to_lock" do + gem_dependency = Gem::Dependency.new("test_gem", "1.0.0") + expect(dependency.to_lock).to eq(gem_dependency.to_lock) + end + + context "when source is passed" do + let(:options) do + { + "source" => Bundler::Source::Git.new({}), + } + end + + it "returns formatted string with exclamation mark" do + expect(dependency.to_lock).to eq(" test_gem (= 1.0.0)!") + end + end + end + + describe "PLATFORM_MAP" do + subject { described_class::PLATFORM_MAP } + + # rubocop:disable Naming/VariableNumber + let(:platforms) do + { ruby: Gem::Platform::RUBY, + ruby_18: Gem::Platform::RUBY, + ruby_19: Gem::Platform::RUBY, + ruby_20: Gem::Platform::RUBY, + ruby_21: Gem::Platform::RUBY, + ruby_22: Gem::Platform::RUBY, + ruby_23: Gem::Platform::RUBY, + ruby_24: Gem::Platform::RUBY, + ruby_25: Gem::Platform::RUBY, + ruby_26: Gem::Platform::RUBY, + ruby_27: Gem::Platform::RUBY, + ruby_30: Gem::Platform::RUBY, + ruby_31: Gem::Platform::RUBY, + ruby_32: Gem::Platform::RUBY, + ruby_33: Gem::Platform::RUBY, + ruby_34: Gem::Platform::RUBY, + mri: Gem::Platform::RUBY, + mri_18: Gem::Platform::RUBY, + mri_19: Gem::Platform::RUBY, + mri_20: Gem::Platform::RUBY, + mri_21: Gem::Platform::RUBY, + mri_22: Gem::Platform::RUBY, + mri_23: Gem::Platform::RUBY, + mri_24: Gem::Platform::RUBY, + mri_25: Gem::Platform::RUBY, + mri_26: Gem::Platform::RUBY, + mri_27: Gem::Platform::RUBY, + mri_30: Gem::Platform::RUBY, + mri_31: Gem::Platform::RUBY, + mri_32: Gem::Platform::RUBY, + mri_33: Gem::Platform::RUBY, + mri_34: Gem::Platform::RUBY, + rbx: Gem::Platform::RUBY, + truffleruby: Gem::Platform::RUBY, + jruby: Gem::Platform::JAVA, + jruby_18: Gem::Platform::JAVA, + jruby_19: Gem::Platform::JAVA, + windows: Gem::Platform::WINDOWS, + windows_18: Gem::Platform::WINDOWS, + windows_19: Gem::Platform::WINDOWS, + windows_20: Gem::Platform::WINDOWS, + windows_21: Gem::Platform::WINDOWS, + windows_22: Gem::Platform::WINDOWS, + windows_23: Gem::Platform::WINDOWS, + windows_24: Gem::Platform::WINDOWS, + windows_25: Gem::Platform::WINDOWS, + windows_26: Gem::Platform::WINDOWS, + windows_27: Gem::Platform::WINDOWS, + windows_30: Gem::Platform::WINDOWS, + windows_31: Gem::Platform::WINDOWS, + windows_32: Gem::Platform::WINDOWS, + windows_33: Gem::Platform::WINDOWS, + windows_34: Gem::Platform::WINDOWS } + end + + let(:deprecated) do + { mswin: Gem::Platform::MSWIN, + mswin_18: Gem::Platform::MSWIN, + mswin_19: Gem::Platform::MSWIN, + mswin_20: Gem::Platform::MSWIN, + mswin_21: Gem::Platform::MSWIN, + mswin_22: Gem::Platform::MSWIN, + mswin_23: Gem::Platform::MSWIN, + mswin_24: Gem::Platform::MSWIN, + mswin_25: Gem::Platform::MSWIN, + mswin_26: Gem::Platform::MSWIN, + mswin_27: Gem::Platform::MSWIN, + mswin_30: Gem::Platform::MSWIN, + mswin_31: Gem::Platform::MSWIN, + mswin_32: Gem::Platform::MSWIN, + mswin_33: Gem::Platform::MSWIN, + mswin_34: Gem::Platform::MSWIN, + mswin64: Gem::Platform::MSWIN64, + mswin64_19: Gem::Platform::MSWIN64, + mswin64_20: Gem::Platform::MSWIN64, + mswin64_21: Gem::Platform::MSWIN64, + mswin64_22: Gem::Platform::MSWIN64, + mswin64_23: Gem::Platform::MSWIN64, + mswin64_24: Gem::Platform::MSWIN64, + mswin64_25: Gem::Platform::MSWIN64, + mswin64_26: Gem::Platform::MSWIN64, + mswin64_27: Gem::Platform::MSWIN64, + mswin64_30: Gem::Platform::MSWIN64, + mswin64_31: Gem::Platform::MSWIN64, + mswin64_32: Gem::Platform::MSWIN64, + mswin64_33: Gem::Platform::MSWIN64, + mswin64_34: Gem::Platform::MSWIN64, + mingw: Gem::Platform::MINGW, + mingw_18: Gem::Platform::MINGW, + mingw_19: Gem::Platform::MINGW, + mingw_20: Gem::Platform::MINGW, + mingw_21: Gem::Platform::MINGW, + mingw_22: Gem::Platform::MINGW, + mingw_23: Gem::Platform::MINGW, + mingw_24: Gem::Platform::MINGW, + mingw_25: Gem::Platform::MINGW, + mingw_26: Gem::Platform::MINGW, + mingw_27: Gem::Platform::MINGW, + mingw_30: Gem::Platform::MINGW, + mingw_31: Gem::Platform::MINGW, + mingw_32: Gem::Platform::MINGW, + mingw_33: Gem::Platform::MINGW, + mingw_34: Gem::Platform::MINGW, + x64_mingw: Gem::Platform::X64_MINGW, + x64_mingw_20: Gem::Platform::X64_MINGW, + x64_mingw_21: Gem::Platform::X64_MINGW, + x64_mingw_22: Gem::Platform::X64_MINGW, + x64_mingw_23: Gem::Platform::X64_MINGW, + x64_mingw_24: Gem::Platform::X64_MINGW, + x64_mingw_25: Gem::Platform::X64_MINGW, + x64_mingw_26: Gem::Platform::X64_MINGW, + x64_mingw_27: Gem::Platform::X64_MINGW, + x64_mingw_30: Gem::Platform::X64_MINGW, + x64_mingw_31: Gem::Platform::X64_MINGW, + x64_mingw_32: Gem::Platform::X64_MINGW, + x64_mingw_33: Gem::Platform::X64_MINGW, + x64_mingw_34: Gem::Platform::X64_MINGW } + end + # rubocop:enable Naming/VariableNumber + + it "includes all platforms" do + expect(subject).to eq(platforms.merge(deprecated)) + end + end +end diff --git a/spec/bundler/bundler/digest_spec.rb b/spec/bundler/bundler/digest_spec.rb new file mode 100644 index 0000000000..f876827964 --- /dev/null +++ b/spec/bundler/bundler/digest_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "openssl" +require "bundler/digest" + +RSpec.describe Bundler::Digest do + context "SHA1" do + subject { Bundler::Digest } + let(:stdlib) { OpenSSL::Digest::SHA1 } + + it "is compatible with stdlib" do + random_strings = ["foo", "skfjsdlkfjsdf", "3924m", "ldskfj"] + + # https://www.rfc-editor.org/rfc/rfc3174#section-7.3 + rfc3174_test_cases = ["abc", "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", "a", "01234567" * 8] + + (random_strings + rfc3174_test_cases).each do |payload| + sha1 = subject.sha1(payload) + sha1_stdlib = stdlib.hexdigest(payload) + expect(sha1).to be == sha1_stdlib, "#{payload}'s sha1 digest (#{sha1}) did not match stlib's result (#{sha1_stdlib})" + end + end + end +end diff --git a/spec/bundler/bundler/dsl_spec.rb b/spec/bundler/bundler/dsl_spec.rb index 4d14949c89..3c3b6c26c3 100644 --- a/spec/bundler/bundler/dsl_spec.rb +++ b/spec/bundler/bundler/dsl_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Bundler::Dsl do it "registers custom hosts" do subject.git_source(:example) {|repo_name| "git@git.example.com:#{repo_name}.git" } subject.git_source(:foobar) {|repo_name| "git@foobar.com:#{repo_name}.git" } - subject.gem("dobry-pies", :example => "strzalek/dobry-pies") + subject.gem("dobry-pies", example: "strzalek/dobry-pies") example_uri = "git@git.example.com:strzalek/dobry-pies.git" expect(subject.dependencies.first.source.uri).to eq(example_uri) end @@ -25,47 +25,137 @@ RSpec.describe Bundler::Dsl do expect { subject.git_source(:example) }.to raise_error(Bundler::InvalidOption) end - context "default hosts", :bundler => "< 3" do + it "converts :github PR to URI using https" do + subject.gem("sparks", github: "https://github.com/indirect/sparks/pull/5") + github_uri = "https://github.com/indirect/sparks.git" + expect(subject.dependencies.first.source.uri).to eq(github_uri) + expect(subject.dependencies.first.source.ref).to eq("refs/pull/5/head") + end + + it "converts :gitlab PR to URI using https" do + subject.gem("sparks", gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5") + gitlab_uri = "https://gitlab.com/indirect/sparks.git" + expect(subject.dependencies.first.source.uri).to eq(gitlab_uri) + expect(subject.dependencies.first.source.ref).to eq("refs/merge-requests/5/head") + end + + it "rejects :github PR URI with a branch, ref or tag" do + expect do + subject.gem("sparks", github: "https://github.com/indirect/sparks/pull/5", branch: "foo") + end.to raise_error( + Bundler::GemfileError, + %(The :branch option can't be used with `github: "https://github.com/indirect/sparks/pull/5"`), + ) + + expect do + subject.gem("sparks", github: "https://github.com/indirect/sparks/pull/5", ref: "foo") + end.to raise_error( + Bundler::GemfileError, + %(The :ref option can't be used with `github: "https://github.com/indirect/sparks/pull/5"`), + ) + + expect do + subject.gem("sparks", github: "https://github.com/indirect/sparks/pull/5", tag: "foo") + end.to raise_error( + Bundler::GemfileError, + %(The :tag option can't be used with `github: "https://github.com/indirect/sparks/pull/5"`), + ) + end + + it "rejects :gitlab PR URI with a branch, ref or tag" do + expect do + subject.gem("sparks", gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5", branch: "foo") + end.to raise_error( + Bundler::GemfileError, + %(The :branch option can't be used with `gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5"`), + ) + + expect do + subject.gem("sparks", gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5", ref: "foo") + end.to raise_error( + Bundler::GemfileError, + %(The :ref option can't be used with `gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5"`), + ) + + expect do + subject.gem("sparks", gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5", tag: "foo") + end.to raise_error( + Bundler::GemfileError, + %(The :tag option can't be used with `gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5"`), + ) + end + + it "rejects :github with :git" do + expect do + subject.gem("sparks", github: "indirect/sparks", git: "https://github.com/indirect/sparks.git") + end.to raise_error( + Bundler::GemfileError, + %(The :git option can't be used with `github: "indirect/sparks"`), + ) + end + + it "rejects :gitlab with :git" do + expect do + subject.gem("sparks", gitlab: "indirect/sparks", git: "https://gitlab.com/indirect/sparks.git") + end.to raise_error( + Bundler::GemfileError, + %(The :git option can't be used with `gitlab: "indirect/sparks"`), + ) + end + + context "default hosts", bundler: "< 3" do it "converts :github to URI using https" do - subject.gem("sparks", :github => "indirect/sparks") + subject.gem("sparks", github: "indirect/sparks") github_uri = "https://github.com/indirect/sparks.git" expect(subject.dependencies.first.source.uri).to eq(github_uri) end it "converts :github shortcut to URI using https" do - subject.gem("sparks", :github => "rails") + subject.gem("sparks", github: "rails") github_uri = "https://github.com/rails/rails.git" expect(subject.dependencies.first.source.uri).to eq(github_uri) end + it "converts :gitlab to URI using https" do + subject.gem("sparks", gitlab: "indirect/sparks") + gitlab_uri = "https://gitlab.com/indirect/sparks.git" + expect(subject.dependencies.first.source.uri).to eq(gitlab_uri) + end + + it "converts :gitlab shortcut to URI using https" do + subject.gem("sparks", gitlab: "rails") + gitlab_uri = "https://gitlab.com/rails/rails.git" + expect(subject.dependencies.first.source.uri).to eq(gitlab_uri) + end + it "converts numeric :gist to :git" do - subject.gem("not-really-a-gem", :gist => 2_859_988) + subject.gem("not-really-a-gem", gist: 2_859_988) github_uri = "https://gist.github.com/2859988.git" expect(subject.dependencies.first.source.uri).to eq(github_uri) end it "converts :gist to :git" do - subject.gem("not-really-a-gem", :gist => "2859988") + subject.gem("not-really-a-gem", gist: "2859988") github_uri = "https://gist.github.com/2859988.git" expect(subject.dependencies.first.source.uri).to eq(github_uri) end it "converts :bitbucket to :git" do - subject.gem("not-really-a-gem", :bitbucket => "mcorp/flatlab-rails") + subject.gem("not-really-a-gem", bitbucket: "mcorp/flatlab-rails") bitbucket_uri = "https://mcorp@bitbucket.org/mcorp/flatlab-rails.git" expect(subject.dependencies.first.source.uri).to eq(bitbucket_uri) end it "converts 'mcorp' to 'mcorp/mcorp'" do - subject.gem("not-really-a-gem", :bitbucket => "mcorp") + subject.gem("not-really-a-gem", bitbucket: "mcorp") bitbucket_uri = "https://mcorp@bitbucket.org/mcorp/mcorp.git" expect(subject.dependencies.first.source.uri).to eq(bitbucket_uri) end end context "default git sources" do - it "has bitbucket, gist, and github" do - expect(subject.instance_variable_get(:@git_sources).keys.sort).to eq(%w[bitbucket gist github]) + it "has bitbucket, gist, github, and gitlab" do + expect(subject.instance_variable_get(:@git_sources).keys.sort).to eq(%w[bitbucket gist github gitlab]) end end end @@ -95,18 +185,37 @@ RSpec.describe Bundler::Dsl do expect { subject.eval_gemfile("Gemfile") }. to raise_error(Bundler::GemfileError, /There was an error evaluating `Gemfile`: ruby_version must match the :engine_version for MRI/) end + + it "populates __dir__ and __FILE__ correctly" do + abs_path = source_root.join("../fragment.rb").to_s + expect(Bundler).to receive(:read_file).with(abs_path).and_return(<<~RUBY) + @fragment_dir = __dir__ + @fragment_file = __FILE__ + RUBY + subject.eval_gemfile("../fragment.rb") + expect(subject.instance_variable_get(:@fragment_dir)).to eq(source_root.dirname.to_s) + expect(subject.instance_variable_get(:@fragment_file)).to eq(abs_path) + end end describe "#gem" do - [:ruby, :ruby_18, :ruby_19, :ruby_20, :ruby_21, :ruby_22, :ruby_23, :ruby_24, :ruby_25, :ruby_26, :mri, :mri_18, :mri_19, - :mri_20, :mri_21, :mri_22, :mri_23, :mri_24, :mri_25, :mri_26, :jruby, :rbx, :truffleruby].each do |platform| + # rubocop:disable Naming/VariableNumber + [:ruby, :ruby_18, :ruby_19, :ruby_20, :ruby_21, :ruby_22, :ruby_23, :ruby_24, :ruby_25, :ruby_26, :ruby_27, + :ruby_30, :ruby_31, :ruby_32, :ruby_33, :mri, :mri_18, :mri_19, :mri_20, :mri_21, :mri_22, :mri_23, :mri_24, + :mri_25, :mri_26, :mri_27, :mri_30, :mri_31, :mri_32, :mri_33, :jruby, :rbx, :truffleruby].each do |platform| it "allows #{platform} as a valid platform" do - subject.gem("foo", :platform => platform) + subject.gem("foo", platform: platform) end end + # rubocop:enable Naming/VariableNumber + + it "allows platforms matching the running Ruby version" do + platform = "ruby_#{RbConfig::CONFIG["MAJOR"]}#{RbConfig::CONFIG["MINOR"]}" + subject.gem("foo", platform: platform) + end it "rejects invalid platforms" do - expect { subject.gem("foo", :platform => :bogus) }. + expect { subject.gem("foo", platform: :bogus) }. to raise_error(Bundler::GemfileError, /is not a valid platform/) end @@ -156,19 +265,19 @@ RSpec.describe Bundler::Dsl do end it "rejects branch option on non-git gems" do - expect { subject.gem("foo", :branch => "test") }. + expect { subject.gem("foo", branch: "test") }. to raise_error(Bundler::GemfileError, /The `branch` option for `gem 'foo'` is not allowed. Only gems with a git source can specify a branch/) end it "allows specifying a branch on git gems" do - subject.gem("foo", :branch => "test", :git => "http://mytestrepo") + subject.gem("foo", branch: "test", git: "http://mytestrepo") dep = subject.dependencies.last expect(dep.name).to eq "foo" end it "allows specifying a branch on git gems with a git_source" do subject.git_source(:test_source) {|n| "https://github.com/#{n}" } - subject.gem("foo", :branch => "test", :test_source => "bundler/bundler") + subject.gem("foo", branch: "test", test_source: "bundler/bundler") dep = subject.dependencies.last expect(dep.name).to eq "foo" end @@ -233,7 +342,7 @@ RSpec.describe Bundler::Dsl do allow(Bundler).to receive(:default_gemfile).and_return(Pathname.new("./Gemfile")) subject.source("https://other-source.org") do - subject.gem("dobry-pies", :path => "foo") + subject.gem("dobry-pies", path: "foo") subject.gem("foo") end @@ -245,7 +354,7 @@ RSpec.describe Bundler::Dsl do describe "#check_primary_source_safety" do context "when a global source is not defined implicitly" do it "will raise a major deprecation warning" do - not_a_global_source = double("not-a-global-source", :no_remotes? => true) + not_a_global_source = double("not-a-global-source", no_remotes?: true) allow(Bundler::Source::Rubygems).to receive(:new).and_return(not_a_global_source) warning = "This Gemfile does not include an explicit global source. " \ diff --git a/spec/bundler/bundler/endpoint_specification_spec.rb b/spec/bundler/bundler/endpoint_specification_spec.rb index 2e2c16ec44..e7e10730cf 100644 --- a/spec/bundler/bundler/endpoint_specification_spec.rb +++ b/spec/bundler/bundler/endpoint_specification_spec.rb @@ -5,9 +5,10 @@ RSpec.describe Bundler::EndpointSpecification do let(:version) { "1.0.0" } let(:platform) { Gem::Platform::RUBY } let(:dependencies) { [] } + let(:spec_fetcher) { double(:spec_fetcher) } let(:metadata) { nil } - subject(:spec) { described_class.new(name, version, platform, dependencies, metadata) } + subject(:spec) { described_class.new(name, version, platform, spec_fetcher, dependencies, metadata) } describe "#build_dependency" do let(:name) { "foo" } @@ -47,8 +48,35 @@ RSpec.describe Bundler::EndpointSpecification do end end + describe "#required_ruby_version" do + context "required_ruby_version is already set on endpoint specification" do + existing_value = "already set value" + let(:required_ruby_version) { existing_value } + + it "should return the current value when already set on endpoint specification" do + expect(spec.required_ruby_version). eql?(existing_value) + end + end + + it "should return the remote spec value when not set on endpoint specification and remote spec has one" do + remote_value = "remote_value" + remote_spec = double(:remote_spec, required_ruby_version: remote_value, required_rubygems_version: nil) + allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec) + + expect(spec.required_ruby_version). eql?(remote_value) + end + + it "should use the default Gem Requirement value when not set on endpoint specification and not set on remote spec" do + remote_spec = double(:remote_spec, required_ruby_version: nil, required_rubygems_version: nil) + allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec) + expect(spec.required_ruby_version). eql?(Gem::Requirement.default) + end + end + it "supports equality comparison" do - other_spec = described_class.new("bar", version, platform, dependencies, metadata) + remote_spec = double(:remote_spec, required_ruby_version: nil, required_rubygems_version: nil) + allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec) + other_spec = described_class.new("bar", version, platform, spec_fetcher, dependencies, metadata) expect(spec).to eql(spec) expect(spec).to_not eql(other_spec) end diff --git a/spec/bundler/bundler/env_spec.rb b/spec/bundler/bundler/env_spec.rb index a6f4b2ba85..7997cb0c40 100644 --- a/spec/bundler/bundler/env_spec.rb +++ b/spec/bundler/bundler/env_spec.rb @@ -4,7 +4,7 @@ require "bundler/settings" require "openssl" RSpec.describe Bundler::Env do - let(:git_proxy_stub) { Bundler::Source::Git::GitProxy.new(nil, nil, nil) } + let(:git_proxy_stub) { Bundler::Source::Git::GitProxy.new(nil, nil) } describe "#report" do it "prints the environment" do @@ -34,8 +34,6 @@ RSpec.describe Bundler::Env do end it "prints user home" do - skip "needs to use a valid HOME" if Gem.win_platform? && RUBY_VERSION < "2.6.0" - with_clear_paths("HOME", "/a/b/c") do out = described_class.report expect(out).to include("User Home /a/b/c") @@ -43,8 +41,6 @@ RSpec.describe Bundler::Env do end it "prints user path" do - skip "needs to use a valid HOME" if Gem.win_platform? && RUBY_VERSION < "2.6.0" - with_clear_paths("HOME", "/a/b/c") do allow(File).to receive(:exist?) allow(File).to receive(:exist?).with("/a/b/c/.gem").and_return(true) @@ -92,7 +88,7 @@ RSpec.describe Bundler::Env do allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) end - let(:output) { described_class.report(:print_gemfile => true) } + let(:output) { described_class.report(print_gemfile: true) } it "prints the Gemfile" do expect(output).to include("Gemfile") @@ -106,7 +102,7 @@ RSpec.describe Bundler::Env do end context "when there no Gemfile and print_gemfile is true" do - let(:output) { described_class.report(:print_gemfile => true) } + let(:output) { described_class.report(print_gemfile: true) } it "prints the environment" do expect(output).to start_with("## Environment") @@ -118,7 +114,7 @@ RSpec.describe Bundler::Env do bundle "config set https://localgemserver.test/ user:pass" end - let(:output) { described_class.report(:print_gemfile => true) } + let(:output) { described_class.report(print_gemfile: true) } it "prints the config with redacted values" do expect(output).to include("https://localgemserver.test") @@ -132,7 +128,7 @@ RSpec.describe Bundler::Env do bundle "config set https://localgemserver.test/ api_token:x-oauth-basic" end - let(:output) { described_class.report(:print_gemfile => true) } + let(:output) { described_class.report(print_gemfile: true) } it "prints the config with redacted values" do expect(output).to include("https://localgemserver.test") @@ -143,7 +139,7 @@ RSpec.describe Bundler::Env do context "when Gemfile contains a gemspec and print_gemspecs is true" do let(:gemspec) do - strip_whitespace(<<-GEMSPEC) + <<~GEMSPEC Gem::Specification.new do |gem| gem.name = "foo" gem.author = "Fumofu" @@ -162,7 +158,7 @@ RSpec.describe Bundler::Env do end it "prints the gemspec" do - output = described_class.report(:print_gemspecs => true) + output = described_class.report(print_gemspecs: true) expect(output).to include("foo.gemspec") expect(output).to include(gemspec) @@ -181,8 +177,8 @@ RSpec.describe Bundler::Env do allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) allow(Bundler::SharedHelpers).to receive(:pwd).and_return(bundled_app) - output = described_class.report(:print_gemspecs => true) - expect(output).to include(strip_whitespace(<<-ENV)) + output = described_class.report(print_gemspecs: true) + expect(output).to include(<<~ENV) ## Gemfile ### Gemfile @@ -221,7 +217,7 @@ RSpec.describe Bundler::Env do context "when the git version is OS specific" do it "includes OS specific information with the version number" do - expect(git_proxy_stub).to receive(:git).with("--version"). + expect(git_proxy_stub).to receive(:git_local).with("--version"). and_return("git version 1.2.3 (Apple Git-BS)") expect(Bundler::Source::Git::GitProxy).to receive(:new).and_return(git_proxy_stub) diff --git a/spec/bundler/bundler/environment_preserver_spec.rb b/spec/bundler/bundler/environment_preserver_spec.rb index 530ca6f835..6c7066d0c6 100644 --- a/spec/bundler/bundler/environment_preserver_spec.rb +++ b/spec/bundler/bundler/environment_preserver_spec.rb @@ -27,8 +27,12 @@ RSpec.describe Bundler::EnvironmentPreserver do context "when a key is empty" do let(:env) { { "foo" => "" } } - it "should not create backup entries" do - expect(subject).not_to have_key "BUNDLER_ORIG_foo" + it "should keep the original entry" do + expect(subject["foo"]).to be_empty + end + + it "should still create backup entries" do + expect(subject).to have_key "BUNDLER_ORIG_foo" end end @@ -71,8 +75,12 @@ RSpec.describe Bundler::EnvironmentPreserver do context "when the original key is empty" do let(:env) { { "foo" => "my-foo", "BUNDLER_ORIG_foo" => "" } } - it "should keep the current value" do - expect(subject["foo"]).to eq("my-foo") + it "should restore the original value" do + expect(subject["foo"]).to be_empty + end + + it "should delete the backup value" do + expect(subject.key?("BUNDLER_ORIG_foo")).to eq(false) end end end diff --git a/spec/bundler/bundler/fetcher/base_spec.rb b/spec/bundler/bundler/fetcher/base_spec.rb index 02506591f3..b8c6b57b10 100644 --- a/spec/bundler/bundler/fetcher/base_spec.rb +++ b/spec/bundler/bundler/fetcher/base_spec.rb @@ -4,15 +4,16 @@ RSpec.describe Bundler::Fetcher::Base do let(:downloader) { double(:downloader) } let(:remote) { double(:remote) } let(:display_uri) { "http://sample_uri.com" } + let(:gem_remote_fetcher) { nil } class TestClass < described_class; end - subject { TestClass.new(downloader, remote, display_uri) } + subject { TestClass.new(downloader, remote, display_uri, gem_remote_fetcher) } describe "#initialize" do context "with the abstract Base class" do it "should raise an error" do - expect { described_class.new(downloader, remote, display_uri) }.to raise_error(RuntimeError, "Abstract class") + expect { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) }.to raise_error(RuntimeError, "Abstract class") end end @@ -36,7 +37,7 @@ RSpec.describe Bundler::Fetcher::Base do end describe "#fetch_uri" do - let(:remote_uri_obj) { Bundler::URI("http://rubygems.org") } + let(:remote_uri_obj) { Gem::URI("http://rubygems.org") } before { allow(subject).to receive(:remote_uri).and_return(remote_uri_obj) } @@ -49,10 +50,10 @@ RSpec.describe Bundler::Fetcher::Base do end context "when the remote uri's host is not rubygems.org" do - let(:remote_uri_obj) { Bundler::URI("http://otherhost.org") } + let(:remote_uri_obj) { Gem::URI("http://otherhost.org") } it "should return the remote uri" do - expect(subject.fetch_uri).to eq(Bundler::URI("http://otherhost.org")) + expect(subject.fetch_uri).to eq(Gem::URI("http://otherhost.org")) end end diff --git a/spec/bundler/bundler/fetcher/compact_index_spec.rb b/spec/bundler/bundler/fetcher/compact_index_spec.rb index 00eb27edea..a988171f34 100644 --- a/spec/bundler/bundler/fetcher/compact_index_spec.rb +++ b/spec/bundler/bundler/fetcher/compact_index_spec.rb @@ -5,9 +5,10 @@ require "bundler/compact_index_client" RSpec.describe Bundler::Fetcher::CompactIndex do let(:downloader) { double(:downloader) } - let(:display_uri) { Bundler::URI("http://sampleuri.com") } - let(:remote) { double(:remote, :cache_slug => "lsjdf", :uri => display_uri) } - let(:compact_index) { described_class.new(downloader, remote, display_uri) } + let(:display_uri) { Gem::URI("http://sampleuri.com") } + let(:remote) { double(:remote, cache_slug: "lsjdf", uri: display_uri) } + let(:gem_remote_fetcher) { nil } + let(:compact_index) { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) } before do allow(compact_index).to receive(:log_specs) {} @@ -33,7 +34,7 @@ RSpec.describe Bundler::Fetcher::CompactIndex do describe "#available?" do before do allow(compact_index).to receive(:compact_index_client). - and_return(double(:compact_index_client, :update_and_parse_checksums! => true)) + and_return(double(:compact_index_client, update_and_parse_checksums!: true)) end it "returns true" do diff --git a/spec/bundler/bundler/fetcher/dependency_spec.rb b/spec/bundler/bundler/fetcher/dependency_spec.rb index 53249116cd..c420b7c07f 100644 --- a/spec/bundler/bundler/fetcher/dependency_spec.rb +++ b/spec/bundler/bundler/fetcher/dependency_spec.rb @@ -2,10 +2,11 @@ RSpec.describe Bundler::Fetcher::Dependency do let(:downloader) { double(:downloader) } - let(:remote) { double(:remote, :uri => Bundler::URI("http://localhost:5000")) } + let(:remote) { double(:remote, uri: Gem::URI("http://localhost:5000")) } let(:display_uri) { "http://sample_uri.com" } + let(:gem_remote_fetcher) { nil } - subject { described_class.new(downloader, remote, display_uri) } + subject { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) } describe "#available?" do let(:dependency_api_uri) { double(:dependency_api_uri) } @@ -155,9 +156,9 @@ RSpec.describe Bundler::Fetcher::Dependency do end end - shared_examples_for "the error suggests retrying with the full index" do - it "should log the inability to fetch from API at debug level" do - expect(Bundler).to receive_message_chain(:ui, :debug).with("could not fetch from the dependency API\nit's suggested to retry using the full index via `bundle install --full-index`") + shared_examples_for "the error is logged" do + it "should log the inability to fetch from API at debug level, and mention retrying" do + expect(Bundler).to receive_message_chain(:ui, :debug).with("could not fetch from the dependency API, trying the full index") subject.specs(gem_names, full_dependency_list, last_spec_list) end end @@ -166,25 +167,21 @@ RSpec.describe Bundler::Fetcher::Dependency do before { allow(subject).to receive(:dependency_specs) { raise Bundler::HTTPError.new } } it_behaves_like "the error is properly handled" - it_behaves_like "the error suggests retrying with the full index" + it_behaves_like "the error is logged" end context "when a GemspecError occurs" do before { allow(subject).to receive(:dependency_specs) { raise Bundler::GemspecError.new } } it_behaves_like "the error is properly handled" - it_behaves_like "the error suggests retrying with the full index" + it_behaves_like "the error is logged" end context "when a MarshalError occurs" do before { allow(subject).to receive(:dependency_specs) { raise Bundler::MarshalError.new } } it_behaves_like "the error is properly handled" - - it "should log the inability to fetch from API and mention retrying" do - expect(Bundler).to receive_message_chain(:ui, :debug).with("could not fetch from the dependency API, trying the full index") - subject.specs(gem_names, full_dependency_list, last_spec_list) - end + it_behaves_like "the error is logged" end end @@ -214,7 +211,7 @@ RSpec.describe Bundler::Fetcher::Dependency do let(:gem_names) { [%w[foo bar], %w[bundler rubocop]] } let(:dep_api_uri) { double(:dep_api_uri) } let(:unmarshalled_gems) { double(:unmarshalled_gems) } - let(:fetch_response) { double(:fetch_response, :body => double(:body)) } + let(:fetch_response) { double(:fetch_response, body: double(:body)) } let(:rubygems_limit) { 50 } before { allow(subject).to receive(:dependency_api_uri).with(gem_names).and_return(dep_api_uri) } @@ -222,7 +219,7 @@ RSpec.describe Bundler::Fetcher::Dependency do it "should fetch dependencies from RubyGems and unmarshal them" do expect(gem_names).to receive(:each_slice).with(rubygems_limit).and_call_original expect(downloader).to receive(:fetch).with(dep_api_uri).and_return(fetch_response) - expect(Bundler).to receive(:load_marshal).with(fetch_response.body).and_return([unmarshalled_gems]) + expect(Bundler).to receive(:safe_load_marshal).with(fetch_response.body).and_return([unmarshalled_gems]) expect(subject.unmarshalled_dep_gems(gem_names)).to eq([unmarshalled_gems]) end end @@ -231,20 +228,20 @@ RSpec.describe Bundler::Fetcher::Dependency do let(:gem_list) do [ { - :dependencies => { + dependencies: { "resque" => "req3,req4", }, - :name => "typhoeus", - :number => "1.0.1", - :platform => "ruby", + name: "typhoeus", + number: "1.0.1", + platform: "ruby", }, { - :dependencies => { + dependencies: { "faraday" => "req1,req2", }, - :name => "grape", - :number => "2.0.2", - :platform => "jruby", + name: "grape", + number: "2.0.2", + platform: "jruby", }, ] end @@ -258,7 +255,7 @@ RSpec.describe Bundler::Fetcher::Dependency do end describe "#dependency_api_uri" do - let(:uri) { Bundler::URI("http://gem-api.com") } + let(:uri) { Gem::URI("http://gem-api.com") } context "with gem names" do let(:gem_names) { %w[foo bar bundler rubocop] } diff --git a/spec/bundler/bundler/fetcher/downloader_spec.rb b/spec/bundler/bundler/fetcher/downloader_spec.rb index 4d3dff3a89..d5c32f4730 100644 --- a/spec/bundler/bundler/fetcher/downloader_spec.rb +++ b/spec/bundler/bundler/fetcher/downloader_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Bundler::Fetcher::Downloader do let(:connection) { double(:connection) } let(:redirect_limit) { 5 } - let(:uri) { Bundler::URI("http://www.uri-to-fetch.com/api/v2/endpoint") } + let(:uri) { Gem::URI("http://www.uri-to-fetch.com/api/v2/endpoint") } let(:options) { double(:options) } subject { described_class.new(connection, redirect_limit) } @@ -27,7 +27,7 @@ RSpec.describe Bundler::Fetcher::Downloader do end context "logging" do - let(:http_response) { Net::HTTPSuccess.new("1.1", 200, "Success") } + let(:http_response) { Gem::Net::HTTPSuccess.new("1.1", 200, "Success") } it "should log the HTTP response code and message to debug" do expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP 200 Success #{uri}") @@ -35,48 +35,48 @@ RSpec.describe Bundler::Fetcher::Downloader do end end - context "when the request response is a Net::HTTPRedirection" do - let(:http_response) { Net::HTTPRedirection.new(httpv, 308, "Moved") } + context "when the request response is a Gem::Net::HTTPRedirection" do + let(:http_response) { Gem::Net::HTTPRedirection.new(httpv, 308, "Moved") } before { http_response["location"] = "http://www.redirect-uri.com/api/v2/endpoint" } it "should try to fetch the redirect uri and iterate the # requests counter" do - expect(subject).to receive(:fetch).with(Bundler::URI("http://www.uri-to-fetch.com/api/v2/endpoint"), options, 0).and_call_original - expect(subject).to receive(:fetch).with(Bundler::URI("http://www.redirect-uri.com/api/v2/endpoint"), options, 1) + expect(subject).to receive(:fetch).with(Gem::URI("http://www.uri-to-fetch.com/api/v2/endpoint"), options, 0).and_call_original + expect(subject).to receive(:fetch).with(Gem::URI("http://www.redirect-uri.com/api/v2/endpoint"), options, 1) subject.fetch(uri, options, counter) end context "when the redirect uri and original uri are the same" do - let(:uri) { Bundler::URI("ssh://username:password@www.uri-to-fetch.com/api/v2/endpoint") } + let(:uri) { Gem::URI("ssh://username:password@www.uri-to-fetch.com/api/v2/endpoint") } before { http_response["location"] = "ssh://www.uri-to-fetch.com/api/v1/endpoint" } it "should set the same user and password for the redirect uri" do - expect(subject).to receive(:fetch).with(Bundler::URI("ssh://username:password@www.uri-to-fetch.com/api/v2/endpoint"), options, 0).and_call_original - expect(subject).to receive(:fetch).with(Bundler::URI("ssh://username:password@www.uri-to-fetch.com/api/v1/endpoint"), options, 1) + expect(subject).to receive(:fetch).with(Gem::URI("ssh://username:password@www.uri-to-fetch.com/api/v2/endpoint"), options, 0).and_call_original + expect(subject).to receive(:fetch).with(Gem::URI("ssh://username:password@www.uri-to-fetch.com/api/v1/endpoint"), options, 1) subject.fetch(uri, options, counter) end end end - context "when the request response is a Net::HTTPSuccess" do - let(:http_response) { Net::HTTPSuccess.new("1.1", 200, "Success") } + context "when the request response is a Gem::Net::HTTPSuccess" do + let(:http_response) { Gem::Net::HTTPSuccess.new("1.1", 200, "Success") } it "should return the response body" do expect(subject.fetch(uri, options, counter)).to eq(http_response) end end - context "when the request response is a Net::HTTPRequestEntityTooLarge" do - let(:http_response) { Net::HTTPRequestEntityTooLarge.new("1.1", 413, "Too Big") } + context "when the request response is a Gem::Net::HTTPRequestEntityTooLarge" do + let(:http_response) { Gem::Net::HTTPRequestEntityTooLarge.new("1.1", 413, "Too Big") } it "should raise a Bundler::Fetcher::FallbackError with the response body" do expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::FallbackError, "Body with info") end end - context "when the request response is a Net::HTTPUnauthorized" do - let(:http_response) { Net::HTTPUnauthorized.new("1.1", 401, "Unauthorized") } + context "when the request response is a Gem::Net::HTTPUnauthorized" do + let(:http_response) { Gem::Net::HTTPUnauthorized.new("1.1", 401, "Unauthorized") } it "should raise a Bundler::Fetcher::AuthenticationRequiredError with the uri host" do expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError, @@ -89,7 +89,7 @@ RSpec.describe Bundler::Fetcher::Downloader do end context "when the there are credentials provided in the request" do - let(:uri) { Bundler::URI("http://user:password@www.uri-to-fetch.com") } + let(:uri) { Gem::URI("http://user:password@www.uri-to-fetch.com") } it "should raise a Bundler::Fetcher::BadAuthenticationError that doesn't contain the password" do expect { subject.fetch(uri, options, counter) }. @@ -98,29 +98,39 @@ RSpec.describe Bundler::Fetcher::Downloader do end end - context "when the request response is a Net::HTTPNotFound" do - let(:http_response) { Net::HTTPNotFound.new("1.1", 404, "Not Found") } + context "when the request response is a Gem::Net::HTTPForbidden" do + let(:http_response) { Gem::Net::HTTPForbidden.new("1.1", 403, "Forbidden") } + let(:uri) { Gem::URI("http://user:password@www.uri-to-fetch.com") } - it "should raise a Bundler::Fetcher::FallbackError with Net::HTTPNotFound" do + it "should raise a Bundler::Fetcher::AuthenticationForbiddenError with the uri host" do + expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::AuthenticationForbiddenError, + /Access token could not be authenticated for www.uri-to-fetch.com/) + end + end + + context "when the request response is a Gem::Net::HTTPNotFound" do + let(:http_response) { Gem::Net::HTTPNotFound.new("1.1", 404, "Not Found") } + + it "should raise a Bundler::Fetcher::FallbackError with Gem::Net::HTTPNotFound" do expect { subject.fetch(uri, options, counter) }. - to raise_error(Bundler::Fetcher::FallbackError, "Net::HTTPNotFound: http://www.uri-to-fetch.com/api/v2/endpoint") + to raise_error(Bundler::Fetcher::FallbackError, "Gem::Net::HTTPNotFound: http://www.uri-to-fetch.com/api/v2/endpoint") end context "when the there are credentials provided in the request" do - let(:uri) { Bundler::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") } + let(:uri) { Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") } it "should raise a Bundler::Fetcher::FallbackError that doesn't contain the password" do expect { subject.fetch(uri, options, counter) }. - to raise_error(Bundler::Fetcher::FallbackError, "Net::HTTPNotFound: http://username@www.uri-to-fetch.com/api/v2/endpoint") + to raise_error(Bundler::Fetcher::FallbackError, "Gem::Net::HTTPNotFound: http://username@www.uri-to-fetch.com/api/v2/endpoint") end end end context "when the request response is some other type" do - let(:http_response) { Net::HTTPBadGateway.new("1.1", 500, "Fatal Error") } + let(:http_response) { Gem::Net::HTTPBadGateway.new("1.1", 500, "Fatal Error") } it "should raise a Bundler::HTTPError with the response class and body" do - expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::HTTPError, "Net::HTTPBadGateway: Body with info") + expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::HTTPError, "Gem::Net::HTTPBadGateway: Body with info") end end end @@ -130,7 +140,7 @@ RSpec.describe Bundler::Fetcher::Downloader do let(:response) { double(:response) } before do - allow(Net::HTTP::Get).to receive(:new).with("/api/v2/endpoint", options).and_return(net_http_get) + allow(Gem::Net::HTTP::Get).to receive(:new).with("/api/v2/endpoint", options).and_return(net_http_get) allow(connection).to receive(:request).with(uri, net_http_get).and_return(response) end @@ -142,7 +152,7 @@ RSpec.describe Bundler::Fetcher::Downloader do context "when there is a user provided in the request" do context "and there is also a password provided" do context "that contains cgi escaped characters" do - let(:uri) { Bundler::URI("http://username:password%24@www.uri-to-fetch.com/api/v2/endpoint") } + let(:uri) { Gem::URI("http://username:password%24@www.uri-to-fetch.com/api/v2/endpoint") } it "should request basic authentication with the username and password" do expect(net_http_get).to receive(:basic_auth).with("username", "password$") @@ -151,7 +161,7 @@ RSpec.describe Bundler::Fetcher::Downloader do end context "that is all unescaped characters" do - let(:uri) { Bundler::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") } + let(:uri) { Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") } it "should request basic authentication with the username and proper cgi compliant password" do expect(net_http_get).to receive(:basic_auth).with("username", "password") subject.request(uri, options) @@ -160,7 +170,7 @@ RSpec.describe Bundler::Fetcher::Downloader do end context "and there is no password provided" do - let(:uri) { Bundler::URI("http://username@www.uri-to-fetch.com/api/v2/endpoint") } + let(:uri) { Gem::URI("http://username@www.uri-to-fetch.com/api/v2/endpoint") } it "should request basic authentication with just the user" do expect(net_http_get).to receive(:basic_auth).with("username", nil) @@ -169,7 +179,7 @@ RSpec.describe Bundler::Fetcher::Downloader do end context "that contains cgi escaped characters" do - let(:uri) { Bundler::URI("http://username%24@www.uri-to-fetch.com/api/v2/endpoint") } + let(:uri) { Gem::URI("http://username%24@www.uri-to-fetch.com/api/v2/endpoint") } it "should request basic authentication with the proper cgi compliant password user" do expect(net_http_get).to receive(:basic_auth).with("username$", nil) @@ -178,26 +188,6 @@ RSpec.describe Bundler::Fetcher::Downloader do end end - context "when the request response causes a NoMethodError" do - before { allow(connection).to receive(:request).with(uri, net_http_get) { raise NoMethodError.new(message) } } - - context "and the error message is about use_ssl=" do - let(:message) { "undefined method 'use_ssl='" } - - it "should raise a LoadError about openssl" do - expect { subject.request(uri, options) }.to raise_error(LoadError, "cannot load such file -- openssl") - end - end - - context "and the error message is not about use_ssl=" do - let(:message) { "undefined method 'undefined_method_call'" } - - it "should raise the original NoMethodError" do - expect { subject.request(uri, options) }.to raise_error(NoMethodError, "undefined method 'undefined_method_call'") - end - end - end - context "when the request response causes a OpenSSL::SSL::SSLError" do before { allow(connection).to receive(:request).with(uri, net_http_get) { raise OpenSSL::SSL::SSLError.new } } @@ -240,7 +230,7 @@ RSpec.describe Bundler::Fetcher::Downloader do end context "when the there are credentials provided in the request" do - let(:uri) { Bundler::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") } + let(:uri) { Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") } before do allow(net_http_get).to receive(:basic_auth).with("username", "password") end diff --git a/spec/bundler/bundler/fetcher/index_spec.rb b/spec/bundler/bundler/fetcher/index_spec.rb index f0db07583c..dff9ccc3cc 100644 --- a/spec/bundler/bundler/fetcher/index_spec.rb +++ b/spec/bundler/bundler/fetcher/index_spec.rb @@ -8,8 +8,9 @@ RSpec.describe Bundler::Fetcher::Index do let(:display_uri) { "http://sample_uri.com" } let(:rubygems) { double(:rubygems) } let(:gem_names) { %w[foo bar] } + let(:gem_remote_fetcher) { nil } - subject { described_class.new(downloader, remote, display_uri) } + subject { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) } before { allow(Bundler).to receive(:rubygems).and_return(rubygems) } @@ -19,7 +20,7 @@ RSpec.describe Bundler::Fetcher::Index do end context "error handling" do - let(:remote_uri) { Bundler::URI("http://remote-uri.org") } + let(:remote_uri) { Gem::URI("http://remote-uri.org") } before do allow(rubygems).to receive(:fetch_all_remote_specs) { raise Gem::RemoteFetcher::FetchError.new(error_message, display_uri) } allow(subject).to receive(:remote_uri).and_return(remote_uri) @@ -63,33 +64,16 @@ RSpec.describe Bundler::Fetcher::Index do context "when a 403 response occurs" do let(:error_message) { "403" } - before do - allow(remote_uri).to receive(:userinfo).and_return(userinfo) - end - - context "and there was userinfo" do - let(:userinfo) { double(:userinfo) } - - it "should raise a Bundler::Fetcher::BadAuthenticationError" do - expect { subject.specs(gem_names) }.to raise_error(Bundler::Fetcher::BadAuthenticationError, - %r{Bad username or password for http://remote-uri.org}) - end - end - - context "and there was no userinfo" do - let(:userinfo) { nil } - - it "should raise a Bundler::Fetcher::AuthenticationRequiredError" do - expect { subject.specs(gem_names) }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError, - %r{Authentication is required for http://remote-uri.org}) - end + it "should raise a Bundler::Fetcher::AuthenticationForbiddenError" do + expect { subject.specs(gem_names) }.to raise_error(Bundler::Fetcher::AuthenticationForbiddenError, + %r{Access token could not be authenticated for http://remote-uri.org}) end end context "any other message is returned" do let(:error_message) { "You get an error, you get an error!" } - before { allow(Bundler).to receive(:ui).and_return(double(:trace => nil)) } + before { allow(Bundler).to receive(:ui).and_return(double(trace: nil)) } it "should raise a Bundler::HTTPError" do expect { subject.specs(gem_names) }.to raise_error(Bundler::HTTPError, "Could not fetch specs from http://sample_uri.com due to underlying error <You get an error, you get an error! (http://sample_uri.com)>") diff --git a/spec/bundler/bundler/fetcher_spec.rb b/spec/bundler/bundler/fetcher_spec.rb index 256d342775..e20f7e7c48 100644 --- a/spec/bundler/bundler/fetcher_spec.rb +++ b/spec/bundler/bundler/fetcher_spec.rb @@ -3,8 +3,8 @@ require "bundler/fetcher" RSpec.describe Bundler::Fetcher do - let(:uri) { Bundler::URI("https://example.com") } - let(:remote) { double("remote", :uri => uri, :original_uri => nil) } + let(:uri) { Gem::URI("https://example.com") } + let(:remote) { double("remote", uri: uri, original_uri: nil) } subject(:fetcher) { Bundler::Fetcher.new(remote) } @@ -26,7 +26,7 @@ RSpec.describe Bundler::Fetcher do context "when Gem.configuration specifies http_proxy " do let(:proxy) { "http://proxy-example2.com" } before do - allow(Bundler.rubygems.configuration).to receive(:[]).with(:http_proxy).and_return(proxy) + allow(Gem.configuration).to receive(:[]).with(:http_proxy).and_return(proxy) end it "consider Gem.configuration when determine proxy" do expect(fetcher.http_proxy).to match("http://proxy-example2.com") @@ -45,9 +45,9 @@ RSpec.describe Bundler::Fetcher do end context "when a rubygems source mirror is set" do - let(:orig_uri) { Bundler::URI("http://zombo.com") } + let(:orig_uri) { Gem::URI("http://zombo.com") } let(:remote_with_mirror) do - double("remote", :uri => uri, :original_uri => orig_uri, :anonymized_uri => uri) + double("remote", uri: uri, original_uri: orig_uri, anonymized_uri: uri) end let(:fetcher) { Bundler::Fetcher.new(remote_with_mirror) } @@ -61,7 +61,7 @@ RSpec.describe Bundler::Fetcher do context "when there is no rubygems source mirror set" do let(:remote_no_mirror) do - double("remote", :uri => uri, :original_uri => nil, :anonymized_uri => uri) + double("remote", uri: uri, original_uri: nil, anonymized_uri: uri) end let(:fetcher) { Bundler::Fetcher.new(remote_no_mirror) } @@ -113,10 +113,10 @@ RSpec.describe Bundler::Fetcher do context "when gem ssl configuration is set" do before do - allow(Bundler.rubygems.configuration).to receive_messages( - :http_proxy => nil, - :ssl_client_cert => "cert", - :ssl_ca_cert => "ca" + allow(Gem.configuration).to receive_messages( + http_proxy: nil, + ssl_client_cert: "cert", + ssl_ca_cert: "ca" ) expect(File).to receive(:read).and_return("") expect(OpenSSL::X509::Certificate).to receive(:new).and_return("cert") @@ -143,20 +143,118 @@ RSpec.describe Bundler::Fetcher do describe "include CI information" do it "from one CI" do - with_env_vars("JENKINS_URL" => "foo") do + with_env_vars("CI" => nil, "JENKINS_URL" => "foo") do ci_part = fetcher.user_agent.split(" ").find {|x| x.start_with?("ci/") } - expect(ci_part).to match("jenkins") + cis = ci_part.split("/").last.split(",") + expect(cis).to include("jenkins") + expect(cis).not_to include("ci") end end it "from many CI" do - with_env_vars("TRAVIS" => "foo", "GITLAB_CI" => "gitlab", "CI_NAME" => "my_ci") do + with_env_vars("CI" => "true", "SEMAPHORE" => nil, "TRAVIS" => "foo", "GITLAB_CI" => "gitlab", "CI_NAME" => "MY_ci") do ci_part = fetcher.user_agent.split(" ").find {|x| x.start_with?("ci/") } - expect(ci_part).to match("travis") - expect(ci_part).to match("gitlab") - expect(ci_part).to match("my_ci") + cis = ci_part.split("/").last.split(",") + expect(cis).to include("ci", "gitlab", "my_ci", "travis") + expect(cis).not_to include("semaphore") end end end end + + describe "#fetch_spec" do + let(:name) { "name" } + let(:version) { "1.3.17" } + let(:platform) { "platform" } + let(:downloader) { double("downloader") } + let(:body) { double(Gem::Net::HTTP::Get, body: downloaded_data) } + + context "when attempting to load a Gem::Specification" do + let(:spec) { Gem::Specification.new(name, version) } + let(:downloaded_data) { Zlib::Deflate.deflate(Marshal.dump(spec)) } + + it "returns the spec" do + expect(Bundler::Fetcher::Downloader).to receive(:new).and_return(downloader) + expect(downloader).to receive(:fetch).once.and_return(body) + result = fetcher.fetch_spec([name, version, platform]) + expect(result).to eq(spec) + end + end + + context "when attempting to load an unexpected class" do + let(:downloaded_data) { Zlib::Deflate.deflate(Marshal.dump(3)) } + + it "raises a HTTPError error" do + expect(Bundler::Fetcher::Downloader).to receive(:new).and_return(downloader) + expect(downloader).to receive(:fetch).once.and_return(body) + expect { fetcher.fetch_spec([name, version, platform]) }.to raise_error(Bundler::HTTPError, /Gemspec .* contained invalid data/i) + end + end + end + + describe "#specs_with_retry" do + let(:downloader) { double(:downloader) } + let(:remote) { double(:remote, cache_slug: "slug", uri: uri, original_uri: nil, anonymized_uri: uri) } + let(:compact_index) { double(Bundler::Fetcher::CompactIndex, available?: true, api_fetcher?: true) } + let(:dependency) { double(Bundler::Fetcher::Dependency, available?: true, api_fetcher?: true) } + let(:index) { double(Bundler::Fetcher::Index, available?: true, api_fetcher?: false) } + + before do + allow(Bundler::Fetcher::CompactIndex).to receive(:new).and_return(compact_index) + allow(Bundler::Fetcher::Dependency).to receive(:new).and_return(dependency) + allow(Bundler::Fetcher::Index).to receive(:new).and_return(index) + end + + it "picks the first fetcher that works" do + expect(compact_index).to receive(:specs).with("name").and_return([["name", "1.2.3", "ruby"]]) + expect(dependency).not_to receive(:specs) + expect(index).not_to receive(:specs) + fetcher.specs_with_retry("name", double(Bundler::Source::Rubygems)) + end + + context "when APIs are not available" do + before do + allow(compact_index).to receive(:available?).and_return(false) + allow(dependency).to receive(:available?).and_return(false) + end + + it "uses the index" do + expect(compact_index).not_to receive(:specs) + expect(dependency).not_to receive(:specs) + expect(index).to receive(:specs).with("name").and_return([["name", "1.2.3", "ruby"]]) + + fetcher.specs_with_retry("name", double(Bundler::Source::Rubygems)) + end + end + end + + describe "#api_fetcher?" do + let(:downloader) { double(:downloader) } + let(:remote) { double(:remote, cache_slug: "slug", uri: uri, original_uri: nil, anonymized_uri: uri) } + let(:compact_index) { double(Bundler::Fetcher::CompactIndex, available?: false, api_fetcher?: true) } + let(:dependency) { double(Bundler::Fetcher::Dependency, available?: false, api_fetcher?: true) } + let(:index) { double(Bundler::Fetcher::Index, available?: true, api_fetcher?: false) } + + before do + allow(Bundler::Fetcher::CompactIndex).to receive(:new).and_return(compact_index) + allow(Bundler::Fetcher::Dependency).to receive(:new).and_return(dependency) + allow(Bundler::Fetcher::Index).to receive(:new).and_return(index) + end + + context "when an api fetcher is available" do + before do + allow(compact_index).to receive(:available?).and_return(true) + end + + it "is truthy" do + expect(fetcher).to be_api_fetcher + end + end + + context "when only the index fetcher is available" do + it "is falsey" do + expect(fetcher).not_to be_api_fetcher + end + end + end end diff --git a/spec/bundler/bundler/friendly_errors_spec.rb b/spec/bundler/bundler/friendly_errors_spec.rb index 496191f891..cda2ef31de 100644 --- a/spec/bundler/bundler/friendly_errors_spec.rb +++ b/spec/bundler/bundler/friendly_errors_spec.rb @@ -22,7 +22,7 @@ RSpec.describe Bundler, "friendly errors" do gem "rack" G - bundle :install, :env => { "DEBUG" => "true" } + bundle :install, env: { "DEBUG" => "true" } expect(err).to include("Failed to load #{home(".gemrc")}") end @@ -101,29 +101,15 @@ RSpec.describe Bundler, "friendly errors" do context "BundlerError" do it "Bundler.ui receive error" do error = Bundler::BundlerError.new - expect(Bundler.ui).to receive(:error).with(error.message, :wrap => true) + expect(Bundler.ui).to receive(:error).with(error.message, wrap: true) Bundler::FriendlyErrors.log_error(error) end - it_behaves_like "Bundler.ui receive trace", Bundler::BundlerError.new end context "Thor::Error" do it_behaves_like "Bundler.ui receive error", Bundler::Thor::Error.new end - context "LoadError" do - let(:error) { LoadError.new("cannot load such file -- openssl") } - - before do - allow(error).to receive(:backtrace).and_return(["backtrace"]) - end - - it "Bundler.ui receive error" do - expect(Bundler.ui).to receive(:error).with("\nCould not load OpenSSL. LoadError: cannot load such file -- openssl\nbacktrace") - Bundler::FriendlyErrors.log_error(error) - end - end - context "Interrupt" do it "Bundler.ui receive error" do expect(Bundler.ui).to receive(:error).with("\nQuitting...") @@ -135,7 +121,7 @@ RSpec.describe Bundler, "friendly errors" do context "Gem::InvalidSpecificationException" do it "Bundler.ui receive error" do error = Gem::InvalidSpecificationException.new - expect(Bundler.ui).to receive(:error).with(error.message, :wrap => true) + expect(Bundler.ui).to receive(:error).with(error.message, wrap: true) Bundler::FriendlyErrors.log_error(error) end end diff --git a/spec/bundler/bundler/gem_helper_spec.rb b/spec/bundler/bundler/gem_helper_spec.rb index 6c3ac3e035..940e5df9de 100644 --- a/spec/bundler/bundler/gem_helper_spec.rb +++ b/spec/bundler/bundler/gem_helper_spec.rb @@ -11,6 +11,7 @@ RSpec.describe Bundler::GemHelper do before(:each) do global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false", "BUNDLE_GEM__LINTER" => "false", "BUNDLE_GEM__CI" => "false", "BUNDLE_GEM__CHANGELOG" => "false" + sys_exec("git config --global init.defaultBranch main") bundle "gem #{app_name}" prepare_gemspec(app_gemspec_path) end @@ -66,6 +67,10 @@ RSpec.describe Bundler::GemHelper do mock_confirm_message message end + def sha512_hexdigest(path) + Digest::SHA512.file(path).hexdigest + end + subject! { Bundler::GemHelper.new(app_path) } let(:app_version) { "0.1.0" } let(:app_gem_dir) { app_path.join("pkg") } @@ -169,12 +174,21 @@ RSpec.describe Bundler::GemHelper do end describe "#build_checksum" do + it "calculates SHA512 of the content" do + FileUtils.mkdir_p(app_gem_dir) + File.write(app_gem_path, "") + mock_checksum_message app_name, app_version + subject.build_checksum(app_gem_path) + expect(File.read(app_sha_path).chomp).to eql(Digest::SHA512.hexdigest("")) + end + context "when build was successful" do it "creates .sha512 file" do mock_build_message app_name, app_version mock_checksum_message app_name, app_version subject.build_checksum expect(app_sha_path).to exist + expect(File.read(app_sha_path).chomp).to eql(sha512_hexdigest(app_gem_path)) end end context "when building in the current working directory" do @@ -185,6 +199,7 @@ RSpec.describe Bundler::GemHelper do Bundler::GemHelper.new.build_checksum end expect(app_sha_path).to exist + expect(File.read(app_sha_path).chomp).to eql(sha512_hexdigest(app_gem_path)) end end context "when building in a location relative to the current working directory" do @@ -195,6 +210,7 @@ RSpec.describe Bundler::GemHelper do Bundler::GemHelper.new(File.basename(app_path)).build_checksum end expect(app_sha_path).to exist + expect(File.read(app_sha_path).chomp).to eql(sha512_hexdigest(app_gem_path)) end end end @@ -219,7 +235,7 @@ RSpec.describe Bundler::GemHelper do FileUtils.touch app_gem_path app_gem_path end - expect { subject.install_gem }.to raise_error(/Couldn't install gem/) + expect { subject.install_gem }.to raise_error(/Running `#{gem_bin} install #{app_gem_path}` failed/) end end end @@ -237,11 +253,11 @@ RSpec.describe Bundler::GemHelper do end before do - sys_exec("git init", :dir => app_path) - sys_exec("git config user.email \"you@example.com\"", :dir => app_path) - sys_exec("git config user.name \"name\"", :dir => app_path) - sys_exec("git config commit.gpgsign false", :dir => app_path) - sys_exec("git config push.default simple", :dir => app_path) + sys_exec("git init", dir: app_path) + sys_exec("git config user.email \"you@example.com\"", dir: app_path) + sys_exec("git config user.name \"name\"", dir: app_path) + sys_exec("git config commit.gpgsign false", dir: app_path) + sys_exec("git config push.default simple", dir: app_path) # silence messages allow(Bundler.ui).to receive(:confirm) @@ -255,23 +271,23 @@ RSpec.describe Bundler::GemHelper do end it "when there are uncommitted files" do - sys_exec("git add .", :dir => app_path) + sys_exec("git add .", dir: app_path) expect { Rake.application["release"].invoke }. to raise_error("There are files that need to be committed first.") end it "when there is no git remote" do - sys_exec("git commit -a -m \"initial commit\"", :dir => app_path) + sys_exec("git commit -a -m \"initial commit\"", dir: app_path) expect { Rake.application["release"].invoke }.to raise_error(RuntimeError) end end context "succeeds" do - let(:repo) { build_git("foo", :bare => true) } + let(:repo) { build_git("foo", bare: true) } before do - sys_exec("git remote add origin #{file_uri_for(repo.path)}", :dir => app_path) - sys_exec('git commit -a -m "initial commit"', :dir => app_path) + sys_exec("git remote add origin #{file_uri_for(repo.path)}", dir: app_path) + sys_exec('git commit -a -m "initial commit"', dir: app_path) end context "on releasing" do @@ -280,7 +296,7 @@ RSpec.describe Bundler::GemHelper do mock_confirm_message "Tagged v#{app_version}." mock_confirm_message "Pushed git commits and release tag." - sys_exec("git push -u origin master", :dir => app_path) + sys_exec("git push -u origin main", dir: app_path) end it "calls rubygem_push with proper arguments" do @@ -298,8 +314,8 @@ RSpec.describe Bundler::GemHelper do it "also works when releasing from an ambiguous reference" do # Create a branch with the same name as the tag - sys_exec("git checkout -b v#{app_version}", :dir => app_path) - sys_exec("git push -u origin v#{app_version}", :dir => app_path) + sys_exec("git checkout -b v#{app_version}", dir: app_path) + sys_exec("git push -u origin v#{app_version}", dir: app_path) expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s) @@ -307,7 +323,7 @@ RSpec.describe Bundler::GemHelper do end it "also works with releasing from a branch not yet pushed" do - sys_exec("git checkout -b module_function", :dir => app_path) + sys_exec("git checkout -b module_function", dir: app_path) expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s) @@ -321,7 +337,7 @@ RSpec.describe Bundler::GemHelper do mock_build_message app_name, app_version mock_confirm_message "Pushed git commits and release tag." - sys_exec("git push -u origin master", :dir => app_path) + sys_exec("git push -u origin main", dir: app_path) expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s) end @@ -337,7 +353,7 @@ RSpec.describe Bundler::GemHelper do mock_confirm_message "Tag v#{app_version} has already been created." expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s) - sys_exec("git tag -a -m \"Version #{app_version}\" v#{app_version}", :dir => app_path) + sys_exec("git tag -a -m \"Version #{app_version}\" v#{app_version}", dir: app_path) Rake.application["release"].invoke end @@ -358,10 +374,10 @@ RSpec.describe Bundler::GemHelper do end before do - sys_exec("git init", :dir => app_path) - sys_exec("git config user.email \"you@example.com\"", :dir => app_path) - sys_exec("git config user.name \"name\"", :dir => app_path) - sys_exec("git config push.gpgsign simple", :dir => app_path) + sys_exec("git init", dir: app_path) + sys_exec("git config user.email \"you@example.com\"", dir: app_path) + sys_exec("git config user.name \"name\"", dir: app_path) + sys_exec("git config push.gpgsign simple", dir: app_path) # silence messages allow(Bundler.ui).to receive(:confirm) diff --git a/spec/bundler/bundler/gem_version_promoter_spec.rb b/spec/bundler/bundler/gem_version_promoter_spec.rb index 43a3630bbb..917daba95d 100644 --- a/spec/bundler/bundler/gem_version_promoter_spec.rb +++ b/spec/bundler/bundler/gem_version_promoter_spec.rb @@ -1,178 +1,162 @@ # frozen_string_literal: true RSpec.describe Bundler::GemVersionPromoter do - context "conservative resolver" do - def versions(result) - result.flatten.map(&:version).map(&:to_s) + let(:gvp) { described_class.new } + + # Rightmost (highest array index) in result is most preferred. + # Leftmost (lowest array index) in result is least preferred. + # `build_candidates` has all versions of gem in index. + # `build_spec` is the version currently in the .lock file. + # + # In default (not strict) mode, all versions in the index will + # be returned, allowing Bundler the best chance to resolve all + # dependencies, but sometimes resulting in upgrades that some + # would not consider conservative. + + describe "#sort_versions" do + def build_candidates(versions) + versions.map do |v| + Bundler::Resolver::Candidate.new(v) + end end - def make_instance(*args) - @gvp = Bundler::GemVersionPromoter.new(*args).tap do |gvp| - gvp.class.class_eval { public :filter_dep_specs, :sort_dep_specs } - end + def build_package(name, version, locked = []) + Bundler::Resolver::Package.new(name, [], locked_specs: Bundler::SpecSet.new(build_spec(name, version)), unlock: locked) end - def unlocking(options) - make_instance(Bundler::SpecSet.new([]), ["foo"]).tap do |p| - p.level = options[:level] if options[:level] - p.strict = options[:strict] if options[:strict] - end + def sorted_versions(candidates:, current:, name: "foo", locked: []) + gvp.sort_versions( + build_package(name, current, locked), + build_candidates(candidates) + ).flatten.map(&:version).map(&:to_s) end - def keep_locked(options) - make_instance(Bundler::SpecSet.new([]), ["bar"]).tap do |p| - p.level = options[:level] if options[:level] - p.strict = options[:strict] if options[:strict] - end + it "numerically sorts versions" do + versions = sorted_versions(candidates: %w[1.7.7 1.7.8 1.7.9 1.7.15 1.8.0], current: "1.7.8") + expect(versions).to eq %w[1.8.0 1.7.15 1.7.9 1.7.8 1.7.7] end - def build_spec_groups(name, versions) - versions.map do |v| - Bundler::Resolver::SpecGroup.create_for({ Gem::Platform::RUBY => build_spec(name, v) }, [Gem::Platform::RUBY], Gem::Platform::RUBY) + context "with no options" do + it "defaults to level=:major, strict=false, pre=false" do + versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") + expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0] end end - # Rightmost (highest array index) in result is most preferred. - # Leftmost (lowest array index) in result is least preferred. - # `build_spec_groups` has all versions of gem in index. - # `build_spec` is the version currently in the .lock file. - # - # In default (not strict) mode, all versions in the index will - # be returned, allowing Bundler the best chance to resolve all - # dependencies, but sometimes resulting in upgrades that some - # would not consider conservative. - context "filter specs (strict) level patch" do - it "when keeping build_spec, keep current, next release" do - keep_locked(:level => :patch) - res = @gvp.filter_dep_specs( - build_spec_groups("foo", %w[1.7.8 1.7.9 1.8.0]), - build_spec("foo", "1.7.8").first - ) - expect(versions(res)).to eq %w[1.7.9 1.7.8] - end + context "when strict" do + before { gvp.strict = true } - it "when unlocking prefer next release first" do - unlocking(:level => :patch) - res = @gvp.filter_dep_specs( - build_spec_groups("foo", %w[1.7.8 1.7.9 1.8.0]), - build_spec("foo", "1.7.8").first - ) - expect(versions(res)).to eq %w[1.7.8 1.7.9] - end + context "when level is major" do + before { gvp.level = :major } - it "when unlocking keep current when already at latest release" do - unlocking(:level => :patch) - res = @gvp.filter_dep_specs( - build_spec_groups("foo", %w[1.7.9 1.8.0 2.0.0]), - build_spec("foo", "1.7.9").first - ) - expect(versions(res)).to eq %w[1.7.9] + it "keeps downgrades" do + versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") + expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0] + end end - end - context "filter specs (strict) level minor" do - it "when unlocking favor next releases, remove minor and major increases" do - unlocking(:level => :minor) - res = @gvp.filter_dep_specs( - build_spec_groups("foo", %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.0 2.0.1]), - build_spec("foo", "0.2.0").first - ) - expect(versions(res)).to eq %w[0.2.0 0.3.0 0.3.1 0.9.0] + context "when level is minor" do + before { gvp.level = :minor } + + it "sorts highest minor within same major in first position" do + versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") + expect(versions).to eq %w[0.9.0 0.3.1 0.3.0 1.0.0 2.1.0 2.0.1 0.2.0] + end end - it "when keep locked, keep current, then favor next release, remove minor and major increases" do - keep_locked(:level => :minor) - res = @gvp.filter_dep_specs( - build_spec_groups("foo", %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.0 2.0.1]), - build_spec("foo", "0.2.0").first - ) - expect(versions(res)).to eq %w[0.3.0 0.3.1 0.9.0 0.2.0] + context "when level is patch" do + before { gvp.level = :patch } + + it "sorts highest patch within same minor in first position" do + versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") + expect(versions).to eq %w[0.3.1 0.3.0 0.9.0 1.0.0 2.0.1 2.1.0 0.2.0] + end end end - context "sort specs (not strict) level patch" do - it "when not unlocking, same order but make sure build_spec version is most preferred to stay put" do - keep_locked(:level => :patch) - res = @gvp.sort_dep_specs( - build_spec_groups("foo", %w[1.5.4 1.6.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 2.0.0 2.0.1]), - build_spec("foo", "1.7.7").first - ) - expect(versions(res)).to eq %w[1.5.4 1.6.5 1.7.6 2.0.0 2.0.1 1.8.0 1.8.1 1.7.8 1.7.9 1.7.7] - end + context "when not strict" do + before { gvp.strict = false } - it "when unlocking favor next release, then current over minor increase" do - unlocking(:level => :patch) - res = @gvp.sort_dep_specs( - build_spec_groups("foo", %w[1.7.7 1.7.8 1.7.9 1.8.0]), - build_spec("foo", "1.7.8").first - ) - expect(versions(res)).to eq %w[1.7.7 1.8.0 1.7.8 1.7.9] + context "when level is major" do + before { gvp.level = :major } + + it "orders by version" do + versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") + expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0] + end end - it "when unlocking do proper integer comparison, not string" do - unlocking(:level => :patch) - res = @gvp.sort_dep_specs( - build_spec_groups("foo", %w[1.7.7 1.7.8 1.7.9 1.7.15 1.8.0]), - build_spec("foo", "1.7.8").first - ) - expect(versions(res)).to eq %w[1.7.7 1.8.0 1.7.8 1.7.9 1.7.15] + context "when level is minor" do + before { gvp.level = :minor } + + it "favors minor upgrades, then patch upgrades, then major upgrades, then downgrades" do + versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") + expect(versions).to eq %w[0.9.0 0.3.1 0.3.0 1.0.0 2.1.0 2.0.1 0.2.0] + end end - it "leave current when unlocking but already at latest release" do - unlocking(:level => :patch) - res = @gvp.sort_dep_specs( - build_spec_groups("foo", %w[1.7.9 1.8.0 2.0.0]), - build_spec("foo", "1.7.9").first - ) - expect(versions(res)).to eq %w[2.0.0 1.8.0 1.7.9] + context "when level is patch" do + before { gvp.level = :patch } + + it "favors patch upgrades, then minor upgrades, then major upgrades, then downgrades" do + versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") + expect(versions).to eq %w[0.3.1 0.3.0 0.9.0 1.0.0 2.0.1 2.1.0 0.2.0] + end end end - context "sort specs (not strict) level minor" do - it "when unlocking favor next release, then minor increase over current" do - unlocking(:level => :minor) - res = @gvp.sort_dep_specs( - build_spec_groups("foo", %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.0 2.0.1]), - build_spec("foo", "0.2.0").first - ) - expect(versions(res)).to eq %w[2.0.0 2.0.1 1.0.0 0.2.0 0.3.0 0.3.1 0.9.0] + context "when pre" do + before { gvp.pre = true } + + it "sorts regardless of prerelease status" do + versions = sorted_versions(candidates: %w[1.7.7.pre 1.8.0 1.8.1.pre 1.8.1 2.0.0.pre 2.0.0], current: "1.8.0") + expect(versions).to eq %w[2.0.0 2.0.0.pre 1.8.1 1.8.1.pre 1.8.0 1.7.7.pre] end end - context "level error handling" do - subject { Bundler::GemVersionPromoter.new } + context "when not pre" do + before { gvp.pre = false } - it "should raise if not major, minor or patch is passed" do - expect { subject.level = :minjor }.to raise_error ArgumentError + it "deprioritizes prerelease gems" do + versions = sorted_versions(candidates: %w[1.7.7.pre 1.8.0 1.8.1.pre 1.8.1 2.0.0.pre 2.0.0], current: "1.8.0") + expect(versions).to eq %w[2.0.0 1.8.1 1.8.0 2.0.0.pre 1.8.1.pre 1.7.7.pre] end + end - it "should raise if invalid classes passed" do - [123, nil].each do |value| - expect { subject.level = value }.to raise_error ArgumentError - end + context "when locking and not major" do + before { gvp.level = :minor } + + it "keeps the current version first" do + versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.1.0 2.0.1], current: "0.3.0", locked: ["bar"]) + expect(versions.first).to eq("0.3.0") end + end + end - it "should accept major, minor patch symbols" do - [:major, :minor, :patch].each do |value| - subject.level = value - expect(subject.level).to eq value - end + describe "#level=" do + subject { described_class.new } + + it "should raise if not major, minor or patch is passed" do + expect { subject.level = :minjor }.to raise_error ArgumentError + end + + it "should raise if invalid classes passed" do + [123, nil].each do |value| + expect { subject.level = value }.to raise_error ArgumentError end + end - it "should accept major, minor patch strings" do - %w[major minor patch].each do |value| - subject.level = value - expect(subject.level).to eq value.to_sym - end + it "should accept major, minor patch symbols" do + [:major, :minor, :patch].each do |value| + subject.level = value + expect(subject.level).to eq value end end - context "debug output" do - it "should not kerblooie on its own debug output" do - gvp = unlocking(:level => :patch) - dep = Bundler::DepProxy.get_proxy(dep("foo", "1.2.0").first, "ruby") - result = gvp.send(:debug_format_result, dep, build_spec_groups("foo", %w[1.2.0 1.3.0])) - expect(result.class).to eq Array + it "should accept major, minor patch strings" do + %w[major minor patch].each do |value| + subject.level = value + expect(subject.level).to eq value.to_sym end end end diff --git a/spec/bundler/bundler/installer/gem_installer_spec.rb b/spec/bundler/bundler/installer/gem_installer_spec.rb index 8f8d1c6d15..4b6a07f344 100644 --- a/spec/bundler/bundler/installer/gem_installer_spec.rb +++ b/spec/bundler/bundler/installer/gem_installer_spec.rb @@ -3,15 +3,19 @@ require "bundler/installer/gem_installer" RSpec.describe Bundler::GemInstaller do - let(:installer) { instance_double("Installer") } + let(:definition) { instance_double("Definition", locked_gems: nil) } + let(:installer) { instance_double("Installer", definition: definition) } let(:spec_source) { instance_double("SpecSource") } - let(:spec) { instance_double("Specification", :name => "dummy", :version => "0.0.1", :loaded_from => "dummy", :source => spec_source) } + let(:spec) { instance_double("Specification", name: "dummy", version: "0.0.1", loaded_from: "dummy", source: spec_source) } subject { described_class.new(spec, installer) } context "spec_settings is nil" do it "invokes install method with empty build_args" do - allow(spec_source).to receive(:install).with(spec, :force => false, :ensure_builtin_gems_cached => false, :build_args => []) + allow(spec_source).to receive(:install).with( + spec, + { force: false, ensure_builtin_gems_cached: false, build_args: [], previous_spec: nil } + ) subject.install_from_spec end end @@ -22,7 +26,10 @@ RSpec.describe Bundler::GemInstaller do allow(Bundler.settings).to receive(:[]).with(:inline) allow(Bundler.settings).to receive(:[]).with(:forget_cli_options) allow(Bundler.settings).to receive(:[]).with("build.dummy").and_return("--with-dummy-config=dummy") - expect(spec_source).to receive(:install).with(spec, :force => false, :ensure_builtin_gems_cached => false, :build_args => ["--with-dummy-config=dummy"]) + expect(spec_source).to receive(:install).with( + spec, + { force: false, ensure_builtin_gems_cached: false, build_args: ["--with-dummy-config=dummy"], previous_spec: nil } + ) subject.install_from_spec end end @@ -33,7 +40,10 @@ RSpec.describe Bundler::GemInstaller do allow(Bundler.settings).to receive(:[]).with(:inline) allow(Bundler.settings).to receive(:[]).with(:forget_cli_options) allow(Bundler.settings).to receive(:[]).with("build.dummy").and_return("--with-dummy-config=dummy --with-another-dummy-config") - expect(spec_source).to receive(:install).with(spec, :force => false, :ensure_builtin_gems_cached => false, :build_args => ["--with-dummy-config=dummy", "--with-another-dummy-config"]) + expect(spec_source).to receive(:install).with( + spec, + { force: false, ensure_builtin_gems_cached: false, build_args: ["--with-dummy-config=dummy", "--with-another-dummy-config"], previous_spec: nil } + ) subject.install_from_spec end end diff --git a/spec/bundler/bundler/installer/parallel_installer_spec.rb b/spec/bundler/bundler/installer/parallel_installer_spec.rb deleted file mode 100644 index e680633862..0000000000 --- a/spec/bundler/bundler/installer/parallel_installer_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -require "bundler/installer/parallel_installer" - -RSpec.describe Bundler::ParallelInstaller do - let(:installer) { instance_double("Installer") } - let(:all_specs) { [] } - let(:size) { 1 } - let(:standalone) { false } - let(:force) { false } - - subject { described_class.new(installer, all_specs, size, standalone, force) } - - context "when dependencies that are not on the overall installation list are the only ones not installed" do - let(:all_specs) do - [ - build_spec("alpha", "1.0") {|s| s.runtime "a", "1" }, - ].flatten - end - - it "prints a warning" do - expect(Bundler.ui).to receive(:warn).with(<<-W.strip) -Your lockfile was created by an old Bundler that left some things out. -You can fix this by adding the missing gems to your Gemfile, running bundle install, and then removing the gems from your Gemfile. -The missing gems are: -* a depended upon by alpha - W - subject.check_for_corrupt_lockfile - end - - context "when size > 1" do - let(:size) { 500 } - - it "prints a warning and sets size to 1" do - expect(Bundler.ui).to receive(:warn).with(<<-W.strip) -Your lockfile was created by an old Bundler that left some things out. -Because of the missing DEPENDENCIES, we can only install gems one at a time, instead of installing 500 at a time. -You can fix this by adding the missing gems to your Gemfile, running bundle install, and then removing the gems from your Gemfile. -The missing gems are: -* a depended upon by alpha - W - subject.check_for_corrupt_lockfile - expect(subject.size).to eq(1) - end - end - end - - context "when the spec set is not a valid resolution" do - let(:all_specs) do - [ - build_spec("cucumber", "4.1.0") {|s| s.runtime "diff-lcs", "< 1.4" }, - build_spec("diff-lcs", "1.4.4"), - ].flatten - end - - it "prints a warning" do - expect(Bundler.ui).to receive(:warn).with(<<-W.strip) -Your lockfile doesn't include a valid resolution. -You can fix this by regenerating your lockfile or trying to manually editing the bad locked gems to a version that satisfies all dependencies. -The unmet dependencies are: -* diff-lcs (< 1.4), depended upon cucumber-4.1.0, unsatisfied by diff-lcs-1.4.4 - W - subject.check_for_unmet_dependencies - end - end - - context "when the spec set is a valid resolution" do - let(:all_specs) do - [ - build_spec("cucumber", "4.1.0") {|s| s.runtime "diff-lcs", "< 1.4" }, - build_spec("diff-lcs", "1.3"), - ].flatten - end - - it "doesn't print a warning" do - expect(Bundler.ui).not_to receive(:warn) - subject.check_for_unmet_dependencies - end - end -end diff --git a/spec/bundler/bundler/installer/spec_installation_spec.rb b/spec/bundler/bundler/installer/spec_installation_spec.rb index e63ef26cb3..cbe2589b99 100644 --- a/spec/bundler/bundler/installer/spec_installation_spec.rb +++ b/spec/bundler/bundler/installer/spec_installation_spec.rb @@ -42,24 +42,26 @@ RSpec.describe Bundler::ParallelInstaller::SpecInstallation do context "when all dependencies are installed" do it "returns true" do dependencies = [] - dependencies << instance_double("SpecInstallation", :spec => "alpha", :name => "alpha", :installed? => true, :all_dependencies => [], :type => :production) - dependencies << instance_double("SpecInstallation", :spec => "beta", :name => "beta", :installed? => true, :all_dependencies => [], :type => :production) - all_specs = dependencies + [instance_double("SpecInstallation", :spec => "gamma", :name => "gamma", :installed? => false, :all_dependencies => [], :type => :production)] + dependencies << instance_double("SpecInstallation", spec: "alpha", name: "alpha", installed?: true, all_dependencies: [], type: :production) + dependencies << instance_double("SpecInstallation", spec: "beta", name: "beta", installed?: true, all_dependencies: [], type: :production) + all_specs = dependencies + [instance_double("SpecInstallation", spec: "gamma", name: "gamma", installed?: false, all_dependencies: [], type: :production)] spec = described_class.new(dep) allow(spec).to receive(:all_dependencies).and_return(dependencies) - expect(spec.dependencies_installed?(all_specs)).to be_truthy + installed_specs = all_specs.select(&:installed?).map {|s| [s.name, true] }.to_h + expect(spec.dependencies_installed?(installed_specs)).to be_truthy end end context "when all dependencies are not installed" do it "returns false" do dependencies = [] - dependencies << instance_double("SpecInstallation", :spec => "alpha", :name => "alpha", :installed? => false, :all_dependencies => [], :type => :production) - dependencies << instance_double("SpecInstallation", :spec => "beta", :name => "beta", :installed? => true, :all_dependencies => [], :type => :production) - all_specs = dependencies + [instance_double("SpecInstallation", :spec => "gamma", :name => "gamma", :installed? => false, :all_dependencies => [], :type => :production)] + dependencies << instance_double("SpecInstallation", spec: "alpha", name: "alpha", installed?: false, all_dependencies: [], type: :production) + dependencies << instance_double("SpecInstallation", spec: "beta", name: "beta", installed?: true, all_dependencies: [], type: :production) + all_specs = dependencies + [instance_double("SpecInstallation", spec: "gamma", name: "gamma", installed?: false, all_dependencies: [], type: :production)] spec = described_class.new(dep) allow(spec).to receive(:all_dependencies).and_return(dependencies) - expect(spec.dependencies_installed?(all_specs)).to be_falsey + installed_specs = all_specs.select(&:installed?).map {|s| [s.name, true] }.to_h + expect(spec.dependencies_installed?(installed_specs)).to be_falsey end end end diff --git a/spec/bundler/bundler/lockfile_parser_spec.rb b/spec/bundler/bundler/lockfile_parser_spec.rb index 3a6d61336f..88932bf009 100644 --- a/spec/bundler/bundler/lockfile_parser_spec.rb +++ b/spec/bundler/bundler/lockfile_parser_spec.rb @@ -3,7 +3,7 @@ require "bundler/lockfile_parser" RSpec.describe Bundler::LockfileParser do - let(:lockfile_contents) { strip_whitespace(<<-L) } + let(:lockfile_contents) { <<~L } GIT remote: https://github.com/alloy/peiji-san.git revision: eca485d8dc95f12aaec1a434b49d295c7e91844b @@ -22,6 +22,9 @@ RSpec.describe Bundler::LockfileParser do peiji-san! rake + CHECKSUMS + rake (10.3.2) sha256=814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8 + RUBY VERSION ruby 2.1.3p242 @@ -33,13 +36,13 @@ RSpec.describe Bundler::LockfileParser do it "returns the attributes" do attributes = described_class.sections_in_lockfile(lockfile_contents) expect(attributes).to contain_exactly( - "BUNDLED WITH", "DEPENDENCIES", "GEM", "GIT", "PLATFORMS", "RUBY VERSION" + "BUNDLED WITH", "CHECKSUMS", "DEPENDENCIES", "GEM", "GIT", "PLATFORMS", "RUBY VERSION" ) end end describe ".unknown_sections_in_lockfile" do - let(:lockfile_contents) { strip_whitespace(<<-L) } + let(:lockfile_contents) { <<~L } UNKNOWN ATTR UNKNOWN ATTR 2 @@ -60,7 +63,7 @@ RSpec.describe Bundler::LockfileParser do it "returns the same as > 1.0" do expect(subject).to contain_exactly( - described_class::BUNDLED, described_class::RUBY, described_class::PLUGIN + described_class::BUNDLED, described_class::CHECKSUMS, described_class::RUBY, described_class::PLUGIN ) end end @@ -70,7 +73,7 @@ RSpec.describe Bundler::LockfileParser do it "returns the same as for the release version" do expect(subject).to contain_exactly( - described_class::RUBY, described_class::PLUGIN + described_class::CHECKSUMS, described_class::RUBY, described_class::PLUGIN ) end end @@ -115,6 +118,14 @@ RSpec.describe Bundler::LockfileParser do let(:platforms) { [rb] } let(:bundler_version) { Gem::Version.new("1.12.0.rc.2") } let(:ruby_version) { "ruby 2.1.3p242" } + let(:lockfile_path) { Bundler.default_lockfile.relative_path_from(Dir.pwd) } + let(:rake_sha256_checksum) do + Bundler::Checksum.from_lock( + "sha256=814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8", + "#{lockfile_path}:20:17" + ) + end + let(:rake_checksums) { [rake_sha256_checksum] } shared_examples_for "parsing" do it "parses correctly" do @@ -125,6 +136,9 @@ RSpec.describe Bundler::LockfileParser do expect(subject.platforms).to eq platforms expect(subject.bundler_version).to eq bundler_version expect(subject.ruby_version).to eq ruby_version + rake_spec = specs.last + checksums = subject.sources.last.checksum_store.to_lock(specs.last) + expect(checksums).to eq("#{rake_spec.name_tuple.lock_name} #{rake_checksums.map(&:to_lock).sort.join(",")}") end end @@ -149,5 +163,59 @@ RSpec.describe Bundler::LockfileParser do let(:lockfile_contents) { super().sub("peiji-san!", "peiji-san!\n foo: bar") } include_examples "parsing" end + + context "when the checksum is urlsafe base64 encoded" do + let(:lockfile_contents) do + super().sub( + "sha256=814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8", + "sha256=gUgow08TFdfnt-gpUYRXfMTpabrWFWrAadAtY_WNgug=" + ) + end + include_examples "parsing" + end + + context "when the checksum is of an unknown algorithm" do + let(:rake_sha512_checksum) do + Bundler::Checksum.from_lock( + "sha512=pVDn9GLmcFkz8vj1ueiVxj5uGKkAyaqYjEX8zG6L5O4BeVg3wANaKbQdpj/B82Nd/MHVszy6polHcyotUdwilQ==", + "#{lockfile_path}:20:17" + ) + end + let(:lockfile_contents) do + super().sub( + "sha256=", + "sha512=pVDn9GLmcFkz8vj1ueiVxj5uGKkAyaqYjEX8zG6L5O4BeVg3wANaKbQdpj/B82Nd/MHVszy6polHcyotUdwilQ==,sha256=" + ) + end + let(:rake_checksums) { [rake_sha256_checksum, rake_sha512_checksum] } + include_examples "parsing" + end + + context "when CHECKSUMS has duplicate checksums in the lockfile that don't match" do + let(:bad_checksum) { "sha256=c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11" } + let(:lockfile_contents) { super().split(/(?<=CHECKSUMS\n)/m).insert(1, " rake (10.3.2) #{bad_checksum}\n").join } + + it "raises a security error" do + expect { subject }.to raise_error(Bundler::SecurityError) do |e| + expect(e.message).to match <<~MESSAGE + Bundler found mismatched checksums. This is a potential security risk. + rake (10.3.2) #{bad_checksum} + from the lockfile CHECKSUMS at #{lockfile_path}:20:17 + rake (10.3.2) #{rake_sha256_checksum.to_lock} + from the lockfile CHECKSUMS at #{lockfile_path}:21:17 + + To resolve this issue you can either: + 1. remove the matching checksum in #{lockfile_path}:21:17 + 2. run `bundle install` + or if you are sure that the new checksum from the lockfile CHECKSUMS at #{lockfile_path}:21:17 is correct: + 1. remove the matching checksum in #{lockfile_path}:20:17 + 2. run `bundle install` + + To ignore checksum security warnings, disable checksum validation with + `bundle config set --local disable_checksum_validation true` + MESSAGE + end + end + end end end diff --git a/spec/bundler/bundler/mirror_spec.rb b/spec/bundler/bundler/mirror_spec.rb index 1eaf1e9a8e..ba1c6ed413 100644 --- a/spec/bundler/bundler/mirror_spec.rb +++ b/spec/bundler/bundler/mirror_spec.rb @@ -36,12 +36,12 @@ RSpec.describe Bundler::Settings::Mirror do it "takes a string for the uri but returns an uri object" do mirror.uri = "http://localhost:9292" - expect(mirror.uri).to eq(Bundler::URI("http://localhost:9292")) + expect(mirror.uri).to eq(Gem::URI("http://localhost:9292")) end it "takes an uri object for the uri" do - mirror.uri = Bundler::URI("http://localhost:9293") - expect(mirror.uri).to eq(Bundler::URI("http://localhost:9293")) + mirror.uri = Gem::URI("http://localhost:9293") + expect(mirror.uri).to eq(Gem::URI("http://localhost:9293")) end context "without a uri" do @@ -145,7 +145,7 @@ RSpec.describe Bundler::Settings::Mirror do end RSpec.describe Bundler::Settings::Mirrors do - let(:localhost_uri) { Bundler::URI("http://localhost:9292") } + let(:localhost_uri) { Gem::URI("http://localhost:9292") } context "with a just created mirror" do let(:mirrors) do @@ -260,7 +260,7 @@ RSpec.describe Bundler::Settings::Mirrors do before { mirrors.parse("mirror.all.fallback_timeout", "true") } it "returns the source uri, not localhost" do - expect(mirrors.for("http://whatever.com").uri).to eq(Bundler::URI("http://whatever.com/")) + expect(mirrors.for("http://whatever.com").uri).to eq(Gem::URI("http://whatever.com/")) end end end @@ -270,7 +270,7 @@ RSpec.describe Bundler::Settings::Mirrors do context "without a fallback timeout" do it "returns the uri that is not mirrored" do - expect(mirrors.for("http://whatever.com").uri).to eq(Bundler::URI("http://whatever.com/")) + expect(mirrors.for("http://whatever.com").uri).to eq(Gem::URI("http://whatever.com/")) end it "returns localhost for rubygems.org" do @@ -282,11 +282,11 @@ RSpec.describe Bundler::Settings::Mirrors do before { mirrors.parse("mirror.http://rubygems.org/.fallback_timeout", "true") } it "returns the uri that is not mirrored" do - expect(mirrors.for("http://whatever.com").uri).to eq(Bundler::URI("http://whatever.com/")) + expect(mirrors.for("http://whatever.com").uri).to eq(Gem::URI("http://whatever.com/")) end it "returns rubygems.org for rubygems.org" do - expect(mirrors.for("http://rubygems.org/").uri).to eq(Bundler::URI("http://rubygems.org/")) + expect(mirrors.for("http://rubygems.org/").uri).to eq(Gem::URI("http://rubygems.org/")) end end end diff --git a/spec/bundler/bundler/plugin/api/source_spec.rb b/spec/bundler/bundler/plugin/api/source_spec.rb index 428ceb220a..ae02e08bea 100644 --- a/spec/bundler/bundler/plugin/api/source_spec.rb +++ b/spec/bundler/bundler/plugin/api/source_spec.rb @@ -51,7 +51,7 @@ RSpec.describe Bundler::Plugin::API::Source do context "to_lock" do it "returns the string with remote and type" do - expected = strip_whitespace <<-L + expected = <<~L PLUGIN SOURCE remote: #{uri} type: #{type} @@ -67,7 +67,7 @@ RSpec.describe Bundler::Plugin::API::Source do end it "includes them" do - expected = strip_whitespace <<-L + expected = <<~L PLUGIN SOURCE remote: #{uri} type: #{type} diff --git a/spec/bundler/bundler/plugin/dsl_spec.rb b/spec/bundler/bundler/plugin/dsl_spec.rb index 00e39dca69..235a549735 100644 --- a/spec/bundler/bundler/plugin/dsl_spec.rb +++ b/spec/bundler/bundler/plugin/dsl_spec.rb @@ -23,7 +23,7 @@ RSpec.describe Bundler::Plugin::DSL do it "adds #source with :type to list and also inferred_plugins list" do expect(dsl).to receive(:plugin).with("bundler-source-news").once - dsl.source("some_random_url", :type => "news") {} + dsl.source("some_random_url", type: "news") {} expect(dsl.inferred_plugins).to eq(["bundler-source-news"]) end @@ -31,8 +31,8 @@ RSpec.describe Bundler::Plugin::DSL do it "registers a source type plugin only once for multiple declarations" do expect(dsl).to receive(:plugin).with("bundler-source-news").and_call_original.once - dsl.source("some_random_url", :type => "news") {} - dsl.source("another_random_url", :type => "news") {} + dsl.source("some_random_url", type: "news") {} + dsl.source("another_random_url", type: "news") {} end end end diff --git a/spec/bundler/bundler/plugin/index_spec.rb b/spec/bundler/bundler/plugin/index_spec.rb index d34b0de342..5a7047459f 100644 --- a/spec/bundler/bundler/plugin/index_spec.rb +++ b/spec/bundler/bundler/plugin/index_spec.rb @@ -140,7 +140,7 @@ RSpec.describe Bundler::Plugin::Index do describe "after conflict" do let(:commands) { ["foo"] } let(:sources) { ["bar"] } - let(:hooks) { ["hoook"] } + let(:hooks) { ["thehook"] } shared_examples "it cleans up" do it "the path" do @@ -156,7 +156,7 @@ RSpec.describe Bundler::Plugin::Index do end it "the hook" do - expect(index.hook_plugins("xhoook")).to be_empty + expect(index.hook_plugins("xthehook")).to be_empty end end @@ -164,7 +164,7 @@ RSpec.describe Bundler::Plugin::Index do before do expect do path = lib_path("cplugin") - index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["xbar"], ["xhoook"]) + index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["xbar"], ["xthehook"]) end.to raise_error(Index::CommandConflict) end @@ -175,7 +175,7 @@ RSpec.describe Bundler::Plugin::Index do before do expect do path = lib_path("cplugin") - index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["xfoo"], ["bar"], ["xhoook"]) + index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["xfoo"], ["bar"], ["xthehook"]) end.to raise_error(Index::SourceConflict) end @@ -186,7 +186,7 @@ RSpec.describe Bundler::Plugin::Index do before do expect do path = lib_path("cplugin") - index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["bar"], ["xhoook"]) + index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["bar"], ["xthehook"]) end.to raise_error(Index::CommandConflict) end diff --git a/spec/bundler/bundler/plugin/installer_spec.rb b/spec/bundler/bundler/plugin/installer_spec.rb index e89720f6f7..ed40029f5a 100644 --- a/spec/bundler/bundler/plugin/installer_spec.rb +++ b/spec/bundler/bundler/plugin/installer_spec.rb @@ -6,8 +6,7 @@ RSpec.describe Bundler::Plugin::Installer do describe "cli install" do it "uses Gem.sources when non of the source is provided" do sources = double(:sources) - Bundler.settings # initialize it before we have to touch rubygems.ext_lock - allow(Bundler).to receive_message_chain("rubygems.sources") { sources } + allow(Gem).to receive(:sources) { sources } allow(installer).to receive(:install_rubygems). with("new-plugin", [">= 0"], sources).once @@ -21,15 +20,15 @@ RSpec.describe Bundler::Plugin::Installer do allow(installer).to receive(:install_git). and_return("new-plugin" => spec) - expect(installer.install(["new-plugin"], :git => "https://some.ran/dom")). + expect(installer.install(["new-plugin"], git: "https://some.ran/dom")). to eq("new-plugin" => spec) end it "returns the installed spec after installing local git plugins" do - allow(installer).to receive(:install_local_git). + allow(installer).to receive(:install_git). and_return("new-plugin" => spec) - expect(installer.install(["new-plugin"], :local_git => "/phony/path/repo")). + expect(installer.install(["new-plugin"], git: "/phony/path/repo")). to eq("new-plugin" => spec) end @@ -37,7 +36,7 @@ RSpec.describe Bundler::Plugin::Installer do allow(installer).to receive(:install_rubygems). and_return("new-plugin" => spec) - expect(installer.install(["new-plugin"], :source => "https://some.ran/dom")). + expect(installer.install(["new-plugin"], source: "https://some.ran/dom")). to eq("new-plugin" => spec) end end @@ -52,13 +51,13 @@ RSpec.describe Bundler::Plugin::Installer do context "git plugins" do before do - build_git "ga-plugin", :path => lib_path("ga-plugin") do |s| + build_git "ga-plugin", path: lib_path("ga-plugin") do |s| s.write "plugins.rb" end end let(:result) do - installer.install(["ga-plugin"], :git => file_uri_for(lib_path("ga-plugin"))) + installer.install(["ga-plugin"], git: file_uri_for(lib_path("ga-plugin"))) end it "returns the installed spec after installing" do @@ -75,13 +74,13 @@ RSpec.describe Bundler::Plugin::Installer do context "local git plugins" do before do - build_git "ga-plugin", :path => lib_path("ga-plugin") do |s| + build_git "ga-plugin", path: lib_path("ga-plugin") do |s| s.write "plugins.rb" end end let(:result) do - installer.install(["ga-plugin"], :local_git => lib_path("ga-plugin").to_s) + installer.install(["ga-plugin"], git: lib_path("ga-plugin").to_s) end it "returns the installed spec after installing" do @@ -98,7 +97,7 @@ RSpec.describe Bundler::Plugin::Installer do context "rubygems plugins" do let(:result) do - installer.install(["re-plugin"], :source => file_uri_for(gem_repo2)) + installer.install(["re-plugin"], source: file_uri_for(gem_repo2)) end it "returns the installed spec after installing " do @@ -113,7 +112,7 @@ RSpec.describe Bundler::Plugin::Installer do context "multiple plugins" do let(:result) do - installer.install(["re-plugin", "ma-plugin"], :source => file_uri_for(gem_repo2)) + installer.install(["re-plugin", "ma-plugin"], source: file_uri_for(gem_repo2)) end it "returns the installed spec after installing " do diff --git a/spec/bundler/bundler/plugin_spec.rb b/spec/bundler/bundler/plugin_spec.rb index 8a1a6cd97a..f41b4eff3a 100644 --- a/spec/bundler/bundler/plugin_spec.rb +++ b/spec/bundler/bundler/plugin_spec.rb @@ -9,11 +9,11 @@ RSpec.describe Bundler::Plugin do let(:spec2) { double(:spec2) } before do - build_lib "new-plugin", :path => lib_path("new-plugin") do |s| + build_lib "new-plugin", path: lib_path("new-plugin") do |s| s.write "plugins.rb" end - build_lib "another-plugin", :path => lib_path("another-plugin") do |s| + build_lib "another-plugin", path: lib_path("another-plugin") do |s| s.write "plugins.rb" end @@ -225,7 +225,7 @@ RSpec.describe Bundler::Plugin do end end - describe "#source_from_lock" do + describe "#from_lock" do it "returns instance of registered class initialized with locked opts" do opts = { "type" => "l_source", "remote" => "xyz", "other" => "random" } allow(index).to receive(:source_plugin).with("l_source") { "plugin_name" } @@ -236,7 +236,7 @@ RSpec.describe Bundler::Plugin do expect(SClass).to receive(:new). with(hash_including("type" => "l_source", "uri" => "xyz", "other" => "random")) { s_instance } - expect(subject.source_from_lock(opts)).to be(s_instance) + expect(subject.from_lock(opts)).to be(s_instance) end end @@ -275,17 +275,17 @@ RSpec.describe Bundler::Plugin do describe "#hook" do before do path = lib_path("foo-plugin") - build_lib "foo-plugin", :path => path do |s| + build_lib "foo-plugin", path: path do |s| s.write "plugins.rb", code end Bundler::Plugin::Events.send(:reset) - Bundler::Plugin::Events.send(:define, :EVENT_1, "event-1") - Bundler::Plugin::Events.send(:define, :EVENT_2, "event-2") + Bundler::Plugin::Events.send(:define, :EVENT1, "event-1") + Bundler::Plugin::Events.send(:define, :EVENT2, "event-2") - allow(index).to receive(:hook_plugins).with(Bundler::Plugin::Events::EVENT_1). + allow(index).to receive(:hook_plugins).with(Bundler::Plugin::Events::EVENT1). and_return(["foo-plugin", "", nil]) - allow(index).to receive(:hook_plugins).with(Bundler::Plugin::Events::EVENT_2). + allow(index).to receive(:hook_plugins).with(Bundler::Plugin::Events::EVENT2). and_return(["foo-plugin"]) allow(index).to receive(:plugin_path).with("foo-plugin").and_return(path) allow(index).to receive(:load_paths).with("foo-plugin").and_return([]) @@ -303,33 +303,33 @@ RSpec.describe Bundler::Plugin do it "executes the hook" do expect do - Plugin.hook(Bundler::Plugin::Events::EVENT_1) + Plugin.hook(Bundler::Plugin::Events::EVENT1) end.to output("hook for event 1\n").to_stdout end context "single plugin declaring more than one hook" do let(:code) { <<-RUBY } - Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT_1) {} - Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT_2) {} + Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT1) {} + Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT2) {} puts "loaded" RUBY it "evals plugins.rb once" do expect do - Plugin.hook(Bundler::Plugin::Events::EVENT_1) - Plugin.hook(Bundler::Plugin::Events::EVENT_2) + Plugin.hook(Bundler::Plugin::Events::EVENT1) + Plugin.hook(Bundler::Plugin::Events::EVENT2) end.to output("loaded\n").to_stdout end end context "a block is passed" do let(:code) { <<-RUBY } - Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT_1) { |&blk| blk.call } + Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT1) { |&blk| blk.call } RUBY it "is passed to the hook" do expect do - Plugin.hook(Bundler::Plugin::Events::EVENT_1) { puts "win" } + Plugin.hook(Bundler::Plugin::Events::EVENT1) { puts "win" } end.to output("win\n").to_stdout end end diff --git a/spec/bundler/bundler/remote_specification_spec.rb b/spec/bundler/bundler/remote_specification_spec.rb index 8115e026d8..f35b231d58 100644 --- a/spec/bundler/bundler/remote_specification_spec.rb +++ b/spec/bundler/bundler/remote_specification_spec.rb @@ -17,7 +17,7 @@ RSpec.describe Bundler::RemoteSpecification do end describe "#fetch_platform" do - let(:remote_spec) { double(:remote_spec, :platform => "jruby") } + let(:remote_spec) { double(:remote_spec, platform: "jruby") } before { allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec) } @@ -45,7 +45,7 @@ RSpec.describe Bundler::RemoteSpecification do let(:platform) { "jruby" } it "should return the spec name, version, and platform" do - expect(subject.full_name).to eq("foo-1.0.0-jruby") + expect(subject.full_name).to eq("foo-1.0.0-java") end end end @@ -113,7 +113,7 @@ RSpec.describe Bundler::RemoteSpecification do context "comparing a non sortable object" do let(:other) { Object.new } - let(:remote_spec) { double(:remote_spec, :platform => "jruby") } + let(:remote_spec) { double(:remote_spec, platform: "jruby") } before do allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec) @@ -127,8 +127,8 @@ RSpec.describe Bundler::RemoteSpecification do end describe "#__swap__" do - let(:spec) { double(:spec, :dependencies => []) } - let(:new_spec) { double(:new_spec, :dependencies => [], :runtime_dependencies => []) } + let(:spec) { double(:spec, dependencies: []) } + let(:new_spec) { double(:new_spec, dependencies: [], runtime_dependencies: []) } before { subject.instance_variable_set(:@_remote_specification, spec) } @@ -157,7 +157,7 @@ RSpec.describe Bundler::RemoteSpecification do describe "method missing" do context "and is present in Gem::Specification" do - let(:remote_spec) { double(:remote_spec, :authors => "abcd") } + let(:remote_spec) { double(:remote_spec, authors: "abcd") } before do allow(subject).to receive(:_remote_specification).and_return(remote_spec) @@ -172,7 +172,7 @@ RSpec.describe Bundler::RemoteSpecification do describe "respond to missing?" do context "and is present in Gem::Specification" do - let(:remote_spec) { double(:remote_spec, :authors => "abcd") } + let(:remote_spec) { double(:remote_spec, authors: "abcd") } before do allow(subject).to receive(:_remote_specification).and_return(remote_spec) diff --git a/spec/bundler/bundler/resolver/candidate_spec.rb b/spec/bundler/bundler/resolver/candidate_spec.rb new file mode 100644 index 0000000000..f7b378d32b --- /dev/null +++ b/spec/bundler/bundler/resolver/candidate_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Resolver::Candidate do + it "compares fine" do + version1 = described_class.new("1.12.5", specs: [Gem::Specification.new("foo", "1.12.5") {|s| s.platform = Gem::Platform::RUBY }]) + version2 = described_class.new("1.12.5") # passing no specs creates a platform specific candidate, so sorts higher + + expect(version2 >= version1).to be true + + expect(version1.generic! == version2.generic!).to be true + expect(version1.platform_specific! == version2.platform_specific!).to be true + + expect(version1.platform_specific! >= version2.generic!).to be true + expect(version2.platform_specific! >= version1.generic!).to be true + + version1 = described_class.new("1.12.5", specs: [Gem::Specification.new("foo", "1.12.5") {|s| s.platform = Gem::Platform::RUBY }]) + version2 = described_class.new("1.12.5", specs: [Gem::Specification.new("foo", "1.12.5") {|s| s.platform = Gem::Platform::X64_LINUX }]) + + expect(version2 >= version1).to be true + end +end diff --git a/spec/bundler/bundler/ruby_dsl_spec.rb b/spec/bundler/bundler/ruby_dsl_spec.rb index bc1ca98457..384ac4b8b2 100644 --- a/spec/bundler/bundler/ruby_dsl_spec.rb +++ b/spec/bundler/bundler/ruby_dsl_spec.rb @@ -7,28 +7,37 @@ RSpec.describe Bundler::RubyDsl do include Bundler::RubyDsl attr_reader :ruby_version + attr_accessor :gemfile end let(:dsl) { MockDSL.new } let(:ruby_version) { "2.0.0" } + let(:ruby_version_arg) { ruby_version } let(:version) { "2.0.0" } let(:engine) { "jruby" } let(:engine_version) { "9000" } let(:patchlevel) { "100" } let(:options) do - { :patchlevel => patchlevel, - :engine => engine, - :engine_version => engine_version } + { patchlevel: patchlevel, + engine: engine, + engine_version: engine_version } end + let(:project_root) { Pathname.new("/path/to/project") } + let(:gemfile) { project_root.join("Gemfile") } + before { allow(Bundler).to receive(:root).and_return(project_root) } let(:invoke) do proc do - args = Array(ruby_version) + [options] + args = [] + args << ruby_version_arg if ruby_version_arg + args << options + dsl.ruby(*args) end end subject do + dsl.gemfile = gemfile invoke.call dsl.ruby_version end @@ -59,6 +68,15 @@ RSpec.describe Bundler::RubyDsl do it_behaves_like "it stores the ruby version" end + context "with a preview version" do + let(:ruby_version) { "3.3.0-preview2" } + + it "stores the version" do + expect(subject.versions).to eq(Array("3.3.0.preview2")) + expect(subject.gem_version.version).to eq("3.3.0.preview2") + end + end + context "with two requirements in the same string" do let(:ruby_version) { ">= 2.0.0, < 3.0" } it "raises an error" do @@ -91,5 +109,94 @@ RSpec.describe Bundler::RubyDsl do it_behaves_like "it stores the ruby version" end end + + context "with a file option" do + let(:file) { ".ruby-version" } + let(:ruby_version_file_path) { gemfile.dirname.join(file) } + let(:options) do + { file: file, + patchlevel: patchlevel, + engine: engine, + engine_version: engine_version } + end + let(:ruby_version_arg) { nil } + let(:file_content) { "#{version}\n" } + + before do + allow(Bundler).to receive(:read_file) do |path| + raise Errno::ENOENT, <<~ERROR unless path == ruby_version_file_path + #{file} not found in specs: + expected: #{ruby_version_file_path} + received: #{path} + ERROR + file_content + end + end + + it_behaves_like "it stores the ruby version" + + context "with the Gemfile ruby file: path is relative to the Gemfile in a subdir" do + let(:gemfile) { project_root.join("subdir", "Gemfile") } + let(:file) { "../.ruby-version" } + let(:ruby_version_file_path) { gemfile.dirname.join(file) } + + it_behaves_like "it stores the ruby version" + end + + context "with bundler root in a subdir of the project" do + let(:project_root) { Pathname.new("/path/to/project/subdir") } + let(:gemfile) { project_root.parent.join("Gemfile") } + + it_behaves_like "it stores the ruby version" + end + + context "with the ruby- prefix in the file" do + let(:file_content) { "ruby-#{version}\n" } + + it_behaves_like "it stores the ruby version" + end + + context "and a version" do + let(:ruby_version_arg) { version } + + it "raises an error" do + expect { subject }.to raise_error(Bundler::GemfileError, "Do not pass version argument when using :file option") + end + end + + context "with a @gemset" do + let(:file_content) { "ruby-#{version}@gemset\n" } + + it "raises an error" do + expect { subject }.to raise_error(Gem::Requirement::BadRequirementError, "Illformed requirement [\"#{version}@gemset\"]") + end + end + + context "with a .tool-versions file format" do + let(:file) { ".tool-versions" } + let(:ruby_version_arg) { nil } + let(:file_content) do + <<~TOOLS + nodejs 18.16.0 + ruby #{version} # This is a comment + pnpm 8.6.12 + TOOLS + end + + it_behaves_like "it stores the ruby version" + + context "with extra spaces and a very cozy comment" do + let(:file_content) do + <<~TOOLS + nodejs 18.16.0 + ruby #{version}# This is a cozy comment + pnpm 8.6.12 + TOOLS + end + + it_behaves_like "it stores the ruby version" + end + end + end end end diff --git a/spec/bundler/bundler/ruby_version_spec.rb b/spec/bundler/bundler/ruby_version_spec.rb index 8c6c071d7f..39d0571361 100644 --- a/spec/bundler/bundler/ruby_version_spec.rb +++ b/spec/bundler/bundler/ruby_version_spec.rb @@ -400,19 +400,19 @@ RSpec.describe "Bundler::RubyVersion and its subclasses" do let(:bundler_system_ruby_version) { subject } around do |example| - if Bundler::RubyVersion.instance_variable_defined?("@ruby_version") + if Bundler::RubyVersion.instance_variable_defined?("@system") begin - old_ruby_version = Bundler::RubyVersion.instance_variable_get("@ruby_version") - Bundler::RubyVersion.remove_instance_variable("@ruby_version") + old_ruby_version = Bundler::RubyVersion.instance_variable_get("@system") + Bundler::RubyVersion.remove_instance_variable("@system") example.run ensure - Bundler::RubyVersion.instance_variable_set("@ruby_version", old_ruby_version) + Bundler::RubyVersion.instance_variable_set("@system", old_ruby_version) end else begin example.run ensure - Bundler::RubyVersion.remove_instance_variable("@ruby_version") + Bundler::RubyVersion.remove_instance_variable("@system") end end end @@ -427,9 +427,8 @@ RSpec.describe "Bundler::RubyVersion and its subclasses" do end describe "#version" do - it "should return a copy of the value of RUBY_VERSION" do - expect(subject.versions).to eq([RUBY_VERSION]) - expect(subject.versions.first).to_not be(RUBY_VERSION) + it "should return the value of Gem.ruby_version as a string" do + expect(subject.versions).to eq([Gem.ruby_version.to_s]) end end @@ -446,13 +445,12 @@ RSpec.describe "Bundler::RubyVersion and its subclasses" do describe "#engine_version" do context "engine is ruby" do before do - stub_const("RUBY_ENGINE_VERSION", "2.2.4") + allow(Gem).to receive(:ruby_version).and_return(Gem::Version.new("2.2.4")) stub_const("RUBY_ENGINE", "ruby") end - it "should return a copy of the value of RUBY_ENGINE_VERSION" do + it "should return the value of Gem.ruby_version as a string" do expect(bundler_system_ruby_version.engine_versions).to eq(["2.2.4"]) - expect(bundler_system_ruby_version.engine_versions.first).to_not be(RUBY_ENGINE_VERSION) end end @@ -498,31 +496,5 @@ RSpec.describe "Bundler::RubyVersion and its subclasses" do end end end - - describe "#to_gem_version_with_patchlevel" do - shared_examples_for "the patchlevel is omitted" do - it "does not include a patch level" do - expect(subject.to_gem_version_with_patchlevel.to_s).to eq(version) - end - end - - context "with nil patch number" do - let(:patchlevel) { nil } - - it_behaves_like "the patchlevel is omitted" - end - - context "with negative patch number" do - let(:patchlevel) { -1 } - - it_behaves_like "the patchlevel is omitted" - end - - context "with a valid patch number" do - it "uses the specified patchlevel as patchlevel" do - expect(subject.to_gem_version_with_patchlevel.to_s).to eq("#{version}.#{patchlevel}") - end - end - end end end diff --git a/spec/bundler/bundler/rubygems_integration_spec.rb b/spec/bundler/bundler/rubygems_integration_spec.rb index 11fa2f4e0d..b6bda9f43e 100644 --- a/spec/bundler/bundler/rubygems_integration_spec.rb +++ b/spec/bundler/bundler/rubygems_integration_spec.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true RSpec.describe Bundler::RubygemsIntegration do - it "uses the same chdir lock as rubygems" do - expect(Bundler.rubygems.ext_lock).to eq(Gem::Ext::Builder::CHDIR_MONITOR) - end - context "#validate" do let(:spec) do Gem::Specification.new do |s| @@ -34,35 +30,24 @@ RSpec.describe Bundler::RubygemsIntegration do end end - describe "#configuration" do - it "handles Gem::SystemExitException errors" do - allow(Gem).to receive(:configuration) { raise Gem::SystemExitException.new(1) } - expect { Bundler.rubygems.configuration }.to raise_error(Gem::SystemExitException) - end - end - describe "#download_gem" do let(:bundler_retry) { double(Bundler::Retry) } - let(:retry) { double("Bundler::Retry") } - let(:uri) { Bundler::URI.parse("https://foo.bar") } - let(:path) { Gem.path.first } + let(:uri) { Gem::URI.parse("https://foo.bar") } + let(:cache_dir) { "#{Gem.path.first}/cache" } let(:spec) do - spec = Bundler::RemoteSpecification.new("Foo", Gem::Version.new("2.5.2"), - Gem::Platform::RUBY, nil) + spec = Gem::Specification.new("Foo", Gem::Version.new("2.5.2")) spec.remote = Bundler::Source::Rubygems::Remote.new(uri.to_s) spec end let(:fetcher) { double("gem_remote_fetcher") } it "successfully downloads gem with retries" do - expect(Bundler.rubygems).to receive(:gem_remote_fetcher).and_return(fetcher) - expect(fetcher).to receive(:headers=).with("X-Gemfile-Source" => "https://foo.bar") expect(Bundler::Retry).to receive(:new).with("download gem from #{uri}/"). and_return(bundler_retry) expect(bundler_retry).to receive(:attempts).and_yield - expect(fetcher).to receive(:download).with(spec, uri, path) + expect(fetcher).to receive(:cache_update_path) - Bundler.rubygems.download_gem(spec, uri, path) + Bundler.rubygems.download_gem(spec, uri, cache_dir, fetcher) end end @@ -73,30 +58,36 @@ RSpec.describe Bundler::RubygemsIntegration do let(:prerelease_specs_response) { Marshal.dump(["prerelease_specs"]) } context "when a rubygems source mirror is set" do - let(:orig_uri) { Bundler::URI("http://zombo.com") } - let(:remote_with_mirror) { double("remote", :uri => uri, :original_uri => orig_uri) } + let(:orig_uri) { Gem::URI("http://zombo.com") } + let(:remote_with_mirror) { double("remote", uri: uri, original_uri: orig_uri) } it "sets the 'X-Gemfile-Source' header containing the original source" do - expect(Bundler.rubygems).to receive(:gem_remote_fetcher).twice.and_return(fetcher) - expect(fetcher).to receive(:headers=).with("X-Gemfile-Source" => "http://zombo.com").twice expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(specs_response) expect(fetcher).to receive(:fetch_path).with(uri + "prerelease_specs.4.8.gz").and_return(prerelease_specs_response) - result = Bundler.rubygems.fetch_all_remote_specs(remote_with_mirror) + result = Bundler.rubygems.fetch_all_remote_specs(remote_with_mirror, fetcher) expect(result).to eq(%w[specs prerelease_specs]) end end context "when there is no rubygems source mirror set" do - let(:remote_no_mirror) { double("remote", :uri => uri, :original_uri => nil) } + let(:remote_no_mirror) { double("remote", uri: uri, original_uri: nil) } it "does not set the 'X-Gemfile-Source' header" do - expect(Bundler.rubygems).to receive(:gem_remote_fetcher).twice.and_return(fetcher) - expect(fetcher).to_not receive(:headers=) expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(specs_response) expect(fetcher).to receive(:fetch_path).with(uri + "prerelease_specs.4.8.gz").and_return(prerelease_specs_response) - result = Bundler.rubygems.fetch_all_remote_specs(remote_no_mirror) + result = Bundler.rubygems.fetch_all_remote_specs(remote_no_mirror, fetcher) expect(result).to eq(%w[specs prerelease_specs]) end end + + context "when loading an unexpected class" do + let(:remote_no_mirror) { double("remote", uri: uri, original_uri: nil) } + let(:unexpected_specs_response) { Marshal.dump(3) } + + it "raises a MarshalError error" do + expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(unexpected_specs_response) + expect { Bundler.rubygems.fetch_all_remote_specs(remote_no_mirror, fetcher) }.to raise_error(Bundler::MarshalError, /unexpected class/i) + end + end end end diff --git a/spec/bundler/bundler/settings/validator_spec.rb b/spec/bundler/bundler/settings/validator_spec.rb index e4ffd89435..b252ba59a0 100644 --- a/spec/bundler/bundler/settings/validator_spec.rb +++ b/spec/bundler/bundler/settings/validator_spec.rb @@ -44,14 +44,14 @@ RSpec.describe Bundler::Settings::Validator do validate!("without", "b:c", "BUNDLE_WITH" => "a") end.not_to raise_error - expect { validate!("with", "b:c", "BUNDLE_WITHOUT" => "c:d") }.to raise_error Bundler::InvalidOption, strip_whitespace(<<-EOS).strip + expect { validate!("with", "b:c", "BUNDLE_WITHOUT" => "c:d") }.to raise_error Bundler::InvalidOption, <<~EOS.strip Setting `with` to "b:c" failed: - a group cannot be in both `with` & `without` simultaneously - `without` is current set to [:c, :d] - the `c` groups conflict EOS - expect { validate!("without", "b:c", "BUNDLE_WITH" => "c:d") }.to raise_error Bundler::InvalidOption, strip_whitespace(<<-EOS).strip + expect { validate!("without", "b:c", "BUNDLE_WITH" => "c:d") }.to raise_error Bundler::InvalidOption, <<~EOS.strip Setting `without` to "b:c" failed: - a group cannot be in both `with` & `without` simultaneously - `with` is current set to [:c, :d] @@ -74,7 +74,7 @@ RSpec.describe Bundler::Settings::Validator do describe "#fail!" do it "raises with a helpful message" do - expect { subject.fail!("key", "value", "reason1", "reason2") }.to raise_error Bundler::InvalidOption, strip_whitespace(<<-EOS).strip + expect { subject.fail!("key", "value", "reason1", "reason2") }.to raise_error Bundler::InvalidOption, <<~EOS.strip Setting `key` to "value" failed: - rule description - reason1 diff --git a/spec/bundler/bundler/settings_spec.rb b/spec/bundler/bundler/settings_spec.rb index 24e3de7ba8..634e0faf91 100644 --- a/spec/bundler/bundler/settings_spec.rb +++ b/spec/bundler/bundler/settings_spec.rb @@ -27,7 +27,7 @@ RSpec.describe Bundler::Settings do "gem.mit" => "false", "gem.test" => "minitest", "thingy" => <<-EOS.tr("\n", " "), ---asdf --fdsa --ty=oh man i hope this doesnt break bundler because +--asdf --fdsa --ty=oh man i hope this doesn't break bundler because that would suck --ehhh=oh geez it looks like i might have broken bundler somehow --very-important-option=DontDeleteRoo --very-important-option=DontDeleteRoo @@ -131,7 +131,7 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow Bundler.settings.set_command_option :no_install, true - Bundler.settings.temporary(:no_install => false) do + Bundler.settings.temporary(no_install: false) do expect(Bundler.settings[:no_install]).to eq false end @@ -147,12 +147,12 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow context "when called without a block" do it "leaves the setting changed" do - Bundler.settings.temporary(:foo => :random) + Bundler.settings.temporary(foo: :random) expect(Bundler.settings[:foo]).to eq "random" end it "returns nil" do - expect(Bundler.settings.temporary(:foo => :bar)).to be_nil + expect(Bundler.settings.temporary(foo: :bar)).to be_nil end end end @@ -179,7 +179,7 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow end describe "#mirror_for" do - let(:uri) { Bundler::URI("https://rubygems.org/") } + let(:uri) { Gem::URI("https://rubygems.org/") } context "with no configured mirror" do it "returns the original URI" do @@ -192,7 +192,7 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow end context "with a configured mirror" do - let(:mirror_uri) { Bundler::URI("https://rubygems-mirror.org/") } + let(:mirror_uri) { Gem::URI("https://rubygems-mirror.org/") } before { settings.set_local "mirror.https://rubygems.org/", mirror_uri.to_s } @@ -213,7 +213,7 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow end context "with a file URI" do - let(:mirror_uri) { Bundler::URI("file:/foo/BAR/baz/qUx/") } + let(:mirror_uri) { Gem::URI("file:/foo/BAR/baz/qUx/") } it "returns the mirror URI" do expect(settings.mirror_for(uri)).to eq(mirror_uri) @@ -231,7 +231,7 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow end describe "#credentials_for" do - let(:uri) { Bundler::URI("https://gemserver.example.org/") } + let(:uri) { Gem::URI("https://gemserver.example.org/") } let(:credentials) { "username:password" } context "with no configured credentials" do @@ -291,7 +291,7 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow it "reads older keys without trailing slashes" do settings.set_local "mirror.https://rubygems.org", "http://rubygems-mirror.org" expect(settings.mirror_for("https://rubygems.org/")).to eq( - Bundler::URI("http://rubygems-mirror.org/") + Gem::URI("http://rubygems-mirror.org/") ) end @@ -319,6 +319,15 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow expect(settings["mirror.https://rubygems.org/"]).to eq("http://rubygems-mirror.org") end + it "ignores commented out keys" do + create_file bundled_app(".bundle/config"), <<~C + # BUNDLE_MY-PERSONAL-SERVER__ORG: my-personal-server.org + C + + expect(Bundler.ui).not_to receive(:warn) + expect(settings.all).to be_empty + end + it "converts older keys with dashes" do config("BUNDLE_MY-PERSONAL-SERVER__ORG" => "my-personal-server.org") expect(Bundler.ui).to receive(:warn).with( diff --git a/spec/bundler/bundler/shared_helpers_spec.rb b/spec/bundler/bundler/shared_helpers_spec.rb index 68a24be31c..918f73b337 100644 --- a/spec/bundler/bundler/shared_helpers_spec.rb +++ b/spec/bundler/bundler/shared_helpers_spec.rb @@ -1,12 +1,8 @@ # frozen_string_literal: true RSpec.describe Bundler::SharedHelpers do - let(:ext_lock_double) { double(:ext_lock) } - before do pwd_stub - allow(Bundler.rubygems).to receive(:ext_lock).and_return(ext_lock_double) - allow(ext_lock_double).to receive(:synchronize) {|&block| block.call } end let(:pwd_stub) { allow(subject).to receive(:pwd).and_return(bundled_app) } @@ -242,7 +238,14 @@ RSpec.describe Bundler::SharedHelpers do shared_examples_for "ENV['RUBYOPT'] gets set correctly" do it "ensures -rbundler/setup is at the beginning of ENV['RUBYOPT']" do subject.set_bundle_environment - expect(ENV["RUBYOPT"].split(" ")).to start_with("-r#{source_lib_dir}/bundler/setup") + expect(ENV["RUBYOPT"].split(" ")).to start_with("-r#{install_path}/bundler/setup") + end + end + + shared_examples_for "ENV['BUNDLER_SETUP'] gets set correctly" do + it "ensures bundler/setup is set in ENV['BUNDLER_SETUP']" do + subject.set_bundle_environment + expect(ENV["BUNDLER_SETUP"]).to eq("#{source_lib_dir}/bundler/setup") end end @@ -281,7 +284,7 @@ RSpec.describe Bundler::SharedHelpers do if Gem.respond_to?(:path_separator) allow(Gem).to receive(:path_separator).and_return(":") else - stub_const("File::PATH_SEPARATOR", ":".freeze) + stub_const("File::PATH_SEPARATOR", ":") end allow(Bundler).to receive(:bundle_path) { Pathname.new("so:me/dir/bin") } expect { subject.send(:validate_bundle_path) }.to raise_error( @@ -358,20 +361,41 @@ RSpec.describe Bundler::SharedHelpers do end end - context "ENV['RUBYOPT'] does not exist" do - before { ENV.delete("RUBYOPT") } + context "when bundler install path is standard" do + let(:install_path) { source_lib_dir } - it_behaves_like "ENV['RUBYOPT'] gets set correctly" - end + context "ENV['RUBYOPT'] does not exist" do + before { ENV.delete("RUBYOPT") } - context "ENV['RUBYOPT'] exists without -rbundler/setup" do - before { ENV["RUBYOPT"] = "-I/some_app_path/lib" } + it_behaves_like "ENV['RUBYOPT'] gets set correctly" + end - it_behaves_like "ENV['RUBYOPT'] gets set correctly" + context "ENV['RUBYOPT'] exists without -rbundler/setup" do + before { ENV["RUBYOPT"] = "-I/some_app_path/lib" } + + it_behaves_like "ENV['RUBYOPT'] gets set correctly" + end + + context "ENV['RUBYOPT'] exists and contains -rbundler/setup" do + before { ENV["RUBYOPT"] = "-rbundler/setup" } + + it_behaves_like "ENV['RUBYOPT'] gets set correctly" + end end - context "ENV['RUBYOPT'] exists and contains -rbundler/setup" do - before { ENV["RUBYOPT"] = "-rbundler/setup" } + context "when bundler install path contains special characters" do + let(:install_path) { "/opt/ruby3.3.0-preview2/lib/ruby/3.3.0+0" } + + before do + ENV["RUBYOPT"] = "-r#{install_path}/bundler/setup" + allow(File).to receive(:expand_path).and_return("#{install_path}/bundler/setup") + allow(Gem).to receive(:bin_path).and_return("#{install_path}/bundler/setup") + end + + it "ensures -rbundler/setup is not duplicated" do + subject.set_bundle_environment + expect(ENV["RUBYOPT"].split(" ").grep(%r{-r.*/bundler/setup}).length).to eq(1) + end it_behaves_like "ENV['RUBYOPT'] gets set correctly" end @@ -494,4 +518,34 @@ RSpec.describe Bundler::SharedHelpers do end end end + + describe "#major_deprecation" do + before { allow(Bundler).to receive(:bundler_major_version).and_return(37) } + before { allow(Bundler.ui).to receive(:warn) } + + it "prints and raises nothing below the deprecated major version" do + subject.major_deprecation(38, "Message") + subject.major_deprecation(39, "Message", removed_message: "Removal", print_caller_location: true) + expect(Bundler.ui).not_to have_received(:warn) + end + + it "prints but does not raise _at_ the deprecated major version" do + subject.major_deprecation(37, "Message") + subject.major_deprecation(37, "Message", removed_message: "Removal") + expect(Bundler.ui).to have_received(:warn).with("[DEPRECATED] Message").twice + + subject.major_deprecation(37, "Message", print_caller_location: true) + expect(Bundler.ui).to have_received(:warn). + with(a_string_matching(/^\[DEPRECATED\] Message \(called at .*:\d+\)$/)) + end + + it "raises the appropriate errors when _past_ the deprecated major version" do + expect { subject.major_deprecation(36, "Message") }. + to raise_error(Bundler::DeprecatedError, "[REMOVED] Message") + expect { subject.major_deprecation(36, "Message", removed_message: "Removal") }. + to raise_error(Bundler::DeprecatedError, "[REMOVED] Removal") + expect { subject.major_deprecation(35, "Message", removed_message: "Removal", print_caller_location: true) }. + to raise_error(Bundler::DeprecatedError, /^\[REMOVED\] Removal \(called at .*:\d+\)$/) + end + end end diff --git a/spec/bundler/bundler/source/git/git_proxy_spec.rb b/spec/bundler/bundler/source/git/git_proxy_spec.rb index 97f06973cb..1450316d59 100644 --- a/spec/bundler/bundler/source/git/git_proxy_spec.rb +++ b/spec/bundler/bundler/source/git/git_proxy_spec.rb @@ -3,29 +3,84 @@ RSpec.describe Bundler::Source::Git::GitProxy do let(:path) { Pathname("path") } let(:uri) { "https://github.com/rubygems/rubygems.git" } - let(:ref) { "HEAD" } + let(:ref) { nil } + let(:branch) { nil } + let(:tag) { nil } + let(:options) { { "ref" => ref, "branch" => branch, "tag" => tag }.compact } let(:revision) { nil } let(:git_source) { nil } - subject { described_class.new(path, uri, ref, revision, git_source) } + let(:clone_result) { double(Process::Status, success?: true) } + let(:base_clone_args) { ["clone", "--bare", "--no-hardlinks", "--quiet", "--no-tags", "--depth", "1", "--single-branch"] } + subject(:git_proxy) { described_class.new(path, uri, options, revision, git_source) } + + context "with explicit ref" do + context "with branch only" do + let(:branch) { "main" } + it "sets explicit ref to branch" do + expect(git_proxy.explicit_ref).to eq(branch) + end + end + + context "with ref only" do + let(:ref) { "HEAD" } + it "sets explicit ref to ref" do + expect(git_proxy.explicit_ref).to eq(ref) + end + end + + context "with tag only" do + let(:tag) { "v1.0" } + it "sets explicit ref to ref" do + expect(git_proxy.explicit_ref).to eq(tag) + end + end + + context "with tag and branch" do + let(:tag) { "v1.0" } + let(:branch) { "main" } + it "raises error" do + expect { git_proxy }.to raise_error(Bundler::Source::Git::AmbiguousGitReference) + end + end + + context "with tag and ref" do + let(:tag) { "v1.0" } + let(:ref) { "HEAD" } + it "raises error" do + expect { git_proxy }.to raise_error(Bundler::Source::Git::AmbiguousGitReference) + end + end + + context "with branch and ref" do + let(:branch) { "main" } + let(:ref) { "HEAD" } + it "honors ref over branch" do + expect(git_proxy.explicit_ref).to eq(ref) + end + end + end context "with configured credentials" do it "adds username and password to URI" do Bundler.settings.temporary(uri => "u:p") do - expect(subject).to receive(:git_retry).with("clone", "https://u:p@github.com/rubygems/rubygems.git", any_args) + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", "https://u:p@github.com/rubygems/rubygems.git", path.to_s], nil).and_return(["", "", clone_result]) subject.checkout end end it "adds username and password to URI for host" do Bundler.settings.temporary("github.com" => "u:p") do - expect(subject).to receive(:git_retry).with("clone", "https://u:p@github.com/rubygems/rubygems.git", any_args) + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", "https://u:p@github.com/rubygems/rubygems.git", path.to_s], nil).and_return(["", "", clone_result]) subject.checkout end end it "does not add username and password to mismatched URI" do Bundler.settings.temporary("https://u:p@github.com/rubygems/rubygems-mismatch.git" => "u:p") do - expect(subject).to receive(:git_retry).with("clone", uri, any_args) + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", uri, path.to_s], nil).and_return(["", "", clone_result]) subject.checkout end end @@ -33,9 +88,10 @@ RSpec.describe Bundler::Source::Git::GitProxy do it "keeps original userinfo" do Bundler.settings.temporary("github.com" => "u:p") do original = "https://orig:info@github.com/rubygems/rubygems.git" - subject = described_class.new(Pathname("path"), original, "HEAD") - expect(subject).to receive(:git_retry).with("clone", original, any_args) - subject.checkout + git_proxy = described_class.new(Pathname("path"), original, options) + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", original, path.to_s], nil).and_return(["", "", clone_result]) + git_proxy.checkout end end end @@ -43,46 +99,46 @@ RSpec.describe Bundler::Source::Git::GitProxy do describe "#version" do context "with a normal version number" do before do - expect(subject).to receive(:git).with("--version"). + expect(git_proxy).to receive(:git_local).with("--version"). and_return("git version 1.2.3") end it "returns the git version number" do - expect(subject.version).to eq("1.2.3") + expect(git_proxy.version).to eq("1.2.3") end it "does not raise an error when passed into Gem::Version.create" do - expect { Gem::Version.create subject.version }.not_to raise_error + expect { Gem::Version.create git_proxy.version }.not_to raise_error end end context "with a OSX version number" do before do - expect(subject).to receive(:git).with("--version"). + expect(git_proxy).to receive(:git_local).with("--version"). and_return("git version 1.2.3 (Apple Git-BS)") end it "strips out OSX specific additions in the version string" do - expect(subject.version).to eq("1.2.3") + expect(git_proxy.version).to eq("1.2.3") end it "does not raise an error when passed into Gem::Version.create" do - expect { Gem::Version.create subject.version }.not_to raise_error + expect { Gem::Version.create git_proxy.version }.not_to raise_error end end context "with a msysgit version number" do before do - expect(subject).to receive(:git).with("--version"). + expect(git_proxy).to receive(:git_local).with("--version"). and_return("git version 1.2.3.msysgit.0") end it "strips out msysgit specific additions in the version string" do - expect(subject.version).to eq("1.2.3") + expect(git_proxy.version).to eq("1.2.3") end it "does not raise an error when passed into Gem::Version.create" do - expect { Gem::Version.create subject.version }.not_to raise_error + expect { Gem::Version.create git_proxy.version }.not_to raise_error end end end @@ -90,62 +146,55 @@ RSpec.describe Bundler::Source::Git::GitProxy do describe "#full_version" do context "with a normal version number" do before do - expect(subject).to receive(:git).with("--version"). + expect(git_proxy).to receive(:git_local).with("--version"). and_return("git version 1.2.3") end it "returns the git version number" do - expect(subject.full_version).to eq("1.2.3") + expect(git_proxy.full_version).to eq("1.2.3") end end context "with a OSX version number" do before do - expect(subject).to receive(:git).with("--version"). + expect(git_proxy).to receive(:git_local).with("--version"). and_return("git version 1.2.3 (Apple Git-BS)") end it "does not strip out OSX specific additions in the version string" do - expect(subject.full_version).to eq("1.2.3 (Apple Git-BS)") + expect(git_proxy.full_version).to eq("1.2.3 (Apple Git-BS)") end end context "with a msysgit version number" do before do - expect(subject).to receive(:git).with("--version"). + expect(git_proxy).to receive(:git_local).with("--version"). and_return("git version 1.2.3.msysgit.0") end it "does not strip out msysgit specific additions in the version string" do - expect(subject.full_version).to eq("1.2.3.msysgit.0") + expect(git_proxy.full_version).to eq("1.2.3.msysgit.0") end end end - describe "#copy_to" do - let(:cache) { tmpdir("cache_path") } - let(:destination) { tmpdir("copy_to_path") } - let(:submodules) { false } + it "doesn't allow arbitrary code execution through Gemfile uris with a leading dash" do + gemfile <<~G + gem "poc", git: "-u./pay:load.sh" + G - context "when given a SHA as a revision" do - let(:revision) { "abcd" * 10 } - let(:command) { ["reset", "--hard", revision] } - let(:command_for_display) { "git #{command.shelljoin}" } + file = bundled_app("pay:load.sh") - it "fails gracefully when resetting to the revision fails" do - expect(subject).to receive(:git_retry).with("clone", any_args) { destination.mkpath } - expect(subject).to receive(:git_retry).with("fetch", any_args, :dir => destination) - expect(subject).to receive(:git).with(*command, :dir => destination).and_raise(Bundler::Source::Git::GitCommandError.new(command_for_display, destination)) - expect(subject).not_to receive(:git) + create_file file, <<~RUBY + #!/bin/sh - expect { subject.copy_to(destination, submodules) }. - to raise_error( - Bundler::Source::Git::MissingGitRevisionError, - "Git error: command `#{command_for_display}` in directory #{destination} has failed.\n" \ - "Revision #{revision} does not exist in the repository #{uri}. Maybe you misspelled it?\n" \ - "If this error persists you could try removing the cache directory '#{destination}'" - ) - end - end + touch #{bundled_app("canary")} + RUBY + + FileUtils.chmod("+x", file) + + bundle :lock, raise_on_error: false + + expect(Pathname.new(bundled_app("canary"))).not_to exist end end diff --git a/spec/bundler/bundler/source/git_spec.rb b/spec/bundler/bundler/source/git_spec.rb index 6668b6e69a..feef54bbf4 100644 --- a/spec/bundler/bundler/source/git_spec.rb +++ b/spec/bundler/bundler/source/git_spec.rb @@ -24,5 +24,50 @@ RSpec.describe Bundler::Source::Git do expect(subject.to_s).to eq "https://x-oauth-basic@github.com/foo/bar.git" end end + + context "when the source has a glob specifier" do + let(:glob) { "bar/baz/*.gemspec" } + let(:options) do + { "uri" => uri, "glob" => glob } + end + + it "includes it" do + expect(subject.to_s).to eq "https://github.com/foo/bar.git (glob: bar/baz/*.gemspec)" + end + end + + context "when the source has a reference" do + let(:git_proxy_stub) do + instance_double(Bundler::Source::Git::GitProxy, revision: "123abc", branch: "v1.0.0") + end + let(:options) do + { "uri" => uri, "ref" => "v1.0.0" } + end + + before do + allow(Bundler::Source::Git::GitProxy).to receive(:new).and_return(git_proxy_stub) + end + + it "includes it" do + expect(subject.to_s).to eq "https://github.com/foo/bar.git (at v1.0.0@123abc)" + end + end + + context "when the source has both reference and glob specifiers" do + let(:git_proxy_stub) do + instance_double(Bundler::Source::Git::GitProxy, revision: "123abc", branch: "v1.0.0") + end + let(:options) do + { "uri" => uri, "ref" => "v1.0.0", "glob" => "gems/foo/*.gemspec" } + end + + before do + allow(Bundler::Source::Git::GitProxy).to receive(:new).and_return(git_proxy_stub) + end + + it "includes both" do + expect(subject.to_s).to eq "https://github.com/foo/bar.git (at v1.0.0@123abc, glob: gems/foo/*.gemspec)" + end + end end end diff --git a/spec/bundler/bundler/source/rubygems/remote_spec.rb b/spec/bundler/bundler/source/rubygems/remote_spec.rb index 07ce4f968e..56f3bee459 100644 --- a/spec/bundler/bundler/source/rubygems/remote_spec.rb +++ b/spec/bundler/bundler/source/rubygems/remote_spec.rb @@ -11,8 +11,8 @@ RSpec.describe Bundler::Source::Rubygems::Remote do allow(Digest(:MD5)).to receive(:hexdigest).with(duck_type(:to_s)) {|string| "MD5HEX(#{string})" } end - let(:uri_no_auth) { Bundler::URI("https://gems.example.com") } - let(:uri_with_auth) { Bundler::URI("https://#{credentials}@gems.example.com") } + let(:uri_no_auth) { Gem::URI("https://gems.example.com") } + let(:uri_with_auth) { Gem::URI("https://#{credentials}@gems.example.com") } let(:credentials) { "username:password" } context "when the original URI has no credentials" do @@ -89,11 +89,11 @@ RSpec.describe Bundler::Source::Rubygems::Remote do end context "when the original URI has only a username" do - let(:uri) { Bundler::URI("https://SeCrEt-ToKeN@gem.fury.io/me/") } + let(:uri) { Gem::URI("https://SeCrEt-ToKeN@gem.fury.io/me/") } describe "#anonymized_uri" do it "returns the URI without username and password" do - expect(remote(uri).anonymized_uri).to eq(Bundler::URI("https://gem.fury.io/me/")) + expect(remote(uri).anonymized_uri).to eq(Gem::URI("https://gem.fury.io/me/")) end end @@ -105,9 +105,9 @@ RSpec.describe Bundler::Source::Rubygems::Remote do end context "when a mirror with inline credentials is configured for the URI" do - let(:uri) { Bundler::URI("https://rubygems.org/") } - let(:mirror_uri_with_auth) { Bundler::URI("https://username:password@rubygems-mirror.org/") } - let(:mirror_uri_no_auth) { Bundler::URI("https://rubygems-mirror.org/") } + let(:uri) { Gem::URI("https://rubygems.org/") } + let(:mirror_uri_with_auth) { Gem::URI("https://username:password@rubygems-mirror.org/") } + let(:mirror_uri_no_auth) { Gem::URI("https://rubygems-mirror.org/") } before { Bundler.settings.temporary("mirror.https://rubygems.org/" => mirror_uri_with_auth.to_s) } @@ -131,9 +131,9 @@ RSpec.describe Bundler::Source::Rubygems::Remote do end context "when a mirror with configured credentials is configured for the URI" do - let(:uri) { Bundler::URI("https://rubygems.org/") } - let(:mirror_uri_with_auth) { Bundler::URI("https://#{credentials}@rubygems-mirror.org/") } - let(:mirror_uri_no_auth) { Bundler::URI("https://rubygems-mirror.org/") } + let(:uri) { Gem::URI("https://rubygems.org/") } + let(:mirror_uri_with_auth) { Gem::URI("https://#{credentials}@rubygems-mirror.org/") } + let(:mirror_uri_no_auth) { Gem::URI("https://rubygems-mirror.org/") } before do Bundler.settings.temporary("mirror.https://rubygems.org/" => mirror_uri_no_auth.to_s) diff --git a/spec/bundler/bundler/source_list_spec.rb b/spec/bundler/bundler/source_list_spec.rb index f860e9ff58..13453cb2a3 100644 --- a/spec/bundler/bundler/source_list_spec.rb +++ b/spec/bundler/bundler/source_list_spec.rb @@ -85,7 +85,7 @@ RSpec.describe Bundler::SourceList do end it "ignores git protocols on request" do - Bundler.settings.temporary(:"git.allow_insecure" => true) + Bundler.settings.temporary("git.allow_insecure": true) expect(Bundler.ui).to_not receive(:warn).with(msg) source_list.add_git_source("uri" => "git://existing-git.org/path.git") end @@ -125,8 +125,8 @@ RSpec.describe Bundler::SourceList do it "adds the provided remote to the beginning of the aggregate source" do source_list.add_global_rubygems_remote("https://othersource.org") expect(returned_source.remotes).to eq [ - Bundler::URI("https://othersource.org/"), - Bundler::URI("https://rubygems.org/"), + Gem::URI("https://othersource.org/"), + Gem::URI("https://rubygems.org/"), ] end end diff --git a/spec/bundler/bundler/source_spec.rb b/spec/bundler/bundler/source_spec.rb index af370bb45c..3b49c37431 100644 --- a/spec/bundler/bundler/source_spec.rb +++ b/spec/bundler/bundler/source_spec.rb @@ -21,7 +21,7 @@ RSpec.describe Bundler::Source do end describe "#version_message" do - let(:spec) { double(:spec, :name => "nokogiri", :version => ">= 1.6", :platform => rb) } + let(:spec) { double(:spec, name: "nokogiri", version: ">= 1.6", platform: rb) } shared_examples_for "the lockfile specs are not relevant" do it "should return a string with the spec name and version" do @@ -30,31 +30,21 @@ RSpec.describe Bundler::Source do end context "when there are locked gems" do - let(:locked_gems) { double(:locked_gems) } - - before { allow(Bundler).to receive(:locked_gems).and_return(locked_gems) } - context "that contain the relevant gem spec" do - before do - specs = double(:specs) - allow(locked_gems).to receive(:specs).and_return(specs) - allow(specs).to receive(:find).and_return(locked_gem) - end - context "without a version" do - let(:locked_gem) { double(:locked_gem, :name => "nokogiri", :version => nil) } + let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: nil) } it_behaves_like "the lockfile specs are not relevant" end context "with the same version" do - let(:locked_gem) { double(:locked_gem, :name => "nokogiri", :version => ">= 1.6") } + let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: ">= 1.6") } it_behaves_like "the lockfile specs are not relevant" end context "with a different version" do - let(:locked_gem) { double(:locked_gem, :name => "nokogiri", :version => "< 1.5") } + let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: "< 1.5") } context "with color", :no_color_tty do before do @@ -62,7 +52,7 @@ RSpec.describe Bundler::Source do end it "should return a string with the spec name and version and locked spec version" do - expect(subject.version_message(spec)).to eq("nokogiri >= 1.6\e[32m (was < 1.5)\e[0m") + expect(subject.version_message(spec, locked_gem)).to eq("nokogiri >= 1.6\e[32m (was < 1.5)\e[0m") end end @@ -74,14 +64,14 @@ RSpec.describe Bundler::Source do end it "should return a string with the spec name and version and locked spec version" do - expect(subject.version_message(spec)).to eq("nokogiri >= 1.6 (was < 1.5)") + expect(subject.version_message(spec, locked_gem)).to eq("nokogiri >= 1.6 (was < 1.5)") end end end context "with a more recent version" do - let(:spec) { double(:spec, :name => "nokogiri", :version => "1.6.1", :platform => rb) } - let(:locked_gem) { double(:locked_gem, :name => "nokogiri", :version => "1.7.0") } + let(:spec) { double(:spec, name: "nokogiri", version: "1.6.1", platform: rb) } + let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: "1.7.0") } context "with color", :no_color_tty do before do @@ -89,7 +79,7 @@ RSpec.describe Bundler::Source do end it "should return a string with the locked spec version in yellow" do - expect(subject.version_message(spec)).to eq("nokogiri 1.6.1\e[33m (was 1.7.0)\e[0m") + expect(subject.version_message(spec, locked_gem)).to eq("nokogiri 1.6.1\e[33m (was 1.7.0)\e[0m") end end @@ -101,14 +91,14 @@ RSpec.describe Bundler::Source do end it "should return a string with the locked spec version in yellow" do - expect(subject.version_message(spec)).to eq("nokogiri 1.6.1 (was 1.7.0)") + expect(subject.version_message(spec, locked_gem)).to eq("nokogiri 1.6.1 (was 1.7.0)") end end end context "with an older version" do - let(:spec) { double(:spec, :name => "nokogiri", :version => "1.7.1", :platform => rb) } - let(:locked_gem) { double(:locked_gem, :name => "nokogiri", :version => "1.7.0") } + let(:spec) { double(:spec, name: "nokogiri", version: "1.7.1", platform: rb) } + let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: "1.7.0") } context "with color", :no_color_tty do before do @@ -116,7 +106,7 @@ RSpec.describe Bundler::Source do end it "should return a string with the locked spec version in green" do - expect(subject.version_message(spec)).to eq("nokogiri 1.7.1\e[32m (was 1.7.0)\e[0m") + expect(subject.version_message(spec, locked_gem)).to eq("nokogiri 1.7.1\e[32m (was 1.7.0)\e[0m") end end @@ -128,33 +118,17 @@ RSpec.describe Bundler::Source do end it "should return a string with the locked spec version in yellow" do - expect(subject.version_message(spec)).to eq("nokogiri 1.7.1 (was 1.7.0)") + expect(subject.version_message(spec, locked_gem)).to eq("nokogiri 1.7.1 (was 1.7.0)") end end end end - - context "that do not contain the relevant gem spec" do - before do - specs = double(:specs) - allow(locked_gems).to receive(:specs).and_return(specs) - allow(specs).to receive(:find).and_return(nil) - end - - it_behaves_like "the lockfile specs are not relevant" - end - end - - context "when there are no locked gems" do - before { allow(Bundler).to receive(:locked_gems).and_return(nil) } - - it_behaves_like "the lockfile specs are not relevant" end end describe "#can_lock?" do context "when the passed spec's source is equivalent" do - let(:spec) { double(:spec, :source => subject) } + let(:spec) { double(:spec, source: subject) } it "should return true" do expect(subject.can_lock?(spec)).to be_truthy @@ -162,7 +136,7 @@ RSpec.describe Bundler::Source do end context "when the passed spec's source is not equivalent" do - let(:spec) { double(:spec, :source => double(:other_source)) } + let(:spec) { double(:spec, source: double(:other_source)) } it "should return false" do expect(subject.can_lock?(spec)).to be_falsey diff --git a/spec/bundler/bundler/spec_set_spec.rb b/spec/bundler/bundler/spec_set_spec.rb index 6fedd38b50..c4b6676223 100644 --- a/spec/bundler/bundler/spec_set_spec.rb +++ b/spec/bundler/bundler/spec_set_spec.rb @@ -45,24 +45,6 @@ RSpec.describe Bundler::SpecSet do end end - describe "#merge" do - let(:other_specs) do - [ - build_spec("f", "1.0"), - build_spec("g", "2.0"), - ].flatten - end - - let(:other_spec_set) { described_class.new(other_specs) } - - it "merges the items in each gemspec" do - new_spec_set = subject.merge(other_spec_set) - specs = new_spec_set.to_a.map(&:full_name) - expect(specs).to include("a-1.0") - expect(specs).to include("f-1.0") - end - end - describe "#to_a" do it "returns the specs in order" do expect(subject.to_a.map(&:full_name)).to eq %w[ diff --git a/spec/bundler/bundler/specifications/foo.gemspec b/spec/bundler/bundler/specifications/foo.gemspec new file mode 100644 index 0000000000..46eb068cd1 --- /dev/null +++ b/spec/bundler/bundler/specifications/foo.gemspec @@ -0,0 +1,13 @@ +# rubocop:disable Style/FrozenStringLiteralComment +# stub: foo 1.0.0 ruby lib + +# The first line would be '# -*- encoding: utf-8 -*-' in a real stub gemspec + +Gem::Specification.new do |s| + s.name = "foo" + s.version = "1.0.0" + s.loaded_from = __FILE__ + s.extensions = "ext/foo" + s.required_ruby_version = ">= 3.0.0" +end +# rubocop:enable Style/FrozenStringLiteralComment diff --git a/spec/bundler/bundler/stub_specification_spec.rb b/spec/bundler/bundler/stub_specification_spec.rb index fb612813c2..dae9f3cfba 100644 --- a/spec/bundler/bundler/stub_specification_spec.rb +++ b/spec/bundler/bundler/stub_specification_spec.rb @@ -19,6 +19,17 @@ RSpec.describe Bundler::StubSpecification do end end + describe "#gem_build_complete_path" do + it "StubSpecification should have equal gem_build_complete_path as Specification" do + spec_path = File.join(File.dirname(__FILE__), "specifications", "foo.gemspec") + spec = Gem::Specification.load(spec_path) + gem_stub = Gem::StubSpecification.new(spec_path, File.dirname(__FILE__),"","") + + stub = described_class.from_stub(gem_stub) + expect(stub.gem_build_complete_path).to eq spec.gem_build_complete_path + end + end + describe "#manually_installed?" do it "returns true if installed_by_version is nil or 0" do stub = described_class.from_stub(with_bundler_stub_spec) diff --git a/spec/bundler/bundler/uri_credentials_filter_spec.rb b/spec/bundler/bundler/uri_credentials_filter_spec.rb index 466c1b8594..ed24744a1c 100644 --- a/spec/bundler/bundler/uri_credentials_filter_spec.rb +++ b/spec/bundler/bundler/uri_credentials_filter_spec.rb @@ -16,7 +16,7 @@ RSpec.describe Bundler::URICredentialsFilter do let(:credentials) { "oauth_token:x-oauth-basic@" } it "returns the uri without the oauth token" do - expect(subject.credential_filtered_uri(uri).to_s).to eq(Bundler::URI("https://x-oauth-basic@github.com/company/private-repo").to_s) + expect(subject.credential_filtered_uri(uri).to_s).to eq(Gem::URI("https://x-oauth-basic@github.com/company/private-repo").to_s) end it_behaves_like "original type of uri is maintained" @@ -26,7 +26,7 @@ RSpec.describe Bundler::URICredentialsFilter do let(:credentials) { "oauth_token:x@" } it "returns the uri without the oauth token" do - expect(subject.credential_filtered_uri(uri).to_s).to eq(Bundler::URI("https://x@github.com/company/private-repo").to_s) + expect(subject.credential_filtered_uri(uri).to_s).to eq(Gem::URI("https://x@github.com/company/private-repo").to_s) end it_behaves_like "original type of uri is maintained" @@ -37,7 +37,7 @@ RSpec.describe Bundler::URICredentialsFilter do let(:credentials) { "username1:hunter3@" } it "returns the uri without the password" do - expect(subject.credential_filtered_uri(uri).to_s).to eq(Bundler::URI("https://username1@github.com/company/private-repo").to_s) + expect(subject.credential_filtered_uri(uri).to_s).to eq(Gem::URI("https://username1@github.com/company/private-repo").to_s) end it_behaves_like "original type of uri is maintained" @@ -55,7 +55,7 @@ RSpec.describe Bundler::URICredentialsFilter do end context "uri is a uri object" do - let(:uri) { Bundler::URI("https://#{credentials}github.com/company/private-repo") } + let(:uri) { Gem::URI("https://#{credentials}github.com/company/private-repo") } it_behaves_like "sensitive credentials in uri are filtered out" end @@ -90,7 +90,7 @@ RSpec.describe Bundler::URICredentialsFilter do describe "#credential_filtered_string" do let(:str_to_filter) { "This is a git message containing a uri #{uri}!" } let(:credentials) { "" } - let(:uri) { Bundler::URI("https://#{credentials}github.com/company/private-repo") } + let(:uri) { Gem::URI("https://#{credentials}github.com/company/private-repo") } context "with a uri that contains credentials" do let(:credentials) { "oauth_token:x-oauth-basic@" } diff --git a/spec/bundler/bundler/vendored_persistent_spec.rb b/spec/bundler/bundler/vendored_persistent_spec.rb deleted file mode 100644 index 3ed899dbcf..0000000000 --- a/spec/bundler/bundler/vendored_persistent_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -require "bundler/vendored_persistent" - -RSpec.describe Bundler::PersistentHTTP do - describe "#warn_old_tls_version_rubygems_connection" do - let(:uri) { "https://index.rubygems.org" } - let(:connection) { instance_double(Bundler::Persistent::Net::HTTP::Persistent::Connection) } - let(:tls_version) { "TLSv1.2" } - let(:socket) { double("Socket") } - let(:socket_io) { double("SocketIO") } - - before do - allow(connection).to receive_message_chain(:http, :use_ssl?).and_return(!tls_version.nil?) - allow(socket).to receive(:io).and_return(socket_io) if socket - connection.instance_variable_set(:@socket, socket) - - if tls_version - allow(socket_io).to receive(:ssl_version).and_return(tls_version) - end - end - - shared_examples_for "does not warn" do - it "does not warn" do - allow(Bundler.ui).to receive(:warn).never - subject.warn_old_tls_version_rubygems_connection(Bundler::URI(uri), connection) - end - end - - shared_examples_for "does warn" do |*expected| - it "warns" do - expect(Bundler.ui).to receive(:warn).with(*expected) - subject.warn_old_tls_version_rubygems_connection(Bundler::URI(uri), connection) - end - end - - context "an HTTPS uri with TLSv1.2" do - include_examples "does not warn" - end - - context "without SSL" do - let(:tls_version) { nil } - - include_examples "does not warn" - end - - context "without a socket" do - let(:socket) { nil } - - include_examples "does not warn" - end - - context "with a different TLD" do - let(:uri) { "https://foo.bar" } - include_examples "does not warn" - - context "and an outdated TLS version" do - let(:tls_version) { "TLSv1" } - include_examples "does not warn" - end - end - - context "with a nonsense TLS version" do - let(:tls_version) { "BlahBlah2.0Blah" } - include_examples "does not warn" - end - - context "with an outdated TLS version" do - let(:tls_version) { "TLSv1" } - include_examples "does warn", - "Warning: Your Ruby version is compiled against a copy of OpenSSL that is very old. " \ - "Starting in January 2018, RubyGems.org will refuse connection requests from these very old versions of OpenSSL. " \ - "If you will need to continue installing gems after January 2018, please follow this guide to upgrade: http://ruby.to/tls-outdated.", - :wrap => true - end - end -end diff --git a/spec/bundler/bundler/version_ranges_spec.rb b/spec/bundler/bundler/version_ranges_spec.rb deleted file mode 100644 index bca044b0c0..0000000000 --- a/spec/bundler/bundler/version_ranges_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require "bundler/version_ranges" - -RSpec.describe Bundler::VersionRanges do - describe ".empty?" do - shared_examples_for "empty?" do |exp, *req| - it "returns #{exp} for #{req}" do - r = Gem::Requirement.new(*req) - ranges = described_class.for(r) - expect(described_class.empty?(*ranges)).to eq(exp), "expected `#{r}` #{exp ? "" : "not "}to be empty" - end - end - - include_examples "empty?", false - include_examples "empty?", false, "!= 1" - include_examples "empty?", false, "!= 1", "= 2" - include_examples "empty?", false, "!= 1", "> 1" - include_examples "empty?", false, "!= 1", ">= 1" - include_examples "empty?", false, "= 1", ">= 0.1", "<= 1.1" - include_examples "empty?", false, "= 1", ">= 1", "<= 1" - include_examples "empty?", false, "= 1", "~> 1" - include_examples "empty?", false, ">= 0.z", "= 0" - include_examples "empty?", false, ">= 0" - include_examples "empty?", false, ">= 1.0.0", "< 2.0.0" - include_examples "empty?", false, "~> 1" - include_examples "empty?", false, "~> 2.0", "~> 2.1" - include_examples "empty?", true, ">= 4.1.0", "< 5.0", "= 5.2.1" - include_examples "empty?", true, "< 5.0", "< 5.3", "< 6.0", "< 6", "= 5.2.0", "> 2", ">= 3.0", ">= 3.1", ">= 3.2", ">= 4.0.0", ">= 4.1.0", ">= 4.2.0", ">= 4.2", ">= 4" - include_examples "empty?", true, "!= 1", "< 2", "> 2" - include_examples "empty?", true, "!= 1", "<= 1", ">= 1" - include_examples "empty?", true, "< 2", "> 2" - include_examples "empty?", true, "< 2", "> 2", "= 2" - include_examples "empty?", true, "= 1", "!= 1" - include_examples "empty?", true, "= 1", "= 2" - include_examples "empty?", true, "= 1", "~> 2" - include_examples "empty?", true, ">= 0", "<= 0.a" - include_examples "empty?", true, "~> 2.0", "~> 3" - end -end diff --git a/spec/bundler/bundler/yaml_serializer_spec.rb b/spec/bundler/bundler/yaml_serializer_spec.rb index 1241c74bbf..de437f764a 100644 --- a/spec/bundler/bundler/yaml_serializer_spec.rb +++ b/spec/bundler/bundler/yaml_serializer_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Bundler::YAMLSerializer do it "works for simple hash" do hash = { "Q" => "Where does Thursday come before Wednesday? In the dictionary. :P" } - expected = strip_whitespace <<-YAML + expected = <<~YAML --- Q: "Where does Thursday come before Wednesday? In the dictionary. :P" YAML @@ -24,7 +24,7 @@ RSpec.describe Bundler::YAMLSerializer do }, } - expected = strip_whitespace <<-YAML + expected = <<~YAML --- nice-one: read_ahead: "All generalizations are false, including this one" @@ -45,7 +45,7 @@ RSpec.describe Bundler::YAMLSerializer do }, } - expected = strip_whitespace <<-YAML + expected = <<~YAML --- nested_hash: contains_array: @@ -57,11 +57,24 @@ RSpec.describe Bundler::YAMLSerializer do expect(serializer.dump(hash)).to eq(expected) end + + it "handles empty array" do + hash = { + "empty_array" => [], + } + + expected = <<~YAML + --- + empty_array: [] + YAML + + expect(serializer.dump(hash)).to eq(expected) + end end describe "#load" do it "works for simple hash" do - yaml = strip_whitespace <<-YAML + yaml = <<~YAML --- Jon: "Air is free dude!" Jack: "Yes.. until you buy a bag of chips!" @@ -76,7 +89,7 @@ RSpec.describe Bundler::YAMLSerializer do end it "works for nested hash" do - yaml = strip_whitespace <<-YAML + yaml = <<~YAML --- baa: baa: "black sheep" @@ -98,7 +111,7 @@ RSpec.describe Bundler::YAMLSerializer do end it "handles colon in key/value" do - yaml = strip_whitespace <<-YAML + yaml = <<~YAML BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/: http://rubygems-mirror.org YAML @@ -106,7 +119,7 @@ RSpec.describe Bundler::YAMLSerializer do end it "handles arrays inside hashes" do - yaml = strip_whitespace <<-YAML + yaml = <<~YAML --- nested_hash: contains_array: @@ -127,7 +140,7 @@ RSpec.describe Bundler::YAMLSerializer do end it "handles windows-style CRLF line endings" do - yaml = strip_whitespace(<<-YAML).gsub("\n", "\r\n") + yaml = <<~YAML.gsub("\n", "\r\n") --- nested_hash: contains_array: @@ -148,6 +161,34 @@ RSpec.describe Bundler::YAMLSerializer do expect(serializer.load(yaml)).to eq(hash) end + + it "handles empty array" do + yaml = <<~YAML + --- + empty_array: [] + YAML + + hash = { + "empty_array" => [], + } + + expect(serializer.load(yaml)).to eq(hash) + end + + it "skip commented out words" do + yaml = <<~YAML + --- + foo: bar + buzz: foo # bar + YAML + + hash = { + "foo" => "bar", + "buzz" => "foo", + } + + expect(serializer.load(yaml)).to eq(hash) + end end describe "against yaml lib" do |