diff options
author | hsbt <hsbt@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2018-11-02 23:07:56 +0000 |
---|---|---|
committer | hsbt <hsbt@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2018-11-02 23:07:56 +0000 |
commit | 59c8d50653480bef3f24517296e6ddf937fdf6bc (patch) | |
tree | df10aaf4f3307837fe3d1d129d66f6c0c7586bc5 /spec/bundler/bundler | |
parent | 7deb37777a230837e865e0a11fb8d7c1dc6d03ce (diff) |
Added bundler as default gems. Revisit [Feature #12733]
* bin/*, lib/bundler/*, lib/bundler.rb, spec/bundler, man/*:
Merge from latest stable branch of bundler/bundler repository and
added workaround patches. I will backport them into upstream.
* common.mk, defs/gmake.mk: Added `test-bundler` task for test suite
of bundler.
* tool/sync_default_gems.rb: Added sync task for bundler.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@65509 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'spec/bundler/bundler')
58 files changed, 8740 insertions, 0 deletions
diff --git a/spec/bundler/bundler/bundler_spec.rb b/spec/bundler/bundler/bundler_spec.rb new file mode 100644 index 0000000000..194d6752b2 --- /dev/null +++ b/spec/bundler/bundler/bundler_spec.rb @@ -0,0 +1,490 @@ +# encoding: utf-8 +# frozen_string_literal: true + +require "bundler" +require "tmpdir" + +RSpec.describe Bundler do + describe "#load_gemspec_uncached" do + let(:app_gemspec_path) { tmp("test.gemspec") } + subject { Bundler.load_gemspec_uncached(app_gemspec_path) } + + context "with incorrect YAML file" do + before do + File.open(app_gemspec_path, "wb") do |f| + f.write strip_whitespace(<<-GEMSPEC) + --- + {:!00 ao=gu\g1= 7~f + GEMSPEC + end + end + + it "catches YAML syntax errors" do + expect { subject }.to raise_error(Bundler::GemspecError, /error while loading `test.gemspec`/) + end + + context "on Rubies with a settable YAML engine", :if => defined?(YAML::ENGINE) do + context "with Syck as YAML::Engine" do + it "raises a GemspecError after YAML load throws ArgumentError" do + orig_yamler = YAML::ENGINE.yamler + YAML::ENGINE.yamler = "syck" + + expect { subject }.to raise_error(Bundler::GemspecError) + + YAML::ENGINE.yamler = orig_yamler + end + end + + context "with Psych as YAML::Engine" do + it "raises a GemspecError after YAML load throws Psych::SyntaxError" do + orig_yamler = YAML::ENGINE.yamler + YAML::ENGINE.yamler = "psych" + + expect { subject }.to raise_error(Bundler::GemspecError) + + YAML::ENGINE.yamler = orig_yamler + end + end + end + end + + 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 + verbose = $VERBOSE + $VERBOSE = false + encoding = Encoding.default_external + Encoding.default_external = "ASCII" + $VERBOSE = verbose + + File.open(app_gemspec_path, "wb") do |file| + file.puts <<-GEMSPEC.gsub(/^\s+/, "") + # -*- encoding: utf-8 -*- + Gem::Specification.new do |gem| + gem.author = "André the Giant" + end + GEMSPEC + end + + expect(subject.author).to eq("André the Giant") + + verbose = $VERBOSE + $VERBOSE = false + Encoding.default_external = encoding + $VERBOSE = verbose + end + end + + it "sets loaded_from" do + app_gemspec_path.open("w") do |f| + f.puts <<-GEMSPEC + Gem::Specification.new do |gem| + gem.name = "validated" + end + GEMSPEC + end + + expect(subject.loaded_from).to eq(app_gemspec_path.expand_path.to_s) + end + + context "validate is true" do + subject { Bundler.load_gemspec_uncached(app_gemspec_path, true) } + + it "validates the specification" do + app_gemspec_path.open("w") do |f| + f.puts <<-GEMSPEC + Gem::Specification.new do |gem| + gem.name = "validated" + end + GEMSPEC + end + expect(Bundler.rubygems).to receive(:validate).with have_attributes(:name => "validated") + subject + end + end + + context "with gemspec containing local variables" do + before do + File.open(app_gemspec_path, "wb") do |f| + f.write strip_whitespace(<<-GEMSPEC) + must_not_leak = true + Gem::Specification.new do |gem| + gem.name = "leak check" + end + GEMSPEC + end + end + + it "should not pollute the TOPLEVEL_BINDING" do + subject + expect(TOPLEVEL_BINDING.eval("local_variables")).to_not include(:must_not_leak) + end + end + end + + describe "#which" do + let(:executable) { "executable" } + let(:path) { %w[/a /b c ../d /e] } + let(:expected) { "executable" } + + before do + ENV["PATH"] = path.join(File::PATH_SEPARATOR) + + allow(File).to receive(:file?).and_return(false) + allow(File).to receive(:executable?).and_return(false) + if expected + expect(File).to receive(:file?).with(expected).and_return(true) + expect(File).to receive(:executable?).with(expected).and_return(true) + end + end + + subject { described_class.which(executable) } + + shared_examples_for "it returns the correct executable" do + it "returns the expected file" do + expect(subject).to eq(expected) + end + end + + it_behaves_like "it returns the correct executable" + + context "when the executable in inside a quoted path" do + let(:expected) { "/e/executable" } + it_behaves_like "it returns the correct executable" + end + + context "when the executable is not found" do + let(:expected) { nil } + it_behaves_like "it returns the correct executable" + end + end + + describe "configuration" do + context "disable_shared_gems" do + it "should unset GEM_PATH with empty string" do + env = {} + expect(Bundler).to receive(:use_system_gems?).and_return(false) + Bundler.send(:configure_gem_path, env) + expect(env.keys).to include("GEM_PATH") + expect(env["GEM_PATH"]).to eq "" + end + 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 http://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 + source "file://#{gem_repo1}" + gem "rack" + G + + 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 + context "home directory is set" do + it "should return the user home" do + path = "/home/oggy" + allow(Bundler.rubygems).to receive(:user_home).and_return(path) + allow(File).to receive(:directory?).with(path).and_return true + allow(File).to receive(:writable?).with(path).and_return true + expect(Bundler.user_home).to eq(Pathname(path)) + end + + context "is not a directory" do + it "should issue a warning and return a temporary user home" do + path = "/home/oggy" + allow(Bundler.rubygems).to receive(:user_home).and_return(path) + allow(File).to receive(:directory?).with(path).and_return false + allow(Etc).to receive(:getlogin).and_return("USER") + allow(Dir).to receive(:tmpdir).and_return("/TMP") + allow(FileTest).to receive(:exist?).with("/TMP/bundler/home").and_return(true) + expect(FileUtils).to receive(:mkpath).with("/TMP/bundler/home/USER") + message = <<EOF +`/home/oggy` is not a directory. +Bundler will use `/TMP/bundler/home/USER' as your home directory temporarily. +EOF + expect(Bundler.ui).to receive(:warn).with(message) + expect(Bundler.user_home).to eq(Pathname("/TMP/bundler/home/USER")) + end + end + + context "is not writable" do + let(:path) { "/home/oggy" } + let(:dotbundle) { "/home/oggy/.bundle" } + + it "should issue a warning and return a temporary user home" do + allow(Bundler.rubygems).to receive(:user_home).and_return(path) + allow(File).to receive(:directory?).with(path).and_return true + allow(File).to receive(:writable?).with(path).and_return false + allow(File).to receive(:directory?).with(dotbundle).and_return false + allow(Etc).to receive(:getlogin).and_return("USER") + allow(Dir).to receive(:tmpdir).and_return("/TMP") + allow(FileTest).to receive(:exist?).with("/TMP/bundler/home").and_return(true) + expect(FileUtils).to receive(:mkpath).with("/TMP/bundler/home/USER") + message = <<EOF +`/home/oggy` is not writable. +Bundler will use `/TMP/bundler/home/USER' as your home directory temporarily. +EOF + expect(Bundler.ui).to receive(:warn).with(message) + expect(Bundler.user_home).to eq(Pathname("/TMP/bundler/home/USER")) + end + + context ".bundle exists and have correct permissions" do + it "should return the user home" do + allow(Bundler.rubygems).to receive(:user_home).and_return(path) + allow(File).to receive(:directory?).with(path).and_return true + allow(File).to receive(:writable?).with(path).and_return false + allow(File).to receive(:directory?).with(dotbundle).and_return true + allow(File).to receive(:writable?).with(dotbundle).and_return true + expect(Bundler.user_home).to eq(Pathname(path)) + end + end + end + end + + context "home directory is not set" do + it "should issue warning and return a temporary user home" do + allow(Bundler.rubygems).to receive(:user_home).and_return(nil) + allow(Etc).to receive(:getlogin).and_return("USER") + allow(Dir).to receive(:tmpdir).and_return("/TMP") + allow(FileTest).to receive(:exist?).with("/TMP/bundler/home").and_return(true) + expect(FileUtils).to receive(:mkpath).with("/TMP/bundler/home/USER") + message = <<EOF +Your home directory is not set. +Bundler will use `/TMP/bundler/home/USER' as your home directory temporarily. +EOF + expect(Bundler.ui).to receive(:warn).with(message) + expect(Bundler.user_home).to eq(Pathname("/TMP/bundler/home/USER")) + end + end + end + + describe "#tmp_home_path" do + it "should create temporary user home" do + allow(Dir).to receive(:tmpdir).and_return("/TMP") + allow(FileTest).to receive(:exist?).with("/TMP/bundler/home").and_return(false) + expect(FileUtils).to receive(:mkpath).once.ordered.with("/TMP/bundler/home") + expect(FileUtils).to receive(:mkpath).once.ordered.with("/TMP/bundler/home/USER") + expect(File).to receive(:chmod).with(0o777, "/TMP/bundler/home") + expect(Bundler.tmp_home_path("USER", "")).to eq(Pathname("/TMP/bundler/home/USER")) + end + end + + describe "#requires_sudo?" do + let!(:tmpdir) { Dir.mktmpdir } + let(:bundle_path) { Pathname("#{tmpdir}/bundle") } + + def clear_cached_requires_sudo + # Private in ruby 1.8.7 + return unless Bundler.instance_variable_defined?(:@requires_sudo_ran) + Bundler.send(:remove_instance_variable, :@requires_sudo_ran) + Bundler.send(: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/bundler/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 + end + + describe "#requires_sudo?" do + before do + allow(Bundler).to receive(:which).with("sudo").and_return("/usr/bin/sudo") + 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") + if Bundler.respond_to?(:remove_instance_variable) + Bundler.remove_instance_variable(:@requires_sudo_ran) + Bundler.remove_instance_variable(:@requires_sudo) + else + # TODO: Remove these code when Bundler drops Ruby 1.8.7 support + Bundler.send(:remove_instance_variable, :@requires_sudo_ran) + Bundler.send(:remove_instance_variable, :@requires_sudo) + end + 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 + + context "user cache dir" do + let(:home_path) { Pathname.new(ENV["HOME"]) } + + let(:xdg_data_home) { home_path.join(".local") } + let(:xdg_cache_home) { home_path.join(".cache") } + let(:xdg_config_home) { home_path.join(".config") } + + let(:bundle_user_home_default) { home_path.join(".bundle") } + let(:bundle_user_home_custom) { xdg_data_home.join("bundle") } + + let(:bundle_user_cache_default) { bundle_user_home_default.join("cache") } + let(:bundle_user_cache_custom) { xdg_cache_home.join("bundle") } + + let(:bundle_user_config_default) { bundle_user_home_default.join("config") } + let(:bundle_user_config_custom) { xdg_config_home.join("bundle") } + + let(:bundle_user_plugin_default) { bundle_user_home_default.join("plugin") } + let(:bundle_user_plugin_custom) { xdg_data_home.join("bundle").join("plugin") } + + describe "#user_bundle_path" do + before do + allow(Bundler.rubygems).to receive(:user_home).and_return(home_path) + end + + it "should use the default home path" do + expect(Bundler.user_bundle_path).to eq(bundle_user_home_default) + expect(Bundler.user_bundle_path("home")).to eq(bundle_user_home_default) + expect(Bundler.user_bundle_path("cache")).to eq(bundle_user_cache_default) + expect(Bundler.user_cache).to eq(bundle_user_cache_default) + expect(Bundler.user_bundle_path("config")).to eq(bundle_user_config_default) + expect(Bundler.user_bundle_path("plugin")).to eq(bundle_user_plugin_default) + end + + it "should use custom home path as root for other paths" do + ENV["BUNDLE_USER_HOME"] = bundle_user_home_custom.to_s + expect(Bundler.user_bundle_path).to eq(bundle_user_home_custom) + expect(Bundler.user_bundle_path("home")).to eq(bundle_user_home_custom) + expect(Bundler.user_bundle_path("cache")).to eq(bundle_user_home_custom.join("cache")) + expect(Bundler.user_cache).to eq(bundle_user_home_custom.join("cache")) + expect(Bundler.user_bundle_path("config")).to eq(bundle_user_home_custom.join("config")) + expect(Bundler.user_bundle_path("plugin")).to eq(bundle_user_home_custom.join("plugin")) + end + + it "should use all custom paths, except home" do + ENV.delete("BUNDLE_USER_HOME") + ENV["BUNDLE_USER_CACHE"] = bundle_user_cache_custom.to_s + ENV["BUNDLE_USER_CONFIG"] = bundle_user_config_custom.to_s + ENV["BUNDLE_USER_PLUGIN"] = bundle_user_plugin_custom.to_s + expect(Bundler.user_bundle_path).to eq(bundle_user_home_default) + expect(Bundler.user_bundle_path("home")).to eq(bundle_user_home_default) + expect(Bundler.user_bundle_path("cache")).to eq(bundle_user_cache_custom) + expect(Bundler.user_cache).to eq(bundle_user_cache_custom) + expect(Bundler.user_bundle_path("config")).to eq(bundle_user_config_custom) + expect(Bundler.user_bundle_path("plugin")).to eq(bundle_user_plugin_custom) + end + end + end +end diff --git a/spec/bundler/bundler/cli_spec.rb b/spec/bundler/bundler/cli_spec.rb new file mode 100644 index 0000000000..c82d46587e --- /dev/null +++ b/spec/bundler/bundler/cli_spec.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +require "bundler/cli" + +RSpec.describe "bundle executable" do + it "returns non-zero exit status when passed unrecognized options" do + bundle "--invalid_argument" + expect(exitstatus).to_not be_zero if exitstatus + end + + it "returns non-zero exit status when passed unrecognized task" do + bundle "unrecognized-task" + expect(exitstatus).to_not be_zero if exitstatus + end + + it "looks for a binary and executes it if it's named bundler-<task>" do + File.open(tmp("bundler-testtasks"), "w", 0o755) do |f| + ruby = ENV["BUNDLE_RUBY"] || "/usr/bin/env ruby" + f.puts "#!#{ruby}\nputs 'Hello, world'\n" + end + + with_path_added(tmp) do + bundle "testtasks" + end + + expect(exitstatus).to be_zero if exitstatus + expect(out).to eq("Hello, world") + end + + context "with no arguments" do + it "prints a concise help message", :bundler => "2" do + bundle! "" + expect(last_command.stderr).to be_empty + expect(last_command.stdout).to include("Bundler version #{Bundler::VERSION}"). + and include("\n\nBundler commands:\n\n"). + and include("\n\n Primary commands:\n"). + and include("\n\n Utilities:\n"). + and include("\n\nOptions:\n") + end + end + + context "when ENV['BUNDLE_GEMFILE'] is set to an empty string" do + it "ignores it" do + gemfile bundled_app("Gemfile"), <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + bundle :install, :env => { "BUNDLE_GEMFILE" => "" } + + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + context "when ENV['RUBYGEMS_GEMDEPS'] is set" do + it "displays a warning" do + gemfile bundled_app("Gemfile"), <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + bundle :install, :env => { "RUBYGEMS_GEMDEPS" => "foo" } + expect(out).to include("RUBYGEMS_GEMDEPS") + expect(out).to include("conflict with Bundler") + + bundle :install, :env => { "RUBYGEMS_GEMDEPS" => "" } + expect(out).not_to include("RUBYGEMS_GEMDEPS") + end + end + + context "with --verbose" do + it "prints the running command" do + gemfile "" + bundle! "info bundler", :verbose => true + expect(last_command.stdout).to start_with("Running `bundle info bundler --verbose` with bundler #{Bundler::VERSION}") + end + + it "doesn't print defaults" do + install_gemfile! "", :verbose => true + expect(last_command.stdout).to start_with("Running `bundle install --retry 0 --verbose` with bundler #{Bundler::VERSION}") + end + + it "doesn't print defaults" do + install_gemfile! "", :verbose => true + expect(last_command.stdout).to start_with("Running `bundle install --retry 0 --verbose` with bundler #{Bundler::VERSION}") + end + end + + describe "printing the outdated warning" do + shared_examples_for "no warning" do + it "prints no warning" do + bundle "fail" + expect(last_command.stdboth).to eq("Could not find command \"fail\".") + end + end + + let(:bundler_version) { "1.1" } + let(:latest_version) { nil } + before do + bundle! "config --global disable_version_check false" + + simulate_bundler_version(bundler_version) + if latest_version + info_path = home(".bundle/cache/compact_index/rubygems.org.443.29b0360b937aa4d161703e6160654e47/info/bundler") + info_path.parent.mkpath + info_path.open("w") {|f| f.write "#{latest_version}\n" } + end + end + + context "when there is no latest version" do + include_examples "no warning" + end + + context "when the latest version is equal to the current version" do + let(:latest_version) { bundler_version } + include_examples "no warning" + end + + context "when the latest version is less than the current version" do + let(:latest_version) { "0.9" } + include_examples "no warning" + end + + 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" + expect(last_command.stdout).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` + EOS + end + + context "and disable_version_check is set" do + before { bundle! "config disable_version_check true" } + include_examples "no warning" + end + + context "running a parseable command" do + it "prints no warning" do + bundle! "config --parseable foo" + expect(last_command.stdboth).to eq "" + + bundle "platform --ruby" + expect(last_command.stdboth).to eq "Could not locate Gemfile" + end + end + + context "and is a pre-release" do + let(:latest_version) { "222.0.0.pre.4" } + it "prints the version warning" do + bundle "fail" + expect(last_command.stdout).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` + EOS + end + end + end + end +end + +RSpec.describe "bundler executable" do + it "shows the bundler version just as the `bundle` executable does", :bundler => "< 2" do + bundler "--version" + expect(out).to eq("Bundler version #{Bundler::VERSION}") + end + + it "shows the bundler version just as the `bundle` executable does", :bundler => "2" do + bundler "--version" + expect(out).to eq(Bundler::VERSION) + end +end diff --git a/spec/bundler/bundler/compact_index_client/updater_spec.rb b/spec/bundler/bundler/compact_index_client/updater_spec.rb new file mode 100644 index 0000000000..fd554a7b0d --- /dev/null +++ b/spec/bundler/bundler/compact_index_client/updater_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "net/http" +require "bundler/compact_index_client" +require "bundler/compact_index_client/updater" + +RSpec.describe Bundler::CompactIndexClient::Updater do + let(:fetcher) { double(:fetcher) } + let(:local_path) { Pathname("/tmp/localpath") } + let(:remote_path) { double(:remote_path) } + + subject(:updater) { described_class.new(fetcher) } + + context "when the ETag header is missing" do + # Regression test for https://github.com/bundler/bundler/issues/5463 + + let(:response) { double(:response, :body => "") } + + it "MisMatchedChecksumError is raised" do + # Twice: #update retries on failure + expect(response).to receive(:[]).with("Content-Encoding").twice { "" } + expect(response).to receive(:[]).with("ETag").twice { nil } + expect(fetcher).to receive(:call).twice { response } + + expect do + updater.update(local_path, remote_path) + end.to raise_error(Bundler::CompactIndexClient::Updater::MisMatchedChecksumError) + end + end + + context "when the download is corrupt" do + let(:response) { double(:response, :body => "") } + + it "raises HTTPError" do + expect(response).to receive(:[]).with("Content-Encoding") { "gzip" } + expect(fetcher).to receive(:call) { response } + + expect do + updater.update(local_path, remote_path) + end.to raise_error(Bundler::HTTPError) + end + end + + context "when bundler doesn't have permissions on Dir.tmpdir" do + let(:response) { double(:response, :body => "") } + + it "Errno::EACCES is raised" do + allow(Dir).to receive(:mktmpdir) { raise Errno::EACCES } + + expect do + updater.update(local_path, remote_path) + end.to raise_error(Bundler::PermissionError) + end + end +end diff --git a/spec/bundler/bundler/definition_spec.rb b/spec/bundler/bundler/definition_spec.rb new file mode 100644 index 0000000000..2ed87ec81d --- /dev/null +++ b/spec/bundler/bundler/definition_spec.rb @@ -0,0 +1,358 @@ +# frozen_string_literal: true + +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 => "") } + end + context "when it's not possible to write to the file" do + subject { Bundler::Definition.new(nil, [], Bundler::SourceList.new, []) } + + it "raises an PermissionError with explanation" do + expect(File).to receive(:open).with("Gemfile.lock", "wb"). + and_raise(Errno::EACCES) + expect { subject.lock("Gemfile.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 + expect(File).to receive(:open).with("Gemfile.lock", "wb"). + and_raise(Errno::EAGAIN) + expect { subject.lock("Gemfile.lock") }. + to raise_error(Bundler::TemporaryResourceError, /temporarily unavailable/) + end + end + end + + describe "detects changes" do + it "for a path gem with changes", :bundler => "< 2" do + build_lib "foo", "1.0", :path => lib_path("foo") + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "foo", :path => "#{lib_path("foo")}" + G + + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "rack", "1.0" + end + + bundle :install, :env => { "DEBUG" => 1 } + + expect(out).to match(/re-resolving dependencies/) + lockfile_should_be <<-G + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + rack (= 1.0) + + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "for a path gem with changes", :bundler => "2" do + build_lib "foo", "1.0", :path => lib_path("foo") + + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "foo", :path => "#{lib_path("foo")}" + G + + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "rack", "1.0" + end + + bundle :install, :env => { "DEBUG" => 1 } + + expect(out).to match(/re-resolving dependencies/) + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + rack (= 1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "for a path gem with deps and no changes", :bundler => "< 2" do + 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 + + install_gemfile <<-G + source "file://localhost#{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 + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + rack (= 1.0) + + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "for a path gem with deps and no changes", :bundler => "2" do + 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 + + install_gemfile <<-G + source "file://localhost#{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 + GEM + remote: file://localhost#{gem_repo1}/ + specs: + rack (1.0.0) + + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + rack (= 1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "for a rubygems gem" do + install_gemfile <<-G + source "file://localhost#{gem_repo1}" + gem "foo" + G + + bundle :check, :env => { "DEBUG" => 1 } + + expect(out).to match(/using resolution from the lockfile/) + lockfile_should_be <<-G + GEM + remote: file://localhost#{gem_repo1}/ + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo + + BUNDLED WITH + #{Bundler::VERSION} + G + end + end + + describe "initialize" do + context "gem version promoter" do + context "with lockfile" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo" + G + end + + it "should get a locked specs list when updating all" do + definition = Bundler::Definition.new(bundled_app("Gemfile.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| + source_list.global_rubygems_source = "file://#{gem_repo4}" + end + end + + before do + gemfile <<-G + source "file://#{gem_repo4}" + gem 'isolated_owner' + + gem 'shared_owner_a' + gem 'shared_owner_b' + G + + lockfile <<-L + GEM + remote: file://#{gem_repo4} + specs: + isolated_dep (2.0.1) + isolated_owner (1.0.1) + isolated_dep (~> 2.0) + shared_dep (5.0.1) + shared_owner_a (3.0.1) + shared_dep (~> 5.0) + shared_owner_b (4.0.1) + shared_dep (~> 5.0) + + PLATFORMS + ruby + + DEPENDENCIES + shared_owner_a + shared_owner_b + isolated_owner + + BUNDLED WITH + 1.13.0 + L + end + + it "should not eagerly unlock shared dependency with bundle install conservative updating behavior" do + updated_deps_in_gemfile = [Bundler::Dependency.new("isolated_owner", ">= 0"), + Bundler::Dependency.new("shared_owner_a", "3.0.2"), + Bundler::Dependency.new("shared_owner_b", ">= 0")] + unlock_hash_for_bundle_install = {} + definition = Bundler::Definition.new( + bundled_app("Gemfile.lock"), + updated_deps_in_gemfile, + source_list, + unlock_hash_for_bundle_install + ) + locked = definition.send(:converge_locked_specs).map(&:name) + expect(locked).to include "shared_dep" + end + + it "should not eagerly unlock shared dependency with bundle update conservative updating behavior" do + updated_deps_in_gemfile = [Bundler::Dependency.new("isolated_owner", ">= 0"), + Bundler::Dependency.new("shared_owner_a", ">= 0"), + Bundler::Dependency.new("shared_owner_b", ">= 0")] + definition = Bundler::Definition.new( + bundled_app("Gemfile.lock"), + updated_deps_in_gemfile, + source_list, + :gems => ["shared_owner_a"], :lock_shared_dependencies => true + ) + locked = definition.send(:converge_locked_specs).map(&:name) + expect(locked).to eq %w[isolated_dep isolated_owner shared_dep shared_owner_b] + expect(locked.include?("shared_dep")).to be_truthy + end + end + end + end + + describe "find_resolved_spec" do + it "with no platform set in SpecSet" do + ss = Bundler::SpecSet.new([build_stub_spec("a", "1.0"), build_stub_spec("b", "1.0")]) + dfn = Bundler::Definition.new(nil, [], mock_source_list, true) + dfn.instance_variable_set("@specs", ss) + found = dfn.find_resolved_spec(build_spec("a", "0.9", "ruby").first) + expect(found.name).to eq "a" + expect(found.version.to_s).to eq "1.0" + end + end + + describe "find_indexed_specs" do + it "with no platform set in indexed specs" do + index = Bundler::Index.new + %w[1.0.0 1.0.1 1.1.0].each {|v| index << build_stub_spec("foo", v) } + + dfn = Bundler::Definition.new(nil, [], mock_source_list, true) + dfn.instance_variable_set("@index", index) + found = dfn.find_indexed_specs(build_spec("foo", "0.9", "ruby").first) + expect(found.length).to eq 3 + end + end + + def build_stub_spec(name, version) + Bundler::StubSpecification.new(name, version, nil, nil) + end + + def mock_source_list + Class.new do + def all_sources + [] + end + + def path_sources + [] + end + + def rubygems_remotes + [] + end + + def replace_sources!(arg) + nil + end + end.new + end +end diff --git a/spec/bundler/bundler/dep_proxy_spec.rb b/spec/bundler/bundler/dep_proxy_spec.rb new file mode 100644 index 0000000000..0f8d6b1076 --- /dev/null +++ b/spec/bundler/bundler/dep_proxy_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::DepProxy do + let(:dep) { Bundler::Dependency.new("rake", ">= 0") } + subject { described_class.new(dep, Gem::Platform::RUBY) } + let(:same) { subject } + let(:other) { subject.dup } + let(:different) { described_class.new(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 "#hash" do + it { expect(subject.hash).to eq(same.hash) } + it { expect(subject.hash).to eq(other.hash) } + end +end diff --git a/spec/bundler/bundler/dsl_spec.rb b/spec/bundler/bundler/dsl_spec.rb new file mode 100644 index 0000000000..bffe4f1608 --- /dev/null +++ b/spec/bundler/bundler/dsl_spec.rb @@ -0,0 +1,289 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Dsl do + before do + @rubygems = double("rubygems") + allow(Bundler::Source::Rubygems).to receive(:new) { @rubygems } + end + + describe "#git_source" 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") + example_uri = "git@git.example.com:strzalek/dobry-pies.git" + expect(subject.dependencies.first.source.uri).to eq(example_uri) + end + + it "raises exception on invalid hostname" do + expect do + subject.git_source(:group) {|repo_name| "git@git.example.com:#{repo_name}.git" } + end.to raise_error(Bundler::InvalidOption) + end + + it "expects block passed" do + expect { subject.git_source(:example) }.to raise_error(Bundler::InvalidOption) + end + + context "default hosts (git, gist)", :bundler => "< 2" do + it "converts :github to :git" do + subject.gem("sparks", :github => "indirect/sparks") + github_uri = "git://github.com/indirect/sparks.git" + expect(subject.dependencies.first.source.uri).to eq(github_uri) + end + + it "converts numeric :gist to :git" do + 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") + github_uri = "https://gist.github.com/2859988.git" + expect(subject.dependencies.first.source.uri).to eq(github_uri) + end + + it "converts 'rails' to 'rails/rails'" do + subject.gem("rails", :github => "rails") + github_uri = "git://github.com/rails/rails.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") + 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") + 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", :bundler => "2" do + it "has none" do + expect(subject.instance_variable_get(:@git_sources)).to eq({}) + end + end + end + + describe "#method_missing" do + it "raises an error for unknown DSL methods" do + expect(Bundler).to receive(:read_file).with(bundled_app("Gemfile").to_s). + and_return("unknown") + + error_msg = "There was an error parsing `Gemfile`: Undefined local variable or method `unknown' for Gemfile. Bundler cannot continue." + expect { subject.eval_gemfile("Gemfile") }. + to raise_error(Bundler::GemfileError, Regexp.new(error_msg)) + end + end + + describe "#eval_gemfile" do + it "handles syntax errors with a useful message" do + expect(Bundler).to receive(:read_file).with(bundled_app("Gemfile").to_s).and_return("}") + expect { subject.eval_gemfile("Gemfile") }. + to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`: (syntax error, unexpected tSTRING_DEND|(compile error - )?syntax error, unexpected '\}'). Bundler cannot continue./) + end + + it "distinguishes syntax errors from evaluation errors" do + expect(Bundler).to receive(:read_file).with(bundled_app("Gemfile").to_s).and_return( + "ruby '2.1.5', :engine => 'ruby', :engine_version => '1.2.4'" + ) + 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 + end + + describe "#gem" do + [:ruby, :ruby_18, :ruby_19, :ruby_20, :ruby_21, :ruby_22, :ruby_23, :ruby_24, :ruby_25, :mri, :mri_18, :mri_19, + :mri_20, :mri_21, :mri_22, :mri_23, :mri_24, :mri_25, :jruby, :rbx, :truffleruby].each do |platform| + it "allows #{platform} as a valid platform" do + subject.gem("foo", :platform => platform) + end + end + + it "rejects invalid platforms" do + expect { subject.gem("foo", :platform => :bogus) }. + to raise_error(Bundler::GemfileError, /is not a valid platform/) + end + + it "rejects empty gem name" do + expect { subject.gem("") }. + to raise_error(Bundler::GemfileError, /an empty gem name is not valid/) + end + + it "rejects with a leading space in the name" do + expect { subject.gem(" foo") }. + to raise_error(Bundler::GemfileError, /' foo' is not a valid gem name because it contains whitespace/) + end + + it "rejects with a trailing space in the name" do + expect { subject.gem("foo ") }. + to raise_error(Bundler::GemfileError, /'foo ' is not a valid gem name because it contains whitespace/) + end + + it "rejects with a space in the gem name" do + expect { subject.gem("fo o") }. + to raise_error(Bundler::GemfileError, /'fo o' is not a valid gem name because it contains whitespace/) + end + + it "rejects with a tab in the gem name" do + expect { subject.gem("fo\to") }. + to raise_error(Bundler::GemfileError, /'fo\to' is not a valid gem name because it contains whitespace/) + end + + it "rejects with a newline in the gem name" do + expect { subject.gem("fo\no") }. + to raise_error(Bundler::GemfileError, /'fo\no' is not a valid gem name because it contains whitespace/) + end + + it "rejects with a carriage return in the gem name" do + expect { subject.gem("fo\ro") }. + to raise_error(Bundler::GemfileError, /'fo\ro' is not a valid gem name because it contains whitespace/) + end + + it "rejects with a form feed in the gem name" do + expect { subject.gem("fo\fo") }. + to raise_error(Bundler::GemfileError, /'fo\fo' is not a valid gem name because it contains whitespace/) + end + + it "rejects symbols as gem name" do + expect { subject.gem(:foo) }. + to raise_error(Bundler::GemfileError, /You need to specify gem names as Strings. Use 'gem "foo"' instead/) + end + + it "rejects branch option on non-git gems" do + 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") + 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") + dep = subject.dependencies.last + expect(dep.name).to eq "foo" + end + end + + describe "#gemspec" do + let(:spec) do + Gem::Specification.new do |gem| + gem.name = "example" + gem.platform = platform + end + end + + before do + allow(Dir).to receive(:[]).and_return(["spec_path"]) + allow(Bundler).to receive(:load_gemspec).with("spec_path").and_return(spec) + allow(Bundler).to receive(:default_gemfile).and_return(Pathname.new("./Gemfile")) + end + + context "with a ruby platform" do + let(:platform) { "ruby" } + + it "keeps track of the ruby platforms in the dependency" do + subject.gemspec + expect(subject.dependencies.last.platforms).to eq(Bundler::Dependency::REVERSE_PLATFORM_MAP[Gem::Platform::RUBY]) + end + end + + context "with a jruby platform" do + let(:platform) { "java" } + + it "keeps track of the jruby platforms in the dependency" do + allow(Gem::Platform).to receive(:local).and_return(java) + subject.gemspec + expect(subject.dependencies.last.platforms).to eq(Bundler::Dependency::REVERSE_PLATFORM_MAP[Gem::Platform::JAVA]) + end + end + end + + context "can bundle groups of gems with" do + # git "https://github.com/rails/rails.git" do + # gem "railties" + # gem "action_pack" + # gem "active_model" + # end + describe "#git" do + it "from a single repo" do + rails_gems = %w[railties action_pack active_model] + subject.git "https://github.com/rails/rails.git" do + rails_gems.each {|rails_gem| subject.send :gem, rails_gem } + end + expect(subject.dependencies.map(&:name)).to match_array rails_gems + end + end + + # github 'spree' do + # gem 'spree_core' + # gem 'spree_api' + # gem 'spree_backend' + # end + describe "#github", :bundler => "< 2" do + it "from github" do + spree_gems = %w[spree_core spree_api spree_backend] + subject.github "spree" do + spree_gems.each {|spree_gem| subject.send :gem, spree_gem } + end + + subject.dependencies.each do |d| + expect(d.source.uri).to eq("git://github.com/spree/spree.git") + end + end + end + + describe "#github", :bundler => "2" do + it "from github" do + expect do + spree_gems = %w[spree_core spree_api spree_backend] + subject.github "spree" do + spree_gems.each {|spree_gem| subject.send :gem, spree_gem } + end + end.to raise_error(Bundler::DeprecatedError, /github method has been removed/) + end + end + end + + describe "syntax errors" do + it "will raise a Bundler::GemfileError" do + gemfile "gem 'foo', :path => /unquoted/string/syntax/error" + expect { Bundler::Dsl.evaluate(bundled_app("Gemfile"), nil, true) }. + to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`:( compile error -)? unknown regexp options - trg. Bundler cannot continue./) + end + end + + describe "Runtime errors", :unless => Bundler.current_ruby.on_18? do + it "will raise a Bundler::GemfileError" do + gemfile "s = 'foo'.freeze; s.strip!" + expect { Bundler::Dsl.evaluate(bundled_app("Gemfile"), nil, true) }. + to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`: can't modify frozen String. Bundler cannot continue./i) + end + end + + describe "#with_source" do + context "if there was a rubygem source already defined" do + it "restores it after it's done" do + other_source = double("other-source") + allow(Bundler::Source::Rubygems).to receive(:new).and_return(other_source) + 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("foo") + end + + expect(subject.dependencies.last.source).to eq(other_source) + end + end + end +end diff --git a/spec/bundler/bundler/endpoint_specification_spec.rb b/spec/bundler/bundler/endpoint_specification_spec.rb new file mode 100644 index 0000000000..a9371f6617 --- /dev/null +++ b/spec/bundler/bundler/endpoint_specification_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::EndpointSpecification do + let(:name) { "foo" } + let(:version) { "1.0.0" } + let(:platform) { Gem::Platform::RUBY } + let(:dependencies) { [] } + let(:metadata) { nil } + + subject(:spec) { described_class.new(name, version, platform, dependencies, metadata) } + + describe "#build_dependency" do + let(:name) { "foo" } + let(:requirement1) { "~> 1.1" } + let(:requirement2) { ">= 1.1.7" } + + it "should return a Gem::Dependency" do + expect(subject.send(:build_dependency, name, [requirement1, requirement2])). + to eq(Gem::Dependency.new(name, requirement1, requirement2)) + end + + context "when an ArgumentError occurs" do + before do + allow(Gem::Dependency).to receive(:new).with(name, [requirement1, requirement2]) { + raise ArgumentError.new("Some error occurred") + } + end + + it "should raise the original error" do + expect { subject.send(:build_dependency, name, [requirement1, requirement2]) }.to raise_error( + ArgumentError, "Some error occurred" + ) + end + end + + context "when there is an ill formed requirement" do + before do + allow(Gem::Dependency).to receive(:new).with(name, [requirement1, requirement2]) { + raise ArgumentError.new("Ill-formed requirement [\"#<YAML::Syck::DefaultKey") + } + # Eliminate extra line break in rspec output due to `puts` in `#build_dependency` + allow(subject).to receive(:puts) {} + end + + it "should raise a Bundler::GemspecError with invalid gemspec message" do + expect { subject.send(:build_dependency, name, [requirement1, requirement2]) }.to raise_error( + Bundler::GemspecError, /Unfortunately, the gem foo \(1\.0\.0\) has an invalid gemspec/ + ) + end + end + end + + describe "#parse_metadata" do + context "when the metadata has malformed requirements" do + let(:metadata) { { "rubygems" => ">\n" } } + it "raises a helpful error message" do + expect { subject }.to raise_error( + Bundler::GemspecError, + a_string_including("There was an error parsing the metadata for the gem foo (1.0.0)"). + and(a_string_including('The metadata was {"rubygems"=>">\n"}')) + ) + end + end + end + + it "supports equality comparison" do + other_spec = described_class.new("bar", version, platform, dependencies, metadata) + expect(spec).to eql(spec) + expect(spec).to_not eql(other_spec) + end +end diff --git a/spec/bundler/bundler/env_spec.rb b/spec/bundler/bundler/env_spec.rb new file mode 100644 index 0000000000..20bd38b021 --- /dev/null +++ b/spec/bundler/bundler/env_spec.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +require "bundler/settings" + +RSpec.describe Bundler::Env do + let(:git_proxy_stub) { Bundler::Source::Git::GitProxy.new(nil, nil, nil) } + + describe "#report" do + it "prints the environment" do + out = described_class.report + + expect(out).to include("Environment") + expect(out).to include(Bundler::VERSION) + expect(out).to include(Gem::VERSION) + expect(out).to include(described_class.send(:ruby_version)) + expect(out).to include(described_class.send(:git_version)) + expect(out).to include(OpenSSL::OPENSSL_VERSION) + end + + context "when there is a Gemfile and a lockfile and print_gemfile is true" do + before do + gemfile "gem 'rack', '1.0.0'" + + lockfile <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + DEPENDENCIES + rack + + BUNDLED WITH + 1.10.0 + L + end + + let(:output) { described_class.report(:print_gemfile => true) } + + it "prints the Gemfile" do + expect(output).to include("Gemfile") + expect(output).to include("'rack', '1.0.0'") + end + + it "prints the lockfile" do + expect(output).to include("Gemfile.lock") + expect(output).to include("rack (1.0.0)") + end + end + + context "when there no Gemfile and print_gemfile is true" do + let(:output) { described_class.report(:print_gemfile => true) } + + it "prints the environment" do + expect(output).to start_with("## Environment") + end + end + + context "when Gemfile contains a gemspec and print_gemspecs is true" do + let(:gemspec) do + strip_whitespace(<<-GEMSPEC) + Gem::Specification.new do |gem| + gem.name = "foo" + gem.author = "Fumofu" + end + GEMSPEC + end + + before do + gemfile("gemspec") + + File.open(bundled_app.join("foo.gemspec"), "wb") do |f| + f.write(gemspec) + end + end + + it "prints the gemspec" do + output = described_class.report(:print_gemspecs => true) + + expect(output).to include("foo.gemspec") + expect(output).to include(gemspec) + end + end + + context "when eval_gemfile is used" do + it "prints all gemfiles" do + create_file "other/Gemfile-other", "gem 'rack'" + create_file "other/Gemfile", "eval_gemfile 'Gemfile-other'" + create_file "Gemfile-alt", <<-G + source "file:#{gem_repo1}" + eval_gemfile "other/Gemfile" + G + gemfile "eval_gemfile #{File.expand_path("Gemfile-alt").dump}" + + output = described_class.report(:print_gemspecs => true) + expect(output).to include(strip_whitespace(<<-ENV)) + ## Gemfile + + ### Gemfile + + ```ruby + eval_gemfile #{File.expand_path("Gemfile-alt").dump} + ``` + + ### Gemfile-alt + + ```ruby + source "file:#{gem_repo1}" + eval_gemfile "other/Gemfile" + ``` + + ### other/Gemfile + + ```ruby + eval_gemfile 'Gemfile-other' + ``` + + ### other/Gemfile-other + + ```ruby + gem 'rack' + ``` + + ### Gemfile.lock + + ``` + <No #{bundled_app("Gemfile.lock")} found> + ``` + ENV + end + end + + 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"). + and_return("git version 1.2.3 (Apple Git-BS)") + expect(Bundler::Source::Git::GitProxy).to receive(:new).and_return(git_proxy_stub) + + expect(described_class.report).to include("Git 1.2.3 (Apple Git-BS)") + end + end + end + + describe ".version_of", :ruby_repo do + let(:parsed_version) { described_class.send(:version_of, "ruby") } + + it "strips version of new line characters" do + expect(parsed_version).to_not include("\n") + end + end +end diff --git a/spec/bundler/bundler/environment_preserver_spec.rb b/spec/bundler/bundler/environment_preserver_spec.rb new file mode 100644 index 0000000000..530ca6f835 --- /dev/null +++ b/spec/bundler/bundler/environment_preserver_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::EnvironmentPreserver do + let(:preserver) { described_class.new(env, ["foo"]) } + + describe "#backup" do + let(:env) { { "foo" => "my-foo", "bar" => "my-bar" } } + subject { preserver.backup } + + it "should create backup entries" do + expect(subject["BUNDLER_ORIG_foo"]).to eq("my-foo") + end + + it "should keep the original entry" do + expect(subject["foo"]).to eq("my-foo") + end + + it "should not create backup entries for unspecified keys" do + expect(subject.key?("BUNDLER_ORIG_bar")).to eq(false) + end + + it "should not affect the original env" do + subject + expect(env.keys.sort).to eq(%w[bar foo]) + end + + 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" + end + end + + context "when an original key is set" do + let(:env) { { "foo" => "my-foo", "BUNDLER_ORIG_foo" => "orig-foo" } } + + it "should keep the original value in the BUNDLER_ORIG_ variable" do + expect(subject["BUNDLER_ORIG_foo"]).to eq("orig-foo") + end + + it "should keep the variable" do + expect(subject["foo"]).to eq("my-foo") + end + end + end + + describe "#restore" do + subject { preserver.restore } + + context "when an original key is set" do + let(:env) { { "foo" => "my-foo", "BUNDLER_ORIG_foo" => "orig-foo" } } + + it "should restore the original value" do + expect(subject["foo"]).to eq("orig-foo") + end + + it "should delete the backup value" do + expect(subject.key?("BUNDLER_ORIG_foo")).to eq(false) + end + end + + context "when no original key is set" do + let(:env) { { "foo" => "my-foo" } } + + it "should keep the current value" do + expect(subject["foo"]).to eq("my-foo") + end + end + + 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") + end + end + end +end diff --git a/spec/bundler/bundler/fetcher/base_spec.rb b/spec/bundler/bundler/fetcher/base_spec.rb new file mode 100644 index 0000000000..df1245d44d --- /dev/null +++ b/spec/bundler/bundler/fetcher/base_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Fetcher::Base do + let(:downloader) { double(:downloader) } + let(:remote) { double(:remote) } + let(:display_uri) { "http://sample_uri.com" } + + class TestClass < described_class; end + + subject { TestClass.new(downloader, remote, display_uri) } + + 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") + end + end + + context "with a class that inherits the Base class" do + it "should set the passed attributes" do + expect(subject.downloader).to eq(downloader) + expect(subject.remote).to eq(remote) + expect(subject.display_uri).to eq("http://sample_uri.com") + end + end + end + + describe "#remote_uri" do + let(:remote_uri_obj) { double(:remote_uri_obj) } + + before { allow(remote).to receive(:uri).and_return(remote_uri_obj) } + + it "should return the remote's uri" do + expect(subject.remote_uri).to eq(remote_uri_obj) + end + end + + describe "#fetch_uri" do + let(:remote_uri_obj) { URI("http://rubygems.org") } + + before { allow(subject).to receive(:remote_uri).and_return(remote_uri_obj) } + + context "when the remote uri's host is rubygems.org" do + it "should create a copy of the remote uri with index.rubygems.org as the host" do + fetched_uri = subject.fetch_uri + expect(fetched_uri.host).to eq("index.rubygems.org") + expect(fetched_uri).to_not be(remote_uri_obj) + end + end + + context "when the remote uri's host is not rubygems.org" do + let(:remote_uri_obj) { URI("http://otherhost.org") } + + it "should return the remote uri" do + expect(subject.fetch_uri).to eq(URI("http://otherhost.org")) + end + end + + it "memoizes the fetched uri" do + expect(remote_uri_obj).to receive(:host).once + 2.times { subject.fetch_uri } + end + end + + describe "#available?" do + it "should return whether the api is available" do + expect(subject.available?).to be_truthy + end + end + + describe "#api_fetcher?" do + it "should return false" do + expect(subject.api_fetcher?).to be_falsey + end + end +end diff --git a/spec/bundler/bundler/fetcher/compact_index_spec.rb b/spec/bundler/bundler/fetcher/compact_index_spec.rb new file mode 100644 index 0000000000..e0f58766ea --- /dev/null +++ b/spec/bundler/bundler/fetcher/compact_index_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Fetcher::CompactIndex do + let(:downloader) { double(:downloader) } + let(:display_uri) { URI("http://sampleuri.com") } + let(:remote) { double(:remote, :cache_slug => "lsjdf", :uri => display_uri) } + let(:compact_index) { described_class.new(downloader, remote, display_uri) } + + before do + allow(compact_index).to receive(:log_specs) {} + end + + describe "#specs_for_names" do + it "has only one thread open at the end of the run" do + compact_index.specs_for_names(["lskdjf"]) + + thread_count = Thread.list.count {|thread| thread.status == "run" } + expect(thread_count).to eq 1 + end + + it "calls worker#stop during the run" do + expect_any_instance_of(Bundler::Worker).to receive(:stop).at_least(:once) + + compact_index.specs_for_names(["lskdjf"]) + end + + describe "#available?" do + before do + allow(compact_index).to receive(:compact_index_client). + and_return(double(:compact_index_client, :update_and_parse_checksums! => true)) + end + + it "returns true" do + expect(compact_index).to be_available + end + + context "when OpenSSL is not available" do + before do + allow(compact_index).to receive(:require).with("openssl").and_raise(LoadError) + end + + it "returns true" do + expect(compact_index).to be_available + end + end + + context "when OpenSSL is FIPS-enabled", :ruby => ">= 2.0.0" do + def remove_cached_md5_availability + return unless Bundler::SharedHelpers.instance_variable_defined?(:@md5_available) + Bundler::SharedHelpers.remove_instance_variable(:@md5_available) + end + + before do + remove_cached_md5_availability + stub_const("OpenSSL::OPENSSL_FIPS", true) + end + + after { remove_cached_md5_availability } + + context "when FIPS-mode is active" do + before do + allow(OpenSSL::Digest::MD5).to receive(:digest). + and_raise(OpenSSL::Digest::DigestError) + end + + it "returns false" do + expect(compact_index).to_not be_available + end + end + + it "returns true" do + expect(compact_index).to be_available + end + end + end + + context "logging" do + before { allow(compact_index).to receive(:log_specs).and_call_original } + + context "with debug on" do + before do + allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(true) + end + + it "should log at info level" do + expect(Bundler).to receive_message_chain(:ui, :debug).with('Looking up gems ["lskdjf"]') + compact_index.specs_for_names(["lskdjf"]) + end + end + + context "with debug off" do + before do + allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(false) + end + + it "should log at info level" do + expect(Bundler).to receive_message_chain(:ui, :info).with(".", false) + compact_index.specs_for_names(["lskdjf"]) + end + end + end + end +end diff --git a/spec/bundler/bundler/fetcher/dependency_spec.rb b/spec/bundler/bundler/fetcher/dependency_spec.rb new file mode 100644 index 0000000000..081fdff34d --- /dev/null +++ b/spec/bundler/bundler/fetcher/dependency_spec.rb @@ -0,0 +1,287 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Fetcher::Dependency do + let(:downloader) { double(:downloader) } + let(:remote) { double(:remote, :uri => URI("http://localhost:5000")) } + let(:display_uri) { "http://sample_uri.com" } + + subject { described_class.new(downloader, remote, display_uri) } + + describe "#available?" do + let(:dependency_api_uri) { double(:dependency_api_uri) } + let(:fetched_spec) { double(:fetched_spec) } + + before do + allow(subject).to receive(:dependency_api_uri).and_return(dependency_api_uri) + allow(downloader).to receive(:fetch).with(dependency_api_uri).and_return(fetched_spec) + end + + it "should be truthy" do + expect(subject.available?).to be_truthy + end + + context "when there is no network access" do + before do + allow(downloader).to receive(:fetch).with(dependency_api_uri) { + raise Bundler::Fetcher::NetworkDownError.new("Network Down Message") + } + end + + it "should raise an HTTPError with the original message" do + expect { subject.available? }.to raise_error(Bundler::HTTPError, "Network Down Message") + end + end + + context "when authentication is required" do + let(:remote_uri) { "http://remote_uri.org" } + + before do + allow(downloader).to receive(:fetch).with(dependency_api_uri) { + raise Bundler::Fetcher::AuthenticationRequiredError.new(remote_uri) + } + end + + it "should raise the original error" do + expect { subject.available? }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError, + %r{Authentication is required for http://remote_uri.org}) + end + end + + context "when there is an http error" do + before { allow(downloader).to receive(:fetch).with(dependency_api_uri) { raise Bundler::HTTPError.new } } + + it "should be falsey" do + expect(subject.available?).to be_falsey + end + end + end + + describe "#api_fetcher?" do + it "should return true" do + expect(subject.api_fetcher?).to be_truthy + end + end + + describe "#specs" do + let(:gem_names) { %w[foo bar] } + let(:full_dependency_list) { ["bar"] } + let(:last_spec_list) { [["boulder", gem_version1, "ruby", resque]] } + let(:fail_errors) { double(:fail_errors) } + let(:bundler_retry) { double(:bundler_retry) } + let(:gem_version1) { double(:gem_version1) } + let(:resque) { double(:resque) } + let(:remote_uri) { "http://remote-uri.org" } + + before do + stub_const("Bundler::Fetcher::FAIL_ERRORS", fail_errors) + allow(Bundler::Retry).to receive(:new).with("dependency api", fail_errors).and_return(bundler_retry) + allow(bundler_retry).to receive(:attempts) {|&block| block.call } + allow(subject).to receive(:log_specs) {} + allow(subject).to receive(:remote_uri).and_return(remote_uri) + allow(Bundler).to receive_message_chain(:ui, :debug?) + allow(Bundler).to receive_message_chain(:ui, :info) + allow(Bundler).to receive_message_chain(:ui, :debug) + end + + context "when there are given gem names that are not in the full dependency list" do + let(:spec_list) { [["top", gem_version2, "ruby", faraday]] } + let(:deps_list) { [] } + let(:dependency_specs) { [spec_list, deps_list] } + let(:gem_version2) { double(:gem_version2) } + let(:faraday) { double(:faraday) } + + before { allow(subject).to receive(:dependency_specs).with(["foo"]).and_return(dependency_specs) } + + it "should return a hash with the remote_uri and the list of specs" do + expect(subject.specs(gem_names, full_dependency_list, last_spec_list)).to eq([ + ["top", gem_version2, "ruby", faraday], + ["boulder", gem_version1, "ruby", resque], + ]) + end + end + + context "when all given gem names are in the full dependency list" do + let(:gem_names) { ["foo"] } + let(:full_dependency_list) { %w[foo bar] } + let(:last_spec_list) { ["boulder"] } + + it "should return a hash with the remote_uri and the last spec list" do + expect(subject.specs(gem_names, full_dependency_list, last_spec_list)).to eq(["boulder"]) + end + end + + context "logging" do + before { allow(subject).to receive(:log_specs).and_call_original } + + context "with debug on" do + before do + allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(true) + allow(subject).to receive(:dependency_specs).with(["foo"]).and_return([[], []]) + end + + it "should log the query list at debug level" do + expect(Bundler).to receive_message_chain(:ui, :debug).with("Query List: [\"foo\"]") + expect(Bundler).to receive_message_chain(:ui, :debug).with("Query List: []") + subject.specs(gem_names, full_dependency_list, last_spec_list) + end + end + + context "with debug off" do + before do + allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(false) + allow(subject).to receive(:dependency_specs).with(["foo"]).and_return([[], []]) + end + + it "should log at info level" do + expect(Bundler).to receive_message_chain(:ui, :info).with(".", false) + expect(Bundler).to receive_message_chain(:ui, :info).with(".", false) + subject.specs(gem_names, full_dependency_list, last_spec_list) + end + end + end + + shared_examples_for "the error is properly handled" do + it "should return nil" do + expect(subject.specs(gem_names, full_dependency_list, last_spec_list)).to be_nil + end + + context "debug logging is not on" do + before { allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(false) } + + it "should log a new line to info" do + expect(Bundler).to receive_message_chain(:ui, :info).with("") + subject.specs(gem_names, full_dependency_list, last_spec_list) + end + 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`") + subject.specs(gem_names, full_dependency_list, last_spec_list) + end + end + + context "when an HTTPError occurs" 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" + 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" + 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 + end + end + + describe "#dependency_specs" do + let(:gem_names) { [%w[foo bar], %w[bundler rubocop]] } + let(:gem_list) { double(:gem_list) } + let(:formatted_specs_and_deps) { double(:formatted_specs_and_deps) } + + before do + allow(subject).to receive(:unmarshalled_dep_gems).with(gem_names).and_return(gem_list) + allow(subject).to receive(:get_formatted_specs_and_deps).with(gem_list).and_return(formatted_specs_and_deps) + end + + it "should log the query list at debug level" do + expect(Bundler).to receive_message_chain(:ui, :debug).with( + "Query Gemcutter Dependency Endpoint API: foo,bar,bundler,rubocop" + ) + subject.dependency_specs(gem_names) + end + + it "should return formatted specs and a unique list of dependencies" do + expect(subject.dependency_specs(gem_names)).to eq(formatted_specs_and_deps) + end + end + + describe "#unmarshalled_dep_gems" 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(:rubygems_limit) { 50 } + + before { allow(subject).to receive(:dependency_api_uri).with(gem_names).and_return(dep_api_uri) } + + 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(subject.unmarshalled_dep_gems(gem_names)).to eq([unmarshalled_gems]) + end + end + + describe "#get_formatted_specs_and_deps" do + let(:gem_list) do + [ + { + :dependencies => { + "resque" => "req3,req4", + }, + :name => "typhoeus", + :number => "1.0.1", + :platform => "ruby", + }, + { + :dependencies => { + "faraday" => "req1,req2", + }, + :name => "grape", + :number => "2.0.2", + :platform => "jruby", + }, + ] + end + + it "should return formatted specs and a unique list of dependencies" do + spec_list, deps_list = subject.get_formatted_specs_and_deps(gem_list) + expect(spec_list).to eq([["typhoeus", "1.0.1", "ruby", [["resque", ["req3,req4"]]]], + ["grape", "2.0.2", "jruby", [["faraday", ["req1,req2"]]]]]) + expect(deps_list).to eq(%w[resque faraday]) + end + end + + describe "#dependency_api_uri" do + let(:uri) { URI("http://gem-api.com") } + + context "with gem names" do + let(:gem_names) { %w[foo bar bundler rubocop] } + + before { allow(subject).to receive(:fetch_uri).and_return(uri) } + + it "should return an api calling uri with the gems in the query" do + expect(subject.dependency_api_uri(gem_names).to_s).to eq( + "http://gem-api.com/api/v1/dependencies?gems=bar%2Cbundler%2Cfoo%2Crubocop" + ) + end + end + + context "with no gem names" do + let(:gem_names) { [] } + + before { allow(subject).to receive(:fetch_uri).and_return(uri) } + + it "should return an api calling uri with no query" do + expect(subject.dependency_api_uri(gem_names).to_s).to eq( + "http://gem-api.com/api/v1/dependencies" + ) + end + end + end +end diff --git a/spec/bundler/bundler/fetcher/downloader_spec.rb b/spec/bundler/bundler/fetcher/downloader_spec.rb new file mode 100644 index 0000000000..c9b4fa662a --- /dev/null +++ b/spec/bundler/bundler/fetcher/downloader_spec.rb @@ -0,0 +1,250 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Fetcher::Downloader do + let(:connection) { double(:connection) } + let(:redirect_limit) { 5 } + let(:uri) { URI("http://www.uri-to-fetch.com/api/v2/endpoint") } + let(:options) { double(:options) } + + subject { described_class.new(connection, redirect_limit) } + + describe "fetch" do + let(:counter) { 0 } + let(:httpv) { "1.1" } + let(:http_response) { double(:response) } + + before do + allow(subject).to receive(:request).with(uri, options).and_return(http_response) + allow(http_response).to receive(:body).and_return("Body with info") + end + + context "when the # requests counter is greater than the redirect limit" do + let(:counter) { redirect_limit + 1 } + + it "should raise a Bundler::HTTPError specifying too many redirects" do + expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::HTTPError, "Too many redirects") + end + end + + context "logging" do + let(:http_response) { 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}") + subject.fetch(uri, options, counter) + end + end + + context "when the request response is a Net::HTTPRedirection" do + let(:http_response) { 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(URI("http://www.uri-to-fetch.com/api/v2/endpoint"), options, 0).and_call_original + expect(subject).to receive(:fetch).with(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) { 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(URI("ssh://username:password@www.uri-to-fetch.com/api/v2/endpoint"), options, 0).and_call_original + expect(subject).to receive(:fetch).with(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") } + + 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") } + + 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") } + + it "should raise a Bundler::Fetcher::AuthenticationRequiredError with the uri host" do + expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError, + /Authentication is required for www.uri-to-fetch.com/) + end + end + + context "when the request response is a Net::HTTPNotFound" do + let(:http_response) { Net::HTTPNotFound.new("1.1", 404, "Not Found") } + + it "should raise a Bundler::Fetcher::FallbackError with Net::HTTPNotFound" do + expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::FallbackError, "Net::HTTPNotFound") + end + end + + context "when the request response is some other type" do + let(:http_response) { 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") + end + end + end + + describe "request" do + let(:net_http_get) { double(:net_http_get) } + let(:response) { double(:response) } + + before do + allow(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 + + it "should log the HTTP GET request to debug" do + expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://www.uri-to-fetch.com/api/v2/endpoint") + subject.request(uri, options) + end + + 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) { 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$") + subject.request(uri, options) + end + end + + context "that is all unescaped characters" do + let(:uri) { 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) + end + end + end + + context "and there is no password provided" do + let(:uri) { 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) + subject.request(uri, options) + end + end + + context "that contains cgi escaped characters" do + let(:uri) { 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) + subject.request(uri, options) + end + 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 } } + + it "should raise a LoadError about openssl" do + expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::CertificateFailureError, + %r{Could not verify the SSL certificate for http://www.uri-to-fetch.com/api/v2/endpoint}) + end + end + + context "when the request response causes an error included in HTTP_ERRORS" do + let(:message) { nil } + let(:error) { RuntimeError.new(message) } + + before do + stub_const("Bundler::Fetcher::HTTP_ERRORS", [RuntimeError]) + allow(connection).to receive(:request).with(uri, net_http_get) { raise error } + end + + it "should trace log the error" do + allow(Bundler).to receive_message_chain(:ui, :debug) + expect(Bundler).to receive_message_chain(:ui, :trace).with(error) + expect { subject.request(uri, options) }.to raise_error(Bundler::HTTPError) + end + + context "when error message is about the host being down" do + let(:message) { "host down: http://www.uri-to-fetch.com" } + + it "should raise a Bundler::Fetcher::NetworkDownError" do + expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::NetworkDownError, + /Could not reach host www.uri-to-fetch.com/) + end + end + + context "when error message is about getaddrinfo issues" do + let(:message) { "getaddrinfo: nodename nor servname provided for http://www.uri-to-fetch.com" } + + it "should raise a Bundler::Fetcher::NetworkDownError" do + expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::NetworkDownError, + /Could not reach host www.uri-to-fetch.com/) + end + end + + context "when error message is about neither host down or getaddrinfo" do + let(:message) { "other error about network" } + + it "should raise a Bundler::HTTPError" do + expect { subject.request(uri, options) }.to raise_error(Bundler::HTTPError, + "Network error while fetching http://www.uri-to-fetch.com/api/v2/endpoint (other error about network)") + end + + context "when the there are credentials provided in the request" do + let(:uri) { 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 + + it "should raise a Bundler::HTTPError that doesn't contain the password" do + expect { subject.request(uri, options) }.to raise_error(Bundler::HTTPError, + "Network error while fetching http://username@www.uri-to-fetch.com/api/v2/endpoint (other error about network)") + end + end + end + + context "when error message is about no route to host" do + let(:message) { "Failed to open TCP connection to www.uri-to-fetch.com:443 " } + + it "should raise a Bundler::Fetcher::HTTPError" do + expect { subject.request(uri, options) }.to raise_error(Bundler::HTTPError, + "Network error while fetching http://www.uri-to-fetch.com/api/v2/endpoint (#{message})") + end + end + end + end +end diff --git a/spec/bundler/bundler/fetcher/index_spec.rb b/spec/bundler/bundler/fetcher/index_spec.rb new file mode 100644 index 0000000000..0cf0ae764e --- /dev/null +++ b/spec/bundler/bundler/fetcher/index_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Fetcher::Index do + let(:downloader) { nil } + let(:remote) { nil } + let(:display_uri) { "http://sample_uri.com" } + let(:rubygems) { double(:rubygems) } + let(:gem_names) { %w[foo bar] } + + subject { described_class.new(downloader, remote, display_uri) } + + before { allow(Bundler).to receive(:rubygems).and_return(rubygems) } + + it "fetches and returns the list of remote specs" do + expect(rubygems).to receive(:fetch_all_remote_specs) { nil } + subject.specs(gem_names) + end + + context "error handling" do + shared_examples_for "the error is properly handled" do + let(:remote_uri) { URI("http://remote-uri.org") } + before do + allow(subject).to receive(:remote_uri).and_return(remote_uri) + end + + context "when certificate verify failed" do + let(:error_message) { "certificate verify failed" } + + it "should raise a Bundler::Fetcher::CertificateFailureError" do + expect { subject.specs(gem_names) }.to raise_error(Bundler::Fetcher::CertificateFailureError, + %r{Could not verify the SSL certificate for http://sample_uri.com}) + end + end + + context "when a 401 response occurs" do + let(:error_message) { "401" } + + 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 + end + + 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 + 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)) } + + 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") + end + end + end + + context "when a Gem::RemoteFetcher::FetchError occurs" do + before { allow(rubygems).to receive(:fetch_all_remote_specs) { raise Gem::RemoteFetcher::FetchError.new(error_message, nil) } } + + it_behaves_like "the error is properly handled" + end + + context "when a OpenSSL::SSL::SSLError occurs" do + before { allow(rubygems).to receive(:fetch_all_remote_specs) { raise OpenSSL::SSL::SSLError.new(error_message) } } + + it_behaves_like "the error is properly handled" + end + + context "when a Net::HTTPFatalError occurs" do + before { allow(rubygems).to receive(:fetch_all_remote_specs) { raise Net::HTTPFatalError.new(error_message, 404) } } + + it_behaves_like "the error is properly handled" + end + end +end diff --git a/spec/bundler/bundler/fetcher_spec.rb b/spec/bundler/bundler/fetcher_spec.rb new file mode 100644 index 0000000000..184b9efa64 --- /dev/null +++ b/spec/bundler/bundler/fetcher_spec.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +require "bundler/fetcher" + +RSpec.describe Bundler::Fetcher do + let(:uri) { URI("https://example.com") } + let(:remote) { double("remote", :uri => uri, :original_uri => nil) } + + subject(:fetcher) { Bundler::Fetcher.new(remote) } + + before do + allow(Bundler).to receive(:root) { Pathname.new("root") } + end + + describe "#connection" do + context "when Gem.configuration doesn't specify http_proxy" do + it "specify no http_proxy" do + expect(fetcher.http_proxy).to be_nil + end + it "consider environment vars when determine proxy" do + with_env_vars("HTTP_PROXY" => "http://proxy-example.com") do + expect(fetcher.http_proxy).to match("http://proxy-example.com") + end + end + end + 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) + end + it "consider Gem.configuration when determine proxy" do + expect(fetcher.http_proxy).to match("http://proxy-example2.com") + end + it "consider Gem.configuration when determine proxy" do + with_env_vars("HTTP_PROXY" => "http://proxy-example.com") do + expect(fetcher.http_proxy).to match("http://proxy-example2.com") + end + end + context "when the proxy is :no_proxy" do + let(:proxy) { :no_proxy } + it "does not set a proxy" do + expect(fetcher.http_proxy).to be_nil + end + end + end + + context "when a rubygems source mirror is set" do + let(:orig_uri) { URI("http://zombo.com") } + let(:remote_with_mirror) do + double("remote", :uri => uri, :original_uri => orig_uri, :anonymized_uri => uri) + end + + let(:fetcher) { Bundler::Fetcher.new(remote_with_mirror) } + + it "sets the 'X-Gemfile-Source' header containing the original source" do + expect( + fetcher.send(:connection).override_headers["X-Gemfile-Source"] + ).to eq("http://zombo.com") + end + end + + 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) + end + + let(:fetcher) { Bundler::Fetcher.new(remote_no_mirror) } + + it "does not set the 'X-Gemfile-Source' header" do + expect(fetcher.send(:connection).override_headers["X-Gemfile-Source"]).to be_nil + end + end + + context "when there are proxy environment variable(s) set" do + it "consider http_proxy" do + with_env_vars("HTTP_PROXY" => "http://proxy-example3.com") do + expect(fetcher.http_proxy).to match("http://proxy-example3.com") + end + end + it "consider no_proxy" do + with_env_vars("HTTP_PROXY" => "http://proxy-example4.com", "NO_PROXY" => ".example.com,.example.net") do + expect( + fetcher.send(:connection).no_proxy + ).to eq([".example.com", ".example.net"]) + end + end + end + + context "when no ssl configuration is set" do + it "no cert" do + expect(fetcher.send(:connection).cert).to be_nil + expect(fetcher.send(:connection).key).to be_nil + end + end + + context "when bunder ssl ssl configuration is set" do + before do + cert = File.join(Spec::Path.tmpdir, "cert") + File.open(cert, "w") {|f| f.write "PEM" } + allow(Bundler.settings).to receive(:[]).and_return(nil) + allow(Bundler.settings).to receive(:[]).with(:ssl_client_cert).and_return(cert) + expect(OpenSSL::X509::Certificate).to receive(:new).with("PEM").and_return("cert") + expect(OpenSSL::PKey::RSA).to receive(:new).with("PEM").and_return("key") + end + after do + FileUtils.rm File.join(Spec::Path.tmpdir, "cert") + end + it "use bundler configuration" do + expect(fetcher.send(:connection).cert).to eq("cert") + expect(fetcher.send(:connection).key).to eq("key") + end + end + + 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" + ) + expect(File).to receive(:read).and_return("") + expect(OpenSSL::X509::Certificate).to receive(:new).and_return("cert") + expect(OpenSSL::PKey::RSA).to receive(:new).and_return("key") + store = double("ca store") + expect(store).to receive(:add_file) + expect(OpenSSL::X509::Store).to receive(:new).and_return(store) + end + it "use gem configuration" do + expect(fetcher.send(:connection).cert).to eq("cert") + expect(fetcher.send(:connection).key).to eq("key") + end + end + end + + describe "#user_agent" do + it "builds user_agent with current ruby version and Bundler settings" do + allow(Bundler.settings).to receive(:all).and_return(%w[foo bar]) + expect(fetcher.user_agent).to match(%r{bundler/(\d.)}) + expect(fetcher.user_agent).to match(%r{rubygems/(\d.)}) + expect(fetcher.user_agent).to match(%r{ruby/(\d.)}) + expect(fetcher.user_agent).to match(%r{options/foo,bar}) + end + + describe "include CI information" do + it "from one CI" do + with_env_vars("JENKINS_URL" => "foo") do + ci_part = fetcher.user_agent.split(" ").find {|x| x.match(%r{\Aci/}) } + expect(ci_part).to match("jenkins") + end + end + + it "from many CI" do + with_env_vars("TRAVIS" => "foo", "CI_NAME" => "my_ci") do + ci_part = fetcher.user_agent.split(" ").find {|x| x.match(%r{\Aci/}) } + expect(ci_part).to match("travis") + expect(ci_part).to match("my_ci") + end + end + end + end +end diff --git a/spec/bundler/bundler/friendly_errors_spec.rb b/spec/bundler/bundler/friendly_errors_spec.rb new file mode 100644 index 0000000000..2a1be491ef --- /dev/null +++ b/spec/bundler/bundler/friendly_errors_spec.rb @@ -0,0 +1,270 @@ +# frozen_string_literal: true + +require "bundler" +require "bundler/friendly_errors" +require "cgi" + +RSpec.describe Bundler, "friendly errors" do + context "with invalid YAML in .gemrc" do + before do + File.open(Gem.configuration.config_file_name, "w") do |f| + f.write "invalid: yaml: hah" + end + end + + after do + FileUtils.rm(Gem.configuration.config_file_name) + end + + it "reports a relevant friendly error message", :ruby => ">= 1.9", :rubygems => "< 2.5.0" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install, :env => { "DEBUG" => true } + + expect(out).to include("Your RubyGems configuration") + expect(out).to include("invalid YAML syntax") + expect(out).to include("Psych::SyntaxError") + expect(out).not_to include("ERROR REPORT TEMPLATE") + expect(exitstatus).to eq(25) if exitstatus + end + + it "reports a relevant friendly error message", :ruby => ">= 1.9", :rubygems => ">= 2.5.0" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install, :env => { "DEBUG" => true } + + expect(last_command.stderr).to include("Failed to load #{home(".gemrc")}") + expect(exitstatus).to eq(0) if exitstatus + end + end + + it "calls log_error in case of exception" do + exception = Exception.new + expect(Bundler::FriendlyErrors).to receive(:exit_status).with(exception).and_return(1) + expect do + Bundler.with_friendly_errors do + raise exception + end + end.to raise_error(SystemExit) + end + + it "calls exit_status on exception" do + exception = Exception.new + expect(Bundler::FriendlyErrors).to receive(:log_error).with(exception) + expect do + Bundler.with_friendly_errors do + raise exception + end + end.to raise_error(SystemExit) + end + + describe "#log_error" do + shared_examples "Bundler.ui receive error" do |error, message| + it "" do + expect(Bundler.ui).to receive(:error).with(message || error.message) + Bundler::FriendlyErrors.log_error(error) + end + end + + shared_examples "Bundler.ui receive trace" do |error| + it "" do + expect(Bundler.ui).to receive(:trace).with(error) + Bundler::FriendlyErrors.log_error(error) + end + end + + context "YamlSyntaxError" do + it_behaves_like "Bundler.ui receive error", Bundler::YamlSyntaxError.new(StandardError.new, "sample_message") + + it "Bundler.ui receive trace" do + std_error = StandardError.new + exception = Bundler::YamlSyntaxError.new(std_error, "sample_message") + expect(Bundler.ui).to receive(:trace).with(std_error) + Bundler::FriendlyErrors.log_error(exception) + end + end + + context "Dsl::DSLError, GemspecError" do + it_behaves_like "Bundler.ui receive error", Bundler::Dsl::DSLError.new("description", "dsl_path", "backtrace") + it_behaves_like "Bundler.ui receive error", Bundler::GemspecError.new + end + + context "GemRequireError" do + let(:orig_error) { StandardError.new } + let(:error) { Bundler::GemRequireError.new(orig_error, "sample_message") } + + before do + allow(orig_error).to receive(:backtrace).and_return([]) + end + + it "Bundler.ui receive error" do + expect(Bundler.ui).to receive(:error).with(error.message) + Bundler::FriendlyErrors.log_error(error) + end + + it "writes to Bundler.ui.trace" do + expect(Bundler.ui).to receive(:trace).with(orig_error, nil, true) + Bundler::FriendlyErrors.log_error(error) + end + end + + context "BundlerError" do + it "Bundler.ui receive error" do + error = Bundler::BundlerError.new + 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") } + + it "Bundler.ui receive error" do + expect(Bundler.ui).to receive(:error).with("\nCould not load OpenSSL.") + Bundler::FriendlyErrors.log_error(error) + end + + it "Bundler.ui receive warn" do + expect(Bundler.ui).to receive(:warn).with(any_args, :wrap => true) + Bundler::FriendlyErrors.log_error(error) + end + + it "Bundler.ui receive trace" do + expect(Bundler.ui).to receive(:trace).with(error) + Bundler::FriendlyErrors.log_error(error) + end + end + + context "Interrupt" do + it "Bundler.ui receive error" do + expect(Bundler.ui).to receive(:error).with("\nQuitting...") + Bundler::FriendlyErrors.log_error(Interrupt.new) + end + it_behaves_like "Bundler.ui receive trace", Interrupt.new + end + + 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) + Bundler::FriendlyErrors.log_error(error) + end + end + + context "SystemExit" do + # Does nothing + end + + context "Java::JavaLang::OutOfMemoryError" do + module Java + module JavaLang + class OutOfMemoryError < StandardError; end + end + end + + it "Bundler.ui receive error" do + error = Java::JavaLang::OutOfMemoryError.new + expect(Bundler.ui).to receive(:error).with(/JVM has run out of memory/) + Bundler::FriendlyErrors.log_error(error) + end + end + + context "unexpected error" do + it "calls request_issue_report_for with error" do + error = StandardError.new + expect(Bundler::FriendlyErrors).to receive(:request_issue_report_for).with(error) + Bundler::FriendlyErrors.log_error(error) + end + end + end + + describe "#exit_status" do + it "calls status_code for BundlerError" do + error = Bundler::BundlerError.new + expect(error).to receive(:status_code).and_return("sample_status_code") + expect(Bundler::FriendlyErrors.exit_status(error)).to eq("sample_status_code") + end + + it "returns 15 for Thor::Error" do + error = Bundler::Thor::Error.new + expect(Bundler::FriendlyErrors.exit_status(error)).to eq(15) + end + + it "calls status for SystemExit" do + error = SystemExit.new + expect(error).to receive(:status).and_return("sample_status") + expect(Bundler::FriendlyErrors.exit_status(error)).to eq("sample_status") + end + + it "returns 1 in other cases" do + error = StandardError.new + expect(Bundler::FriendlyErrors.exit_status(error)).to eq(1) + end + end + + describe "#request_issue_report_for" do + it "calls relevant methods for Bundler.ui" do + expect(Bundler.ui).to receive(:info) + expect(Bundler.ui).to receive(:error) + expect(Bundler.ui).to receive(:warn) + Bundler::FriendlyErrors.request_issue_report_for(StandardError.new) + end + + it "includes error class, message and backlog" do + error = StandardError.new + allow(Bundler::FriendlyErrors).to receive(:issues_url).and_return("") + + expect(error).to receive(:class).at_least(:once) + expect(error).to receive(:message).at_least(:once) + expect(error).to receive(:backtrace).at_least(:once) + Bundler::FriendlyErrors.request_issue_report_for(error) + end + end + + describe "#issues_url" do + it "generates a search URL for the exception message" do + exception = Exception.new("Exception message") + + expect(Bundler::FriendlyErrors.issues_url(exception)).to eq("https://github.com/bundler/bundler/search?q=Exception+message&type=Issues") + end + + it "generates a search URL for only the first line of a multi-line exception message" do + exception = Exception.new(<<END) +First line of the exception message +Second line of the exception message +END + + expect(Bundler::FriendlyErrors.issues_url(exception)).to eq("https://github.com/bundler/bundler/search?q=First+line+of+the+exception+message&type=Issues") + end + + it "generates the url without colons" do + exception = Exception.new(<<END) +Exception ::: with ::: colons ::: +END + issues_url = Bundler::FriendlyErrors.issues_url(exception) + expect(issues_url).not_to include("%3A") + expect(issues_url).to eq("https://github.com/bundler/bundler/search?q=#{CGI.escape("Exception with colons ")}&type=Issues") + end + + it "removes information after - for Errono::EACCES" do + exception = Exception.new(<<END) +Errno::EACCES: Permission denied @ dir_s_mkdir - /Users/foo/bar/ +END + allow(exception).to receive(:is_a?).with(Errno).and_return(true) + issues_url = Bundler::FriendlyErrors.issues_url(exception) + expect(issues_url).not_to include("/Users/foo/bar") + expect(issues_url).to eq("https://github.com/bundler/bundler/search?q=#{CGI.escape("Errno EACCES Permission denied @ dir_s_mkdir ")}&type=Issues") + end + end +end diff --git a/spec/bundler/bundler/gem_helper_spec.rb b/spec/bundler/bundler/gem_helper_spec.rb new file mode 100644 index 0000000000..a627129fe3 --- /dev/null +++ b/spec/bundler/bundler/gem_helper_spec.rb @@ -0,0 +1,351 @@ +# frozen_string_literal: true + +require "rake" +require "bundler/gem_helper" + +RSpec.describe Bundler::GemHelper do + let(:app_name) { "lorem__ipsum" } + let(:app_path) { bundled_app app_name } + let(:app_gemspec_path) { app_path.join("#{app_name}.gemspec") } + + before(:each) do + global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false" + bundle "gem #{app_name}" + end + + context "determining gemspec" do + subject { Bundler::GemHelper.new(app_path) } + + context "fails" do + it "when there is no gemspec" do + FileUtils.rm app_gemspec_path + expect { subject }.to raise_error(/Unable to determine name/) + end + + it "when there are two gemspecs and the name isn't specified" do + FileUtils.touch app_path.join("#{app_name}-2.gemspec") + expect { subject }.to raise_error(/Unable to determine name/) + end + end + + context "interpolates the name" do + before do + # Remove exception that prevents public pushes on older RubyGems versions + if Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.0") + content = File.read(app_gemspec_path) + content.sub!(/raise "RubyGems 2\.0 or newer.*/, "") + File.open(app_gemspec_path, "w") {|f| f.write(content) } + end + end + + it "when there is only one gemspec" do + expect(subject.gemspec.name).to eq(app_name) + end + + it "for a hidden gemspec" do + FileUtils.mv app_gemspec_path, app_path.join(".gemspec") + expect(subject.gemspec.name).to eq(app_name) + end + end + + it "handles namespaces and converts them to CamelCase" do + bundle "gem #{app_name}-foo_bar" + underscore_path = bundled_app "#{app_name}-foo_bar" + + lib = underscore_path.join("lib/#{app_name}/foo_bar.rb").read + expect(lib).to include("module LoremIpsum") + expect(lib).to include("module FooBar") + end + end + + context "gem management" do + def mock_confirm_message(message) + expect(Bundler.ui).to receive(:confirm).with(message) + end + + def mock_build_message(name, version) + message = "#{name} #{version} built to pkg/#{name}-#{version}.gem." + mock_confirm_message message + end + + subject! { Bundler::GemHelper.new(app_path) } + let(:app_version) { "0.1.0" } + let(:app_gem_dir) { app_path.join("pkg") } + let(:app_gem_path) { app_gem_dir.join("#{app_name}-#{app_version}.gem") } + let(:app_gemspec_content) { remove_push_guard(File.read(app_gemspec_path)) } + + before(:each) do + content = app_gemspec_content.gsub("TODO: ", "") + content.sub!(/homepage\s+= ".*"/, 'homepage = ""') + content.gsub!(/spec\.metadata.+\n/, "") + File.open(app_gemspec_path, "w") {|file| file << content } + end + + def remove_push_guard(gemspec_content) + # Remove exception that prevents public pushes on older RubyGems versions + if Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.0") + gemspec_content.sub!(/raise "RubyGems 2\.0 or newer.*/, "") + end + gemspec_content + end + + it "uses a shell UI for output" do + expect(Bundler.ui).to be_a(Bundler::UI::Shell) + end + + describe "#install" do + let!(:rake_application) { Rake.application } + + before(:each) do + Rake.application = Rake::Application.new + end + + after(:each) do + Rake.application = rake_application + end + + context "defines Rake tasks" do + let(:task_names) do + %w[build install release release:guard_clean + release:source_control_push release:rubygem_push] + end + + context "before installation" do + it "raises an error with appropriate message" do + task_names.each do |name| + expect { Rake.application[name] }. + to raise_error(/^Don't know how to build task '#{name}'/) + end + end + end + + context "after installation" do + before do + subject.install + end + + it "adds Rake tasks successfully" do + task_names.each do |name| + expect { Rake.application[name] }.not_to raise_error + expect(Rake.application[name]).to be_instance_of Rake::Task + end + end + + it "provides a way to access the gemspec object" do + expect(subject.gemspec.name).to eq(app_name) + end + end + end + end + + describe "#build_gem" do + context "when build failed" do + it "raises an error with appropriate message" do + # break the gemspec by adding back the TODOs + File.open(app_gemspec_path, "w") {|file| file << app_gemspec_content } + expect { subject.build_gem }.to raise_error(/TODO/) + end + end + + context "when build was successful" do + it "creates .gem file" do + mock_build_message app_name, app_version + subject.build_gem + expect(app_gem_path).to exist + end + end + end + + describe "#install_gem" do + context "when installation was successful" do + it "gem is installed" do + mock_build_message app_name, app_version + mock_confirm_message "#{app_name} (#{app_version}) installed." + subject.install_gem(nil, :local) + expect(app_gem_path).to exist + gem_command! :list + expect(out).to include("#{app_name} (#{app_version})") + end + end + + context "when installation fails" do + it "raises an error with appropriate message" do + # create empty gem file in order to simulate install failure + allow(subject).to receive(:build_gem) do + FileUtils.mkdir_p(app_gem_dir) + FileUtils.touch app_gem_path + app_gem_path + end + expect { subject.install_gem }.to raise_error(/Couldn't install gem/) + end + end + end + + describe "rake release" do + let!(:rake_application) { Rake.application } + + before(:each) do + Rake.application = Rake::Application.new + subject.install + end + + after(:each) do + Rake.application = rake_application + end + + before do + Dir.chdir(app_path) do + `git init` + `git config user.email "you@example.com"` + `git config user.name "name"` + `git config push.default simple` + end + + # silence messages + allow(Bundler.ui).to receive(:confirm) + allow(Bundler.ui).to receive(:error) + end + + context "fails" do + it "when there are unstaged files" do + expect { Rake.application["release"].invoke }. + to raise_error("There are files that need to be committed first.") + end + + it "when there are uncommitted files" do + Dir.chdir(app_path) { `git add .` } + 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 + Dir.chdir(app_path) { `git commit -a -m "initial commit"` } + expect { Rake.application["release"].invoke }.to raise_error(RuntimeError) + end + end + + context "succeeds" do + before do + Dir.chdir(gem_repo1) { `git init --bare` } + Dir.chdir(app_path) do + `git remote add origin file://#{gem_repo1}` + `git commit -a -m "initial commit"` + end + end + + it "on releasing" do + mock_build_message app_name, app_version + mock_confirm_message "Tagged v#{app_version}." + mock_confirm_message "Pushed git commits and tags." + expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s) + + Dir.chdir(app_path) { sys_exec("git push -u origin master") } + + Rake.application["release"].invoke + end + + it "even if tag already exists" do + mock_build_message app_name, app_version + mock_confirm_message "Tag v#{app_version} has already been created." + expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s) + + Dir.chdir(app_path) do + `git tag -a -m \"Version #{app_version}\" v#{app_version}` + end + + Rake.application["release"].invoke + end + end + end + + describe "release:rubygem_push" do + let!(:rake_application) { Rake.application } + + before(:each) do + Rake.application = Rake::Application.new + subject.install + allow(subject).to receive(:sh) + end + + after(:each) do + Rake.application = rake_application + end + + before do + Dir.chdir(app_path) do + `git init` + `git config user.email "you@example.com"` + `git config user.name "name"` + `git config push.default simple` + end + + # silence messages + allow(Bundler.ui).to receive(:confirm) + allow(Bundler.ui).to receive(:error) + + credentials = double("credentials", "file?" => true) + allow(Bundler.user_home).to receive(:join). + with(".gem/credentials").and_return(credentials) + end + + describe "success messaging" do + context "No allowed_push_host set" do + before do + allow(subject).to receive(:allowed_push_host).and_return(nil) + end + + around do |example| + orig_host = ENV["RUBYGEMS_HOST"] + ENV["RUBYGEMS_HOST"] = rubygems_host_env + + example.run + + ENV["RUBYGEMS_HOST"] = orig_host + end + + context "RUBYGEMS_HOST env var is set" do + let(:rubygems_host_env) { "https://custom.env.gemhost.com" } + + it "should report successful push to the host from the environment" do + mock_confirm_message "Pushed #{app_name} #{app_version} to #{rubygems_host_env}" + + Rake.application["release:rubygem_push"].invoke + end + end + + context "RUBYGEMS_HOST env var is not set" do + let(:rubygems_host_env) { nil } + + it "should report successful push to rubygems.org" do + mock_confirm_message "Pushed #{app_name} #{app_version} to rubygems.org" + + Rake.application["release:rubygem_push"].invoke + end + end + + context "RUBYGEMS_HOST env var is an empty string" do + let(:rubygems_host_env) { "" } + + it "should report successful push to rubygems.org" do + mock_confirm_message "Pushed #{app_name} #{app_version} to rubygems.org" + + Rake.application["release:rubygem_push"].invoke + end + end + end + + context "allowed_push_host set in gemspec" do + before do + allow(subject).to receive(:allowed_push_host).and_return("https://my.gemhost.com") + end + + it "should report successful push to the allowed gem host" do + mock_confirm_message "Pushed #{app_name} #{app_version} to https://my.gemhost.com" + + Rake.application["release:rubygem_push"].invoke + end + end + end + end + end +end diff --git a/spec/bundler/bundler/gem_version_promoter_spec.rb b/spec/bundler/bundler/gem_version_promoter_spec.rb new file mode 100644 index 0000000000..01e0232fba --- /dev/null +++ b/spec/bundler/bundler/gem_version_promoter_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::GemVersionPromoter do + context "conservative resolver" do + def versions(result) + result.flatten.map(&:version).map(&:to_s) + 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 + 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 + 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 + end + + def build_spec_groups(name, versions) + versions.map do |v| + Bundler::Resolver::SpecGroup.new(build_spec(name, v)) + 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 + + 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 + + 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] + 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] + 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] + 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 + + 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] + 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] + 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] + 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] + end + end + + context "level error handling" do + subject { Bundler::GemVersionPromoter.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 symbols" do + [:major, :minor, :patch].each do |value| + subject.level = value + expect(subject.level).to eq value + 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 + end + end + + context "debug output" do + it "should not kerblooie on its own debug output" do + gvp = unlocking(:level => :patch) + dep = Bundler::DepProxy.new(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 + end + end + end +end diff --git a/spec/bundler/bundler/index_spec.rb b/spec/bundler/bundler/index_spec.rb new file mode 100644 index 0000000000..0f3f6e4944 --- /dev/null +++ b/spec/bundler/bundler/index_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Index do + let(:specs) { [] } + subject { described_class.build {|i| i.use(specs) } } + + context "specs with a nil platform" do + let(:spec) do + Gem::Specification.new do |s| + s.name = "json" + s.version = "1.8.3" + allow(s).to receive(:platform).and_return(nil) + end + end + let(:specs) { [spec] } + + describe "#search_by_spec" do + it "finds the spec when a nil platform is specified" do + expect(subject.search(spec)).to eq([spec]) + end + + it "finds the spec when a ruby platform is specified" do + query = spec.dup.tap {|s| s.platform = "ruby" } + expect(subject.search(query)).to eq([spec]) + end + end + end + + context "with specs that include development dependencies" do + let(:specs) { [*build_spec("a", "1.0.0") {|s| s.development("b", "~> 1.0") }] } + + it "does not include b in #dependency_names" do + expect(subject.dependency_names).not_to include("b") + end + end +end diff --git a/spec/bundler/bundler/installer/gem_installer_spec.rb b/spec/bundler/bundler/installer/gem_installer_spec.rb new file mode 100644 index 0000000000..7340a3acc0 --- /dev/null +++ b/spec/bundler/bundler/installer/gem_installer_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "bundler/installer/gem_installer" + +RSpec.describe Bundler::GemInstaller do + let(:installer) { instance_double("Installer") } + let(:spec_source) { instance_double("SpecSource") } + 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", :rubygems => ">= 2" do + allow(spec_source).to receive(:install).with(spec, :force => false, :ensure_builtin_gems_cached => false, :build_args => []) + subject.install_from_spec + end + end + + context "spec_settings is build option" do + it "invokes install method with build_args", :rubygems => ">= 2" do + allow(Bundler.settings).to receive(:[]).with(:bin) + 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"]) + subject.install_from_spec + end + end +end diff --git a/spec/bundler/bundler/installer/parallel_installer_spec.rb b/spec/bundler/bundler/installer/parallel_installer_spec.rb new file mode 100644 index 0000000000..ace5c1a23a --- /dev/null +++ b/spec/bundler/bundler/installer/parallel_installer_spec.rb @@ -0,0 +1,47 @@ +# 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 +end diff --git a/spec/bundler/bundler/installer/spec_installation_spec.rb b/spec/bundler/bundler/installer/spec_installation_spec.rb new file mode 100644 index 0000000000..a9cf09a372 --- /dev/null +++ b/spec/bundler/bundler/installer/spec_installation_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "bundler/installer/parallel_installer" + +RSpec.describe Bundler::ParallelInstaller::SpecInstallation do + let!(:dep) do + a_spec = Object.new + def a_spec.name + "I like tests" + end + a_spec + end + + describe "#ready_to_enqueue?" do + context "when in enqueued state" do + it "is falsey" do + spec = described_class.new(dep) + spec.state = :enqueued + expect(spec.ready_to_enqueue?).to be_falsey + end + end + + context "when in installed state" do + it "returns falsey" do + spec = described_class.new(dep) + spec.state = :installed + expect(spec.ready_to_enqueue?).to be_falsey + end + end + + it "returns truthy" do + spec = described_class.new(dep) + expect(spec.ready_to_enqueue?).to be_truthy + end + end + + describe "#dependencies_installed?" 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)] + spec = described_class.new(dep) + allow(spec).to receive(:all_dependencies).and_return(dependencies) + expect(spec.dependencies_installed?(all_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)] + spec = described_class.new(dep) + allow(spec).to receive(:all_dependencies).and_return(dependencies) + expect(spec.dependencies_installed?(all_specs)).to be_falsey + end + end + end +end diff --git a/spec/bundler/bundler/lockfile_parser_spec.rb b/spec/bundler/bundler/lockfile_parser_spec.rb new file mode 100644 index 0000000000..3a6d61336f --- /dev/null +++ b/spec/bundler/bundler/lockfile_parser_spec.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require "bundler/lockfile_parser" + +RSpec.describe Bundler::LockfileParser do + let(:lockfile_contents) { strip_whitespace(<<-L) } + GIT + remote: https://github.com/alloy/peiji-san.git + revision: eca485d8dc95f12aaec1a434b49d295c7e91844b + specs: + peiji-san (1.2.0) + + GEM + remote: https://rubygems.org/ + specs: + rake (10.3.2) + + PLATFORMS + ruby + + DEPENDENCIES + peiji-san! + rake + + RUBY VERSION + ruby 2.1.3p242 + + BUNDLED WITH + 1.12.0.rc.2 + L + + describe ".sections_in_lockfile" 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" + ) + end + end + + describe ".unknown_sections_in_lockfile" do + let(:lockfile_contents) { strip_whitespace(<<-L) } + UNKNOWN ATTR + + UNKNOWN ATTR 2 + random contents + L + + it "returns the unknown attributes" do + attributes = described_class.unknown_sections_in_lockfile(lockfile_contents) + expect(attributes).to contain_exactly("UNKNOWN ATTR", "UNKNOWN ATTR 2") + end + end + + describe ".sections_to_ignore" do + subject { described_class.sections_to_ignore(base_version) } + + context "with a nil base version" do + let(:base_version) { nil } + + it "returns the same as > 1.0" do + expect(subject).to contain_exactly( + described_class::BUNDLED, described_class::RUBY, described_class::PLUGIN + ) + end + end + + context "with a prerelease base version" do + let(:base_version) { Gem::Version.create("1.11.0.rc.1") } + + it "returns the same as for the release version" do + expect(subject).to contain_exactly( + described_class::RUBY, described_class::PLUGIN + ) + end + end + + context "with a current version" do + let(:base_version) { Gem::Version.create(Bundler::VERSION) } + + it "returns an empty array" do + expect(subject).to eq([]) + end + end + + context "with a future version" do + let(:base_version) { Gem::Version.create("5.5.5") } + + it "returns an empty array" do + expect(subject).to eq([]) + end + end + end + + describe "#initialize" do + before { allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app("gems.rb")) } + subject { described_class.new(lockfile_contents) } + + let(:sources) do + [Bundler::Source::Git.new("uri" => "https://github.com/alloy/peiji-san.git", "revision" => "eca485d8dc95f12aaec1a434b49d295c7e91844b"), + Bundler::Source::Rubygems.new("remotes" => ["https://rubygems.org"])] + end + let(:dependencies) do + { + "peiji-san" => Bundler::Dependency.new("peiji-san", ">= 0"), + "rake" => Bundler::Dependency.new("rake", ">= 0"), + } + end + let(:specs) do + [ + Bundler::LazySpecification.new("peiji-san", v("1.2.0"), rb), + Bundler::LazySpecification.new("rake", v("10.3.2"), rb), + ] + end + let(:platforms) { [rb] } + let(:bundler_version) { Gem::Version.new("1.12.0.rc.2") } + let(:ruby_version) { "ruby 2.1.3p242" } + + shared_examples_for "parsing" do + it "parses correctly" do + expect(subject.sources).to eq sources + expect(subject.dependencies).to eq dependencies + expect(subject.specs).to eq specs + expect(Hash[subject.specs.map {|s| [s, s.dependencies] }]).to eq Hash[subject.specs.map {|s| [s, s.dependencies] }] + expect(subject.platforms).to eq platforms + expect(subject.bundler_version).to eq bundler_version + expect(subject.ruby_version).to eq ruby_version + end + end + + include_examples "parsing" + + context "when an extra section is at the end" do + let(:lockfile_contents) { super() + "\n\nFOO BAR\n baz\n baa\n qux\n" } + include_examples "parsing" + end + + context "when an extra section is at the start" do + let(:lockfile_contents) { "FOO BAR\n baz\n baa\n qux\n\n" + super() } + include_examples "parsing" + end + + context "when an extra section is in the middle" do + let(:lockfile_contents) { super().split(/(?=GEM)/).insert(1, "FOO BAR\n baz\n baa\n qux\n\n").join } + include_examples "parsing" + end + + context "when a dependency has options" do + let(:lockfile_contents) { super().sub("peiji-san!", "peiji-san!\n foo: bar") } + include_examples "parsing" + end + end +end diff --git a/spec/bundler/bundler/mirror_spec.rb b/spec/bundler/bundler/mirror_spec.rb new file mode 100644 index 0000000000..acd0895f2f --- /dev/null +++ b/spec/bundler/bundler/mirror_spec.rb @@ -0,0 +1,329 @@ +# frozen_string_literal: true + +require "bundler/mirror" + +RSpec.describe Bundler::Settings::Mirror do + let(:mirror) { Bundler::Settings::Mirror.new } + + it "returns zero when fallback_timeout is not set" do + expect(mirror.fallback_timeout).to eq(0) + end + + it "takes a number as a fallback_timeout" do + mirror.fallback_timeout = 1 + expect(mirror.fallback_timeout).to eq(1) + end + + it "takes truthy as a default fallback timeout" do + mirror.fallback_timeout = true + expect(mirror.fallback_timeout).to eq(0.1) + end + + it "takes falsey as a zero fallback timeout" do + mirror.fallback_timeout = false + expect(mirror.fallback_timeout).to eq(0) + end + + it "takes a string with 'true' as a default fallback timeout" do + mirror.fallback_timeout = "true" + expect(mirror.fallback_timeout).to eq(0.1) + end + + it "takes a string with 'false' as a zero fallback timeout" do + mirror.fallback_timeout = "false" + expect(mirror.fallback_timeout).to eq(0) + end + + it "takes a string for the uri but returns an uri object" do + mirror.uri = "http://localhost:9292" + expect(mirror.uri).to eq(URI("http://localhost:9292")) + end + + it "takes an uri object for the uri" do + mirror.uri = URI("http://localhost:9293") + expect(mirror.uri).to eq(URI("http://localhost:9293")) + end + + context "without a uri" do + it "invalidates the mirror" do + mirror.validate! + expect(mirror.valid?).to be_falsey + end + end + + context "with an uri" do + before { mirror.uri = "http://localhost:9292" } + + context "without a fallback timeout" do + it "is not valid by default" do + expect(mirror.valid?).to be_falsey + end + + context "when probed" do + let(:probe) { double } + + context "with a replying mirror" do + before do + allow(probe).to receive(:replies?).and_return(true) + mirror.validate!(probe) + end + + it "is valid" do + expect(mirror.valid?).to be_truthy + end + end + + context "with a non replying mirror" do + before do + allow(probe).to receive(:replies?).and_return(false) + mirror.validate!(probe) + end + + it "is still valid" do + expect(mirror.valid?).to be_truthy + end + end + end + end + + context "with a fallback timeout" do + before { mirror.fallback_timeout = 1 } + + it "is not valid by default" do + expect(mirror.valid?).to be_falsey + end + + context "when probed" do + let(:probe) { double } + + context "with a replying mirror" do + before do + allow(probe).to receive(:replies?).and_return(true) + mirror.validate!(probe) + end + + it "is valid" do + expect(mirror.valid?).to be_truthy + end + + it "is validated only once" do + allow(probe).to receive(:replies?).and_raise("Only once!") + mirror.validate!(probe) + expect(mirror.valid?).to be_truthy + end + end + + context "with a non replying mirror" do + before do + allow(probe).to receive(:replies?).and_return(false) + mirror.validate!(probe) + end + + it "is not valid" do + expect(mirror.valid?).to be_falsey + end + + it "is validated only once" do + allow(probe).to receive(:replies?).and_raise("Only once!") + mirror.validate!(probe) + expect(mirror.valid?).to be_falsey + end + end + end + end + + describe "#==" do + it "returns true if uri and fallback timeout are the same" do + uri = "https://ruby.taobao.org" + mirror = Bundler::Settings::Mirror.new(uri, 1) + another_mirror = Bundler::Settings::Mirror.new(uri, 1) + + expect(mirror == another_mirror).to be true + end + end + end +end + +RSpec.describe Bundler::Settings::Mirrors do + let(:localhost_uri) { URI("http://localhost:9292") } + + context "with a just created mirror" do + let(:mirrors) do + probe = double + allow(probe).to receive(:replies?).and_return(true) + Bundler::Settings::Mirrors.new(probe) + end + + it "returns a mirror that contains the source uri for an unknown uri" do + mirror = mirrors.for("http://rubygems.org/") + expect(mirror).to eq(Bundler::Settings::Mirror.new("http://rubygems.org/")) + end + + it "parses a mirror key and returns a mirror for the parsed uri" do + mirrors.parse("mirror.http://rubygems.org/", localhost_uri) + expect(mirrors.for("http://rubygems.org/").uri).to eq(localhost_uri) + end + + it "parses a relative mirror key and returns a mirror for the parsed http uri" do + mirrors.parse("mirror.rubygems.org", localhost_uri) + expect(mirrors.for("http://rubygems.org/").uri).to eq(localhost_uri) + end + + it "parses a relative mirror key and returns a mirror for the parsed https uri" do + mirrors.parse("mirror.rubygems.org", localhost_uri) + expect(mirrors.for("https://rubygems.org/").uri).to eq(localhost_uri) + end + + context "with a uri parsed already" do + before { mirrors.parse("mirror.http://rubygems.org/", localhost_uri) } + + it "takes a mirror fallback_timeout and assigns the timeout" do + mirrors.parse("mirror.http://rubygems.org.fallback_timeout", "2") + expect(mirrors.for("http://rubygems.org/").fallback_timeout).to eq(2) + end + + it "parses a 'true' fallback timeout and sets the default timeout" do + mirrors.parse("mirror.http://rubygems.org.fallback_timeout", "true") + expect(mirrors.for("http://rubygems.org/").fallback_timeout).to eq(0.1) + end + + it "parses a 'false' fallback timeout and sets it to zero" do + mirrors.parse("mirror.http://rubygems.org.fallback_timeout", "false") + expect(mirrors.for("http://rubygems.org/").fallback_timeout).to eq(0) + end + end + end + + context "with a mirror prober that replies on time" do + let(:mirrors) do + probe = double + allow(probe).to receive(:replies?).and_return(true) + Bundler::Settings::Mirrors.new(probe) + end + + context "with a default fallback_timeout for rubygems.org" do + before do + mirrors.parse("mirror.http://rubygems.org/", localhost_uri) + mirrors.parse("mirror.http://rubygems.org.fallback_timeout", "true") + end + + it "returns localhost" do + expect(mirrors.for("http://rubygems.org").uri).to eq(localhost_uri) + end + end + + context "with a mirror for all" do + before do + mirrors.parse("mirror.all", localhost_uri) + end + + context "without a fallback timeout" do + it "returns localhost uri for rubygems" do + expect(mirrors.for("http://rubygems.org").uri).to eq(localhost_uri) + end + + it "returns localhost for any other url" do + expect(mirrors.for("http://whatever.com/").uri).to eq(localhost_uri) + end + end + context "with a fallback timeout" do + before { mirrors.parse("mirror.all.fallback_timeout", "1") } + + it "returns localhost uri for rubygems" do + expect(mirrors.for("http://rubygems.org").uri).to eq(localhost_uri) + end + + it "returns localhost for any other url" do + expect(mirrors.for("http://whatever.com/").uri).to eq(localhost_uri) + end + end + end + end + + context "with a mirror prober that does not reply on time" do + let(:mirrors) do + probe = double + allow(probe).to receive(:replies?).and_return(false) + Bundler::Settings::Mirrors.new(probe) + end + + context "with a localhost mirror for all" do + before { mirrors.parse("mirror.all", localhost_uri) } + + context "without a fallback timeout" do + it "returns localhost" do + expect(mirrors.for("http://whatever.com").uri).to eq(localhost_uri) + end + end + + context "with a fallback timeout" 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(URI("http://whatever.com/")) + end + end + end + + context "with localhost as a mirror for rubygems.org" do + before { mirrors.parse("mirror.http://rubygems.org/", localhost_uri) } + + context "without a fallback timeout" do + it "returns the uri that is not mirrored" do + expect(mirrors.for("http://whatever.com").uri).to eq(URI("http://whatever.com/")) + end + + it "returns localhost for rubygems.org" do + expect(mirrors.for("http://rubygems.org/").uri).to eq(localhost_uri) + end + end + + context "with a fallback timeout" 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(URI("http://whatever.com/")) + end + + it "returns rubygems.org for rubygems.org" do + expect(mirrors.for("http://rubygems.org/").uri).to eq(URI("http://rubygems.org/")) + end + end + end + end +end + +RSpec.describe Bundler::Settings::TCPSocketProbe do + let(:probe) { Bundler::Settings::TCPSocketProbe.new } + + context "with a listening TCP Server" do + def with_server_and_mirror + server = TCPServer.new("127.0.0.1", 0) + mirror = Bundler::Settings::Mirror.new("http://localhost:#{server.addr[1]}", 1) + yield server, mirror + server.close unless server.closed? + end + + it "probes the server correctly", :ruby_repo do + with_server_and_mirror do |server, mirror| + expect(server.closed?).to be_falsey + expect(probe.replies?(mirror)).to be_truthy + end + end + + it "probes falsey when the server is down" do + with_server_and_mirror do |server, mirror| + server.close + expect(probe.replies?(mirror)).to be_falsey + end + end + end + + context "with an invalid mirror" do + let(:mirror) { Bundler::Settings::Mirror.new("http://127.0.0.127:9292", true) } + + it "fails with a timeout when there is nothing to tcp handshake" do + expect(probe.replies?(mirror)).to be_falsey + end + end +end diff --git a/spec/bundler/bundler/plugin/api/source_spec.rb b/spec/bundler/bundler/plugin/api/source_spec.rb new file mode 100644 index 0000000000..2c50ff56a4 --- /dev/null +++ b/spec/bundler/bundler/plugin/api/source_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Plugin::API::Source do + let(:uri) { "uri://to/test" } + let(:type) { "spec_type" } + + subject(:source) do + klass = Class.new + klass.send :include, Bundler::Plugin::API::Source + klass.new("uri" => uri, "type" => type) + end + + describe "attributes" do + it "allows access to uri" do + expect(source.uri).to eq("uri://to/test") + end + + it "allows access to name" do + expect(source.name).to eq("spec_type at uri://to/test") + end + end + + context "post_install" do + let(:installer) { double(:installer) } + + before do + allow(Bundler::Source::Path::Installer).to receive(:new) { installer } + end + + it "calls Path::Installer's post_install" do + expect(installer).to receive(:post_install).once + + source.post_install(double(:spec)) + end + end + + context "install_path" do + let(:uri) { "uri://to/a/repository-name" } + let(:hash) { Digest(:SHA1).hexdigest(uri) } + let(:install_path) { Pathname.new "/bundler/install/path" } + + before do + allow(Bundler).to receive(:install_path) { install_path } + end + + it "returns basename with uri_hash" do + expected = Pathname.new "#{install_path}/repository-name-#{hash[0..11]}" + expect(source.install_path).to eq(expected) + end + end + + context "to_lock" do + it "returns the string with remote and type" do + expected = strip_whitespace <<-L + PLUGIN SOURCE + remote: #{uri} + type: #{type} + specs: + L + + expect(source.to_lock).to eq(expected) + end + + context "with additional options to lock" do + before do + allow(source).to receive(:options_to_lock) { { "first" => "option" } } + end + + it "includes them" do + expected = strip_whitespace <<-L + PLUGIN SOURCE + remote: #{uri} + type: #{type} + first: option + specs: + L + + expect(source.to_lock).to eq(expected) + end + end + end +end diff --git a/spec/bundler/bundler/plugin/api_spec.rb b/spec/bundler/bundler/plugin/api_spec.rb new file mode 100644 index 0000000000..58fb908572 --- /dev/null +++ b/spec/bundler/bundler/plugin/api_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Plugin::API do + context "plugin declarations" do + before do + stub_const "UserPluginClass", Class.new(Bundler::Plugin::API) + end + + describe "#command" do + it "declares a command plugin with same class as handler" do + expect(Bundler::Plugin). + to receive(:add_command).with("meh", UserPluginClass).once + + UserPluginClass.command "meh" + end + + it "accepts another class as argument that handles the command" do + stub_const "NewClass", Class.new + expect(Bundler::Plugin).to receive(:add_command).with("meh", NewClass).once + + UserPluginClass.command "meh", NewClass + end + end + + describe "#source" do + it "declares a source plugin with same class as handler" do + expect(Bundler::Plugin). + to receive(:add_source).with("a_source", UserPluginClass).once + + UserPluginClass.source "a_source" + end + + it "accepts another class as argument that handles the command" do + stub_const "NewClass", Class.new + expect(Bundler::Plugin).to receive(:add_source).with("a_source", NewClass).once + + UserPluginClass.source "a_source", NewClass + end + end + + describe "#hook" do + it "accepts a block and passes it to Plugin module" do + foo = double("tester") + expect(foo).to receive(:called) + + expect(Bundler::Plugin).to receive(:add_hook).with("post-foo").and_yield + + Bundler::Plugin::API.hook("post-foo") { foo.called } + end + end + end + + context "bundler interfaces provided" do + before do + stub_const "UserPluginClass", Class.new(Bundler::Plugin::API) + end + + subject(:api) { UserPluginClass.new } + + # A test of delegation + it "provides the Bundler's functions" do + expect(Bundler).to receive(:an_unknown_function).once + + api.an_unknown_function + end + + it "includes Bundler::SharedHelpers' functions" do + expect(Bundler::SharedHelpers).to receive(:an_unknown_helper).once + + api.an_unknown_helper + end + + context "#tmp" do + it "provides a tmp dir" do + expect(api.tmp("mytmp")).to be_directory + end + + it "accepts multiple names for suffix" do + expect(api.tmp("myplugin", "download")).to be_directory + end + end + end +end diff --git a/spec/bundler/bundler/plugin/dsl_spec.rb b/spec/bundler/bundler/plugin/dsl_spec.rb new file mode 100644 index 0000000000..be23db3bba --- /dev/null +++ b/spec/bundler/bundler/plugin/dsl_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Plugin::DSL do + DSL = Bundler::Plugin::DSL + + subject(:dsl) { Bundler::Plugin::DSL.new } + + before do + allow(Bundler).to receive(:root) { Pathname.new "/" } + end + + describe "it ignores only the methods defined in Bundler::Dsl" do + it "doesn't raises error for Dsl methods" do + expect { dsl.install_if }.not_to raise_error + end + + it "raises error for other methods" do + expect { dsl.no_method }.to raise_error(DSL::PluginGemfileError) + end + end + + describe "source block" 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") {} + + expect(dsl.inferred_plugins).to eq(["bundler-source-news"]) + end + + it "registers a source type plugin only once for multiple declataions" 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") {} + end + end +end diff --git a/spec/bundler/bundler/plugin/events_spec.rb b/spec/bundler/bundler/plugin/events_spec.rb new file mode 100644 index 0000000000..b09e915682 --- /dev/null +++ b/spec/bundler/bundler/plugin/events_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Plugin::Events do + context "plugin events" do + describe "#define" do + it "raises when redefining a constant" do + expect do + Bundler::Plugin::Events.send(:define, :GEM_BEFORE_INSTALL_ALL, "another-value") + end.to raise_error(ArgumentError) + end + + it "can define a new constant" do + Bundler::Plugin::Events.send(:define, :NEW_CONSTANT, "value") + expect(Bundler::Plugin::Events::NEW_CONSTANT).to eq("value") + end + end + end +end diff --git a/spec/bundler/bundler/plugin/index_spec.rb b/spec/bundler/bundler/plugin/index_spec.rb new file mode 100644 index 0000000000..ca3476ea2a --- /dev/null +++ b/spec/bundler/bundler/plugin/index_spec.rb @@ -0,0 +1,186 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Plugin::Index do + Index = Bundler::Plugin::Index + + before do + gemfile "" + path = lib_path(plugin_name) + index.register_plugin("new-plugin", path.to_s, [path.join("lib").to_s], commands, sources, hooks) + end + + let(:plugin_name) { "new-plugin" } + let(:commands) { [] } + let(:sources) { [] } + let(:hooks) { [] } + + subject(:index) { Index.new } + + describe "#register plugin" do + it "is available for retrieval" do + expect(index.plugin_path(plugin_name)).to eq(lib_path(plugin_name)) + end + + it "load_paths is available for retrival" do + expect(index.load_paths(plugin_name)).to eq([lib_path(plugin_name).join("lib").to_s]) + end + + it "is persistent" do + new_index = Index.new + expect(new_index.plugin_path(plugin_name)).to eq(lib_path(plugin_name)) + end + + it "load_paths are persistent" do + new_index = Index.new + expect(new_index.load_paths(plugin_name)).to eq([lib_path(plugin_name).join("lib").to_s]) + end + end + + describe "commands" do + let(:commands) { ["newco"] } + + it "returns the plugins name on query" do + expect(index.command_plugin("newco")).to eq(plugin_name) + end + + it "raises error on conflict" do + expect do + index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, ["newco"], [], []) + end.to raise_error(Index::CommandConflict) + end + + it "is persistent" do + new_index = Index.new + expect(new_index.command_plugin("newco")).to eq(plugin_name) + end + end + + describe "source" do + let(:sources) { ["new_source"] } + + it "returns the plugins name on query" do + expect(index.source_plugin("new_source")).to eq(plugin_name) + end + + it "raises error on conflict" do + expect do + index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, [], ["new_source"], []) + end.to raise_error(Index::SourceConflict) + end + + it "is persistent" do + new_index = Index.new + expect(new_index.source_plugin("new_source")).to eq(plugin_name) + end + end + + describe "hook" do + let(:hooks) { ["after-bar"] } + + it "returns the plugins name on query" do + expect(index.hook_plugins("after-bar")).to include(plugin_name) + end + + it "is persistent" do + new_index = Index.new + expect(new_index.hook_plugins("after-bar")).to eq([plugin_name]) + end + + context "that are not registered", :focused do + let(:file) { double("index-file") } + + before do + index.hook_plugins("not-there") + allow(File).to receive(:open).and_yield(file) + end + + it "should not save it with next registered hook" do + expect(file).to receive(:puts) do |content| + expect(content).not_to include("not-there") + end + + index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, [], [], []) + end + end + end + + describe "global index" do + before do + Dir.chdir(tmp) do + Bundler::Plugin.reset! + path = lib_path("gplugin") + index.register_plugin("gplugin", path.to_s, [path.join("lib").to_s], [], ["glb_source"], []) + end + end + + it "skips sources" do + new_index = Index.new + expect(new_index.source_plugin("glb_source")).to be_falsy + end + end + + describe "after conflict" do + let(:commands) { ["foo"] } + let(:sources) { ["bar"] } + let(:hooks) { ["hoook"] } + + shared_examples "it cleans up" do + it "the path" do + expect(index.installed?("cplugin")).to be_falsy + end + + it "the command" do + expect(index.command_plugin("xfoo")).to be_falsy + end + + it "the source" do + expect(index.source_plugin("xbar")).to be_falsy + end + + it "the hook" do + expect(index.hook_plugins("xhoook")).to be_empty + end + end + + context "on command conflict it cleans up" do + before do + expect do + path = lib_path("cplugin") + index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["xbar"], ["xhoook"]) + end.to raise_error(Index::CommandConflict) + end + + include_examples "it cleans up" + end + + context "on source conflict it cleans up" do + before do + expect do + path = lib_path("cplugin") + index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["xfoo"], ["bar"], ["xhoook"]) + end.to raise_error(Index::SourceConflict) + end + + include_examples "it cleans up" + end + + context "on command and source conflict it cleans up" do + before do + expect do + path = lib_path("cplugin") + index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["bar"], ["xhoook"]) + end.to raise_error(Index::CommandConflict) + end + + include_examples "it cleans up" + end + end + + describe "readonly disk without home" do + it "ignores being unable to create temp home dir" do + expect_any_instance_of(Bundler::Plugin::Index).to receive(:global_index_file). + and_raise(Bundler::GenericSystemCallError.new("foo", "bar")) + Bundler::Plugin::Index.new + end + end +end diff --git a/spec/bundler/bundler/plugin/installer_spec.rb b/spec/bundler/bundler/plugin/installer_spec.rb new file mode 100644 index 0000000000..f8bf8450c9 --- /dev/null +++ b/spec/bundler/bundler/plugin/installer_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Plugin::Installer do + subject(:installer) { Bundler::Plugin::Installer.new } + + before do + # allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(Pathname.new("/Gemfile")) + end + + 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(installer).to receive(:install_rubygems). + with("new-plugin", [">= 0"], sources).once + + installer.install("new-plugin", {}) + end + + describe "with mocked installers" do + let(:spec) { double(:spec) } + it "returns the installed spec after installing git plugins" do + allow(installer).to receive(:install_git). + and_return("new-plugin" => spec) + + expect(installer.install(["new-plugin"], :git => "https://some.ran/dom")). + to eq("new-plugin" => spec) + end + + it "returns the installed spec after installing rubygems plugins" do + allow(installer).to receive(:install_rubygems). + and_return("new-plugin" => spec) + + expect(installer.install(["new-plugin"], :source => "https://some.ran/dom")). + to eq("new-plugin" => spec) + end + end + + describe "with actual installers" do + before do + build_repo2 do + build_plugin "re-plugin" + build_plugin "ma-plugin" + end + end + + context "git plugins" do + before do + 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://#{lib_path("ga-plugin")}") + end + + it "returns the installed spec after installing" do + spec = result["ga-plugin"] + expect(spec.full_name).to eq "ga-plugin-1.0" + end + + it "has expected full gem path" do + rev = revision_for(lib_path("ga-plugin")) + expect(result["ga-plugin"].full_gem_path). + to eq(Bundler::Plugin.root.join("bundler", "gems", "ga-plugin-#{rev[0..11]}").to_s) + end + end + + context "rubygems plugins" do + let(:result) do + installer.install(["re-plugin"], :source => "file://#{gem_repo2}") + end + + it "returns the installed spec after installing " do + expect(result["re-plugin"]).to be_kind_of(Bundler::RemoteSpecification) + end + + it "has expected full_gem)path" do + expect(result["re-plugin"].full_gem_path). + to eq(global_plugin_gem("re-plugin-1.0").to_s) + end + end + + context "multiple plugins" do + let(:result) do + installer.install(["re-plugin", "ma-plugin"], :source => "file://#{gem_repo2}") + end + + it "returns the installed spec after installing " do + expect(result["re-plugin"]).to be_kind_of(Bundler::RemoteSpecification) + expect(result["ma-plugin"]).to be_kind_of(Bundler::RemoteSpecification) + end + + it "has expected full_gem)path" do + expect(result["re-plugin"].full_gem_path).to eq(global_plugin_gem("re-plugin-1.0").to_s) + expect(result["ma-plugin"].full_gem_path).to eq(global_plugin_gem("ma-plugin-1.0").to_s) + end + end + end + end +end diff --git a/spec/bundler/bundler/plugin/source_list_spec.rb b/spec/bundler/bundler/plugin/source_list_spec.rb new file mode 100644 index 0000000000..64a1233dd1 --- /dev/null +++ b/spec/bundler/bundler/plugin/source_list_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Plugin::SourceList do + SourceList = Bundler::Plugin::SourceList + + before do + allow(Bundler).to receive(:root) { Pathname.new "/" } + end + + subject(:source_list) { SourceList.new } + + describe "adding sources uses classes for plugin" do + it "uses Plugin::Installer::Rubygems for rubygems sources" do + source = source_list. + add_rubygems_source("remotes" => ["https://existing-rubygems.org"]) + expect(source).to be_instance_of(Bundler::Plugin::Installer::Rubygems) + end + + it "uses Plugin::Installer::Git for git sources" do + source = source_list. + add_git_source("uri" => "git://existing-git.org/path.git") + expect(source).to be_instance_of(Bundler::Plugin::Installer::Git) + end + end +end diff --git a/spec/bundler/bundler/plugin_spec.rb b/spec/bundler/bundler/plugin_spec.rb new file mode 100644 index 0000000000..9266fad1eb --- /dev/null +++ b/spec/bundler/bundler/plugin_spec.rb @@ -0,0 +1,309 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Plugin do + Plugin = Bundler::Plugin + + let(:installer) { double(:installer) } + let(:index) { double(:index) } + let(:spec) { double(:spec) } + let(:spec2) { double(:spec2) } + + before do + 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| + s.write "plugins.rb" + end + + allow(spec).to receive(:full_gem_path). + and_return(lib_path("new-plugin").to_s) + allow(spec).to receive(:load_paths). + and_return([lib_path("new-plugin").join("lib").to_s]) + + allow(spec2).to receive(:full_gem_path). + and_return(lib_path("another-plugin").to_s) + allow(spec2).to receive(:load_paths). + and_return([lib_path("another-plugin").join("lib").to_s]) + + allow(Plugin::Installer).to receive(:new) { installer } + allow(Plugin).to receive(:index) { index } + allow(index).to receive(:register_plugin) + end + + describe "install command" do + let(:opts) { { "version" => "~> 1.0", "source" => "foo" } } + + before do + allow(installer).to receive(:install).with(["new-plugin"], opts) do + { "new-plugin" => spec } + end + end + + it "passes the name and options to installer" do + allow(installer).to receive(:install).with(["new-plugin"], opts) do + { "new-plugin" => spec } + end.once + + subject.install ["new-plugin"], opts + end + + it "validates the installed plugin" do + allow(subject). + to receive(:validate_plugin!).with(lib_path("new-plugin")).once + + subject.install ["new-plugin"], opts + end + + it "registers the plugin with index" do + allow(index).to receive(:register_plugin). + with("new-plugin", lib_path("new-plugin").to_s, [lib_path("new-plugin").join("lib").to_s], []).once + subject.install ["new-plugin"], opts + end + + context "multiple plugins" do + it do + allow(installer).to receive(:install). + with(["new-plugin", "another-plugin"], opts) do + { + "new-plugin" => spec, + "another-plugin" => spec2, + } + end.once + + allow(subject).to receive(:validate_plugin!).twice + allow(index).to receive(:register_plugin).twice + subject.install ["new-plugin", "another-plugin"], opts + end + end + end + + describe "evaluate gemfile for plugins" do + let(:definition) { double("definition") } + let(:builder) { double("builder") } + let(:gemfile) { bundled_app("Gemfile") } + + before do + allow(Plugin::DSL).to receive(:new) { builder } + allow(builder).to receive(:eval_gemfile).with(gemfile) + allow(builder).to receive(:to_definition) { definition } + allow(builder).to receive(:inferred_plugins) { [] } + end + + it "doesn't calls installer without any plugins" do + allow(definition).to receive(:dependencies) { [] } + allow(installer).to receive(:install_definition).never + + subject.gemfile_install(gemfile) + end + + context "with dependencies" do + let(:plugin_specs) do + { + "new-plugin" => spec, + "another-plugin" => spec2, + } + end + + before do + allow(index).to receive(:installed?) { nil } + allow(definition).to receive(:dependencies) { [Bundler::Dependency.new("new-plugin", ">=0"), Bundler::Dependency.new("another-plugin", ">=0")] } + allow(installer).to receive(:install_definition) { plugin_specs } + end + + it "should validate and register the plugins" do + expect(subject).to receive(:validate_plugin!).twice + expect(subject).to receive(:register_plugin).twice + + subject.gemfile_install(gemfile) + end + + it "should pass the optional plugins to #register_plugin" do + allow(builder).to receive(:inferred_plugins) { ["another-plugin"] } + + expect(subject).to receive(:register_plugin). + with("new-plugin", spec, false).once + + expect(subject).to receive(:register_plugin). + with("another-plugin", spec2, true).once + + subject.gemfile_install(gemfile) + end + end + end + + describe "#command?" do + it "returns true value for commands in index" do + allow(index). + to receive(:command_plugin).with("newcommand") { "my-plugin" } + result = subject.command? "newcommand" + expect(result).to be_truthy + end + + it "returns false value for commands not in index" do + allow(index).to receive(:command_plugin).with("newcommand") { nil } + result = subject.command? "newcommand" + expect(result).to be_falsy + end + end + + describe "#exec_command" do + it "raises UndefinedCommandError when command is not found" do + allow(index).to receive(:command_plugin).with("newcommand") { nil } + expect { subject.exec_command("newcommand", []) }. + to raise_error(Plugin::UndefinedCommandError) + end + end + + describe "#source?" do + it "returns true value for sources in index" do + allow(index). + to receive(:command_plugin).with("foo-source") { "my-plugin" } + result = subject.command? "foo-source" + expect(result).to be_truthy + end + + it "returns false value for source not in index" do + allow(index).to receive(:command_plugin).with("foo-source") { nil } + result = subject.command? "foo-source" + expect(result).to be_falsy + end + end + + describe "#source" do + it "raises UnknownSourceError when source is not found" do + allow(index).to receive(:source_plugin).with("bar") { nil } + expect { subject.source("bar") }. + to raise_error(Plugin::UnknownSourceError) + end + + it "loads the plugin, if not loaded" do + allow(index).to receive(:source_plugin).with("foo-bar") { "plugin_name" } + + expect(subject).to receive(:load_plugin).with("plugin_name") + subject.source("foo-bar") + end + + it "returns the class registered with #add_source" do + allow(index).to receive(:source_plugin).with("foo") { "plugin_name" } + stub_const "NewClass", Class.new + + subject.add_source("foo", NewClass) + expect(subject.source("foo")).to be(NewClass) + end + end + + describe "#source_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" } + + stub_const "SClass", Class.new + s_instance = double(:s_instance) + subject.add_source("l_source", SClass) + + 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) + end + end + + describe "#root" do + context "in app dir" do + before do + gemfile "" + end + + it "returns plugin dir in app .bundle path" do + expect(subject.root).to eq(bundled_app.join(".bundle/plugin")) + end + end + + context "outside app dir" do + it "returns plugin dir in global bundle path" do + Dir.chdir tmp + expect(subject.root).to eq(home.join(".bundle/plugin")) + end + end + end + + describe "#add_hook" do + it "raises an ArgumentError on an unregistered event" do + ran = false + expect do + Plugin.add_hook("unregistered-hook") { ran = true } + end.to raise_error(ArgumentError) + expect(ran).to be(false) + end + end + + describe "#hook" do + before do + path = lib_path("foo-plugin") + 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") + + allow(index).to receive(:hook_plugins).with(Bundler::Plugin::Events::EVENT_1). + and_return(["foo-plugin"]) + allow(index).to receive(:hook_plugins).with(Bundler::Plugin::Events::EVENT_2). + 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([]) + end + + let(:code) { <<-RUBY } + Bundler::Plugin::API.hook("event-1") { puts "hook for event 1" } + RUBY + + it "raises an ArgumentError on an unregistered event" do + expect do + Plugin.hook("unregistered-hook") + end.to raise_error(ArgumentError) + end + + it "executes the hook" do + out = capture(:stdout) do + Plugin.hook(Bundler::Plugin::Events::EVENT_1) + end.strip + + expect(out).to eq("hook for event 1") + 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) {} + puts "loaded" + RUBY + + it "evals plugins.rb once" do + out = capture(:stdout) do + Plugin.hook(Bundler::Plugin::Events::EVENT_1) + Plugin.hook(Bundler::Plugin::Events::EVENT_2) + end.strip + + expect(out).to eq("loaded") + end + end + + context "a block is passed" do + let(:code) { <<-RUBY } + Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT_1) { |&blk| blk.call } + RUBY + + it "is passed to the hook" do + out = capture(:stdout) do + Plugin.hook(Bundler::Plugin::Events::EVENT_1) { puts "win" } + end.strip + + expect(out).to eq("win") + end + end + end +end diff --git a/spec/bundler/bundler/psyched_yaml_spec.rb b/spec/bundler/bundler/psyched_yaml_spec.rb new file mode 100644 index 0000000000..d5d68c5cc3 --- /dev/null +++ b/spec/bundler/bundler/psyched_yaml_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "bundler/psyched_yaml" + +RSpec.describe "Bundler::YamlLibrarySyntaxError" do + it "is raised on YAML parse errors" do + expect { YAML.parse "{foo" }.to raise_error(Bundler::YamlLibrarySyntaxError) + end +end diff --git a/spec/bundler/bundler/remote_specification_spec.rb b/spec/bundler/bundler/remote_specification_spec.rb new file mode 100644 index 0000000000..8115e026d8 --- /dev/null +++ b/spec/bundler/bundler/remote_specification_spec.rb @@ -0,0 +1,187 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::RemoteSpecification do + let(:name) { "foo" } + let(:version) { Gem::Version.new("1.0.0") } + let(:platform) { Gem::Platform::RUBY } + let(:spec_fetcher) { double(:spec_fetcher) } + + subject { described_class.new(name, version, platform, spec_fetcher) } + + it "is Comparable" do + expect(described_class.ancestors).to include(Comparable) + end + + it "can match platforms" do + expect(described_class.ancestors).to include(Bundler::MatchPlatform) + end + + describe "#fetch_platform" do + let(:remote_spec) { double(:remote_spec, :platform => "jruby") } + + before { allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec) } + + it "should return the spec platform" do + expect(subject.fetch_platform).to eq("jruby") + end + end + + describe "#full_name" do + context "when platform is ruby" do + it "should return the spec name and version" do + expect(subject.full_name).to eq("foo-1.0.0") + end + end + + context "when platform is nil" do + let(:platform) { nil } + + it "should return the spec name and version" do + expect(subject.full_name).to eq("foo-1.0.0") + end + end + + context "when platform is a non-ruby platform" 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") + end + end + end + + describe "#<=>" do + let(:other_name) { name } + let(:other_version) { version } + let(:other_platform) { platform } + let(:other_spec_fetcher) { spec_fetcher } + + shared_examples_for "a comparison" do + context "which exactly matches" do + it "returns 0" do + expect(subject <=> other).to eq(0) + end + end + + context "which is different by name" do + let(:other_name) { "a" } + it "returns 1" do + expect(subject <=> other).to eq(1) + end + end + + context "which has a lower version" do + let(:other_version) { Gem::Version.new("0.9.0") } + it "returns 1" do + expect(subject <=> other).to eq(1) + end + end + + context "which has a higher version" do + let(:other_version) { Gem::Version.new("1.1.0") } + it "returns -1" do + expect(subject <=> other).to eq(-1) + end + end + + context "which has a different platform" do + let(:other_platform) { Gem::Platform.new("x86-mswin32") } + it "returns -1" do + expect(subject <=> other).to eq(-1) + end + end + end + + context "comparing another Bundler::RemoteSpecification" do + let(:other) do + Bundler::RemoteSpecification.new(other_name, other_version, + other_platform, nil) + end + + it_should_behave_like "a comparison" + end + + context "comparing a Gem::Specification" do + let(:other) do + Gem::Specification.new(other_name, other_version).tap do |s| + s.platform = other_platform + end + end + + it_should_behave_like "a comparison" + end + + context "comparing a non sortable object" do + let(:other) { Object.new } + let(:remote_spec) { double(:remote_spec, :platform => "jruby") } + + before do + allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec) + allow(remote_spec).to receive(:<=>).and_return(nil) + end + + it "should use default object comparison" do + expect(subject <=> other).to eq(nil) + end + end + end + + describe "#__swap__" do + let(:spec) { double(:spec, :dependencies => []) } + let(:new_spec) { double(:new_spec, :dependencies => [], :runtime_dependencies => []) } + + before { subject.instance_variable_set(:@_remote_specification, spec) } + + it "should replace remote specification with the passed spec" do + expect(subject.instance_variable_get(:@_remote_specification)).to be(spec) + subject.__swap__(new_spec) + expect(subject.instance_variable_get(:@_remote_specification)).to be(new_spec) + end + end + + describe "#sort_obj" do + context "when platform is ruby" do + it "should return a sorting delegate array with name, version, and -1" do + expect(subject.sort_obj).to match_array(["foo", version, -1]) + end + end + + context "when platform is not ruby" do + let(:platform) { "jruby" } + + it "should return a sorting delegate array with name, version, and 1" do + expect(subject.sort_obj).to match_array(["foo", version, 1]) + end + end + end + + describe "method missing" do + context "and is present in Gem::Specification" do + let(:remote_spec) { double(:remote_spec, :authors => "abcd") } + + before do + allow(subject).to receive(:_remote_specification).and_return(remote_spec) + expect(subject.methods.map(&:to_sym)).not_to include(:authors) + end + + it "should send through to Gem::Specification" do + expect(subject.authors).to eq("abcd") + end + end + end + + describe "respond to missing?" do + context "and is present in Gem::Specification" do + let(:remote_spec) { double(:remote_spec, :authors => "abcd") } + + before do + allow(subject).to receive(:_remote_specification).and_return(remote_spec) + expect(subject.methods.map(&:to_sym)).not_to include(:authors) + end + + it "should send through to Gem::Specification" do + expect(subject.respond_to?(:authors)).to be_truthy + end + end + end +end diff --git a/spec/bundler/bundler/retry_spec.rb b/spec/bundler/bundler/retry_spec.rb new file mode 100644 index 0000000000..b893580d72 --- /dev/null +++ b/spec/bundler/bundler/retry_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Retry do + it "return successful result if no errors" do + attempts = 0 + result = Bundler::Retry.new(nil, nil, 3).attempt do + attempts += 1 + :success + end + expect(result).to eq(:success) + expect(attempts).to eq(1) + end + + it "returns the first valid result" do + jobs = [proc { raise "foo" }, proc { :bar }, proc { raise "foo" }] + attempts = 0 + result = Bundler::Retry.new(nil, nil, 3).attempt do + attempts += 1 + jobs.shift.call + end + expect(result).to eq(:bar) + expect(attempts).to eq(2) + end + + it "raises the last error" do + errors = [StandardError, StandardError, StandardError, Bundler::GemfileNotFound] + attempts = 0 + expect do + Bundler::Retry.new(nil, nil, 3).attempt do + attempts += 1 + raise errors.shift + end + end.to raise_error(Bundler::GemfileNotFound) + expect(attempts).to eq(4) + end + + it "raises exceptions" do + error = Bundler::GemfileNotFound + attempts = 0 + expect do + Bundler::Retry.new(nil, error).attempt do + attempts += 1 + raise error + end + end.to raise_error(error) + expect(attempts).to eq(1) + end + + context "logging" do + let(:error) { Bundler::GemfileNotFound } + let(:failure_message) { "Retrying test due to error (2/2): #{error} #{error}" } + + context "with debugging on" do + it "print error message with newline" do + allow(Bundler.ui).to receive(:debug?).and_return(true) + expect(Bundler.ui).to_not receive(:info) + expect(Bundler.ui).to receive(:warn).with(failure_message, true) + + expect do + Bundler::Retry.new("test", [], 1).attempt do + raise error + end + end.to raise_error(error) + end + end + + context "with debugging off" do + it "print error message with newlines" do + allow(Bundler.ui).to receive(:debug?).and_return(false) + expect(Bundler.ui).to receive(:info).with("").twice + expect(Bundler.ui).to receive(:warn).with(failure_message, false) + + expect do + Bundler::Retry.new("test", [], 1).attempt do + raise error + end + end.to raise_error(error) + end + end + end +end diff --git a/spec/bundler/bundler/ruby_dsl_spec.rb b/spec/bundler/bundler/ruby_dsl_spec.rb new file mode 100644 index 0000000000..bc1ca98457 --- /dev/null +++ b/spec/bundler/bundler/ruby_dsl_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require "bundler/ruby_dsl" + +RSpec.describe Bundler::RubyDsl do + class MockDSL + include Bundler::RubyDsl + + attr_reader :ruby_version + end + + let(:dsl) { MockDSL.new } + let(:ruby_version) { "2.0.0" } + 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 } + end + + let(:invoke) do + proc do + args = Array(ruby_version) + [options] + dsl.ruby(*args) + end + end + + subject do + invoke.call + dsl.ruby_version + end + + describe "#ruby_version" do + shared_examples_for "it stores the ruby version" do + it "stores the version" do + expect(subject.versions).to eq(Array(ruby_version)) + expect(subject.gem_version.version).to eq(version) + end + + it "stores the engine details" do + expect(subject.engine).to eq(engine) + expect(subject.engine_versions).to eq(Array(engine_version)) + end + + it "stores the patchlevel" do + expect(subject.patchlevel).to eq(patchlevel) + end + end + + context "with a plain version" do + it_behaves_like "it stores the ruby version" + end + + context "with a single requirement" do + let(:ruby_version) { ">= 2.0.0" } + it_behaves_like "it stores the ruby version" + end + + context "with two requirements in the same string" do + let(:ruby_version) { ">= 2.0.0, < 3.0" } + it "raises an error" do + expect { subject }.to raise_error(ArgumentError) + end + end + + context "with two requirements" do + let(:ruby_version) { ["~> 2.0.0", "> 2.0.1"] } + it_behaves_like "it stores the ruby version" + end + + context "with multiple engine versions" do + let(:engine_version) { ["> 200", "< 300"] } + it_behaves_like "it stores the ruby version" + end + + context "with no options hash" do + let(:invoke) { proc { dsl.ruby(ruby_version) } } + + let(:patchlevel) { nil } + let(:engine) { "ruby" } + let(:engine_version) { version } + + it_behaves_like "it stores the ruby version" + + context "and with multiple requirements" do + let(:ruby_version) { ["~> 2.0.0", "> 2.0.1"] } + let(:engine_version) { ruby_version } + it_behaves_like "it stores the ruby version" + end + end + end +end diff --git a/spec/bundler/bundler/ruby_version_spec.rb b/spec/bundler/bundler/ruby_version_spec.rb new file mode 100644 index 0000000000..46a1b2918b --- /dev/null +++ b/spec/bundler/bundler/ruby_version_spec.rb @@ -0,0 +1,524 @@ +# frozen_string_literal: true + +require "bundler/ruby_version" + +RSpec.describe "Bundler::RubyVersion and its subclasses" do + let(:version) { "2.0.0" } + let(:patchlevel) { "645" } + let(:engine) { "jruby" } + let(:engine_version) { "2.0.1" } + + describe Bundler::RubyVersion do + subject { Bundler::RubyVersion.new(version, patchlevel, engine, engine_version) } + + let(:ruby_version) { subject } + let(:other_version) { version } + let(:other_patchlevel) { patchlevel } + let(:other_engine) { engine } + let(:other_engine_version) { engine_version } + let(:other_ruby_version) { Bundler::RubyVersion.new(other_version, other_patchlevel, other_engine, other_engine_version) } + + describe "#initialize" do + context "no engine is passed" do + let(:engine) { nil } + + it "should set ruby as the engine" do + expect(subject.engine).to eq("ruby") + end + end + + context "no engine_version is passed" do + let(:engine_version) { nil } + + it "should set engine version as the passed version" do + expect(subject.engine_versions).to eq(["2.0.0"]) + end + end + + context "with engine in symbol" do + let(:engine) { :jruby } + + it "should coerce engine to string" do + expect(subject.engine).to eq("jruby") + end + end + + context "is called with multiple requirements" do + let(:version) { ["<= 2.0.0", "> 1.9.3"] } + let(:engine_version) { nil } + + it "sets the versions" do + expect(subject.versions).to eq(version) + end + + it "sets the engine versions" do + expect(subject.engine_versions).to eq(version) + end + end + + context "is called with multiple engine requirements" do + let(:engine_version) { [">= 2.0", "< 2.3"] } + + it "sets the engine versions" do + expect(subject.engine_versions).to eq(engine_version) + end + end + end + + describe ".from_string" do + shared_examples_for "returning" do + it "returns the original RubyVersion" do + expect(described_class.from_string(subject.to_s)).to eq(subject) + end + end + + include_examples "returning" + + context "no patchlevel" do + let(:patchlevel) { nil } + + include_examples "returning" + end + + context "engine is ruby" do + let(:engine) { "ruby" } + let(:engine_version) { version } + + include_examples "returning" + end + + context "with multiple requirements" do + let(:engine_version) { ["> 9", "< 11"] } + let(:version) { ["> 8", "< 10"] } + let(:patchlevel) { nil } + + it "returns nil" do + expect(described_class.from_string(subject.to_s)).to be_nil + end + end + end + + describe "#to_s" do + it "should return info string with the ruby version, patchlevel, engine, and engine version" do + expect(subject.to_s).to eq("ruby 2.0.0p645 (jruby 2.0.1)") + end + + context "no patchlevel" do + let(:patchlevel) { nil } + + it "should return info string with the version, engine, and engine version" do + expect(subject.to_s).to eq("ruby 2.0.0 (jruby 2.0.1)") + end + end + + context "engine is ruby" do + let(:engine) { "ruby" } + + it "should return info string with the ruby version and patchlevel" do + expect(subject.to_s).to eq("ruby 2.0.0p645") + end + end + + context "with multiple requirements" do + let(:engine_version) { ["> 9", "< 11"] } + let(:version) { ["> 8", "< 10"] } + let(:patchlevel) { nil } + + it "should return info string with all requirements" do + expect(subject.to_s).to eq("ruby > 8, < 10 (jruby > 9, < 11)") + end + end + end + + describe "#==" do + shared_examples_for "two ruby versions are not equal" do + it "should return false" do + expect(subject).to_not eq(other_ruby_version) + end + end + + context "the versions, pathlevels, engines, and engine_versions match" do + it "should return true" do + expect(subject).to eq(other_ruby_version) + end + end + + context "the versions do not match" do + let(:other_version) { "1.21.6" } + + it_behaves_like "two ruby versions are not equal" + end + + context "the patchlevels do not match" do + let(:other_patchlevel) { "21" } + + it_behaves_like "two ruby versions are not equal" + end + + context "the engines do not match" do + let(:other_engine) { "ruby" } + + it_behaves_like "two ruby versions are not equal" + end + + context "the engine versions do not match" do + let(:other_engine_version) { "1.11.2" } + + it_behaves_like "two ruby versions are not equal" + end + end + + describe "#host" do + before do + allow(RbConfig::CONFIG).to receive(:[]).with("host_cpu").and_return("x86_64") + allow(RbConfig::CONFIG).to receive(:[]).with("host_vendor").and_return("apple") + allow(RbConfig::CONFIG).to receive(:[]).with("host_os").and_return("darwin14.5.0") + end + + it "should return an info string with the host cpu, vendor, and os" do + expect(subject.host).to eq("x86_64-apple-darwin14.5.0") + end + + it "memoizes the info string with the host cpu, vendor, and os" do + expect(RbConfig::CONFIG).to receive(:[]).with("host_cpu").once.and_call_original + expect(RbConfig::CONFIG).to receive(:[]).with("host_vendor").once.and_call_original + expect(RbConfig::CONFIG).to receive(:[]).with("host_os").once.and_call_original + 2.times { ruby_version.host } + end + end + + describe "#gem_version" do + let(:gem_version) { "2.0.0" } + let(:gem_version_obj) { Gem::Version.new(gem_version) } + + shared_examples_for "it parses the version from the requirement string" do |version| + let(:version) { version } + it "should return the underlying version" do + expect(ruby_version.gem_version).to eq(gem_version_obj) + expect(ruby_version.gem_version.version).to eq(gem_version) + end + end + + it_behaves_like "it parses the version from the requirement string", "2.0.0" + it_behaves_like "it parses the version from the requirement string", ">= 2.0.0" + it_behaves_like "it parses the version from the requirement string", "~> 2.0.0" + it_behaves_like "it parses the version from the requirement string", "< 2.0.0" + it_behaves_like "it parses the version from the requirement string", "= 2.0.0" + it_behaves_like "it parses the version from the requirement string", ["> 2.0.0", "< 2.4.5"] + end + + describe "#diff" do + let(:engine) { "ruby" } + + shared_examples_for "there is a difference in the engines" do + it "should return a tuple with :engine and the two different engines" do + expect(ruby_version.diff(other_ruby_version)).to eq([:engine, engine, other_engine]) + end + end + + shared_examples_for "there is a difference in the versions" do + it "should return a tuple with :version and the two different versions" do + expect(ruby_version.diff(other_ruby_version)).to eq([:version, Array(version).join(", "), Array(other_version).join(", ")]) + end + end + + shared_examples_for "there is a difference in the engine versions" do + it "should return a tuple with :engine_version and the two different engine versions" do + expect(ruby_version.diff(other_ruby_version)).to eq([:engine_version, Array(engine_version).join(", "), Array(other_engine_version).join(", ")]) + end + end + + shared_examples_for "there is a difference in the patchlevels" do + it "should return a tuple with :patchlevel and the two different patchlevels" do + expect(ruby_version.diff(other_ruby_version)).to eq([:patchlevel, patchlevel, other_patchlevel]) + end + end + + shared_examples_for "there are no differences" do + it "should return nil" do + expect(ruby_version.diff(other_ruby_version)).to be_nil + end + end + + context "all things match exactly" do + it_behaves_like "there are no differences" + end + + context "detects engine discrepancies first" do + let(:other_version) { "2.0.1" } + let(:other_patchlevel) { "643" } + let(:other_engine) { "rbx" } + let(:other_engine_version) { "2.0.0" } + + it_behaves_like "there is a difference in the engines" + end + + context "detects version discrepancies second" do + let(:other_version) { "2.0.1" } + let(:other_patchlevel) { "643" } + let(:other_engine_version) { "2.0.0" } + + it_behaves_like "there is a difference in the versions" + end + + context "detects version discrepancies with multiple requirements second" do + let(:other_version) { "2.0.1" } + let(:other_patchlevel) { "643" } + let(:other_engine_version) { "2.0.0" } + + let(:version) { ["> 2.0.0", "< 1.0.0"] } + + it_behaves_like "there is a difference in the versions" + end + + context "detects engine version discrepancies third" do + let(:other_patchlevel) { "643" } + let(:other_engine_version) { "2.0.0" } + + it_behaves_like "there is a difference in the engine versions" + end + + context "detects engine version discrepancies with multiple requirements third" do + let(:other_patchlevel) { "643" } + let(:other_engine_version) { "2.0.0" } + + let(:engine_version) { ["> 2.0.0", "< 1.0.0"] } + + it_behaves_like "there is a difference in the engine versions" + end + + context "detects patchlevel discrepancies last" do + let(:other_patchlevel) { "643" } + + it_behaves_like "there is a difference in the patchlevels" + end + + context "successfully matches gem requirements" do + let(:version) { ">= 2.0.0" } + let(:patchlevel) { "< 643" } + let(:engine) { "ruby" } + let(:engine_version) { "~> 2.0.1" } + let(:other_version) { "2.0.0" } + let(:other_patchlevel) { "642" } + let(:other_engine) { "ruby" } + let(:other_engine_version) { "2.0.5" } + + it_behaves_like "there are no differences" + end + + context "successfully matches multiple gem requirements" do + let(:version) { [">= 2.0.0", "< 2.4.5"] } + let(:patchlevel) { "< 643" } + let(:engine) { "ruby" } + let(:engine_version) { ["~> 2.0.1", "< 2.4.5"] } + let(:other_version) { "2.0.0" } + let(:other_patchlevel) { "642" } + let(:other_engine) { "ruby" } + let(:other_engine_version) { "2.0.5" } + + it_behaves_like "there are no differences" + end + + context "successfully detects bad gem requirements with versions with multiple requirements" do + let(:version) { ["~> 2.0.0", "< 2.0.5"] } + let(:patchlevel) { "< 643" } + let(:engine) { "ruby" } + let(:engine_version) { "~> 2.0.1" } + let(:other_version) { "2.0.5" } + let(:other_patchlevel) { "642" } + let(:other_engine) { "ruby" } + let(:other_engine_version) { "2.0.5" } + + it_behaves_like "there is a difference in the versions" + end + + context "successfully detects bad gem requirements with versions" do + let(:version) { "~> 2.0.0" } + let(:patchlevel) { "< 643" } + let(:engine) { "ruby" } + let(:engine_version) { "~> 2.0.1" } + let(:other_version) { "2.1.0" } + let(:other_patchlevel) { "642" } + let(:other_engine) { "ruby" } + let(:other_engine_version) { "2.0.5" } + + it_behaves_like "there is a difference in the versions" + end + + context "successfully detects bad gem requirements with patchlevels" do + let(:version) { ">= 2.0.0" } + let(:patchlevel) { "< 643" } + let(:engine) { "ruby" } + let(:engine_version) { "~> 2.0.1" } + let(:other_version) { "2.0.0" } + let(:other_patchlevel) { "645" } + let(:other_engine) { "ruby" } + let(:other_engine_version) { "2.0.5" } + + it_behaves_like "there is a difference in the patchlevels" + end + + context "successfully detects bad gem requirements with engine versions" do + let(:version) { ">= 2.0.0" } + let(:patchlevel) { "< 643" } + let(:engine) { "ruby" } + let(:engine_version) { "~> 2.0.1" } + let(:other_version) { "2.0.0" } + let(:other_patchlevel) { "642" } + let(:other_engine) { "ruby" } + let(:other_engine_version) { "2.1.0" } + + it_behaves_like "there is a difference in the engine versions" + end + + context "with a patchlevel of -1" do + let(:version) { ">= 2.0.0" } + let(:patchlevel) { "-1" } + let(:engine) { "ruby" } + let(:engine_version) { "~> 2.0.1" } + let(:other_version) { version } + let(:other_engine) { engine } + let(:other_engine_version) { engine_version } + + context "and comparing with another patchlevel of -1" do + let(:other_patchlevel) { patchlevel } + + it_behaves_like "there are no differences" + end + + context "and comparing with a patchlevel that is not -1" do + let(:other_patchlevel) { "642" } + + it_behaves_like "there is a difference in the patchlevels" + end + end + end + + describe "#system" do + subject { Bundler::RubyVersion.system } + + let(:bundler_system_ruby_version) { subject } + + before do + Bundler::RubyVersion.instance_variable_set("@ruby_version", nil) + end + + it "should return an instance of Bundler::RubyVersion" do + expect(subject).to be_kind_of(Bundler::RubyVersion) + end + + it "memoizes the instance of Bundler::RubyVersion" do + expect(Bundler::RubyVersion).to receive(:new).once.and_call_original + 2.times { subject } + 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) + end + end + + describe "#engine" do + context "RUBY_ENGINE is defined" do + before { stub_const("RUBY_ENGINE", "jruby") } + before { stub_const("JRUBY_VERSION", "2.1.1") } + + it "should return a copy of the value of RUBY_ENGINE" do + expect(subject.engine).to eq("jruby") + expect(subject.engine).to_not be(RUBY_ENGINE) + end + end + + context "RUBY_ENGINE is not defined" do + before { stub_const("RUBY_ENGINE", nil) } + + it "should return the string 'ruby'" do + expect(subject.engine).to eq("ruby") + end + end + end + + describe "#engine_version" do + context "engine is ruby" do + before do + stub_const("RUBY_VERSION", "2.2.4") + stub_const("RUBY_ENGINE", "ruby") + end + + it "should return a copy of the value of RUBY_VERSION" 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_VERSION) + end + end + + context "engine is rbx" do + before do + stub_const("RUBY_ENGINE", "rbx") + stub_const("Rubinius::VERSION", "2.0.0") + end + + it "should return a copy of the value of Rubinius::VERSION" do + expect(bundler_system_ruby_version.engine_versions).to eq(["2.0.0"]) + expect(bundler_system_ruby_version.engine_versions.first).to_not be(Rubinius::VERSION) + end + end + + context "engine is jruby" do + before do + stub_const("RUBY_ENGINE", "jruby") + stub_const("JRUBY_VERSION", "2.1.1") + end + + it "should return a copy of the value of JRUBY_VERSION" do + expect(subject.engine_versions).to eq(["2.1.1"]) + expect(bundler_system_ruby_version.engine_versions.first).to_not be(JRUBY_VERSION) + end + end + + context "engine is some other ruby engine" do + before do + stub_const("RUBY_ENGINE", "not_supported_ruby_engine") + stub_const("RUBY_ENGINE_VERSION", "1.2.3") + end + + it "returns RUBY_ENGINE_VERSION" do + expect(bundler_system_ruby_version.engine_versions).to eq(["1.2.3"]) + end + end + end + + describe "#patchlevel" do + it "should return a string with the value of RUBY_PATCHLEVEL" do + expect(subject.patchlevel).to eq(RUBY_PATCHLEVEL.to_s) + 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 new file mode 100644 index 0000000000..b1b15d9e5d --- /dev/null +++ b/spec/bundler/bundler/rubygems_integration_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::RubygemsIntegration do + it "uses the same chdir lock as rubygems", :rubygems => "2.1" 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| + s.name = "to-validate" + s.version = "1.0.0" + s.loaded_from = __FILE__ + end + end + subject { Bundler.rubygems.validate(spec) } + + it "skips overly-strict gemspec validation", :rubygems => "< 1.7" do + expect(spec).to_not receive(:validate) + subject + end + + it "validates with packaging mode disabled", :rubygems => "1.7" do + expect(spec).to receive(:validate).with(false) + subject + end + + it "should set a summary to avoid an overly-strict error", :rubygems => "~> 1.7.0" do + spec.summary = nil + expect { subject }.not_to raise_error + expect(spec.summary).to eq("") + end + + context "with an invalid spec" do + before do + expect(spec).to receive(:validate).with(false). + and_raise(Gem::InvalidSpecificationException.new("TODO is not an author")) + end + + it "should raise a Gem::InvalidSpecificationException and produce a helpful warning message", + :rubygems => "1.7" do + expect { subject }.to raise_error(Gem::InvalidSpecificationException, + "The gemspec at #{__FILE__} is not valid. "\ + "Please fix this gemspec.\nThe validation error was 'TODO is not an author'\n") + end + 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", :rubygems => ">= 2.0" do + let(:bundler_retry) { double(Bundler::Retry) } + let(:retry) { double("Bundler::Retry") } + let(:uri) { URI.parse("https://foo.bar") } + let(:path) { Gem.path.first } + let(:spec) do + spec = Bundler::RemoteSpecification.new("Foo", Gem::Version.new("2.5.2"), + Gem::Platform::RUBY, nil) + 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) + + Bundler.rubygems.download_gem(spec, uri, path) + end + end + + describe "#fetch_all_remote_specs", :rubygems => ">= 2.0" do + let(:uri) { URI("https://example.com") } + let(:fetcher) { double("gem_remote_fetcher") } + let(:specs_response) { Marshal.dump(["specs"]) } + let(:prerelease_specs_response) { Marshal.dump(["prerelease_specs"]) } + + context "when a rubygems source mirror is set" do + let(:orig_uri) { 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) + 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) } + + 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) + expect(result).to eq(%w[specs prerelease_specs]) + end + end + end +end diff --git a/spec/bundler/bundler/settings/validator_spec.rb b/spec/bundler/bundler/settings/validator_spec.rb new file mode 100644 index 0000000000..e4ffd89435 --- /dev/null +++ b/spec/bundler/bundler/settings/validator_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Settings::Validator do + describe ".validate!" do + def validate!(key, value, settings) + transformed_key = Bundler.settings.key_for(key) + if value.nil? + settings.delete(transformed_key) + else + settings[transformed_key] = value + end + described_class.validate!(key, value, settings) + settings + end + + it "path and path.system are mutually exclusive" do + expect(validate!("path", "bundle", {})).to eq("BUNDLE_PATH" => "bundle") + expect(validate!("path", "bundle", "BUNDLE_PATH__SYSTEM" => false)).to eq("BUNDLE_PATH" => "bundle") + expect(validate!("path", "bundle", "BUNDLE_PATH__SYSTEM" => true)).to eq("BUNDLE_PATH" => "bundle") + expect(validate!("path", nil, "BUNDLE_PATH__SYSTEM" => true)).to eq("BUNDLE_PATH__SYSTEM" => true) + expect(validate!("path", nil, "BUNDLE_PATH__SYSTEM" => false)).to eq("BUNDLE_PATH__SYSTEM" => false) + expect(validate!("path", nil, {})).to eq({}) + + expect(validate!("path.system", true, "BUNDLE_PATH" => "bundle")).to eq("BUNDLE_PATH__SYSTEM" => true) + expect(validate!("path.system", false, "BUNDLE_PATH" => "bundle")).to eq("BUNDLE_PATH" => "bundle", "BUNDLE_PATH__SYSTEM" => false) + expect(validate!("path.system", nil, "BUNDLE_PATH" => "bundle")).to eq("BUNDLE_PATH" => "bundle") + expect(validate!("path.system", true, {})).to eq("BUNDLE_PATH__SYSTEM" => true) + expect(validate!("path.system", false, {})).to eq("BUNDLE_PATH__SYSTEM" => false) + expect(validate!("path.system", nil, {})).to eq({}) + end + + it "a group cannot be in both `with` & `without` simultaneously" do + expect do + validate!("with", "", {}) + validate!("with", nil, {}) + validate!("with", "", "BUNDLE_WITHOUT" => "a") + validate!("with", nil, "BUNDLE_WITHOUT" => "a") + validate!("with", "b:c", "BUNDLE_WITHOUT" => "a") + + validate!("without", "", {}) + validate!("without", nil, {}) + validate!("without", "", "BUNDLE_WITH" => "a") + validate!("without", nil, "BUNDLE_WITH" => "a") + 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 + 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 + Setting `without` to "b:c" failed: + - a group cannot be in both `with` & `without` simultaneously + - `with` is current set to [:c, :d] + - the `c` groups conflict + EOS + end + end + + describe described_class::Rule do + let(:keys) { %w[key] } + let(:description) { "rule description" } + let(:validate) { proc { raise "validate called!" } } + subject(:rule) { described_class.new(keys, description, &validate) } + + describe "#validate!" do + it "calls the block" do + expect { rule.validate!("key", nil, {}) }.to raise_error(RuntimeError, /validate called!/) + end + end + + 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 + Setting `key` to "value" failed: + - rule description + - reason1 + - reason2 + EOS + end + end + + describe "#set" do + it "works when the value has not changed" do + allow(Bundler.ui).to receive(:info).never + + subject.set({}, "key", nil) + subject.set({ "BUNDLE_KEY" => "value" }, "key", "value") + end + + it "prints out when the value is changing" do + settings = {} + + expect(Bundler.ui).to receive(:info).with("Setting `key` to \"value\", since rule description, reason1") + subject.set(settings, "key", "value", "reason1") + expect(settings).to eq("BUNDLE_KEY" => "value") + + expect(Bundler.ui).to receive(:info).with("Setting `key` to \"value2\", since rule description, reason2") + subject.set(settings, "key", "value2", "reason2") + expect(settings).to eq("BUNDLE_KEY" => "value2") + + expect(Bundler.ui).to receive(:info).with("Setting `key` to nil, since rule description, reason3") + subject.set(settings, "key", nil, "reason3") + expect(settings).to eq({}) + end + end + end +end diff --git a/spec/bundler/bundler/settings_spec.rb b/spec/bundler/bundler/settings_spec.rb new file mode 100644 index 0000000000..1a31493e20 --- /dev/null +++ b/spec/bundler/bundler/settings_spec.rb @@ -0,0 +1,326 @@ +# frozen_string_literal: true + +require "bundler/settings" + +RSpec.describe Bundler::Settings do + subject(:settings) { described_class.new(bundled_app) } + + describe "#set_local" do + context "when the local config file is not found" do + subject(:settings) { described_class.new(nil) } + + it "raises a GemfileNotFound error with explanation" do + expect { subject.set_local("foo", "bar") }. + to raise_error(Bundler::GemfileNotFound, "Could not locate Gemfile") + end + end + end + + describe "load_config" do + let(:hash) do + { + "build.thrift" => "--with-cppflags=-D_FORTIFY_SOURCE=0", + "build.libv8" => "--with-system-v8", + "build.therubyracer" => "--with-v8-dir", + "build.pg" => "--with-pg-config=/usr/local/Cellar/postgresql92/9.2.8_1/bin/pg_config", + "gem.coc" => "false", + "gem.mit" => "false", + "gem.test" => "minitest", + "thingy" => <<-EOS.tr("\n", " "), +--asdf --fdsa --ty=oh man i hope this doesnt 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 +--very-important-option=DontDeleteRoo +--very-important-option=DontDeleteRoo + EOS + "xyz" => "zyx", + } + end + + before do + hash.each do |key, value| + settings.set_local key, value + end + end + + it "can load the config" do + loaded = settings.send(:load_config, bundled_app("config")) + expected = Hash[hash.map do |k, v| + [settings.send(:key_for, k), v.to_s] + end] + expect(loaded).to eq(expected) + end + + context "when BUNDLE_IGNORE_CONFIG is set" do + before { ENV["BUNDLE_IGNORE_CONFIG"] = "TRUE" } + + it "ignores the config" do + loaded = settings.send(:load_config, bundled_app("config")) + expect(loaded).to eq({}) + end + end + end + + describe "#global_config_file" do + context "when $HOME is not accessible" do + context "when $TMPDIR is not writable" do + it "does not raise" do + expect(Bundler.rubygems).to receive(:user_home).twice.and_return(nil) + expect(FileUtils).to receive(:mkpath).twice.with(File.join(Dir.tmpdir, "bundler", "home")).and_raise(Errno::EROFS, "Read-only file system @ dir_s_mkdir - /tmp/bundler") + + expect(subject.send(:global_config_file)).to be_nil + end + end + end + end + + describe "#[]" do + context "when the local config file is not found" do + subject(:settings) { described_class.new } + + it "does not raise" do + expect do + subject["foo"] + end.not_to raise_error + end + end + + context "when not set" do + context "when default value present" do + it "retrieves value" do + expect(settings[:retry]).to be 3 + end + end + + it "returns nil" do + expect(settings[:buttermilk]).to be nil + end + end + + context "when is boolean" do + it "returns a boolean" do + settings.set_local :frozen, "true" + expect(settings[:frozen]).to be true + end + context "when specific gem is configured" do + it "returns a boolean" do + settings.set_local "ignore_messages.foobar", "true" + expect(settings["ignore_messages.foobar"]).to be true + end + end + end + + context "when is number" do + it "returns a number" do + settings.set_local :ssl_verify_mode, "1" + expect(settings[:ssl_verify_mode]).to be 1 + end + end + + context "when it's not possible to write to the file" do + it "raises an PermissionError with explanation" do + expect(bundler_fileutils).to receive(:mkdir_p).with(settings.send(:local_config_file).dirname). + and_raise(Errno::EACCES) + expect { settings.set_local :frozen, "1" }. + to raise_error(Bundler::PermissionError, /config/) + end + end + end + + describe "#temporary" do + it "reset after used" do + Bundler.settings.set_local :no_install, true + + Bundler.settings.temporary(:no_install => false) do + expect(Bundler.settings[:no_install]).to eq false + end + + expect(Bundler.settings[:no_install]).to eq true + end + + it "returns the return value of the block" do + ret = Bundler.settings.temporary({}) { :ret } + expect(ret).to eq :ret + end + + context "when called without a block" do + it "leaves the setting changed" do + 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 + end + end + end + + describe "#set_global" do + context "when it's not possible to write to the file" do + it "raises an PermissionError with explanation" do + expect(bundler_fileutils).to receive(:mkdir_p).with(settings.send(:global_config_file).dirname). + and_raise(Errno::EACCES) + expect { settings.set_global(:frozen, "1") }. + to raise_error(Bundler::PermissionError, %r{\.bundle/config}) + end + end + end + + describe "#pretty_values_for" do + it "prints the converted value rather than the raw string" do + bool_key = described_class::BOOL_KEYS.first + settings.set_local(bool_key, "false") + expect(subject.pretty_values_for(bool_key)).to eq [ + "Set for your local app (#{bundled_app("config")}): false", + ] + end + end + + describe "#mirror_for" do + let(:uri) { URI("https://rubygems.org/") } + + context "with no configured mirror" do + it "returns the original URI" do + expect(settings.mirror_for(uri)).to eq(uri) + end + + it "converts a string parameter to a URI" do + expect(settings.mirror_for("https://rubygems.org/")).to eq(uri) + end + end + + context "with a configured mirror" do + let(:mirror_uri) { URI("https://rubygems-mirror.org/") } + + before { settings.set_local "mirror.https://rubygems.org/", mirror_uri.to_s } + + it "returns the mirror URI" do + expect(settings.mirror_for(uri)).to eq(mirror_uri) + end + + it "converts a string parameter to a URI" do + expect(settings.mirror_for("https://rubygems.org/")).to eq(mirror_uri) + end + + it "normalizes the URI" do + expect(settings.mirror_for("https://rubygems.org")).to eq(mirror_uri) + end + + it "is case insensitive" do + expect(settings.mirror_for("HTTPS://RUBYGEMS.ORG/")).to eq(mirror_uri) + end + + context "with a file URI" do + let(:mirror_uri) { URI("file:/foo/BAR/baz/qUx/") } + + it "returns the mirror URI" do + expect(settings.mirror_for(uri)).to eq(mirror_uri) + end + + it "converts a string parameter to a URI" do + expect(settings.mirror_for("file:/foo/BAR/baz/qUx/")).to eq(mirror_uri) + end + + it "normalizes the URI" do + expect(settings.mirror_for("file:/foo/BAR/baz/qUx")).to eq(mirror_uri) + end + end + end + end + + describe "#credentials_for" do + let(:uri) { URI("https://gemserver.example.org/") } + let(:credentials) { "username:password" } + + context "with no configured credentials" do + it "returns nil" do + expect(settings.credentials_for(uri)).to be_nil + end + end + + context "with credentials configured by URL" do + before { settings.set_local "https://gemserver.example.org/", credentials } + + it "returns the configured credentials" do + expect(settings.credentials_for(uri)).to eq(credentials) + end + end + + context "with credentials configured by hostname" do + before { settings.set_local "gemserver.example.org", credentials } + + it "returns the configured credentials" do + expect(settings.credentials_for(uri)).to eq(credentials) + end + end + end + + describe "URI normalization" do + it "normalizes HTTP URIs in credentials configuration" do + settings.set_local "http://gemserver.example.org", "username:password" + expect(settings.all).to include("http://gemserver.example.org/") + end + + it "normalizes HTTPS URIs in credentials configuration" do + settings.set_local "https://gemserver.example.org", "username:password" + expect(settings.all).to include("https://gemserver.example.org/") + end + + it "normalizes HTTP URIs in mirror configuration" do + settings.set_local "mirror.http://rubygems.org", "http://rubygems-mirror.org" + expect(settings.all).to include("mirror.http://rubygems.org/") + end + + it "normalizes HTTPS URIs in mirror configuration" do + settings.set_local "mirror.https://rubygems.org", "http://rubygems-mirror.org" + expect(settings.all).to include("mirror.https://rubygems.org/") + end + + it "does not normalize other config keys that happen to contain 'http'" do + settings.set_local "local.httparty", home("httparty") + expect(settings.all).to include("local.httparty") + end + + it "does not normalize other config keys that happen to contain 'https'" do + settings.set_local "local.httpsmarty", home("httpsmarty") + expect(settings.all).to include("local.httpsmarty") + end + + 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( + URI("http://rubygems-mirror.org/") + ) + end + + it "normalizes URIs with a fallback_timeout option" do + settings.set_local "mirror.https://rubygems.org/.fallback_timeout", "true" + expect(settings.all).to include("mirror.https://rubygems.org/.fallback_timeout") + end + + it "normalizes URIs with a fallback_timeout option without a trailing slash" do + settings.set_local "mirror.https://rubygems.org.fallback_timeout", "true" + expect(settings.all).to include("mirror.https://rubygems.org/.fallback_timeout") + end + end + + describe "BUNDLE_ keys format" do + let(:settings) { described_class.new(bundled_app(".bundle")) } + + it "converts older keys without double dashes" do + config("BUNDLE_MY__PERSONAL.RACK" => "~/Work/git/rack") + expect(settings["my.personal.rack"]).to eq("~/Work/git/rack") + end + + it "converts older keys without trailing slashes and double dashes" do + config("BUNDLE_MIRROR__HTTPS://RUBYGEMS.ORG" => "http://rubygems-mirror.org") + expect(settings["mirror.https://rubygems.org/"]).to eq("http://rubygems-mirror.org") + end + + it "reads newer keys format properly" do + config("BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/" => "http://rubygems-mirror.org") + expect(settings["mirror.https://rubygems.org/"]).to eq("http://rubygems-mirror.org") + end + end +end diff --git a/spec/bundler/bundler/shared_helpers_spec.rb b/spec/bundler/bundler/shared_helpers_spec.rb new file mode 100644 index 0000000000..b66c43fd92 --- /dev/null +++ b/spec/bundler/bundler/shared_helpers_spec.rb @@ -0,0 +1,504 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::SharedHelpers do + let(:ext_lock_double) { double(:ext_lock) } + + before do + allow(Bundler.rubygems).to receive(:ext_lock).and_return(ext_lock_double) + allow(ext_lock_double).to receive(:synchronize) {|&block| block.call } + end + + subject { Bundler::SharedHelpers } + + describe "#default_gemfile" do + before { ENV["BUNDLE_GEMFILE"] = "/path/Gemfile" } + + context "Gemfile is present" do + let(:expected_gemfile_path) { Pathname.new("/path/Gemfile") } + + it "returns the Gemfile path" do + expect(subject.default_gemfile).to eq(expected_gemfile_path) + end + end + + context "Gemfile is not present" do + before { ENV["BUNDLE_GEMFILE"] = nil } + + it "raises a GemfileNotFound error" do + expect { subject.default_gemfile }.to raise_error( + Bundler::GemfileNotFound, "Could not locate Gemfile" + ) + end + end + + context "Gemfile is not an absolute path" do + before { ENV["BUNDLE_GEMFILE"] = "Gemfile" } + + let(:expected_gemfile_path) { Pathname.new("Gemfile").expand_path } + + it "returns the Gemfile path" do + expect(subject.default_gemfile).to eq(expected_gemfile_path) + end + end + end + + describe "#default_lockfile" do + context "gemfile is gems.rb" do + let(:gemfile_path) { Pathname.new("/path/gems.rb") } + let(:expected_lockfile_path) { Pathname.new("/path/gems.locked") } + + before { allow(subject).to receive(:default_gemfile).and_return(gemfile_path) } + + it "returns the gems.locked path" do + expect(subject.default_lockfile).to eq(expected_lockfile_path) + end + end + + context "is a regular Gemfile" do + let(:gemfile_path) { Pathname.new("/path/Gemfile") } + let(:expected_lockfile_path) { Pathname.new("/path/Gemfile.lock") } + + before { allow(subject).to receive(:default_gemfile).and_return(gemfile_path) } + + it "returns the lock file path" do + expect(subject.default_lockfile).to eq(expected_lockfile_path) + end + end + end + + describe "#default_bundle_dir" do + context ".bundle does not exist" do + it "returns nil" do + expect(subject.default_bundle_dir).to be_nil + end + end + + context ".bundle is global .bundle" do + let(:global_rubygems_dir) { Pathname.new("#{bundled_app}") } + + before do + Dir.mkdir ".bundle" + allow(Bundler.rubygems).to receive(:user_home).and_return(global_rubygems_dir) + end + + it "returns nil" do + expect(subject.default_bundle_dir).to be_nil + end + end + + context ".bundle is not global .bundle" do + let(:global_rubygems_dir) { Pathname.new("/path/rubygems") } + let(:expected_bundle_dir_path) { Pathname.new("#{bundled_app}/.bundle") } + + before do + Dir.mkdir ".bundle" + allow(Bundler.rubygems).to receive(:user_home).and_return(global_rubygems_dir) + end + + it "returns the .bundle path" do + expect(subject.default_bundle_dir).to eq(expected_bundle_dir_path) + end + end + end + + describe "#in_bundle?" do + it "calls the find_gemfile method" do + expect(subject).to receive(:find_gemfile) + subject.in_bundle? + end + + shared_examples_for "correctly determines whether to return a Gemfile path" do + context "currently in directory with a Gemfile" do + before { File.new("Gemfile", "w") } + + it "returns path of the bundle Gemfile" do + expect(subject.in_bundle?).to eq("#{bundled_app}/Gemfile") + end + end + + context "currently in directory without a Gemfile" do + it "returns nil" do + expect(subject.in_bundle?).to be_nil + end + end + end + + context "ENV['BUNDLE_GEMFILE'] set" do + before { ENV["BUNDLE_GEMFILE"] = "/path/Gemfile" } + + it "returns ENV['BUNDLE_GEMFILE']" do + expect(subject.in_bundle?).to eq("/path/Gemfile") + end + end + + context "ENV['BUNDLE_GEMFILE'] not set" do + before { ENV["BUNDLE_GEMFILE"] = nil } + + it_behaves_like "correctly determines whether to return a Gemfile path" + end + + context "ENV['BUNDLE_GEMFILE'] is blank" do + before { ENV["BUNDLE_GEMFILE"] = "" } + + it_behaves_like "correctly determines whether to return a Gemfile path" + end + end + + describe "#chdir" do + let(:op_block) { proc { Dir.mkdir "nested_dir" } } + + before { Dir.mkdir "chdir_test_dir" } + + it "executes the passed block while in the specified directory" do + subject.chdir("chdir_test_dir", &op_block) + expect(Pathname.new("chdir_test_dir/nested_dir")).to exist + end + end + + describe "#pwd" do + it "returns the current absolute path" do + expect(subject.pwd).to eq(bundled_app) + end + end + + describe "#with_clean_git_env" do + let(:with_clean_git_env_block) { proc { Dir.mkdir "with_clean_git_env_test_dir" } } + + before do + ENV["GIT_DIR"] = "ORIGINAL_ENV_GIT_DIR" + ENV["GIT_WORK_TREE"] = "ORIGINAL_ENV_GIT_WORK_TREE" + end + + it "executes the passed block" do + subject.with_clean_git_env(&with_clean_git_env_block) + expect(Pathname.new("with_clean_git_env_test_dir")).to exist + end + + context "when a block is passed" do + let(:with_clean_git_env_block) do + proc do + Dir.mkdir "git_dir_test_dir" unless ENV["GIT_DIR"].nil? + Dir.mkdir "git_work_tree_test_dir" unless ENV["GIT_WORK_TREE"].nil? + end end + + it "uses a fresh git env for execution" do + subject.with_clean_git_env(&with_clean_git_env_block) + expect(Pathname.new("git_dir_test_dir")).to_not exist + expect(Pathname.new("git_work_tree_test_dir")).to_not exist + end + end + + context "passed block does not throw errors" do + let(:with_clean_git_env_block) do + proc do + ENV["GIT_DIR"] = "NEW_ENV_GIT_DIR" + ENV["GIT_WORK_TREE"] = "NEW_ENV_GIT_WORK_TREE" + end end + + it "restores the git env after" do + subject.with_clean_git_env(&with_clean_git_env_block) + expect(ENV["GIT_DIR"]).to eq("ORIGINAL_ENV_GIT_DIR") + expect(ENV["GIT_WORK_TREE"]).to eq("ORIGINAL_ENV_GIT_WORK_TREE") + end + end + + context "passed block throws errors" do + let(:with_clean_git_env_block) do + proc do + ENV["GIT_DIR"] = "NEW_ENV_GIT_DIR" + ENV["GIT_WORK_TREE"] = "NEW_ENV_GIT_WORK_TREE" + raise RuntimeError.new + end end + + it "restores the git env after" do + expect { subject.with_clean_git_env(&with_clean_git_env_block) }.to raise_error(RuntimeError) + expect(ENV["GIT_DIR"]).to eq("ORIGINAL_ENV_GIT_DIR") + expect(ENV["GIT_WORK_TREE"]).to eq("ORIGINAL_ENV_GIT_WORK_TREE") + end + end + end + + describe "#set_bundle_environment" do + before do + ENV["BUNDLE_GEMFILE"] = "Gemfile" + end + + shared_examples_for "ENV['PATH'] gets set correctly" do + before { Dir.mkdir ".bundle" } + + it "ensures bundle bin path is in ENV['PATH']" do + subject.set_bundle_environment + paths = ENV["PATH"].split(File::PATH_SEPARATOR) + expect(paths).to include("#{Bundler.bundle_path}/bin") + end + end + + 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("-rbundler/setup") + end + end + + shared_examples_for "ENV['RUBYLIB'] gets set correctly" do + let(:ruby_lib_path) { "stubbed_ruby_lib_dir" } + + before do + allow(Bundler::SharedHelpers).to receive(:bundler_ruby_lib).and_return(ruby_lib_path) + end + + it "ensures bundler's ruby version lib path is in ENV['RUBYLIB']" do + subject.set_bundle_environment + paths = (ENV["RUBYLIB"]).split(File::PATH_SEPARATOR) + expect(paths).to include(ruby_lib_path) + end + end + + it "calls the appropriate set methods" do + expect(subject).to receive(:set_path) + expect(subject).to receive(:set_rubyopt) + expect(subject).to receive(:set_rubylib) + subject.set_bundle_environment + end + + it "exits if bundle path contains the unix-like path separator" do + if Gem.respond_to?(:path_separator) + allow(Gem).to receive(:path_separator).and_return(":") + else + stub_const("File::PATH_SEPARATOR", ":".freeze) + end + allow(Bundler).to receive(:bundle_path) { Pathname.new("so:me/dir/bin") } + expect { subject.send(:validate_bundle_path) }.to raise_error( + Bundler::PathError, + "Your bundle path contains text matching \":\", which is the " \ + "path separator for your system. Bundler cannot " \ + "function correctly when the Bundle path contains the " \ + "system's PATH separator. Please change your " \ + "bundle path to not match \":\".\nYour current bundle " \ + "path is '#{Bundler.bundle_path}'." + ) + end + + context "with a jruby path_separator regex", :ruby => "1.9" do + # In versions of jruby that supported ruby 1.8, the path separator was the standard File::PATH_SEPARATOR + let(:regex) { Regexp.new("(?<!jar:file|jar|file|classpath|uri:classloader|uri|http|https):") } + it "does not exit if bundle path is the standard uri path" do + allow(Bundler.rubygems).to receive(:path_separator).and_return(regex) + allow(Bundler).to receive(:bundle_path) { Pathname.new("uri:classloader:/WEB-INF/gems") } + expect { subject.send(:validate_bundle_path) }.not_to raise_error + end + + it "exits if bundle path contains another directory" do + allow(Bundler.rubygems).to receive(:path_separator).and_return(regex) + allow(Bundler).to receive(:bundle_path) { + Pathname.new("uri:classloader:/WEB-INF/gems:other/dir") + } + + expect { subject.send(:validate_bundle_path) }.to raise_error( + Bundler::PathError, + "Your bundle path contains text matching " \ + "/(?<!jar:file|jar|file|classpath|uri:classloader|uri|http|https):/, which is the " \ + "path separator for your system. Bundler cannot " \ + "function correctly when the Bundle path contains the " \ + "system's PATH separator. Please change your " \ + "bundle path to not match " \ + "/(?<!jar:file|jar|file|classpath|uri:classloader|uri|http|https):/." \ + "\nYour current bundle path is '#{Bundler.bundle_path}'." + ) + end + end + + context "ENV['PATH'] does not exist" do + before { ENV.delete("PATH") } + + it_behaves_like "ENV['PATH'] gets set correctly" + end + + context "ENV['PATH'] is empty" do + before { ENV["PATH"] = "" } + + it_behaves_like "ENV['PATH'] gets set correctly" + end + + context "ENV['PATH'] exists" do + before { ENV["PATH"] = "/some_path/bin" } + + it_behaves_like "ENV['PATH'] gets set correctly" + end + + context "ENV['PATH'] already contains the bundle bin path" do + let(:bundle_path) { "#{Bundler.bundle_path}/bin" } + + before do + ENV["PATH"] = bundle_path + end + + it_behaves_like "ENV['PATH'] gets set correctly" + + it "ENV['PATH'] should only contain one instance of bundle bin path" do + subject.set_bundle_environment + paths = (ENV["PATH"]).split(File::PATH_SEPARATOR) + expect(paths.count(bundle_path)).to eq(1) + end + end + + context "ENV['RUBYOPT'] does not exist" do + before { ENV.delete("RUBYOPT") } + + it_behaves_like "ENV['RUBYOPT'] gets set correctly" + end + + 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 + + context "ENV['RUBYLIB'] does not exist" do + before { ENV.delete("RUBYLIB") } + + it_behaves_like "ENV['RUBYLIB'] gets set correctly" + end + + context "ENV['RUBYLIB'] is empty" do + before { ENV["PATH"] = "" } + + it_behaves_like "ENV['RUBYLIB'] gets set correctly" + end + + context "ENV['RUBYLIB'] exists" do + before { ENV["PATH"] = "/some_path/bin" } + + it_behaves_like "ENV['RUBYLIB'] gets set correctly" + end + + context "bundle executable in ENV['BUNDLE_BIN_PATH'] does not exist" do + before { ENV["BUNDLE_BIN_PATH"] = "/does/not/exist" } + before { Bundler.rubygems.replace_bin_path [], [] } + + it "sets BUNDLE_BIN_PATH to the bundle executable file" do + subject.set_bundle_environment + bundle_exe = ruby_core? ? "../../../../exe/bundle" : "../../../exe/bundle" + expect(ENV["BUNDLE_BIN_PATH"]).to eq(File.expand_path(bundle_exe, __FILE__)) + end + end + + context "ENV['RUBYLIB'] already contains the bundler's ruby version lib path" do + let(:ruby_lib_path) { "stubbed_ruby_lib_dir" } + + before do + ENV["RUBYLIB"] = ruby_lib_path + end + + it_behaves_like "ENV['RUBYLIB'] gets set correctly" + + it "ENV['RUBYLIB'] should only contain one instance of bundler's ruby version lib path" do + subject.set_bundle_environment + paths = (ENV["RUBYLIB"]).split(File::PATH_SEPARATOR) + expect(paths.count(ruby_lib_path)).to eq(1) + end + end + end + + describe "#filesystem_access" do + context "system has proper permission access" do + let(:file_op_block) { proc {|path| FileUtils.mkdir_p(path) } } + + it "performs the operation in the passed block" do + subject.filesystem_access("./test_dir", &file_op_block) + expect(Pathname.new("test_dir")).to exist + end + end + + context "system throws Errno::EACESS" do + let(:file_op_block) { proc {|_path| raise Errno::EACCES } } + + it "raises a PermissionError" do + expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error( + Bundler::PermissionError + ) + end + end + + context "system throws Errno::EAGAIN" do + let(:file_op_block) { proc {|_path| raise Errno::EAGAIN } } + + it "raises a TemporaryResourceError" do + expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error( + Bundler::TemporaryResourceError + ) + end + end + + context "system throws Errno::EPROTO" do + let(:file_op_block) { proc {|_path| raise Errno::EPROTO } } + + it "raises a VirtualProtocolError" do + expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error( + Bundler::VirtualProtocolError + ) + end + end + + context "system throws Errno::ENOTSUP", :ruby => "1.9" do + let(:file_op_block) { proc {|_path| raise Errno::ENOTSUP } } + + it "raises a OperationNotSupportedError" do + expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error( + Bundler::OperationNotSupportedError + ) + end + end + + context "system throws Errno::ENOSPC" do + let(:file_op_block) { proc {|_path| raise Errno::ENOSPC } } + + it "raises a NoSpaceOnDeviceError" do + expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error( + Bundler::NoSpaceOnDeviceError + ) + end + end + + context "system throws an unhandled SystemCallError" do + let(:error) { SystemCallError.new("Shields down", 1337) } + let(:file_op_block) { proc {|_path| raise error } } + + it "raises a GenericSystemCallError" do + expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error( + Bundler::GenericSystemCallError, /error accessing.+underlying.+Shields down/m + ) + end + end + end + + describe "#const_get_safely" do + module TargetNamespace + VALID_CONSTANT = 1 + end + + context "when the namespace does have the requested constant" do + it "returns the value of the requested constant" do + expect(subject.const_get_safely(:VALID_CONSTANT, TargetNamespace)).to eq(1) + end + end + + context "when the requested constant is passed as a string" do + it "returns the value of the requested constant" do + expect(subject.const_get_safely("VALID_CONSTANT", TargetNamespace)).to eq(1) + end + end + + context "when the namespace does not have the requested constant" do + it "returns nil" do + expect(subject.const_get_safely("INVALID_CONSTANT", TargetNamespace)).to be_nil + end + 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 new file mode 100644 index 0000000000..3a29c97461 --- /dev/null +++ b/spec/bundler/bundler/source/git/git_proxy_spec.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Source::Git::GitProxy do + let(:path) { Pathname("path") } + let(:uri) { "https://github.com/bundler/bundler.git" } + let(:ref) { "HEAD" } + let(:revision) { nil } + let(:git_source) { nil } + subject { described_class.new(path, uri, ref, revision, git_source) } + + context "with configured credentials" do + it "adds username and password to URI" do + Bundler.settings.temporary(uri => "u:p") + expect(subject).to receive(:git_retry).with(match("https://u:p@github.com/bundler/bundler.git")) + subject.checkout + end + + it "adds username and password to URI for host" do + Bundler.settings.temporary("github.com" => "u:p") + expect(subject).to receive(:git_retry).with(match("https://u:p@github.com/bundler/bundler.git")) + subject.checkout + end + + it "does not add username and password to mismatched URI" do + Bundler.settings.temporary("https://u:p@github.com/bundler/bundler-mismatch.git" => "u:p") + expect(subject).to receive(:git_retry).with(match(uri)) + subject.checkout + end + + it "keeps original userinfo" do + Bundler.settings.temporary("github.com" => "u:p") + original = "https://orig:info@github.com/bundler/bundler.git" + subject = described_class.new(Pathname("path"), original, "HEAD") + expect(subject).to receive(:git_retry).with(match(original)) + subject.checkout + end + end + + describe "#version" do + context "with a normal version number" do + before do + expect(subject).to receive(:git).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") + end + + it "does not raise an error when passed into Gem::Version.create" do + expect { Gem::Version.create subject.version }.not_to raise_error + end + end + + context "with a OSX version number" do + before do + expect(subject).to receive(:git).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") + end + + it "does not raise an error when passed into Gem::Version.create" do + expect { Gem::Version.create subject.version }.not_to raise_error + end + end + + context "with a msysgit version number" do + before do + expect(subject).to receive(:git).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") + end + + it "does not raise an error when passed into Gem::Version.create" do + expect { Gem::Version.create subject.version }.not_to raise_error + end + end + end + + describe "#full_version" do + context "with a normal version number" do + before do + expect(subject).to receive(:git).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") + end + end + + context "with a OSX version number" do + before do + expect(subject).to receive(:git).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)") + end + end + + context "with a msysgit version number" do + before do + expect(subject).to receive(:git).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") + end + end + end + + describe "#copy_to" do + let(:destination) { tmpdir("copy_to_path") } + let(:submodules) { false } + + context "when given a SHA as a revision" do + let(:revision) { "abcd" * 10 } + + it "fails gracefully when resetting to the revision fails" do + expect(subject).to receive(:git_retry).with(start_with("clone ")) { destination.mkpath } + expect(subject).to receive(:git_retry).with(start_with("fetch ")) + expect(subject).to receive(:git).with("reset --hard #{revision}").and_raise(Bundler::Source::Git::GitCommandError, "command") + expect(subject).not_to receive(:git) + + expect { subject.copy_to(destination, submodules) }. + to raise_error(Bundler::Source::Git::MissingGitRevisionError, + "Revision #{revision} does not exist in the repository #{uri}. Maybe you misspelled it?") + end + end + end +end diff --git a/spec/bundler/bundler/source/git_spec.rb b/spec/bundler/bundler/source/git_spec.rb new file mode 100644 index 0000000000..f7475a35aa --- /dev/null +++ b/spec/bundler/bundler/source/git_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Source::Git do + before do + allow(Bundler).to receive(:root) { Pathname.new("root") } + end + + let(:uri) { "https://github.com/foo/bar.git" } + let(:options) do + { "uri" => uri } + end + + subject { described_class.new(options) } + + describe "#to_s" do + it "returns a description" do + expect(subject.to_s).to eq "https://github.com/foo/bar.git (at master)" + end + + context "when the URI contains credentials" do + let(:uri) { "https://my-secret-token:x-oauth-basic@github.com/foo/bar.git" } + + it "filters credentials" do + expect(subject.to_s).to eq "https://x-oauth-basic@github.com/foo/bar.git (at master)" + end + end + end +end diff --git a/spec/bundler/bundler/source/path_spec.rb b/spec/bundler/bundler/source/path_spec.rb new file mode 100644 index 0000000000..1d13e03ec1 --- /dev/null +++ b/spec/bundler/bundler/source/path_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Source::Path do + before do + allow(Bundler).to receive(:root) { Pathname.new("root") } + end + + describe "#eql?" do + subject { described_class.new("path" => "gems/a") } + + context "with two equivalent relative paths from different roots" do + let(:a_gem_opts) { { "path" => "../gems/a", "root_path" => Bundler.root.join("nested") } } + let(:a_gem) { described_class.new a_gem_opts } + + it "returns true" do + expect(subject).to eq a_gem + end + end + + context "with the same (but not equivalent) relative path from different roots" do + subject { described_class.new("path" => "gems/a") } + + let(:a_gem_opts) { { "path" => "gems/a", "root_path" => Bundler.root.join("nested") } } + let(:a_gem) { described_class.new a_gem_opts } + + it "returns false" do + expect(subject).to_not eq a_gem + end + end + end +end diff --git a/spec/bundler/bundler/source/rubygems/remote_spec.rb b/spec/bundler/bundler/source/rubygems/remote_spec.rb new file mode 100644 index 0000000000..9a7ab42128 --- /dev/null +++ b/spec/bundler/bundler/source/rubygems/remote_spec.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require "bundler/source/rubygems/remote" + +RSpec.describe Bundler::Source::Rubygems::Remote do + def remote(uri) + Bundler::Source::Rubygems::Remote.new(uri) + end + + before do + allow(Digest(:MD5)).to receive(:hexdigest).with(duck_type(:to_s)) {|string| "MD5HEX(#{string})" } + end + + let(:uri_no_auth) { URI("https://gems.example.com") } + let(:uri_with_auth) { URI("https://#{credentials}@gems.example.com") } + let(:credentials) { "username:password" } + + context "when the original URI has no credentials" do + describe "#uri" do + it "returns the original URI" do + expect(remote(uri_no_auth).uri).to eq(uri_no_auth) + end + + it "applies configured credentials" do + Bundler.settings.temporary(uri_no_auth.to_s => credentials) + expect(remote(uri_no_auth).uri).to eq(uri_with_auth) + end + end + + describe "#anonymized_uri" do + it "returns the original URI" do + expect(remote(uri_no_auth).anonymized_uri).to eq(uri_no_auth) + end + + it "does not apply given credentials" do + Bundler.settings.temporary(uri_no_auth.to_s => credentials) + expect(remote(uri_no_auth).anonymized_uri).to eq(uri_no_auth) + end + end + + describe "#cache_slug" do + it "returns the correct slug" do + expect(remote(uri_no_auth).cache_slug).to eq("gems.example.com.443.MD5HEX(gems.example.com.443./)") + end + + it "only applies the given user" do + Bundler.settings.temporary(uri_no_auth.to_s => credentials) + expect(remote(uri_no_auth).cache_slug).to eq("gems.example.com.username.443.MD5HEX(gems.example.com.username.443./)") + end + end + end + + context "when the original URI has a username and password" do + describe "#uri" do + it "returns the original URI" do + expect(remote(uri_with_auth).uri).to eq(uri_with_auth) + end + + it "does not apply configured credentials" do + Bundler.settings.temporary(uri_no_auth.to_s => "other:stuff") + expect(remote(uri_with_auth).uri).to eq(uri_with_auth) + end + end + + describe "#anonymized_uri" do + it "returns the URI without username and password" do + expect(remote(uri_with_auth).anonymized_uri).to eq(uri_no_auth) + end + + it "does not apply given credentials" do + Bundler.settings.temporary(uri_no_auth.to_s => "other:stuff") + expect(remote(uri_with_auth).anonymized_uri).to eq(uri_no_auth) + end + end + + describe "#cache_slug" do + it "returns the correct slug" do + expect(remote(uri_with_auth).cache_slug).to eq("gems.example.com.username.443.MD5HEX(gems.example.com.username.443./)") + end + + it "does not apply given credentials" do + Bundler.settings.temporary(uri_with_auth.to_s => credentials) + expect(remote(uri_with_auth).cache_slug).to eq("gems.example.com.username.443.MD5HEX(gems.example.com.username.443./)") + end + end + end + + context "when the original URI has only a username" do + let(:uri) { 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(URI("https://gem.fury.io/me/")) + end + end + + describe "#cache_slug" do + it "returns the correct slug" do + expect(remote(uri).cache_slug).to eq("gem.fury.io.SeCrEt-ToKeN.443.MD5HEX(gem.fury.io.SeCrEt-ToKeN.443./me/)") + end + end + end + + context "when a mirror with inline credentials is configured for the URI" do + let(:uri) { URI("https://rubygems.org/") } + let(:mirror_uri_with_auth) { URI("https://username:password@rubygems-mirror.org/") } + let(:mirror_uri_no_auth) { URI("https://rubygems-mirror.org/") } + + before { Bundler.settings.set_local("mirror.https://rubygems.org/", mirror_uri_with_auth.to_s) } + + specify "#uri returns the mirror URI with credentials" do + expect(remote(uri).uri).to eq(mirror_uri_with_auth) + end + + specify "#anonymized_uri returns the mirror URI without credentials" do + expect(remote(uri).anonymized_uri).to eq(mirror_uri_no_auth) + end + + specify "#original_uri returns the original source" do + expect(remote(uri).original_uri).to eq(uri) + end + + specify "#cache_slug returns the correct slug" do + expect(remote(uri).cache_slug).to eq("rubygems.org.443.MD5HEX(rubygems.org.443./)") + end + end + + context "when a mirror with configured credentials is configured for the URI" do + let(:uri) { URI("https://rubygems.org/") } + let(:mirror_uri_with_auth) { URI("https://#{credentials}@rubygems-mirror.org/") } + let(:mirror_uri_no_auth) { URI("https://rubygems-mirror.org/") } + + before do + Bundler.settings.temporary("mirror.https://rubygems.org/" => mirror_uri_no_auth.to_s) + Bundler.settings.temporary(mirror_uri_no_auth.to_s => credentials) + end + + specify "#uri returns the mirror URI with credentials" do + expect(remote(uri).uri).to eq(mirror_uri_with_auth) + end + + specify "#anonymized_uri returns the mirror URI without credentials" do + expect(remote(uri).anonymized_uri).to eq(mirror_uri_no_auth) + end + + specify "#original_uri returns the original source" do + expect(remote(uri).original_uri).to eq(uri) + end + + specify "#cache_slug returns the original source" do + expect(remote(uri).cache_slug).to eq("rubygems.org.443.MD5HEX(rubygems.org.443./)") + end + end + + context "when there is no mirror set" do + describe "#original_uri" do + it "is not set" do + expect(remote(uri_no_auth).original_uri).to be_nil + end + end + end +end diff --git a/spec/bundler/bundler/source/rubygems_spec.rb b/spec/bundler/bundler/source/rubygems_spec.rb new file mode 100644 index 0000000000..7c457a7265 --- /dev/null +++ b/spec/bundler/bundler/source/rubygems_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Source::Rubygems do + before do + allow(Bundler).to receive(:root) { Pathname.new("root") } + end + + describe "caches" do + it "includes Bundler.app_cache" do + expect(subject.caches).to include(Bundler.app_cache) + end + + it "includes GEM_PATH entries" do + Gem.path.each do |path| + expect(subject.caches).to include(File.expand_path("#{path}/cache")) + end + end + + it "is an array of strings or pathnames" do + subject.caches.each do |cache| + expect([String, Pathname]).to include(cache.class) + end + end + end + + describe "#add_remote" do + context "when the source is an HTTP(s) URI with no host" do + it "raises error" do + expect { subject.add_remote("https:rubygems.org") }.to raise_error(ArgumentError) + end + end + end +end diff --git a/spec/bundler/bundler/source_list_spec.rb b/spec/bundler/bundler/source_list_spec.rb new file mode 100644 index 0000000000..ce3353012c --- /dev/null +++ b/spec/bundler/bundler/source_list_spec.rb @@ -0,0 +1,463 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::SourceList do + before do + allow(Bundler).to receive(:root) { Pathname.new "./tmp/bundled_app" } + + stub_const "ASourcePlugin", Class.new(Bundler::Plugin::API) + ASourcePlugin.source "new_source" + allow(Bundler::Plugin).to receive(:source?).with("new_source").and_return(true) + end + + subject(:source_list) { Bundler::SourceList.new } + + let(:rubygems_aggregate) { Bundler::Source::Rubygems.new } + let(:metadata_source) { Bundler::Source::Metadata.new } + + describe "adding sources" do + before do + source_list.add_path_source("path" => "/existing/path/to/gem") + source_list.add_git_source("uri" => "git://existing-git.org/path.git") + source_list.add_rubygems_source("remotes" => ["https://existing-rubygems.org"]) + source_list.add_plugin_source("new_source", "uri" => "https://some.url/a") + end + + describe "#add_path_source" do + before do + @duplicate = source_list.add_path_source("path" => "/path/to/gem") + @new_source = source_list.add_path_source("path" => "/path/to/gem") + end + + it "returns the new path source" do + expect(@new_source).to be_instance_of(Bundler::Source::Path) + end + + it "passes the provided options to the new source" do + expect(@new_source.options).to eq("path" => "/path/to/gem") + end + + it "adds the source to the beginning of path_sources" do + expect(source_list.path_sources.first).to equal(@new_source) + end + + it "removes existing duplicates" do + expect(source_list.path_sources).not_to include equal(@duplicate) + end + end + + describe "#add_git_source" do + before do + @duplicate = source_list.add_git_source("uri" => "git://host/path.git") + @new_source = source_list.add_git_source("uri" => "git://host/path.git") + end + + it "returns the new git source" do + expect(@new_source).to be_instance_of(Bundler::Source::Git) + end + + it "passes the provided options to the new source" do + @new_source = source_list.add_git_source("uri" => "git://host/path.git") + expect(@new_source.options).to eq("uri" => "git://host/path.git") + end + + it "adds the source to the beginning of git_sources" do + @new_source = source_list.add_git_source("uri" => "git://host/path.git") + expect(source_list.git_sources.first).to equal(@new_source) + end + + it "removes existing duplicates" do + @duplicate = source_list.add_git_source("uri" => "git://host/path.git") + @new_source = source_list.add_git_source("uri" => "git://host/path.git") + expect(source_list.git_sources).not_to include equal(@duplicate) + end + + context "with the git: protocol" do + let(:msg) do + "The git source `git://existing-git.org/path.git` " \ + "uses the `git` protocol, which transmits data without encryption. " \ + "Disable this warning with `bundle config git.allow_insecure true`, " \ + "or switch to the `https` protocol to keep your data secure." + end + + it "warns about git protocols" do + expect(Bundler.ui).to receive(:warn).with(msg) + source_list.add_git_source("uri" => "git://existing-git.org/path.git") + end + + it "ignores git protocols on request" do + 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 + end + end + + describe "#add_rubygems_source" do + before do + @duplicate = source_list.add_rubygems_source("remotes" => ["https://rubygems.org/"]) + @new_source = source_list.add_rubygems_source("remotes" => ["https://rubygems.org/"]) + end + + it "returns the new rubygems source" do + expect(@new_source).to be_instance_of(Bundler::Source::Rubygems) + end + + it "passes the provided options to the new source" do + expect(@new_source.options).to eq("remotes" => ["https://rubygems.org/"]) + end + + it "adds the source to the beginning of rubygems_sources" do + expect(source_list.rubygems_sources.first).to equal(@new_source) + end + + it "removes duplicates" do + expect(source_list.rubygems_sources).not_to include equal(@duplicate) + end + end + + describe "#add_rubygems_remote", :bundler => "< 2" do + let!(:returned_source) { source_list.add_rubygems_remote("https://rubygems.org/") } + + it "returns the aggregate rubygems source" do + expect(returned_source).to be_instance_of(Bundler::Source::Rubygems) + end + + it "adds the provided remote to the beginning of the aggregate source" do + source_list.add_rubygems_remote("https://othersource.org") + expect(returned_source.remotes).to eq [ + URI("https://othersource.org/"), + URI("https://rubygems.org/"), + ] + end + end + + describe "#add_plugin_source" do + before do + @duplicate = source_list.add_plugin_source("new_source", "uri" => "http://host/path.") + @new_source = source_list.add_plugin_source("new_source", "uri" => "http://host/path.") + end + + it "returns the new plugin source" do + expect(@new_source).to be_a(Bundler::Plugin::API::Source) + end + + it "passes the provided options to the new source" do + expect(@new_source.options).to eq("uri" => "http://host/path.") + end + + it "adds the source to the beginning of git_sources" do + expect(source_list.plugin_sources.first).to equal(@new_source) + end + + it "removes existing duplicates" do + expect(source_list.plugin_sources).not_to include equal(@duplicate) + end + end + end + + describe "#all_sources" do + it "includes the aggregate rubygems source when rubygems sources have been added" do + source_list.add_git_source("uri" => "git://host/path.git") + source_list.add_rubygems_source("remotes" => ["https://rubygems.org"]) + source_list.add_path_source("path" => "/path/to/gem") + source_list.add_plugin_source("new_source", "uri" => "https://some.url/a") + + expect(source_list.all_sources).to include rubygems_aggregate + end + + it "includes the aggregate rubygems source when no rubygems sources have been added" do + source_list.add_git_source("uri" => "git://host/path.git") + source_list.add_path_source("path" => "/path/to/gem") + source_list.add_plugin_source("new_source", "uri" => "https://some.url/a") + + expect(source_list.all_sources).to include rubygems_aggregate + end + + it "returns sources of the same type in the reverse order that they were added" do + source_list.add_git_source("uri" => "git://third-git.org/path.git") + source_list.add_rubygems_source("remotes" => ["https://fifth-rubygems.org"]) + source_list.add_path_source("path" => "/third/path/to/gem") + source_list.add_plugin_source("new_source", "uri" => "https://some.url/b") + source_list.add_rubygems_source("remotes" => ["https://fourth-rubygems.org"]) + source_list.add_path_source("path" => "/second/path/to/gem") + source_list.add_rubygems_source("remotes" => ["https://third-rubygems.org"]) + source_list.add_plugin_source("new_source", "uri" => "https://some.o.url/") + source_list.add_git_source("uri" => "git://second-git.org/path.git") + source_list.add_rubygems_source("remotes" => ["https://second-rubygems.org"]) + source_list.add_path_source("path" => "/first/path/to/gem") + source_list.add_plugin_source("new_source", "uri" => "https://some.url/c") + source_list.add_rubygems_source("remotes" => ["https://first-rubygems.org"]) + source_list.add_git_source("uri" => "git://first-git.org/path.git") + + expect(source_list.all_sources).to eq [ + Bundler::Source::Path.new("path" => "/first/path/to/gem"), + Bundler::Source::Path.new("path" => "/second/path/to/gem"), + Bundler::Source::Path.new("path" => "/third/path/to/gem"), + Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"), + Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"), + Bundler::Source::Git.new("uri" => "git://third-git.org/path.git"), + ASourcePlugin.new("uri" => "https://some.url/c"), + ASourcePlugin.new("uri" => "https://some.o.url/"), + ASourcePlugin.new("uri" => "https://some.url/b"), + Bundler::Source::Rubygems.new("remotes" => ["https://first-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://second-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://third-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://fourth-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://fifth-rubygems.org"]), + rubygems_aggregate, + metadata_source, + ] + end + end + + describe "#path_sources" do + it "returns an empty array when no path sources have been added" do + source_list.add_rubygems_remote("https://rubygems.org") + source_list.add_git_source("uri" => "git://host/path.git") + expect(source_list.path_sources).to be_empty + end + + it "returns path sources in the reverse order that they were added" do + source_list.add_git_source("uri" => "git://third-git.org/path.git") + source_list.add_rubygems_remote("https://fifth-rubygems.org") + source_list.add_path_source("path" => "/third/path/to/gem") + source_list.add_rubygems_remote("https://fourth-rubygems.org") + source_list.add_path_source("path" => "/second/path/to/gem") + source_list.add_rubygems_remote("https://third-rubygems.org") + source_list.add_git_source("uri" => "git://second-git.org/path.git") + source_list.add_rubygems_remote("https://second-rubygems.org") + source_list.add_path_source("path" => "/first/path/to/gem") + source_list.add_rubygems_remote("https://first-rubygems.org") + source_list.add_git_source("uri" => "git://first-git.org/path.git") + + expect(source_list.path_sources).to eq [ + Bundler::Source::Path.new("path" => "/first/path/to/gem"), + Bundler::Source::Path.new("path" => "/second/path/to/gem"), + Bundler::Source::Path.new("path" => "/third/path/to/gem"), + ] + end + end + + describe "#git_sources" do + it "returns an empty array when no git sources have been added" do + source_list.add_rubygems_remote("https://rubygems.org") + source_list.add_path_source("path" => "/path/to/gem") + + expect(source_list.git_sources).to be_empty + end + + it "returns git sources in the reverse order that they were added" do + source_list.add_git_source("uri" => "git://third-git.org/path.git") + source_list.add_rubygems_remote("https://fifth-rubygems.org") + source_list.add_path_source("path" => "/third/path/to/gem") + source_list.add_rubygems_remote("https://fourth-rubygems.org") + source_list.add_path_source("path" => "/second/path/to/gem") + source_list.add_rubygems_remote("https://third-rubygems.org") + source_list.add_git_source("uri" => "git://second-git.org/path.git") + source_list.add_rubygems_remote("https://second-rubygems.org") + source_list.add_path_source("path" => "/first/path/to/gem") + source_list.add_rubygems_remote("https://first-rubygems.org") + source_list.add_git_source("uri" => "git://first-git.org/path.git") + + expect(source_list.git_sources).to eq [ + Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"), + Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"), + Bundler::Source::Git.new("uri" => "git://third-git.org/path.git"), + ] + end + end + + describe "#plugin_sources" do + it "returns an empty array when no plugin sources have been added" do + source_list.add_rubygems_remote("https://rubygems.org") + source_list.add_path_source("path" => "/path/to/gem") + + expect(source_list.plugin_sources).to be_empty + end + + it "returns plugin sources in the reverse order that they were added" do + source_list.add_plugin_source("new_source", "uri" => "https://third-git.org/path.git") + source_list.add_git_source("https://new-git.org") + source_list.add_path_source("path" => "/third/path/to/gem") + source_list.add_rubygems_remote("https://fourth-rubygems.org") + source_list.add_path_source("path" => "/second/path/to/gem") + source_list.add_rubygems_remote("https://third-rubygems.org") + source_list.add_plugin_source("new_source", "uri" => "git://second-git.org/path.git") + source_list.add_rubygems_remote("https://second-rubygems.org") + source_list.add_path_source("path" => "/first/path/to/gem") + source_list.add_rubygems_remote("https://first-rubygems.org") + source_list.add_plugin_source("new_source", "uri" => "git://first-git.org/path.git") + + expect(source_list.plugin_sources).to eq [ + ASourcePlugin.new("uri" => "git://first-git.org/path.git"), + ASourcePlugin.new("uri" => "git://second-git.org/path.git"), + ASourcePlugin.new("uri" => "https://third-git.org/path.git"), + ] + end + end + + describe "#rubygems_sources" do + it "includes the aggregate rubygems source when rubygems sources have been added" do + source_list.add_git_source("uri" => "git://host/path.git") + source_list.add_rubygems_source("remotes" => ["https://rubygems.org"]) + source_list.add_path_source("path" => "/path/to/gem") + + expect(source_list.rubygems_sources).to include rubygems_aggregate + end + + it "returns only the aggregate rubygems source when no rubygems sources have been added" do + source_list.add_git_source("uri" => "git://host/path.git") + source_list.add_path_source("path" => "/path/to/gem") + + expect(source_list.rubygems_sources).to eq [rubygems_aggregate] + end + + it "returns rubygems sources in the reverse order that they were added" do + source_list.add_git_source("uri" => "git://third-git.org/path.git") + source_list.add_rubygems_source("remotes" => ["https://fifth-rubygems.org"]) + source_list.add_path_source("path" => "/third/path/to/gem") + source_list.add_rubygems_source("remotes" => ["https://fourth-rubygems.org"]) + source_list.add_path_source("path" => "/second/path/to/gem") + source_list.add_rubygems_source("remotes" => ["https://third-rubygems.org"]) + source_list.add_git_source("uri" => "git://second-git.org/path.git") + source_list.add_rubygems_source("remotes" => ["https://second-rubygems.org"]) + source_list.add_path_source("path" => "/first/path/to/gem") + source_list.add_rubygems_source("remotes" => ["https://first-rubygems.org"]) + source_list.add_git_source("uri" => "git://first-git.org/path.git") + + expect(source_list.rubygems_sources).to eq [ + Bundler::Source::Rubygems.new("remotes" => ["https://first-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://second-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://third-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://fourth-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://fifth-rubygems.org"]), + rubygems_aggregate, + ] + end + end + + describe "#get" do + context "when it includes an equal source" do + let(:rubygems_source) { Bundler::Source::Rubygems.new("remotes" => ["https://rubygems.org"]) } + before { @equal_source = source_list.add_rubygems_remote("https://rubygems.org") } + + it "returns the equal source" do + expect(source_list.get(rubygems_source)).to be @equal_source + end + end + + context "when it does not include an equal source" do + let(:path_source) { Bundler::Source::Path.new("path" => "/path/to/gem") } + + it "returns nil" do + expect(source_list.get(path_source)).to be_nil + end + end + end + + describe "#lock_sources" do + before do + source_list.add_git_source("uri" => "git://third-git.org/path.git") + source_list.add_rubygems_source("remotes" => ["https://duplicate-rubygems.org"]) + source_list.add_plugin_source("new_source", "uri" => "https://third-bar.org/foo") + source_list.add_path_source("path" => "/third/path/to/gem") + source_list.add_rubygems_source("remotes" => ["https://third-rubygems.org"]) + source_list.add_path_source("path" => "/second/path/to/gem") + source_list.add_rubygems_source("remotes" => ["https://second-rubygems.org"]) + source_list.add_git_source("uri" => "git://second-git.org/path.git") + source_list.add_rubygems_source("remotes" => ["https://first-rubygems.org"]) + source_list.add_plugin_source("new_source", "uri" => "https://second-plugin.org/random") + source_list.add_path_source("path" => "/first/path/to/gem") + source_list.add_rubygems_source("remotes" => ["https://duplicate-rubygems.org"]) + source_list.add_git_source("uri" => "git://first-git.org/path.git") + end + + it "combines the rubygems sources into a single instance, removing duplicate remotes from the end", :bundler => "< 2" do + expect(source_list.lock_sources).to eq [ + Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"), + Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"), + Bundler::Source::Git.new("uri" => "git://third-git.org/path.git"), + ASourcePlugin.new("uri" => "https://second-plugin.org/random"), + ASourcePlugin.new("uri" => "https://third-bar.org/foo"), + Bundler::Source::Path.new("path" => "/first/path/to/gem"), + Bundler::Source::Path.new("path" => "/second/path/to/gem"), + Bundler::Source::Path.new("path" => "/third/path/to/gem"), + Bundler::Source::Rubygems.new("remotes" => [ + "https://duplicate-rubygems.org", + "https://first-rubygems.org", + "https://second-rubygems.org", + "https://third-rubygems.org", + ]), + ] + end + + it "returns all sources, without combining rubygems sources", :bundler => "2" do + expect(source_list.lock_sources).to eq [ + Bundler::Source::Rubygems.new, + Bundler::Source::Rubygems.new("remotes" => ["https://duplicate-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://first-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://second-rubygems.org"]), + Bundler::Source::Rubygems.new("remotes" => ["https://third-rubygems.org"]), + Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"), + Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"), + Bundler::Source::Git.new("uri" => "git://third-git.org/path.git"), + Bundler::Source::Path.new("path" => "/first/path/to/gem"), + Bundler::Source::Path.new("path" => "/second/path/to/gem"), + Bundler::Source::Path.new("path" => "/third/path/to/gem"), + ASourcePlugin.new("uri" => "https://second-plugin.org/random"), + ASourcePlugin.new("uri" => "https://third-bar.org/foo"), + ] + end + end + + describe "replace_sources!" do + let(:existing_locked_source) { Bundler::Source::Path.new("path" => "/existing/path") } + let(:removed_locked_source) { Bundler::Source::Path.new("path" => "/removed/path") } + + let(:locked_sources) { [existing_locked_source, removed_locked_source] } + + before do + @existing_source = source_list.add_path_source("path" => "/existing/path") + @new_source = source_list.add_path_source("path" => "/new/path") + source_list.replace_sources!(locked_sources) + end + + it "maintains the order and number of sources" do + expect(source_list.path_sources).to eq [@new_source, @existing_source] + end + + it "retains the same instance of the new source" do + expect(source_list.path_sources[0]).to be @new_source + end + + it "replaces the instance of the existing source" do + expect(source_list.path_sources[1]).to be existing_locked_source + end + end + + describe "#cached!" do + let(:rubygems_source) { source_list.add_rubygems_source("remotes" => ["https://rubygems.org"]) } + let(:git_source) { source_list.add_git_source("uri" => "git://host/path.git") } + let(:path_source) { source_list.add_path_source("path" => "/path/to/gem") } + + it "calls #cached! on all the sources" do + expect(rubygems_source).to receive(:cached!) + expect(git_source).to receive(:cached!) + expect(path_source).to receive(:cached!) + source_list.cached! + end + end + + describe "#remote!" do + let(:rubygems_source) { source_list.add_rubygems_source("remotes" => ["https://rubygems.org"]) } + let(:git_source) { source_list.add_git_source("uri" => "git://host/path.git") } + let(:path_source) { source_list.add_path_source("path" => "/path/to/gem") } + + it "calls #remote! on all the sources" do + expect(rubygems_source).to receive(:remote!) + expect(git_source).to receive(:remote!) + expect(path_source).to receive(:remote!) + source_list.remote! + end + end +end diff --git a/spec/bundler/bundler/source_spec.rb b/spec/bundler/bundler/source_spec.rb new file mode 100644 index 0000000000..9ef8e7e50f --- /dev/null +++ b/spec/bundler/bundler/source_spec.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Source do + class ExampleSource < Bundler::Source + end + + subject { ExampleSource.new } + + describe "#unmet_deps" do + let(:specs) { double(:specs) } + let(:unmet_dependency_names) { double(:unmet_dependency_names) } + + before do + allow(subject).to receive(:specs).and_return(specs) + allow(specs).to receive(:unmet_dependency_names).and_return(unmet_dependency_names) + end + + it "should return the names of unmet dependencies" do + expect(subject.unmet_deps).to eq(unmet_dependency_names) + end + end + + describe "#version_message" do + 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 + expect(subject.version_message(spec)).to eq("nokogiri >= 1.6") + end + 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) } + + 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") } + + 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") } + + context "with color" do + before { Bundler.ui = Bundler::UI::Shell.new } + + 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") + end + end + + context "without color" do + 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)") + 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") } + + context "with color" do + before { Bundler.ui = Bundler::UI::Shell.new } + + 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") + 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") } + + context "with color" do + before { Bundler.ui = Bundler::UI::Shell.new } + + 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") + 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) } + + it "should return true" do + expect(subject.can_lock?(spec)).to be_truthy + end + end + + context "when the passed spec's source is not equivalent" do + let(:spec) { double(:spec, :source => double(:other_source)) } + + it "should return false" do + expect(subject.can_lock?(spec)).to be_falsey + end + end + end + + describe "#include?" do + context "when the passed source is equivalent" do + let(:source) { subject } + + it "should return true" do + expect(subject).to include(source) + end + end + + context "when the passed source is not equivalent" do + let(:source) { double(:source) } + + it "should return false" do + expect(subject).to_not include(source) + end + end + end +end diff --git a/spec/bundler/bundler/spec_set_spec.rb b/spec/bundler/bundler/spec_set_spec.rb new file mode 100644 index 0000000000..6fedd38b50 --- /dev/null +++ b/spec/bundler/bundler/spec_set_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::SpecSet do + let(:specs) do + [ + build_spec("a", "1.0"), + build_spec("b", "1.0"), + build_spec("c", "1.1") do |s| + s.dep "a", "< 2.0" + s.dep "e", "> 0" + end, + build_spec("d", "2.0") do |s| + s.dep "a", "1.0" + s.dep "c", "~> 1.0" + end, + build_spec("e", "1.0.0.pre.1"), + ].flatten + end + + subject { described_class.new(specs) } + + context "enumerable methods" do + it "has a length" do + expect(subject.length).to eq(5) + end + + it "has a size" do + expect(subject.size).to eq(5) + end + end + + describe "#find_by_name_and_platform" do + let(:platform) { Gem::Platform.new("universal-darwin-64") } + let(:platform_spec) { build_spec("b", "2.0", platform).first } + let(:specs) do + [ + build_spec("a", "1.0"), + platform_spec, + ].flatten + end + + it "finds spec with given name and platform" do + spec = described_class.new(specs).find_by_name_and_platform("b", platform) + expect(spec).to eq platform_spec + 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[ + a-1.0 + b-1.0 + e-1.0.0.pre.1 + c-1.1 + d-2.0 + ] + end + end +end diff --git a/spec/bundler/bundler/ssl_certs/certificate_manager_spec.rb b/spec/bundler/bundler/ssl_certs/certificate_manager_spec.rb new file mode 100644 index 0000000000..4250bfc497 --- /dev/null +++ b/spec/bundler/bundler/ssl_certs/certificate_manager_spec.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +require "bundler/ssl_certs/certificate_manager" + +RSpec.describe Bundler::SSLCerts::CertificateManager do + let(:rubygems_path) { root } + let(:stub_cert) { File.join(root.to_s, "lib", "rubygems", "ssl_certs", "rubygems.org", "ssl-cert.pem") } + let(:rubygems_certs_dir) { File.join(root.to_s, "lib", "rubygems", "ssl_certs", "rubygems.org") } + + subject { described_class.new(rubygems_path) } + + # Pretend bundler root is rubygems root + before do + # Backing up rubygems ceriticates + FileUtils.mv(rubygems_certs_dir, rubygems_certs_dir + ".back") if ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"] + + FileUtils.mkdir_p(rubygems_certs_dir) + FileUtils.touch(stub_cert) + end + + after do + FileUtils.rm_rf(rubygems_certs_dir) + + # Restore rubygems certificates + FileUtils.mv(rubygems_certs_dir + ".back", rubygems_certs_dir) if ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"] + end + + describe "#update_from" do + let(:cert_manager) { double(:cert_manager) } + + before { allow(described_class).to receive(:new).with(rubygems_path).and_return(cert_manager) } + + it "should update the certs through a new certificate manager" do + allow(cert_manager).to receive(:update!) + expect(described_class.update_from!(rubygems_path)).to be_nil + end + end + + describe "#initialize" do + it "should set bundler_cert_path as path of the subdir with bundler ssl certs" do + expect(subject.bundler_cert_path).to eq(File.join(root, "lib/bundler/ssl_certs")) + end + + it "should set bundler_certs as the paths of the bundler ssl certs" do + expect(subject.bundler_certs).to include(File.join(root, "lib/bundler/ssl_certs/rubygems.global.ssl.fastly.net/DigiCertHighAssuranceEVRootCA.pem")) + expect(subject.bundler_certs).to include(File.join(root, "lib/bundler/ssl_certs/index.rubygems.org/GlobalSignRootCA.pem")) + end + + context "when rubygems_path is not nil" do + it "should set rubygems_certs" do + expect(subject.rubygems_certs).to include(File.join(root, "lib", "rubygems", "ssl_certs", "rubygems.org", "ssl-cert.pem")) + end + end + end + + describe "#up_to_date?" do + context "when bundler certs and rubygems certs are the same" do + before do + bundler_certs = Dir[File.join(root.to_s, "lib", "bundler", "ssl_certs", "**", "*.pem")] + FileUtils.rm(stub_cert) + FileUtils.cp(bundler_certs, rubygems_certs_dir) + end + + it "should return true" do + expect(subject).to be_up_to_date + end + end + + context "when bundler certs and rubygems certs are not the same" do + it "should return false" do + expect(subject).to_not be_up_to_date + end + end + end + + describe "#update!" do + context "when certificate manager is not up to date" do + before do + allow(subject).to receive(:up_to_date?).and_return(false) + allow(bundler_fileutils).to receive(:rm) + allow(bundler_fileutils).to receive(:cp) + end + + it "should remove the current bundler certs" do + expect(bundler_fileutils).to receive(:rm).with(subject.bundler_certs) + subject.update! + end + + it "should copy the rubygems certs into bundler certs" do + expect(bundler_fileutils).to receive(:cp).with(subject.rubygems_certs, subject.bundler_cert_path) + subject.update! + end + + it "should return nil" do + expect(subject.update!).to be_nil + end + end + + context "when certificate manager is up to date" do + before { allow(subject).to receive(:up_to_date?).and_return(true) } + + it "should return nil" do + expect(subject.update!).to be_nil + end + end + end + + describe "#connect_to" do + let(:host) { "http://www.host.com" } + let(:http) { Net::HTTP.new(host, 443) } + let(:cert_store) { OpenSSL::X509::Store.new } + let(:http_header_response) { double(:http_header_response) } + + before do + allow(Net::HTTP).to receive(:new).with(host, 443).and_return(http) + allow(OpenSSL::X509::Store).to receive(:new).and_return(cert_store) + allow(http).to receive(:head).with("/").and_return(http_header_response) + end + + it "should use ssl for the http request" do + expect(http).to receive(:use_ssl=).with(true) + subject.connect_to(host) + end + + it "use verify peer mode" do + expect(http).to receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER) + subject.connect_to(host) + end + + it "set its cert store as a OpenSSL::X509::Store populated with bundler certs" do + expect(cert_store).to receive(:add_file).at_least(:once) + expect(http).to receive(:cert_store=).with(cert_store) + subject.connect_to(host) + end + + it "return the headers of the request response" do + expect(subject.connect_to(host)).to eq(http_header_response) + end + end +end diff --git a/spec/bundler/bundler/stub_specification_spec.rb b/spec/bundler/bundler/stub_specification_spec.rb new file mode 100644 index 0000000000..5521d83769 --- /dev/null +++ b/spec/bundler/bundler/stub_specification_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::StubSpecification do + let(:gemspec) do + Gem::Specification.new do |s| + s.name = "gemname" + s.version = "1.0.0" + s.loaded_from = __FILE__ + end + end + + let(:with_bundler_stub_spec) do + described_class.from_stub(gemspec) + end + + if Bundler.rubygems.provides?(">= 2.1") + describe "#from_stub" do + it "returns the same stub if already a Bundler::StubSpecification" do + stub = described_class.from_stub(with_bundler_stub_spec) + expect(stub).to be(with_bundler_stub_spec) + end + end + end +end diff --git a/spec/bundler/bundler/ui/shell_spec.rb b/spec/bundler/bundler/ui/shell_spec.rb new file mode 100644 index 0000000000..951a446aff --- /dev/null +++ b/spec/bundler/bundler/ui/shell_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::UI::Shell do + subject { described_class.new } + + before { subject.level = "debug" } + + describe "#info" do + before { subject.level = "info" } + it "prints to stdout" do + expect { subject.info("info") }.to output("info\n").to_stdout + end + end + + describe "#confirm" do + before { subject.level = "confirm" } + it "prints to stdout" do + expect { subject.confirm("confirm") }.to output("confirm\n").to_stdout + end + end + + describe "#warn" do + before { subject.level = "warn" } + it "prints to stdout", :bundler => "< 2" do + expect { subject.warn("warning") }.to output("warning\n").to_stdout + end + + it "prints to stderr", :bundler => "2" do + expect { subject.warn("warning") }.to output("warning\n").to_stderr + end + + context "when stderr flag is enabled" do + before { Bundler.settings.temporary(:error_on_stderr => true) } + it "prints to stderr" do + expect { subject.warn("warning!") }.to output("warning!\n").to_stderr + end + end + end + + describe "#debug" do + it "prints to stdout" do + expect { subject.debug("debug") }.to output("debug\n").to_stdout + end + end + + describe "#error" do + before { subject.level = "error" } + + it "prints to stdout", :bundler => "< 2" do + expect { subject.error("error!!!") }.to output("error!!!\n").to_stdout + end + + it "prints to stderr", :bundler => "2" do + expect { subject.error("error!!!") }.to output("error!!!\n").to_stderr + end + + context "when stderr flag is enabled" do + before { Bundler.settings.temporary(:error_on_stderr => true) } + it "prints to stderr" do + expect { subject.error("error!!!") }.to output("error!!!\n").to_stderr + end + + context "when stderr is closed" do + it "doesn't report anything" do + output = capture(:stderr, :closed => true) do + subject.error("Something went wrong") + end + expect(output).to_not eq("Something went wrong\n") + end + end + end + end +end diff --git a/spec/bundler/bundler/ui_spec.rb b/spec/bundler/bundler/ui_spec.rb new file mode 100644 index 0000000000..6ef8729277 --- /dev/null +++ b/spec/bundler/bundler/ui_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::UI do + describe Bundler::UI::Silent do + it "has the same instance methods as Shell", :ruby => ">= 1.9" do + shell = Bundler::UI::Shell + methods = proc do |cls| + cls.instance_methods.map do |i| + m = shell.instance_method(i) + [i, m.parameters] + end.sort_by(&:first) + end + expect(methods.call(described_class)).to eq(methods.call(shell)) + end + + it "has the same instance class as Shell", :ruby => ">= 1.9" do + shell = Bundler::UI::Shell + methods = proc do |cls| + cls.methods.map do |i| + m = shell.method(i) + [i, m.parameters] + end.sort_by(&:first) + end + expect(methods.call(described_class)).to eq(methods.call(shell)) + end + end + + describe Bundler::UI::Shell do + let(:options) { {} } + subject { described_class.new(options) } + describe "debug?" do + it "returns a boolean" do + subject.level = :debug + expect(subject.debug?).to eq(true) + + subject.level = :error + expect(subject.debug?).to eq(false) + end + end + end +end diff --git a/spec/bundler/bundler/uri_credentials_filter_spec.rb b/spec/bundler/bundler/uri_credentials_filter_spec.rb new file mode 100644 index 0000000000..fe52d16306 --- /dev/null +++ b/spec/bundler/bundler/uri_credentials_filter_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::URICredentialsFilter do + subject { described_class } + + describe "#credential_filtered_uri" do + shared_examples_for "original type of uri is maintained" do + it "maintains same type for return value as uri input type" do + expect(subject.credential_filtered_uri(uri)).to be_kind_of(uri.class) + end + end + + shared_examples_for "sensitive credentials in uri are filtered out" do + context "authentication using oauth credentials" do + context "specified via 'x-oauth-basic'" 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(URI("https://x-oauth-basic@github.com/company/private-repo").to_s) + end + + it_behaves_like "original type of uri is maintained" + end + + context "specified via 'x'" 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(URI("https://x@github.com/company/private-repo").to_s) + end + + it_behaves_like "original type of uri is maintained" + end + end + + context "authentication using login credentials" do + let(:credentials) { "username1:hunter3@" } + + it "returns the uri without the password" do + expect(subject.credential_filtered_uri(uri).to_s).to eq(URI("https://username1@github.com/company/private-repo").to_s) + end + + it_behaves_like "original type of uri is maintained" + end + + context "authentication without credentials" do + let(:credentials) { "" } + + it "returns the same uri" do + expect(subject.credential_filtered_uri(uri).to_s).to eq(uri.to_s) + end + + it_behaves_like "original type of uri is maintained" + end + end + + context "uri is a uri object" do + let(:uri) { URI("https://#{credentials}github.com/company/private-repo") } + + it_behaves_like "sensitive credentials in uri are filtered out" + end + + context "uri is a uri string" do + let(:uri) { "https://#{credentials}github.com/company/private-repo" } + + it_behaves_like "sensitive credentials in uri are filtered out" + end + + context "uri is a non-uri format string (ex. path)" do + let(:uri) { "/path/to/repo" } + + it "returns the same uri" do + expect(subject.credential_filtered_uri(uri).to_s).to eq(uri.to_s) + end + + it_behaves_like "original type of uri is maintained" + end + + context "uri is nil" do + let(:uri) { nil } + + it "returns nil" do + expect(subject.credential_filtered_uri(uri)).to be_nil + end + + it_behaves_like "original type of uri is maintained" + end + end + + describe "#credential_filtered_string" do + let(:str_to_filter) { "This is a git message containing a uri #{uri}!" } + let(:credentials) { "" } + let(:uri) { URI("https://#{credentials}github.com/company/private-repo") } + + context "with a uri that contains credentials" do + let(:credentials) { "oauth_token:x-oauth-basic@" } + + it "returns the string without the sensitive credentials" do + expect(subject.credential_filtered_string(str_to_filter, uri)).to eq( + "This is a git message containing a uri https://x-oauth-basic@github.com/company/private-repo!" + ) + end + end + + context "that does not contains credentials" do + it "returns the same string" do + expect(subject.credential_filtered_string(str_to_filter, uri)).to eq(str_to_filter) + end + end + + context "string to filter is nil" do + let(:str_to_filter) { nil } + + it "returns nil" do + expect(subject.credential_filtered_string(str_to_filter, uri)).to be_nil + end + end + + context "uri to filter out is nil" do + let(:uri) { nil } + + it "returns the same string" do + expect(subject.credential_filtered_string(str_to_filter, uri)).to eq(str_to_filter) + end + end + end +end diff --git a/spec/bundler/bundler/vendored_persistent_spec.rb b/spec/bundler/bundler/vendored_persistent_spec.rb new file mode 100644 index 0000000000..338431c4a6 --- /dev/null +++ b/spec/bundler/bundler/vendored_persistent_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require "spec_helper" +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(subject.http_class) } + let(:tls_version) { "TLSv1.2" } + let(:socket) { double("Socket") } + let(:socket_io) { double("SocketIO") } + + before do + allow(connection).to receive(:use_ssl?).and_return(!tls_version.nil?) + allow(socket).to receive(:io).and_return(socket_io) + 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(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(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 new file mode 100644 index 0000000000..ccbb9285d5 --- /dev/null +++ b/spec/bundler/bundler/version_ranges_spec.rb @@ -0,0 +1,37 @@ +# 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, "!= 1", "< 2", "> 2" + include_examples "empty?", true, "!= 1", "<= 1", ">= 1" + include_examples "empty?", true, "< 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/worker_spec.rb b/spec/bundler/bundler/worker_spec.rb new file mode 100644 index 0000000000..2e5642709d --- /dev/null +++ b/spec/bundler/bundler/worker_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "bundler/worker" + +RSpec.describe Bundler::Worker do + let(:size) { 5 } + let(:name) { "Spec Worker" } + let(:function) { proc {|object, worker_number| [object, worker_number] } } + subject { described_class.new(size, name, function) } + + after { subject.stop } + + describe "#initialize" do + context "when Thread.start raises ThreadError" do + it "raises when no threads can be created" do + allow(Thread).to receive(:start).and_raise(ThreadError, "error creating thread") + + expect { subject.enq "a" }.to raise_error(Bundler::ThreadCreationError, "Failed to create threads for the Spec Worker worker: error creating thread") + end + end + end +end diff --git a/spec/bundler/bundler/yaml_serializer_spec.rb b/spec/bundler/bundler/yaml_serializer_spec.rb new file mode 100644 index 0000000000..1241c74bbf --- /dev/null +++ b/spec/bundler/bundler/yaml_serializer_spec.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +require "bundler/yaml_serializer" + +RSpec.describe Bundler::YAMLSerializer do + subject(:serializer) { Bundler::YAMLSerializer } + + describe "#dump" do + it "works for simple hash" do + hash = { "Q" => "Where does Thursday come before Wednesday? In the dictionary. :P" } + + expected = strip_whitespace <<-YAML + --- + Q: "Where does Thursday come before Wednesday? In the dictionary. :P" + YAML + + expect(serializer.dump(hash)).to eq(expected) + end + + it "handles nested hash" do + hash = { + "nice-one" => { + "read_ahead" => "All generalizations are false, including this one", + }, + } + + expected = strip_whitespace <<-YAML + --- + nice-one: + read_ahead: "All generalizations are false, including this one" + YAML + + expect(serializer.dump(hash)).to eq(expected) + end + + it "array inside an hash" do + hash = { + "nested_hash" => { + "contains_array" => [ + "Jack and Jill went up the hill", + "To fetch a pail of water.", + "Jack fell down and broke his crown,", + "And Jill came tumbling after.", + ], + }, + } + + expected = strip_whitespace <<-YAML + --- + nested_hash: + contains_array: + - "Jack and Jill went up the hill" + - "To fetch a pail of water." + - "Jack fell down and broke his crown," + - "And Jill came tumbling after." + YAML + + expect(serializer.dump(hash)).to eq(expected) + end + end + + describe "#load" do + it "works for simple hash" do + yaml = strip_whitespace <<-YAML + --- + Jon: "Air is free dude!" + Jack: "Yes.. until you buy a bag of chips!" + YAML + + hash = { + "Jon" => "Air is free dude!", + "Jack" => "Yes.. until you buy a bag of chips!", + } + + expect(serializer.load(yaml)).to eq(hash) + end + + it "works for nested hash" do + yaml = strip_whitespace <<-YAML + --- + baa: + baa: "black sheep" + have: "you any wool?" + yes: "merry have I" + three: "bags full" + YAML + + hash = { + "baa" => { + "baa" => "black sheep", + "have" => "you any wool?", + "yes" => "merry have I", + }, + "three" => "bags full", + } + + expect(serializer.load(yaml)).to eq(hash) + end + + it "handles colon in key/value" do + yaml = strip_whitespace <<-YAML + BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/: http://rubygems-mirror.org + YAML + + expect(serializer.load(yaml)).to eq("BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/" => "http://rubygems-mirror.org") + end + + it "handles arrays inside hashes" do + yaml = strip_whitespace <<-YAML + --- + nested_hash: + contains_array: + - "Why shouldn't you write with a broken pencil?" + - "Because it's pointless!" + YAML + + hash = { + "nested_hash" => { + "contains_array" => [ + "Why shouldn't you write with a broken pencil?", + "Because it's pointless!", + ], + }, + } + + expect(serializer.load(yaml)).to eq(hash) + end + + it "handles windows-style CRLF line endings" do + yaml = strip_whitespace(<<-YAML).gsub("\n", "\r\n") + --- + nested_hash: + contains_array: + - "Why shouldn't you write with a broken pencil?" + - "Because it's pointless!" + - oh so silly + YAML + + hash = { + "nested_hash" => { + "contains_array" => [ + "Why shouldn't you write with a broken pencil?", + "Because it's pointless!", + "oh so silly", + ], + }, + } + + expect(serializer.load(yaml)).to eq(hash) + end + end + + describe "against yaml lib" do + let(:hash) do + { + "a_joke" => { + "my-stand" => "I can totally keep secrets", + "but" => "The people I tell them to can't :P", + "wouldn't it be funny if this string were empty?" => "", + }, + "more" => { + "first" => [ + "Can a kangaroo jump higher than a house?", + "Of course, a house doesn't jump at all.", + ], + "second" => [ + "What did the sea say to the sand?", + "Nothing, it simply waved.", + ], + "array with empty string" => [""], + }, + "sales" => { + "item" => "A Parachute", + "description" => "Only used once, never opened.", + }, + "one-more" => "I'd tell you a chemistry joke but I know I wouldn't get a reaction.", + } + end + + context "#load" do + it "retrieves the original hash" do + require "yaml" + expect(serializer.load(YAML.dump(hash))).to eq(hash) + end + end + + context "#dump" do + it "retrieves the original hash" do + require "yaml" + expect(YAML.load(serializer.dump(hash))).to eq(hash) + end + end + end +end |