diff options
author | hsbt <hsbt@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2017-09-08 08:45:41 +0000 |
---|---|---|
committer | hsbt <hsbt@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2017-09-08 08:45:41 +0000 |
commit | 8598f8c2dc78c6d1ae87cb6ae19c34ba2cb29241 (patch) | |
tree | 0bbd28f684e745cb212761b7c74fe08668f85cc8 /spec | |
parent | f2e04b77aa8a363d7e36ce5a9cdb60714a537a3c (diff) | |
download | ruby-8598f8c2dc78c6d1ae87cb6ae19c34ba2cb29241.tar.gz |
Merge bundler to standard libraries.
rubygems 2.7.x depends bundler-1.15.x. This is preparation for
rubygems and bundler migration.
* lib/bundler.rb, lib/bundler/*: files of bundler-1.15.4
* spec/bundler/*: rspec examples of bundler-1.15.4. I applied patches.
* https://github.com/bundler/bundler/pull/6007
* Exclude not working examples on ruby repository.
* Fake ruby interpriter instead of installed ruby.
* Makefile.in: Added test task named `test-bundler`. This task is only
working macOS/linux yet. I'm going to support Windows environment later.
* tool/sync_default_gems.rb: Added sync task for bundler.
[Feature #12733][ruby-core:77172]
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@59779 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'spec')
201 files changed, 35679 insertions, 1 deletions
diff --git a/spec/README.md b/spec/README.md index 3c32a6727e..a17a93f8cc 100644 --- a/spec/README.md +++ b/spec/README.md @@ -1,4 +1,15 @@ -# ruby/spec +# spec/bundler + +spec/bundler is rspec examples for bundler library(lib/bundler.rb, lib/bundler/*). + +## Running spec/bundler + +To run rspec for bundler: +```bash +make test-bundler +``` + +# spec/rubyspec ruby/spec (https://github.com/ruby/spec/) is a test suite for the Ruby language. diff --git a/spec/bundler/bundler/bundler_spec.rb b/spec/bundler/bundler/bundler_spec.rb new file mode 100644 index 0000000000..268c0d99ac --- /dev/null +++ b/spec/bundler/bundler/bundler_spec.rb @@ -0,0 +1,212 @@ +# encoding: utf-8 +# frozen_string_literal: true +require "spec_helper" +require "bundler" + +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 + 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 = {} + settings = { :disable_shared_gems => true } + Bundler.send(:configure_gem_path, env, settings) + 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(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 "#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 + 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 +end diff --git a/spec/bundler/bundler/cli_spec.rb b/spec/bundler/bundler/cli_spec.rb new file mode 100644 index 0000000000..07ae92719c --- /dev/null +++ b/spec/bundler/bundler/cli_spec.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true +require "spec_helper" +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| + f.puts "#!/usr/bin/env 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 "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(out).to start_with("Running `bundle info bundler --no-color --verbose` with bundler #{Bundler::VERSION}") + end + + it "doesn't print defaults" do + install_gemfile! "", :verbose => true + expect(out).to start_with("Running `bundle install --no-color --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(err + out).to eq("Could not find command \"fail\".") + end + end + + let(:bundler_version) { "1.1" } + let(:latest_version) { nil } + before do + 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) { "2.0" } + it "prints the version warning" do + bundle "fail" + expect(err + out).to eq(<<-EOS.strip) +The latest bundler is #{latest_version}, but you are currently running #{bundler_version}. +To update, run `gem install bundler` +Could not find command "fail". + 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(out).to eq "" + + bundle "platform --ruby" + expect(out).to eq "Could not locate Gemfile" + end + end + + context "and is a pre-release" do + let(:latest_version) { "2.0.0.pre.4" } + it "prints the version warning" do + bundle "fail" + expect(err + out).to eq(<<-EOS.strip) +The latest bundler is #{latest_version}, but you are currently running #{bundler_version}. +To update, run `gem install bundler --pre` +Could not find command "fail". + EOS + end + end + end + end +end + +RSpec.describe "bundler executable" do + it "shows the bundler version just as the `bundle` executable does" do + bundler "--version" + expect(out).to eq("Bundler version #{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..c1cae31956 --- /dev/null +++ b/spec/bundler/bundler/compact_index_client/updater_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true +require "spec_helper" +require "net/http" +require "bundler/compact_index_client" +require "bundler/compact_index_client/updater" + +RSpec.describe Bundler::CompactIndexClient::Updater do + subject(:updater) { described_class.new(fetcher) } + + let(:fetcher) { double(:fetcher) } + + context "when the ETag header is missing" do + # Regression test for https://github.com/bundler/bundler/issues/5463 + + let(:response) { double(:response, :body => "") } + let(:local_path) { Pathname("/tmp/localpath") } + let(:remote_path) { double(:remote_path) } + + 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 +end diff --git a/spec/bundler/bundler/definition_spec.rb b/spec/bundler/bundler/definition_spec.rb new file mode 100644 index 0000000000..73d44a93ab --- /dev/null +++ b/spec/bundler/bundler/definition_spec.rb @@ -0,0 +1,277 @@ +# frozen_string_literal: true +require "spec_helper" +require "bundler/definition" + +RSpec.describe Bundler::Definition do + describe "#lock" do + before do + allow(Bundler).to receive(:settings) { Bundler::Settings.new(".") } + allow(Bundler).to receive(:default_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" do + build_lib "foo", "1.0", :path => lib_path("foo") + + install_gemfile <<-G + source "file://#{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:#{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" 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://#{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:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "for a rubygems gem" do + install_gemfile <<-G + source "file://#{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:#{gem_repo1}/ + specs: + foo (1.0) + + PLATFORMS + ruby + + 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 + 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, + Bundler::SourceList.new, + unlock_hash_for_bundle_install + ) + locked = definition.send(:converge_locked_specs).map(&:name) + expect(locked.include?("shared_dep")).to be_truthy + 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, + Bundler::SourceList.new, + :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/dsl_spec.rb b/spec/bundler/bundler/dsl_spec.rb new file mode 100644 index 0000000000..4f5eb6dc92 --- /dev/null +++ b/spec/bundler/bundler/dsl_spec.rb @@ -0,0 +1,268 @@ +# frozen_string_literal: true +require "spec_helper" + +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)" 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 + end + + describe "#method_missing" do + it "raises an error for unknown DSL methods" do + expect(Bundler).to receive(:read_file).with("Gemfile"). + 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("Gemfile").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("Gemfile").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].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 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 specifiying 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 specifiying 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" 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 + 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..0b8da840d2 --- /dev/null +++ b/spec/bundler/bundler/endpoint_specification_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe Bundler::EndpointSpecification do + let(:name) { "foo" } + let(:version) { "1.0.0" } + let(:platform) { Gem::Platform::RUBY } + let(:dependencies) { [] } + let(:metadata) { nil } + + subject { 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 +end diff --git a/spec/bundler/bundler/env_spec.rb b/spec/bundler/bundler/env_spec.rb new file mode 100644 index 0000000000..269c323ac6 --- /dev/null +++ b/spec/bundler/bundler/env_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true +require "spec_helper" +require "bundler/settings" + +RSpec.describe Bundler::Env do + let(:env) { described_class.new } + let(:git_proxy_stub) { Bundler::Source::Git::GitProxy.new(nil, nil, nil) } + + describe "#report" do + it "prints the environment" do + out = env.report + + expect(out).to include("Environment") + expect(out).to include(Bundler::VERSION) + expect(out).to include(Gem::VERSION) + expect(out).to include(env.send(:ruby_version)) + expect(out).to include(env.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) { env.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) { env.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 = env.report(:print_gemspecs => true) + + expect(output).to include("foo.gemspec") + expect(output).to include(gemspec) + 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(env.report).to include("Git 1.2.3 (Apple Git-BS)") + end + 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..41d2650055 --- /dev/null +++ b/spec/bundler/bundler/environment_preserver_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true +require "spec_helper" + +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.key?("BUNDLER_ORIG_foo")).to eq(false) + 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..38b69429bc --- /dev/null +++ b/spec/bundler/bundler/fetcher/base_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true +require "spec_helper" + +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..e653c1ea43 --- /dev/null +++ b/spec/bundler/bundler/fetcher/compact_index_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true +require "spec_helper" + +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 + before { stub_const("OpenSSL::OPENSSL_FIPS", true) } + + 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..134ca1bc57 --- /dev/null +++ b/spec/bundler/bundler/fetcher/dependency_spec.rb @@ -0,0 +1,288 @@ +# frozen_string_literal: true +require "spec_helper" + +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..4dcd94b1b2 --- /dev/null +++ b/spec/bundler/bundler/fetcher/downloader_spec.rb @@ -0,0 +1,251 @@ +# frozen_string_literal: true +require "spec_helper" + +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..b17e0d1727 --- /dev/null +++ b/spec/bundler/bundler/fetcher/index_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true +require "spec_helper" + +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..585768343f --- /dev/null +++ b/spec/bundler/bundler/fetcher_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true +require "spec_helper" +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 + 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..19799d5495 --- /dev/null +++ b/spec/bundler/bundler/friendly_errors_spec.rb @@ -0,0 +1,270 @@ +# frozen_string_literal: true +require "spec_helper" +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(err).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..498ed89447 --- /dev/null +++ b/spec/bundler/bundler/gem_helper_spec.rb @@ -0,0 +1,260 @@ +# frozen_string_literal: true +require "spec_helper" +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 = ""') + 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 + 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..c7620e2620 --- /dev/null +++ b/spec/bundler/bundler/gem_version_promoter_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true +require "spec_helper" + +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_group(name, version) + Bundler::Resolver::SpecGroup.new(build_spec(name, version)) + end + + # Rightmost (highest array index) in result is most preferred. + # Leftmost (lowest array index) in result is least preferred. + # `build_spec_group` has all version 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_group("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_group("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_group("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_group("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_group("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_group("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_group("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_group("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_group("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_group("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_group("foo", "1.2.0"), + build_spec_group("foo", "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..09b09e08fa --- /dev/null +++ b/spec/bundler/bundler/index_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true +require "spec_helper" + +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..e2f30cdd70 --- /dev/null +++ b/spec/bundler/bundler/installer/gem_installer_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true +require "spec_helper" +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("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..7d2c441399 --- /dev/null +++ b/spec/bundler/bundler/installer/parallel_installer_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true +require "spec_helper" +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..1e368ab7c5 --- /dev/null +++ b/spec/bundler/bundler/installer/spec_installation_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true +require "spec_helper" +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..17bb447194 --- /dev/null +++ b/spec/bundler/bundler/lockfile_parser_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true +require "spec_helper" +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 +end diff --git a/spec/bundler/bundler/mirror_spec.rb b/spec/bundler/bundler/mirror_spec.rb new file mode 100644 index 0000000000..9051a80465 --- /dev/null +++ b/spec/bundler/bundler/mirror_spec.rb @@ -0,0 +1,329 @@ +# frozen_string_literal: true +require "spec_helper" +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" 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..4dbb993b89 --- /dev/null +++ b/spec/bundler/bundler/plugin/api/source_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true +require "spec_helper" + +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..e40b9adb0f --- /dev/null +++ b/spec/bundler/bundler/plugin/api_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true +require "spec_helper" + +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_unkown_function).once + + api.an_unkown_function + end + + it "includes Bundler::SharedHelpers' functions" do + expect(Bundler::SharedHelpers).to receive(:an_unkown_helper).once + + api.an_unkown_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..cd15b6ea9d --- /dev/null +++ b/spec/bundler/bundler/plugin/dsl_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true +require "spec_helper" + +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/index_spec.rb b/spec/bundler/bundler/plugin/index_spec.rb new file mode 100644 index 0000000000..24b9a408ff --- /dev/null +++ b/spec/bundler/bundler/plugin/index_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true +require "spec_helper" + +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 +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..e8d5941e33 --- /dev/null +++ b/spec/bundler/bundler/plugin/installer_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe Bundler::Plugin::Installer do + subject(:installer) { Bundler::Plugin::Installer.new } + + describe "cli install" do + it "uses Gem.sources when non of the source is provided" do + sources = double(:sources) + 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..86cc4ac4ed --- /dev/null +++ b/spec/bundler/bundler/plugin/source_list_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +require "spec_helper" + +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..5bbb7384c8 --- /dev/null +++ b/spec/bundler/bundler/plugin_spec.rb @@ -0,0 +1,292 @@ +# frozen_string_literal: true +require "spec_helper" + +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 "#hook" do + before do + path = lib_path("foo-plugin") + build_lib "foo-plugin", :path => path do |s| + s.write "plugins.rb", code + end + + allow(index).to receive(:hook_plugins).with(event). + 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 + + let(:event) { "event-1" } + + it "executes the hook" do + out = capture(:stdout) do + Plugin.hook("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("event-1") {} + Bundler::Plugin::API.hook("event-2") {} + puts "loaded" + RUBY + + let(:event) { /event-1|event-2/ } + + it "evals plugins.rb once" do + out = capture(:stdout) do + Plugin.hook("event-1") + Plugin.hook("event-2") + end.strip + + expect(out).to eq("loaded") + end + end + + context "a block is passed" do + let(:code) { <<-RUBY } + Bundler::Plugin::API.hook("#{event}") { |&blk| blk.call } + RUBY + + it "is passed to the hook" do + out = capture(:stdout) do + Plugin.hook("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..18e40d6b5a --- /dev/null +++ b/spec/bundler/bundler/psyched_yaml_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +require "spec_helper" +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..644814c563 --- /dev/null +++ b/spec/bundler/bundler/remote_specification_spec.rb @@ -0,0 +1,188 @@ +# frozen_string_literal: true +require "spec_helper" + +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..525f05d327 --- /dev/null +++ b/spec/bundler/bundler/retry_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true +require "spec_helper" + +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..3e0ec9d7f0 --- /dev/null +++ b/spec/bundler/bundler/ruby_dsl_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true +require "spec_helper" +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..f77ec606fc --- /dev/null +++ b/spec/bundler/bundler/ruby_version_spec.rb @@ -0,0 +1,524 @@ +# frozen_string_literal: true +require "spec_helper" +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") + allow(Bundler).to receive(:ruby_engine).and_return("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") + allow(Bundler).to receive(:ruby_engine).and_return("not_supported_ruby_engine") + end + + it "should raise a BundlerError with a 'not recognized' message" do + expect { bundler_system_ruby_version.engine_versions }.to raise_error(Bundler::BundlerError, "RUBY_ENGINE value not_supported_ruby_engine is not recognized") + 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..38ff9dae7e --- /dev/null +++ b/spec/bundler/bundler/rubygems_integration_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true +require "spec_helper" + +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 "succesfully 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_spec.rb b/spec/bundler/bundler/settings_spec.rb new file mode 100644 index 0000000000..7302da5421 --- /dev/null +++ b/spec/bundler/bundler/settings_spec.rb @@ -0,0 +1,284 @@ +# frozen_string_literal: true +require "spec_helper" +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[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[:frozen] = "true" + expect(settings[:frozen]).to be true + end + context "when specific gem is configured" do + it "returns a boolean" do + settings["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[: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(FileUtils).to receive(:mkdir_p).with(settings.send(:local_config_file).dirname). + and_raise(Errno::EACCES) + expect { settings[:frozen] = "1" }. + to raise_error(Bundler::PermissionError, /config/) + end + end + end + + describe "#temporary" do + it "reset after used" do + Bundler.settings[: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 + 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(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[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["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 + 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["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["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["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["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["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["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["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["local.httpsmarty"] = home("httpsmarty") + expect(settings.all).to include("local.httpsmarty") + end + + it "reads older keys without trailing slashes" do + settings["mirror.https://rubygems.org"] = "http://rubygems-mirror.org" + expect(settings.mirror_for("https://rubygems.org/")).to eq( + URI("http://rubygems-mirror.org/") + ) + 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..d3b93b56d0 --- /dev/null +++ b/spec/bundler/bundler/shared_helpers_spec.rb @@ -0,0 +1,451 @@ +# frozen_string_literal: true +require "spec_helper" + +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 + 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 path seperator" do + stub_const("File::PATH_SEPARATOR", ":".freeze) + 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 a ':', 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 include ':'.\nYour current bundle " \ + "path is '#{Bundler.bundle_path}'." + ) + 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 "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..34fe21e9fb --- /dev/null +++ b/spec/bundler/bundler/source/git/git_proxy_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe Bundler::Source::Git::GitProxy do + let(:uri) { "https://github.com/bundler/bundler.git" } + subject { described_class.new(Pathname("path"), uri, "HEAD") } + + context "with configured credentials" do + it "adds username and password to URI" do + Bundler.settings[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["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["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["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 +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..54394fc0ca --- /dev/null +++ b/spec/bundler/bundler/source/rubygems/remote_spec.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true +require "spec_helper" +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[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[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[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[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[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[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["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["mirror.https://rubygems.org/"] = mirror_uri_no_auth.to_s + Bundler.settings[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..b8f9f09c20 --- /dev/null +++ b/spec/bundler/bundler/source/rubygems_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true +require "spec_helper" + +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..6a23c8bcbf --- /dev/null +++ b/spec/bundler/bundler/source_list_spec.rb @@ -0,0 +1,441 @@ +# frozen_string_literal: true +require "spec_helper" + +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 } + + 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["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" do + before do + @returned_source = source_list.add_rubygems_remote("https://rubygems.org/") + end + + 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.first).to eq(URI("https://othersource.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, + ] + 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 + it "combines the rubygems sources into a single instance, removing duplicate remotes from the end" 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") + + 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 + 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_remote("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_remote("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..08d1698fcd --- /dev/null +++ b/spec/bundler/bundler/source_spec.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true +require "spec_helper" + +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..8f7c27f065 --- /dev/null +++ b/spec/bundler/bundler/spec_set_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true +require "spec_helper" + +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 "#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..66853a6815 --- /dev/null +++ b/spec/bundler/bundler/ssl_certs/certificate_manager_spec.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true +require "spec_helper" +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 + rubygems_dir = File.join(root.to_s, "lib", "rubygems") + 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(FileUtils).to receive(:rm) + allow(FileUtils).to receive(:cp) + end + + it "should remove the current bundler certs" do + expect(FileUtils).to receive(:rm).with(subject.bundler_certs) + subject.update! + end + + it "should copy the rubygems certs into bundler certs" do + expect(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..f1ddf43bb4 --- /dev/null +++ b/spec/bundler/bundler/stub_specification_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +require "spec_helper" + +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_spec.rb b/spec/bundler/bundler/ui_spec.rb new file mode 100644 index 0000000000..fc76eb1ee7 --- /dev/null +++ b/spec/bundler/bundler/ui_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true +require "spec_helper" + +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..1dd01b4be0 --- /dev/null +++ b/spec/bundler/bundler/uri_credentials_filter_spec.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true +require "spec_helper" + +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/version_ranges_spec.rb b/spec/bundler/bundler/version_ranges_spec.rb new file mode 100644 index 0000000000..f746aa88ad --- /dev/null +++ b/spec/bundler/bundler/version_ranges_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true +require "spec_helper" +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..fbfe6ddab3 --- /dev/null +++ b/spec/bundler/bundler/worker_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +require "spec_helper" +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..c28db59223 --- /dev/null +++ b/spec/bundler/bundler/yaml_serializer_spec.rb @@ -0,0 +1,193 @@ +# frozen_string_literal: true +require "spec_helper" +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", + }, + "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 diff --git a/spec/bundler/cache/cache_path_spec.rb b/spec/bundler/cache/cache_path_spec.rb new file mode 100644 index 0000000000..ec6d6e312a --- /dev/null +++ b/spec/bundler/cache/cache_path_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle package" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + context "with --cache-path" do + it "caches gems at given path" do + bundle :package, "cache-path" => "vendor/cache-foo" + expect(bundled_app("vendor/cache-foo/rack-1.0.0.gem")).to exist + end + end + + context "with config cache_path" do + it "caches gems at given path" do + bundle "config cache_path vendor/cache-foo" + bundle :package + expect(bundled_app("vendor/cache-foo/rack-1.0.0.gem")).to exist + end + end + + context "when given an absolute path" do + it "exits with non-zero status" do + bundle :package, "cache-path" => "/tmp/cache-foo" + expect(out).to match(/must be relative/) + expect(exitstatus).to eq(15) if exitstatus + end + end +end diff --git a/spec/bundler/cache/gems_spec.rb b/spec/bundler/cache/gems_spec.rb new file mode 100644 index 0000000000..7828c87fec --- /dev/null +++ b/spec/bundler/cache/gems_spec.rb @@ -0,0 +1,292 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle cache" do + describe "when there are only gemsources" do + before :each do + gemfile <<-G + gem 'rack' + G + + system_gems "rack-1.0.0" + bundle :cache + end + + it "copies the .gem file to vendor/cache" do + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + end + + it "uses the cache as a source when installing gems" do + build_gem "omg", :path => bundled_app("vendor/cache") + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "omg" + G + + expect(the_bundle).to include_gems "omg 1.0.0" + end + + it "uses the cache as a source when installing gems with --local" do + system_gems [] + bundle "install --local" + + expect(the_bundle).to include_gems("rack 1.0.0") + end + + it "does not reinstall gems from the cache if they exist on the system" do + build_gem "rack", "1.0.0", :path => bundled_app("vendor/cache") do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + + install_gemfile <<-G + gem "rack" + G + + expect(the_bundle).to include_gems("rack 1.0.0") + end + + it "does not reinstall gems from the cache if they exist in the bundle" do + system_gems "rack-1.0.0" + + gemfile <<-G + gem "rack" + G + + build_gem "rack", "1.0.0", :path => bundled_app("vendor/cache") do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + + bundle "install --local" + expect(the_bundle).to include_gems("rack 1.0.0") + end + + it "creates a lockfile" do + cache_gems "rack-1.0.0" + + gemfile <<-G + gem "rack" + G + + bundle "cache" + + expect(bundled_app("Gemfile.lock")).to exist + end + end + + describe "when there is a built-in gem", :ruby => "2.0" do + before :each do + build_repo2 do + build_gem "builtin_gem", "1.0.2" + end + + build_gem "builtin_gem", "1.0.2", :to_system => true do |s| + s.summary = "This builtin_gem is bundled with Ruby" + end + + FileUtils.rm("#{system_gem_path}/cache/builtin_gem-1.0.2.gem") + end + + it "uses builtin gems" do + install_gemfile %(gem 'builtin_gem', '1.0.2') + expect(the_bundle).to include_gems("builtin_gem 1.0.2") + end + + it "caches remote and builtin gems" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'builtin_gem', '1.0.2' + gem 'rack', '1.0.0' + G + + bundle :cache + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/builtin_gem-1.0.2.gem")).to exist + end + + it "doesn't make remote request after caching the gem" do + build_gem "builtin_gem_2", "1.0.2", :path => bundled_app("vendor/cache") do |s| + s.summary = "This builtin_gem is bundled with Ruby" + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'builtin_gem_2', '1.0.2' + G + + bundle "install --local" + expect(the_bundle).to include_gems("builtin_gem_2 1.0.2") + end + + it "errors if the builtin gem isn't available to cache" do + install_gemfile <<-G + gem 'builtin_gem', '1.0.2' + G + + bundle :cache + expect(exitstatus).to_not eq(0) if exitstatus + expect(out).to include("builtin_gem-1.0.2 is built in to Ruby, and can't be cached") + end + end + + describe "when there are also git sources" do + before do + build_git "foo" + system_gems "rack-1.0.0" + + install_gemfile <<-G + source "file://#{gem_repo1}" + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + gem 'rack' + G + end + + it "still works" do + bundle :cache + + system_gems [] + bundle "install --local" + + expect(the_bundle).to include_gems("rack 1.0.0", "foo 1.0") + end + + it "should not explode if the lockfile is not present" do + FileUtils.rm(bundled_app("Gemfile.lock")) + + bundle :cache + + expect(bundled_app("Gemfile.lock")).to exist + end + end + + describe "when previously cached" do + before :each do + build_repo2 + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack" + gem "actionpack" + G + bundle :cache + expect(cached_gem("rack-1.0.0")).to exist + expect(cached_gem("actionpack-2.3.2")).to exist + expect(cached_gem("activesupport-2.3.2")).to exist + end + + it "re-caches during install" do + cached_gem("rack-1.0.0").rmtree + bundle :install + expect(out).to include("Updating files in vendor/cache") + expect(cached_gem("rack-1.0.0")).to exist + end + + it "adds and removes when gems are updated" do + update_repo2 + bundle "update" + expect(cached_gem("rack-1.2")).to exist + expect(cached_gem("rack-1.0.0")).not_to exist + end + + it "adds new gems and dependencies" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rails" + G + expect(cached_gem("rails-2.3.2")).to exist + expect(cached_gem("activerecord-2.3.2")).to exist + end + + it "removes .gems for removed gems and dependencies" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack" + G + expect(cached_gem("rack-1.0.0")).to exist + expect(cached_gem("actionpack-2.3.2")).not_to exist + expect(cached_gem("activesupport-2.3.2")).not_to exist + end + + it "removes .gems when gem changes to git source" do + build_git "rack" + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack", :git => "#{lib_path("rack-1.0")}" + gem "actionpack" + G + expect(cached_gem("rack-1.0.0")).not_to exist + expect(cached_gem("actionpack-2.3.2")).to exist + expect(cached_gem("activesupport-2.3.2")).to exist + end + + it "doesn't remove gems that are for another platform" do + simulate_platform "java" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "platform_specific" + G + + bundle :cache + expect(cached_gem("platform_specific-1.0-java")).to exist + end + + simulate_new_machine + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "platform_specific" + G + + expect(cached_gem("platform_specific-1.0-#{Bundler.local_platform}")).to exist + expect(cached_gem("platform_specific-1.0-java")).to exist + end + + it "doesn't remove gems with mismatched :rubygems_version or :date" do + cached_gem("rack-1.0.0").rmtree + build_gem "rack", "1.0.0", + :path => bundled_app("vendor/cache"), + :rubygems_version => "1.3.2" + simulate_new_machine + + bundle :install + expect(cached_gem("rack-1.0.0")).to exist + end + + it "handles directories and non .gem files in the cache" do + bundled_app("vendor/cache/foo").mkdir + File.open(bundled_app("vendor/cache/bar"), "w") {|f| f.write("not a gem") } + bundle :cache + end + + it "does not say that it is removing gems when it isn't actually doing so" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + bundle "cache" + bundle "install" + expect(out).not_to match(/removing/i) + end + + it "does not warn about all if it doesn't have any git/path dependency" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + bundle "cache" + expect(out).not_to match(/\-\-all/) + end + + it "should install gems with the name bundler in them (that aren't bundler)" do + build_gem "foo-bundler", "1.0", + :path => bundled_app("vendor/cache") + + install_gemfile <<-G + gem "foo-bundler" + G + + expect(the_bundle).to include_gems "foo-bundler 1.0" + end + end +end diff --git a/spec/bundler/cache/git_spec.rb b/spec/bundler/cache/git_spec.rb new file mode 100644 index 0000000000..31b3816a3b --- /dev/null +++ b/spec/bundler/cache/git_spec.rb @@ -0,0 +1,215 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "git base name" do + it "base_name should strip private repo uris" do + source = Bundler::Source::Git.new("uri" => "git@github.com:bundler.git") + expect(source.send(:base_name)).to eq("bundler") + end + + it "base_name should strip network share paths" do + source = Bundler::Source::Git.new("uri" => "//MachineName/ShareFolder") + expect(source.send(:base_name)).to eq("ShareFolder") + end +end + +%w(cache package).each do |cmd| + RSpec.describe "bundle #{cmd} with git" do + it "copies repository to vendor cache and uses it" do + git = build_git "foo" + ref = git.ref_for("master", 11) + + install_gemfile <<-G + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + bundle "#{cmd} --all" + expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist + expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.git")).not_to exist + expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.bundlecache")).to be_file + + FileUtils.rm_rf lib_path("foo-1.0") + expect(the_bundle).to include_gems "foo 1.0" + end + + it "copies repository to vendor cache and uses it even when installed with bundle --path" do + git = build_git "foo" + ref = git.ref_for("master", 11) + + install_gemfile <<-G + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + bundle "install --path vendor/bundle" + bundle "#{cmd} --all" + + expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist + expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.git")).not_to exist + + FileUtils.rm_rf lib_path("foo-1.0") + expect(the_bundle).to include_gems "foo 1.0" + end + + it "runs twice without exploding" do + build_git "foo" + + install_gemfile <<-G + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + bundle "#{cmd} --all" + bundle "#{cmd} --all" + + expect(err).to lack_errors + FileUtils.rm_rf lib_path("foo-1.0") + expect(the_bundle).to include_gems "foo 1.0" + end + + it "tracks updates" do + git = build_git "foo" + old_ref = git.ref_for("master", 11) + + install_gemfile <<-G + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + bundle "#{cmd} --all" + + update_git "foo" do |s| + s.write "lib/foo.rb", "puts :CACHE" + end + + ref = git.ref_for("master", 11) + expect(ref).not_to eq(old_ref) + + bundle "update" + bundle "#{cmd} --all" + + expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist + expect(bundled_app("vendor/cache/foo-1.0-#{old_ref}")).not_to exist + + FileUtils.rm_rf lib_path("foo-1.0") + run "require 'foo'" + expect(out).to eq("CACHE") + end + + it "tracks updates when specifying the gem" do + git = build_git "foo" + old_ref = git.ref_for("master", 11) + + install_gemfile <<-G + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + bundle "#{cmd} --all" + + update_git "foo" do |s| + s.write "lib/foo.rb", "puts :CACHE" + end + + ref = git.ref_for("master", 11) + expect(ref).not_to eq(old_ref) + + bundle "update foo" + + expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist + expect(bundled_app("vendor/cache/foo-1.0-#{old_ref}")).not_to exist + + FileUtils.rm_rf lib_path("foo-1.0") + run "require 'foo'" + expect(out).to eq("CACHE") + end + + it "uses the local repository to generate the cache" do + git = build_git "foo" + ref = git.ref_for("master", 11) + + gemfile <<-G + gem "foo", :git => '#{lib_path("foo-invalid")}', :branch => :master + G + + bundle %(config local.foo #{lib_path("foo-1.0")}) + bundle "install" + bundle "#{cmd} --all" + + expect(bundled_app("vendor/cache/foo-invalid-#{ref}")).to exist + + # Updating the local still uses the local. + update_git "foo" do |s| + s.write "lib/foo.rb", "puts :LOCAL" + end + + run "require 'foo'" + expect(out).to eq("LOCAL") + end + + it "copies repository to vendor cache, including submodules" do + build_git "submodule", "1.0" + + git = build_git "has_submodule", "1.0" do |s| + s.add_dependency "submodule" + end + + Dir.chdir(lib_path("has_submodule-1.0")) do + sys_exec "git submodule add #{lib_path("submodule-1.0")} submodule-1.0" + `git commit -m "submodulator"` + end + + install_gemfile <<-G + git "#{lib_path("has_submodule-1.0")}", :submodules => true do + gem "has_submodule" + end + G + + ref = git.ref_for("master", 11) + bundle "#{cmd} --all" + + expect(bundled_app("vendor/cache/has_submodule-1.0-#{ref}")).to exist + expect(bundled_app("vendor/cache/has_submodule-1.0-#{ref}/submodule-1.0")).to exist + expect(the_bundle).to include_gems "has_submodule 1.0" + end + + it "displays warning message when detecting git repo in Gemfile" do + build_git "foo" + + install_gemfile <<-G + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + bundle "#{cmd}" + + expect(out).to include("Your Gemfile contains path and git dependencies.") + end + + it "does not display warning message if cache_all is set in bundle config" do + build_git "foo" + + install_gemfile <<-G + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + bundle "#{cmd} --all" + bundle "#{cmd}" + + expect(out).not_to include("Your Gemfile contains path and git dependencies.") + end + + it "caches pre-evaluated gemspecs" do + git = build_git "foo" + + # Insert a gemspec method that shells out + spec_lines = lib_path("foo-1.0/foo.gemspec").read.split("\n") + spec_lines.insert(-2, "s.description = `echo bob`") + update_git("foo") {|s| s.write "foo.gemspec", spec_lines.join("\n") } + + install_gemfile <<-G + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + bundle "#{cmd} --all" + + ref = git.ref_for("master", 11) + gemspec = bundled_app("vendor/cache/foo-1.0-#{ref}/foo.gemspec").read + expect(gemspec).to_not match("`echo bob`") + end + end +end diff --git a/spec/bundler/cache/path_spec.rb b/spec/bundler/cache/path_spec.rb new file mode 100644 index 0000000000..bbce448759 --- /dev/null +++ b/spec/bundler/cache/path_spec.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true +require "spec_helper" + +%w(cache package).each do |cmd| + RSpec.describe "bundle #{cmd} with path" do + it "is no-op when the path is within the bundle" do + build_lib "foo", :path => bundled_app("lib/foo") + + install_gemfile <<-G + gem "foo", :path => '#{bundled_app("lib/foo")}' + G + + bundle "#{cmd} --all" + expect(bundled_app("vendor/cache/foo-1.0")).not_to exist + expect(the_bundle).to include_gems "foo 1.0" + end + + it "copies when the path is outside the bundle " do + build_lib "foo" + + install_gemfile <<-G + gem "foo", :path => '#{lib_path("foo-1.0")}' + G + + bundle "#{cmd} --all" + expect(bundled_app("vendor/cache/foo-1.0")).to exist + expect(bundled_app("vendor/cache/foo-1.0/.bundlecache")).to be_file + + FileUtils.rm_rf lib_path("foo-1.0") + expect(the_bundle).to include_gems "foo 1.0" + end + + it "copies when the path is outside the bundle and the paths intersect" do + libname = File.basename(Dir.pwd) + "_gem" + libpath = File.join(File.dirname(Dir.pwd), libname) + + build_lib libname, :path => libpath + + install_gemfile <<-G + gem "#{libname}", :path => '#{libpath}' + G + + bundle "#{cmd} --all" + expect(bundled_app("vendor/cache/#{libname}")).to exist + expect(bundled_app("vendor/cache/#{libname}/.bundlecache")).to be_file + + FileUtils.rm_rf libpath + expect(the_bundle).to include_gems "#{libname} 1.0" + end + + it "updates the path on each cache" do + build_lib "foo" + + install_gemfile <<-G + gem "foo", :path => '#{lib_path("foo-1.0")}' + G + + bundle "#{cmd} --all" + + build_lib "foo" do |s| + s.write "lib/foo.rb", "puts :CACHE" + end + + bundle "#{cmd} --all" + + expect(bundled_app("vendor/cache/foo-1.0")).to exist + FileUtils.rm_rf lib_path("foo-1.0") + + run "require 'foo'" + expect(out).to eq("CACHE") + end + + it "removes stale entries cache" do + build_lib "foo" + + install_gemfile <<-G + gem "foo", :path => '#{lib_path("foo-1.0")}' + G + + bundle "#{cmd} --all" + + install_gemfile <<-G + gem "bar", :path => '#{lib_path("bar-1.0")}' + G + + bundle "#{cmd} --all" + expect(bundled_app("vendor/cache/bar-1.0")).not_to exist + end + + it "raises a warning without --all" do + build_lib "foo" + + install_gemfile <<-G + gem "foo", :path => '#{lib_path("foo-1.0")}' + G + + bundle cmd + expect(out).to match(/please pass the \-\-all flag/) + expect(bundled_app("vendor/cache/foo-1.0")).not_to exist + end + + it "stores the given flag" do + build_lib "foo" + + install_gemfile <<-G + gem "foo", :path => '#{lib_path("foo-1.0")}' + G + + bundle "#{cmd} --all" + build_lib "bar" + + install_gemfile <<-G + gem "foo", :path => '#{lib_path("foo-1.0")}' + gem "bar", :path => '#{lib_path("bar-1.0")}' + G + + bundle cmd + expect(bundled_app("vendor/cache/bar-1.0")).to exist + end + + it "can rewind chosen configuration" do + build_lib "foo" + + install_gemfile <<-G + gem "foo", :path => '#{lib_path("foo-1.0")}' + G + + bundle "#{cmd} --all" + build_lib "baz" + + gemfile <<-G + gem "foo", :path => '#{lib_path("foo-1.0")}' + gem "baz", :path => '#{lib_path("baz-1.0")}' + G + + bundle "#{cmd} --no-all" + expect(bundled_app("vendor/cache/baz-1.0")).not_to exist + end + end +end diff --git a/spec/bundler/cache/platform_spec.rb b/spec/bundler/cache/platform_spec.rb new file mode 100644 index 0000000000..ed80c949aa --- /dev/null +++ b/spec/bundler/cache/platform_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle cache with multiple platforms" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + + platforms :mri, :rbx do + gem "rack", "1.0.0" + end + + platforms :jruby do + gem "activesupport", "2.3.5" + end + G + + lockfile <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + activesupport (2.3.5) + + PLATFORMS + ruby + java + + DEPENDENCIES + rack (1.0.0) + activesupport (2.3.5) + G + + cache_gems "rack-1.0.0", "activesupport-2.3.5" + end + + it "ensures that a successful bundle install does not delete gems for other platforms" do + bundle "install" + + expect(exitstatus).to eq 0 if exitstatus + + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/activesupport-2.3.5.gem")).to exist + end + + it "ensures that a successful bundle update does not delete gems for other platforms" do + bundle "update" + + expect(exitstatus).to eq 0 if exitstatus + + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/activesupport-2.3.5.gem")).to exist + end +end diff --git a/spec/bundler/commands/add_spec.rb b/spec/bundler/commands/add_spec.rb new file mode 100644 index 0000000000..4931402c33 --- /dev/null +++ b/spec/bundler/commands/add_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle add" do + before :each do + build_repo2 do + build_gem "foo", "1.1" + build_gem "foo", "2.0" + build_gem "baz", "1.2.3" + build_gem "bar", "0.12.3" + build_gem "cat", "0.12.3.pre" + build_gem "dog", "1.1.3.pre" + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "weakling", "~> 0.0.1" + G + end + + describe "without version specified" do + it "version requirement becomes ~> major.minor.patch when resolved version is < 1.0" do + bundle "add 'bar'" + expect(bundled_app("Gemfile").read).to match(/gem "bar", "~> 0.12.3"/) + expect(the_bundle).to include_gems "bar 0.12.3" + end + + it "version requirement becomes ~> major.minor when resolved version is > 1.0" do + bundle "add 'baz'" + expect(bundled_app("Gemfile").read).to match(/gem "baz", "~> 1.2"/) + expect(the_bundle).to include_gems "baz 1.2.3" + end + + it "version requirement becomes ~> major.minor.patch.pre when resolved version is < 1.0" do + bundle "add 'cat'" + expect(bundled_app("Gemfile").read).to match(/gem "cat", "~> 0.12.3.pre"/) + expect(the_bundle).to include_gems "cat 0.12.3.pre" + end + + it "version requirement becomes ~> major.minor.pre when resolved version is > 1.0.pre" do + bundle "add 'dog'" + expect(bundled_app("Gemfile").read).to match(/gem "dog", "~> 1.1.pre"/) + expect(the_bundle).to include_gems "dog 1.1.3.pre" + end + end + + describe "with --version" do + it "adds dependency of specified version and runs install" do + bundle "add 'foo' --version='~> 1.0'" + expect(bundled_app("Gemfile").read).to match(/gem "foo", "~> 1.0"/) + expect(the_bundle).to include_gems "foo 1.1" + end + + it "adds multiple version constraints when specified" do + bundle "add 'foo' --version='< 3.0, > 1.1'" + expect(bundled_app("Gemfile").read).to match(/gem "foo", "< 3.0", "> 1.1"/) + expect(the_bundle).to include_gems "foo 2.0" + end + end + + describe "with --group" do + it "adds dependency for the specified group" do + bundle "add 'foo' --group='development'" + expect(bundled_app("Gemfile").read).to match(/gem "foo", "~> 2.0", :group => \[:development\]/) + expect(the_bundle).to include_gems "foo 2.0" + end + + it "adds dependency to more than one group" do + bundle "add 'foo' --group='development, test'" + expect(bundled_app("Gemfile").read).to match(/gem "foo", "~> 2.0", :groups => \[:development, :test\]/) + expect(the_bundle).to include_gems "foo 2.0" + end + end + + describe "with --source" do + it "adds dependency with specified source" do + bundle "add 'foo' --source='file://#{gem_repo2}'" + expect(bundled_app("Gemfile").read).to match(%r{gem "foo", "~> 2.0", :source => "file:\/\/#{gem_repo2}"}) + expect(the_bundle).to include_gems "foo 2.0" + end + end + + it "using combination of short form options works like long form" do + bundle "add 'foo' -s='file://#{gem_repo2}' -g='development' -v='~>1.0'" + expect(bundled_app("Gemfile").read).to match(%r{gem "foo", "~> 1.0", :group => \[:development\], :source => "file:\/\/#{gem_repo2}"}) + expect(the_bundle).to include_gems "foo 1.1" + end + + it "shows error message when version is not formatted correctly" do + bundle "add 'foo' -v='~>1 . 0'" + expect(out).to match("Invalid gem requirement pattern '~>1 . 0'") + end + + it "shows error message when gem cannot be found" do + bundle "add 'werk_it'" + expect(out).to match("Could not find gem 'werk_it' in any of the gem sources listed in your Gemfile.") + + bundle "add 'werk_it' -s='file://#{gem_repo2}'" + expect(out).to match("Could not find gem 'werk_it' in rubygems repository") + end + + it "shows error message when source cannot be reached" do + bundle "add 'baz' --source='http://badhostasdf'" + expect(out).to include("Could not reach host badhostasdf. Check your network connection and try again.") + + bundle "add 'baz' --source='file://does/not/exist'" + expect(out).to include("Could not fetch specs from file://does/not/exist/") + end +end diff --git a/spec/bundler/commands/binstubs_spec.rb b/spec/bundler/commands/binstubs_spec.rb new file mode 100644 index 0000000000..cb0999348e --- /dev/null +++ b/spec/bundler/commands/binstubs_spec.rb @@ -0,0 +1,261 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle binstubs <gem>" do + context "when the gem exists in the lockfile" do + it "sets up the binstub" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "binstubs rack" + + expect(bundled_app("bin/rackup")).to exist + end + + it "does not install other binstubs" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rails" + G + + bundle "binstubs rails" + + expect(bundled_app("bin/rackup")).not_to exist + expect(bundled_app("bin/rails")).to exist + end + + it "does install multiple binstubs" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rails" + G + + bundle "binstubs rails rack" + + expect(bundled_app("bin/rackup")).to exist + expect(bundled_app("bin/rails")).to exist + end + + it "displays an error when used without any gem" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "binstubs" + expect(exitstatus).to eq(1) if exitstatus + expect(out).to include("`bundle binstubs` needs at least one gem to run.") + end + + it "does not bundle the bundler binary" do + install_gemfile <<-G + source "file://#{gem_repo1}" + G + + bundle "binstubs bundler" + + expect(bundled_app("bin/bundle")).not_to exist + expect(out).to include("Sorry, Bundler can only be run via Rubygems.") + end + + it "installs binstubs from git gems" do + FileUtils.mkdir_p(lib_path("foo/bin")) + FileUtils.touch(lib_path("foo/bin/foo")) + build_git "foo", "1.0", :path => lib_path("foo") do |s| + s.executables = %w(foo) + end + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo")}" + G + + bundle "binstubs foo" + + expect(bundled_app("bin/foo")).to exist + end + + it "installs binstubs from path gems" do + FileUtils.mkdir_p(lib_path("foo/bin")) + FileUtils.touch(lib_path("foo/bin/foo")) + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.executables = %w(foo) + end + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo")}" + G + + bundle "binstubs foo" + + expect(bundled_app("bin/foo")).to exist + end + + it "sets correct permissions for binstubs" do + with_umask(0o002) do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "binstubs rack" + binary = bundled_app("bin/rackup") + expect(File.stat(binary).mode.to_s(8)).to eq("100775") + end + end + end + + context "when the gem doesn't exist" do + it "displays an error with correct status" do + install_gemfile <<-G + source "file://#{gem_repo1}" + G + + bundle "binstubs doesnt_exist" + + expect(exitstatus).to eq(7) if exitstatus + expect(out).to include("Could not find gem 'doesnt_exist'.") + end + end + + context "--path" do + it "sets the binstubs dir" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "binstubs rack --path exec" + + expect(bundled_app("exec/rackup")).to exist + end + + it "setting is saved for bundle install" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rails" + G + + bundle "binstubs rack --path exec" + bundle :install + + expect(bundled_app("exec/rails")).to exist + end + end + + context "after installing with --standalone" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + bundle "install --standalone" + end + + it "includes the standalone path" do + bundle "binstubs rack --standalone" + standalone_line = File.read(bundled_app("bin/rackup")).each_line.find {|line| line.include? "$:.unshift" }.strip + expect(standalone_line).to eq %($:.unshift File.expand_path "../../bundle", path.realpath) + end + end + + context "when the bin already exists" do + it "doesn't overwrite and warns" do + FileUtils.mkdir_p(bundled_app("bin")) + File.open(bundled_app("bin/rackup"), "wb") do |file| + file.print "OMG" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "binstubs rack" + + expect(bundled_app("bin/rackup")).to exist + expect(File.read(bundled_app("bin/rackup"))).to eq("OMG") + expect(out).to include("Skipped rackup") + expect(out).to include("overwrite skipped stubs, use --force") + end + + context "when using --force" do + it "overwrites the binstub" do + FileUtils.mkdir_p(bundled_app("bin")) + File.open(bundled_app("bin/rackup"), "wb") do |file| + file.print "OMG" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "binstubs rack --force" + + expect(bundled_app("bin/rackup")).to exist + expect(File.read(bundled_app("bin/rackup"))).not_to eq("OMG") + end + end + end + + context "when the gem has no bins" do + it "suggests child gems if they have bins" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack-obama" + G + + bundle "binstubs rack-obama" + expect(out).to include("rack-obama has no executables") + expect(out).to include("rack has: rackup") + end + + it "works if child gems don't have bins" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "actionpack" + G + + bundle "binstubs actionpack" + expect(out).to include("no executables for the gem actionpack") + end + + it "works if the gem has development dependencies" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "with_development_dependency" + G + + bundle "binstubs with_development_dependency" + expect(out).to include("no executables for the gem with_development_dependency") + end + end + + context "when BUNDLE_INSTALL is specified" do + it "performs an automatic bundle install" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "config auto_install 1" + bundle "binstubs rack" + expect(out).to include("Installing rack 1.0.0") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "does nothing when already up to date" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "config auto_install 1" + bundle "binstubs rack", :env => { "BUNDLE_INSTALL" => 1 } + expect(out).not_to include("Installing rack 1.0.0") + end + end +end diff --git a/spec/bundler/commands/check_spec.rb b/spec/bundler/commands/check_spec.rb new file mode 100644 index 0000000000..532be07c3f --- /dev/null +++ b/spec/bundler/commands/check_spec.rb @@ -0,0 +1,348 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle check" do + it "returns success when the Gemfile is satisfied" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + bundle :check + expect(exitstatus).to eq(0) if exitstatus + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "works with the --gemfile flag when not in the directory" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + Dir.chdir tmp + bundle "check --gemfile bundled_app/Gemfile" + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "creates a Gemfile.lock by default if one does not exist" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + FileUtils.rm("Gemfile.lock") + + bundle "check" + + expect(bundled_app("Gemfile.lock")).to exist + end + + it "does not create a Gemfile.lock if --dry-run was passed" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + FileUtils.rm("Gemfile.lock") + + bundle "check --dry-run" + + expect(bundled_app("Gemfile.lock")).not_to exist + end + + it "prints a generic error if the missing gems are unresolvable" do + system_gems ["rails-2.3.2"] + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + bundle :check + expect(out).to include("Bundler can't satisfy your Gemfile's dependencies.") + end + + it "prints a generic error if a Gemfile.lock does not exist and a toplevel dependency does not exist" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + bundle :check + expect(exitstatus).to be > 0 if exitstatus + expect(out).to include("Bundler can't satisfy your Gemfile's dependencies.") + end + + it "prints a generic message if you changed your lockfile" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rails' + G + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rails_fail' + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + gem "rails_fail" + G + + bundle :check + expect(out).to include("Bundler can't satisfy your Gemfile's dependencies.") + end + + it "remembers --without option from install" do + gemfile <<-G + source "file://#{gem_repo1}" + group :foo do + gem "rack" + end + G + + bundle "install --without foo" + bundle "check" + expect(exitstatus).to eq(0) if exitstatus + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "ensures that gems are actually installed and not just cached" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :group => :foo + G + + bundle "install --without foo" + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "check" + expect(out).to include("* rack (1.0.0)") + expect(exitstatus).to eq(1) if exitstatus + end + + it "ignores missing gems restricted to other platforms" do + system_gems "rack-1.0.0" + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + platforms :#{not_local_tag} do + gem "activesupport" + end + G + + lockfile <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + activesupport (2.3.5) + rack (1.0.0) + + PLATFORMS + #{local} + #{not_local} + + DEPENDENCIES + rack + activesupport + G + + bundle :check + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "works with env conditionals" do + system_gems "rack-1.0.0" + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + env :NOT_GOING_TO_BE_SET do + gem "activesupport" + end + G + + lockfile <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + activesupport (2.3.5) + rack (1.0.0) + + PLATFORMS + #{local} + #{not_local} + + DEPENDENCIES + rack + activesupport + G + + bundle :check + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "outputs an error when the default Gemfile is not found" do + bundle :check + expect(exitstatus).to eq(10) if exitstatus + expect(out).to include("Could not locate Gemfile") + end + + it "does not output fatal error message" do + bundle :check + expect(exitstatus).to eq(10) if exitstatus + expect(out).not_to include("Unfortunately, a fatal error has occurred. ") + end + + it "should not crash when called multiple times on a new machine" do + gemfile <<-G + gem 'rails', '3.0.0.beta3' + gem 'paperclip', :git => 'git://github.com/thoughtbot/paperclip.git' + G + + simulate_new_machine + bundle "check" + last_out = out + 3.times do + bundle :check + expect(out).to eq(last_out) + expect(err).to lack_errors + end + end + + it "fails when there's no lock file and frozen is set" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "foo" + G + + bundle "install" + bundle "install --deployment" + FileUtils.rm(bundled_app("Gemfile.lock")) + + bundle :check + expect(exitstatus).not_to eq(0) if exitstatus + end + + context "--path" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + bundle "install --path vendor/bundle" + + FileUtils.rm_rf(bundled_app(".bundle")) + end + + it "returns success" do + bundle "check --path vendor/bundle" + expect(exitstatus).to eq(0) if exitstatus + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "should write to .bundle/config" do + bundle "check --path vendor/bundle" + bundle "check" + expect(exitstatus).to eq(0) if exitstatus + end + end + + context "--path vendor/bundle after installing gems in the default directory" do + it "returns false" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + bundle "check --path vendor/bundle" + expect(exitstatus).to eq(1) if exitstatus + expect(out).to match(/The following gems are missing/) + end + end + + describe "when locked" do + before :each do + system_gems "rack-1.0.0" + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0" + G + end + + it "returns success when the Gemfile is satisfied" do + bundle :install + bundle :check + expect(exitstatus).to eq(0) if exitstatus + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "shows what is missing with the current Gemfile if it is not satisfied" do + simulate_new_machine + bundle :check + expect(out).to match(/The following gems are missing/) + expect(out).to include("* rack (1.0") + end + end + + describe "BUNDLED WITH" do + def lock_with(bundler_version = nil) + lock = <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + L + + if bundler_version + lock += "\n BUNDLED WITH\n #{bundler_version}\n" + end + + lock + end + + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + context "is not present" do + it "does not change the lock" do + lockfile lock_with(nil) + bundle :check + lockfile_should_be lock_with(nil) + end + end + + context "is newer" do + it "does not change the lock but warns" do + lockfile lock_with(Bundler::VERSION.succ) + bundle :check + expect(out).to include("the running version of Bundler (#{Bundler::VERSION}) is older than the version that created the lockfile (#{Bundler::VERSION.succ})") + expect(err).to lack_errors + lockfile_should_be lock_with(Bundler::VERSION.succ) + end + end + + context "is older" do + it "does not change the lock" do + lockfile lock_with("1.10.1") + bundle :check + lockfile_should_be lock_with("1.10.1") + end + end + end +end diff --git a/spec/bundler/commands/clean_spec.rb b/spec/bundler/commands/clean_spec.rb new file mode 100644 index 0000000000..02d96a0ff7 --- /dev/null +++ b/spec/bundler/commands/clean_spec.rb @@ -0,0 +1,703 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle clean" do + def should_have_gems(*gems) + gems.each do |g| + expect(vendored_gems("gems/#{g}")).to exist + expect(vendored_gems("specifications/#{g}.gemspec")).to exist + expect(vendored_gems("cache/#{g}.gem")).to exist + end + end + + def should_not_have_gems(*gems) + gems.each do |g| + expect(vendored_gems("gems/#{g}")).not_to exist + expect(vendored_gems("specifications/#{g}.gemspec")).not_to exist + expect(vendored_gems("cache/#{g}.gem")).not_to exist + end + end + + it "removes unused gems that are different" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "foo" + G + + bundle "install --path vendor/bundle --no-clean" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + G + bundle "install" + + bundle :clean + + expect(out).to include("Removing foo (1.0)") + + should_have_gems "thin-1.0", "rack-1.0.0" + should_not_have_gems "foo-1.0" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "removes old version of gem if unused" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "0.9.1" + gem "foo" + G + + bundle "install --path vendor/bundle --no-clean" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + gem "foo" + G + bundle "install" + + bundle :clean + + expect(out).to include("Removing rack (0.9.1)") + + should_have_gems "foo-1.0", "rack-1.0.0" + should_not_have_gems "rack-0.9.1" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "removes new version of gem if unused" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + gem "foo" + G + + bundle "install --path vendor/bundle --no-clean" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "0.9.1" + gem "foo" + G + bundle "install" + + bundle :clean + + expect(out).to include("Removing rack (1.0.0)") + + should_have_gems "foo-1.0", "rack-0.9.1" + should_not_have_gems "rack-1.0.0" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "removes gems in bundle without groups" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo" + + group :test_group do + gem "rack", "1.0.0" + end + G + + bundle "install --path vendor/bundle" + bundle "install --without test_group" + bundle :clean + + expect(out).to include("Removing rack (1.0.0)") + + should_have_gems "foo-1.0" + should_not_have_gems "rack-1.0.0" + + expect(vendored_gems("bin/rackup")).to_not exist + end + + it "does not remove cached git dir if it's being used" do + build_git "foo" + revision = revision_for(lib_path("foo-1.0")) + git_path = lib_path("foo-1.0") + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + git "#{git_path}", :ref => "#{revision}" do + gem "foo" + end + G + + bundle "install --path vendor/bundle" + + bundle :clean + + digest = Digest::SHA1.hexdigest(git_path.to_s) + expect(vendored_gems("cache/bundler/git/foo-1.0-#{digest}")).to exist + end + + it "removes unused git gems" do + build_git "foo", :path => lib_path("foo") + git_path = lib_path("foo") + revision = revision_for(git_path) + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + git "#{git_path}", :ref => "#{revision}" do + gem "foo" + end + G + + bundle "install --path vendor/bundle" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + G + bundle "install" + + bundle :clean + + expect(out).to include("Removing foo (#{revision[0..11]})") + + expect(vendored_gems("gems/rack-1.0.0")).to exist + expect(vendored_gems("bundler/gems/foo-#{revision[0..11]}")).not_to exist + digest = Digest::SHA1.hexdigest(git_path.to_s) + expect(vendored_gems("cache/bundler/git/foo-#{digest}")).not_to exist + + expect(vendored_gems("specifications/rack-1.0.0.gemspec")).to exist + + expect(vendored_gems("bin/rackup")).to exist + end + + it "removes old git gems" do + build_git "foo-bar", :path => lib_path("foo-bar") + revision = revision_for(lib_path("foo-bar")) + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + git "#{lib_path("foo-bar")}" do + gem "foo-bar" + end + G + + bundle "install --path vendor/bundle" + + update_git "foo", :path => lib_path("foo-bar") + revision2 = revision_for(lib_path("foo-bar")) + + bundle "update" + bundle :clean + + expect(out).to include("Removing foo-bar (#{revision[0..11]})") + + expect(vendored_gems("gems/rack-1.0.0")).to exist + expect(vendored_gems("bundler/gems/foo-bar-#{revision[0..11]}")).not_to exist + expect(vendored_gems("bundler/gems/foo-bar-#{revision2[0..11]}")).to exist + + expect(vendored_gems("specifications/rack-1.0.0.gemspec")).to exist + + expect(vendored_gems("bin/rackup")).to exist + end + + it "does not remove nested gems in a git repo" do + build_lib "activesupport", "3.0", :path => lib_path("rails/activesupport") + build_git "rails", "3.0", :path => lib_path("rails") do |s| + s.add_dependency "activesupport", "= 3.0" + end + revision = revision_for(lib_path("rails")) + + gemfile <<-G + gem "activesupport", :git => "#{lib_path("rails")}", :ref => '#{revision}' + G + + bundle "install --path vendor/bundle" + bundle :clean + expect(out).to include("") + + expect(vendored_gems("bundler/gems/rails-#{revision[0..11]}")).to exist + end + + it "does not remove git sources that are in without groups" do + build_git "foo", :path => lib_path("foo") + git_path = lib_path("foo") + revision = revision_for(git_path) + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + group :test do + git "#{git_path}", :ref => "#{revision}" do + gem "foo" + end + end + G + bundle "install --path vendor/bundle --without test" + + bundle :clean + + expect(out).to include("") + expect(vendored_gems("bundler/gems/foo-#{revision[0..11]}")).to exist + digest = Digest::SHA1.hexdigest(git_path.to_s) + expect(vendored_gems("cache/bundler/git/foo-#{digest}")).to_not exist + end + + it "does not blow up when using without groups" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + + group :development do + gem "foo" + end + G + + bundle "install --path vendor/bundle --without development" + + bundle :clean + expect(exitstatus).to eq(0) if exitstatus + end + + it "displays an error when used without --path" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + G + + bundle :clean + + expect(exitstatus).to eq(1) if exitstatus + expect(out).to include("--force") + end + + # handling bundle clean upgrade path from the pre's + it "removes .gem/.gemspec file even if there's no corresponding gem dir" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "foo" + G + + bundle "install --path vendor/bundle" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo" + G + bundle "install" + + FileUtils.rm(vendored_gems("bin/rackup")) + FileUtils.rm_rf(vendored_gems("gems/thin-1.0")) + FileUtils.rm_rf(vendored_gems("gems/rack-1.0.0")) + + bundle :clean + + should_not_have_gems "thin-1.0", "rack-1.0" + should_have_gems "foo-1.0" + + expect(vendored_gems("bin/rackup")).not_to exist + end + + it "does not call clean automatically when using system gems" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "rack" + G + bundle :install + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + bundle :install + + sys_exec "gem list" + expect(out).to include("rack (1.0.0)") + expect(out).to include("thin (1.0)") + end + + it "--clean should override the bundle setting on install" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "rack" + G + bundle "install --path vendor/bundle --clean" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + bundle "install" + + should_have_gems "rack-1.0.0" + should_not_have_gems "thin-1.0" + end + + it "--clean should override the bundle setting on update" do + build_repo2 + + gemfile <<-G + source "file://#{gem_repo2}" + + gem "foo" + G + bundle "install --path vendor/bundle --clean" + + update_repo2 do + build_gem "foo", "1.0.1" + end + + bundle "update" + + should_have_gems "foo-1.0.1" + should_not_have_gems "foo-1.0" + end + + it "does not clean automatically on --path" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "rack" + G + bundle "install --path vendor/bundle" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + bundle "install" + + should_have_gems "rack-1.0.0", "thin-1.0" + end + + it "does not clean on bundle update with --path" do + build_repo2 + + gemfile <<-G + source "file://#{gem_repo2}" + + gem "foo" + G + bundle "install --path vendor/bundle" + + update_repo2 do + build_gem "foo", "1.0.1" + end + + bundle :update + should_have_gems "foo-1.0", "foo-1.0.1" + end + + it "does not clean on bundle update when using --system" do + build_repo2 + + gemfile <<-G + source "file://#{gem_repo2}" + + gem "foo" + G + bundle "install" + + update_repo2 do + build_gem "foo", "1.0.1" + end + bundle :update + + sys_exec "gem list" + expect(out).to include("foo (1.0.1, 1.0)") + end + + it "cleans system gems when --force is used" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo" + gem "rack" + G + bundle :install + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + bundle :install + bundle "clean --force" + + expect(out).to include("Removing foo (1.0)") + sys_exec "gem list" + expect(out).not_to include("foo (1.0)") + expect(out).to include("rack (1.0.0)") + end + + describe "when missing permissions" do + after do + FileUtils.chmod(0o755, default_bundle_path("cache")) + end + it "returns a helpful error message" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo" + gem "rack" + G + bundle :install + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + bundle :install + + system_cache_path = default_bundle_path("cache") + FileUtils.chmod(0o500, system_cache_path) + + bundle :clean, :force => true + + expect(out).to include(system_gem_path.to_s) + expect(out).to include("grant write permissions") + + sys_exec "gem list" + expect(out).to include("foo (1.0)") + expect(out).to include("rack (1.0.0)") + end + end + + it "cleans git gems with a 7 length git revision" do + build_git "foo" + revision = revision_for(lib_path("foo-1.0")) + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + bundle "install --path vendor/bundle" + + # mimic 7 length git revisions in Gemfile.lock + gemfile_lock = File.read(bundled_app("Gemfile.lock")).split("\n") + gemfile_lock.each_with_index do |line, index| + gemfile_lock[index] = line[0..(11 + 7)] if line.include?(" revision:") + end + File.open(bundled_app("Gemfile.lock"), "w") do |file| + file.print gemfile_lock.join("\n") + end + + bundle "install --path vendor/bundle" + + bundle :clean + + expect(out).not_to include("Removing foo (1.0 #{revision[0..6]})") + + expect(vendored_gems("bundler/gems/foo-1.0-#{revision[0..6]}")).to exist + end + + it "when using --force on system gems, it doesn't remove binaries" do + build_repo2 + update_repo2 do + build_gem "bindir" do |s| + s.bindir = "exe" + s.executables = "foo" + end + end + + gemfile <<-G + source "file://#{gem_repo2}" + + gem "bindir" + G + bundle :install + + bundle "clean --force" + + sys_exec "foo" + + expect(exitstatus).to eq(0) if exitstatus + expect(out).to eq("1.0") + end + + it "doesn't blow up on path gems without a .gempsec" do + relative_path = "vendor/private_gems/bar-1.0" + absolute_path = bundled_app(relative_path) + FileUtils.mkdir_p("#{absolute_path}/lib/bar") + File.open("#{absolute_path}/lib/bar/bar.rb", "wb") do |file| + file.puts "module Bar; end" + end + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo" + gem "bar", "1.0", :path => "#{relative_path}" + G + + bundle "install --path vendor/bundle" + bundle :clean + + expect(exitstatus).to eq(0) if exitstatus + end + + it "doesn't remove gems in dry-run mode with path set" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "foo" + G + + bundle "install --path vendor/bundle --no-clean" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + G + + bundle :install + + bundle "clean --dry-run" + + expect(out).not_to include("Removing foo (1.0)") + expect(out).to include("Would have removed foo (1.0)") + + should_have_gems "thin-1.0", "rack-1.0.0", "foo-1.0" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "doesn't remove gems in dry-run mode with no path set" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "foo" + G + + bundle "install --path vendor/bundle --no-clean" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + G + + bundle :install + + bundle "configuration --delete path" + + bundle "clean --dry-run" + + expect(out).not_to include("Removing foo (1.0)") + expect(out).to include("Would have removed foo (1.0)") + + should_have_gems "thin-1.0", "rack-1.0.0", "foo-1.0" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "doesn't store dry run as a config setting" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "foo" + G + + bundle "install --path vendor/bundle --no-clean" + bundle "config dry_run false" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + G + + bundle :install + + bundle "clean" + + expect(out).to include("Removing foo (1.0)") + expect(out).not_to include("Would have removed foo (1.0)") + + should_have_gems "thin-1.0", "rack-1.0.0" + should_not_have_gems "foo-1.0" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "foo" + G + + bundle "install --path vendor/bundle --no-clean" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "weakling" + G + + bundle "config auto_install 1" + bundle :clean + expect(out).to include("Installing weakling 0.0.3") + should_have_gems "thin-1.0", "rack-1.0.0", "weakling-0.0.3" + should_not_have_gems "foo-1.0" + end + + it "doesn't remove extensions artifacts from bundled git gems after clean", :ruby_repo, :rubygems => "2.2" do + build_git "very_simple_git_binary", &:add_c_extension + + revision = revision_for(lib_path("very_simple_git_binary-1.0")) + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}" + G + + bundle! "install --path vendor/bundle" + expect(vendored_gems("bundler/gems/extensions")).to exist + expect(vendored_gems("bundler/gems/very_simple_git_binary-1.0-#{revision[0..11]}")).to exist + + bundle! :clean + expect(out).to eq("") + + expect(vendored_gems("bundler/gems/extensions")).to exist + expect(vendored_gems("bundler/gems/very_simple_git_binary-1.0-#{revision[0..11]}")).to exist + end +end diff --git a/spec/bundler/commands/config_spec.rb b/spec/bundler/commands/config_spec.rb new file mode 100644 index 0000000000..a3ca696ec1 --- /dev/null +++ b/spec/bundler/commands/config_spec.rb @@ -0,0 +1,385 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe ".bundle/config" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0.0" + G + end + + describe "config" do + before { bundle "config foo bar" } + + it "prints a detailed report of local and user configuration" do + bundle "config" + + expect(out).to include("Settings are listed in order of priority. The top value will be used") + expect(out).to include("foo\nSet for the current user") + expect(out).to include(": \"bar\"") + end + + context "given --parseable flag" do + it "prints a minimal report of local and user configuration" do + bundle "config --parseable" + expect(out).to include("foo=bar") + end + + context "with global config" do + it "prints config assigned to local scope" do + bundle "config --local foo bar2" + bundle "config --parseable" + expect(out).to include("foo=bar2") + end + end + + context "with env overwrite" do + it "prints config with env" do + bundle "config --parseable", :env => { "BUNDLE_FOO" => "bar3" } + expect(out).to include("foo=bar3") + end + end + end + end + + describe "BUNDLE_APP_CONFIG" do + it "can be moved with an environment variable" do + ENV["BUNDLE_APP_CONFIG"] = tmp("foo/bar").to_s + bundle "install --path vendor/bundle" + + expect(bundled_app(".bundle")).not_to exist + expect(tmp("foo/bar/config")).to exist + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "can provide a relative path with the environment variable" do + FileUtils.mkdir_p bundled_app("omg") + Dir.chdir bundled_app("omg") + + ENV["BUNDLE_APP_CONFIG"] = "../foo" + bundle "install --path vendor/bundle" + + expect(bundled_app(".bundle")).not_to exist + expect(bundled_app("../foo/config")).to exist + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + describe "global" do + before(:each) { bundle :install } + + it "is the default" do + bundle "config foo global" + run "puts Bundler.settings[:foo]" + expect(out).to eq("global") + end + + it "can also be set explicitly" do + bundle! "config --global foo global" + run! "puts Bundler.settings[:foo]" + expect(out).to eq("global") + end + + it "has lower precedence than local" do + bundle "config --local foo local" + + bundle "config --global foo global" + expect(out).to match(/Your application has set foo to "local"/) + + run "puts Bundler.settings[:foo]" + expect(out).to eq("local") + end + + it "has lower precedence than env" do + begin + ENV["BUNDLE_FOO"] = "env" + + bundle "config --global foo global" + expect(out).to match(/You have a bundler environment variable for foo set to "env"/) + + run "puts Bundler.settings[:foo]" + expect(out).to eq("env") + ensure + ENV.delete("BUNDLE_FOO") + end + end + + it "can be deleted" do + bundle "config --global foo global" + bundle "config --delete foo" + + run "puts Bundler.settings[:foo] == nil" + expect(out).to eq("true") + end + + it "warns when overriding" do + bundle "config --global foo previous" + bundle "config --global foo global" + expect(out).to match(/You are replacing the current global value of foo/) + + run "puts Bundler.settings[:foo]" + expect(out).to eq("global") + end + + it "does not warn when using the same value twice" do + bundle "config --global foo value" + bundle "config --global foo value" + expect(out).not_to match(/You are replacing the current global value of foo/) + + run "puts Bundler.settings[:foo]" + expect(out).to eq("value") + end + + it "expands the path at time of setting" do + bundle "config --global local.foo .." + run "puts Bundler.settings['local.foo']" + expect(out).to eq(File.expand_path(Dir.pwd + "/..")) + end + + it "saves with parseable option" do + bundle "config --global --parseable foo value" + expect(out).to eq("foo=value") + run "puts Bundler.settings['foo']" + expect(out).to eq("value") + end + + context "when replacing a current value with the parseable flag" do + before { bundle "config --global foo value" } + it "prints the current value in a parseable format" do + bundle "config --global --parseable foo value2" + expect(out).to eq "foo=value2" + run "puts Bundler.settings['foo']" + expect(out).to eq("value2") + end + end + end + + describe "local" do + before(:each) { bundle :install } + + it "can also be set explicitly" do + bundle "config --local foo local" + run "puts Bundler.settings[:foo]" + expect(out).to eq("local") + end + + it "has higher precedence than env" do + begin + ENV["BUNDLE_FOO"] = "env" + bundle "config --local foo local" + + run "puts Bundler.settings[:foo]" + expect(out).to eq("local") + ensure + ENV.delete("BUNDLE_FOO") + end + end + + it "can be deleted" do + bundle "config --local foo local" + bundle "config --delete foo" + + run "puts Bundler.settings[:foo] == nil" + expect(out).to eq("true") + end + + it "warns when overriding" do + bundle "config --local foo previous" + bundle "config --local foo local" + expect(out).to match(/You are replacing the current local value of foo/) + + run "puts Bundler.settings[:foo]" + expect(out).to eq("local") + end + + it "expands the path at time of setting" do + bundle "config --local local.foo .." + run "puts Bundler.settings['local.foo']" + expect(out).to eq(File.expand_path(Dir.pwd + "/..")) + end + + it "can be deleted with parseable option" do + bundle "config --local foo value" + bundle "config --delete --parseable foo" + expect(out).to eq "" + run "puts Bundler.settings['foo'] == nil" + expect(out).to eq("true") + end + end + + describe "env" do + before(:each) { bundle :install } + + it "can set boolean properties via the environment" do + ENV["BUNDLE_FROZEN"] = "true" + + run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end" + expect(out).to eq("true") + end + + it "can set negative boolean properties via the environment" do + run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end" + expect(out).to eq("false") + + ENV["BUNDLE_FROZEN"] = "false" + + run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end" + expect(out).to eq("false") + + ENV["BUNDLE_FROZEN"] = "0" + + run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end" + expect(out).to eq("false") + + ENV["BUNDLE_FROZEN"] = "" + + run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end" + expect(out).to eq("false") + end + + it "can set properties with periods via the environment" do + ENV["BUNDLE_FOO__BAR"] = "baz" + + run "puts Bundler.settings['foo.bar']" + expect(out).to eq("baz") + end + end + + describe "parseable option" do + it "prints an empty string" do + bundle "config foo --parseable" + + expect(out).to eq "" + end + + it "only prints the value of the config" do + bundle "config foo local" + bundle "config foo --parseable" + + expect(out).to eq "foo=local" + end + + it "can print global config" do + bundle "config --global bar value" + bundle "config bar --parseable" + + expect(out).to eq "bar=value" + end + + it "preferes local config over global" do + bundle "config --local bar value2" + bundle "config --global bar value" + bundle "config bar --parseable" + + expect(out).to eq "bar=value2" + end + end + + describe "gem mirrors" do + before(:each) { bundle :install } + + it "configures mirrors using keys with `mirror.`" do + bundle "config --local mirror.http://gems.example.org http://gem-mirror.example.org" + run(<<-E) +Bundler.settings.gem_mirrors.each do |k, v| + puts "\#{k} => \#{v}" +end +E + expect(out).to eq("http://gems.example.org/ => http://gem-mirror.example.org/") + end + end + + describe "quoting" do + before(:each) { gemfile "# no gems" } + let(:long_string) do + "--with-xml2-include=/usr/pkg/include/libxml2 --with-xml2-lib=/usr/pkg/lib " \ + "--with-xslt-dir=/usr/pkg" + end + + it "saves quotes" do + bundle "config foo something\\'" + run "puts Bundler.settings[:foo]" + expect(out).to eq("something'") + end + + it "doesn't return quotes around values", :ruby => "1.9" do + bundle "config foo '1'" + run "puts Bundler.settings.send(:global_config_file).read" + expect(out).to include('"1"') + run "puts Bundler.settings[:foo]" + expect(out).to eq("1") + end + + it "doesn't duplicate quotes around values", :if => (RUBY_VERSION >= "2.1") do + bundled_app(".bundle").mkpath + File.open(bundled_app(".bundle/config"), "w") do |f| + f.write 'BUNDLE_FOO: "$BUILD_DIR"' + end + + bundle "config bar baz" + run "puts Bundler.settings.send(:local_config_file).read" + + # Starting in Ruby 2.1, YAML automatically adds double quotes + # around some values, including $ and newlines. + expect(out).to include('BUNDLE_FOO: "$BUILD_DIR"') + end + + it "doesn't duplicate quotes around long wrapped values" do + bundle "config foo #{long_string}" + + run "puts Bundler.settings[:foo]" + expect(out).to eq(long_string) + + bundle "config bar baz" + + run "puts Bundler.settings[:foo]" + expect(out).to eq(long_string) + end + end + + describe "very long lines" do + before(:each) { bundle :install } + + let(:long_string) do + "--with-xml2-include=/usr/pkg/include/libxml2 --with-xml2-lib=/usr/pkg/lib " \ + "--with-xslt-dir=/usr/pkg" + end + + let(:long_string_without_special_characters) do + "here is quite a long string that will wrap to a second line but will not be " \ + "surrounded by quotes" + end + + it "doesn't wrap values" do + bundle "config foo #{long_string}" + run "puts Bundler.settings[:foo]" + expect(out).to match(long_string) + end + + it "can read wrapped unquoted values" do + bundle "config foo #{long_string_without_special_characters}" + run "puts Bundler.settings[:foo]" + expect(out).to match(long_string_without_special_characters) + end + end +end + +RSpec.describe "setting gemfile via config" do + context "when only the non-default Gemfile exists" do + it "persists the gemfile location to .bundle/config" do + File.open(bundled_app("NotGemfile"), "w") do |f| + f.write <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + end + + bundle "config --local gemfile #{bundled_app("NotGemfile")}" + expect(File.exist?(".bundle/config")).to eq(true) + + bundle "config" + expect(out).to include("NotGemfile") + end + end +end diff --git a/spec/bundler/commands/console_spec.rb b/spec/bundler/commands/console_spec.rb new file mode 100644 index 0000000000..de14b6db5f --- /dev/null +++ b/spec/bundler/commands/console_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle console" do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + G + end + + it "starts IRB with the default group loaded" do + bundle "console" do |input, _, _| + input.puts("puts RACK") + input.puts("exit") + end + expect(out).to include("0.9.1") + end + + it "uses IRB as default console" do + bundle "console" do |input, _, _| + input.puts("__method__") + input.puts("exit") + end + expect(out).to include(":irb_binding") + end + + it "starts another REPL if configured as such" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "pry" + G + bundle "config console pry" + + bundle "console" do |input, _, _| + input.puts("__method__") + input.puts("exit") + end + expect(out).to include(":__pry__") + end + + it "falls back to IRB if the other REPL isn't available" do + bundle "config console pry" + # make sure pry isn't there + + bundle "console" do |input, _, _| + input.puts("__method__") + input.puts("exit") + end + expect(out).to include(":irb_binding") + end + + it "doesn't load any other groups" do + bundle "console" do |input, _, _| + input.puts("puts ACTIVESUPPORT") + input.puts("exit") + end + expect(out).to include("NameError") + end + + describe "when given a group" do + it "loads the given group" do + bundle "console test" do |input, _, _| + input.puts("puts ACTIVESUPPORT") + input.puts("exit") + end + expect(out).to include("2.3.5") + end + + it "loads the default group" do + bundle "console test" do |input, _, _| + input.puts("puts RACK") + input.puts("exit") + end + expect(out).to include("0.9.1") + end + + it "doesn't load other groups" do + bundle "console test" do |input, _, _| + input.puts("puts RACK_MIDDLEWARE") + input.puts("exit") + end + expect(out).to include("NameError") + end + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + gem "foo" + G + + bundle "config auto_install 1" + bundle :console do |input, _, _| + input.puts("puts 'hello'") + input.puts("exit") + end + expect(out).to include("Installing foo 1.0") + expect(out).to include("hello") + expect(the_bundle).to include_gems "foo 1.0" + end +end diff --git a/spec/bundler/commands/doctor_spec.rb b/spec/bundler/commands/doctor_spec.rb new file mode 100644 index 0000000000..7c6e48ce19 --- /dev/null +++ b/spec/bundler/commands/doctor_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true +require "spec_helper" +require "stringio" +require "bundler/cli" +require "bundler/cli/doctor" + +RSpec.describe "bundle doctor" do + before(:each) do + @stdout = StringIO.new + + [:error, :warn].each do |method| + allow(Bundler.ui).to receive(method).and_wrap_original do |m, message| + m.call message + @stdout.puts message + end + end + end + + it "exits with no message if the installed gem has no C extensions" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install + Bundler::CLI::Doctor.new({}).run + expect(@stdout.string).to be_empty + end + + it "exits with no message if the installed gem's C extension dylib breakage is fine" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install + doctor = Bundler::CLI::Doctor.new({}) + expect(doctor).to receive(:bundles_for_gem).exactly(2).times.and_return ["/path/to/rack/rack.bundle"] + expect(doctor).to receive(:dylibs).exactly(2).times.and_return ["/usr/lib/libSystem.dylib"] + allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:exist?).with("/usr/lib/libSystem.dylib").and_return(true) + doctor.run + expect(@stdout.string).to be_empty + end + + it "exits with a message if one of the linked libraries is missing" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install + doctor = Bundler::CLI::Doctor.new({}) + expect(doctor).to receive(:bundles_for_gem).exactly(2).times.and_return ["/path/to/rack/rack.bundle"] + expect(doctor).to receive(:dylibs).exactly(2).times.and_return ["/usr/local/opt/icu4c/lib/libicui18n.57.1.dylib"] + allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:exist?).with("/usr/local/opt/icu4c/lib/libicui18n.57.1.dylib").and_return(false) + expect { doctor.run }.to raise_error Bundler::ProductionError, strip_whitespace(<<-E).strip + The following gems are missing OS dependencies: + * bundler: /usr/local/opt/icu4c/lib/libicui18n.57.1.dylib + * rack: /usr/local/opt/icu4c/lib/libicui18n.57.1.dylib + E + end +end diff --git a/spec/bundler/commands/exec_spec.rb b/spec/bundler/commands/exec_spec.rb new file mode 100644 index 0000000000..7736adefe1 --- /dev/null +++ b/spec/bundler/commands/exec_spec.rb @@ -0,0 +1,736 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle exec" do + let(:system_gems_to_install) { %w(rack-1.0.0 rack-0.9.1) } + before :each do + system_gems(system_gems_to_install) + end + + it "activates the correct gem" do + gemfile <<-G + gem "rack", "0.9.1" + G + + bundle "exec rackup" + expect(out).to eq("0.9.1") + end + + it "works when the bins are in ~/.bundle" do + install_gemfile <<-G + gem "rack" + G + + bundle "exec rackup" + expect(out).to eq("1.0.0") + end + + it "works when running from a random directory", :ruby_repo do + install_gemfile <<-G + gem "rack" + G + + bundle "exec 'cd #{tmp("gems")} && rackup'" + + expect(out).to include("1.0.0") + end + + it "works when exec'ing something else" do + install_gemfile 'gem "rack"' + bundle "exec echo exec" + expect(out).to eq("exec") + end + + it "works when exec'ing to ruby" do + install_gemfile 'gem "rack"' + bundle "exec ruby -e 'puts %{hi}'" + expect(out).to eq("hi") + end + + it "accepts --verbose" do + install_gemfile 'gem "rack"' + bundle "exec --verbose echo foobar" + expect(out).to eq("foobar") + end + + it "passes --verbose to command if it is given after the command" do + install_gemfile 'gem "rack"' + bundle "exec echo --verbose" + expect(out).to eq("--verbose") + end + + it "handles --keep-file-descriptors" do + require "tempfile" + + command = Tempfile.new("io-test") + command.sync = true + command.write <<-G + if ARGV[0] + IO.for_fd(ARGV[0].to_i) + else + require 'tempfile' + io = Tempfile.new("io-test-fd") + args = %W[#{Gem.ruby} -I#{lib} #{bindir.join("bundle")} exec --keep-file-descriptors #{Gem.ruby} #{command.path} \#{io.to_i}] + args << { io.to_i => io } if RUBY_VERSION >= "2.0" + exec(*args) + end + G + + install_gemfile "" + sys_exec("#{Gem.ruby} #{command.path}") + + if Bundler.current_ruby.ruby_2? + expect(out).to eq("") + else + expect(out).to eq("Ruby version #{RUBY_VERSION} defaults to keeping non-standard file descriptors on Kernel#exec.") + end + + expect(err).to lack_errors + end + + it "accepts --keep-file-descriptors" do + install_gemfile "" + bundle "exec --keep-file-descriptors echo foobar" + + expect(err).to lack_errors + end + + it "can run a command named --verbose" do + install_gemfile 'gem "rack"' + File.open("--verbose", "w") do |f| + f.puts "#!/bin/sh" + f.puts "echo foobar" + end + File.chmod(0o744, "--verbose") + with_path_as(".") do + bundle "exec -- --verbose" + end + expect(out).to eq("foobar") + end + + it "handles different versions in different bundles" do + build_repo2 do + build_gem "rack_two", "1.0.0" do |s| + s.executables = "rackup" + end + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "0.9.1" + G + + Dir.chdir bundled_app2 do + install_gemfile bundled_app2("Gemfile"), <<-G + source "file://#{gem_repo2}" + gem "rack_two", "1.0.0" + G + end + + bundle! "exec rackup" + + expect(out).to eq("0.9.1") + + Dir.chdir bundled_app2 do + bundle! "exec rackup" + expect(out).to eq("1.0.0") + end + end + + it "handles gems installed with --without" do + install_gemfile <<-G, :without => :middleware + source "file://#{gem_repo1}" + gem "rack" # rack 0.9.1 and 1.0 exist + + group :middleware do + gem "rack_middleware" # rack_middleware depends on rack 0.9.1 + end + G + + bundle "exec rackup" + + expect(out).to eq("0.9.1") + expect(the_bundle).not_to include_gems "rack_middleware 1.0" + end + + it "does not duplicate already exec'ed RUBYOPT" do + install_gemfile <<-G + gem "rack" + G + + rubyopt = ENV["RUBYOPT"] + rubyopt = "-rbundler/setup #{rubyopt}" + + bundle "exec 'echo $RUBYOPT'" + expect(out).to have_rubyopts(rubyopt) + + bundle "exec 'echo $RUBYOPT'", :env => { "RUBYOPT" => rubyopt } + expect(out).to have_rubyopts(rubyopt) + end + + it "does not duplicate already exec'ed RUBYLIB", :ruby_repo do + install_gemfile <<-G + gem "rack" + G + + rubylib = ENV["RUBYLIB"] + rubylib = "#{rubylib}".split(File::PATH_SEPARATOR).unshift "#{bundler_path}" + rubylib = rubylib.uniq.join(File::PATH_SEPARATOR) + + bundle "exec 'echo $RUBYLIB'" + expect(out).to include(rubylib) + + bundle "exec 'echo $RUBYLIB'", :env => { "RUBYLIB" => rubylib } + expect(out).to include(rubylib) + end + + it "errors nicely when the argument doesn't exist" do + install_gemfile <<-G + gem "rack" + G + + bundle "exec foobarbaz" + expect(exitstatus).to eq(127) if exitstatus + expect(out).to include("bundler: command not found: foobarbaz") + expect(out).to include("Install missing gem executables with `bundle install`") + end + + it "errors nicely when the argument is not executable" do + install_gemfile <<-G + gem "rack" + G + + bundle "exec touch foo" + bundle "exec ./foo" + expect(exitstatus).to eq(126) if exitstatus + expect(out).to include("bundler: not executable: ./foo") + end + + it "errors nicely when no arguments are passed" do + install_gemfile <<-G + gem "rack" + G + + bundle "exec" + expect(exitstatus).to eq(128) if exitstatus + expect(out).to include("bundler: exec needs a command to run") + end + + it "raises a helpful error when exec'ing to something outside of the bundle", :ruby_repo, :rubygems => ">= 2.5.2" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "with_license" + G + [true, false].each do |l| + bundle! "config disable_exec_load #{l}" + bundle "exec rackup" + expect(err).to include "can't find executable rackup for gem rack. rack is not currently included in the bundle, perhaps you meant to add it to your Gemfile?" + end + end + + # Different error message on old RG versions (before activate_bin_path) because they + # called `Kernel#gem` directly + it "raises a helpful error when exec'ing to something outside of the bundle", :rubygems => "< 2.5.2" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "with_license" + G + [true, false].each do |l| + bundle! "config disable_exec_load #{l}" + bundle "exec rackup" + expect(err).to include "rack is not part of the bundle. Add it to your Gemfile." + end + end + + describe "with help flags" do + each_prefix = proc do |string, &blk| + 1.upto(string.length) {|l| blk.call(string[0, l]) } + end + each_prefix.call("exec") do |exec| + describe "when #{exec} is used" do + before(:each) do + install_gemfile <<-G + gem "rack" + G + + create_file("print_args", <<-'RUBY') + #!/usr/bin/env ruby + puts "args: #{ARGV.inspect}" + RUBY + bundled_app("print_args").chmod(0o755) + end + + it "shows executable's man page when --help is after the executable" do + bundle "#{exec} print_args --help" + expect(out).to eq('args: ["--help"]') + end + + it "shows executable's man page when --help is after the executable and an argument" do + bundle "#{exec} print_args foo --help" + expect(out).to eq('args: ["foo", "--help"]') + + bundle "#{exec} print_args foo bar --help" + expect(out).to eq('args: ["foo", "bar", "--help"]') + + bundle "#{exec} print_args foo --help bar" + expect(out).to eq('args: ["foo", "--help", "bar"]') + end + + it "shows executable's man page when the executable has a -" do + FileUtils.mv(bundled_app("print_args"), bundled_app("docker-template")) + bundle "#{exec} docker-template build discourse --help" + expect(out).to eq('args: ["build", "discourse", "--help"]') + end + + it "shows executable's man page when --help is after another flag" do + bundle "#{exec} print_args --bar --help" + expect(out).to eq('args: ["--bar", "--help"]') + end + + it "uses executable's original behavior for -h" do + bundle "#{exec} print_args -h" + expect(out).to eq('args: ["-h"]') + end + + it "shows bundle-exec's man page when --help is between exec and the executable", :ruby_repo do + with_fake_man do + bundle "#{exec} --help cat" + end + expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + end + + it "shows bundle-exec's man page when --help is before exec", :ruby_repo do + with_fake_man do + bundle "--help #{exec}" + end + expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + end + + it "shows bundle-exec's man page when -h is before exec", :ruby_repo do + with_fake_man do + bundle "-h #{exec}" + end + expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + end + + it "shows bundle-exec's man page when --help is after exec", :ruby_repo do + with_fake_man do + bundle "#{exec} --help" + end + expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + end + + it "shows bundle-exec's man page when -h is after exec", :ruby_repo do + with_fake_man do + bundle "#{exec} -h" + end + expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + end + end + end + end + + describe "with gem executables" do + describe "run from a random directory" do + before(:each) do + install_gemfile <<-G + gem "rack" + G + end + + it "works when unlocked", :ruby_repo do + bundle "exec 'cd #{tmp("gems")} && rackup'" + expect(out).to eq("1.0.0") + expect(out).to include("1.0.0") + end + + it "works when locked", :ruby_repo do + expect(the_bundle).to be_locked + bundle "exec 'cd #{tmp("gems")} && rackup'" + expect(out).to include("1.0.0") + end + end + + describe "from gems bundled via :path" do + before(:each) do + build_lib "fizz", :path => home("fizz") do |s| + s.executables = "fizz" + end + + install_gemfile <<-G + gem "fizz", :path => "#{File.expand_path(home("fizz"))}" + G + end + + it "works when unlocked" do + bundle "exec fizz" + expect(out).to eq("1.0") + end + + it "works when locked" do + expect(the_bundle).to be_locked + + bundle "exec fizz" + expect(out).to eq("1.0") + end + end + + describe "from gems bundled via :git" do + before(:each) do + build_git "fizz_git" do |s| + s.executables = "fizz_git" + end + + install_gemfile <<-G + gem "fizz_git", :git => "#{lib_path("fizz_git-1.0")}" + G + end + + it "works when unlocked" do + bundle "exec fizz_git" + expect(out).to eq("1.0") + end + + it "works when locked" do + expect(the_bundle).to be_locked + bundle "exec fizz_git" + expect(out).to eq("1.0") + end + end + + describe "from gems bundled via :git with no gemspec" do + before(:each) do + build_git "fizz_no_gemspec", :gemspec => false do |s| + s.executables = "fizz_no_gemspec" + end + + install_gemfile <<-G + gem "fizz_no_gemspec", "1.0", :git => "#{lib_path("fizz_no_gemspec-1.0")}" + G + end + + it "works when unlocked" do + bundle "exec fizz_no_gemspec" + expect(out).to eq("1.0") + end + + it "works when locked" do + expect(the_bundle).to be_locked + bundle "exec fizz_no_gemspec" + expect(out).to eq("1.0") + end + end + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "0.9.1" + gem "foo" + G + + bundle "config auto_install 1" + bundle "exec rackup" + expect(out).to include("Installing foo 1.0") + end + + describe "with gems bundled via :path with invalid gemspecs" do + it "outputs the gemspec validation errors", :rubygems => ">= 1.7.2" do + build_lib "foo" + + gemspec = lib_path("foo-1.0").join("foo.gemspec").to_s + File.open(gemspec, "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = 'foo' + s.version = '1.0' + s.summary = 'TODO: Add summary' + s.authors = 'Me' + end + G + end + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo-1.0")}" + G + + bundle "exec irb" + + expect(err).to match("The gemspec at #{lib_path("foo-1.0").join("foo.gemspec")} is not valid") + expect(err).to match('"TODO" is not a summary') + end + end + + describe "with gems bundled for deployment" do + it "works when calling bundler from another script" do + gemfile <<-G + module Monkey + def bin_path(a,b,c) + raise Gem::GemNotFoundException.new('Fail') + end + end + Bundler.rubygems.extend(Monkey) + G + bundle "install --deployment" + bundle "exec ruby -e '`#{bindir.join("bundler")} -v`; puts $?.success?'" + expect(out).to match("true") + end + end + + context "`load`ing a ruby file instead of `exec`ing" do + let(:path) { bundled_app("ruby_executable") } + let(:shebang) { "#!/usr/bin/env ruby" } + let(:executable) { <<-RUBY.gsub(/^ */, "").strip } + #{shebang} + + require "rack" + puts "EXEC: \#{caller.grep(/load/).empty? ? 'exec' : 'load'}" + puts "ARGS: \#{$0} \#{ARGV.join(' ')}" + puts "RACK: \#{RACK}" + process_title = `ps -o args -p \#{Process.pid}`.split("\n", 2).last.strip + puts "PROCESS: \#{process_title}" + RUBY + + before do + path.open("w") {|f| f << executable } + path.chmod(0o755) + + install_gemfile <<-G + gem "rack" + G + end + + let(:exec) { "EXEC: load" } + let(:args) { "ARGS: #{path} arg1 arg2" } + let(:rack) { "RACK: 1.0.0" } + let(:process) do + title = "PROCESS: #{path}" + title += " arg1 arg2" if RUBY_VERSION >= "2.1" + title + end + let(:exit_code) { 0 } + let(:expected) { [exec, args, rack, process].join("\n") } + let(:expected_err) { "" } + + subject { bundle "exec #{path} arg1 arg2" } + + shared_examples_for "it runs" do + it "like a normally executed executable" do + subject + expect(exitstatus).to eq(exit_code) if exitstatus + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end + end + + it_behaves_like "it runs" + + context "the executable exits explicitly" do + let(:executable) { super() << "\nexit #{exit_code}\nputs 'POST_EXIT'\n" } + + context "with exit 0" do + it_behaves_like "it runs" + end + + context "with exit 99" do + let(:exit_code) { 99 } + it_behaves_like "it runs" + end + end + + context "the executable is empty" do + let(:executable) { "" } + + let(:exit_code) { 0 } + let(:expected) { "#{path} is empty" } + let(:expected_err) { "" } + if LessThanProc.with(RUBY_VERSION).call("1.9") + # Kernel#exec in ruby < 1.9 will raise Errno::ENOEXEC if the command content is empty, + # even if the command is set as an executable. + pending "Kernel#exec is different" + else + it_behaves_like "it runs" + end + end + + context "the executable raises" do + let(:executable) { super() << "\nraise 'ERROR'" } + let(:exit_code) { 1 } + let(:expected) { super() << "\nbundler: failed to load command: #{path} (#{path})" } + let(:expected_err) do + "RuntimeError: ERROR\n #{path}:10" + + (Bundler.current_ruby.ruby_18? ? "" : ":in `<top (required)>'") + end + it_behaves_like "it runs" + end + + context "when the file uses the current ruby shebang", :ruby_repo do + let(:shebang) { "#!#{Gem.ruby}" } + it_behaves_like "it runs" + end + + context "when Bundler.setup fails" do + before do + gemfile <<-G + gem 'rack', '2' + G + ENV["BUNDLER_FORCE_TTY"] = "true" + end + + let(:exit_code) { Bundler::GemNotFound.new.status_code } + let(:expected) { <<-EOS.strip } +\e[31mCould not find gem 'rack (= 2)' in any of the gem sources listed in your Gemfile.\e[0m +\e[33mRun `bundle install` to install missing gems.\e[0m + EOS + + it_behaves_like "it runs" + end + + context "when the executable exits non-zero via at_exit" do + let(:executable) { super() + "\n\nat_exit { $! ? raise($!) : exit(1) }" } + let(:exit_code) { 1 } + + it_behaves_like "it runs" + end + + context "when disable_exec_load is set" do + let(:exec) { "EXEC: exec" } + let(:process) { "PROCESS: ruby #{path} arg1 arg2" } + + before do + bundle "config disable_exec_load true" + end + + it_behaves_like "it runs" + end + + context "regarding $0 and __FILE__" do + let(:executable) { super() + <<-'RUBY' } + + puts "$0: #{$0.inspect}" + puts "__FILE__: #{__FILE__.inspect}" + RUBY + + let(:expected) { super() + <<-EOS.chomp } + +$0: #{path.to_s.inspect} +__FILE__: #{path.to_s.inspect} + EOS + + it_behaves_like "it runs" + + context "when the path is relative" do + let(:path) { super().relative_path_from(bundled_app) } + + if LessThanProc.with(RUBY_VERSION).call("1.9") + pending "relative paths have ./ __FILE__" + else + it_behaves_like "it runs" + end + end + + context "when the path is relative with a leading ./" do + let(:path) { Pathname.new("./#{super().relative_path_from(Pathname.pwd)}") } + + if LessThanProc.with(RUBY_VERSION).call("< 1.9") + pending "relative paths with ./ have absolute __FILE__" + else + it_behaves_like "it runs" + end + end + end + + context "signals being trapped by bundler" do + let(:executable) { strip_whitespace <<-RUBY } + #{shebang} + begin + Thread.new do + puts 'Started' # For process sync + STDOUT.flush + sleep 1 # ignore quality_spec + raise "Didn't receive INT at all" + end.join + rescue Interrupt + puts "foo" + end + RUBY + + it "receives the signal" do + skip "popen3 doesn't provide a way to get pid " unless RUBY_VERSION >= "1.9.3" + + bundle("exec #{path}") do |_, o, thr| + o.gets # Consumes 'Started' and ensures that thread has started + Process.kill("INT", thr.pid) + end + + expect(out).to eq("foo") + end + end + end + + context "nested bundle exec", :ruby_repo do + let(:system_gems_to_install) { super() << :bundler } + + context "with shared gems disabled" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + bundle :install, :system_bundler => true, :path => "vendor/bundler" + end + + it "overrides disable_shared_gems so bundler can be found" do + file = bundled_app("file_that_bundle_execs.rb") + create_file(file, <<-RB) + #!#{Gem.ruby} + puts `bundle exec echo foo` + RB + file.chmod(0o777) + bundle! "exec #{file}", :system_bundler => true + expect(out).to eq("foo") + end + end + + context "with a system gem that shadows a default gem" do + let(:openssl_version) { "99.9.9" } + let(:expected) { ruby "gem 'openssl', '< 999999'; require 'openssl'; puts OpenSSL::VERSION", :artifice => nil } + + it "only leaves the default gem in the stdlib available" do + skip "openssl isn't a default gem" if expected.empty? + + install_gemfile! "" # must happen before installing the broken system gem + + build_repo4 do + build_gem "openssl", openssl_version do |s| + s.write("lib/openssl.rb", <<-RB) + raise "custom openssl should not be loaded, it's not in the gemfile!" + RB + end + end + + system_gems(:bundler, "openssl-#{openssl_version}", :gem_repo => gem_repo4) + + file = bundled_app("require_openssl.rb") + create_file(file, <<-RB) + #!/usr/bin/env ruby + require "openssl" + puts OpenSSL::VERSION + warn Gem.loaded_specs.values.map(&:full_name) + RB + file.chmod(0o777) + + aggregate_failures do + expect(bundle!("exec #{file}", :system_bundler => true, :artifice => nil)).to eq(expected) + expect(bundle!("exec bundle exec #{file}", :system_bundler => true, :artifice => nil)).to eq(expected) + expect(bundle!("exec ruby #{file}", :system_bundler => true, :artifice => nil)).to eq(expected) + expect(run!(file.read, :no_lib => true, :artifice => nil)).to eq(expected) + end + + # sanity check that we get the newer, custom version without bundler + sys_exec("#{Gem.ruby} #{file}") + expect(err).to include("custom openssl should not be loaded") + end + end + end +end diff --git a/spec/bundler/commands/help_spec.rb b/spec/bundler/commands/help_spec.rb new file mode 100644 index 0000000000..6faeed058e --- /dev/null +++ b/spec/bundler/commands/help_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle help" do + # Rubygems 1.4+ no longer load gem plugins so this test is no longer needed + it "complains if older versions of bundler are installed", :rubygems => "< 1.4" do + system_gems "bundler-0.8.1" + + bundle "help" + expect(err).to include("older than 0.9") + expect(err).to include("running `gem cleanup bundler`.") + end + + it "uses mann when available", :ruby_repo do + with_fake_man do + bundle "help gemfile" + end + expect(out).to eq(%(["#{root}/man/gemfile.5"])) + end + + it "prefixes bundle commands with bundle- when finding the groff files", :ruby_repo do + with_fake_man do + bundle "help install" + end + expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + end + + it "simply outputs the txt file when there is no man on the path", :ruby_repo do + with_path_as("") do + bundle "help install" + end + expect(out).to match(/BUNDLE-INSTALL/) + end + + it "still outputs the old help for commands that do not have man pages yet" do + bundle "help version" + expect(out).to include("Prints the bundler's version information") + end + + it "looks for a binary and executes it with --help option if it's named bundler-<task>" do + File.open(tmp("bundler-testtasks"), "w", 0o755) do |f| + f.puts "#!/usr/bin/env ruby\nputs ARGV.join(' ')\n" + end + + with_path_added(tmp) do + bundle "help testtasks" + end + + expect(exitstatus).to be_zero if exitstatus + expect(out).to eq("--help") + end + + it "is called when the --help flag is used after the command", :ruby_repo do + with_fake_man do + bundle "install --help" + end + expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + end + + it "is called when the --help flag is used before the command", :ruby_repo do + with_fake_man do + bundle "--help install" + end + expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + end + + it "is called when the -h flag is used before the command", :ruby_repo do + with_fake_man do + bundle "-h install" + end + expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + end + + it "is called when the -h flag is used after the command", :ruby_repo do + with_fake_man do + bundle "install -h" + end + expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + end + + it "has helpful output when using --help flag for a non-existent command" do + with_fake_man do + bundle "instill -h" + end + expect(out).to include('Could not find command "instill".') + end + + it "is called when only using the --help flag", :ruby_repo do + with_fake_man do + bundle "--help" + end + expect(out).to eq(%(["#{root}/man/bundle.1"])) + + with_fake_man do + bundle "-h" + end + expect(out).to eq(%(["#{root}/man/bundle.1"])) + end +end diff --git a/spec/bundler/commands/info_spec.rb b/spec/bundler/commands/info_spec.rb new file mode 100644 index 0000000000..cdfea983dc --- /dev/null +++ b/spec/bundler/commands/info_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle info" do + context "info from specific gem in gemfile" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + end + + it "prints information about the current gem" do + bundle "info rails" + expect(out).to include "* rails (2.3.2) +\tSummary: This is just a fake gem for testing +\tHomepage: http://example.com" + expect(out).to match(%r{Path\: .*\/rails\-2\.3\.2}) + end + + context "given a gem that is not installed" do + it "prints missing gem error" do + bundle "info foo" + expect(out).to eq "Could not find gem 'foo'." + end + end + + context "given a default gem shippped in ruby", :ruby_repo do + it "prints information about the default gem", :if => (RUBY_VERSION >= "2.0") do + bundle "info rdoc" + expect(out).to include("* rdoc") + expect(out).to include("Default Gem: yes") + end + end + + context "when gem does not have homepage" do + before do + build_repo1 do + build_gem "rails", "2.3.2" do |s| + s.executables = "rails" + s.summary = "Just another test gem" + end + end + end + + it "excludes the homepage field from the output" do + expect(out).to_not include("Homepage:") + end + end + + context "given --path option" do + it "prints the path to the gem" do + bundle "info rails" + expect(out).to match(%r{.*\/rails\-2\.3\.2}) + end + end + end +end diff --git a/spec/bundler/commands/init_spec.rb b/spec/bundler/commands/init_spec.rb new file mode 100644 index 0000000000..6ab7e25cc3 --- /dev/null +++ b/spec/bundler/commands/init_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle init" do + it "generates a Gemfile" do + bundle :init + expect(bundled_app("Gemfile")).to exist + end + + context "when a Gemfile already exists" do + before do + gemfile <<-G + gem "rails" + G + end + + it "does not change existing Gemfiles" do + expect { bundle :init }.not_to change { File.read(bundled_app("Gemfile")) } + end + + it "notifies the user that an existing Gemfile already exists" do + bundle :init + expect(out).to include("Gemfile already exists") + end + end + + context "given --gemspec option" do + let(:spec_file) { tmp.join("test.gemspec") } + + it "should generate from an existing gemspec" do + File.open(spec_file, "w") do |file| + file << <<-S + Gem::Specification.new do |s| + s.name = 'test' + s.add_dependency 'rack', '= 1.0.1' + s.add_development_dependency 'rspec', '1.2' + end + S + end + + bundle :init, :gemspec => spec_file + + gemfile = bundled_app("Gemfile").read + expect(gemfile).to match(%r{source 'https://rubygems.org'}) + expect(gemfile.scan(/gem "rack", "= 1.0.1"/).size).to eq(1) + expect(gemfile.scan(/gem "rspec", "= 1.2"/).size).to eq(1) + expect(gemfile.scan(/group :development/).size).to eq(1) + end + + context "when gemspec file is invalid" do + it "notifies the user that specification is invalid" do + File.open(spec_file, "w") do |file| + file << <<-S + Gem::Specification.new do |s| + s.name = 'test' + s.invalid_method_name + end + S + end + + bundle :init, :gemspec => spec_file + expect(out).to include("There was an error while loading `test.gemspec`") + end + end + end +end diff --git a/spec/bundler/commands/inject_spec.rb b/spec/bundler/commands/inject_spec.rb new file mode 100644 index 0000000000..dd0f1348cc --- /dev/null +++ b/spec/bundler/commands/inject_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle inject" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + context "without a lockfile" do + it "locks with the injected gems" do + expect(bundled_app("Gemfile.lock")).not_to exist + bundle "inject 'rack-obama' '> 0'" + expect(bundled_app("Gemfile.lock").read).to match(/rack-obama/) + end + end + + context "with a lockfile" do + before do + bundle "install" + end + + it "adds the injected gems to the Gemfile" do + expect(bundled_app("Gemfile").read).not_to match(/rack-obama/) + bundle "inject 'rack-obama' '> 0'" + expect(bundled_app("Gemfile").read).to match(/rack-obama/) + end + + it "locks with the injected gems" do + expect(bundled_app("Gemfile.lock").read).not_to match(/rack-obama/) + bundle "inject 'rack-obama' '> 0'" + expect(bundled_app("Gemfile.lock").read).to match(/rack-obama/) + end + end + + context "with injected gems already in the Gemfile" do + it "doesn't add existing gems" do + bundle "inject 'rack' '> 0'" + expect(out).to match(/cannot specify the same gem twice/i) + end + end + + context "incorrect arguments" do + it "fails when more than 2 arguments are passed" do + bundle "inject gem_name 1 v" + expect(out).to eq(<<-E.strip) +ERROR: "bundle inject" was called with arguments ["gem_name", "1", "v"] +Usage: "bundle inject GEM VERSION" + E + end + end + + context "with source option" do + it "add gem with source option in gemfile" do + bundle "inject 'foo' '>0' --source file://#{gem_repo1}" + gemfile = bundled_app("Gemfile").read + str = "gem \"foo\", \"> 0\", :source => \"file://#{gem_repo1}\"" + expect(gemfile).to include str + end + end + + context "with group option" do + it "add gem with group option in gemfile" do + bundle "inject 'rack-obama' '>0' --group=development" + gemfile = bundled_app("Gemfile").read + str = "gem \"rack-obama\", \"> 0\", :group => [:development]" + expect(gemfile).to include str + end + + it "add gem with multiple groups in gemfile" do + bundle "inject 'rack-obama' '>0' --group=development,test" + gemfile = bundled_app("Gemfile").read + str = "gem \"rack-obama\", \"> 0\", :groups => [:development, :test]" + expect(gemfile).to include str + end + end + + context "when frozen" do + before do + bundle "install" + bundle "config --local frozen 1" + end + + it "injects anyway" do + bundle "inject 'rack-obama' '> 0'" + expect(bundled_app("Gemfile").read).to match(/rack-obama/) + end + + it "locks with the injected gems" do + expect(bundled_app("Gemfile.lock").read).not_to match(/rack-obama/) + bundle "inject 'rack-obama' '> 0'" + expect(bundled_app("Gemfile.lock").read).to match(/rack-obama/) + end + + it "restores frozen afterwards" do + bundle "inject 'rack-obama' '> 0'" + config = YAML.load(bundled_app(".bundle/config").read) + expect(config["BUNDLE_FROZEN"]).to eq("1") + end + + it "doesn't allow Gemfile changes" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack-obama" + G + bundle "inject 'rack' '> 0'" + expect(out).to match(/trying to install in deployment mode after changing/) + + expect(bundled_app("Gemfile.lock").read).not_to match(/rack-obama/) + end + end +end diff --git a/spec/bundler/commands/install_spec.rb b/spec/bundler/commands/install_spec.rb new file mode 100644 index 0000000000..2d67a39f1e --- /dev/null +++ b/spec/bundler/commands/install_spec.rb @@ -0,0 +1,513 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install with gem sources" do + describe "the simple case" do + it "prints output and returns if no dependencies are specified" do + gemfile <<-G + source "file://#{gem_repo1}" + G + + bundle :install + expect(out).to match(/no dependencies/) + end + + it "does not make a lockfile if the install fails" do + install_gemfile <<-G + raise StandardError, "FAIL" + G + + expect(err).to lack_errors + expect(out).to match(/StandardError, "FAIL"/) + expect(bundled_app("Gemfile.lock")).not_to exist + end + + it "creates a Gemfile.lock" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + expect(bundled_app("Gemfile.lock")).to exist + end + + it "does not create ./.bundle by default" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install # can't use install_gemfile since it sets retry + expect(bundled_app(".bundle")).not_to exist + end + + it "creates lock files based on the Gemfile name" do + gemfile bundled_app("OmgFile"), <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0" + G + + bundle "install --gemfile OmgFile" + + expect(bundled_app("OmgFile.lock")).to exist + end + + it "doesn't delete the lockfile if one already exists" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + lockfile = File.read(bundled_app("Gemfile.lock")) + + install_gemfile <<-G + raise StandardError, "FAIL" + G + + expect(File.read(bundled_app("Gemfile.lock"))).to eq(lockfile) + end + + it "does not touch the lockfile if nothing changed" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + expect { run "1" }.not_to change { File.mtime(bundled_app("Gemfile.lock")) } + end + + it "fetches gems" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + expect(default_bundle_path("gems/rack-1.0.0")).to exist + expect(the_bundle).to include_gems("rack 1.0.0") + end + + it "fetches gems when multiple versions are specified" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack', "> 0.9", "< 1.0" + G + + expect(default_bundle_path("gems/rack-0.9.1")).to exist + expect(the_bundle).to include_gems("rack 0.9.1") + end + + it "fetches gems when multiple versions are specified take 2" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack', "< 1.0", "> 0.9" + G + + expect(default_bundle_path("gems/rack-0.9.1")).to exist + expect(the_bundle).to include_gems("rack 0.9.1") + end + + it "raises an appropriate error when gems are specified using symbols" do + install_gemfile(<<-G) + source "file://#{gem_repo1}" + gem :rack + G + expect(exitstatus).to eq(4) if exitstatus + end + + it "pulls in dependencies" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + expect(the_bundle).to include_gems "actionpack 2.3.2", "rails 2.3.2" + end + + it "does the right version" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "0.9.1" + G + + expect(the_bundle).to include_gems "rack 0.9.1" + end + + it "does not install the development dependency" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "with_development_dependency" + G + + expect(the_bundle).to include_gems("with_development_dependency 1.0.0"). + and not_include_gems("activesupport 2.3.5") + end + + it "resolves correctly" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "activemerchant" + gem "rails" + G + + expect(the_bundle).to include_gems "activemerchant 1.0", "activesupport 2.3.2", "actionpack 2.3.2" + end + + it "activates gem correctly according to the resolved gems" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport", "2.3.5" + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "activemerchant" + gem "rails" + G + + expect(the_bundle).to include_gems "activemerchant 1.0", "activesupport 2.3.2", "actionpack 2.3.2" + end + + it "does not reinstall any gem that is already available locally" do + system_gems "activesupport-2.3.2" + + build_repo2 do + build_gem "activesupport", "2.3.2" do |s| + s.write "lib/activesupport.rb", "ACTIVESUPPORT = 'fail'" + end + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activerecord", "2.3.2" + G + + expect(the_bundle).to include_gems "activesupport 2.3.2" + end + + it "works when the gemfile specifies gems that only exist in the system" do + build_gem "foo", :to_system => true + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "foo" + G + + expect(the_bundle).to include_gems "rack 1.0.0", "foo 1.0.0" + end + + it "prioritizes local gems over remote gems" do + build_gem "rack", "1.0.0", :to_system => true do |s| + s.add_dependency "activesupport", "2.3.5" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.5" + end + + describe "with a gem that installs multiple platforms" do + it "installs gems for the local platform as first choice" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "platform_specific" + G + + run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" + expect(out).to eq("1.0.0 #{Bundler.local_platform}") + end + + it "falls back on plain ruby" do + simulate_platform "foo-bar-baz" + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "platform_specific" + G + + run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" + expect(out).to eq("1.0.0 RUBY") + end + + it "installs gems for java" do + simulate_platform "java" + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "platform_specific" + G + + run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" + expect(out).to eq("1.0.0 JAVA") + end + + it "installs gems for windows" do + simulate_platform mswin + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "platform_specific" + G + + run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" + expect(out).to eq("1.0.0 MSWIN") + end + end + + describe "doing bundle install foo" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + it "works" do + bundle "install --path vendor" + expect(the_bundle).to include_gems "rack 1.0" + end + + it "allows running bundle install --system without deleting foo" do + bundle "install --path vendor" + bundle "install --system" + FileUtils.rm_rf(bundled_app("vendor")) + expect(the_bundle).to include_gems "rack 1.0" + end + + it "allows running bundle install --system after deleting foo" do + bundle "install --path vendor" + FileUtils.rm_rf(bundled_app("vendor")) + bundle "install --system" + expect(the_bundle).to include_gems "rack 1.0" + end + end + + it "finds gems in multiple sources" do + build_repo2 + update_repo2 + + install_gemfile <<-G + source "file://#{gem_repo1}" + source "file://#{gem_repo2}" + + gem "activesupport", "1.2.3" + gem "rack", "1.2" + G + + expect(the_bundle).to include_gems "rack 1.2", "activesupport 1.2.3" + end + + it "gives a useful error if no sources are set" do + install_gemfile <<-G + gem "rack" + G + + bundle :install + expect(out).to include("Your Gemfile has no gem server sources") + end + + it "creates a Gemfile.lock on a blank Gemfile" do + install_gemfile <<-G + G + + expect(File.exist?(bundled_app("Gemfile.lock"))).to eq(true) + end + + it "gracefully handles error when rubygems server is unavailable" do + install_gemfile <<-G, :artifice => nil + source "file://#{gem_repo1}" + source "http://localhost:9384" + + gem 'foo' + G + + bundle :install, :artifice => nil + expect(out).to include("Could not fetch specs from http://localhost:9384/") + expect(out).not_to include("file://") + end + + it "fails gracefully when downloading an invalid specification from the full index", :rubygems => "2.5" do + build_repo2 do + build_gem "ajp-rails", "0.0.0", :gemspec => false, :skip_validation => true do |s| + bad_deps = [["ruby-ajp", ">= 0.2.0"], ["rails", ">= 0.14"]] + s. + instance_variable_get(:@spec). + instance_variable_set(:@dependencies, bad_deps) + + raise "failed to set bad deps" unless s.dependencies == bad_deps + end + build_gem "ruby-ajp", "1.0.0" + end + + install_gemfile <<-G, :full_index => true + source "file://#{gem_repo2}" + + gem "ajp-rails", "0.0.0" + G + + expect(out).not_to match(/Error Report/i) + expect(err).not_to match(/Error Report/i) + expect(out).to include("An error occurred while installing ajp-rails (0.0.0), and Bundler cannot continue."). + and include("Make sure that `gem install ajp-rails -v '0.0.0'` succeeds before bundling.") + end + + it "doesn't blow up when the local .bundle/config is empty" do + FileUtils.mkdir_p(bundled_app(".bundle")) + FileUtils.touch(bundled_app(".bundle/config")) + + install_gemfile(<<-G) + source "file://#{gem_repo1}" + + gem 'foo' + G + expect(exitstatus).to eq(0) if exitstatus + end + + it "doesn't blow up when the global .bundle/config is empty" do + FileUtils.mkdir_p("#{Bundler.rubygems.user_home}/.bundle") + FileUtils.touch("#{Bundler.rubygems.user_home}/.bundle/config") + + install_gemfile(<<-G) + source "file://#{gem_repo1}" + + gem 'foo' + G + expect(exitstatus).to eq(0) if exitstatus + end + end + + describe "Ruby version in Gemfile.lock" do + include Bundler::GemHelpers + + context "and using an unsupported Ruby version" do + it "prints an error" do + install_gemfile <<-G + ::RUBY_VERSION = '1.8.7' + ruby '~> 2.1' + G + expect(out).to include("Your Ruby version is 1.8.7, but your Gemfile specified ~> 2.1") + end + end + + context "and using a supported Ruby version" do + before do + install_gemfile <<-G + ::RUBY_VERSION = '2.1.3' + ::RUBY_PATCHLEVEL = 100 + ruby '~> 2.1.0' + G + end + + it "writes current Ruby version to Gemfile.lock" do + lockfile_should_be <<-L + GEM + specs: + + PLATFORMS + ruby + + DEPENDENCIES + + RUBY VERSION + ruby 2.1.3p100 + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "updates Gemfile.lock with updated incompatible ruby version" do + install_gemfile <<-G + ::RUBY_VERSION = '2.2.3' + ::RUBY_PATCHLEVEL = 100 + ruby '~> 2.2.0' + G + + lockfile_should_be <<-L + GEM + specs: + + PLATFORMS + ruby + + DEPENDENCIES + + RUBY VERSION + ruby 2.2.3p100 + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + end + + describe "when Bundler root contains regex chars" do + before do + root_dir = tmp("foo[]bar") + + FileUtils.mkdir_p(root_dir) + in_app_root_custom(root_dir) + end + + it "doesn't blow up" do + build_lib "foo" + gemfile = <<-G + gem 'foo', :path => "#{lib_path("foo-1.0")}" + G + File.open("Gemfile", "w") do |file| + file.puts gemfile + end + + bundle :install + + expect(exitstatus).to eq(0) if exitstatus + end + end + + describe "when requesting a quiet install via --quiet" do + it "should be quiet" do + gemfile <<-G + gem 'rack' + G + + bundle :install, :quiet => true + expect(out).to include("Could not find gem 'rack'") + expect(out).to_not include("Your Gemfile has no gem server sources") + end + end + + describe "when bundle path does not have write access" do + before do + FileUtils.mkdir_p(bundled_app("vendor")) + gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + end + + it "should display a proper message to explain the problem" do + FileUtils.chmod(0o500, bundled_app("vendor")) + + bundle :install, :path => "vendor" + expect(out).to include(bundled_app("vendor").to_s) + expect(out).to include("grant write permissions") + end + end + + describe "when bundle install is executed with unencoded authentication" do + before do + gemfile <<-G + source 'https://rubygems.org/' + gem 'bundler' + G + end + + it "should display a helpful messag explaining how to fix it" do + bundle :install, :env => { "BUNDLE_RUBYGEMS__ORG" => "user:pass{word" } + expect(exitstatus).to eq(17) if exitstatus + expect(out).to eq("Please CGI escape your usernames and passwords before " \ + "setting them for authentication.") + end + end +end diff --git a/spec/bundler/commands/issue_spec.rb b/spec/bundler/commands/issue_spec.rb new file mode 100644 index 0000000000..056ef0f300 --- /dev/null +++ b/spec/bundler/commands/issue_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle issue" do + it "exits with a message" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + bundle "issue" + expect(out).to include "Did you find an issue with Bundler?" + expect(out).to include "## Environment" + expect(out).to include "## Gemfile" + expect(out).to include "## Bundle Doctor" + end +end diff --git a/spec/bundler/commands/licenses_spec.rb b/spec/bundler/commands/licenses_spec.rb new file mode 100644 index 0000000000..0ee1a46945 --- /dev/null +++ b/spec/bundler/commands/licenses_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle licenses" do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + gem "with_license" + G + end + + it "prints license information for all gems in the bundle" do + bundle "licenses" + + expect(out).to include("bundler: Unknown") + expect(out).to include("with_license: MIT") + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + gem "with_license" + gem "foo" + G + + bundle "config auto_install 1" + bundle :licenses + expect(out).to include("Installing foo 1.0") + end +end diff --git a/spec/bundler/commands/lock_spec.rb b/spec/bundler/commands/lock_spec.rb new file mode 100644 index 0000000000..5c15b6a7f6 --- /dev/null +++ b/spec/bundler/commands/lock_spec.rb @@ -0,0 +1,315 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle lock" do + def strip_lockfile(lockfile) + strip_whitespace(lockfile).sub(/\n\Z/, "") + end + + def read_lockfile(file = "Gemfile.lock") + strip_lockfile bundled_app(file).read + end + + let(:repo) { gem_repo1 } + + before :each do + gemfile <<-G + source "file://#{repo}" + gem "rails" + gem "with_license" + gem "foo" + G + + @lockfile = strip_lockfile <<-L + GEM + remote: file:#{repo}/ + specs: + actionmailer (2.3.2) + activesupport (= 2.3.2) + actionpack (2.3.2) + activesupport (= 2.3.2) + activerecord (2.3.2) + activesupport (= 2.3.2) + activeresource (2.3.2) + activesupport (= 2.3.2) + activesupport (2.3.2) + foo (1.0) + rails (2.3.2) + actionmailer (= 2.3.2) + actionpack (= 2.3.2) + activerecord (= 2.3.2) + activeresource (= 2.3.2) + rake (= 10.0.2) + rake (10.0.2) + with_license (1.0) + + PLATFORMS + #{local} + + DEPENDENCIES + foo + rails + with_license + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "prints a lockfile when there is no existing lockfile with --print" do + bundle "lock --print" + + expect(out).to include(@lockfile) + end + + it "prints a lockfile when there is an existing lockfile with --print" do + lockfile @lockfile + + bundle "lock --print" + + expect(out).to eq(@lockfile) + end + + it "writes a lockfile when there is no existing lockfile" do + bundle "lock" + + expect(read_lockfile).to eq(@lockfile) + end + + it "writes a lockfile when there is an outdated lockfile using --update" do + lockfile @lockfile.gsub("2.3.2", "2.3.1") + + bundle! "lock --update" + + expect(read_lockfile).to eq(@lockfile) + end + + it "does not fetch remote specs when using the --local option" do + bundle "lock --update --local" + + expect(out).to include("sources listed in your Gemfile") + end + + it "writes to a custom location using --lockfile" do + bundle "lock --lockfile=lock" + + expect(out).to match(/Writing lockfile to.+lock/) + expect(read_lockfile "lock").to eq(@lockfile) + expect { read_lockfile }.to raise_error(Errno::ENOENT) + end + + it "update specific gems using --update" do + lockfile @lockfile.gsub("2.3.2", "2.3.1").gsub("10.0.2", "10.0.1") + + bundle "lock --update rails rake" + + expect(read_lockfile).to eq(@lockfile) + end + + it "errors when updating a missing specific gems using --update" do + lockfile @lockfile + + bundle "lock --update blahblah" + expect(out).to eq("Could not find gem 'blahblah'.") + + expect(read_lockfile).to eq(@lockfile) + end + + # see update_spec for more coverage on same options. logic is shared so it's not necessary + # to repeat coverage here. + context "conservative updates" do + before do + build_repo4 do + build_gem "foo", %w(1.4.3 1.4.4) do |s| + s.add_dependency "bar", "~> 2.0" + end + build_gem "foo", %w(1.4.5 1.5.0) do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "foo", %w(1.5.1) do |s| + s.add_dependency "bar", "~> 3.0" + end + build_gem "bar", %w(2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0) + build_gem "qux", %w(1.0.0 1.0.1 1.1.0 2.0.0) + end + + # establish a lockfile set to 1.4.3 + install_gemfile <<-G + source "file://#{gem_repo4}" + gem 'foo', '1.4.3' + gem 'bar', '2.0.3' + gem 'qux', '1.0.0' + G + + # remove 1.4.3 requirement and bar altogether + # to setup update specs below + gemfile <<-G + source "file://#{gem_repo4}" + gem 'foo' + gem 'qux' + G + end + + it "single gem updates dependent gem to minor" do + bundle "lock --update foo --patch" + + expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w(foo-1.4.5 bar-2.1.1 qux-1.0.0).sort) + end + + it "minor preferred with strict" do + bundle "lock --update --minor --strict" + + expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w(foo-1.5.0 bar-2.1.1 qux-1.1.0).sort) + end + end + + it "supports adding new platforms" do + bundle! "lock --add-platform java x86-mingw32" + + lockfile = Bundler::LockfileParser.new(read_lockfile) + expect(lockfile.platforms).to eq([java, local, mingw]) + end + + it "supports adding the `ruby` platform" do + bundle! "lock --add-platform ruby" + lockfile = Bundler::LockfileParser.new(read_lockfile) + expect(lockfile.platforms).to eq([local, "ruby"].uniq) + end + + it "warns when adding an unknown platform" do + bundle "lock --add-platform foobarbaz" + expect(out).to include("The platform `foobarbaz` is unknown to RubyGems and adding it will likely lead to resolution errors") + end + + it "allows removing platforms" do + bundle! "lock --add-platform java x86-mingw32" + + lockfile = Bundler::LockfileParser.new(read_lockfile) + expect(lockfile.platforms).to eq([java, local, mingw]) + + bundle! "lock --remove-platform java" + + lockfile = Bundler::LockfileParser.new(read_lockfile) + expect(lockfile.platforms).to eq([local, mingw]) + end + + it "errors when removing all platforms" do + bundle "lock --remove-platform #{local}" + expect(out).to include("Removing all platforms from the bundle is not allowed") + end + + # from https://github.com/bundler/bundler/issues/4896 + it "properly adds platforms when platform requirements come from different dependencies" do + build_repo4 do + build_gem "ffi", "1.9.14" + build_gem "ffi", "1.9.14" do |s| + s.platform = mingw + end + + build_gem "gssapi", "0.1" + build_gem "gssapi", "0.2" + build_gem "gssapi", "0.3" + build_gem "gssapi", "1.2.0" do |s| + s.add_dependency "ffi", ">= 1.0.1" + end + + build_gem "mixlib-shellout", "2.2.6" + build_gem "mixlib-shellout", "2.2.6" do |s| + s.platform = "universal-mingw32" + s.add_dependency "win32-process", "~> 0.8.2" + end + + # we need all these versions to get the sorting the same as it would be + # pulling from rubygems.org + %w(0.8.3 0.8.2 0.8.1 0.8.0).each do |v| + build_gem "win32-process", v do |s| + s.add_dependency "ffi", ">= 1.0.0" + end + end + end + + gemfile <<-G + source "file:#{gem_repo4}" + + gem "mixlib-shellout" + gem "gssapi" + G + + simulate_platform(mingw) { bundle! :lock } + + expect(the_bundle.lockfile).to read_as(strip_whitespace(<<-G)) + GEM + remote: file:#{gem_repo4}/ + specs: + ffi (1.9.14-x86-mingw32) + gssapi (1.2.0) + ffi (>= 1.0.1) + mixlib-shellout (2.2.6-universal-mingw32) + win32-process (~> 0.8.2) + win32-process (0.8.3) + ffi (>= 1.0.0) + + PLATFORMS + x86-mingw32 + + DEPENDENCIES + gssapi + mixlib-shellout + + BUNDLED WITH + #{Bundler::VERSION} + G + + simulate_platform(rb) { bundle! :lock } + + expect(the_bundle.lockfile).to read_as(strip_whitespace(<<-G)) + GEM + remote: file:#{gem_repo4}/ + specs: + ffi (1.9.14) + ffi (1.9.14-x86-mingw32) + gssapi (1.2.0) + ffi (>= 1.0.1) + mixlib-shellout (2.2.6) + mixlib-shellout (2.2.6-universal-mingw32) + win32-process (~> 0.8.2) + win32-process (0.8.3) + ffi (>= 1.0.0) + + PLATFORMS + ruby + x86-mingw32 + + DEPENDENCIES + gssapi + mixlib-shellout + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + context "when an update is available" do + let(:repo) { gem_repo2 } + + before do + lockfile(@lockfile) + build_repo2 do + build_gem "foo", "2.0" + end + end + + it "does not implicitly update" do + bundle! "lock" + + expect(read_lockfile).to eq(@lockfile) + end + + it "accounts for changes in the gemfile" do + gemfile gemfile.gsub('"foo"', '"foo", "2.0"') + bundle! "lock" + + expect(read_lockfile).to eq(@lockfile.sub("foo (1.0)", "foo (2.0)").sub(/foo$/, "foo (= 2.0)")) + end + end +end diff --git a/spec/bundler/commands/newgem_spec.rb b/spec/bundler/commands/newgem_spec.rb new file mode 100644 index 0000000000..e9c19005eb --- /dev/null +++ b/spec/bundler/commands/newgem_spec.rb @@ -0,0 +1,909 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle gem" do + def reset! + super + global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false" + end + + def remove_push_guard(gem_name) + # Remove exception that prevents public pushes on older RubyGems versions + if Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.0") + path = "#{gem_name}/#{gem_name}.gemspec" + content = File.read(path).sub(/raise "RubyGems 2\.0 or newer.*/, "") + File.open(path, "w") {|f| f.write(content) } + end + end + + def execute_bundle_gem(gem_name, flag = "", to_remove_push_guard = true) + bundle! "gem #{gem_name} #{flag}" + remove_push_guard(gem_name) if to_remove_push_guard + # reset gemspec cache for each test because of commit 3d4163a + Bundler.clear_gemspec_cache + end + + def gem_skeleton_assertions(gem_name) + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to exist + expect(bundled_app("#{gem_name}/README.md")).to exist + expect(bundled_app("#{gem_name}/Gemfile")).to exist + expect(bundled_app("#{gem_name}/Rakefile")).to exist + expect(bundled_app("#{gem_name}/lib/test/gem.rb")).to exist + expect(bundled_app("#{gem_name}/lib/test/gem/version.rb")).to exist + end + + before do + git_config_content = <<-EOF + [user] + name = "Bundler User" + email = user@example.com + [github] + user = bundleuser + EOF + @git_config_location = ENV["GIT_CONFIG"] + path = "#{File.expand_path(tmp, File.dirname(__FILE__))}/test_git_config.txt" + File.open(path, "w") {|f| f.write(git_config_content) } + ENV["GIT_CONFIG"] = path + end + + after do + FileUtils.rm(ENV["GIT_CONFIG"]) if File.exist?(ENV["GIT_CONFIG"]) + ENV["GIT_CONFIG"] = @git_config_location + end + + shared_examples_for "git config is present" do + context "git config user.{name,email} present" do + it "sets gemspec author to git user.name if available" do + expect(generated_gem.gemspec.authors.first).to eq("Bundler User") + end + + it "sets gemspec email to git user.email if available" do + expect(generated_gem.gemspec.email.first).to eq("user@example.com") + end + end + end + + shared_examples_for "git config is absent" do + it "sets gemspec author to default message if git user.name is not set or empty" do + expect(generated_gem.gemspec.authors.first).to eq("TODO: Write your name") + end + + it "sets gemspec email to default message if git user.email is not set or empty" do + expect(generated_gem.gemspec.email.first).to eq("TODO: Write your email address") + end + end + + shared_examples_for "--mit flag" do + before do + execute_bundle_gem(gem_name, "--mit") + end + it "generates a gem skeleton with MIT license" do + gem_skeleton_assertions(gem_name) + expect(bundled_app("test-gem/LICENSE.txt")).to exist + skel = Bundler::GemHelper.new(bundled_app(gem_name).to_s) + expect(skel.gemspec.license).to eq("MIT") + end + end + + shared_examples_for "--no-mit flag" do + before do + execute_bundle_gem(gem_name, "--no-mit") + end + it "generates a gem skeleton without MIT license" do + gem_skeleton_assertions(gem_name) + expect(bundled_app("test-gem/LICENSE.txt")).to_not exist + end + end + + shared_examples_for "--coc flag" do + before do + execute_bundle_gem(gem_name, "--coc", false) + end + it "generates a gem skeleton with MIT license" do + gem_skeleton_assertions(gem_name) + expect(bundled_app("test-gem/CODE_OF_CONDUCT.md")).to exist + end + + describe "README additions" do + it "generates the README with a section for the Code of Conduct" do + expect(bundled_app("test-gem/README.md").read).to include("## Code of Conduct") + expect(bundled_app("test-gem/README.md").read).to include("https://github.com/bundleuser/#{gem_name}/blob/master/CODE_OF_CONDUCT.md") + end + end + end + + shared_examples_for "--no-coc flag" do + before do + execute_bundle_gem(gem_name, "--no-coc", false) + end + it "generates a gem skeleton without Code of Conduct" do + gem_skeleton_assertions(gem_name) + expect(bundled_app("test-gem/CODE_OF_CONDUCT.md")).to_not exist + end + + describe "README additions" do + it "generates the README without a section for the Code of Conduct" do + expect(bundled_app("test-gem/README.md").read).not_to include("## Code of Conduct") + expect(bundled_app("test-gem/README.md").read).not_to include("https://github.com/bundleuser/#{gem_name}/blob/master/CODE_OF_CONDUCT.md") + end + end + end + + context "README.md" do + let(:gem_name) { "test_gem" } + let(:generated_gem) { Bundler::GemHelper.new(bundled_app(gem_name).to_s) } + + context "git config github.user present" do + before do + execute_bundle_gem(gem_name) + end + + it "contribute URL set to git username" do + expect(bundled_app("test_gem/README.md").read).not_to include("[USERNAME]") + expect(bundled_app("test_gem/README.md").read).to include("github.com/bundleuser") + end + end + + context "git config github.user is absent" do + before do + sys_exec("git config --unset github.user") + reset! + in_app_root + bundle "gem #{gem_name}" + remove_push_guard(gem_name) + end + + it "contribute URL set to [USERNAME]" do + expect(bundled_app("test_gem/README.md").read).to include("[USERNAME]") + expect(bundled_app("test_gem/README.md").read).not_to include("github.com/bundleuser") + end + end + end + + it "creates a new git repository" do + in_app_root + bundle "gem test_gem" + expect(bundled_app("test_gem/.git")).to exist + end + + context "when git is not avaiable" do + let(:gem_name) { "test_gem" } + + # This spec cannot have `git` avaiable in the test env + before do + load_paths = [lib, spec] + load_path_str = "-I#{load_paths.join(File::PATH_SEPARATOR)}" + + sys_exec "PATH=\"\" #{Gem.ruby} #{load_path_str} #{bindir.join("bundle")} gem #{gem_name}" + end + + it "creates the gem without the need for git" do + expect(bundled_app("#{gem_name}/README.md")).to exist + end + + it "doesn't create a git repo" do + expect(bundled_app("#{gem_name}/.git")).to_not exist + end + + it "doesn't create a .gitignore file" do + expect(bundled_app("#{gem_name}/.gitignore")).to_not exist + end + end + + it "generates a valid gemspec" do + system_gems ["rake-10.0.2"] + + in_app_root + bundle "gem newgem --bin" + + process_file(bundled_app("newgem", "newgem.gemspec")) do |line| + # Simulate replacing TODOs with real values + case line + when /spec\.metadata\['allowed_push_host'\]/, /spec\.homepage/ + line.gsub(/\=.*$/, "= 'http://example.org'") + when /spec\.summary/ + line.gsub(/\=.*$/, "= %q{A short summary of my new gem.}") + when /spec\.description/ + line.gsub(/\=.*$/, "= %q{A longer description of my new gem.}") + # Remove exception that prevents public pushes on older RubyGems versions + when /raise "RubyGems 2.0 or newer/ + line.gsub(/.*/, "") if Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.0") + else + line + end + end + + Dir.chdir(bundled_app("newgem")) do + bundle "exec rake build" + end + + expect(exitstatus).to be_zero if exitstatus + expect(out).not_to include("ERROR") + expect(err).not_to include("ERROR") + end + + context "gem naming with relative paths" do + before do + reset! + in_app_root + end + + it "resolves ." do + create_temporary_dir("tmp") + + bundle "gem ." + + expect(bundled_app("tmp/lib/tmp.rb")).to exist + end + + it "resolves .." do + create_temporary_dir("temp/empty_dir") + + bundle "gem .." + + expect(bundled_app("temp/lib/temp.rb")).to exist + end + + it "resolves relative directory" do + create_temporary_dir("tmp/empty/tmp") + + bundle "gem ../../empty" + + expect(bundled_app("tmp/empty/lib/empty.rb")).to exist + end + + def create_temporary_dir(dir) + FileUtils.mkdir_p(dir) + Dir.chdir(dir) + end + end + + context "gem naming with underscore" do + let(:gem_name) { "test_gem" } + + before do + execute_bundle_gem(gem_name) + end + + let(:generated_gem) { Bundler::GemHelper.new(bundled_app(gem_name).to_s) } + + it "generates a gem skeleton" do + expect(bundled_app("test_gem/test_gem.gemspec")).to exist + expect(bundled_app("test_gem/Gemfile")).to exist + expect(bundled_app("test_gem/Rakefile")).to exist + expect(bundled_app("test_gem/lib/test_gem.rb")).to exist + expect(bundled_app("test_gem/lib/test_gem/version.rb")).to exist + expect(bundled_app("test_gem/.gitignore")).to exist + + expect(bundled_app("test_gem/bin/setup")).to exist + expect(bundled_app("test_gem/bin/console")).to exist + expect(bundled_app("test_gem/bin/setup")).to be_executable + expect(bundled_app("test_gem/bin/console")).to be_executable + end + + it "starts with version 0.1.0" do + expect(bundled_app("test_gem/lib/test_gem/version.rb").read).to match(/VERSION = "0.1.0"/) + end + + it "does not nest constants" do + expect(bundled_app("test_gem/lib/test_gem/version.rb").read).to match(/module TestGem/) + expect(bundled_app("test_gem/lib/test_gem.rb").read).to match(/module TestGem/) + end + + it_should_behave_like "git config is present" + + context "git config user.{name,email} is not set" do + before do + `git config --unset user.name` + `git config --unset user.email` + reset! + in_app_root + bundle "gem #{gem_name}" + remove_push_guard(gem_name) + end + + it_should_behave_like "git config is absent" + end + + it "sets gemspec metadata['allowed_push_host']", :rubygems => "2.0" do + expect(generated_gem.gemspec.metadata["allowed_push_host"]). + to match(/mygemserver\.com/) + end + + it "requires the version file" do + expect(bundled_app("test_gem/lib/test_gem.rb").read).to match(%r{require "test_gem/version"}) + end + + it "runs rake without problems" do + system_gems ["rake-10.0.2"] + + rakefile = strip_whitespace <<-RAKEFILE + task :default do + puts 'SUCCESS' + end + RAKEFILE + File.open(bundled_app("test_gem/Rakefile"), "w") do |file| + file.puts rakefile + end + + Dir.chdir(bundled_app(gem_name)) do + sys_exec(rake) + expect(out).to include("SUCCESS") + end + end + + context "--exe parameter set" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --exe" + end + + it "builds exe skeleton" do + expect(bundled_app("test_gem/exe/test_gem")).to exist + end + + it "requires 'test-gem'" do + expect(bundled_app("test_gem/exe/test_gem").read).to match(/require "test_gem"/) + end + end + + context "--bin parameter set" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --bin" + end + + it "builds exe skeleton" do + expect(bundled_app("test_gem/exe/test_gem")).to exist + end + + it "requires 'test-gem'" do + expect(bundled_app("test_gem/exe/test_gem").read).to match(/require "test_gem"/) + end + end + + context "no --test parameter" do + before do + reset! + in_app_root + bundle "gem #{gem_name}" + end + + it "doesn't create any spec/test file" do + expect(bundled_app("test_gem/.rspec")).to_not exist + expect(bundled_app("test_gem/spec/test_gem_spec.rb")).to_not exist + expect(bundled_app("test_gem/spec/spec_helper.rb")).to_not exist + expect(bundled_app("test_gem/test/test_test_gem.rb")).to_not exist + expect(bundled_app("test_gem/test/minitest_helper.rb")).to_not exist + end + end + + context "--test parameter set to rspec" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --test=rspec" + end + + it "builds spec skeleton" do + expect(bundled_app("test_gem/.rspec")).to exist + expect(bundled_app("test_gem/spec/test_gem_spec.rb")).to exist + expect(bundled_app("test_gem/spec/spec_helper.rb")).to exist + end + + it "depends on a specific version of rspec", :rubygems => ">= 1.8.1" do + remove_push_guard(gem_name) + rspec_dep = generated_gem.gemspec.development_dependencies.find {|d| d.name == "rspec" } + expect(rspec_dep).to be_specific + end + + it "requires 'test-gem'" do + expect(bundled_app("test_gem/spec/spec_helper.rb").read).to include(%(require "test_gem")) + end + + it "creates a default test which fails" do + expect(bundled_app("test_gem/spec/test_gem_spec.rb").read).to include("expect(false).to eq(true)") + end + end + + context "gem.test setting set to rspec" do + before do + reset! + in_app_root + bundle "config gem.test rspec" + bundle "gem #{gem_name}" + end + + it "builds spec skeleton" do + expect(bundled_app("test_gem/.rspec")).to exist + expect(bundled_app("test_gem/spec/test_gem_spec.rb")).to exist + expect(bundled_app("test_gem/spec/spec_helper.rb")).to exist + end + end + + context "gem.test setting set to rspec and --test is set to minitest" do + before do + reset! + in_app_root + bundle "config gem.test rspec" + bundle "gem #{gem_name} --test=minitest" + end + + it "builds spec skeleton" do + expect(bundled_app("test_gem/test/test_gem_test.rb")).to exist + expect(bundled_app("test_gem/test/test_helper.rb")).to exist + end + end + + context "--test parameter set to minitest" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --test=minitest" + end + + it "depends on a specific version of minitest", :rubygems => ">= 1.8.1" do + remove_push_guard(gem_name) + rspec_dep = generated_gem.gemspec.development_dependencies.find {|d| d.name == "minitest" } + expect(rspec_dep).to be_specific + end + + it "builds spec skeleton" do + expect(bundled_app("test_gem/test/test_gem_test.rb")).to exist + expect(bundled_app("test_gem/test/test_helper.rb")).to exist + end + + it "requires 'test-gem'" do + expect(bundled_app("test_gem/test/test_helper.rb").read).to include(%(require "test_gem")) + end + + it "requires 'minitest_helper'" do + expect(bundled_app("test_gem/test/test_gem_test.rb").read).to include(%(require "test_helper")) + end + + it "creates a default test which fails" do + expect(bundled_app("test_gem/test/test_gem_test.rb").read).to include("assert false") + end + end + + context "gem.test setting set to minitest" do + before do + reset! + in_app_root + bundle "config gem.test minitest" + bundle "gem #{gem_name}" + end + + it "creates a default rake task to run the test suite" do + rakefile = strip_whitespace <<-RAKEFILE + require "bundler/gem_tasks" + require "rake/testtask" + + Rake::TestTask.new(:test) do |t| + t.libs << "test" + t.libs << "lib" + t.test_files = FileList["test/**/*_test.rb"] + end + + task :default => :test + RAKEFILE + + expect(bundled_app("test_gem/Rakefile").read).to eq(rakefile) + end + end + + context "--test with no arguments" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --test" + end + + it "defaults to rspec" do + expect(bundled_app("test_gem/spec/spec_helper.rb")).to exist + expect(bundled_app("test_gem/test/minitest_helper.rb")).to_not exist + end + + it "creates a .travis.yml file to test the library against the current Ruby version on Travis CI" do + expect(bundled_app("test_gem/.travis.yml").read).to match(/- #{RUBY_VERSION}/) + end + end + + context "--edit option" do + it "opens the generated gemspec in the user's text editor" do + reset! + in_app_root + output = bundle "gem #{gem_name} --edit=echo" + gemspec_path = File.join(Dir.pwd, gem_name, "#{gem_name}.gemspec") + expect(output).to include("echo \"#{gemspec_path}\"") + end + end + end + + context "testing --mit and --coc options against bundle config settings" do + let(:gem_name) { "test-gem" } + + context "with mit option in bundle config settings set to true" do + before do + global_config "BUNDLE_GEM__MIT" => "true", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false" + end + after { reset! } + it_behaves_like "--mit flag" + it_behaves_like "--no-mit flag" + end + + context "with mit option in bundle config settings set to false" do + it_behaves_like "--mit flag" + it_behaves_like "--no-mit flag" + end + + context "with coc option in bundle config settings set to true" do + before do + global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "true" + end + after { reset! } + it_behaves_like "--coc flag" + it_behaves_like "--no-coc flag" + end + + context "with coc option in bundle config settings set to false" do + it_behaves_like "--coc flag" + it_behaves_like "--no-coc flag" + end + end + + context "gem naming with dashed" do + let(:gem_name) { "test-gem" } + + before do + execute_bundle_gem(gem_name) + end + + let(:generated_gem) { Bundler::GemHelper.new(bundled_app(gem_name).to_s) } + + it "generates a gem skeleton" do + expect(bundled_app("test-gem/test-gem.gemspec")).to exist + expect(bundled_app("test-gem/Gemfile")).to exist + expect(bundled_app("test-gem/Rakefile")).to exist + expect(bundled_app("test-gem/lib/test/gem.rb")).to exist + expect(bundled_app("test-gem/lib/test/gem/version.rb")).to exist + end + + it "starts with version 0.1.0" do + expect(bundled_app("test-gem/lib/test/gem/version.rb").read).to match(/VERSION = "0.1.0"/) + end + + it "nests constants so they work" do + expect(bundled_app("test-gem/lib/test/gem/version.rb").read).to match(/module Test\n module Gem/) + expect(bundled_app("test-gem/lib/test/gem.rb").read).to match(/module Test\n module Gem/) + end + + it_should_behave_like "git config is present" + + context "git config user.{name,email} is not set" do + before do + `git config --unset user.name` + `git config --unset user.email` + reset! + in_app_root + bundle "gem #{gem_name}" + remove_push_guard(gem_name) + end + + it_should_behave_like "git config is absent" + end + + it "requires the version file" do + expect(bundled_app("test-gem/lib/test/gem.rb").read).to match(%r{require "test/gem/version"}) + end + + it "runs rake without problems" do + system_gems ["rake-10.0.2"] + + rakefile = strip_whitespace <<-RAKEFILE + task :default do + puts 'SUCCESS' + end + RAKEFILE + File.open(bundled_app("test-gem/Rakefile"), "w") do |file| + file.puts rakefile + end + + Dir.chdir(bundled_app(gem_name)) do + sys_exec(rake) + expect(out).to include("SUCCESS") + end + end + + context "--bin parameter set" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --bin" + end + + it "builds bin skeleton" do + expect(bundled_app("test-gem/exe/test-gem")).to exist + end + + it "requires 'test/gem'" do + expect(bundled_app("test-gem/exe/test-gem").read).to match(%r{require "test/gem"}) + end + end + + context "no --test parameter" do + before do + reset! + in_app_root + bundle "gem #{gem_name}" + end + + it "doesn't create any spec/test file" do + expect(bundled_app("test-gem/.rspec")).to_not exist + expect(bundled_app("test-gem/spec/test/gem_spec.rb")).to_not exist + expect(bundled_app("test-gem/spec/spec_helper.rb")).to_not exist + expect(bundled_app("test-gem/test/test_test/gem.rb")).to_not exist + expect(bundled_app("test-gem/test/minitest_helper.rb")).to_not exist + end + end + + context "--test parameter set to rspec" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --test=rspec" + end + + it "builds spec skeleton" do + expect(bundled_app("test-gem/.rspec")).to exist + expect(bundled_app("test-gem/spec/test/gem_spec.rb")).to exist + expect(bundled_app("test-gem/spec/spec_helper.rb")).to exist + end + + it "requires 'test/gem'" do + expect(bundled_app("test-gem/spec/spec_helper.rb").read).to include(%(require "test/gem")) + end + + it "creates a default test which fails" do + expect(bundled_app("test-gem/spec/test/gem_spec.rb").read).to include("expect(false).to eq(true)") + end + + it "creates a default rake task to run the specs" do + rakefile = strip_whitespace <<-RAKEFILE + require "bundler/gem_tasks" + require "rspec/core/rake_task" + + RSpec::Core::RakeTask.new(:spec) + + task :default => :spec + RAKEFILE + + expect(bundled_app("test-gem/Rakefile").read).to eq(rakefile) + end + end + + context "--test parameter set to minitest" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --test=minitest" + end + + it "builds spec skeleton" do + expect(bundled_app("test-gem/test/test/gem_test.rb")).to exist + expect(bundled_app("test-gem/test/test_helper.rb")).to exist + end + + it "requires 'test/gem'" do + expect(bundled_app("test-gem/test/test_helper.rb").read).to match(%r{require "test/gem"}) + end + + it "requires 'test_helper'" do + expect(bundled_app("test-gem/test/test/gem_test.rb").read).to match(/require "test_helper"/) + end + + it "creates a default test which fails" do + expect(bundled_app("test-gem/test/test/gem_test.rb").read).to match(/assert false/) + end + + it "creates a default rake task to run the test suite" do + rakefile = strip_whitespace <<-RAKEFILE + require "bundler/gem_tasks" + require "rake/testtask" + + Rake::TestTask.new(:test) do |t| + t.libs << "test" + t.libs << "lib" + t.test_files = FileList["test/**/*_test.rb"] + end + + task :default => :test + RAKEFILE + + expect(bundled_app("test-gem/Rakefile").read).to eq(rakefile) + end + end + + context "--test with no arguments" do + before do + reset! + in_app_root + bundle "gem #{gem_name} --test" + end + + it "defaults to rspec" do + expect(bundled_app("test-gem/spec/spec_helper.rb")).to exist + expect(bundled_app("test-gem/test/minitest_helper.rb")).to_not exist + end + end + + context "--ext parameter set" do + before do + reset! + in_app_root + bundle "gem test_gem --ext" + end + + it "builds ext skeleton" do + expect(bundled_app("test_gem/ext/test_gem/extconf.rb")).to exist + expect(bundled_app("test_gem/ext/test_gem/test_gem.h")).to exist + expect(bundled_app("test_gem/ext/test_gem/test_gem.c")).to exist + end + + it "includes rake-compiler" do + expect(bundled_app("test_gem/test_gem.gemspec").read).to include('spec.add_development_dependency "rake-compiler"') + end + + it "depends on compile task for build" do + rakefile = strip_whitespace <<-RAKEFILE + require "bundler/gem_tasks" + require "rake/extensiontask" + + task :build => :compile + + Rake::ExtensionTask.new("test_gem") do |ext| + ext.lib_dir = "lib/test_gem" + end + + task :default => [:clobber, :compile, :spec] + RAKEFILE + + expect(bundled_app("test_gem/Rakefile").read).to eq(rakefile) + end + end + end + + describe "uncommon gem names" do + it "can deal with two dashes" do + bundle "gem a--a" + Bundler.clear_gemspec_cache + + expect(bundled_app("a--a/a--a.gemspec")).to exist + end + + it "fails gracefully with a ." do + bundle "gem foo.gemspec" + expect(out).to end_with("Invalid gem name foo.gemspec -- `Foo.gemspec` is an invalid constant name") + end + + it "fails gracefully with a ^" do + bundle "gem ^" + expect(out).to end_with("Invalid gem name ^ -- `^` is an invalid constant name") + end + + it "fails gracefully with a space" do + bundle "gem 'foo bar'" + expect(out).to end_with("Invalid gem name foo bar -- `Foo bar` is an invalid constant name") + end + + it "fails gracefully when multiple names are passed" do + bundle "gem foo bar baz" + expect(out).to eq(<<-E.strip) +ERROR: "bundle gem" was called with arguments ["foo", "bar", "baz"] +Usage: "bundle gem GEM [OPTIONS]" + E + end + end + + describe "#ensure_safe_gem_name" do + before do + bundle "gem #{subject}" + end + after do + Bundler.clear_gemspec_cache + end + + context "with an existing const name" do + subject { "gem" } + it { expect(out).to include("Invalid gem name #{subject}") } + end + + context "with an existing hyphenated const name" do + subject { "gem-specification" } + it { expect(out).to include("Invalid gem name #{subject}") } + end + + context "starting with an existing const name" do + subject { "gem-somenewconstantname" } + it { expect(out).not_to include("Invalid gem name #{subject}") } + end + + context "ending with an existing const name" do + subject { "somenewconstantname-gem" } + it { expect(out).not_to include("Invalid gem name #{subject}") } + end + end + + context "on first run" do + before do + in_app_root + end + + it "asks about test framework" do + global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__COC" => "false" + + bundle "gem foobar" do |input, _, _| + input.puts "rspec" + end + + expect(bundled_app("foobar/spec/spec_helper.rb")).to exist + rakefile = strip_whitespace <<-RAKEFILE + require "bundler/gem_tasks" + require "rspec/core/rake_task" + + RSpec::Core::RakeTask.new(:spec) + + task :default => :spec + RAKEFILE + + expect(bundled_app("foobar/Rakefile").read).to eq(rakefile) + expect(bundled_app("foobar/foobar.gemspec").read).to include('spec.add_development_dependency "rspec"') + end + + it "asks about MIT license" do + global_config "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false" + + bundle :config + + bundle "gem foobar" do |input, _, _| + input.puts "yes" + end + + expect(bundled_app("foobar/LICENSE.txt")).to exist + end + + it "asks about CoC" do + global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false" + + bundle "gem foobar" do |input, _, _| + input.puts "yes" + end + + expect(bundled_app("foobar/CODE_OF_CONDUCT.md")).to exist + end + end + + context "on conflicts with a previously created file" do + it "should fail gracefully" do + in_app_root do + FileUtils.touch("conflict-foobar") + end + output = bundle "gem conflict-foobar" + expect(output).to include("Errno::ENOTDIR") + expect(exitstatus).to eql(32) if exitstatus + end + end + + context "on conflicts with a previously created directory" do + it "should succeed" do + in_app_root do + FileUtils.mkdir_p("conflict-foobar/Gemfile") + end + bundle! "gem conflict-foobar" + expect(out).to include("file_clash conflict-foobar/Gemfile"). + and include "Initializing git repo in #{bundled_app("conflict-foobar")}" + end + end +end diff --git a/spec/bundler/commands/open_spec.rb b/spec/bundler/commands/open_spec.rb new file mode 100644 index 0000000000..6872e859d2 --- /dev/null +++ b/spec/bundler/commands/open_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle open" do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + end + + it "opens the gem with BUNDLER_EDITOR as highest priority" do + bundle "open rails", :env => { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" } + expect(out).to include("bundler_editor #{default_bundle_path("gems", "rails-2.3.2")}") + end + + it "opens the gem with VISUAL as 2nd highest priority" do + bundle "open rails", :env => { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "" } + expect(out).to include("visual #{default_bundle_path("gems", "rails-2.3.2")}") + end + + it "opens the gem with EDITOR as 3rd highest priority" do + bundle "open rails", :env => { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("editor #{default_bundle_path("gems", "rails-2.3.2")}") + end + + it "complains if no EDITOR is set" do + bundle "open rails", :env => { "EDITOR" => "", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to eq("To open a bundled gem, set $EDITOR or $BUNDLER_EDITOR") + end + + it "complains if gem not in bundle" do + bundle "open missing", :env => { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to match(/could not find gem 'missing'/i) + end + + it "does not blow up if the gem to open does not have a Gemfile" do + git = build_git "foo" + ref = git.ref_for("master", 11) + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'foo', :git => "#{lib_path("foo-1.0")}" + G + + bundle "open foo", :env => { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to match("editor #{default_bundle_path.join("bundler/gems/foo-1.0-#{ref}")}") + end + + it "suggests alternatives for similar-sounding gems" do + bundle "open Rails", :env => { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to match(/did you mean rails\?/i) + end + + it "opens the gem with short words" do + bundle "open rec", :env => { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" } + + expect(out).to include("bundler_editor #{default_bundle_path("gems", "activerecord-2.3.2")}") + end + + it "select the gem from many match gems" do + env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" } + bundle "open active", :env => env do |input, _, _| + input.puts "2" + end + + expect(out).to match(/bundler_editor #{default_bundle_path('gems', 'activerecord-2.3.2')}\z/) + end + + it "allows selecting exit from many match gems" do + env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" } + bundle! "open active", :env => env do |input, _, _| + input.puts "0" + end + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + gem "foo" + G + + bundle "config auto_install 1" + bundle "open rails", :env => { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("Installing foo 1.0") + end + + it "opens the editor with a clean env" do + bundle "open", :env => { "EDITOR" => "sh -c 'env'", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).not_to include("BUNDLE_GEMFILE=") + end +end diff --git a/spec/bundler/commands/outdated_spec.rb b/spec/bundler/commands/outdated_spec.rb new file mode 100644 index 0000000000..c6b6c9f59e --- /dev/null +++ b/spec/bundler/commands/outdated_spec.rb @@ -0,0 +1,731 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle outdated" do + before :each do + build_repo2 do + build_git "foo", :path => lib_path("foo") + build_git "zebra", :path => lib_path("zebra") + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + end + + describe "with no arguments" do + it "returns a sorted list of outdated gems" do + update_repo2 do + build_gem "activesupport", "3.0" + build_gem "weakling", "0.2" + update_git "foo", :path => lib_path("foo") + update_git "zebra", :path => lib_path("zebra") + end + + bundle "outdated" + + expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5)") + expect(out).to include("weakling (newest 0.2, installed 0.0.3, requested ~> 0.0.1)") + expect(out).to include("foo (newest 1.0") + + # Gem names are one per-line, between "*" and their parenthesized version. + gem_list = out.split("\n").map {|g| g[/\* (.*) \(/, 1] }.compact + expect(gem_list).to eq(gem_list.sort) + end + + it "returns non zero exit status if outdated gems present" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + bundle "outdated" + + expect(exitstatus).to_not be_zero if exitstatus + end + + it "returns success exit status if no outdated gems present" do + bundle "outdated" + + expect(exitstatus).to be_zero if exitstatus + end + + it "adds gem group to dependency output when repo is updated" do + install_gemfile <<-G + source "file://#{gem_repo2}" + + group :development, :test do + gem 'activesupport', '2.3.5' + end + G + + update_repo2 { build_gem "activesupport", "3.0" } + + bundle "outdated --verbose" + expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5) in groups \"development, test\"") + end + end + + describe "with --group option" do + def test_group_option(group = nil, gems_list_size = 1) + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "weakling", "~> 0.0.1" + gem "terranova", '8' + group :development, :test do + gem "duradura", '7.0' + gem 'activesupport', '2.3.5' + end + G + + update_repo2 do + build_gem "activesupport", "3.0" + build_gem "terranova", "9" + build_gem "duradura", "8.0" + end + + bundle "outdated --group #{group}" + + # Gem names are one per-line, between "*" and their parenthesized version. + gem_list = out.split("\n").map {|g| g[/\* (.*) \(/, 1] }.compact + expect(gem_list).to eq(gem_list.sort) + expect(gem_list.size).to eq gems_list_size + end + + it "not outdated gems" do + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "weakling", "~> 0.0.1" + gem "terranova", '8' + group :development, :test do + gem 'activesupport', '2.3.5' + gem "duradura", '7.0' + end + G + + bundle "outdated --group" + expect(out).to include("Bundle up to date!") + end + + it "returns a sorted list of outdated gems from one group => 'default'" do + test_group_option("default") + + expect(out).to include("===== Group default =====") + expect(out).to include("terranova (") + + expect(out).not_to include("===== Group development, test =====") + expect(out).not_to include("activesupport") + expect(out).not_to include("duradura") + end + + it "returns a sorted list of outdated gems from one group => 'development'" do + test_group_option("development", 2) + + expect(out).not_to include("===== Group default =====") + expect(out).not_to include("terranova (") + + expect(out).to include("===== Group development, test =====") + expect(out).to include("activesupport") + expect(out).to include("duradura") + end + end + + describe "with --groups option" do + it "not outdated gems" do + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "weakling", "~> 0.0.1" + gem "terranova", '8' + group :development, :test do + gem 'activesupport', '2.3.5' + gem "duradura", '7.0' + end + G + + bundle "outdated --groups" + expect(out).to include("Bundle up to date!") + end + + it "returns a sorted list of outdated gems by groups" do + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "weakling", "~> 0.0.1" + gem "terranova", '8' + group :development, :test do + gem 'activesupport', '2.3.5' + gem "duradura", '7.0' + end + G + + update_repo2 do + build_gem "activesupport", "3.0" + build_gem "terranova", "9" + build_gem "duradura", "8.0" + end + + bundle "outdated --groups" + expect(out).to include("===== Group default =====") + expect(out).to include("terranova (newest 9, installed 8, requested = 8)") + expect(out).to include("===== Group development, test =====") + expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5)") + expect(out).to include("duradura (newest 8.0, installed 7.0, requested = 7.0)") + + expect(out).not_to include("weakling (") + + # TODO: check gems order inside the group + end + end + + describe "with --local option" do + it "uses local cache to return a list of outdated gems" do + update_repo2 do + build_gem "activesupport", "2.3.4" + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.4" + G + + bundle "outdated --local" + + expect(out).to include("activesupport (newest 2.3.5, installed 2.3.4, requested = 2.3.4)") + end + + it "doesn't hit repo2" do + FileUtils.rm_rf(gem_repo2) + + bundle "outdated --local" + expect(out).not_to match(/Fetching (gem|version|dependency) metadata from/) + end + end + + shared_examples_for "a minimal output is desired" do + context "and gems are outdated" do + before do + update_repo2 do + build_gem "activesupport", "3.0" + build_gem "weakling", "0.2" + end + end + + it "outputs a sorted list of outdated gems with a more minimal format" do + minimal_output = "activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5)\n" \ + "weakling (newest 0.2, installed 0.0.3, requested ~> 0.0.1)" + subject + expect(out).to eq(minimal_output) + end + end + + context "and no gems are outdated" do + it "has empty output" do + subject + expect(out).to eq("") + end + end + end + + describe "with --parseable option" do + subject { bundle "outdated --parseable" } + + it_behaves_like "a minimal output is desired" + end + + describe "with aliased --porcelain option" do + subject { bundle "outdated --porcelain" } + + it_behaves_like "a minimal output is desired" + end + + describe "with specified gems" do + it "returns list of outdated gems" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + bundle "outdated foo" + expect(out).not_to include("activesupport (newest") + expect(out).to include("foo (newest 1.0") + end + end + + describe "pre-release gems" do + context "without the --pre option" do + it "ignores pre-release versions" do + update_repo2 do + build_gem "activesupport", "3.0.0.beta" + end + + bundle "outdated" + expect(out).not_to include("activesupport (3.0.0.beta > 2.3.5)") + end + end + + context "with the --pre option" do + it "includes pre-release versions" do + update_repo2 do + build_gem "activesupport", "3.0.0.beta" + end + + bundle "outdated --pre" + expect(out).to include("activesupport (newest 3.0.0.beta, installed 2.3.5, requested = 2.3.5)") + end + end + + context "when current gem is a pre-release" do + it "includes the gem" do + update_repo2 do + build_gem "activesupport", "3.0.0.beta.1" + build_gem "activesupport", "3.0.0.beta.2" + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "3.0.0.beta.1" + G + + bundle "outdated" + expect(out).to include("(newest 3.0.0.beta.2, installed 3.0.0.beta.1, requested = 3.0.0.beta.1)") + end + end + end + + describe "with --strict option" do + it "only reports gems that have a newer version that matches the specified dependency version requirements" do + update_repo2 do + build_gem "activesupport", "3.0" + build_gem "weakling", "0.0.5" + end + + bundle "outdated --strict" + + expect(out).to_not include("activesupport (newest") + expect(out).to include("(newest 0.0.5, installed 0.0.3, requested ~> 0.0.1)") + end + + it "only reports gem dependencies when they can actually be updated" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack_middleware", "1.0" + G + + bundle "outdated --strict" + + expect(out).to_not include("rack (1.2") + end + + describe "and filter options" do + it "only reports gems that match requirement and patch filter level" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "~> 2.3" + gem "weakling", ">= 0.0.1" + G + + update_repo2 do + build_gem "activesupport", %w(2.4.0 3.0.0) + build_gem "weakling", "0.0.5" + end + + bundle "outdated --strict --filter-patch" + + expect(out).to_not include("activesupport (newest") + expect(out).to include("(newest 0.0.5, installed 0.0.3") + end + + it "only reports gems that match requirement and minor filter level" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "~> 2.3" + gem "weakling", ">= 0.0.1" + G + + update_repo2 do + build_gem "activesupport", %w(2.3.9) + build_gem "weakling", "0.1.5" + end + + bundle "outdated --strict --filter-minor" + + expect(out).to_not include("activesupport (newest") + expect(out).to include("(newest 0.1.5, installed 0.0.3") + end + + it "only reports gems that match requirement and major filter level" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "~> 2.3" + gem "weakling", ">= 0.0.1" + G + + update_repo2 do + build_gem "activesupport", %w(2.4.0 2.5.0) + build_gem "weakling", "1.1.5" + end + + bundle "outdated --strict --filter-major" + + expect(out).to_not include("activesupport (newest") + expect(out).to include("(newest 1.1.5, installed 0.0.3") + end + end + end + + describe "with invalid gem name" do + it "returns could not find gem name" do + bundle "outdated invalid_gem_name" + expect(out).to include("Could not find gem 'invalid_gem_name'.") + end + + it "returns non-zero exit code" do + bundle "outdated invalid_gem_name" + expect(exitstatus).to_not be_zero if exitstatus + end + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "0.9.1" + gem "foo" + G + + bundle "config auto_install 1" + bundle :outdated + expect(out).to include("Installing foo 1.0") + end + + context "after bundle install --deployment" do + before do + install_gemfile <<-G, :deployment => true + source "file://#{gem_repo2}" + + gem "rack" + gem "foo" + G + end + + it "outputs a helpful message about being in deployment mode" do + update_repo2 { build_gem "activesupport", "3.0" } + + bundle "outdated" + expect(exitstatus).to_not be_zero if exitstatus + expect(out).to include("You are trying to check outdated gems in deployment mode.") + expect(out).to include("Run `bundle outdated` elsewhere.") + expect(out).to include("If this is a development machine, remove the ") + expect(out).to include("Gemfile freeze\nby running `bundle install --no-deployment`.") + end + end + + context "update available for a gem on a different platform" do + before do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "laduradura", '= 5.15.2' + G + end + + it "reports that no updates are available" do + bundle "outdated" + expect(out).to include("Bundle up to date!") + end + end + + context "update available for a gem on the same platform while multiple platforms used for gem" do + it "reports that updates are available if the Ruby platform is used" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] + G + + bundle "outdated" + expect(out).to include("Bundle up to date!") + end + + it "reports that updates are available if the JRuby platform is used" do + simulate_ruby_engine "jruby", "1.6.7" do + simulate_platform "jruby" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] + G + + bundle "outdated" + expect(out).to include("Outdated gems included in the bundle:") + expect(out).to include("laduradura (newest 5.15.3, installed 5.15.2, requested = 5.15.2)") + end + end + end + end + + shared_examples_for "version update is detected" do + it "reports that a gem has a newer version" do + subject + expect(out).to include("Outdated gems included in the bundle:") + expect(out).to include("activesupport (newest") + expect(out).to_not include("ERROR REPORT TEMPLATE") + end + end + + shared_examples_for "major version updates are detected" do + before do + update_repo2 do + build_gem "activesupport", "3.3.5" + build_gem "weakling", "0.8.0" + end + end + + it_behaves_like "version update is detected" + end + + context "when on a new machine" do + before do + simulate_new_machine + + update_git "foo", :path => lib_path("foo") + update_repo2 do + build_gem "activesupport", "3.3.5" + build_gem "weakling", "0.8.0" + end + end + + subject { bundle "outdated" } + it_behaves_like "version update is detected" + end + + shared_examples_for "minor version updates are detected" do + before do + update_repo2 do + build_gem "activesupport", "2.7.5" + build_gem "weakling", "2.0.1" + end + end + + it_behaves_like "version update is detected" + end + + shared_examples_for "patch version updates are detected" do + before do + update_repo2 do + build_gem "activesupport", "2.3.7" + build_gem "weakling", "0.3.1" + end + end + + it_behaves_like "version update is detected" + end + + shared_examples_for "no version updates are detected" do + it "does not detect any version updates" do + subject + expect(out).to include("updates to display.") + expect(out).to_not include("ERROR REPORT TEMPLATE") + expect(out).to_not include("activesupport (newest") + expect(out).to_not include("weakling (newest") + end + end + + shared_examples_for "major version is ignored" do + before do + update_repo2 do + build_gem "activesupport", "3.3.5" + build_gem "weakling", "1.0.1" + end + end + + it_behaves_like "no version updates are detected" + end + + shared_examples_for "minor version is ignored" do + before do + update_repo2 do + build_gem "activesupport", "2.4.5" + build_gem "weakling", "0.3.1" + end + end + + it_behaves_like "no version updates are detected" + end + + shared_examples_for "patch version is ignored" do + before do + update_repo2 do + build_gem "activesupport", "2.3.6" + build_gem "weakling", "0.0.4" + end + end + + it_behaves_like "no version updates are detected" + end + + describe "with --filter-major option" do + subject { bundle "outdated --filter-major" } + + it_behaves_like "major version updates are detected" + it_behaves_like "minor version is ignored" + it_behaves_like "patch version is ignored" + end + + describe "with --filter-minor option" do + subject { bundle "outdated --filter-minor" } + + it_behaves_like "minor version updates are detected" + it_behaves_like "major version is ignored" + it_behaves_like "patch version is ignored" + end + + describe "with --filter-patch option" do + subject { bundle "outdated --filter-patch" } + + it_behaves_like "patch version updates are detected" + it_behaves_like "major version is ignored" + it_behaves_like "minor version is ignored" + end + + describe "with --filter-minor --filter-patch options" do + subject { bundle "outdated --filter-minor --filter-patch" } + + it_behaves_like "minor version updates are detected" + it_behaves_like "patch version updates are detected" + it_behaves_like "major version is ignored" + end + + describe "with --filter-major --filter-minor options" do + subject { bundle "outdated --filter-major --filter-minor" } + + it_behaves_like "major version updates are detected" + it_behaves_like "minor version updates are detected" + it_behaves_like "patch version is ignored" + end + + describe "with --filter-major --filter-patch options" do + subject { bundle "outdated --filter-major --filter-patch" } + + it_behaves_like "major version updates are detected" + it_behaves_like "patch version updates are detected" + it_behaves_like "minor version is ignored" + end + + describe "with --filter-major --filter-minor --filter-patch options" do + subject { bundle "outdated --filter-major --filter-minor --filter-patch" } + + it_behaves_like "major version updates are detected" + it_behaves_like "minor version updates are detected" + it_behaves_like "patch version updates are detected" + end + + context "conservative updates" do + context "without update-strict" do + before do + build_repo4 do + build_gem "patch", %w(1.0.0 1.0.1) + build_gem "minor", %w(1.0.0 1.0.1 1.1.0) + build_gem "major", %w(1.0.0 1.0.1 1.1.0 2.0.0) + end + + # establish a lockfile set to 1.0.0 + install_gemfile <<-G + source "file://#{gem_repo4}" + gem 'patch', '1.0.0' + gem 'minor', '1.0.0' + gem 'major', '1.0.0' + G + + # remove 1.4.3 requirement and bar altogether + # to setup update specs below + gemfile <<-G + source "file://#{gem_repo4}" + gem 'patch' + gem 'minor' + gem 'major' + G + end + + it "shows nothing when patching and filtering to minor" do + bundle "outdated --patch --filter-minor" + + expect(out).to include("No minor updates to display.") + expect(out).not_to include("patch (newest") + expect(out).not_to include("minor (newest") + expect(out).not_to include("major (newest") + end + + it "shows all gems when patching and filtering to patch" do + bundle "outdated --patch --filter-patch" + + expect(out).to include("patch (newest 1.0.1") + expect(out).to include("minor (newest 1.0.1") + expect(out).to include("major (newest 1.0.1") + end + + it "shows minor and major when updating to minor and filtering to patch and minor" do + bundle "outdated --minor --filter-minor" + + expect(out).not_to include("patch (newest") + expect(out).to include("minor (newest 1.1.0") + expect(out).to include("major (newest 1.1.0") + end + + it "shows minor when updating to major and filtering to minor with parseable" do + bundle "outdated --major --filter-minor --parseable" + + expect(out).not_to include("patch (newest") + expect(out).to include("minor (newest") + expect(out).not_to include("major (newest") + end + end + + context "with update-strict" do + before do + build_repo4 do + build_gem "foo", %w(1.4.3 1.4.4) do |s| + s.add_dependency "bar", "~> 2.0" + end + build_gem "foo", %w(1.4.5 1.5.0) do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "foo", %w(1.5.1) do |s| + s.add_dependency "bar", "~> 3.0" + end + build_gem "bar", %w(2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0) + build_gem "qux", %w(1.0.0 1.1.0 2.0.0) + end + + # establish a lockfile set to 1.4.3 + install_gemfile <<-G + source "file://#{gem_repo4}" + gem 'foo', '1.4.3' + gem 'bar', '2.0.3' + gem 'qux', '1.0.0' + G + + # remove 1.4.3 requirement and bar altogether + # to setup update specs below + gemfile <<-G + source "file://#{gem_repo4}" + gem 'foo' + gem 'qux' + G + end + + it "shows gems with update-strict updating to patch and filtering to patch" do + bundle "outdated --patch --update-strict --filter-patch" + + expect(out).to include("foo (newest 1.4.4") + expect(out).to include("bar (newest 2.0.5") + expect(out).not_to include("qux (newest") + end + end + end +end diff --git a/spec/bundler/commands/package_spec.rb b/spec/bundler/commands/package_spec.rb new file mode 100644 index 0000000000..86c09db3ca --- /dev/null +++ b/spec/bundler/commands/package_spec.rb @@ -0,0 +1,306 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle package" do + context "with --gemfile" do + it "finds the gemfile" do + gemfile bundled_app("NotGemfile"), <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + bundle "package --gemfile=NotGemfile" + + ENV["BUNDLE_GEMFILE"] = "NotGemfile" + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + context "with --all" do + context "without a gemspec" do + it "caches all dependencies except bundler itself" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack' + gem 'bundler' + D + + bundle "package --all" + + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist + end + end + + context "with a gemspec" do + context "that has the same name as the gem" do + before do + File.open(bundled_app("mygem.gemspec"), "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = "mygem" + s.version = "0.1.1" + s.summary = "" + s.authors = ["gem author"] + s.add_development_dependency "nokogiri", "=1.4.2" + end + G + end + end + + it "caches all dependencies except bundler and the gemspec specified gem" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack' + gemspec + D + + bundle! "package --all" + + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/nokogiri-1.4.2.gem")).to exist + expect(bundled_app("vendor/cache/mygem-0.1.1.gem")).to_not exist + expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist + end + end + + context "that has a different name as the gem" do + before do + File.open(bundled_app("mygem_diffname.gemspec"), "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = "mygem" + s.version = "0.1.1" + s.summary = "" + s.authors = ["gem author"] + s.add_development_dependency "nokogiri", "=1.4.2" + end + G + end + end + + it "caches all dependencies except bundler and the gemspec specified gem" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack' + gemspec + D + + bundle! "package --all" + + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/nokogiri-1.4.2.gem")).to exist + expect(bundled_app("vendor/cache/mygem-0.1.1.gem")).to_not exist + expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist + end + end + end + + context "with multiple gemspecs" do + before do + File.open(bundled_app("mygem.gemspec"), "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = "mygem" + s.version = "0.1.1" + s.summary = "" + s.authors = ["gem author"] + s.add_development_dependency "nokogiri", "=1.4.2" + end + G + end + File.open(bundled_app("mygem_client.gemspec"), "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = "mygem_test" + s.version = "0.1.1" + s.summary = "" + s.authors = ["gem author"] + s.add_development_dependency "weakling", "=0.0.3" + end + G + end + end + + it "caches all dependencies except bundler and the gemspec specified gems" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack' + gemspec :name => 'mygem' + gemspec :name => 'mygem_test' + D + + bundle! "package --all" + + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/nokogiri-1.4.2.gem")).to exist + expect(bundled_app("vendor/cache/weakling-0.0.3.gem")).to exist + expect(bundled_app("vendor/cache/mygem-0.1.1.gem")).to_not exist + expect(bundled_app("vendor/cache/mygem_test-0.1.1.gem")).to_not exist + expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist + end + end + end + + context "with --path" do + it "sets root directory for gems" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack' + D + + bundle "package --path=#{bundled_app("test")}" + + expect(the_bundle).to include_gems "rack 1.0.0" + expect(bundled_app("test/vendor/cache/")).to exist + end + end + + context "with --no-install" do + it "puts the gems in vendor/cache but does not install them" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack' + D + + bundle "package --no-install" + + expect(the_bundle).not_to include_gems "rack 1.0.0" + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + end + + it "does not prevent installing gems with bundle install" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack' + D + + bundle "package --no-install" + bundle "install" + + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + context "with --all-platforms" do + it "puts the gems in vendor/cache even for other rubies", :ruby => "2.1" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack', :platforms => :ruby_19 + D + + bundle "package --all-platforms" + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + end + end + + context "with --frozen" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + bundle "install" + end + + subject { bundle "package --frozen" } + + it "tries to install with frozen" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama" + G + subject + expect(exitstatus).to eq(16) if exitstatus + expect(out).to include("deployment mode") + expect(out).to include("You have added to the Gemfile") + expect(out).to include("* rack-obama") + bundle "env" + expect(out).to include("frozen") + end + end +end + +RSpec.describe "bundle install with gem sources" do + describe "when cached and locked" do + it "does not hit the remote at all" do + build_repo2 + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack" + G + + bundle :pack + simulate_new_machine + FileUtils.rm_rf gem_repo2 + + bundle "install --local" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "does not hit the remote at all" do + build_repo2 + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack" + G + + bundle :pack + simulate_new_machine + FileUtils.rm_rf gem_repo2 + + bundle "install --deployment" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "does not reinstall already-installed gems" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + bundle :pack + + build_gem "rack", "1.0.0", :path => bundled_app("vendor/cache") do |s| + s.write "lib/rack.rb", "raise 'omg'" + end + + bundle :install + expect(err).to lack_errors + expect(the_bundle).to include_gems "rack 1.0" + end + + it "ignores cached gems for the wrong platform" do + simulate_platform "java" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "platform_specific" + G + bundle :pack + end + + simulate_new_machine + + simulate_platform "ruby" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "platform_specific" + G + run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" + expect(out).to eq("1.0.0 RUBY") + end + end + + it "does not update the cache if --no-cache is passed" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + bundled_app("vendor/cache").mkpath + expect(bundled_app("vendor/cache").children).to be_empty + + bundle "install --no-cache" + expect(bundled_app("vendor/cache").children).to be_empty + end + end +end diff --git a/spec/bundler/commands/pristine_spec.rb b/spec/bundler/commands/pristine_spec.rb new file mode 100644 index 0000000000..3aca313e0f --- /dev/null +++ b/spec/bundler/commands/pristine_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true +require "spec_helper" +require "fileutils" + +RSpec.describe "bundle pristine", :ruby_repo do + before :each do + build_lib "baz", :path => bundled_app do |s| + s.version = "1.0.0" + s.add_development_dependency "baz-dev", "=1.0.0" + end + + build_repo2 do + build_gem "weakling" + build_gem "baz-dev", "1.0.0" + build_gem "very_simple_binary", &:add_c_extension + build_git "foo", :path => lib_path("foo") + build_lib "bar", :path => lib_path("bar") + end + + install_gemfile! <<-G + source "file://#{gem_repo2}" + gem "weakling" + gem "very_simple_binary" + gem "foo", :git => "#{lib_path("foo")}" + gem "bar", :path => "#{lib_path("bar")}" + + gemspec + G + end + + context "when sourced from Rubygems" do + it "reverts using cached .gem file" do + spec = Bundler.definition.specs["weakling"].first + changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt") + + FileUtils.touch(changes_txt) + expect(changes_txt).to be_file + + bundle "pristine" + expect(changes_txt).to_not be_file + end + + it "does not delete the bundler gem", :ruby_repo do + system_gems :bundler + bundle! "install" + bundle! "pristine", :system_bundler => true + bundle! "-v", :system_bundler => true + expect(out).to end_with(Bundler::VERSION) + end + end + + context "when sourced from git repo" do + it "reverts by resetting to current revision`" do + spec = Bundler.definition.specs["foo"].first + changed_file = Pathname.new(spec.full_gem_path).join("lib/foo.rb") + diff = "#Pristine spec changes" + + File.open(changed_file, "a") {|f| f.puts "#Pristine spec changes" } + expect(File.read(changed_file)).to include(diff) + + bundle "pristine" + expect(File.read(changed_file)).to_not include(diff) + end + end + + context "when sourced from gemspec" do + it "displays warning and ignores changes when sourced from gemspec" do + spec = Bundler.definition.specs["baz"].first + changed_file = Pathname.new(spec.full_gem_path).join("lib/baz.rb") + diff = "#Pristine spec changes" + + File.open(changed_file, "a") {|f| f.puts "#Pristine spec changes" } + expect(File.read(changed_file)).to include(diff) + + bundle "pristine" + expect(File.read(changed_file)).to include(diff) + expect(out).to include("Cannot pristine #{spec.name} (#{spec.version}#{spec.git_version}). Gem is sourced from local path.") + end + + it "reinstall gemspec dependency" do + spec = Bundler.definition.specs["baz-dev"].first + changed_file = Pathname.new(spec.full_gem_path).join("lib/baz-dev.rb") + diff = "#Pristine spec changes" + + File.open(changed_file, "a") {|f| f.puts "#Pristine spec changes" } + expect(File.read(changed_file)).to include(diff) + + bundle "pristine" + expect(File.read(changed_file)).to_not include(diff) + end + end + + context "when sourced from path" do + it "displays warning and ignores changes when sourced from local path" do + spec = Bundler.definition.specs["bar"].first + changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt") + FileUtils.touch(changes_txt) + expect(changes_txt).to be_file + bundle "pristine" + expect(out).to include("Cannot pristine #{spec.name} (#{spec.version}#{spec.git_version}). Gem is sourced from local path.") + expect(changes_txt).to be_file + end + end + + context "when a build config exists for one of the gems" do + let(:very_simple_binary) { Bundler.definition.specs["very_simple_binary"].first } + let(:c_ext_dir) { Pathname.new(very_simple_binary.full_gem_path).join("ext") } + let(:build_opt) { "--with-ext-lib=#{c_ext_dir}" } + before { bundle "config build.very_simple_binary -- #{build_opt}" } + + # This just verifies that the generated Makefile from the c_ext gem makes + # use of the build_args from the bundle config + it "applies the config when installing the gem" do + bundle! "pristine" + + makefile_contents = File.read(c_ext_dir.join("Makefile").to_s) + expect(makefile_contents).to match(/libpath =.*#{c_ext_dir}/) + expect(makefile_contents).to match(/LIBPATH =.*-L#{c_ext_dir}/) + end + end +end diff --git a/spec/bundler/commands/show_spec.rb b/spec/bundler/commands/show_spec.rb new file mode 100644 index 0000000000..0391ddec52 --- /dev/null +++ b/spec/bundler/commands/show_spec.rb @@ -0,0 +1,191 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle show" do + context "with a standard Gemfile" do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + end + + it "creates a Gemfile.lock if one did not exist" do + FileUtils.rm("Gemfile.lock") + + bundle "show" + + expect(bundled_app("Gemfile.lock")).to exist + end + + it "creates a Gemfile.lock when invoked with a gem name" do + FileUtils.rm("Gemfile.lock") + + bundle "show rails" + + expect(bundled_app("Gemfile.lock")).to exist + end + + it "prints path if gem exists in bundle" do + bundle "show rails" + expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s) + end + + it "warns if path no longer exists on disk" do + FileUtils.rm_rf("#{system_gem_path}/gems/rails-2.3.2") + + bundle "show rails" + + expect(out).to match(/has been deleted/i) + expect(out).to include(default_bundle_path("gems", "rails-2.3.2").to_s) + end + + it "prints the path to the running bundler", :ruby_repo do + bundle "show bundler" + expect(out).to eq(root.to_s) + end + + it "complains if gem not in bundle" do + bundle "show missing" + expect(out).to match(/could not find gem 'missing'/i) + end + + it "prints path of all gems in bundle sorted by name" do + bundle "show --paths" + + expect(out).to include(default_bundle_path("gems", "rake-10.0.2").to_s) + expect(out).to include(default_bundle_path("gems", "rails-2.3.2").to_s) + + # Gem names are the last component of their path. + gem_list = out.split.map {|p| p.split("/").last } + expect(gem_list).to eq(gem_list.sort) + end + + it "prints summary of gems" do + bundle "show --verbose" + + expect(out).to include("* actionmailer (2.3.2)") + expect(out).to include("\tSummary: This is just a fake gem for testing") + expect(out).to include("\tHomepage: No website available.") + expect(out).to include("\tStatus: Up to date") + end + end + + context "with a git repo in the Gemfile" do + before :each do + @git = build_git "foo", "1.0" + end + + it "prints out git info" do + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + expect(the_bundle).to include_gems "foo 1.0" + + bundle :show + expect(out).to include("foo (1.0 #{@git.ref_for("master", 6)}") + end + + it "prints out branch names other than master" do + update_git "foo", :branch => "omg" do |s| + s.write "lib/foo.rb", "FOO = '1.0.omg'" + end + @revision = revision_for(lib_path("foo-1.0"))[0...6] + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "omg" + G + expect(the_bundle).to include_gems "foo 1.0.omg" + + bundle :show + expect(out).to include("foo (1.0 #{@git.ref_for("omg", 6)}") + end + + it "doesn't print the branch when tied to a ref" do + sha = revision_for(lib_path("foo-1.0")) + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{sha}" + G + + bundle :show + expect(out).to include("foo (1.0 #{sha[0..6]})") + end + + it "handles when a version is a '-' prerelease", :rubygems => "2.1" do + @git = build_git("foo", "1.0.0-beta.1", :path => lib_path("foo")) + install_gemfile <<-G + gem "foo", "1.0.0-beta.1", :git => "#{lib_path("foo")}" + G + expect(the_bundle).to include_gems "foo 1.0.0.pre.beta.1" + + bundle! :show + expect(out).to include("foo (1.0.0.pre.beta.1") + end + end + + context "in a fresh gem in a blank git repo" do + before :each do + build_git "foo", :path => lib_path("foo") + in_app_root_custom lib_path("foo") + File.open("Gemfile", "w") {|f| f.puts "gemspec" } + sys_exec "rm -rf .git && git init" + end + + it "does not output git errors" do + bundle :show + expect(err).to lack_errors + end + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "foo" + G + + bundle "config auto_install 1" + bundle :show + expect(out).to include("Installing foo 1.0") + end + + context "with an invalid regexp for gem name" do + it "does not find the gem" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + invalid_regexp = "[]" + + bundle "show #{invalid_regexp}" + expect(out).to include("Could not find gem '#{invalid_regexp}'.") + end + end + + context "--outdated option" do + # Regression test for https://github.com/bundler/bundler/issues/5375 + before do + build_repo2 + end + + it "doesn't update gems to newer versions" do + install_gemfile! <<-G + source "file://#{gem_repo2}" + gem "rails" + G + + expect(the_bundle).to include_gem("rails 2.3.2") + + update_repo2 do + build_gem "rails", "3.0.0" do |s| + s.executables = "rails" + end + end + + bundle! "show --outdated" + + bundle! "install" + expect(the_bundle).to include_gem("rails 2.3.2") + end + end +end diff --git a/spec/bundler/commands/update_spec.rb b/spec/bundler/commands/update_spec.rb new file mode 100644 index 0000000000..4992e428da --- /dev/null +++ b/spec/bundler/commands/update_spec.rb @@ -0,0 +1,657 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle update" do + before :each do + build_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + gem "rack-obama" + G + end + + describe "with no arguments" do + it "updates the entire bundle" do + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle "update" + expect(out).to include("Bundle updated!") + expect(the_bundle).to include_gems "rack 1.2", "rack-obama 1.0", "activesupport 3.0" + end + + it "doesn't delete the Gemfile.lock file if something goes wrong" do + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + gem "rack-obama" + exit! + G + bundle "update" + expect(bundled_app("Gemfile.lock")).to exist + end + end + + describe "--quiet argument" do + it "hides UI messages" do + bundle "update --quiet" + expect(out).not_to include("Bundle updated!") + end + end + + describe "with a top level dependency" do + it "unlocks all child dependencies that are unrelated to other locked dependencies" do + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle "update rack-obama" + expect(the_bundle).to include_gems "rack 1.2", "rack-obama 1.0", "activesupport 2.3.5" + end + end + + describe "with an unknown dependency" do + it "should inform the user" do + bundle "update halting-problem-solver" + expect(out).to include "Could not find gem 'halting-problem-solver'" + end + it "should suggest alternatives" do + bundle "update active-support" + expect(out).to include "Did you mean activesupport?" + end + end + + describe "with a child dependency" do + it "should update the child dependency" do + update_repo2 + bundle "update rack" + expect(the_bundle).to include_gems "rack 1.2" + end + end + + describe "when a possible resolve requires an older version of a locked gem" do + context "and only_update_to_newer_versions is set" do + before do + bundle! "config only_update_to_newer_versions true" + end + it "does not go to an older version" do + build_repo4 do + build_gem "a" do |s| + s.add_dependency "b" + s.add_dependency "c" + end + build_gem "b" + build_gem "c" + build_gem "c", "2.0" + end + + install_gemfile! <<-G + source "file:#{gem_repo4}" + gem "a" + G + + expect(the_bundle).to include_gems("a 1.0", "b 1.0", "c 2.0") + + update_repo4 do + build_gem "b", "2.0" do |s| + s.add_dependency "c", "< 2" + end + end + + bundle! "update" + + expect(the_bundle).to include_gems("a 1.0", "b 1.0", "c 2.0") + end + end + end + + describe "with --local option" do + it "doesn't hit repo2" do + FileUtils.rm_rf(gem_repo2) + + bundle "update --local" + expect(out).not_to match(/Fetching source index/) + end + end + + describe "with --group option" do + it "should update only specifed group gems" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", :group => :development + gem "rack" + G + update_repo2 do + build_gem "activesupport", "3.0" + end + bundle "update --group development" + expect(the_bundle).to include_gems "activesupport 3.0" + expect(the_bundle).not_to include_gems "rack 1.2" + end + + context "when there is a source with the same name as a gem in a group" do + before :each do + build_git "foo", :path => lib_path("activesupport") + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", :group => :development + gem "foo", :git => "#{lib_path("activesupport")}" + G + end + + it "should not update the gems from that source" do + update_repo2 { build_gem "activesupport", "3.0" } + update_git "foo", "2.0", :path => lib_path("activesupport") + + bundle "update --group development" + expect(the_bundle).to include_gems "activesupport 3.0" + expect(the_bundle).not_to include_gems "foo 2.0" + end + end + end + + describe "in a frozen bundle" do + it "should fail loudly" do + bundle "install --deployment" + bundle "update" + + expect(out).to match(/You are trying to install in deployment mode after changing.your Gemfile/m) + expect(out).to match(/freeze \nby running `bundle install --no-deployment`./m) + expect(exitstatus).not_to eq(0) if exitstatus + end + + it "should suggest different command when frozen is set globally" do + bundler "config --global frozen 1" + bundle "update" + expect(out).to match(/You are trying to install in deployment mode after changing.your Gemfile/m) + expect(out).to match(/freeze \nby running `bundle config --delete frozen`./m) + end + end + + describe "with --source option" do + it "should not update gems not included in the source that happen to have the same name" do + pending("Allowed to fail to preserve backwards-compatibility") + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + G + update_repo2 { build_gem "activesupport", "3.0" } + + bundle "update --source activesupport" + expect(the_bundle).not_to include_gems "activesupport 3.0" + end + + it "should update gems not included in the source that happen to have the same name" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + G + update_repo2 { build_gem "activesupport", "3.0" } + + bundle "update --source activesupport" + expect(the_bundle).to include_gems "activesupport 3.0" + end + end + + context "when there is a child dependency that is also in the gemfile" do + before do + build_repo2 do + build_gem "fred", "1.0" + build_gem "harry", "1.0" do |s| + s.add_dependency "fred" + end + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "harry" + gem "fred" + G + end + + it "should not update the child dependencies of a gem that has the same name as the source" do + update_repo2 do + build_gem "fred", "2.0" + build_gem "harry", "2.0" do |s| + s.add_dependency "fred" + end + end + + bundle "update --source harry" + expect(the_bundle).to include_gems "harry 2.0" + expect(the_bundle).to include_gems "fred 1.0" + end + end + + context "when there is a child dependency that appears elsewhere in the dependency graph" do + before do + build_repo2 do + build_gem "fred", "1.0" do |s| + s.add_dependency "george" + end + build_gem "george", "1.0" + build_gem "harry", "1.0" do |s| + s.add_dependency "george" + end + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "harry" + gem "fred" + G + end + + it "should not update the child dependencies of a gem that has the same name as the source" do + update_repo2 do + build_gem "george", "2.0" + build_gem "harry", "2.0" do |s| + s.add_dependency "george" + end + end + + bundle "update --source harry" + expect(the_bundle).to include_gems "harry 2.0" + expect(the_bundle).to include_gems "fred 1.0" + expect(the_bundle).to include_gems "george 1.0" + end + end +end + +RSpec.describe "bundle update in more complicated situations" do + before :each do + build_repo2 + end + + it "will eagerly unlock dependencies of a specified gem" do + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "thin" + gem "rack-obama" + G + + update_repo2 do + build_gem "thin", "2.0" do |s| + s.add_dependency "rack" + end + end + + bundle "update thin" + expect(the_bundle).to include_gems "thin 2.0", "rack 1.2", "rack-obama 1.0" + end + + it "will update only from pinned source" do + install_gemfile <<-G + source "file://#{gem_repo2}" + + source "file://#{gem_repo1}" do + gem "thin" + end + G + + update_repo2 do + build_gem "thin", "2.0" + end + + bundle "update" + expect(the_bundle).to include_gems "thin 1.0" + end +end + +RSpec.describe "bundle update without a Gemfile.lock" do + it "should not explode" do + build_repo2 + + gemfile <<-G + source "file://#{gem_repo2}" + + gem "rack", "1.0" + G + + bundle "update" + + expect(the_bundle).to include_gems "rack 1.0.0" + end +end + +RSpec.describe "bundle update when a gem depends on a newer version of bundler" do + before(:each) do + build_repo2 do + build_gem "rails", "3.0.1" do |s| + s.add_dependency "bundler", Bundler::VERSION.succ + end + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "rails", "3.0.1" + G + end + + it "should not explode" do + bundle "update" + expect(err).to lack_errors + end + + it "should explain that bundler conflicted" do + bundle "update" + expect(out).not_to match(/in snapshot/i) + expect(out).to match(/current Bundler version/i) + expect(out).to match(/perhaps you need to update bundler/i) + end +end + +RSpec.describe "bundle update" do + it "shows the previous version of the gem when updated from rubygems source" do + build_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + G + + bundle "update" + expect(out).to include("Using activesupport 2.3.5") + + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle "update" + expect(out).to include("Installing activesupport 3.0 (was 2.3.5)") + end + + it "shows error message when Gemfile.lock is not preset and gem is specified" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + G + + bundle "update nonexisting" + expect(out).to include("This Bundle hasn't been installed yet. Run `bundle install` to update and install the bundled gems.") + expect(exitstatus).to eq(22) if exitstatus + end +end + +RSpec.describe "bundle update --ruby" do + before do + install_gemfile <<-G + ::RUBY_VERSION = '2.1.3' + ::RUBY_PATCHLEVEL = 100 + ruby '~> 2.1.0' + G + bundle "update --ruby" + end + + context "when the Gemfile removes the ruby" do + before do + install_gemfile <<-G + ::RUBY_VERSION = '2.1.4' + ::RUBY_PATCHLEVEL = 222 + G + end + it "removes the Ruby from the Gemfile.lock" do + bundle "update --ruby" + + lockfile_should_be <<-L + GEM + specs: + + PLATFORMS + ruby + + DEPENDENCIES + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when the Gemfile specified an updated Ruby version" do + before do + install_gemfile <<-G + ::RUBY_VERSION = '2.1.4' + ::RUBY_PATCHLEVEL = 222 + ruby '~> 2.1.0' + G + end + it "updates the Gemfile.lock with the latest version" do + bundle "update --ruby" + + lockfile_should_be <<-L + GEM + specs: + + PLATFORMS + ruby + + DEPENDENCIES + + RUBY VERSION + ruby 2.1.4p222 + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when a different Ruby is being used than has been versioned" do + before do + install_gemfile <<-G + ::RUBY_VERSION = '2.2.2' + ::RUBY_PATCHLEVEL = 505 + ruby '~> 2.1.0' + G + end + it "shows a helpful error message" do + bundle "update --ruby" + + expect(out).to include("Your Ruby version is 2.2.2, but your Gemfile specified ~> 2.1.0") + end + end + + context "when updating Ruby version and Gemfile `ruby`" do + before do + install_gemfile <<-G + ::RUBY_VERSION = '1.8.3' + ::RUBY_PATCHLEVEL = 55 + ruby '~> 1.8.0' + G + end + it "updates the Gemfile.lock with the latest version" do + bundle "update --ruby" + + lockfile_should_be <<-L + GEM + specs: + + PLATFORMS + ruby + + DEPENDENCIES + + RUBY VERSION + ruby 1.8.3p55 + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end +end + +# these specs are slow and focus on integration and therefore are not exhaustive. unit specs elsewhere handle that. +RSpec.describe "bundle update conservative" do + context "patch and minor options" do + before do + build_repo4 do + build_gem "foo", %w(1.4.3 1.4.4) do |s| + s.add_dependency "bar", "~> 2.0" + end + build_gem "foo", %w(1.4.5 1.5.0) do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "foo", %w(1.5.1) do |s| + s.add_dependency "bar", "~> 3.0" + end + build_gem "bar", %w(2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0) + build_gem "qux", %w(1.0.0 1.0.1 1.1.0 2.0.0) + end + + # establish a lockfile set to 1.4.3 + install_gemfile <<-G + source "file://#{gem_repo4}" + gem 'foo', '1.4.3' + gem 'bar', '2.0.3' + gem 'qux', '1.0.0' + G + + # remove 1.4.3 requirement and bar altogether + # to setup update specs below + gemfile <<-G + source "file://#{gem_repo4}" + gem 'foo' + gem 'qux' + G + end + + context "patch preferred" do + it "single gem updates dependent gem to minor" do + bundle "update --patch foo" + + expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.0" + end + + it "update all" do + bundle "update --patch" + + expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.1" + end + end + + context "minor preferred" do + it "single gem updates dependent gem to major" do + bundle "update --minor foo" + + expect(the_bundle).to include_gems "foo 1.5.1", "bar 3.0.0", "qux 1.0.0" + end + end + + context "strict" do + it "patch preferred" do + bundle "update --patch foo bar --strict" + + expect(the_bundle).to include_gems "foo 1.4.4", "bar 2.0.5", "qux 1.0.0" + end + + it "minor preferred" do + bundle "update --minor --strict" + + expect(the_bundle).to include_gems "foo 1.5.0", "bar 2.1.1", "qux 1.1.0" + end + end + end + + context "eager unlocking" do + before do + build_repo4 do + build_gem "isolated_owner", %w(1.0.1 1.0.2) do |s| + s.add_dependency "isolated_dep", "~> 2.0" + end + build_gem "isolated_dep", %w(2.0.1 2.0.2) + + build_gem "shared_owner_a", %w(3.0.1 3.0.2) do |s| + s.add_dependency "shared_dep", "~> 5.0" + end + build_gem "shared_owner_b", %w(4.0.1 4.0.2) do |s| + s.add_dependency "shared_dep", "~> 5.0" + end + build_gem "shared_dep", %w(5.0.1 5.0.2) + end + + 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 eagerly unlock isolated dependency" do + bundle "update isolated_owner" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.2", "shared_dep 5.0.1", "shared_owner_a 3.0.1", "shared_owner_b 4.0.1" + end + + it "should eagerly unlock shared dependency" do + bundle "update shared_owner_a" + + expect(the_bundle).to include_gems "isolated_owner 1.0.1", "isolated_dep 2.0.1", "shared_dep 5.0.2", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + end + + it "should not eagerly unlock with --conservative" do + bundle "update --conservative shared_owner_a isolated_owner" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.2", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + end + + it "should match bundle install conservative update behavior when not eagerly unlocking" do + gemfile <<-G + source "file://#{gem_repo4}" + gem 'isolated_owner', '1.0.2' + + gem 'shared_owner_a', '3.0.2' + gem 'shared_owner_b' + G + + bundle "install" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.2", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + end + end + + context "error handling" do + before do + gemfile "" + end + + it "raises if too many flags are provided" do + bundle "update --patch --minor" + + expect(out).to eq "Provide only one of the following options: minor, patch" + end + end +end diff --git a/spec/bundler/commands/viz_spec.rb b/spec/bundler/commands/viz_spec.rb new file mode 100644 index 0000000000..77112aace4 --- /dev/null +++ b/spec/bundler/commands/viz_spec.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle viz", :ruby => "1.9.3", :if => Bundler.which("dot") do + let(:ruby_graphviz) do + graphviz_glob = base_system_gems.join("cache/ruby-graphviz*") + Pathname.glob(graphviz_glob).first + end + + before do + system_gems ruby_graphviz + end + + it "graphs gems from the Gemfile" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama" + G + + bundle! "viz" + expect(out).to include("gem_graph.png") + + bundle! "viz", :format => "debug" + expect(out).to eq(strip_whitespace(<<-DOT).strip) + digraph Gemfile { + concentrate = "true"; + normalize = "true"; + nodesep = "0.55"; + edge[ weight = "2"]; + node[ fontname = "Arial, Helvetica, SansSerif"]; + edge[ fontname = "Arial, Helvetica, SansSerif" , fontsize = "12"]; + default [style = "filled", fillcolor = "#B9B9D5", shape = "box3d", fontsize = "16", label = "default"]; + rack [style = "filled", fillcolor = "#B9B9D5", label = "rack"]; + default -> rack [constraint = "false"]; + "rack-obama" [style = "filled", fillcolor = "#B9B9D5", label = "rack-obama"]; + default -> "rack-obama" [constraint = "false"]; + "rack-obama" -> rack; + } + debugging bundle viz... + DOT + end + + it "graphs gems that are prereleases" do + build_repo2 do + build_gem "rack", "1.3.pre" + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack", "= 1.3.pre" + gem "rack-obama" + G + + bundle! "viz" + expect(out).to include("gem_graph.png") + + bundle! "viz", :format => :debug, :version => true + expect(out).to eq(strip_whitespace(<<-EOS).strip) + digraph Gemfile { + concentrate = "true"; + normalize = "true"; + nodesep = "0.55"; + edge[ weight = "2"]; + node[ fontname = "Arial, Helvetica, SansSerif"]; + edge[ fontname = "Arial, Helvetica, SansSerif" , fontsize = "12"]; + default [style = "filled", fillcolor = "#B9B9D5", shape = "box3d", fontsize = "16", label = "default"]; + rack [style = "filled", fillcolor = "#B9B9D5", label = "rack\\n1.3.pre"]; + default -> rack [constraint = "false"]; + "rack-obama" [style = "filled", fillcolor = "#B9B9D5", label = "rack-obama\\n1.0"]; + default -> "rack-obama" [constraint = "false"]; + "rack-obama" -> rack; + } + debugging bundle viz... + EOS + end + + context "with another gem that has a graphviz file" do + before do + build_repo4 do + build_gem "graphviz", "999" do |s| + s.write("lib/graphviz.rb", "abort 'wrong graphviz gem loaded'") + end + end + + system_gems ruby_graphviz, "graphviz-999", :gem_repo => gem_repo4 + end + + it "loads the correct ruby-graphviz gem" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama" + G + + bundle! "viz", :format => "debug" + expect(out).to eq(strip_whitespace(<<-DOT).strip) + digraph Gemfile { + concentrate = "true"; + normalize = "true"; + nodesep = "0.55"; + edge[ weight = "2"]; + node[ fontname = "Arial, Helvetica, SansSerif"]; + edge[ fontname = "Arial, Helvetica, SansSerif" , fontsize = "12"]; + default [style = "filled", fillcolor = "#B9B9D5", shape = "box3d", fontsize = "16", label = "default"]; + rack [style = "filled", fillcolor = "#B9B9D5", label = "rack"]; + default -> rack [constraint = "false"]; + "rack-obama" [style = "filled", fillcolor = "#B9B9D5", label = "rack-obama"]; + default -> "rack-obama" [constraint = "false"]; + "rack-obama" -> rack; + } + debugging bundle viz... + DOT + end + end + + context "--without option" do + it "one group" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + + group :rails do + gem "rails" + end + G + + bundle! "viz --without=rails" + expect(out).to include("gem_graph.png") + end + + it "two groups" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + + group :rack do + gem "rack" + end + + group :rails do + gem "rails" + end + G + + bundle! "viz --without=rails:rack" + expect(out).to include("gem_graph.png") + end + end +end diff --git a/spec/bundler/install/allow_offline_install_spec.rb b/spec/bundler/install/allow_offline_install_spec.rb new file mode 100644 index 0000000000..1bca055c9f --- /dev/null +++ b/spec/bundler/install/allow_offline_install_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install with :allow_offline_install" do + before do + bundle "config allow_offline_install true" + end + + context "with no cached data locally" do + it "still installs" do + install_gemfile! <<-G, :artifice => "compact_index" + source "http://testgemserver.local" + gem "rack-obama" + G + expect(the_bundle).to include_gem("rack 1.0") + end + + it "still fails when the network is down" do + install_gemfile <<-G, :artifice => "fail" + source "http://testgemserver.local" + gem "rack-obama" + G + expect(out).to include("Could not reach host testgemserver.local.") + expect(the_bundle).to_not be_locked + end + end + + context "with cached data locally" do + it "will install from the compact index" do + system_gems ["rack-1.0.0"] + + install_gemfile! <<-G, :artifice => "compact_index" + source "http://testgemserver.local" + gem "rack-obama" + gem "rack", "< 1.0" + G + + expect(the_bundle).to include_gems("rack-obama 1.0", "rack 0.9.1") + + gemfile <<-G + source "http://testgemserver.local" + gem "rack-obama" + G + + bundle! :update, :artifice => "fail" + expect(out).to include("Using the cached data for the new index because of a network error") + + expect(the_bundle).to include_gems("rack-obama 1.0", "rack 1.0.0") + end + + def break_git_remote_ops! + FileUtils.mkdir_p(tmp("broken_path")) + File.open(tmp("broken_path/git"), "w", 0o755) do |f| + f.puts strip_whitespace(<<-RUBY) + #!/usr/bin/env ruby + if %w(fetch --force --quiet --tags refs/heads/*:refs/heads/*).-(ARGV).empty? || %w(clone --bare --no-hardlinks --quiet).-(ARGV).empty? + warn "git remote ops have been disabled" + exit 1 + end + ENV["PATH"] = ENV["PATH"].sub(/^.*?:/, "") + exec("git", *ARGV) + RUBY + end + + old_path = ENV["PATH"] + ENV["PATH"] = "#{tmp("broken_path")}:#{ENV["PATH"]}" + yield if block_given? + ensure + ENV["PATH"] = old_path if block_given? + end + + it "will install from a cached git repo" do + git = build_git "a", "1.0.0", :path => lib_path("a") + update_git("a", :path => git.path, :branch => "new_branch") + install_gemfile! <<-G + gem "a", :git => #{git.path.to_s.dump} + G + + break_git_remote_ops! { bundle! :update } + expect(out).to include("Using cached git data because of network errors") + expect(the_bundle).to be_locked + + break_git_remote_ops! do + install_gemfile! <<-G + gem "a", :git => #{git.path.to_s.dump}, :branch => "new_branch" + G + end + expect(out).to include("Using cached git data because of network errors") + expect(the_bundle).to be_locked + end + end +end diff --git a/spec/bundler/install/binstubs_spec.rb b/spec/bundler/install/binstubs_spec.rb new file mode 100644 index 0000000000..a1a9ab167d --- /dev/null +++ b/spec/bundler/install/binstubs_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install" do + describe "when system_bindir is set" do + # On OS X, Gem.bindir defaults to /usr/bin, so system_bindir is useful if + # you want to avoid sudo installs for system gems with OS X's default ruby + it "overrides Gem.bindir" do + expect(Pathname.new("/usr/bin")).not_to be_writable unless Process.euid == 0 + gemfile <<-G + require 'rubygems' + def Gem.bindir; "/usr/bin"; end + source "file://#{gem_repo1}" + gem "rack" + G + + config "BUNDLE_SYSTEM_BINDIR" => system_gem_path("altbin").to_s + bundle :install + expect(the_bundle).to include_gems "rack 1.0.0" + expect(system_gem_path("altbin/rackup")).to exist + end + end + + describe "when multiple gems contain the same exe" do + before do + build_repo2 do + build_gem "fake", "14" do |s| + s.executables = "rackup" + end + end + + install_gemfile <<-G, :binstubs => true + source "file://#{gem_repo2}" + gem "fake" + gem "rack" + G + end + + it "prints a deprecation notice" do + bundle "config major_deprecations true" + gembin("rackup") + expect(out).to include("Bundler is using a binstub that was created for a different gem.") + end + + it "loads the correct spec's executable" do + gembin("rackup") + expect(out).to eq("1.2") + end + end +end diff --git a/spec/bundler/install/bundler_spec.rb b/spec/bundler/install/bundler_spec.rb new file mode 100644 index 0000000000..c1ce57e60e --- /dev/null +++ b/spec/bundler/install/bundler_spec.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install" do + describe "with bundler dependencies" do + before(:each) do + build_repo2 do + build_gem "rails", "3.0" do |s| + s.add_dependency "bundler", ">= 0.9.0.pre" + end + build_gem "bundler", "0.9.1" + build_gem "bundler", Bundler::VERSION + end + end + + it "are forced to the current bundler version" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rails", "3.0" + G + + expect(the_bundle).to include_gems "bundler #{Bundler::VERSION}" + end + + it "are not added if not already present" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + expect(the_bundle).not_to include_gems "bundler #{Bundler::VERSION}" + end + + it "causes a conflict if explicitly requesting a different version" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rails", "3.0" + gem "bundler", "0.9.2" + G + + nice_error = <<-E.strip.gsub(/^ {8}/, "") + Fetching source index from file:#{gem_repo2}/ + Resolving dependencies... + Bundler could not find compatible versions for gem "bundler": + In Gemfile: + bundler (= 0.9.2) + + Current Bundler version: + bundler (#{Bundler::VERSION}) + This Gemfile requires a different version of Bundler. + Perhaps you need to update Bundler by running `gem install bundler`? + + Could not find gem 'bundler (= 0.9.2)' in any of the sources + E + expect(out).to eq(nice_error) + end + + it "works for gems with multiple versions in its dependencies" do + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "multiple_versioned_deps" + G + + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "multiple_versioned_deps" + gem "rack" + G + + expect(the_bundle).to include_gems "multiple_versioned_deps 1.0.0" + end + + it "includes bundler in the bundle when it's a child dependency" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rails", "3.0" + G + + run "begin; gem 'bundler'; puts 'WIN'; rescue Gem::LoadError; puts 'FAIL'; end" + expect(out).to eq("WIN") + end + + it "allows gem 'bundler' when Bundler is not in the Gemfile or its dependencies" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack" + G + + run "begin; gem 'bundler'; puts 'WIN'; rescue Gem::LoadError => e; puts e.backtrace; end" + expect(out).to eq("WIN") + end + + it "causes a conflict if child dependencies conflict" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activemerchant" + gem "rails_fail" + G + + nice_error = <<-E.strip.gsub(/^ {8}/, "") + Fetching source index from file:#{gem_repo2}/ + Resolving dependencies... + Bundler could not find compatible versions for gem "activesupport": + In Gemfile: + activemerchant was resolved to 1.0, which depends on + activesupport (>= 2.0.0) + + rails_fail was resolved to 1.0, which depends on + activesupport (= 1.2.3) + E + expect(out).to include(nice_error) + end + + it "causes a conflict if a child dependency conflicts with the Gemfile" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rails_fail" + gem "activesupport", "2.3.5" + G + + nice_error = <<-E.strip.gsub(/^ {8}/, "") + Fetching source index from file:#{gem_repo2}/ + Resolving dependencies... + Bundler could not find compatible versions for gem "activesupport": + In Gemfile: + activesupport (= 2.3.5) + + rails_fail was resolved to 1.0, which depends on + activesupport (= 1.2.3) + E + expect(out).to include(nice_error) + end + + it "can install dependencies with newer bundler version" do + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rails", "3.0" + G + + simulate_bundler_version "10.0.0" + + bundle "check" + expect(out).to include("The Gemfile's dependencies are satisfied") + end + end +end diff --git a/spec/bundler/install/deploy_spec.rb b/spec/bundler/install/deploy_spec.rb new file mode 100644 index 0000000000..b66135c6b0 --- /dev/null +++ b/spec/bundler/install/deploy_spec.rb @@ -0,0 +1,301 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "install with --deployment or --frozen" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + it "fails without a lockfile and says that --deployment requires a lock" do + bundle "install --deployment" + expect(out).to include("The --deployment flag requires a Gemfile.lock") + end + + it "fails without a lockfile and says that --frozen requires a lock" do + bundle "install --frozen" + expect(out).to include("The --frozen flag requires a Gemfile.lock") + end + + it "disallows --deployment --system" do + bundle "install --deployment --system" + expect(out).to include("You have specified both --deployment") + expect(out).to include("Please choose only one option") + expect(exitstatus).to eq(15) if exitstatus + end + + it "disallows --deployment --path --system" do + bundle "install --deployment --path . --system" + expect(out).to include("You have specified both --path") + expect(out).to include("as well as --system") + expect(out).to include("Please choose only one option") + expect(exitstatus).to eq(15) if exitstatus + end + + it "works after you try to deploy without a lock" do + bundle "install --deployment" + bundle :install + expect(exitstatus).to eq(0) if exitstatus + expect(the_bundle).to include_gems "rack 1.0" + end + + it "still works if you are not in the app directory and specify --gemfile" do + bundle "install" + Dir.chdir tmp + simulate_new_machine + bundle "install --gemfile #{tmp}/bundled_app/Gemfile --deployment" + Dir.chdir bundled_app + expect(the_bundle).to include_gems "rack 1.0" + end + + it "works if you exclude a group with a git gem" do + build_git "foo" + gemfile <<-G + group :test do + gem "foo", :git => "#{lib_path("foo-1.0")}" + end + G + bundle :install + bundle "install --deployment --without test" + expect(exitstatus).to eq(0) if exitstatus + end + + it "works when you bundle exec bundle", :ruby_repo do + bundle :install + bundle "install --deployment" + bundle "exec bundle check" + expect(exitstatus).to eq(0) if exitstatus + end + + it "works when using path gems from the same path and the version is specified" do + build_lib "foo", :path => lib_path("nested/foo") + build_lib "bar", :path => lib_path("nested/bar") + gemfile <<-G + gem "foo", "1.0", :path => "#{lib_path("nested")}" + gem "bar", :path => "#{lib_path("nested")}" + G + + bundle! :install + bundle! "install --deployment" + end + + it "works when there are credentials in the source URL" do + install_gemfile(<<-G, :artifice => "endpoint_strict_basic_authentication", :quiet => true) + source "http://user:pass@localgemserver.test/" + + gem "rack-obama", ">= 1.0" + G + + bundle "install --deployment", :artifice => "endpoint_strict_basic_authentication" + + expect(exitstatus).to eq(0) if exitstatus + end + + it "works with sources given by a block" do + install_gemfile <<-G + source "file://#{gem_repo1}" do + gem "rack" + end + G + + bundle "install --deployment" + + expect(exitstatus).to eq(0) if exitstatus + expect(the_bundle).to include_gems "rack 1.0" + end + + describe "with an existing lockfile" do + before do + bundle "install" + end + + it "works with the --deployment flag if you didn't change anything" do + bundle "install --deployment" + expect(exitstatus).to eq(0) if exitstatus + end + + it "works with the --frozen flag if you didn't change anything" do + bundle "install --frozen" + expect(exitstatus).to eq(0) if exitstatus + end + + it "explodes with the --deployment flag if you make a change and don't check in the lockfile" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama" + G + + bundle "install --deployment" + expect(out).to include("deployment mode") + expect(out).to include("You have added to the Gemfile") + expect(out).to include("* rack-obama") + expect(out).not_to include("You have deleted from the Gemfile") + expect(out).not_to include("You have changed in the Gemfile") + end + + it "can have --frozen set via an environment variable" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama" + G + + ENV["BUNDLE_FROZEN"] = "1" + bundle "install" + expect(out).to include("deployment mode") + expect(out).to include("You have added to the Gemfile") + expect(out).to include("* rack-obama") + expect(out).not_to include("You have deleted from the Gemfile") + expect(out).not_to include("You have changed in the Gemfile") + end + + it "can have --frozen set to false via an environment variable" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama" + G + + ENV["BUNDLE_FROZEN"] = "false" + bundle "install" + expect(out).not_to include("deployment mode") + expect(out).not_to include("You have added to the Gemfile") + expect(out).not_to include("* rack-obama") + end + + it "explodes with the --frozen flag if you make a change and don't check in the lockfile" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama", "1.1" + G + + bundle "install --frozen" + expect(out).to include("deployment mode") + expect(out).to include("You have added to the Gemfile") + expect(out).to include("* rack-obama (= 1.1)") + expect(out).not_to include("You have deleted from the Gemfile") + expect(out).not_to include("You have changed in the Gemfile") + end + + it "explodes if you remove a gem and don't check in the lockfile" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + G + + bundle "install --deployment" + expect(out).to include("deployment mode") + expect(out).to include("You have added to the Gemfile:\n* activesupport\n\n") + expect(out).to include("You have deleted from the Gemfile:\n* rack") + expect(out).not_to include("You have changed in the Gemfile") + end + + it "explodes if you add a source" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "git://hubz.com" + G + + bundle "install --deployment" + expect(out).to include("deployment mode") + expect(out).to include("You have added to the Gemfile:\n* source: git://hubz.com (at master)") + expect(out).not_to include("You have changed in the Gemfile") + end + + it "explodes if you unpin a source" do + build_git "rack" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-1.0")}" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "install --deployment" + expect(out).to include("deployment mode") + expect(out).to include("You have deleted from the Gemfile:\n* source: #{lib_path("rack-1.0")} (at master@#{revision_for(lib_path("rack-1.0"))[0..6]}") + expect(out).not_to include("You have added to the Gemfile") + expect(out).not_to include("You have changed in the Gemfile") + end + + it "explodes if you unpin a source, leaving it pinned somewhere else" do + build_lib "foo", :path => lib_path("rack/foo") + build_git "rack", :path => lib_path("rack") + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack")}" + gem "foo", :git => "#{lib_path("rack")}" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "foo", :git => "#{lib_path("rack")}" + G + + bundle "install --deployment" + expect(out).to include("deployment mode") + expect(out).to include("You have changed in the Gemfile:\n* rack from `no specified source` to `#{lib_path("rack")} (at master@#{revision_for(lib_path("rack"))[0..6]})`") + expect(out).not_to include("You have added to the Gemfile") + expect(out).not_to include("You have deleted from the Gemfile") + end + + it "remembers that the bundle is frozen at runtime" do + bundle "install --deployment" + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0.0" + gem "rack-obama" + G + + expect(the_bundle).not_to include_gems "rack 1.0.0" + expect(err).to include strip_whitespace(<<-E).strip +The dependencies in your gemfile changed + +You have added to the Gemfile: +* rack (= 1.0.0) +* rack-obama + +You have deleted from the Gemfile: +* rack + E + end + end + + context "with path in Gemfile and packed" do + it "works fine after bundle package and bundle install --local" do + build_lib "foo", :path => lib_path("foo") + install_gemfile! <<-G + gem "foo", :path => "#{lib_path("foo")}" + G + + bundle! :install + expect(the_bundle).to include_gems "foo 1.0" + bundle! "package --all" + expect(bundled_app("vendor/cache/foo")).to be_directory + + bundle! "install --local" + expect(out).to include("Using foo 1.0 from source at") + expect(out).to include("vendor/cache/foo") + + simulate_new_machine + bundle! "install --deployment --verbose" + expect(out).not_to include("You are trying to install in deployment mode after changing your Gemfile") + expect(out).not_to include("You have added to the Gemfile") + expect(out).not_to include("You have deleted from the Gemfile") + expect(out).to include("Using foo 1.0 from source at") + expect(out).to include("vendor/cache/foo") + expect(the_bundle).to include_gems "foo 1.0" + end + end +end diff --git a/spec/bundler/install/failure_spec.rb b/spec/bundler/install/failure_spec.rb new file mode 100644 index 0000000000..738b2cf1bd --- /dev/null +++ b/spec/bundler/install/failure_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install" do + context "installing a gem fails" do + it "prints out why that gem was being installed" do + build_repo2 do + build_gem "activesupport", "2.3.2" do |s| + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + abort "make installing activesupport-2.3.2 fail" + end + RUBY + end + end + + install_gemfile <<-G + source "file:#{gem_repo2}" + gem "rails" + G + expect(out).to end_with(<<-M.strip) +An error occurred while installing activesupport (2.3.2), and Bundler cannot continue. +Make sure that `gem install activesupport -v '2.3.2'` succeeds before bundling. + +In Gemfile: + rails was resolved to 2.3.2, which depends on + actionmailer was resolved to 2.3.2, which depends on + activesupport + M + end + end +end diff --git a/spec/bundler/install/force_spec.rb b/spec/bundler/install/force_spec.rb new file mode 100644 index 0000000000..dc4956a7ae --- /dev/null +++ b/spec/bundler/install/force_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install" do + describe "with --force" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + it "re-installs installed gems" do + rack_lib = default_bundle_path("gems/rack-1.0.0/lib/rack.rb") + + bundle "install" + rack_lib.open("w") {|f| f.write("blah blah blah") } + bundle "install --force" + + expect(exitstatus).to eq(0) if exitstatus + expect(out).to include "Using bundler" + expect(out).to include "Installing rack 1.0.0" + expect(rack_lib.open(&:read)).to eq("RACK = '1.0.0'\n") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "works on first bundle install" do + bundle "install --force" + + expect(exitstatus).to eq(0) if exitstatus + expect(out).to include "Using bundler" + expect(out).to include "Installing rack 1.0.0" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + context "with a git gem" do + let!(:ref) { build_git("foo", "1.0").ref_for("HEAD", 11) } + + before do + gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + end + + it "re-installs installed gems" do + foo_lib = default_bundle_path("bundler/gems/foo-1.0-#{ref}/lib/foo.rb") + + bundle! "install" + foo_lib.open("w") {|f| f.write("blah blah blah") } + bundle! "install --force" + + expect(out).to include "Using bundler" + expect(out).to include "Using foo 1.0 from #{lib_path("foo-1.0")} (at master@#{ref[0, 7]})" + expect(foo_lib.open(&:read)).to eq("FOO = '1.0'\n") + expect(the_bundle).to include_gems "foo 1.0" + end + + it "works on first bundle install" do + bundle! "install --force" + + expect(out).to include "Using bundler" + expect(out).to include "Using foo 1.0 from #{lib_path("foo-1.0")} (at master@#{ref[0, 7]})" + expect(the_bundle).to include_gems "foo 1.0" + end + end + end +end diff --git a/spec/bundler/install/gemfile/eval_gemfile_spec.rb b/spec/bundler/install/gemfile/eval_gemfile_spec.rb new file mode 100644 index 0000000000..f02223d34d --- /dev/null +++ b/spec/bundler/install/gemfile/eval_gemfile_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install with gemfile that uses eval_gemfile" do + before do + build_lib("gunks", :path => bundled_app.join("gems/gunks")) do |s| + s.name = "gunks" + s.version = "0.0.1" + end + end + + context "eval-ed Gemfile points to an internal gemspec" do + before do + create_file "Gemfile-other", <<-G + gemspec :path => 'gems/gunks' + G + end + + it "installs the gemspec specified gem" do + install_gemfile <<-G + eval_gemfile 'Gemfile-other' + G + expect(out).to include("Resolving dependencies") + expect(out).to include("Using gunks 0.0.1 from source at `gems/gunks`") + expect(out).to include("Bundle complete") + end + end + + context "eval-ed Gemfile has relative-path gems" do + before do + build_lib("a", :path => "gems/a") + create_file "nested/Gemfile-nested", <<-G + gem "a", :path => "../gems/a" + G + + gemfile <<-G + eval_gemfile "nested/Gemfile-nested" + G + end + + it "installs the path gem" do + bundle! :install + expect(the_bundle).to include_gem("a 1.0") + end + + # Make sure that we are properly comparing path based gems between the + # parsed lockfile and the evaluated gemfile. + it "bundles with --deployment" do + bundle! :install + bundle! "install --deployment" + end + end + + context "Gemfile uses gemspec paths after eval-ing a Gemfile" do + before { create_file "other/Gemfile-other" } + + it "installs the gemspec specified gem" do + install_gemfile <<-G + eval_gemfile 'other/Gemfile-other' + gemspec :path => 'gems/gunks' + G + expect(out).to include("Resolving dependencies") + expect(out).to include("Using gunks 0.0.1 from source at `gems/gunks`") + expect(out).to include("Bundle complete") + end + end +end diff --git a/spec/bundler/install/gemfile/gemspec_spec.rb b/spec/bundler/install/gemfile/gemspec_spec.rb new file mode 100644 index 0000000000..1ea613c9d2 --- /dev/null +++ b/spec/bundler/install/gemfile/gemspec_spec.rb @@ -0,0 +1,563 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install from an existing gemspec" do + before(:each) do + build_gem "bar", :to_system => true + build_gem "bar-dev", :to_system => true + end + + it "should install runtime and development dependencies" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.write("Gemfile", "source :rubygems\ngemspec") + s.add_dependency "bar", "=1.0.0" + s.add_development_dependency "bar-dev", "=1.0.0" + end + install_gemfile <<-G + source "file://#{gem_repo2}" + gemspec :path => '#{tmp.join("foo")}' + G + + expect(the_bundle).to include_gems "bar 1.0.0" + expect(the_bundle).to include_gems "bar-dev 1.0.0", :groups => :development + end + + it "that is hidden should install runtime and development dependencies" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.write("Gemfile", "source :rubygems\ngemspec") + s.add_dependency "bar", "=1.0.0" + s.add_development_dependency "bar-dev", "=1.0.0" + end + FileUtils.mv tmp.join("foo", "foo.gemspec"), tmp.join("foo", ".gemspec") + + install_gemfile <<-G + source "file://#{gem_repo2}" + gemspec :path => '#{tmp.join("foo")}' + G + + expect(the_bundle).to include_gems "bar 1.0.0" + expect(the_bundle).to include_gems "bar-dev 1.0.0", :groups => :development + end + + it "should handle a list of requirements" do + build_gem "baz", "1.0", :to_system => true + build_gem "baz", "1.1", :to_system => true + + build_lib("foo", :path => tmp.join("foo")) do |s| + s.write("Gemfile", "source :rubygems\ngemspec") + s.add_dependency "baz", ">= 1.0", "< 1.1" + end + install_gemfile <<-G + source "file://#{gem_repo2}" + gemspec :path => '#{tmp.join("foo")}' + G + + expect(the_bundle).to include_gems "baz 1.0" + end + + it "should raise if there are no gemspecs available" do + build_lib("foo", :path => tmp.join("foo"), :gemspec => false) + + error = install_gemfile(<<-G) + source "file://#{gem_repo2}" + gemspec :path => '#{tmp.join("foo")}' + G + expect(error).to match(/There are no gemspecs at #{tmp.join('foo')}/) + end + + it "should raise if there are too many gemspecs available" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.write("foo2.gemspec", build_spec("foo", "4.0").first.to_ruby) + end + + error = install_gemfile(<<-G) + source "file://#{gem_repo2}" + gemspec :path => '#{tmp.join("foo")}' + G + expect(error).to match(/There are multiple gemspecs at #{tmp.join('foo')}/) + end + + it "should pick a specific gemspec" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.write("foo2.gemspec", "") + s.add_dependency "bar", "=1.0.0" + s.add_development_dependency "bar-dev", "=1.0.0" + end + + install_gemfile(<<-G) + source "file://#{gem_repo2}" + gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + G + + expect(the_bundle).to include_gems "bar 1.0.0" + expect(the_bundle).to include_gems "bar-dev 1.0.0", :groups => :development + end + + it "should use a specific group for development dependencies" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.write("foo2.gemspec", "") + s.add_dependency "bar", "=1.0.0" + s.add_development_dependency "bar-dev", "=1.0.0" + end + + install_gemfile(<<-G) + source "file://#{gem_repo2}" + gemspec :path => '#{tmp.join("foo")}', :name => 'foo', :development_group => :dev + G + + expect(the_bundle).to include_gems "bar 1.0.0" + expect(the_bundle).not_to include_gems "bar-dev 1.0.0", :groups => :development + expect(the_bundle).to include_gems "bar-dev 1.0.0", :groups => :dev + end + + it "should match a lockfile even if the gemspec defines development dependencies" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.write("Gemfile", "source 'file://#{gem_repo1}'\ngemspec") + s.add_dependency "actionpack", "=2.3.2" + s.add_development_dependency "rake", "=10.0.2" + end + + Dir.chdir(tmp.join("foo")) do + bundle "install" + # This should really be able to rely on $stderr, but, it's not written + # right, so we can't. In fact, this is a bug negation test, and so it'll + # ghost pass in future, and will only catch a regression if the message + # doesn't change. Exit codes should be used correctly (they can be more + # than just 0 and 1). + output = bundle("install --deployment") + expect(output).not_to match(/You have added to the Gemfile/) + expect(output).not_to match(/You have deleted from the Gemfile/) + expect(output).not_to match(/install in deployment mode after changing/) + end + end + + it "should match a lockfile without needing to re-resolve" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.add_dependency "rack" + end + + install_gemfile! <<-G + source "file://#{gem_repo1}" + gemspec :path => '#{tmp.join("foo")}' + G + + bundle! "install", :verbose => true + expect(out).to include("Found no changes, using resolution from the lockfile") + end + + it "should match a lockfile without needing to re-resolve with development dependencies" do + simulate_platform java + + build_lib("foo", :path => tmp.join("foo")) do |s| + s.add_dependency "rack" + s.add_development_dependency "thin" + end + + install_gemfile! <<-G + source "file://#{gem_repo1}" + gemspec :path => '#{tmp.join("foo")}' + G + + bundle! "install", :verbose => true + expect(out).to include("Found no changes, using resolution from the lockfile") + end + + it "should match a lockfile on non-ruby platforms with a transitive platform dependency" do + simulate_platform java + simulate_ruby_engine "jruby" + + build_lib("foo", :path => tmp.join("foo")) do |s| + s.add_dependency "platform_specific" + end + + install_gem "platform_specific-1.0-java" + + install_gemfile! <<-G + gemspec :path => '#{tmp.join("foo")}' + G + + bundle! "update --bundler", :verbose => true + expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 JAVA" + end + + it "should evaluate the gemspec in its directory" do + build_lib("foo", :path => tmp.join("foo")) + File.open(tmp.join("foo/foo.gemspec"), "w") do |s| + s.write "raise 'ahh' unless Dir.pwd == '#{tmp.join("foo")}'" + end + + install_gemfile <<-G + gemspec :path => '#{tmp.join("foo")}' + G + expect(@err).not_to match(/ahh/) + end + + it "allows the gemspec to activate other gems" do + # see https://github.com/bundler/bundler/issues/5409 + # + # issue was caused by rubygems having an unresolved gem during a require, + # so emulate that + system_gems %w(rack-1.0.0 rack-0.9.1 rack-obama-1.0) + + build_lib("foo", :path => bundled_app) + gemspec = bundled_app("foo.gemspec").read + bundled_app("foo.gemspec").open("w") do |f| + f.write "#{gemspec.strip}.tap { gem 'rack-obama'; require 'rack-obama' }" + end + + install_gemfile! <<-G + gemspec + G + + expect(the_bundle).to include_gem "foo 1.0" + end + + it "allows conflicts" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.version = "1.0.0" + s.add_dependency "bar", "= 1.0.0" + end + build_gem "deps", :to_system => true do |s| + s.add_dependency "foo", "= 0.0.1" + end + build_gem "foo", "0.0.1", :to_system => true + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "deps" + gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + G + + expect(the_bundle).to include_gems "foo 1.0.0" + end + + it "does not break Gem.finish_resolve with conflicts", :rubygems => ">= 2" do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.version = "1.0.0" + s.add_dependency "bar", "= 1.0.0" + end + build_repo2 do + build_gem "deps" do |s| + s.add_dependency "foo", "= 0.0.1" + end + build_gem "foo", "0.0.1" + end + + install_gemfile! <<-G + source "file://#{gem_repo2}" + gem "deps" + gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + G + + expect(the_bundle).to include_gems "foo 1.0.0" + + run! "Gem.finish_resolve; puts 'WIN'" + expect(out).to eq("WIN") + end + + context "in deployment mode" do + context "when the lockfile was not updated after a change to the gemspec's dependencies" do + it "reports that installation failed" do + build_lib "cocoapods", :path => bundled_app do |s| + s.add_dependency "activesupport", ">= 1" + end + + install_gemfile! <<-G + source "file://#{gem_repo1}" + gemspec + G + + expect(the_bundle).to include_gems("cocoapods 1.0", "activesupport 2.3.5") + + build_lib "cocoapods", :path => bundled_app do |s| + s.add_dependency "activesupport", ">= 1.0.1" + end + + bundle "install --deployment" + + expect(out).to include("changed") + end + end + end + + context "when child gemspecs conflict with a released gemspec" do + before do + # build the "parent" gem that depends on another gem in the same repo + build_lib "source_conflict", :path => bundled_app do |s| + s.add_dependency "rack_middleware" + end + + # build the "child" gem that is the same version as a released gem, but + # has completely different and conflicting dependency requirements + build_lib "rack_middleware", "1.0", :path => bundled_app("rack_middleware") do |s| + s.add_dependency "rack", "1.0" # anything other than 0.9.1 + end + end + + it "should install the child gemspec's deps" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gemspec + G + + expect(the_bundle).to include_gems "rack 1.0" + end + end + + context "with a lockfile and some missing dependencies" do + let(:source_uri) { "http://localgemserver.test" } + + context "previously bundled for Ruby" do + let(:platform) { "ruby" } + let(:explicit_platform) { false } + + before do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.add_dependency "rack", "=1.0.0" + end + + if explicit_platform + create_file( + tmp.join("foo", "foo-#{platform}.gemspec"), + build_spec("foo", "1.0", platform) do + dep "rack", "=1.0.0" + @spec.authors = "authors" + @spec.summary = "summary" + end.first.to_ruby + ) + end + + gemfile <<-G + source "#{source_uri}" + gemspec :path => "../foo" + G + + lockfile <<-L + PATH + remote: ../foo + specs: + foo (1.0) + rack (= 1.0.0) + + GEM + remote: #{source_uri} + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + context "using JRuby with explicit platform" do + let(:platform) { "java" } + let(:explicit_platform) { true } + + it "should install" do + simulate_ruby_engine "jruby" do + simulate_platform "java" do + results = bundle "install", :artifice => "endpoint" + expect(results).to include("Installing rack 1.0.0") + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + end + end + + context "using JRuby" do + let(:platform) { "java" } + + it "should install" do + simulate_ruby_engine "jruby" do + simulate_platform "java" do + results = bundle "install", :artifice => "endpoint" + expect(results).to include("Installing rack 1.0.0") + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + end + end + + context "using Windows" do + it "should install" do + simulate_windows do + results = bundle "install", :artifice => "endpoint" + expect(results).to include("Installing rack 1.0.0") + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + end + end + + context "bundled for ruby and jruby" do + let(:platform_specific_type) { :runtime } + let(:dependency) { "platform_specific" } + before do + build_repo2 do + build_gem "indirect_platform_specific" do |s| + s.add_runtime_dependency "platform_specific" + end + end + + build_lib "foo", :path => "." do |s| + if platform_specific_type == :runtime + s.add_runtime_dependency dependency + elsif platform_specific_type == :development + s.add_development_dependency dependency + else + raise "wrong dependency type #{platform_specific_type}, can only be :development or :runtime" + end + end + + %w(ruby jruby).each do |platform| + simulate_platform(platform) do + install_gemfile <<-G + source "file://#{gem_repo2}" + gemspec + G + end + end + end + + context "on ruby" do + before do + simulate_platform("ruby") + bundle :install + end + + context "as a runtime dependency" do + it "keeps java dependencies in the lockfile" do + expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 RUBY" + expect(lockfile).to eq strip_whitespace(<<-L) + PATH + remote: . + specs: + foo (1.0) + platform_specific + + GEM + remote: file:#{gem_repo2}/ + specs: + platform_specific (1.0) + platform_specific (1.0-java) + + PLATFORMS + java + ruby + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "as a development dependency" do + let(:platform_specific_type) { :development } + + it "keeps java dependencies in the lockfile" do + expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 RUBY" + expect(lockfile).to eq strip_whitespace(<<-L) + PATH + remote: . + specs: + foo (1.0) + + GEM + remote: file:#{gem_repo2}/ + specs: + platform_specific (1.0) + platform_specific (1.0-java) + + PLATFORMS + java + ruby + + DEPENDENCIES + foo! + platform_specific + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "with an indirect platform-specific development dependency" do + let(:platform_specific_type) { :development } + let(:dependency) { "indirect_platform_specific" } + + it "keeps java dependencies in the lockfile" do + expect(the_bundle).to include_gems "foo 1.0", "indirect_platform_specific 1.0", "platform_specific 1.0 RUBY" + expect(lockfile).to eq strip_whitespace(<<-L) + PATH + remote: . + specs: + foo (1.0) + + GEM + remote: file:#{gem_repo2}/ + specs: + indirect_platform_specific (1.0) + platform_specific + platform_specific (1.0) + platform_specific (1.0-java) + + PLATFORMS + java + ruby + + DEPENDENCIES + foo! + indirect_platform_specific + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + end + end + end + + context "with multiple platforms" do + before do + build_lib("foo", :path => tmp.join("foo")) do |s| + s.version = "1.0.0" + s.add_development_dependency "rack" + s.write "foo-universal-java.gemspec", build_spec("foo", "1.0.0", "universal-java") {|sj| sj.runtime "rack", "1.0.0" }.first.to_ruby + end + end + + it "installs the ruby platform gemspec" do + simulate_platform "ruby" + + install_gemfile! <<-G + source "file://#{gem_repo1}" + gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + G + + expect(the_bundle).to include_gems "foo 1.0.0", "rack 1.0.0" + end + + it "installs the ruby platform gemspec and skips dev deps with --without development" do + simulate_platform "ruby" + + install_gemfile! <<-G, :without => "development" + source "file://#{gem_repo1}" + gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + G + + expect(the_bundle).to include_gem "foo 1.0.0" + expect(the_bundle).not_to include_gem "rack" + end + end +end diff --git a/spec/bundler/install/gemfile/git_spec.rb b/spec/bundler/install/gemfile/git_spec.rb new file mode 100644 index 0000000000..5868c76570 --- /dev/null +++ b/spec/bundler/install/gemfile/git_spec.rb @@ -0,0 +1,1259 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install with git sources" do + describe "when floating on master" do + before :each do + build_git "foo" do |s| + s.executables = "foobar" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + end + + it "fetches gems" do + expect(the_bundle).to include_gems("foo 1.0") + + run <<-RUBY + require 'foo' + puts "WIN" unless defined?(FOO_PREV_REF) + RUBY + + expect(out).to eq("WIN") + end + + it "caches the git repo" do + expect(Dir["#{default_bundle_path}/cache/bundler/git/foo-1.0-*"].size).to eq(1) + end + + it "caches the evaluated gemspec" do + git = update_git "foo" do |s| + s.executables = ["foobar"] # we added this the first time, so keep it now + s.files = ["bin/foobar"] # updating git nukes the files list + foospec = s.to_ruby.gsub(/s\.files.*/, 's.files = `git ls-files -z`.split("\x0")') + s.write "foo.gemspec", foospec + end + + bundle "update foo" + + sha = git.ref_for("master", 11) + spec_file = default_bundle_path.join("bundler/gems/foo-1.0-#{sha}/foo.gemspec").to_s + ruby_code = Gem::Specification.load(spec_file).to_ruby + file_code = File.read(spec_file) + expect(file_code).to eq(ruby_code) + end + + it "does not update the git source implicitly" do + update_git "foo" + + in_app_root2 do + install_gemfile bundled_app2("Gemfile"), <<-G + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + end + + in_app_root do + run <<-RUBY + require 'foo' + puts "fail" if defined?(FOO_PREV_REF) + RUBY + + expect(out).to be_empty + end + end + + it "sets up git gem executables on the path" do + bundle "exec foobar" + expect(out).to eq("1.0") + end + + it "complains if pinned specs don't exist in the git repo" do + build_git "foo" + + install_gemfile <<-G + gem "foo", "1.1", :git => "#{lib_path("foo-1.0")}" + G + + expect(out).to include("Source contains 'foo' at: 1.0 ruby") + end + + it "complains with version and platform if pinned specs don't exist in the git repo" do + simulate_platform "java" + + build_git "only_java" do |s| + s.platform = "java" + end + + install_gemfile <<-G + platforms :jruby do + gem "only_java", "1.2", :git => "#{lib_path("only_java-1.0-java")}" + end + G + + expect(out).to include("Source contains 'only_java' at: 1.0 java") + end + + it "complains with multiple versions and platforms if pinned specs don't exist in the git repo" do + simulate_platform "java" + + build_git "only_java", "1.0" do |s| + s.platform = "java" + end + + build_git "only_java", "1.1" do |s| + s.platform = "java" + s.write "only_java1-0.gemspec", File.read("#{lib_path("only_java-1.0-java")}/only_java.gemspec") + end + + install_gemfile <<-G + platforms :jruby do + gem "only_java", "1.2", :git => "#{lib_path("only_java-1.1-java")}" + end + G + + expect(out).to include("Source contains 'only_java' at: 1.0 java, 1.1 java") + end + + it "still works after moving the application directory" do + bundle "install --path vendor/bundle" + FileUtils.mv bundled_app, tmp("bundled_app.bck") + + Dir.chdir tmp("bundled_app.bck") + expect(the_bundle).to include_gems "foo 1.0" + end + + it "can still install after moving the application directory" do + bundle "install --path vendor/bundle" + FileUtils.mv bundled_app, tmp("bundled_app.bck") + + update_git "foo", "1.1", :path => lib_path("foo-1.0") + + Dir.chdir tmp("bundled_app.bck") + gemfile tmp("bundled_app.bck/Gemfile"), <<-G + source "file://#{gem_repo1}" + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + + gem "rack", "1.0" + G + + bundle "update foo" + + expect(the_bundle).to include_gems "foo 1.1", "rack 1.0" + end + end + + describe "with an empty git block" do + before do + build_git "foo" + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + git "#{lib_path("foo-1.0")}" do + # this page left intentionally blank + end + G + end + + it "does not explode" do + bundle "install" + expect(the_bundle).to include_gems "rack 1.0" + end + end + + describe "when specifying a revision" do + before(:each) do + build_git "foo" + @revision = revision_for(lib_path("foo-1.0")) + update_git "foo" + end + + it "works" do + install_gemfile <<-G + git "#{lib_path("foo-1.0")}", :ref => "#{@revision}" do + gem "foo" + end + G + + run <<-RUBY + require 'foo' + puts "WIN" unless defined?(FOO_PREV_REF) + RUBY + + expect(out).to eq("WIN") + end + + it "works when the revision is a symbol" do + install_gemfile <<-G + git "#{lib_path("foo-1.0")}", :ref => #{@revision.to_sym.inspect} do + gem "foo" + end + G + expect(err).to lack_errors + + run <<-RUBY + require 'foo' + puts "WIN" unless defined?(FOO_PREV_REF) + RUBY + + expect(out).to eq("WIN") + end + end + + describe "when specifying a branch" do + let(:branch) { "branch" } + let(:repo) { build_git("foo").path } + before(:each) do + update_git("foo", :path => repo, :branch => branch) + end + + it "works" do + install_gemfile <<-G + git "#{repo}", :branch => #{branch.dump} do + gem "foo" + end + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + context "when the branch starts with a `#`" do + let(:branch) { "#149/redirect-url-fragment" } + it "works" do + install_gemfile <<-G + git "#{repo}", :branch => #{branch.dump} do + gem "foo" + end + G + + expect(the_bundle).to include_gems("foo 1.0") + end + end + + context "when the branch includes quotes" do + let(:branch) { %('") } + it "works" do + install_gemfile <<-G + git "#{repo}", :branch => #{branch.dump} do + gem "foo" + end + G + + expect(the_bundle).to include_gems("foo 1.0") + end + end + end + + describe "when specifying a tag" do + let(:tag) { "tag" } + let(:repo) { build_git("foo").path } + before(:each) do + update_git("foo", :path => repo, :tag => tag) + end + + it "works" do + install_gemfile <<-G + git "#{repo}", :tag => #{tag.dump} do + gem "foo" + end + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + context "when the tag starts with a `#`" do + let(:tag) { "#149/redirect-url-fragment" } + it "works" do + install_gemfile <<-G + git "#{repo}", :tag => #{tag.dump} do + gem "foo" + end + G + + expect(the_bundle).to include_gems("foo 1.0") + end + end + + context "when the tag includes quotes" do + let(:tag) { %('") } + it "works" do + install_gemfile <<-G + git "#{repo}", :tag => #{tag.dump} do + gem "foo" + end + G + + expect(the_bundle).to include_gems("foo 1.0") + end + end + end + + describe "when specifying local override" do + it "uses the local repository instead of checking a new one out" do + # We don't generate it because we actually don't need it + # build_git "rack", "0.8" + + build_git "rack", "0.8", :path => lib_path("local-rack") do |s| + s.write "lib/rack.rb", "puts :LOCAL" + end + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle :install + expect(out).to match(/at #{lib_path('local-rack')}/) + + run "require 'rack'" + expect(out).to eq("LOCAL") + end + + it "chooses the local repository on runtime" do + build_git "rack", "0.8" + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + update_git "rack", "0.8", :path => lib_path("local-rack") do |s| + s.write "lib/rack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + run "require 'rack'" + expect(out).to eq("LOCAL") + end + + it "unlocks the source when the dependencies have changed while switching to the local" do + build_git "rack", "0.8" + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + update_git "rack", "0.8", :path => lib_path("local-rack") do |s| + s.write "rack.gemspec", build_spec("rack", "0.8") { runtime "rspec", "> 0" }.first.to_ruby + s.write "lib/rack.rb", "puts :LOCAL" + end + + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle! %(config local.rack #{lib_path("local-rack")}) + bundle! :install + run! "require 'rack'" + expect(out).to eq("LOCAL") + end + + it "updates specs on runtime" do + system_gems "nokogiri-1.4.2" + + build_git "rack", "0.8" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + lockfile0 = File.read(bundled_app("Gemfile.lock")) + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + update_git "rack", "0.8", :path => lib_path("local-rack") do |s| + s.add_dependency "nokogiri", "1.4.2" + end + + bundle %(config local.rack #{lib_path("local-rack")}) + run "require 'rack'" + + lockfile1 = File.read(bundled_app("Gemfile.lock")) + expect(lockfile1).not_to eq(lockfile0) + end + + it "updates ref on install" do + build_git "rack", "0.8" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + lockfile0 = File.read(bundled_app("Gemfile.lock")) + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + update_git "rack", "0.8", :path => lib_path("local-rack") + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle :install + + lockfile1 = File.read(bundled_app("Gemfile.lock")) + expect(lockfile1).not_to eq(lockfile0) + end + + it "explodes if given path does not exist on install" do + build_git "rack", "0.8" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle :install + expect(out).to match(/Cannot use local override for rack-0.8 because #{Regexp.escape(lib_path('local-rack').to_s)} does not exist/) + end + + it "explodes if branch is not given on install" do + build_git "rack", "0.8" + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle :install + expect(out).to match(/cannot use local override/i) + end + + it "does not explode if disable_local_branch_check is given" do + build_git "rack", "0.8" + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle %(config disable_local_branch_check true) + bundle :install + expect(out).to match(/Bundle complete!/) + end + + it "explodes on different branches on install" do + build_git "rack", "0.8" + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + update_git "rack", "0.8", :path => lib_path("local-rack"), :branch => "another" do |s| + s.write "lib/rack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle :install + expect(out).to match(/is using branch another but Gemfile specifies master/) + end + + it "explodes on invalid revision on install" do + build_git "rack", "0.8" + + build_git "rack", "0.8", :path => lib_path("local-rack") do |s| + s.write "lib/rack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle :install + expect(out).to match(/The Gemfile lock is pointing to revision \w+/) + end + end + + describe "specified inline" do + # TODO: Figure out how to write this test so that it is not flaky depending + # on the current network situation. + # it "supports private git URLs" do + # gemfile <<-G + # gem "thingy", :git => "git@notthere.fallingsnow.net:somebody/thingy.git" + # G + # + # bundle :install + # + # # p out + # # p err + # puts err unless err.empty? # This spec fails randomly every so often + # err.should include("notthere.fallingsnow.net") + # err.should include("ssh") + # end + + it "installs from git even if a newer gem is available elsewhere" do + build_git "rack", "0.8" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}" + G + + expect(the_bundle).to include_gems "rack 0.8" + end + + it "installs dependencies from git even if a newer gem is available elsewhere" do + system_gems "rack-1.0.0" + + build_lib "rack", "1.0", :path => lib_path("nested/bar") do |s| + s.write "lib/rack.rb", "puts 'WIN OVERRIDE'" + end + + build_git "foo", :path => lib_path("nested") do |s| + s.add_dependency "rack", "= 1.0" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :git => "#{lib_path("nested")}" + G + + run "require 'rack'" + expect(out).to eq("WIN OVERRIDE") + end + + it "correctly unlocks when changing to a git source" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "0.9.1" + G + + build_git "rack", :path => lib_path("rack") + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0.0", :git => "#{lib_path("rack")}" + G + + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "correctly unlocks when changing to a git source without versions" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + build_git "rack", "1.2", :path => lib_path("rack") + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack")}" + G + + expect(the_bundle).to include_gems "rack 1.2" + end + end + + describe "block syntax" do + it "pulls all gems from a git block" do + build_lib "omg", :path => lib_path("hi2u/omg") + build_lib "hi2u", :path => lib_path("hi2u") + + install_gemfile <<-G + path "#{lib_path("hi2u")}" do + gem "omg" + gem "hi2u" + end + G + + expect(the_bundle).to include_gems "omg 1.0", "hi2u 1.0" + end + end + + it "uses a ref if specified" do + build_git "foo" + @revision = revision_for(lib_path("foo-1.0")) + update_git "foo" + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{@revision}" + G + + run <<-RUBY + require 'foo' + puts "WIN" unless defined?(FOO_PREV_REF) + RUBY + + expect(out).to eq("WIN") + end + + it "correctly handles cases with invalid gemspecs" do + build_git "foo" do |s| + s.summary = nil + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :git => "#{lib_path("foo-1.0")}" + gem "rails", "2.3.2" + G + + expect(the_bundle).to include_gems "foo 1.0" + expect(the_bundle).to include_gems "rails 2.3.2" + end + + it "runs the gemspec in the context of its parent directory" do + build_lib "bar", :path => lib_path("foo/bar"), :gemspec => false do |s| + s.write lib_path("foo/bar/lib/version.rb"), %(BAR_VERSION = '1.0') + s.write "bar.gemspec", <<-G + $:.unshift Dir.pwd # For 1.9 + require 'lib/version' + Gem::Specification.new do |s| + s.name = 'bar' + s.author = 'no one' + s.version = BAR_VERSION + s.summary = 'Bar' + s.files = Dir["lib/**/*.rb"] + end + G + end + + build_git "foo", :path => lib_path("foo") do |s| + s.write "bin/foo", "" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "bar", :git => "#{lib_path("foo")}" + gem "rails", "2.3.2" + G + + expect(the_bundle).to include_gems "bar 1.0" + expect(the_bundle).to include_gems "rails 2.3.2" + end + + it "installs from git even if a rubygems gem is present" do + build_gem "foo", "1.0", :path => lib_path("fake_foo"), :to_system => true do |s| + s.write "lib/foo.rb", "raise 'FAIL'" + end + + build_git "foo", "1.0" + + install_gemfile <<-G + gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "fakes the gem out if there is no gemspec" do + build_git "foo", :gemspec => false + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}" + gem "rails", "2.3.2" + G + + expect(the_bundle).to include_gems("foo 1.0") + expect(the_bundle).to include_gems("rails 2.3.2") + end + + it "catches git errors and spits out useful output" do + gemfile <<-G + gem "foo", "1.0", :git => "omgomg" + G + + bundle :install + + expect(out).to include("Git error:") + expect(err).to include("fatal") + expect(err).to include("omgomg") + end + + it "works when the gem path has spaces in it" do + build_git "foo", :path => lib_path("foo space-1.0") + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo space-1.0")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "handles repos that have been force-pushed" do + build_git "forced", "1.0" + + install_gemfile <<-G + git "#{lib_path("forced-1.0")}" do + gem 'forced' + end + G + expect(the_bundle).to include_gems "forced 1.0" + + update_git "forced" do |s| + s.write "lib/forced.rb", "FORCED = '1.1'" + end + + bundle "update" + expect(the_bundle).to include_gems "forced 1.1" + + Dir.chdir(lib_path("forced-1.0")) do + `git reset --hard HEAD^` + end + + bundle "update" + expect(the_bundle).to include_gems "forced 1.0" + end + + it "ignores submodules if :submodule is not passed" do + build_git "submodule", "1.0" + build_git "has_submodule", "1.0" do |s| + s.add_dependency "submodule" + end + Dir.chdir(lib_path("has_submodule-1.0")) do + sys_exec "git submodule add #{lib_path("submodule-1.0")} submodule-1.0" + `git commit -m "submodulator"` + end + + install_gemfile <<-G + git "#{lib_path("has_submodule-1.0")}" do + gem "has_submodule" + end + G + expect(out).to match(/could not find gem 'submodule/i) + + expect(the_bundle).not_to include_gems "has_submodule 1.0" + end + + it "handles repos with submodules" do + build_git "submodule", "1.0" + build_git "has_submodule", "1.0" do |s| + s.add_dependency "submodule" + end + Dir.chdir(lib_path("has_submodule-1.0")) do + sys_exec "git submodule add #{lib_path("submodule-1.0")} submodule-1.0" + `git commit -m "submodulator"` + end + + install_gemfile <<-G + git "#{lib_path("has_submodule-1.0")}", :submodules => true do + gem "has_submodule" + end + G + + expect(the_bundle).to include_gems "has_submodule 1.0" + end + + it "handles implicit updates when modifying the source info" do + git = build_git "foo" + + install_gemfile <<-G + git "#{lib_path("foo-1.0")}" do + gem "foo" + end + G + + update_git "foo" + update_git "foo" + + install_gemfile <<-G + git "#{lib_path("foo-1.0")}", :ref => "#{git.ref_for("HEAD^")}" do + gem "foo" + end + G + + run <<-RUBY + require 'foo' + puts "WIN" if FOO_PREV_REF == '#{git.ref_for("HEAD^^")}' + RUBY + + expect(out).to eq("WIN") + end + + it "does not to a remote fetch if the revision is cached locally" do + build_git "foo" + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + FileUtils.rm_rf(lib_path("foo-1.0")) + + bundle "install" + expect(out).not_to match(/updating/i) + end + + it "doesn't blow up if bundle install is run twice in a row" do + build_git "foo" + + gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + bundle "install" + bundle "install" + expect(exitstatus).to eq(0) if exitstatus + end + + it "prints a friendly error if a file blocks the git repo" do + build_git "foo" + + FileUtils.touch(default_bundle_path("bundler")) + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + expect(exitstatus).to_not eq(0) if exitstatus + expect(out).to include("Bundler could not install a gem because it " \ + "needs to create a directory, but a file exists " \ + "- #{default_bundle_path("bundler")}") + end + + it "does not duplicate git gem sources" do + build_lib "foo", :path => lib_path("nested/foo") + build_lib "bar", :path => lib_path("nested/bar") + + build_git "foo", :path => lib_path("nested") + build_git "bar", :path => lib_path("nested") + + gemfile <<-G + gem "foo", :git => "#{lib_path("nested")}" + gem "bar", :git => "#{lib_path("nested")}" + G + + bundle "install" + expect(File.read(bundled_app("Gemfile.lock")).scan("GIT").size).to eq(1) + end + + describe "switching sources" do + it "doesn't explode when switching Path to Git sources" do + build_gem "foo", "1.0", :to_system => true do |s| + s.write "lib/foo.rb", "raise 'fail'" + end + build_lib "foo", "1.0", :path => lib_path("bar/foo") + build_git "bar", "1.0", :path => lib_path("bar") do |s| + s.add_dependency "foo" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "bar", :path => "#{lib_path("bar")}" + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "bar", :git => "#{lib_path("bar")}" + G + + expect(the_bundle).to include_gems "foo 1.0", "bar 1.0" + end + + it "doesn't explode when switching Gem to Git source" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack-obama" + gem "rack", "1.0.0" + G + + build_git "rack", "1.0" do |s| + s.write "lib/new_file.rb", "puts 'USING GIT'" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack-obama" + gem "rack", "1.0.0", :git => "#{lib_path("rack-1.0")}" + G + + run "require 'new_file'" + expect(out).to eq("USING GIT") + end + end + + describe "bundle install after the remote has been updated" do + it "installs" do + build_git "valim" + + install_gemfile <<-G + gem "valim", :git => "file://#{lib_path("valim-1.0")}" + G + + old_revision = revision_for(lib_path("valim-1.0")) + update_git "valim" + new_revision = revision_for(lib_path("valim-1.0")) + + lockfile = File.read(bundled_app("Gemfile.lock")) + File.open(bundled_app("Gemfile.lock"), "w") do |file| + file.puts lockfile.gsub(/revision: #{old_revision}/, "revision: #{new_revision}") + end + + bundle "install" + + run <<-R + require "valim" + puts VALIM_PREV_REF + R + + expect(out).to eq(old_revision) + end + + it "gives a helpful error message when the remote ref no longer exists" do + build_git "foo" + revision = revision_for(lib_path("foo-1.0")) + + install_gemfile <<-G + gem "foo", :git => "file://#{lib_path("foo-1.0")}", :ref => "#{revision}" + G + bundle "install" + expect(out).to_not match(/Revision.*does not exist/) + + install_gemfile <<-G + gem "foo", :git => "file://#{lib_path("foo-1.0")}", :ref => "deadbeef" + G + bundle "install" + expect(out).to include("Revision deadbeef does not exist in the repository") + end + end + + describe "bundle install --deployment with git sources" do + it "works" do + build_git "valim", :path => lib_path("valim") + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "valim", "= 1.0", :git => "#{lib_path("valim")}" + G + + simulate_new_machine + + bundle "install --deployment" + expect(exitstatus).to eq(0) if exitstatus + end + end + + describe "gem install hooks" do + it "runs pre-install hooks" do + build_git "foo" + gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + require 'rubygems' + Gem.pre_install_hooks << lambda do |inst| + STDERR.puts "Ran pre-install hook: \#{inst.spec.full_name}" + end + H + end + + bundle :install, + :requires => [lib_path("install_hooks.rb")] + expect(err).to eq_err("Ran pre-install hook: foo-1.0") + end + + it "runs post-install hooks" do + build_git "foo" + gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + require 'rubygems' + Gem.post_install_hooks << lambda do |inst| + STDERR.puts "Ran post-install hook: \#{inst.spec.full_name}" + end + H + end + + bundle :install, + :requires => [lib_path("install_hooks.rb")] + expect(err).to eq_err("Ran post-install hook: foo-1.0") + end + + it "complains if the install hook fails" do + build_git "foo" + gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + require 'rubygems' + Gem.pre_install_hooks << lambda do |inst| + false + end + H + end + + bundle :install, + :requires => [lib_path("install_hooks.rb")] + expect(out).to include("failed for foo-1.0") + end + end + + context "with an extension" do + it "installs the extension", :ruby_repo do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("../lib", __FILE__) + FileUtils.mkdir_p(path) + File.open("\#{path}/foo.rb", "w") do |f| + f.puts "FOO = 'YES'" + end + end + RUBY + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + expect(out).to eq("YES") + + run! <<-R + puts $:.grep(/ext/) + R + expect(out).to eq(Pathname.glob(system_gem_path("bundler/gems/extensions/**/foo-1.0-*")).first.to_s) + end + + it "does not use old extension after ref changes", :ruby_repo do + git_reader = build_git "foo", :no_default => true do |s| + s.extensions = ["ext/extconf.rb"] + s.write "ext/extconf.rb", <<-RUBY + require "mkmf" + create_makefile("foo") + RUBY + s.write "ext/foo.c", "void Init_foo() {}" + end + + 2.times do |i| + Dir.chdir(git_reader.path) do + File.open("ext/foo.c", "w") do |file| + file.write <<-C + #include "ruby.h" + VALUE foo() { return INT2FIX(#{i}); } + void Init_foo() { rb_define_global_function("foo", &foo, 0); } + C + end + `git commit -m 'commit for iteration #{i}' ext/foo.c` + end + git_sha = git_reader.ref_for("HEAD") + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{git_sha}" + G + + run <<-R + require 'foo' + puts foo + R + + expect(out).to eq(i.to_s) + end + end + + it "does not prompt to gem install if extension fails" do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + raise + end + RUBY + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + expect(out).to end_with(<<-M.strip) +An error occurred while installing foo (1.0), and Bundler cannot continue. + +In Gemfile: + foo + M + expect(out).not_to include("gem install foo") + end + + it "does not reinstall the extension", :ruby_repo, :rubygems => ">= 2.3.0" do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("../lib", __FILE__) + FileUtils.mkdir_p(path) + cur_time = Time.now.to_f.to_s + File.open("\#{path}/foo.rb", "w") do |f| + f.puts "FOO = \#{cur_time}" + end + end + RUBY + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run! <<-R + require 'foo' + puts FOO + R + + installed_time = out + expect(installed_time).to match(/\A\d+\.\d+\z/) + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run! <<-R + require 'foo' + puts FOO + R + expect(out).to eq(installed_time) + end + end + + it "ignores git environment variables" do + build_git "xxxxxx" do |s| + s.executables = "xxxxxxbar" + end + + Bundler::SharedHelpers.with_clean_git_env do + ENV["GIT_DIR"] = "bar" + ENV["GIT_WORK_TREE"] = "bar" + + install_gemfile <<-G + source "file://#{gem_repo1}" + git "#{lib_path("xxxxxx-1.0")}" do + gem 'xxxxxx' + end + G + + expect(exitstatus).to eq(0) if exitstatus + expect(ENV["GIT_DIR"]).to eq("bar") + expect(ENV["GIT_WORK_TREE"]).to eq("bar") + end + end + + describe "without git installed" do + it "prints a better error message" do + build_git "foo" + + install_gemfile <<-G + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + + with_path_as("") do + bundle "update" + end + expect(out).to include("You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git") + end + + it "installs a packaged git gem successfully" do + build_git "foo" + + install_gemfile <<-G + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + bundle "package --all" + simulate_new_machine + + bundle "install", :env => { "PATH" => "" } + expect(out).to_not include("You need to install git to be able to use gems from git repositories.") + expect(exitstatus).to be_zero if exitstatus + end + end + + describe "when the git source is overriden with a local git repo" do + before do + bundle "config --global local.foo #{lib_path("foo")}" + end + + describe "and git output is colorized" do + before do + File.open("#{ENV["HOME"]}/.gitconfig", "w") do |f| + f.write("[color]\n\tui = always\n") + end + end + + it "installs successfully" do + build_git "foo", "1.0", :path => lib_path("foo") + + gemfile <<-G + gem "foo", :git => "#{lib_path("foo")}", :branch => "master" + G + + bundle :install + expect(the_bundle).to include_gems "foo 1.0" + end + end + end + + context "git sources that include credentials" do + context "that are username and password" do + let(:credentials) { "user1:password1" } + + it "does not display the password" do + install_gemfile <<-G + git "https://#{credentials}@github.com/company/private-repo" do + gem "foo" + end + G + + bundle :install + expect(out).to_not include("password1") + expect(err).to_not include("password1") + expect(out).to include("Fetching https://user1@github.com/company/private-repo") + end + end + + context "that is an oauth token" do + let(:credentials) { "oauth_token" } + + it "displays the oauth scheme but not the oauth token" do + install_gemfile <<-G + git "https://#{credentials}:x-oauth-basic@github.com/company/private-repo" do + gem "foo" + end + G + + bundle :install + expect(out).to_not include("oauth_token") + expect(err).to_not include("oauth_token") + expect(out).to include("Fetching https://x-oauth-basic@github.com/company/private-repo") + end + end + end +end diff --git a/spec/bundler/install/gemfile/groups_spec.rb b/spec/bundler/install/gemfile/groups_spec.rb new file mode 100644 index 0000000000..a3a5eeefdf --- /dev/null +++ b/spec/bundler/install/gemfile/groups_spec.rb @@ -0,0 +1,371 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install with groups" do + describe "installing with no options" do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + group :emo do + gem "activesupport", "2.3.5" + end + gem "thin", :groups => [:emo] + G + end + + it "installs gems in the default group" do + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "installs gems in a group block into that group" do + expect(the_bundle).to include_gems "activesupport 2.3.5" + + load_error_run <<-R, "activesupport", :default + require 'activesupport' + puts ACTIVESUPPORT + R + + expect(err).to eq_err("ZOMG LOAD ERROR") + end + + it "installs gems with inline :groups into those groups" do + expect(the_bundle).to include_gems "thin 1.0" + + load_error_run <<-R, "thin", :default + require 'thin' + puts THIN + R + + expect(err).to eq_err("ZOMG LOAD ERROR") + end + + it "sets up everything if Bundler.setup is used with no groups" do + output = run("require 'rack'; puts RACK") + expect(output).to eq("1.0.0") + + output = run("require 'activesupport'; puts ACTIVESUPPORT") + expect(output).to eq("2.3.5") + + output = run("require 'thin'; puts THIN") + expect(output).to eq("1.0") + end + + it "removes old groups when new groups are set up" do + load_error_run <<-RUBY, "thin", :emo + Bundler.setup(:default) + require 'thin' + puts THIN + RUBY + + expect(err).to eq_err("ZOMG LOAD ERROR") + end + + it "sets up old groups when they have previously been removed" do + output = run <<-RUBY, :emo + Bundler.setup(:default) + Bundler.setup(:default, :emo) + require 'thin'; puts THIN + RUBY + expect(output).to eq("1.0") + end + end + + describe "installing --without" do + describe "with gems assigned to a single group" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + group :emo do + gem "activesupport", "2.3.5" + end + group :debugging, :optional => true do + gem "thin" + end + G + end + + it "installs gems in the default group" do + bundle :install, :without => "emo" + expect(the_bundle).to include_gems "rack 1.0.0", :groups => [:default] + end + + it "does not install gems from the excluded group" do + bundle :install, :without => "emo" + expect(the_bundle).not_to include_gems "activesupport 2.3.5", :groups => [:default] + end + + it "does not install gems from the previously excluded group" do + bundle :install, :without => "emo" + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + bundle :install + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + end + + it "does not say it installed gems from the excluded group" do + bundle :install, :without => "emo" + expect(out).not_to include("activesupport") + end + + it "allows Bundler.setup for specific groups" do + bundle :install, :without => "emo" + run("require 'rack'; puts RACK", :default) + expect(out).to eq("1.0.0") + end + + it "does not effect the resolve" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + group :emo do + gem "rails", "2.3.2" + end + G + + bundle :install, :without => "emo" + expect(the_bundle).to include_gems "activesupport 2.3.2", :groups => [:default] + end + + it "still works on a different machine and excludes gems" do + bundle :install, :without => "emo" + + simulate_new_machine + bundle :install, :without => "emo" + + expect(the_bundle).to include_gems "rack 1.0.0", :groups => [:default] + expect(the_bundle).not_to include_gems "activesupport 2.3.5", :groups => [:default] + end + + it "still works when BUNDLE_WITHOUT is set" do + ENV["BUNDLE_WITHOUT"] = "emo" + + bundle :install + expect(out).not_to include("activesupport") + + expect(the_bundle).to include_gems "rack 1.0.0", :groups => [:default] + expect(the_bundle).not_to include_gems "activesupport 2.3.5", :groups => [:default] + + ENV["BUNDLE_WITHOUT"] = nil + end + + it "clears without when passed an empty list" do + bundle :install, :without => "emo" + + bundle 'install --without ""' + expect(the_bundle).to include_gems "activesupport 2.3.5" + end + + it "doesn't clear without when nothing is passed" do + bundle :install, :without => "emo" + + bundle :install + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + end + + it "does not install gems from the optional group" do + bundle :install + expect(the_bundle).not_to include_gems "thin 1.0" + end + + it "does install gems from the optional group when requested" do + bundle :install, :with => "debugging" + expect(the_bundle).to include_gems "thin 1.0" + end + + it "does install gems from the previously requested group" do + bundle :install, :with => "debugging" + expect(the_bundle).to include_gems "thin 1.0" + bundle :install + expect(the_bundle).to include_gems "thin 1.0" + end + + it "does install gems from the optional groups requested with BUNDLE_WITH" do + ENV["BUNDLE_WITH"] = "debugging" + bundle :install + expect(the_bundle).to include_gems "thin 1.0" + ENV["BUNDLE_WITH"] = nil + end + + it "clears with when passed an empty list" do + bundle :install, :with => "debugging" + bundle 'install --with ""' + expect(the_bundle).not_to include_gems "thin 1.0" + end + + it "does remove groups from without when passed at with" do + bundle :install, :without => "emo" + bundle :install, :with => "emo" + expect(the_bundle).to include_gems "activesupport 2.3.5" + end + + it "does remove groups from with when passed at without" do + bundle :install, :with => "debugging" + bundle :install, :without => "debugging" + expect(the_bundle).not_to include_gems "thin 1.0" + end + + it "errors out when passing a group to with and without" do + bundle :install, :with => "emo debugging", :without => "emo" + expect(out).to include("The offending groups are: emo") + end + + it "can add and remove a group at the same time" do + bundle :install, :with => "debugging", :without => "emo" + expect(the_bundle).to include_gems "thin 1.0" + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + end + + it "does have no effect when listing a not optional group in with" do + bundle :install, :with => "emo" + expect(the_bundle).to include_gems "activesupport 2.3.5" + end + + it "does have no effect when listing an optional group in without" do + bundle :install, :without => "debugging" + expect(the_bundle).not_to include_gems "thin 1.0" + end + end + + describe "with gems assigned to multiple groups" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + group :emo, :lolercoaster do + gem "activesupport", "2.3.5" + end + G + end + + it "installs gems in the default group" do + bundle :install, :without => "emo lolercoaster" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "installs the gem if any of its groups are installed" do + bundle "install --without emo" + expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.5" + end + + describe "with a gem defined multiple times in different groups" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + group :emo do + gem "activesupport", "2.3.5" + end + + group :lolercoaster do + gem "activesupport", "2.3.5" + end + G + end + + it "installs the gem w/ option --without emo" do + bundle "install --without emo" + expect(the_bundle).to include_gems "activesupport 2.3.5" + end + + it "installs the gem w/ option --without lolercoaster" do + bundle "install --without lolercoaster" + expect(the_bundle).to include_gems "activesupport 2.3.5" + end + + it "does not install the gem w/ option --without emo lolercoaster" do + bundle "install --without emo lolercoaster" + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + end + + it "does not install the gem w/ option --without 'emo lolercoaster'" do + bundle "install --without 'emo lolercoaster'" + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + end + end + end + + describe "nesting groups" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + group :emo do + group :lolercoaster do + gem "activesupport", "2.3.5" + end + end + G + end + + it "installs gems in the default group" do + bundle :install, :without => "emo lolercoaster" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "installs the gem if any of its groups are installed" do + bundle "install --without emo" + expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.5" + end + end + end + + describe "when loading only the default group" do + it "should not load all groups" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :groups => :development + G + + ruby <<-R + require "bundler" + Bundler.setup :default + Bundler.require :default + puts RACK + begin + require "activesupport" + rescue LoadError + puts "no activesupport" + end + R + + expect(out).to include("1.0") + expect(out).to include("no activesupport") + end + end + + describe "when locked and installed with --without" do + before(:each) do + build_repo2 + system_gems "rack-0.9.1" do + install_gemfile <<-G, :without => :rack + source "file://#{gem_repo2}" + gem "rack" + + group :rack do + gem "rack_middleware" + end + G + end + end + + it "uses the correct versions even if --without was used on the original" do + expect(the_bundle).to include_gems "rack 0.9.1" + expect(the_bundle).not_to include_gems "rack_middleware 1.0" + simulate_new_machine + + bundle :install + + expect(the_bundle).to include_gems "rack 0.9.1" + expect(the_bundle).to include_gems "rack_middleware 1.0" + end + + it "does not hit the remote a second time" do + FileUtils.rm_rf gem_repo2 + bundle "install --without rack" + expect(err).to lack_errors + end + end +end diff --git a/spec/bundler/install/gemfile/install_if.rb b/spec/bundler/install/gemfile/install_if.rb new file mode 100644 index 0000000000..b1717ad583 --- /dev/null +++ b/spec/bundler/install/gemfile/install_if.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true +require "spec_helper" + +describe "bundle install with install_if conditionals" do + it "follows the install_if DSL" do + install_gemfile <<-G + source "file://#{gem_repo1}" + install_if(lambda { true }) do + gem "activesupport", "2.3.5" + end + gem "thin", :install_if => false + install_if(lambda { false }) do + gem "foo" + end + gem "rack" + G + + expect(the_bundle).to include_gems("rack 1.0", "activesupport 2.3.5") + expect(the_bundle).not_to include_gems("thin") + expect(the_bundle).not_to include_gems("foo") + + lockfile_should_be <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + activesupport (2.3.5) + foo (1.0) + rack (1.0.0) + thin (1.0) + rack + + PLATFORMS + ruby + + DEPENDENCIES + activesupport (= 2.3.5) + foo + rack + thin + + BUNDLED WITH + #{Bundler::VERSION} + L + end +end diff --git a/spec/bundler/install/gemfile/path_spec.rb b/spec/bundler/install/gemfile/path_spec.rb new file mode 100644 index 0000000000..a1c41aebbb --- /dev/null +++ b/spec/bundler/install/gemfile/path_spec.rb @@ -0,0 +1,595 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install with explicit source paths" do + it "fetches gems" do + build_lib "foo" + + install_gemfile <<-G + path "#{lib_path("foo-1.0")}" + gem 'foo' + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + it "supports pinned paths" do + build_lib "foo" + + install_gemfile <<-G + gem 'foo', :path => "#{lib_path("foo-1.0")}" + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + it "supports relative paths" do + build_lib "foo" + + relative_path = lib_path("foo-1.0").relative_path_from(Pathname.new(Dir.pwd)) + + install_gemfile <<-G + gem 'foo', :path => "#{relative_path}" + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + it "expands paths" do + build_lib "foo" + + relative_path = lib_path("foo-1.0").relative_path_from(Pathname.new("~").expand_path) + + install_gemfile <<-G + gem 'foo', :path => "~/#{relative_path}" + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + it "expands paths raise error with not existing user's home dir" do + build_lib "foo" + username = "some_unexisting_user" + relative_path = lib_path("foo-1.0").relative_path_from(Pathname.new("/home/#{username}").expand_path) + + install_gemfile <<-G + gem 'foo', :path => "~#{username}/#{relative_path}" + G + expect(out).to match("There was an error while trying to use the path `~#{username}/#{relative_path}`.") + expect(out).to match("user #{username} doesn't exist") + end + + it "expands paths relative to Bundler.root" do + build_lib "foo", :path => bundled_app("foo-1.0") + + install_gemfile <<-G + gem 'foo', :path => "./foo-1.0" + G + + bundled_app("subdir").mkpath + Dir.chdir(bundled_app("subdir")) do + expect(the_bundle).to include_gems("foo 1.0") + end + end + + it "expands paths when comparing locked paths to Gemfile paths" do + build_lib "foo", :path => bundled_app("foo-1.0") + + install_gemfile <<-G + gem 'foo', :path => File.expand_path("../foo-1.0", __FILE__) + G + + bundle "install --frozen" + expect(exitstatus).to eq(0) if exitstatus + end + + it "installs dependencies from the path even if a newer gem is available elsewhere" do + system_gems "rack-1.0.0" + + build_lib "rack", "1.0", :path => lib_path("nested/bar") do |s| + s.write "lib/rack.rb", "puts 'WIN OVERRIDE'" + end + + build_lib "foo", :path => lib_path("nested") do |s| + s.add_dependency "rack", "= 1.0" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :path => "#{lib_path("nested")}" + G + + run "require 'rack'" + expect(out).to eq("WIN OVERRIDE") + end + + it "works" do + build_gem "foo", "1.0.0", :to_system => true do |s| + s.write "lib/foo.rb", "puts 'FAIL'" + end + + build_lib "omg", "1.0", :path => lib_path("omg") do |s| + s.add_dependency "foo" + end + + build_lib "foo", "1.0.0", :path => lib_path("omg/foo") + + install_gemfile <<-G + gem "omg", :path => "#{lib_path("omg")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "prefers gemspecs closer to the path root" do + build_lib "premailer", "1.0.0", :path => lib_path("premailer") do |s| + s.write "gemfiles/ruby187.gemspec", <<-G + Gem::Specification.new do |s| + s.name = 'premailer' + s.version = '1.0.0' + s.summary = 'Hi' + s.authors = 'Me' + end + G + end + + install_gemfile <<-G + gem "premailer", :path => "#{lib_path("premailer")}" + G + + # Installation of the 'gemfiles' gemspec would fail since it will be unable + # to require 'premailer.rb' + expect(the_bundle).to include_gems "premailer 1.0.0" + end + + it "warns on invalid specs", :rubygems => "1.7" do + build_lib "foo" + + gemspec = lib_path("foo-1.0").join("foo.gemspec").to_s + File.open(gemspec, "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = "foo" + end + G + end + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo-1.0")}" + G + + expect(out).to_not include("ERROR REPORT") + expect(out).to_not include("Your Gemfile has no gem server sources.") + expect(out).to match(/is not valid. Please fix this gemspec./) + expect(out).to match(/The validation error was 'missing value for attribute version'/) + expect(out).to match(/You have one or more invalid gemspecs that need to be fixed/) + end + + it "supports gemspec syntax" do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "rack", "1.0" + end + + gemfile = <<-G + source "file://#{gem_repo1}" + gemspec + G + + File.open(lib_path("foo/Gemfile"), "w") {|f| f.puts gemfile } + + Dir.chdir(lib_path("foo")) do + bundle "install" + expect(the_bundle).to include_gems "foo 1.0" + expect(the_bundle).to include_gems "rack 1.0" + end + end + + it "supports gemspec syntax with an alternative path" do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "rack", "1.0" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gemspec :path => "#{lib_path("foo")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + expect(the_bundle).to include_gems "rack 1.0" + end + + it "doesn't automatically unlock dependencies when using the gemspec syntax" do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "rack", ">= 1.0" + end + + Dir.chdir lib_path("foo") + + install_gemfile lib_path("foo/Gemfile"), <<-G + source "file://#{gem_repo1}" + gemspec + G + + build_gem "rack", "1.0.1", :to_system => true + + bundle "install" + + expect(the_bundle).to include_gems "foo 1.0" + expect(the_bundle).to include_gems "rack 1.0" + end + + it "doesn't automatically unlock dependencies when using the gemspec syntax and the gem has development dependencies" do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "rack", ">= 1.0" + s.add_development_dependency "activesupport" + end + + Dir.chdir lib_path("foo") + + install_gemfile lib_path("foo/Gemfile"), <<-G + source "file://#{gem_repo1}" + gemspec + G + + build_gem "rack", "1.0.1", :to_system => true + + bundle "install" + + expect(the_bundle).to include_gems "foo 1.0" + expect(the_bundle).to include_gems "rack 1.0" + end + + it "raises if there are multiple gemspecs" do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.write "bar.gemspec", build_spec("bar", "1.0").first.to_ruby + end + + install_gemfile <<-G + gemspec :path => "#{lib_path("foo")}" + G + + expect(exitstatus).to eq(15) if exitstatus + expect(out).to match(/There are multiple gemspecs/) + end + + it "allows :name to be specified to resolve ambiguity" do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.write "bar.gemspec" + end + + install_gemfile <<-G + gemspec :path => "#{lib_path("foo")}", :name => "foo" + G + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "sets up executables" do + build_lib "foo" do |s| + s.executables = "foobar" + end + + install_gemfile <<-G + path "#{lib_path("foo-1.0")}" + gem 'foo' + G + expect(the_bundle).to include_gems "foo 1.0" + + bundle "exec foobar" + expect(out).to eq("1.0") + end + + it "handles directories in bin/" do + build_lib "foo" + lib_path("foo-1.0").join("foo.gemspec").rmtree + lib_path("foo-1.0").join("bin/performance").mkpath + + install_gemfile <<-G + gem 'foo', '1.0', :path => "#{lib_path("foo-1.0")}" + G + expect(err).to lack_errors + end + + it "removes the .gem file after installing" do + build_lib "foo" + + install_gemfile <<-G + gem 'foo', :path => "#{lib_path("foo-1.0")}" + G + + expect(lib_path("foo-1.0").join("foo-1.0.gem")).not_to exist + end + + describe "block syntax" do + it "pulls all gems from a path block" do + build_lib "omg" + build_lib "hi2u" + + install_gemfile <<-G + path "#{lib_path}" do + gem "omg" + gem "hi2u" + end + G + + expect(the_bundle).to include_gems "omg 1.0", "hi2u 1.0" + end + end + + it "keeps source pinning" do + build_lib "foo", "1.0", :path => lib_path("foo") + build_lib "omg", "1.0", :path => lib_path("omg") + build_lib "foo", "1.0", :path => lib_path("omg/foo") do |s| + s.write "lib/foo.rb", "puts 'FAIL'" + end + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo")}" + gem "omg", :path => "#{lib_path("omg")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "works when the path does not have a gemspec" do + build_lib "foo", :gemspec => false + + gemfile <<-G + gem "foo", "1.0", :path => "#{lib_path("foo-1.0")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "works when the path does not have a gemspec but there is a lockfile" do + lockfile <<-L + PATH + remote: vendor/bar + specs: + + GEM + remote: http://rubygems.org + L + + in_app_root { FileUtils.mkdir_p("vendor/bar") } + + install_gemfile <<-G + gem "bar", "1.0.0", path: "vendor/bar", require: "bar/nyard" + G + expect(exitstatus).to eq(0) if exitstatus + end + + context "existing lockfile" do + it "rubygems gems don't re-resolve without changes" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack-obama', '1.0' + gem 'net-ssh', '1.0' + G + + bundle :check, :env => { "DEBUG" => 1 } + expect(out).to match(/using resolution from the lockfile/) + expect(the_bundle).to include_gems "rack-obama 1.0", "net-ssh 1.0" + end + + it "source path gems w/deps don't re-resolve without changes" do + build_lib "rack-obama", "1.0", :path => lib_path("omg") do |s| + s.add_dependency "yard" + end + + build_lib "net-ssh", "1.0", :path => lib_path("omg") do |s| + s.add_dependency "yard" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack-obama', :path => "#{lib_path("omg")}" + gem 'net-ssh', :path => "#{lib_path("omg")}" + G + + bundle :check, :env => { "DEBUG" => 1 } + expect(out).to match(/using resolution from the lockfile/) + expect(the_bundle).to include_gems "rack-obama 1.0", "net-ssh 1.0" + end + end + + it "installs executable stubs" do + build_lib "foo" do |s| + s.executables = ["foo"] + end + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo-1.0")}" + G + + bundle "exec foo" + expect(out).to eq("1.0") + end + + describe "when the gem version in the path is updated" do + before :each do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "bar" + end + build_lib "bar", "1.0", :path => lib_path("foo/bar") + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo")}" + G + end + + it "unlocks all gems when the top level gem is updated" do + build_lib "foo", "2.0", :path => lib_path("foo") do |s| + s.add_dependency "bar" + end + + bundle "install" + + expect(the_bundle).to include_gems "foo 2.0", "bar 1.0" + end + + it "unlocks all gems when a child dependency gem is updated" do + build_lib "bar", "2.0", :path => lib_path("foo/bar") + + bundle "install" + + expect(the_bundle).to include_gems "foo 1.0", "bar 2.0" + end + end + + describe "when dependencies in the path are updated" do + before :each do + build_lib "foo", "1.0", :path => lib_path("foo") + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :path => "#{lib_path("foo")}" + G + end + + it "gets dependencies that are updated in the path" do + build_lib "foo", "1.0", :path => lib_path("foo") do |s| + s.add_dependency "rack" + end + + bundle "install" + + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + describe "switching sources" do + it "doesn't switch pinned git sources to rubygems when pinning the parent gem to a path source" do + build_gem "foo", "1.0", :to_system => true do |s| + s.write "lib/foo.rb", "raise 'fail'" + end + build_lib "foo", "1.0", :path => lib_path("bar/foo") + build_git "bar", "1.0", :path => lib_path("bar") do |s| + s.add_dependency "foo" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "bar", :git => "#{lib_path("bar")}" + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "bar", :path => "#{lib_path("bar")}" + G + + expect(the_bundle).to include_gems "foo 1.0", "bar 1.0" + end + + it "switches the source when the gem existed in rubygems and the path was already being used for another gem" do + build_lib "foo", "1.0", :path => lib_path("foo") + build_gem "bar", "1.0", :to_system => true do |s| + s.write "lib/bar.rb", "raise 'fail'" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "bar" + path "#{lib_path("foo")}" do + gem "foo" + end + G + + build_lib "bar", "1.0", :path => lib_path("foo/bar") + + install_gemfile <<-G + source "file://#{gem_repo1}" + path "#{lib_path("foo")}" do + gem "foo" + gem "bar" + end + G + + expect(the_bundle).to include_gems "bar 1.0" + end + end + + describe "when there are both a gemspec and remote gems" do + it "doesn't query rubygems for local gemspec name" do + build_lib "private_lib", "2.2", :path => lib_path("private_lib") + gemfile = <<-G + source "http://localgemserver.test" + gemspec + gem 'rack' + G + File.open(lib_path("private_lib/Gemfile"), "w") {|f| f.puts gemfile } + + Dir.chdir(lib_path("private_lib")) do + bundle :install, :env => { "DEBUG" => 1 }, :artifice => "endpoint" + expect(out).to match(%r{^HTTP GET http://localgemserver\.test/api/v1/dependencies\?gems=rack$}) + expect(out).not_to match(/^HTTP GET.*private_lib/) + expect(the_bundle).to include_gems "private_lib 2.2" + expect(the_bundle).to include_gems "rack 1.0" + end + end + end + + describe "gem install hooks" do + it "runs pre-install hooks" do + build_git "foo" + gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + require 'rubygems' + Gem.pre_install_hooks << lambda do |inst| + STDERR.puts "Ran pre-install hook: \#{inst.spec.full_name}" + end + H + end + + bundle :install, + :requires => [lib_path("install_hooks.rb")] + expect(err).to eq_err("Ran pre-install hook: foo-1.0") + end + + it "runs post-install hooks" do + build_git "foo" + gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + require 'rubygems' + Gem.post_install_hooks << lambda do |inst| + STDERR.puts "Ran post-install hook: \#{inst.spec.full_name}" + end + H + end + + bundle :install, + :requires => [lib_path("install_hooks.rb")] + expect(err).to eq_err("Ran post-install hook: foo-1.0") + end + + it "complains if the install hook fails" do + build_git "foo" + gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + require 'rubygems' + Gem.pre_install_hooks << lambda do |inst| + false + end + H + end + + bundle :install, + :requires => [lib_path("install_hooks.rb")] + expect(out).to include("failed for foo-1.0") + end + end +end diff --git a/spec/bundler/install/gemfile/platform_spec.rb b/spec/bundler/install/gemfile/platform_spec.rb new file mode 100644 index 0000000000..c6eaec7ca6 --- /dev/null +++ b/spec/bundler/install/gemfile/platform_spec.rb @@ -0,0 +1,265 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install across platforms" do + it "maintains the same lockfile if all gems are compatible across platforms" do + lockfile <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (0.9.1) + + PLATFORMS + #{not_local} + + DEPENDENCIES + rack + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + + expect(the_bundle).to include_gems "rack 0.9.1" + end + + it "pulls in the correct platform specific gem" do + lockfile <<-G + GEM + remote: file:#{gem_repo1} + specs: + platform_specific (1.0) + platform_specific (1.0-java) + platform_specific (1.0-x86-mswin32) + + PLATFORMS + ruby + + DEPENDENCIES + platform_specific + G + + simulate_platform "java" + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "platform_specific" + G + + expect(the_bundle).to include_gems "platform_specific 1.0 JAVA" + end + + it "works with gems that have different dependencies" do + simulate_platform "java" + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "nokogiri" + G + + expect(the_bundle).to include_gems "nokogiri 1.4.2 JAVA", "weakling 0.0.3" + + simulate_new_machine + + simulate_platform "ruby" + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "nokogiri" + G + + expect(the_bundle).to include_gems "nokogiri 1.4.2" + expect(the_bundle).not_to include_gems "weakling" + end + + it "works the other way with gems that have different dependencies" do + simulate_platform "ruby" + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "nokogiri" + G + + simulate_platform "java" + bundle "install" + + expect(the_bundle).to include_gems "nokogiri 1.4.2 JAVA", "weakling 0.0.3" + end + + it "works with gems that have extra platform-specific runtime dependencies" do + simulate_platform x64_mac + + update_repo2 do + build_gem "facter", "2.4.6" + build_gem "facter", "2.4.6" do |s| + s.platform = "universal-darwin" + s.add_runtime_dependency "CFPropertyList" + end + build_gem "CFPropertyList" + end + + install_gemfile! <<-G + source "file://#{gem_repo2}" + + gem "facter" + G + + expect(out).to include "Unable to use the platform-specific (universal-darwin) version of facter (2.4.6) " \ + "because it has different dependencies from the ruby version. " \ + "To use the platform-specific version of the gem, run `bundle config specific_platform true` and install again." + + expect(the_bundle).to include_gem "facter 2.4.6" + expect(the_bundle).not_to include_gem "CFPropertyList" + end + + it "fetches gems again after changing the version of Ruby" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "1.0.0" + G + + bundle "install --path vendor/bundle" + + new_version = Gem::ConfigMap[:ruby_version] == "1.8" ? "1.9.1" : "1.8" + FileUtils.mv(vendored_gems, bundled_app("vendor/bundle", Gem.ruby_engine, new_version)) + + bundle "install --path vendor/bundle" + expect(vendored_gems("gems/rack-1.0.0")).to exist + end +end + +RSpec.describe "bundle install with platform conditionals" do + it "installs gems tagged w/ the current platforms" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + platforms :#{local_tag} do + gem "nokogiri" + end + G + + expect(the_bundle).to include_gems "nokogiri 1.4.2" + end + + it "does not install gems tagged w/ another platforms" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + platforms :#{not_local_tag} do + gem "nokogiri" + end + G + + expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).not_to include_gems "nokogiri 1.4.2" + end + + it "installs gems tagged w/ the current platforms inline" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "nokogiri", :platforms => :#{local_tag} + G + expect(the_bundle).to include_gems "nokogiri 1.4.2" + end + + it "does not install gems tagged w/ another platforms inline" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "nokogiri", :platforms => :#{not_local_tag} + G + expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).not_to include_gems "nokogiri 1.4.2" + end + + it "installs gems tagged w/ the current platform inline" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "nokogiri", :platform => :#{local_tag} + G + expect(the_bundle).to include_gems "nokogiri 1.4.2" + end + + it "doesn't install gems tagged w/ another platform inline" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "nokogiri", :platform => :#{not_local_tag} + G + expect(the_bundle).not_to include_gems "nokogiri 1.4.2" + end + + it "does not blow up on sources with all platform-excluded specs" do + build_git "foo" + + install_gemfile <<-G + platform :#{not_local_tag} do + gem "foo", :git => "#{lib_path("foo-1.0")}" + end + G + + bundle :show + expect(exitstatus).to eq(0) if exitstatus + end + + it "does not attempt to install gems from :rbx when using --local" do + simulate_platform "ruby" + simulate_ruby_engine "ruby" + + gemfile <<-G + source "file://#{gem_repo1}" + gem "some_gem", :platform => :rbx + G + + bundle "install --local" + expect(out).not_to match(/Could not find gem 'some_gem/) + end + + it "does not attempt to install gems from other rubies when using --local" do + simulate_platform "ruby" + simulate_ruby_engine "ruby" + other_ruby_version_tag = RUBY_VERSION =~ /^1\.8/ ? :ruby_19 : :ruby_18 + + gemfile <<-G + source "file://#{gem_repo1}" + gem "some_gem", platform: :#{other_ruby_version_tag} + G + + bundle "install --local" + expect(out).not_to match(/Could not find gem 'some_gem/) + end + + it "prints a helpful warning when a dependency is unused on any platform" do + simulate_platform "ruby" + simulate_ruby_engine "ruby" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", :platform => [:mingw, :mswin, :x64_mingw, :jruby] + G + + bundle! "install" + + expect(out).to include <<-O.strip +The dependency #{Gem::Dependency.new("rack", ">= 0")} will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`. + O + end +end + +RSpec.describe "when a gem has no architecture" do + it "still installs correctly" do + simulate_platform mswin + + gemfile <<-G + # Try to install gem with nil arch + source "http://localgemserver.test/" + gem "rcov" + G + + bundle :install, :artifice => "windows" + expect(the_bundle).to include_gems "rcov 1.0.0" + end +end diff --git a/spec/bundler/install/gemfile/ruby_spec.rb b/spec/bundler/install/gemfile/ruby_spec.rb new file mode 100644 index 0000000000..b9d9683758 --- /dev/null +++ b/spec/bundler/install/gemfile/ruby_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "ruby requirement" do + def locked_ruby_version + Bundler::RubyVersion.from_string(Bundler::LockfileParser.new(lockfile).ruby_version) + end + + # As discovered by https://github.com/bundler/bundler/issues/4147, there is + # no test coverage to ensure that adding a gem is possible with a ruby + # requirement. This test verifies the fix, committed in bfbad5c5. + it "allows adding gems" do + install_gemfile <<-G + source "file://#{gem_repo1}" + ruby "#{RUBY_VERSION}" + gem "rack" + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + ruby "#{RUBY_VERSION}" + gem "rack" + gem "rack-obama" + G + + expect(exitstatus).to eq(0) if exitstatus + expect(the_bundle).to include_gems "rack-obama 1.0" + end + + it "allows removing the ruby version requirement" do + install_gemfile <<-G + source "file://#{gem_repo1}" + ruby "~> #{RUBY_VERSION}" + gem "rack" + G + + expect(lockfile).to include("RUBY VERSION") + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + expect(the_bundle).to include_gems "rack 1.0.0" + expect(lockfile).not_to include("RUBY VERSION") + end + + it "allows changing the ruby version requirement to something compatible" do + install_gemfile <<-G + source "file://#{gem_repo1}" + ruby ">= 1.0.0" + gem "rack" + G + + expect(locked_ruby_version).to eq(Bundler::RubyVersion.system) + + simulate_ruby_version "5100" + + install_gemfile <<-G + source "file://#{gem_repo1}" + ruby ">= 1.0.1" + gem "rack" + G + + expect(the_bundle).to include_gems "rack 1.0.0" + expect(locked_ruby_version).to eq(Bundler::RubyVersion.system) + end + + it "allows changing the ruby version requirement to something incompatible" do + install_gemfile <<-G + source "file://#{gem_repo1}" + ruby ">= 1.0.0" + gem "rack" + G + + expect(locked_ruby_version).to eq(Bundler::RubyVersion.system) + + simulate_ruby_version "5100" + + install_gemfile <<-G + source "file://#{gem_repo1}" + ruby ">= 5000.0" + gem "rack" + G + + expect(the_bundle).to include_gems "rack 1.0.0" + expect(locked_ruby_version.versions).to eq(["5100"]) + end + + it "allows requirements with trailing whitespace" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + ruby "#{RUBY_VERSION}\\n \t\\n" + gem "rack" + G + + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "fails gracefully with malformed requirements" do + install_gemfile <<-G + source "file://#{gem_repo1}" + ruby ">= 0", "-.\\0" + gem "rack" + G + + expect(out).to include("There was an error parsing") # i.e. DSL error, not error template + end +end diff --git a/spec/bundler/install/gemfile/sources_spec.rb b/spec/bundler/install/gemfile/sources_spec.rb new file mode 100644 index 0000000000..c5375b4abf --- /dev/null +++ b/spec/bundler/install/gemfile/sources_spec.rb @@ -0,0 +1,518 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install with gems on multiple sources" do + # repo1 is built automatically before all of the specs run + # it contains rack-obama 1.0.0 and rack 0.9.1 & 1.0.0 amongst other gems + + context "without source affinity" do + before do + # Oh no! Someone evil is trying to hijack rack :( + # need this to be broken to check for correct source ordering + build_repo gem_repo3 do + build_gem "rack", repo3_rack_version do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + end + end + + context "with multiple toplevel sources" do + let(:repo3_rack_version) { "1.0.0" } + + before do + gemfile <<-G + source "file://#{gem_repo3}" + source "file://#{gem_repo1}" + gem "rack-obama" + gem "rack" + G + bundle "config major_deprecations true" + end + + it "warns about ambiguous gems, but installs anyway, prioritizing sources last to first" do + bundle :install + + expect(out).to have_major_deprecation a_string_including("Your Gemfile contains multiple primary sources.") + expect(out).to include("Warning: the gem 'rack' was found in multiple sources.") + expect(out).to include("Installed from: file:#{gem_repo1}") + expect(the_bundle).to include_gems("rack-obama 1.0.0", "rack 1.0.0", :source => "remote1") + end + + it "errors when disable_multisource is set" do + bundle "config disable_multisource true" + bundle :install + expect(out).to include("Each source after the first must include a block") + expect(exitstatus).to eq(4) if exitstatus + end + end + + context "when different versions of the same gem are in multiple sources" do + let(:repo3_rack_version) { "1.2" } + + before do + gemfile <<-G + source "file://#{gem_repo3}" + source "file://#{gem_repo1}" + gem "rack-obama" + gem "rack", "1.0.0" # force it to install the working version in repo1 + G + bundle "config major_deprecations true" + end + + it "warns about ambiguous gems, but installs anyway" do + bundle :install + + expect(out).to have_major_deprecation a_string_including("Your Gemfile contains multiple primary sources.") + expect(out).to include("Warning: the gem 'rack' was found in multiple sources.") + expect(out).to include("Installed from: file:#{gem_repo1}") + expect(the_bundle).to include_gems("rack-obama 1.0.0", "rack 1.0.0", :source => "remote1") + end + end + end + + context "with source affinity" do + context "with sources given by a block" do + before do + # Oh no! Someone evil is trying to hijack rack :( + # need this to be broken to check for correct source ordering + build_repo gem_repo3 do + build_gem "rack", "1.0.0" do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + end + + gemfile <<-G + source "file://#{gem_repo3}" + source "file://#{gem_repo1}" do + gem "thin" # comes first to test name sorting + gem "rack" + end + gem "rack-obama" # shoud come from repo3! + G + end + + it "installs the gems without any warning" do + bundle :install + expect(out).not_to include("Warning") + expect(the_bundle).to include_gems("rack-obama 1.0.0") + expect(the_bundle).to include_gems("rack 1.0.0", :source => "remote1") + end + + it "can cache and deploy" do + bundle :package + + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/rack-obama-1.0.gem")).to exist + + bundle "install --deployment" + + expect(exitstatus).to eq(0) if exitstatus + expect(the_bundle).to include_gems("rack-obama 1.0.0", "rack 1.0.0") + end + end + + context "with sources set by an option" do + before do + # Oh no! Someone evil is trying to hijack rack :( + # need this to be broken to check for correct source ordering + build_repo gem_repo3 do + build_gem "rack", "1.0.0" do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + end + + gemfile <<-G + source "file://#{gem_repo3}" + gem "rack-obama" # should come from repo3! + gem "rack", :source => "file://#{gem_repo1}" + G + end + + it "installs the gems without any warning" do + bundle :install + expect(out).not_to include("Warning") + expect(the_bundle).to include_gems("rack-obama 1.0.0", "rack 1.0.0") + end + end + + context "with an indirect dependency" do + before do + build_repo gem_repo3 do + build_gem "depends_on_rack", "1.0.1" do |s| + s.add_dependency "rack" + end + end + end + + context "when the indirect dependency is in the pinned source" do + before do + # we need a working rack gem in repo3 + update_repo gem_repo3 do + build_gem "rack", "1.0.0" + end + + gemfile <<-G + source "file://#{gem_repo2}" + source "file://#{gem_repo3}" do + gem "depends_on_rack" + end + G + end + + context "and not in any other sources" do + before do + build_repo(gem_repo2) {} + end + + it "installs from the same source without any warning" do + bundle :install + expect(out).not_to include("Warning") + expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + end + end + + context "and in another source" do + before do + # need this to be broken to check for correct source ordering + build_repo gem_repo2 do + build_gem "rack", "1.0.0" do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + end + end + + it "installs from the same source without any warning" do + bundle :install + expect(out).not_to include("Warning") + expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + end + end + end + + context "when the indirect dependency is in a different source" do + before do + # In these tests, we need a working rack gem in repo2 and not repo3 + build_repo gem_repo2 do + build_gem "rack", "1.0.0" + end + end + + context "and not in any other sources" do + before do + gemfile <<-G + source "file://#{gem_repo2}" + source "file://#{gem_repo3}" do + gem "depends_on_rack" + end + G + end + + it "installs from the other source without any warning" do + bundle :install + expect(out).not_to include("Warning") + expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + end + end + + context "and in yet another source" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + source "file://#{gem_repo2}" + source "file://#{gem_repo3}" do + gem "depends_on_rack" + end + G + end + + it "installs from the other source and warns about ambiguous gems" do + bundle "config major_deprecations true" + bundle :install + expect(out).to have_major_deprecation a_string_including("Your Gemfile contains multiple primary sources.") + expect(out).to include("Warning: the gem 'rack' was found in multiple sources.") + expect(out).to include("Installed from: file:#{gem_repo2}") + expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + end + end + + context "and only the dependency is pinned" do + before do + # need this to be broken to check for correct source ordering + build_repo gem_repo2 do + build_gem "rack", "1.0.0" do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + end + + gemfile <<-G + source "file://#{gem_repo3}" # contains depends_on_rack + source "file://#{gem_repo2}" # contains broken rack + + gem "depends_on_rack" # installed from gem_repo3 + gem "rack", :source => "file://#{gem_repo1}" + G + end + + it "installs the dependency from the pinned source without warning" do + bundle :install + + expect(out).not_to include("Warning: the gem 'rack' was found in multiple sources.") + expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + + # In https://github.com/bundler/bundler/issues/3585 this failed + # when there is already a lock file, and the gems are missing, so try again + system_gems [] + bundle :install + + expect(out).not_to include("Warning: the gem 'rack' was found in multiple sources.") + expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + end + end + end + end + + context "with a gem that is only found in the wrong source" do + before do + build_repo gem_repo3 do + build_gem "not_in_repo1", "1.0.0" + end + + gemfile <<-G + source "file://#{gem_repo3}" + gem "not_in_repo1", :source => "file://#{gem_repo1}" + G + end + + it "does not install the gem" do + bundle :install + expect(out).to include("Could not find gem 'not_in_repo1'") + end + end + + context "with an existing lockfile" do + before do + system_gems "rack-0.9.1", "rack-1.0.0" + + lockfile <<-L + GEM + remote: file:#{gem_repo1} + remote: file:#{gem_repo3} + specs: + rack (0.9.1) + + PLATFORMS + ruby + + DEPENDENCIES + rack! + L + + gemfile <<-G + source "file://#{gem_repo1}" + source "file://#{gem_repo3}" do + gem 'rack' + end + G + end + + # Reproduction of https://github.com/bundler/bundler/issues/3298 + it "does not unlock the installed gem on exec" do + expect(the_bundle).to include_gems("rack 0.9.1") + end + end + + context "with a path gem in the same Gemfile" do + before do + build_lib "foo" + + gemfile <<-G + gem "rack", :source => "file://#{gem_repo1}" + gem "foo", :path => "#{lib_path("foo-1.0")}" + G + end + + it "does not unlock the non-path gem after install" do + bundle :install + + bundle %(exec ruby -e 'puts "OK"') + + expect(out).to include("OK") + expect(exitstatus).to eq(0) if exitstatus + end + end + end + + context "when an older version of the same gem also ships with Ruby" do + before do + system_gems "rack-0.9.1" + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" # shoud come from repo1! + G + end + + it "installs the gems without any warning" do + bundle :install + expect(out).not_to include("Warning") + expect(the_bundle).to include_gems("rack 1.0.0") + end + end + + context "when a single source contains multiple locked gems" do + before do + # 1. With these gems, + build_repo4 do + build_gem "foo", "0.1" + build_gem "bar", "0.1" + end + + # 2. Installing this gemfile will produce... + gemfile <<-G + source 'file://#{gem_repo1}' + gem 'rack' + gem 'foo', '~> 0.1', :source => 'file://#{gem_repo4}' + gem 'bar', '~> 0.1', :source => 'file://#{gem_repo4}' + G + + # 3. this lockfile. + lockfile <<-L + GEM + remote: file:/Users/andre/src/bundler/bundler/tmp/gems/remote1/ + remote: file:/Users/andre/src/bundler/bundler/tmp/gems/remote4/ + specs: + bar (0.1) + foo (0.1) + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + bar (~> 0.1)! + foo (~> 0.1)! + rack + L + + bundle "install --path ../gems/system" + + # 4. Then we add some new versions... + update_repo4 do + build_gem "foo", "0.2" + build_gem "bar", "0.3" + end + end + + it "allows them to be unlocked separately" do + # 5. and install this gemfile, updating only foo. + install_gemfile <<-G + source 'file://#{gem_repo1}' + gem 'rack' + gem 'foo', '~> 0.2', :source => 'file://#{gem_repo4}' + gem 'bar', '~> 0.1', :source => 'file://#{gem_repo4}' + G + + # 6. Which should update foo to 0.2, but not the (locked) bar 0.1 + expect(the_bundle).to include_gems("foo 0.2") + expect(the_bundle).to include_gems("bar 0.1") + end + end + + context "re-resolving" do + context "when there is a mix of sources in the gemfile" do + before do + build_repo3 + build_lib "path1" + build_lib "path2" + build_git "git1" + build_git "git2" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + + source "file://#{gem_repo3}" do + gem "rack" + end + + gem "path1", :path => "#{lib_path("path1-1.0")}" + gem "path2", :path => "#{lib_path("path2-1.0")}" + gem "git1", :git => "#{lib_path("git1-1.0")}" + gem "git2", :git => "#{lib_path("git2-1.0")}" + G + end + + it "does not re-resolve" do + bundle :install, :verbose => true + expect(out).to include("using resolution from the lockfile") + expect(out).not_to include("re-resolving dependencies") + end + end + end + + context "when a gem is installed to system gems" do + before do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + context "and the gemfile changes" do + it "is still able to find that gem from remote sources" do + source_uri = "file://#{gem_repo1}" + second_uri = "file://#{gem_repo4}" + + build_repo4 do + build_gem "rack", "2.0.1.1.forked" + build_gem "thor", "0.19.1.1.forked" + end + + # When this gemfile is installed... + gemfile <<-G + source "#{source_uri}" + + source "#{second_uri}" do + gem "rack", "2.0.1.1.forked" + gem "thor" + end + gem "rack-obama" + G + + # It creates this lockfile. + lockfile <<-L + GEM + remote: #{source_uri}/ + remote: #{second_uri}/ + specs: + rack (2.0.1.1.forked) + rack-obama (1.0) + rack + thor (0.19.1.1.forked) + + PLATFORMS + ruby + + DEPENDENCIES + rack (= 2.0.1.1.forked)! + rack-obama + thor! + L + + # Then we change the Gemfile by adding a version to thor + gemfile <<-G + source "#{source_uri}" + + source "#{second_uri}" do + gem "rack", "2.0.1.1.forked" + gem "thor", "0.19.1.1.forked" + end + gem "rack-obama" + G + + # But we should still be able to find rack 2.0.1.1.forked and install it + bundle! :install + end + end + end +end diff --git a/spec/bundler/install/gemfile/specific_platform_spec.rb b/spec/bundler/install/gemfile/specific_platform_spec.rb new file mode 100644 index 0000000000..cc6c82c0ff --- /dev/null +++ b/spec/bundler/install/gemfile/specific_platform_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install with specific_platform enabled" do + before do + bundle "config specific_platform true" + + build_repo2 do + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x86_64-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x86-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x86-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x64-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "universal-darwin" } + + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x86_64-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x86-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x64-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x86-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") + + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "universal-darwin" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "x86_64-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "x86-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "x86-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "x64-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") + + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "x86_64-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "x86-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "x86-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "x64-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "universal-darwin" } + + build_gem("google-protobuf", "3.0.0.alpha.4.0") + build_gem("google-protobuf", "3.0.0.alpha.3.1.pre") + build_gem("google-protobuf", "3.0.0.alpha.3") + build_gem("google-protobuf", "3.0.0.alpha.2.0") + build_gem("google-protobuf", "3.0.0.alpha.1.1") + build_gem("google-protobuf", "3.0.0.alpha.1.0") + + build_gem("facter", "2.4.6") + build_gem("facter", "2.4.6") do |s| + s.platform = "universal-darwin" + s.add_runtime_dependency "CFPropertyList" + end + build_gem("CFPropertyList") + end + end + + let(:google_protobuf) { <<-G } + source "file:#{gem_repo2}" + gem "google-protobuf" + G + + context "when on a darwin machine" do + before { simulate_platform "x86_64-darwin-15" } + + it "locks to both the specific darwin platform and ruby" do + install_gemfile!(google_protobuf) + expect(the_bundle.locked_gems.platforms).to eq([pl("ruby"), pl("x86_64-darwin-15")]) + expect(the_bundle).to include_gem("google-protobuf 3.0.0.alpha.5.0.5.1 universal-darwin") + expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w( + google-protobuf-3.0.0.alpha.5.0.5.1 + google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin + )) + end + + it "caches both the universal-darwin and ruby gems when --all-platforms is passed" do + gemfile(google_protobuf) + bundle! "package --all-platforms" + expect([cached_gem("google-protobuf-3.0.0.alpha.5.0.5.1"), cached_gem("google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin")]). + to all(exist) + end + + it "uses the platform-specific gem with extra dependencies" do + install_gemfile! <<-G + source "file:#{gem_repo2}" + gem "facter" + G + + expect(the_bundle.locked_gems.platforms).to eq([pl("ruby"), pl("x86_64-darwin-15")]) + expect(the_bundle).to include_gems("facter 2.4.6 universal-darwin", "CFPropertyList 1.0") + expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(["CFPropertyList-1.0", + "facter-2.4.6", + "facter-2.4.6-universal-darwin"]) + end + + context "when adding a platform via lock --add_platform" do + it "adds the foreign platform" do + install_gemfile!(google_protobuf) + bundle! "lock --add-platform=#{x64_mingw}" + + expect(the_bundle.locked_gems.platforms).to eq([rb, x64_mingw, pl("x86_64-darwin-15")]) + expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w( + google-protobuf-3.0.0.alpha.5.0.5.1 + google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin + google-protobuf-3.0.0.alpha.5.0.5.1-x64-mingw32 + )) + end + + it "falls back on plain ruby when that version doesnt have a platform-specific gem" do + install_gemfile!(google_protobuf) + bundle! "lock --add-platform=#{java}" + + expect(the_bundle.locked_gems.platforms).to eq([java, rb, pl("x86_64-darwin-15")]) + expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w( + google-protobuf-3.0.0.alpha.5.0.5.1 + google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin + )) + end + end + end +end diff --git a/spec/bundler/install/gemfile_spec.rb b/spec/bundler/install/gemfile_spec.rb new file mode 100644 index 0000000000..bc49053081 --- /dev/null +++ b/spec/bundler/install/gemfile_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install" do + context "with duplicated gems" do + it "will display a warning" do + install_gemfile <<-G + gem 'rails', '~> 4.0.0' + gem 'rails', '~> 4.0.0' + G + expect(out).to include("more than once") + end + end + + context "with --gemfile" do + it "finds the gemfile" do + gemfile bundled_app("NotGemfile"), <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + bundle :install, :gemfile => bundled_app("NotGemfile") + + ENV["BUNDLE_GEMFILE"] = "NotGemfile" + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + context "with gemfile set via config" do + before do + gemfile bundled_app("NotGemfile"), <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + bundle "config --local gemfile #{bundled_app("NotGemfile")}" + end + it "uses the gemfile to install" do + bundle "install" + bundle "show" + + expect(out).to include("rack (1.0.0)") + end + it "uses the gemfile while in a subdirectory" do + bundled_app("subdir").mkpath + Dir.chdir(bundled_app("subdir")) do + bundle "install" + bundle "show" + + expect(out).to include("rack (1.0.0)") + end + end + end + + context "with deprecated features" do + before :each do + in_app_root + end + + it "reports that lib is an invalid option" do + gemfile <<-G + gem "rack", :lib => "rack" + G + + bundle :install + expect(out).to match(/You passed :lib as an option for gem 'rack', but it is invalid/) + end + end + + context "with engine specified in symbol" do + it "does not raise any error parsing Gemfile" do + simulate_ruby_version "2.3.0" do + simulate_ruby_engine "jruby", "9.1.2.0" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + ruby "2.3.0", :engine => :jruby, :engine_version => "9.1.2.0" + G + + expect(out).to match(/Bundle complete!/) + end + end + end + + it "installation succeeds" do + simulate_ruby_version "2.3.0" do + simulate_ruby_engine "jruby", "9.1.2.0" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + ruby "2.3.0", :engine => :jruby, :engine_version => "9.1.2.0" + gem "rack" + G + + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + end + end +end diff --git a/spec/bundler/install/gems/compact_index_spec.rb b/spec/bundler/install/gems/compact_index_spec.rb new file mode 100644 index 0000000000..e9e671105a --- /dev/null +++ b/spec/bundler/install/gems/compact_index_spec.rb @@ -0,0 +1,805 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "compact index api" do + let(:source_hostname) { "localgemserver.test" } + let(:source_uri) { "http://#{source_hostname}" } + + it "should use the API" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle! :install, :artifice => "compact_index" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "should URI encode gem names" do + gemfile <<-G + source "#{source_uri}" + gem " sinatra" + G + + bundle :install, :artifice => "compact_index" + expect(out).to include("' sinatra' is not a valid gem name because it contains whitespace.") + end + + it "should handle nested dependencies" do + gemfile <<-G + source "#{source_uri}" + gem "rails" + G + + bundle! :install, :artifice => "compact_index" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems( + "rails 2.3.2", + "actionpack 2.3.2", + "activerecord 2.3.2", + "actionmailer 2.3.2", + "activeresource 2.3.2", + "activesupport 2.3.2" + ) + end + + it "should handle case sensitivity conflicts" do + build_repo4 do + build_gem "rack", "1.0" do |s| + s.add_runtime_dependency("Rack", "0.1") + end + build_gem "Rack", "0.1" + end + + install_gemfile! <<-G, :artifice => "compact_index", :env => { "BUNDLER_SPEC_GEM_REPO" => gem_repo4 } + source "#{source_uri}" + gem "rack", "1.0" + gem "Rack", "0.1" + G + + # can't use `include_gems` here since the `require` will conflict on a + # case-insensitive FS + run! "Bundler.require; puts Gem.loaded_specs.values_at('rack', 'Rack').map(&:full_name)" + expect(out).to eq("rack-1.0\nRack-0.1") + end + + it "should handle multiple gem dependencies on the same gem" do + gemfile <<-G + source "#{source_uri}" + gem "net-sftp" + G + + bundle! :install, :artifice => "compact_index" + expect(the_bundle).to include_gems "net-sftp 1.1.1" + end + + it "should use the endpoint when using --deployment" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + bundle! :install, :artifice => "compact_index" + + bundle "install --deployment", :artifice => "compact_index" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "handles git dependencies that are in rubygems" do + build_git "foo" do |s| + s.executables = "foobar" + s.add_dependency "rails", "2.3.2" + end + + gemfile <<-G + source "#{source_uri}" + git "file:///#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + + bundle! :install, :artifice => "compact_index" + + expect(the_bundle).to include_gems("rails 2.3.2") + end + + it "handles git dependencies that are in rubygems using --deployment" do + build_git "foo" do |s| + s.executables = "foobar" + s.add_dependency "rails", "2.3.2" + end + + gemfile <<-G + source "#{source_uri}" + gem 'foo', :git => "file:///#{lib_path("foo-1.0")}" + G + + bundle! :install, :artifice => "compact_index" + + bundle "install --deployment", :artifice => "compact_index" + + expect(the_bundle).to include_gems("rails 2.3.2") + end + + it "doesn't fail if you only have a git gem with no deps when using --deployment" do + build_git "foo" + gemfile <<-G + source "#{source_uri}" + gem 'foo', :git => "file:///#{lib_path("foo-1.0")}" + G + + bundle "install", :artifice => "compact_index" + bundle "install --deployment", :artifice => "compact_index" + + expect(exitstatus).to eq(0) if exitstatus + expect(the_bundle).to include_gems("foo 1.0") + end + + it "falls back when the API errors out" do + simulate_platform mswin + + gemfile <<-G + source "#{source_uri}" + gem "rcov" + G + + bundle! :install, :artifice => "windows" + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "rcov 1.0.0" + end + + it "falls back when the API URL returns 403 Forbidden" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle! :install, :verbose => true, :artifice => "compact_index_forbidden" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "falls back when the versions endpoint has a checksum mismatch" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle! :install, :verbose => true, :artifice => "compact_index_checksum_mismatch" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(out).to include <<-'WARN' +The checksum of /versions does not match the checksum provided by the server! Something is wrong (local checksum is "\"d41d8cd98f00b204e9800998ecf8427e\"", was expecting "\"123\""). + WARN + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "falls back when the user's home directory does not exist or is not writable" do + ENV["HOME"] = nil + + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle! :install, :artifice => "compact_index" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "handles host redirects" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle! :install, :artifice => "compact_index_host_redirect" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "handles host redirects without Net::HTTP::Persistent" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + FileUtils.mkdir_p lib_path + File.open(lib_path("disable_net_http_persistent.rb"), "w") do |h| + h.write <<-H + module Kernel + alias require_without_disabled_net_http require + def require(*args) + raise LoadError, 'simulated' if args.first == 'openssl' && !caller.grep(/vendored_persistent/).empty? + require_without_disabled_net_http(*args) + end + end + H + end + + bundle! :install, :artifice => "compact_index_host_redirect", :requires => [lib_path("disable_net_http_persistent.rb")] + expect(out).to_not match(/Too many redirects/) + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "times out when Bundler::Fetcher redirects too much" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle :install, :artifice => "compact_index_redirects" + expect(out).to match(/Too many redirects/) + end + + context "when --full-index is specified" do + it "should use the modern index for install" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "install --full-index", :artifice => "compact_index" + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "should use the modern index for update" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "update --full-index", :artifice => "compact_index" + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + it "fetches again when more dependencies are found in subsequent sources" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" + gem "back_deps" + G + + bundle! :install, :artifice => "compact_index_extra" + expect(the_bundle).to include_gems "back_deps 1.0" + end + + it "fetches gem versions even when those gems are already installed" do + gemfile <<-G + source "#{source_uri}" + gem "rack", "1.0.0" + G + bundle! :install, :artifice => "compact_index_extra_api" + expect(the_bundle).to include_gems "rack 1.0.0" + + build_repo4 do + build_gem "rack", "1.2" do |s| + s.executables = "rackup" + end + end + + gemfile <<-G + source "#{source_uri}" do; end + source "#{source_uri}/extra" + gem "rack", "1.2" + G + bundle! :install, :artifice => "compact_index_extra_api" + expect(the_bundle).to include_gems "rack 1.2" + end + + it "considers all possible versions of dependencies from all api gem sources" do + # In this scenario, the gem "somegem" only exists in repo4. It depends on specific version of activesupport that + # exists only in repo1. There happens also be a version of activesupport in repo4, but not the one that version 1.0.0 + # of somegem wants. This test makes sure that bundler actually finds version 1.2.3 of active support in the other + # repo and installs it. + build_repo4 do + build_gem "activesupport", "1.2.0" + build_gem "somegem", "1.0.0" do |s| + s.add_dependency "activesupport", "1.2.3" # This version exists only in repo1 + end + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" + gem 'somegem', '1.0.0' + G + + bundle! :install, :artifice => "compact_index_extra_api" + + expect(the_bundle).to include_gems "somegem 1.0.0" + expect(the_bundle).to include_gems "activesupport 1.2.3" + end + + it "prints API output properly with back deps" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" + gem "back_deps" + G + + bundle! :install, :artifice => "compact_index_extra" + + expect(out).to include("Fetching gem metadata from http://localgemserver.test/") + expect(out).to include("Fetching source index from http://localgemserver.test/extra") + end + + it "does not fetch every spec if the index of gems is large when doing back deps" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + build_gem "missing" + # need to hit the limit + 1.upto(Bundler::Source::Rubygems::API_REQUEST_LIMIT) do |i| + build_gem "gem#{i}" + end + + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" + gem "back_deps" + G + + bundle! :install, :artifice => "compact_index_extra_missing" + expect(the_bundle).to include_gems "back_deps 1.0" + end + + it "uses the endpoint if all sources support it" do + gemfile <<-G + source "#{source_uri}" + + gem 'foo' + G + + bundle! :install, :artifice => "compact_index_api_missing" + expect(the_bundle).to include_gems "foo 1.0" + end + + it "fetches again when more dependencies are found in subsequent sources using --deployment" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" + gem "back_deps" + G + + bundle! :install, :artifice => "compact_index_extra" + + bundle "install --deployment", :artifice => "compact_index_extra" + expect(the_bundle).to include_gems "back_deps 1.0" + end + + it "does not refetch if the only unmet dependency is bundler" do + gemfile <<-G + source "#{source_uri}" + + gem "bundler_dep" + G + + bundle! :install, :artifice => "compact_index" + expect(out).to include("Fetching gem metadata from #{source_uri}") + end + + it "should install when EndpointSpecification has a bin dir owned by root", :sudo => true do + sudo "mkdir -p #{system_gem_path("bin")}" + sudo "chown -R root #{system_gem_path("bin")}" + + gemfile <<-G + source "#{source_uri}" + gem "rails" + G + bundle! :install, :artifice => "compact_index" + expect(the_bundle).to include_gems "rails 2.3.2" + end + + it "installs the binstubs" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "install --binstubs", :artifice => "compact_index" + + gembin "rackup" + expect(out).to eq("1.0.0") + end + + it "installs the bins when using --path and uses autoclean" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "install --path vendor/bundle", :artifice => "compact_index" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "installs the bins when using --path and uses bundle clean" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "install --path vendor/bundle --no-clean", :artifice => "compact_index" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "prints post_install_messages" do + gemfile <<-G + source "#{source_uri}" + gem 'rack-obama' + G + + bundle! :install, :artifice => "compact_index" + expect(out).to include("Post-install message from rack:") + end + + it "should display the post install message for a dependency" do + gemfile <<-G + source "#{source_uri}" + gem 'rack_middleware' + G + + bundle! :install, :artifice => "compact_index" + expect(out).to include("Post-install message from rack:") + expect(out).to include("Rack's post install message") + end + + context "when using basic authentication" do + let(:user) { "user" } + let(:password) { "pass" } + let(:basic_auth_source_uri) do + uri = URI.parse(source_uri) + uri.user = user + uri.password = password + + uri + end + + it "passes basic authentication details and strips out creds" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle! :install, :artifice => "compact_index_basic_authentication" + expect(out).not_to include("#{user}:#{password}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "strips http basic authentication creds for modern index" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle! :install, :artifice => "endopint_marshal_fail_basic_authentication" + expect(out).not_to include("#{user}:#{password}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "strips http basic auth creds when it can't reach the server" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endpoint_500" + expect(out).not_to include("#{user}:#{password}") + end + + it "strips http basic auth creds when warning about ambiguous sources" do + gemfile <<-G + source "#{basic_auth_source_uri}" + source "file://#{gem_repo1}" + gem "rack" + G + + bundle! :install, :artifice => "compact_index_basic_authentication" + expect(out).to include("Warning: the gem 'rack' was found in multiple sources.") + expect(out).not_to include("#{user}:#{password}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "does not pass the user / password to different hosts on redirect" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle! :install, :artifice => "compact_index_creds_diff_host" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + describe "with authentication details in bundle config" do + before do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + end + + it "reads authentication details by host name from bundle config" do + bundle "config #{source_hostname} #{user}:#{password}" + + bundle! :install, :artifice => "compact_index_strict_basic_authentication" + + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "reads authentication details by full url from bundle config" do + # The trailing slash is necessary here; Fetcher canonicalizes the URI. + bundle "config #{source_uri}/ #{user}:#{password}" + + bundle! :install, :artifice => "compact_index_strict_basic_authentication" + + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "should use the API" do + bundle "config #{source_hostname} #{user}:#{password}" + bundle! :install, :artifice => "compact_index_strict_basic_authentication" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "prefers auth supplied in the source uri" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle "config #{source_hostname} otheruser:wrong" + + bundle! :install, :artifice => "compact_index_strict_basic_authentication" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "shows instructions if auth is not provided for the source" do + bundle :install, :artifice => "compact_index_strict_basic_authentication" + expect(out).to include("bundle config #{source_hostname} username:password") + end + + it "fails if authentication has already been provided, but failed" do + bundle "config #{source_hostname} #{user}:wrong" + + bundle :install, :artifice => "compact_index_strict_basic_authentication" + expect(out).to include("Bad username or password") + end + end + + describe "with no password" do + let(:password) { nil } + + it "passes basic authentication details" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle! :install, :artifice => "compact_index_basic_authentication" + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + end + + context "when ruby is compiled without openssl", :ruby_repo do + before do + # Install a monkeypatch that reproduces the effects of openssl being + # missing when the fetcher runs, as happens in real life. The reason + # we can't just overwrite openssl.rb is that Artifice uses it. + bundled_app("broken_ssl").mkpath + bundled_app("broken_ssl/openssl.rb").open("w") do |f| + f.write <<-RUBY + raise LoadError, "cannot load such file -- openssl" + RUBY + end + end + + it "explains what to do to get it" do + gemfile <<-G + source "#{source_uri.gsub(/http/, "https")}" + gem "rack" + G + + bundle :install, :env => { "RUBYOPT" => "-I#{bundled_app("broken_ssl")}" } + expect(out).to include("OpenSSL") + end + end + + context "when SSL certificate verification fails" do + it "explains what happened" do + # Install a monkeypatch that reproduces the effects of openssl raising + # a certificate validation error when Rubygems tries to connect. + gemfile <<-G + class Net::HTTP + def start + raise OpenSSL::SSL::SSLError, "certificate verify failed" + end + end + + source "#{source_uri.gsub(/http/, "https")}" + gem "rack" + G + + bundle :install + expect(out).to match(/could not verify the SSL certificate/i) + end + end + + context ".gemrc with sources is present" do + before do + File.open(home(".gemrc"), "w") do |file| + file.puts({ :sources => ["https://rubygems.org"] }.to_yaml) + end + end + + after do + home(".gemrc").rmtree + end + + it "uses other sources declared in the Gemfile" do + gemfile <<-G + source "#{source_uri}" + gem 'rack' + G + + bundle! :install, :artifice => "compact_index_forbidden" + end + end + + it "performs partial update with a non-empty range" do + gemfile <<-G + source "#{source_uri}" + gem 'rack', '0.9.1' + G + + # Initial install creates the cached versions file + bundle! :install, :artifice => "compact_index" + + # Update the Gemfile so we can check subsequent install was successful + gemfile <<-G + source "#{source_uri}" + gem 'rack', '1.0.0' + G + + # Second install should make only a partial request to /versions + bundle! :install, :artifice => "compact_index_partial_update" + + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "performs partial update while local cache is updated by another process" do + gemfile <<-G + source "#{source_uri}" + gem 'rack' + G + + # Create an empty file to trigger a partial download + versions = File.join(Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions") + FileUtils.mkdir_p(File.dirname(versions)) + FileUtils.touch(versions) + + bundle! :install, :artifice => "compact_index_concurrent_download" + + expect(File.read(versions)).to start_with("created_at") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "fails gracefully when the source URI has an invalid scheme" do + install_gemfile <<-G + source "htps://rubygems.org" + gem "rack" + G + expect(exitstatus).to eq(15) if exitstatus + expect(out).to end_with(<<-E.strip) + The request uri `htps://index.rubygems.org/versions` has an invalid scheme (`htps`). Did you mean `http` or `https`? + E + end + + describe "checksum validation", :rubygems => ">= 2.3.0" do + it "raises when the checksum does not match" do + install_gemfile <<-G, :artifice => "compact_index_wrong_gem_checksum" + source "#{source_uri}" + gem "rack" + G + + expect(exitstatus).to eq(19) if exitstatus + expect(out). + to include("Bundler cannot continue installing rack (1.0.0)."). + and include("The checksum for the downloaded `rack-1.0.0.gem` does not match the checksum given by the server."). + and include("This means the contents of the downloaded gem is different from what was uploaded to the server, and could be a potential security issue."). + and include("To resolve this issue:"). + and include("1. delete the downloaded gem located at: `#{system_gem_path}/gems/rack-1.0.0/rack-1.0.0.gem`"). + and include("2. run `bundle install`"). + and include("If you wish to continue installing the downloaded gem, and are certain it does not pose a security issue despite the mismatching checksum, do the following:"). + and include("1. run `bundle config disable_checksum_validation true` to turn off checksum verification"). + and include("2. run `bundle install`"). + and match(/\(More info: The expected SHA256 checksum was "#{"ab" * 22}", but the checksum for the downloaded gem was ".+?"\.\)/) + end + + it "raises when the checksum is the wrong length" do + install_gemfile <<-G, :artifice => "compact_index_wrong_gem_checksum", :env => { "BUNDLER_SPEC_RACK_CHECKSUM" => "checksum!" } + source "#{source_uri}" + gem "rack" + G + expect(exitstatus).to eq(5) if exitstatus + expect(out).to include("The given checksum for rack-1.0.0 (\"checksum!\") is not a valid SHA256 hexdigest nor base64digest") + end + + it "does not raise when disable_checksum_validation is set" do + bundle! "config disable_checksum_validation true" + install_gemfile! <<-G, :artifice => "compact_index_wrong_gem_checksum" + source "#{source_uri}" + gem "rack" + G + end + end + + it "works when cache dir is world-writable" do + install_gemfile! <<-G, :artifice => "compact_index" + File.umask(0000) + source "#{source_uri}" + gem "rack" + G + end + + it "doesn't explode when the API dependencies are wrong" do + install_gemfile <<-G, :artifice => "compact_index_wrong_dependencies", :env => { "DEBUG" => "true" } + source "#{source_uri}" + gem "rails" + G + deps = [Gem::Dependency.new("rake", "= 10.0.2"), + Gem::Dependency.new("actionpack", "= 2.3.2"), + Gem::Dependency.new("activerecord", "= 2.3.2"), + Gem::Dependency.new("actionmailer", "= 2.3.2"), + Gem::Dependency.new("activeresource", "= 2.3.2")] + expect(out).to include(<<-E.strip).and include("rails-2.3.2 from rubygems remote at #{source_uri}/ has either corrupted API or lockfile dependencies") +Bundler::APIResponseMismatchError: Downloading rails-2.3.2 revealed dependencies not in the API or the lockfile (#{deps.map(&:to_s).join(", ")}). +Either installing with `--full-index` or running `bundle update rails` should fix the problem. + E + end + + it "does not duplicate specs in the lockfile when updating and a dependency is not installed" do + install_gemfile! <<-G, :artifice => "compact_index" + source "#{source_uri}" do + gem "rails" + gem "activemerchant" + end + G + gem_command! :uninstall, "activemerchant" + bundle! "update rails", :artifice => "compact_index" + expect(lockfile.scan(/activemerchant \(/).size).to eq(1) + end +end diff --git a/spec/bundler/install/gems/dependency_api_spec.rb b/spec/bundler/install/gems/dependency_api_spec.rb new file mode 100644 index 0000000000..d495490745 --- /dev/null +++ b/spec/bundler/install/gems/dependency_api_spec.rb @@ -0,0 +1,671 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "gemcutter's dependency API" do + let(:source_hostname) { "localgemserver.test" } + let(:source_uri) { "http://#{source_hostname}" } + + it "should use the API" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endpoint" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "should URI encode gem names" do + gemfile <<-G + source "#{source_uri}" + gem " sinatra" + G + + bundle :install, :artifice => "endpoint" + expect(out).to include("' sinatra' is not a valid gem name because it contains whitespace.") + end + + it "should handle nested dependencies" do + gemfile <<-G + source "#{source_uri}" + gem "rails" + G + + bundle :install, :artifice => "endpoint" + expect(out).to include("Fetching gem metadata from #{source_uri}/...") + expect(the_bundle).to include_gems( + "rails 2.3.2", + "actionpack 2.3.2", + "activerecord 2.3.2", + "actionmailer 2.3.2", + "activeresource 2.3.2", + "activesupport 2.3.2" + ) + end + + it "should handle multiple gem dependencies on the same gem" do + gemfile <<-G + source "#{source_uri}" + gem "net-sftp" + G + + bundle :install, :artifice => "endpoint" + expect(the_bundle).to include_gems "net-sftp 1.1.1" + end + + it "should use the endpoint when using --deployment" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + bundle :install, :artifice => "endpoint" + + bundle "install --deployment", :artifice => "endpoint" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "handles git dependencies that are in rubygems" do + build_git "foo" do |s| + s.executables = "foobar" + s.add_dependency "rails", "2.3.2" + end + + gemfile <<-G + source "#{source_uri}" + git "file:///#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + + bundle :install, :artifice => "endpoint" + + expect(the_bundle).to include_gems("rails 2.3.2") + end + + it "handles git dependencies that are in rubygems using --deployment" do + build_git "foo" do |s| + s.executables = "foobar" + s.add_dependency "rails", "2.3.2" + end + + gemfile <<-G + source "#{source_uri}" + gem 'foo', :git => "file:///#{lib_path("foo-1.0")}" + G + + bundle :install, :artifice => "endpoint" + + bundle "install --deployment", :artifice => "endpoint" + + expect(the_bundle).to include_gems("rails 2.3.2") + end + + it "doesn't fail if you only have a git gem with no deps when using --deployment" do + build_git "foo" + gemfile <<-G + source "#{source_uri}" + gem 'foo', :git => "file:///#{lib_path("foo-1.0")}" + G + + bundle "install", :artifice => "endpoint" + bundle "install --deployment", :artifice => "endpoint" + + expect(exitstatus).to eq(0) if exitstatus + expect(the_bundle).to include_gems("foo 1.0") + end + + it "falls back when the API errors out" do + simulate_platform mswin + + gemfile <<-G + source "#{source_uri}" + gem "rcov" + G + + bundle :install, :artifice => "windows" + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "rcov 1.0.0" + end + + it "falls back when hitting the Gemcutter Dependency Limit" do + gemfile <<-G + source "#{source_uri}" + gem "activesupport" + gem "actionpack" + gem "actionmailer" + gem "activeresource" + gem "thin" + gem "rack" + gem "rails" + G + bundle :install, :artifice => "endpoint_fallback" + expect(out).to include("Fetching source index from #{source_uri}") + + expect(the_bundle).to include_gems( + "activesupport 2.3.2", + "actionpack 2.3.2", + "actionmailer 2.3.2", + "activeresource 2.3.2", + "activesupport 2.3.2", + "thin 1.0.0", + "rack 1.0.0", + "rails 2.3.2" + ) + end + + it "falls back when Gemcutter API doesn't return proper Marshal format" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle :install, :verbose => true, :artifice => "endpoint_marshal_fail" + expect(out).to include("could not fetch from the dependency API, trying the full index") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "falls back when the API URL returns 403 Forbidden" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle :install, :verbose => true, :artifice => "endpoint_api_forbidden" + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "handles host redirects" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endpoint_host_redirect" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "handles host redirects without Net::HTTP::Persistent" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + FileUtils.mkdir_p lib_path + File.open(lib_path("disable_net_http_persistent.rb"), "w") do |h| + h.write <<-H + module Kernel + alias require_without_disabled_net_http require + def require(*args) + raise LoadError, 'simulated' if args.first == 'openssl' && !caller.grep(/vendored_persistent/).empty? + require_without_disabled_net_http(*args) + end + end + H + end + + bundle :install, :artifice => "endpoint_host_redirect", :requires => [lib_path("disable_net_http_persistent.rb")] + expect(out).to_not match(/Too many redirects/) + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "timeouts when Bundler::Fetcher redirects too much" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endpoint_redirect" + expect(out).to match(/Too many redirects/) + end + + context "when --full-index is specified" do + it "should use the modern index for install" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "install --full-index", :artifice => "endpoint" + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "should use the modern index for update" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "update --full-index", :artifice => "endpoint" + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + it "fetches again when more dependencies are found in subsequent sources" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" + gem "back_deps" + G + + bundle :install, :artifice => "endpoint_extra" + expect(the_bundle).to include_gems "back_deps 1.0" + end + + it "fetches gem versions even when those gems are already installed" do + gemfile <<-G + source "#{source_uri}" + gem "rack", "1.0.0" + G + bundle :install, :artifice => "endpoint_extra_api" + + build_repo4 do + build_gem "rack", "1.2" do |s| + s.executables = "rackup" + end + end + + gemfile <<-G + source "#{source_uri}" do; end + source "#{source_uri}/extra" + gem "rack", "1.2" + G + bundle :install, :artifice => "endpoint_extra_api" + expect(the_bundle).to include_gems "rack 1.2" + end + + it "considers all possible versions of dependencies from all api gem sources" do + # In this scenario, the gem "somegem" only exists in repo4. It depends on specific version of activesupport that + # exists only in repo1. There happens also be a version of activesupport in repo4, but not the one that version 1.0.0 + # of somegem wants. This test makes sure that bundler actually finds version 1.2.3 of active support in the other + # repo and installs it. + build_repo4 do + build_gem "activesupport", "1.2.0" + build_gem "somegem", "1.0.0" do |s| + s.add_dependency "activesupport", "1.2.3" # This version exists only in repo1 + end + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" + gem 'somegem', '1.0.0' + G + + bundle :install, :artifice => "endpoint_extra_api" + + expect(the_bundle).to include_gems "somegem 1.0.0" + expect(the_bundle).to include_gems "activesupport 1.2.3" + end + + it "prints API output properly with back deps" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" + gem "back_deps" + G + + bundle :install, :artifice => "endpoint_extra" + + expect(out).to include("Fetching gem metadata from http://localgemserver.test/..") + expect(out).to include("Fetching source index from http://localgemserver.test/extra") + end + + it "does not fetch every spec if the index of gems is large when doing back deps" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + build_gem "missing" + # need to hit the limit + 1.upto(Bundler::Source::Rubygems::API_REQUEST_LIMIT) do |i| + build_gem "gem#{i}" + end + + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" + gem "back_deps" + G + + bundle :install, :artifice => "endpoint_extra_missing" + expect(the_bundle).to include_gems "back_deps 1.0" + end + + it "uses the endpoint if all sources support it" do + gemfile <<-G + source "#{source_uri}" + + gem 'foo' + G + + bundle :install, :artifice => "endpoint_api_missing" + expect(the_bundle).to include_gems "foo 1.0" + end + + it "fetches again when more dependencies are found in subsequent sources using --deployment" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" + gem "back_deps" + G + + bundle :install, :artifice => "endpoint_extra" + + bundle "install --deployment", :artifice => "endpoint_extra" + expect(the_bundle).to include_gems "back_deps 1.0" + end + + it "does not refetch if the only unmet dependency is bundler" do + gemfile <<-G + source "#{source_uri}" + + gem "bundler_dep" + G + + bundle :install, :artifice => "endpoint" + expect(out).to include("Fetching gem metadata from #{source_uri}") + end + + it "should install when EndpointSpecification has a bin dir owned by root", :sudo => true do + sudo "mkdir -p #{system_gem_path("bin")}" + sudo "chown -R root #{system_gem_path("bin")}" + + gemfile <<-G + source "#{source_uri}" + gem "rails" + G + bundle :install, :artifice => "endpoint" + expect(the_bundle).to include_gems "rails 2.3.2" + end + + it "installs the binstubs" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "install --binstubs", :artifice => "endpoint" + + gembin "rackup" + expect(out).to eq("1.0.0") + end + + it "installs the bins when using --path and uses autoclean" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "install --path vendor/bundle", :artifice => "endpoint" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "installs the bins when using --path and uses bundle clean" do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + + bundle "install --path vendor/bundle --no-clean", :artifice => "endpoint" + + expect(vendored_gems("bin/rackup")).to exist + end + + it "prints post_install_messages" do + gemfile <<-G + source "#{source_uri}" + gem 'rack-obama' + G + + bundle :install, :artifice => "endpoint" + expect(out).to include("Post-install message from rack:") + end + + it "should display the post install message for a dependency" do + gemfile <<-G + source "#{source_uri}" + gem 'rack_middleware' + G + + bundle :install, :artifice => "endpoint" + expect(out).to include("Post-install message from rack:") + expect(out).to include("Rack's post install message") + end + + context "when using basic authentication" do + let(:user) { "user" } + let(:password) { "pass" } + let(:basic_auth_source_uri) do + uri = URI.parse(source_uri) + uri.user = user + uri.password = password + + uri + end + + it "passes basic authentication details and strips out creds" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endpoint_basic_authentication" + expect(out).not_to include("#{user}:#{password}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "strips http basic authentication creds for modern index" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endopint_marshal_fail_basic_authentication" + expect(out).not_to include("#{user}:#{password}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "strips http basic auth creds when it can't reach the server" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endpoint_500" + expect(out).not_to include("#{user}:#{password}") + end + + it "strips http basic auth creds when warning about ambiguous sources" do + gemfile <<-G + source "#{basic_auth_source_uri}" + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install, :artifice => "endpoint_basic_authentication" + expect(out).to include("Warning: the gem 'rack' was found in multiple sources.") + expect(out).not_to include("#{user}:#{password}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "does not pass the user / password to different hosts on redirect" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endpoint_creds_diff_host" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + describe "with authentication details in bundle config" do + before do + gemfile <<-G + source "#{source_uri}" + gem "rack" + G + end + + it "reads authentication details by host name from bundle config" do + bundle "config #{source_hostname} #{user}:#{password}" + + bundle :install, :artifice => "endpoint_strict_basic_authentication" + + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "reads authentication details by full url from bundle config" do + # The trailing slash is necessary here; Fetcher canonicalizes the URI. + bundle "config #{source_uri}/ #{user}:#{password}" + + bundle :install, :artifice => "endpoint_strict_basic_authentication" + + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "should use the API" do + bundle "config #{source_hostname} #{user}:#{password}" + bundle :install, :artifice => "endpoint_strict_basic_authentication" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "prefers auth supplied in the source uri" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle "config #{source_hostname} otheruser:wrong" + + bundle :install, :artifice => "endpoint_strict_basic_authentication" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "shows instructions if auth is not provided for the source" do + bundle :install, :artifice => "endpoint_strict_basic_authentication" + expect(out).to include("bundle config #{source_hostname} username:password") + end + + it "fails if authentication has already been provided, but failed" do + bundle "config #{source_hostname} #{user}:wrong" + + bundle :install, :artifice => "endpoint_strict_basic_authentication" + expect(out).to include("Bad username or password") + end + end + + describe "with no password" do + let(:password) { nil } + + it "passes basic authentication details" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "rack" + G + + bundle :install, :artifice => "endpoint_basic_authentication" + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + end + + context "when ruby is compiled without openssl", :ruby_repo do + before do + # Install a monkeypatch that reproduces the effects of openssl being + # missing when the fetcher runs, as happens in real life. The reason + # we can't just overwrite openssl.rb is that Artifice uses it. + bundled_app("broken_ssl").mkpath + bundled_app("broken_ssl/openssl.rb").open("w") do |f| + f.write <<-RUBY + raise LoadError, "cannot load such file -- openssl" + RUBY + end + end + + it "explains what to do to get it" do + gemfile <<-G + source "#{source_uri.gsub(/http/, "https")}" + gem "rack" + G + + bundle :install, :env => { "RUBYOPT" => "-I#{bundled_app("broken_ssl")}" } + expect(out).to include("OpenSSL") + end + end + + context "when SSL certificate verification fails" do + it "explains what happened" do + # Install a monkeypatch that reproduces the effects of openssl raising + # a certificate validation error when Rubygems tries to connect. + gemfile <<-G + class Net::HTTP + def start + raise OpenSSL::SSL::SSLError, "certificate verify failed" + end + end + + source "#{source_uri.gsub(/http/, "https")}" + gem "rack" + G + + bundle :install + expect(out).to match(/could not verify the SSL certificate/i) + end + end + + context ".gemrc with sources is present" do + before do + File.open(home(".gemrc"), "w") do |file| + file.puts({ :sources => ["https://rubygems.org"] }.to_yaml) + end + end + + after do + home(".gemrc").rmtree + end + + it "uses other sources declared in the Gemfile" do + gemfile <<-G + source "#{source_uri}" + gem 'rack' + G + + bundle "install", :artifice => "endpoint_marshal_fail" + + expect(exitstatus).to eq(0) if exitstatus + end + end +end diff --git a/spec/bundler/install/gems/env_spec.rb b/spec/bundler/install/gems/env_spec.rb new file mode 100644 index 0000000000..9b1d8e5424 --- /dev/null +++ b/spec/bundler/install/gems/env_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install with ENV conditionals" do + describe "when just setting an ENV key as a string" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + + env "BUNDLER_TEST" do + gem "rack" + end + G + end + + it "excludes the gems when the ENV variable is not set" do + bundle :install + expect(the_bundle).not_to include_gems "rack" + end + + it "includes the gems when the ENV variable is set" do + ENV["BUNDLER_TEST"] = "1" + bundle :install + expect(the_bundle).to include_gems "rack 1.0" + end + end + + describe "when just setting an ENV key as a symbol" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + + env :BUNDLER_TEST do + gem "rack" + end + G + end + + it "excludes the gems when the ENV variable is not set" do + bundle :install + expect(the_bundle).not_to include_gems "rack" + end + + it "includes the gems when the ENV variable is set" do + ENV["BUNDLER_TEST"] = "1" + bundle :install + expect(the_bundle).to include_gems "rack 1.0" + end + end + + describe "when setting a string to match the env" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + + env "BUNDLER_TEST" => "foo" do + gem "rack" + end + G + end + + it "excludes the gems when the ENV variable is not set" do + bundle :install + expect(the_bundle).not_to include_gems "rack" + end + + it "excludes the gems when the ENV variable is set but does not match the condition" do + ENV["BUNDLER_TEST"] = "1" + bundle :install + expect(the_bundle).not_to include_gems "rack" + end + + it "includes the gems when the ENV variable is set and matches the condition" do + ENV["BUNDLER_TEST"] = "foo" + bundle :install + expect(the_bundle).to include_gems "rack 1.0" + end + end + + describe "when setting a regex to match the env" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + + env "BUNDLER_TEST" => /foo/ do + gem "rack" + end + G + end + + it "excludes the gems when the ENV variable is not set" do + bundle :install + expect(the_bundle).not_to include_gems "rack" + end + + it "excludes the gems when the ENV variable is set but does not match the condition" do + ENV["BUNDLER_TEST"] = "fo" + bundle :install + expect(the_bundle).not_to include_gems "rack" + end + + it "includes the gems when the ENV variable is set and matches the condition" do + ENV["BUNDLER_TEST"] = "foobar" + bundle :install + expect(the_bundle).to include_gems "rack 1.0" + end + end +end diff --git a/spec/bundler/install/gems/flex_spec.rb b/spec/bundler/install/gems/flex_spec.rb new file mode 100644 index 0000000000..2c2d3c16a1 --- /dev/null +++ b/spec/bundler/install/gems/flex_spec.rb @@ -0,0 +1,319 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle flex_install" do + it "installs the gems as expected" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to be_locked + end + + it "installs even when the lockfile is invalid" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack' + G + + expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to be_locked + + gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack', '1.0' + G + + bundle :install + expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to be_locked + end + + it "keeps child dependencies at the same version" do + build_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack-obama" + G + + expect(the_bundle).to include_gems "rack 1.0.0", "rack-obama 1.0.0" + + update_repo2 + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack-obama", "1.0" + G + + expect(the_bundle).to include_gems "rack 1.0.0", "rack-obama 1.0.0" + end + + describe "adding new gems" do + it "installs added gems without updating previously installed gems" do + build_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'rack' + G + + update_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'rack' + gem 'activesupport', '2.3.5' + G + + expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.5" + end + + it "keeps child dependencies pinned" do + build_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack-obama" + G + + update_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack-obama" + gem "thin" + G + + expect(the_bundle).to include_gems "rack 1.0.0", "rack-obama 1.0", "thin 1.0" + end + end + + describe "removing gems" do + it "removes gems without changing the versions of remaining gems" do + build_repo2 + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'rack' + gem 'activesupport', '2.3.5' + G + + update_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'rack' + G + + expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'rack' + gem 'activesupport', '2.3.2' + G + + expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.2" + end + + it "removes top level dependencies when removed from the Gemfile while leaving other dependencies intact" do + build_repo2 + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'rack' + gem 'activesupport', '2.3.5' + G + + update_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'rack' + G + + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + end + + it "removes child dependencies" do + build_repo2 + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'rack-obama' + gem 'activesupport' + G + + expect(the_bundle).to include_gems "rack 1.0.0", "rack-obama 1.0.0", "activesupport 2.3.5" + + update_repo2 + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'activesupport' + G + + expect(the_bundle).to include_gems "activesupport 2.3.5" + expect(the_bundle).not_to include_gems "rack-obama", "rack" + end + end + + describe "when Gemfile conflicts with lockfile" do + before(:each) do + build_repo2 + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack_middleware" + G + + expect(the_bundle).to include_gems "rack_middleware 1.0", "rack 0.9.1" + + build_repo2 + update_repo2 do + build_gem "rack-obama", "2.0" do |s| + s.add_dependency "rack", "=1.2" + end + build_gem "rack_middleware", "2.0" do |s| + s.add_dependency "rack", ">=1.0" + end + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "rack-obama", "2.0" + gem "rack_middleware" + G + end + + it "does not install gems whose dependencies are not met" do + bundle :install + ruby <<-RUBY + require 'bundler/setup' + RUBY + expect(err).to match(/could not find gem 'rack-obama/i) + end + + it "suggests bundle update when the Gemfile requires different versions than the lock" do + nice_error = <<-E.strip.gsub(/^ {8}/, "") + Fetching source index from file:#{gem_repo2}/ + Resolving dependencies... + Bundler could not find compatible versions for gem "rack": + In snapshot (Gemfile.lock): + rack (= 0.9.1) + + In Gemfile: + rack-obama (= 2.0) was resolved to 2.0, which depends on + rack (= 1.2) + + rack_middleware was resolved to 1.0, which depends on + rack (= 0.9.1) + + Running `bundle update` will rebuild your snapshot from scratch, using only + the gems in your Gemfile, which may resolve the conflict. + E + + bundle :install, :retry => 0 + expect(out).to eq(nice_error) + end + end + + describe "subtler cases" do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack-obama" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "0.9.1" + gem "rack-obama" + G + end + + it "does something" do + expect do + bundle "install" + end.not_to change { File.read(bundled_app("Gemfile.lock")) } + + expect(out).to include("rack = 0.9.1") + expect(out).to include("locked at 1.0.0") + expect(out).to include("bundle update rack") + end + + it "should work when you update" do + bundle "update rack" + end + end + + describe "when adding a new source" do + it "updates the lockfile" do + build_repo2 + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + install_gemfile <<-G + source "file://#{gem_repo1}" + source "file://#{gem_repo2}" + gem "rack" + G + + lockfile_should_be <<-L + GEM + remote: file:#{gem_repo1}/ + remote: file:#{gem_repo2}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + # This was written to test github issue #636 + describe "when a locked child dependency conflicts" do + before(:each) do + build_repo2 do + build_gem "capybara", "0.3.9" do |s| + s.add_dependency "rack", ">= 1.0.0" + end + + build_gem "rack", "1.1.0" + build_gem "rails", "3.0.0.rc4" do |s| + s.add_dependency "rack", "~> 1.1.0" + end + + build_gem "rack", "1.2.1" + build_gem "rails", "3.0.0" do |s| + s.add_dependency "rack", "~> 1.2.1" + end + end + end + + it "prints the correct error message" do + # install Rails 3.0.0.rc + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rails", "3.0.0.rc4" + gem "capybara", "0.3.9" + G + + # upgrade Rails to 3.0.0 and then install again + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rails", "3.0.0" + gem "capybara", "0.3.9" + G + + expect(out).to include("Gemfile.lock") + end + end +end diff --git a/spec/bundler/install/gems/mirror_spec.rb b/spec/bundler/install/gems/mirror_spec.rb new file mode 100644 index 0000000000..798156fb12 --- /dev/null +++ b/spec/bundler/install/gems/mirror_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install with a mirror configured" do + describe "when the mirror does not match the gem source" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + bundle "config --local mirror.http://gems.example.org http://gem-mirror.example.org" + end + + it "installs from the normal location" do + bundle :install + expect(out).to include("Fetching source index from file:#{gem_repo1}") + expect(the_bundle).to include_gems "rack 1.0" + end + end + + describe "when the gem source matches a configured mirror" do + before :each do + gemfile <<-G + # This source is bogus and doesn't have the gem we're looking for + source "file://#{gem_repo2}" + + gem "rack" + G + bundle "config --local mirror.file://#{gem_repo2} file://#{gem_repo1}" + end + + it "installs the gem from the mirror" do + bundle :install + expect(out).to include("Fetching source index from file:#{gem_repo1}") + expect(out).not_to include("Fetching source index from file:#{gem_repo2}") + expect(the_bundle).to include_gems "rack 1.0" + end + end +end diff --git a/spec/bundler/install/gems/native_extensions_spec.rb b/spec/bundler/install/gems/native_extensions_spec.rb new file mode 100644 index 0000000000..dcf67e976e --- /dev/null +++ b/spec/bundler/install/gems/native_extensions_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "installing a gem with native extensions", :ruby_repo do + it "installs" do + build_repo2 do + build_gem "c_extension" do |s| + s.extensions = ["ext/extconf.rb"] + s.write "ext/extconf.rb", <<-E + require "mkmf" + name = "c_extension_bundle" + dir_config(name) + raise "OMG" unless with_config("c_extension") == "hello" + create_makefile(name) + E + + s.write "ext/c_extension.c", <<-C + #include "ruby.h" + + VALUE c_extension_true(VALUE self) { + return Qtrue; + } + + void Init_c_extension_bundle() { + VALUE c_Extension = rb_define_class("CExtension", rb_cObject); + rb_define_method(c_Extension, "its_true", c_extension_true, 0); + } + C + + s.write "lib/c_extension.rb", <<-C + require "c_extension_bundle" + C + end + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "c_extension" + G + + bundle "config build.c_extension --with-c_extension=hello" + bundle "install" + + expect(out).not_to include("extconf.rb failed") + expect(out).to include("Installing c_extension 1.0 with native extensions") + + run "Bundler.require; puts CExtension.new.its_true" + expect(out).to eq("true") + end + + it "installs from git" do + build_git "c_extension" do |s| + s.extensions = ["ext/extconf.rb"] + s.write "ext/extconf.rb", <<-E + require "mkmf" + name = "c_extension_bundle" + dir_config(name) + raise "OMG" unless with_config("c_extension") == "hello" + create_makefile(name) + E + + s.write "ext/c_extension.c", <<-C + #include "ruby.h" + + VALUE c_extension_true(VALUE self) { + return Qtrue; + } + + void Init_c_extension_bundle() { + VALUE c_Extension = rb_define_class("CExtension", rb_cObject); + rb_define_method(c_Extension, "its_true", c_extension_true, 0); + } + C + + s.write "lib/c_extension.rb", <<-C + require "c_extension_bundle" + C + end + + bundle! "config build.c_extension --with-c_extension=hello" + + install_gemfile! <<-G + gem "c_extension", :git => #{lib_path("c_extension-1.0").to_s.dump} + G + + expect(out).not_to include("extconf.rb failed") + expect(out).to include("Using c_extension 1.0") + + run! "Bundler.require; puts CExtension.new.its_true" + expect(out).to eq("true") + end +end diff --git a/spec/bundler/install/gems/post_install_spec.rb b/spec/bundler/install/gems/post_install_spec.rb new file mode 100644 index 0000000000..c3ea3e7c51 --- /dev/null +++ b/spec/bundler/install/gems/post_install_spec.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install" do + context "with gem sources" do + context "when gems include post install messages" do + it "should display the post-install messages after installing" do + gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack' + gem 'thin' + gem 'rack-obama' + G + + bundle :install + expect(out).to include("Post-install message from rack:") + expect(out).to include("Rack's post install message") + expect(out).to include("Post-install message from thin:") + expect(out).to include("Thin's post install message") + expect(out).to include("Post-install message from rack-obama:") + expect(out).to include("Rack-obama's post install message") + end + end + + context "when gems do not include post install messages" do + it "should not display any post-install messages" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + G + + bundle :install + expect(out).not_to include("Post-install message") + end + end + + context "when a dependecy includes a post install message" do + it "should display the post install message" do + gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack_middleware' + G + + bundle :install + expect(out).to include("Post-install message from rack:") + expect(out).to include("Rack's post install message") + end + end + end + + context "with git sources" do + context "when gems include post install messages" do + it "should display the post-install messages after installing" do + build_git "foo" do |s| + s.post_install_message = "Foo's post install message" + end + gemfile <<-G + source "file://#{gem_repo1}" + gem 'foo', :git => '#{lib_path("foo-1.0")}' + G + + bundle :install + expect(out).to include("Post-install message from foo:") + expect(out).to include("Foo's post install message") + end + + it "should display the post-install messages if repo is updated" do + build_git "foo" do |s| + s.post_install_message = "Foo's post install message" + end + gemfile <<-G + source "file://#{gem_repo1}" + gem 'foo', :git => '#{lib_path("foo-1.0")}' + G + bundle :install + + build_git "foo", "1.1" do |s| + s.post_install_message = "Foo's 1.1 post install message" + end + gemfile <<-G + source "file://#{gem_repo1}" + gem 'foo', :git => '#{lib_path("foo-1.1")}' + G + bundle :install + + expect(out).to include("Post-install message from foo:") + expect(out).to include("Foo's 1.1 post install message") + end + + it "should not display the post-install messages if repo is not updated" do + build_git "foo" do |s| + s.post_install_message = "Foo's post install message" + end + gemfile <<-G + source "file://#{gem_repo1}" + gem 'foo', :git => '#{lib_path("foo-1.0")}' + G + + bundle :install + expect(out).to include("Post-install message from foo:") + expect(out).to include("Foo's post install message") + + bundle :install + expect(out).not_to include("Post-install message") + end + end + + context "when gems do not include post install messages" do + it "should not display any post-install messages" do + build_git "foo" do |s| + s.post_install_message = nil + end + gemfile <<-G + source "file://#{gem_repo1}" + gem 'foo', :git => '#{lib_path("foo-1.0")}' + G + + bundle :install + expect(out).not_to include("Post-install message") + end + end + end + + context "when ignore post-install messages for gem is set" do + it "doesn't display any post-install messages" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "config ignore_messages.rack true" + + bundle :install + expect(out).not_to include("Post-install message") + end + end + + context "when ignore post-install messages for all gems" do + it "doesn't display any post-install messages" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "config ignore_messages true" + + bundle :install + expect(out).not_to include("Post-install message") + end + end +end diff --git a/spec/bundler/install/gems/resolving_spec.rb b/spec/bundler/install/gems/resolving_spec.rb new file mode 100644 index 0000000000..7a341fd14f --- /dev/null +++ b/spec/bundler/install/gems/resolving_spec.rb @@ -0,0 +1,195 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install with install-time dependencies" do + it "installs gems with implicit rake dependencies", :ruby_repo do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "with_implicit_rake_dep" + gem "another_implicit_rake_dep" + gem "rake" + G + + run <<-R + require 'implicit_rake_dep' + require 'another_implicit_rake_dep' + puts IMPLICIT_RAKE_DEP + puts ANOTHER_IMPLICIT_RAKE_DEP + R + expect(out).to eq("YES\nYES") + end + + it "installs gems with a dependency with no type" do + build_repo2 + + path = "#{gem_repo2}/#{Gem::MARSHAL_SPEC_DIR}/actionpack-2.3.2.gemspec.rz" + spec = Marshal.load(Gem.inflate(File.read(path))) + spec.dependencies.each do |d| + d.instance_variable_set(:@type, :fail) + end + File.open(path, "w") do |f| + f.write Gem.deflate(Marshal.dump(spec)) + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "actionpack", "2.3.2" + G + + expect(the_bundle).to include_gems "actionpack 2.3.2", "activesupport 2.3.2" + end + + describe "with crazy rubygem plugin stuff" do + it "installs plugins" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "net_b" + G + + expect(the_bundle).to include_gems "net_b 1.0" + end + + it "installs plugins depended on by other plugins", :ruby_repo do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "net_a" + G + + expect(the_bundle).to include_gems "net_a 1.0", "net_b 1.0" + end + + it "installs multiple levels of dependencies", :ruby_repo do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "net_c" + gem "net_e" + G + + expect(the_bundle).to include_gems "net_a 1.0", "net_b 1.0", "net_c 1.0", "net_d 1.0", "net_e 1.0" + end + + context "with ENV['DEBUG_RESOLVER'] set" do + it "produces debug output" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "net_c" + gem "net_e" + G + + bundle :install, :env => { "DEBUG_RESOLVER" => "1" } + + expect(err).to include("Creating possibility state for net_c") + end + end + + context "with ENV['DEBUG_RESOLVER_TREE'] set" do + it "produces debug output" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "net_c" + gem "net_e" + G + + bundle :install, :env => { "DEBUG_RESOLVER_TREE" => "1" } + + expect(err).to include(" net_b") + expect(err).to include(" net_build_extensions (1.0)") + end + end + end + + describe "when a required ruby version" do + context "allows only an older version" do + it "installs the older version" do + build_repo2 do + build_gem "rack", "9001.0.0" do |s| + s.required_ruby_version = "> 9000" + end + end + + install_gemfile <<-G, :artifice => "compact_index", :env => { "BUNDLER_SPEC_GEM_REPO" => gem_repo2 } + ruby "#{RUBY_VERSION}" + source "http://localgemserver.test/" + gem 'rack' + G + + expect(out).to_not include("rack-9001.0.0 requires ruby version > 9000") + expect(the_bundle).to include_gems("rack 1.2") + end + end + + context "allows no gems" do + before do + build_repo2 do + build_gem "require_ruby" do |s| + s.required_ruby_version = "> 9000" + end + end + end + + let(:ruby_requirement) { %("#{RUBY_VERSION}") } + let(:error_message_requirement) { "~> #{RUBY_VERSION}.0" } + + shared_examples_for "ruby version conflicts" do + it "raises an error during resolution" do + install_gemfile <<-G, :artifice => "compact_index", :env => { "BUNDLER_SPEC_GEM_REPO" => gem_repo2 } + source "http://localgemserver.test/" + ruby #{ruby_requirement} + gem 'require_ruby' + G + + expect(out).to_not include("Gem::InstallError: require_ruby requires Ruby version > 9000") + + nice_error = strip_whitespace(<<-E).strip + Fetching gem metadata from http://localgemserver.test/. + Fetching version metadata from http://localgemserver.test/ + Resolving dependencies... + Bundler could not find compatible versions for gem "ruby\0": + In Gemfile: + ruby\0 (#{error_message_requirement}) + + require_ruby was resolved to 1.0, which depends on + ruby\0 (> 9000) + + Could not find gem 'ruby\0 (> 9000)', which is required by gem 'require_ruby', in any of the sources. + E + expect(out).to eq(nice_error) + end + end + + it_behaves_like "ruby version conflicts" + + describe "with a < requirement" do + let(:ruby_requirement) { %("< 5000") } + let(:error_message_requirement) { "< 5000" } + + it_behaves_like "ruby version conflicts" + end + + describe "with a compound requirement" do + let(:ruby_requirement) { %("< 5000", "> 0.1") } + let(:error_message_requirement) { "< 5000, > 0.1" } + + it_behaves_like "ruby version conflicts" + end + end + end + + describe "when a required rubygems version disallows a gem" do + it "does not try to install those gems" do + build_repo2 do + build_gem "require_rubygems" do |s| + s.required_rubygems_version = "> 9000" + end + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem 'require_rubygems' + G + + expect(out).to_not include("Gem::InstallError: require_rubygems requires RubyGems version > 9000") + expect(out).to include("require_rubygems-1.0 requires rubygems version > 9000, which is incompatible with the current version, #{Gem::VERSION}") + end + end +end diff --git a/spec/bundler/install/gems/standalone_spec.rb b/spec/bundler/install/gems/standalone_spec.rb new file mode 100644 index 0000000000..9a79a05b32 --- /dev/null +++ b/spec/bundler/install/gems/standalone_spec.rb @@ -0,0 +1,318 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.shared_examples "bundle install --standalone" do + shared_examples "common functionality" do + it "still makes the gems available to normal bundler" do + args = expected_gems.map {|k, v| "#{k} #{v}" } + expect(the_bundle).to include_gems(*args) + end + + it "generates a bundle/bundler/setup.rb" do + expect(bundled_app("bundle/bundler/setup.rb")).to exist + end + + it "makes the gems available without bundler" do + testrb = String.new <<-RUBY + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + RUBY + expected_gems.each do |k, _| + testrb << "\nrequire \"#{k}\"" + testrb << "\nputs #{k.upcase}" + end + Dir.chdir(bundled_app) do + ruby testrb, :no_lib => true + end + + expect(out).to eq(expected_gems.values.join("\n")) + end + + it "works on a different system" do + FileUtils.mv(bundled_app, "#{bundled_app}2") + + testrb = String.new <<-RUBY + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + RUBY + expected_gems.each do |k, _| + testrb << "\nrequire \"#{k}\"" + testrb << "\nputs #{k.upcase}" + end + Dir.chdir("#{bundled_app}2") do + ruby testrb, :no_lib => true + end + + expect(out).to eq(expected_gems.values.join("\n")) + end + end + + describe "with simple gems" do + before do + install_gemfile <<-G, :standalone => true + source "file://#{gem_repo1}" + gem "rails" + G + end + + let(:expected_gems) do + { + "actionpack" => "2.3.2", + "rails" => "2.3.2", + } + end + + include_examples "common functionality" + end + + describe "with gems with native extension", :ruby_repo do + before do + install_gemfile <<-G, :standalone => true + source "file://#{gem_repo1}" + gem "very_simple_binary" + G + end + + it "generates a bundle/bundler/setup.rb with the proper paths", :rubygems => "2.4" do + extension_line = File.read(bundled_app("bundle/bundler/setup.rb")).each_line.find {|line| line.include? "/extensions/" }.strip + expect(extension_line).to start_with '$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/extensions/' + expect(extension_line).to end_with '/very_simple_binary-1.0"' + end + end + + describe "with gem that has an invalid gemspec" do + before do + build_git "bar", :gemspec => false do |s| + s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0') + s.write "bar.gemspec", <<-G + lib = File.expand_path('../lib/', __FILE__) + $:.unshift lib unless $:.include?(lib) + require 'bar/version' + + Gem::Specification.new do |s| + s.name = 'bar' + s.version = BAR_VERSION + s.summary = 'Bar' + s.files = Dir["lib/**/*.rb"] + s.author = 'Anonymous' + s.require_path = [1,2] + end + G + end + install_gemfile <<-G, :standalone => true + gem "bar", :git => "#{lib_path("bar-1.0")}" + G + end + + it "outputs a helpful error message" do + expect(out).to include("You have one or more invalid gemspecs that need to be fixed.") + expect(out).to include("bar 1.0 has an invalid gemspec") + end + end + + describe "with a combination of gems and git repos" do + before do + build_git "devise", "1.0" + + install_gemfile <<-G, :standalone => true + source "file://#{gem_repo1}" + gem "rails" + gem "devise", :git => "#{lib_path("devise-1.0")}" + G + end + + let(:expected_gems) do + { + "actionpack" => "2.3.2", + "devise" => "1.0", + "rails" => "2.3.2", + } + end + + include_examples "common functionality" + end + + describe "with groups" do + before do + build_git "devise", "1.0" + + install_gemfile <<-G, :standalone => true + source "file://#{gem_repo1}" + gem "rails" + + group :test do + gem "rspec" + gem "rack-test" + end + G + end + + let(:expected_gems) do + { + "actionpack" => "2.3.2", + "rails" => "2.3.2", + } + end + + include_examples "common functionality" + + it "allows creating a standalone file with limited groups" do + bundle "install --standalone default" + + Dir.chdir(bundled_app) do + load_error_ruby <<-RUBY, "spec", :no_lib => true + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + require "actionpack" + puts ACTIONPACK + require "spec" + RUBY + end + + expect(out).to eq("2.3.2") + expect(err).to eq("ZOMG LOAD ERROR") + end + + it "allows --without to limit the groups used in a standalone" do + bundle "install --standalone --without test" + + Dir.chdir(bundled_app) do + load_error_ruby <<-RUBY, "spec", :no_lib => true + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + require "actionpack" + puts ACTIONPACK + require "spec" + RUBY + end + + expect(out).to eq("2.3.2") + expect(err).to eq("ZOMG LOAD ERROR") + end + + it "allows --path to change the location of the standalone bundle" do + bundle "install --standalone --path path/to/bundle" + + Dir.chdir(bundled_app) do + ruby <<-RUBY, :no_lib => true + $:.unshift File.expand_path("path/to/bundle") + require "bundler/setup" + + require "actionpack" + puts ACTIONPACK + RUBY + end + + expect(out).to eq("2.3.2") + end + + it "allows remembered --without to limit the groups used in a standalone" do + bundle "install --without test" + bundle "install --standalone" + + Dir.chdir(bundled_app) do + load_error_ruby <<-RUBY, "spec", :no_lib => true + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + require "actionpack" + puts ACTIONPACK + require "spec" + RUBY + end + + expect(out).to eq("2.3.2") + expect(err).to eq("ZOMG LOAD ERROR") + end + end + + describe "with gemcutter's dependency API" do + let(:source_uri) { "http://localgemserver.test" } + + describe "simple gems" do + before do + gemfile <<-G + source "#{source_uri}" + gem "rails" + G + bundle "install --standalone", :artifice => "endpoint" + end + + let(:expected_gems) do + { + "actionpack" => "2.3.2", + "rails" => "2.3.2", + } + end + + include_examples "common functionality" + end + end + + describe "with --binstubs" do + before do + install_gemfile <<-G, :standalone => true, :binstubs => true + source "file://#{gem_repo1}" + gem "rails" + G + end + + let(:expected_gems) do + { + "actionpack" => "2.3.2", + "rails" => "2.3.2", + } + end + + include_examples "common functionality" + + it "creates stubs that use the standalone load path" do + Dir.chdir(bundled_app) do + expect(`bin/rails -v`.chomp).to eql "2.3.2" + end + end + + it "creates stubs that can be executed from anywhere" do + require "tmpdir" + Dir.chdir(Dir.tmpdir) do + sys_exec!(%(#{bundled_app("bin/rails")} -v)) + expect(out).to eq("2.3.2") + end + end + + it "creates stubs that can be symlinked" do + pending "File.symlink is unsupported on Windows" if Bundler::WINDOWS + + symlink_dir = tmp("symlink") + FileUtils.mkdir_p(symlink_dir) + symlink = File.join(symlink_dir, "rails") + + File.symlink(bundled_app("bin/rails"), symlink) + sys_exec!("#{symlink} -v") + expect(out).to eq("2.3.2") + end + + it "creates stubs with the correct load path" do + extension_line = File.read(bundled_app("bin/rails")).each_line.find {|line| line.include? "$:.unshift" }.strip + expect(extension_line).to eq %($:.unshift File.expand_path "../../bundle", path.realpath) + end + end +end + +RSpec.describe "bundle install --standalone" do + include_examples("bundle install --standalone") +end + +RSpec.describe "bundle install --standalone run in a subdirectory" do + before do + subdir = bundled_app("bob") + FileUtils.mkdir_p(subdir) + Dir.chdir(subdir) + end + + include_examples("bundle install --standalone") +end diff --git a/spec/bundler/install/gems/sudo_spec.rb b/spec/bundler/install/gems/sudo_spec.rb new file mode 100644 index 0000000000..13abffc14e --- /dev/null +++ b/spec/bundler/install/gems/sudo_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "when using sudo", :sudo => true do + describe "and BUNDLE_PATH is writable" do + context "but BUNDLE_PATH/build_info is not writable" do + before do + subdir = system_gem_path("cache") + subdir.mkpath + sudo "chmod u-w #{subdir}" + end + + it "installs" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + expect(out).to_not match(/an error occurred/i) + expect(system_gem_path("cache/rack-1.0.0.gem")).to exist + expect(the_bundle).to include_gems "rack 1.0" + end + end + end + + describe "and GEM_HOME is owned by root" do + before :each do + chown_system_gems_to_root + end + + it "installs" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", '1.0' + gem "thin" + G + + expect(system_gem_path("gems/rack-1.0.0")).to exist + expect(system_gem_path("gems/rack-1.0.0").stat.uid).to eq(0) + expect(the_bundle).to include_gems "rack 1.0" + end + + it "installs rake and a gem dependent on rake in the same session" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rake" + gem "another_implicit_rake_dep" + G + bundle "install" + expect(system_gem_path("gems/another_implicit_rake_dep-1.0")).to exist + end + + it "installs when BUNDLE_PATH is owned by root" do + bundle_path = tmp("owned_by_root") + FileUtils.mkdir_p bundle_path + sudo "chown -R root #{bundle_path}" + + ENV["BUNDLE_PATH"] = bundle_path.to_s + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", '1.0' + G + + expect(bundle_path.join("gems/rack-1.0.0")).to exist + expect(bundle_path.join("gems/rack-1.0.0").stat.uid).to eq(0) + expect(the_bundle).to include_gems "rack 1.0" + end + + it "installs when BUNDLE_PATH does not exist" do + root_path = tmp("owned_by_root") + FileUtils.mkdir_p root_path + sudo "chown -R root #{root_path}" + bundle_path = root_path.join("does_not_exist") + + ENV["BUNDLE_PATH"] = bundle_path.to_s + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", '1.0' + G + + expect(bundle_path.join("gems/rack-1.0.0")).to exist + expect(bundle_path.join("gems/rack-1.0.0").stat.uid).to eq(0) + expect(the_bundle).to include_gems "rack 1.0" + end + + it "installs extensions/ compiled by Rubygems 2.2", :rubygems => "2.2" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "very_simple_binary" + G + + expect(system_gem_path("gems/very_simple_binary-1.0")).to exist + binary_glob = system_gem_path("extensions/*/*/very_simple_binary-1.0") + expect(Dir.glob(binary_glob).first).to be + end + end + + describe "and BUNDLE_PATH is not writable" do + before do + sudo "chmod ugo-w #{default_bundle_path}" + end + + it "installs" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", '1.0' + G + + expect(default_bundle_path("gems/rack-1.0.0")).to exist + expect(the_bundle).to include_gems "rack 1.0" + end + + it "cleans up the tmpdirs generated" do + require "tmpdir" + Dir.glob("#{Dir.tmpdir}/bundler*").each do |tmpdir| + FileUtils.remove_entry_secure(tmpdir) + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + tmpdirs = Dir.glob("#{Dir.tmpdir}/bundler*") + + expect(tmpdirs).to be_empty + end + end + + describe "and GEM_HOME is not writable" do + it "installs" do + gem_home = tmp("sudo_gem_home") + sudo "mkdir -p #{gem_home}" + sudo "chmod ugo-w #{gem_home}" + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", '1.0' + G + + bundle :install, :env => { "GEM_HOME" => gem_home.to_s, "GEM_PATH" => nil } + expect(gem_home.join("bin/rackup")).to exist + expect(the_bundle).to include_gems "rack 1.0", :env => { "GEM_HOME" => gem_home.to_s, "GEM_PATH" => nil } + end + end + + describe "and root runs install" do + let(:warning) { "Don't run Bundler as root." } + + before do + gemfile %(source "file://#{gem_repo1}") + end + + it "warns against that" do + bundle :install, :sudo => true + expect(out).to include(warning) + end + + context "when ENV['BUNDLE_SILENCE_ROOT_WARNING'] is set" do + it "skips the warning" do + bundle :install, :sudo => :preserve_env, :env => { "BUNDLE_SILENCE_ROOT_WARNING" => true } + expect(out).to_not include(warning) + end + end + + context "when silence_root_warning is passed as an option" do + it "skips the warning" do + bundle :install, :sudo => true, :silence_root_warning => true + expect(out).to_not include(warning) + end + end + + context "when silence_root_warning = false" do + it "warns against that" do + bundle :install, :sudo => true, :silence_root_warning => false + expect(out).to include(warning) + end + end + end +end diff --git a/spec/bundler/install/gems/win32_spec.rb b/spec/bundler/install/gems/win32_spec.rb new file mode 100644 index 0000000000..cdad9a8821 --- /dev/null +++ b/spec/bundler/install/gems/win32_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install with win32-generated lockfile" do + it "should read lockfile" do + File.open(bundled_app("Gemfile.lock"), "wb") do |f| + f << "GEM\r\n" + f << " remote: file:#{gem_repo1}/\r\n" + f << " specs:\r\n" + f << "\r\n" + f << " rack (1.0.0)\r\n" + f << "\r\n" + f << "PLATFORMS\r\n" + f << " ruby\r\n" + f << "\r\n" + f << "DEPENDENCIES\r\n" + f << " rack\r\n" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + expect(exitstatus).to eq(0) if exitstatus + end +end diff --git a/spec/bundler/install/gemspecs_spec.rb b/spec/bundler/install/gemspecs_spec.rb new file mode 100644 index 0000000000..97eaf149c1 --- /dev/null +++ b/spec/bundler/install/gemspecs_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install" do + describe "when a gem has a YAML gemspec" do + before :each do + build_repo2 do + build_gem "yaml_spec", :gemspec => :yaml + end + end + + it "still installs correctly" do + gemfile <<-G + source "file://#{gem_repo2}" + gem "yaml_spec" + G + bundle :install + expect(err).to lack_errors + end + + it "still installs correctly when using path" do + build_lib "yaml_spec", :gemspec => :yaml + + install_gemfile <<-G + gem 'yaml_spec', :path => "#{lib_path("yaml_spec-1.0")}" + G + expect(err).to lack_errors + end + end + + it "should use gemspecs in the system cache when available" do + gemfile <<-G + source "http://localtestserver.gem" + gem 'rack' + G + + FileUtils.mkdir_p "#{tmp}/gems/system/specifications" + File.open("#{tmp}/gems/system/specifications/rack-1.0.0.gemspec", "w+") do |f| + spec = Gem::Specification.new do |s| + s.name = "rack" + s.version = "1.0.0" + s.add_runtime_dependency "activesupport", "2.3.2" + end + f.write spec.to_ruby + end + bundle :install, :artifice => "endpoint_marshal_fail" # force gemspec load + expect(the_bundle).to include_gems "activesupport 2.3.2" + end + + context "when ruby version is specified in gemspec and gemfile" do + it "installs when patch level is not specified and the version matches" do + build_lib("foo", :path => bundled_app) do |s| + s.required_ruby_version = "~> #{RUBY_VERSION}.0" + end + + install_gemfile <<-G + ruby '#{RUBY_VERSION}', :engine_version => '#{RUBY_VERSION}', :engine => 'ruby' + gemspec + G + expect(the_bundle).to include_gems "foo 1.0" + end + + it "installs when patch level is specified and the version still matches the current version", + :if => RUBY_PATCHLEVEL >= 0 do + build_lib("foo", :path => bundled_app) do |s| + s.required_ruby_version = "#{RUBY_VERSION}.#{RUBY_PATCHLEVEL}" + end + + install_gemfile <<-G + ruby '#{RUBY_VERSION}', :engine_version => '#{RUBY_VERSION}', :engine => 'ruby', :patchlevel => '#{RUBY_PATCHLEVEL}' + gemspec + G + expect(the_bundle).to include_gems "foo 1.0" + end + + it "fails and complains about patchlevel on patchlevel mismatch", + :if => RUBY_PATCHLEVEL >= 0 do + patchlevel = RUBY_PATCHLEVEL.to_i + 1 + build_lib("foo", :path => bundled_app) do |s| + s.required_ruby_version = "#{RUBY_VERSION}.#{patchlevel}" + end + + install_gemfile <<-G + ruby '#{RUBY_VERSION}', :engine_version => '#{RUBY_VERSION}', :engine => 'ruby', :patchlevel => '#{patchlevel}' + gemspec + G + + expect(out).to include("Ruby patchlevel") + expect(out).to include("but your Gemfile specified") + expect(exitstatus).to eq(18) if exitstatus + end + + it "fails and complains about version on version mismatch" do + version = Gem::Requirement.create(RUBY_VERSION).requirements.first.last.bump.version + + build_lib("foo", :path => bundled_app) do |s| + s.required_ruby_version = version + end + + install_gemfile <<-G + ruby '#{version}', :engine_version => '#{version}', :engine => 'ruby' + gemspec + G + + expect(out).to include("Ruby version") + expect(out).to include("but your Gemfile specified") + expect(exitstatus).to eq(18) if exitstatus + end + end +end diff --git a/spec/bundler/install/git_spec.rb b/spec/bundler/install/git_spec.rb new file mode 100644 index 0000000000..04f2380b45 --- /dev/null +++ b/spec/bundler/install/git_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install" do + context "git sources" do + it "displays the revision hash of the gem repository" do + build_git "foo", "1.0", :path => lib_path("foo") + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo")}" + G + + bundle :install + expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at master@#{revision_for(lib_path("foo"))[0..6]})") + expect(the_bundle).to include_gems "foo 1.0", :source => "git@#{lib_path("foo")}" + end + + it "displays the ref of the gem repository when using branch~num as a ref" do + build_git "foo", "1.0", :path => lib_path("foo") + rev = revision_for(lib_path("foo"))[0..6] + update_git "foo", "2.0", :path => lib_path("foo"), :gemspec => true + rev2 = revision_for(lib_path("foo"))[0..6] + update_git "foo", "3.0", :path => lib_path("foo"), :gemspec => true + + install_gemfile! <<-G + gem "foo", :git => "#{lib_path("foo")}", :ref => "master~2" + G + + bundle! :install + expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at master~2@#{rev})") + expect(the_bundle).to include_gems "foo 1.0", :source => "git@#{lib_path("foo")}" + + update_git "foo", "4.0", :path => lib_path("foo"), :gemspec => true + + bundle! :update + expect(out).to include("Using foo 2.0 (was 1.0) from #{lib_path("foo")} (at master~2@#{rev2})") + expect(the_bundle).to include_gems "foo 2.0", :source => "git@#{lib_path("foo")}" + end + + it "should check out git repos that are missing but not being installed" do + build_git "foo" + + gemfile <<-G + gem "foo", :git => "file://#{lib_path("foo-1.0")}", :group => :development + G + + lockfile <<-L + GIT + remote: file://#{lib_path("foo-1.0")} + specs: + foo (1.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo! + L + + bundle "install --path=vendor/bundle --without development" + + expect(out).to include("Bundle complete!") + expect(vendored_gems("bundler/gems/foo-1.0-#{revision_for(lib_path("foo-1.0"))[0..11]}")).to be_directory + end + end +end diff --git a/spec/bundler/install/path_spec.rb b/spec/bundler/install/path_spec.rb new file mode 100644 index 0000000000..7a501d42b3 --- /dev/null +++ b/spec/bundler/install/path_spec.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install" do + describe "with --path" do + before :each do + build_gem "rack", "1.0.0", :to_system => true do |s| + s.write "lib/rack.rb", "puts 'FAIL'" + end + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + it "does not use available system gems with bundle --path vendor/bundle" do + bundle "install --path vendor/bundle" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "handles paths with regex characters in them" do + dir = bundled_app("bun++dle") + dir.mkpath + + Dir.chdir(dir) do + bundle "install --path vendor/bundle" + expect(out).to include("installed into ./vendor/bundle") + end + + dir.rmtree + end + + it "prints a warning to let the user know what has happened with bundle --path vendor/bundle" do + bundle "install --path vendor/bundle" + expect(out).to include("gems are installed into ./vendor") + end + + it "disallows --path vendor/bundle --system" do + bundle "install --path vendor/bundle --system" + expect(out).to include("Please choose only one option.") + expect(exitstatus).to eq(15) if exitstatus + end + + it "remembers to disable system gems after the first time with bundle --path vendor/bundle" do + bundle "install --path vendor/bundle" + FileUtils.rm_rf bundled_app("vendor") + bundle "install" + + expect(vendored_gems("gems/rack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + describe "when BUNDLE_PATH or the global path config is set" do + before :each do + build_lib "rack", "1.0.0", :to_system => true do |s| + s.write "lib/rack.rb", "raise 'FAIL'" + end + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + def set_bundle_path(type, location) + if type == :env + ENV["BUNDLE_PATH"] = location + elsif type == :global + bundle "config path #{location}", "no-color" => nil + end + end + + [:env, :global].each do |type| + it "installs gems to a path if one is specified" do + set_bundle_path(type, bundled_app("vendor2").to_s) + bundle "install --path vendor/bundle" + + expect(vendored_gems("gems/rack-1.0.0")).to be_directory + expect(bundled_app("vendor2")).not_to be_directory + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "installs gems to BUNDLE_PATH with #{type}" do + set_bundle_path(type, bundled_app("vendor").to_s) + + bundle :install + + expect(bundled_app("vendor/gems/rack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "installs gems to BUNDLE_PATH relative to root when relative" do + set_bundle_path(type, "vendor") + + FileUtils.mkdir_p bundled_app("lol") + Dir.chdir(bundled_app("lol")) do + bundle :install + end + + expect(bundled_app("vendor/gems/rack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + it "installs gems to BUNDLE_PATH from .bundle/config" do + config "BUNDLE_PATH" => bundled_app("vendor/bundle").to_s + + bundle :install + + expect(vendored_gems("gems/rack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "sets BUNDLE_PATH as the first argument to bundle install" do + bundle "install --path ./vendor/bundle" + + expect(vendored_gems("gems/rack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "disables system gems when passing a path to install" do + # This is so that vendored gems can be distributed to others + build_gem "rack", "1.1.0", :to_system => true + bundle "install --path ./vendor/bundle" + + expect(vendored_gems("gems/rack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "re-installs gems whose extensions have been deleted", :ruby_repo, :rubygems => ">= 2.3" do + build_lib "very_simple_binary", "1.0.0", :to_system => true do |s| + s.write "lib/very_simple_binary.rb", "raise 'FAIL'" + end + + gemfile <<-G + source "file://#{gem_repo1}" + gem "very_simple_binary" + G + + bundle "install --path ./vendor/bundle" + + expect(vendored_gems("gems/very_simple_binary-1.0")).to be_directory + expect(vendored_gems("extensions")).to be_directory + expect(the_bundle).to include_gems "very_simple_binary 1.0", :source => "remote1" + + vendored_gems("extensions").rmtree + + run "require 'very_simple_binary_c'" + expect(err).to include("Bundler::GemNotFound") + + bundle "install --path ./vendor/bundle" + + expect(vendored_gems("gems/very_simple_binary-1.0")).to be_directory + expect(vendored_gems("extensions")).to be_directory + expect(the_bundle).to include_gems "very_simple_binary 1.0", :source => "remote1" + end + end + + describe "to a file" do + before do + in_app_root do + `touch /tmp/idontexist bundle` + end + end + + it "reports the file exists" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "install --path bundle" + expect(out).to match(/file already exists/) + end + end +end diff --git a/spec/bundler/install/post_bundle_message_spec.rb b/spec/bundler/install/post_bundle_message_spec.rb new file mode 100644 index 0000000000..4453e4190f --- /dev/null +++ b/spec/bundler/install/post_bundle_message_spec.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "post bundle message" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", "2.3.5", :group => [:emo, :test] + group :test do + gem "rspec" + end + gem "rack-obama", :group => :obama + G + end + + let(:bundle_show_message) { "Use `bundle info [gemname]` to see where a bundled gem is installed." } + let(:bundle_deployment_message) { "Bundled gems are installed into ./vendor" } + let(:bundle_complete_message) { "Bundle complete!" } + let(:bundle_updated_message) { "Bundle updated!" } + let(:installed_gems_stats) { "4 Gemfile dependencies, 5 gems now installed." } + + describe "for fresh bundle install" do + it "without any options" do + bundle :install + expect(out).to include(bundle_show_message) + expect(out).not_to include("Gems in the group") + expect(out).to include(bundle_complete_message) + expect(out).to include(installed_gems_stats) + end + + it "with --without one group" do + bundle "install --without emo" + expect(out).to include(bundle_show_message) + expect(out).to include("Gems in the group emo were not installed") + expect(out).to include(bundle_complete_message) + expect(out).to include(installed_gems_stats) + end + + it "with --without two groups" do + bundle "install --without emo test" + expect(out).to include(bundle_show_message) + expect(out).to include("Gems in the groups emo and test were not installed") + expect(out).to include(bundle_complete_message) + expect(out).to include("4 Gemfile dependencies, 3 gems now installed.") + end + + it "with --without more groups" do + bundle "install --without emo obama test" + expect(out).to include(bundle_show_message) + expect(out).to include("Gems in the groups emo, obama and test were not installed") + expect(out).to include(bundle_complete_message) + expect(out).to include("4 Gemfile dependencies, 2 gems now installed.") + end + + describe "with --path and" do + it "without any options" do + bundle "install --path vendor" + expect(out).to include(bundle_deployment_message) + expect(out).to_not include("Gems in the group") + expect(out).to include(bundle_complete_message) + end + + it "with --without one group" do + bundle "install --without emo --path vendor" + expect(out).to include(bundle_deployment_message) + expect(out).to include("Gems in the group emo were not installed") + expect(out).to include(bundle_complete_message) + end + + it "with --without two groups" do + bundle "install --without emo test --path vendor" + expect(out).to include(bundle_deployment_message) + expect(out).to include("Gems in the groups emo and test were not installed") + expect(out).to include(bundle_complete_message) + end + + it "with --without more groups" do + bundle "install --without emo obama test --path vendor" + expect(out).to include(bundle_deployment_message) + expect(out).to include("Gems in the groups emo, obama and test were not installed") + expect(out).to include(bundle_complete_message) + end + + it "with an absolute --path inside the cwd" do + bundle "install --path #{bundled_app}/cache" + expect(out).to include("Bundled gems are installed into ./cache") + expect(out).to_not include("Gems in the group") + expect(out).to include(bundle_complete_message) + end + + it "with an absolute --path outside the cwd" do + bundle "install --path #{bundled_app}_cache" + expect(out).to include("Bundled gems are installed into #{bundled_app}_cache") + expect(out).to_not include("Gems in the group") + expect(out).to include(bundle_complete_message) + end + end + + describe "with misspelled or non-existent gem name" do + it "should report a helpful error message" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "not-a-gem", :group => :development + G + expect(out).to include("Could not find gem 'not-a-gem' in any of the gem sources listed in your Gemfile.") + end + + it "should report a helpful error message with reference to cache if available" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + bundle :cache + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "not-a-gem", :group => :development + G + expect(out).to include("Could not find gem 'not-a-gem' in any of the gem sources listed in your Gemfile or in gems cached in vendor/cache.") + end + end + end + + describe "for second bundle install run" do + it "without any options" do + 2.times { bundle :install } + expect(out).to include(bundle_show_message) + expect(out).to_not include("Gems in the groups") + expect(out).to include(bundle_complete_message) + expect(out).to include(installed_gems_stats) + end + + it "with --without one group" do + bundle "install --without emo" + bundle :install + expect(out).to include(bundle_show_message) + expect(out).to include("Gems in the group emo were not installed") + expect(out).to include(bundle_complete_message) + expect(out).to include(installed_gems_stats) + end + + it "with --without two groups" do + bundle "install --without emo test" + bundle :install + expect(out).to include(bundle_show_message) + expect(out).to include("Gems in the groups emo and test were not installed") + expect(out).to include(bundle_complete_message) + end + + it "with --without more groups" do + bundle "install --without emo obama test" + bundle :install + expect(out).to include(bundle_show_message) + expect(out).to include("Gems in the groups emo, obama and test were not installed") + expect(out).to include(bundle_complete_message) + end + end + + describe "for bundle update" do + it "without any options" do + bundle :update + expect(out).not_to include("Gems in the groups") + expect(out).to include(bundle_updated_message) + end + + it "with --without one group" do + bundle :install, :without => :emo + bundle :update + expect(out).to include("Gems in the group emo were not installed") + expect(out).to include(bundle_updated_message) + end + + it "with --without two groups" do + bundle "install --without emo test" + bundle :update + expect(out).to include("Gems in the groups emo and test were not installed") + expect(out).to include(bundle_updated_message) + end + + it "with --without more groups" do + bundle "install --without emo obama test" + bundle :update + expect(out).to include("Gems in the groups emo, obama and test were not installed") + expect(out).to include(bundle_updated_message) + end + end +end diff --git a/spec/bundler/install/prereleases_spec.rb b/spec/bundler/install/prereleases_spec.rb new file mode 100644 index 0000000000..6c32094d90 --- /dev/null +++ b/spec/bundler/install/prereleases_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle install" do + describe "when prerelease gems are available" do + it "finds prereleases" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "not_released" + G + expect(the_bundle).to include_gems "not_released 1.0.pre" + end + + it "uses regular releases if available" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "has_prerelease" + G + expect(the_bundle).to include_gems "has_prerelease 1.0" + end + + it "uses prereleases if requested" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "has_prerelease", "1.1.pre" + G + expect(the_bundle).to include_gems "has_prerelease 1.1.pre" + end + end + + describe "when prerelease gems are not available" do + it "still works" do + build_repo3 + install_gemfile <<-G + source "file://#{gem_repo3}" + gem "rack" + G + + expect(the_bundle).to include_gems "rack 1.0" + end + end +end diff --git a/spec/bundler/install/security_policy_spec.rb b/spec/bundler/install/security_policy_spec.rb new file mode 100644 index 0000000000..ab531bdad6 --- /dev/null +++ b/spec/bundler/install/security_policy_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true +require "spec_helper" +require "rubygems/security" + +# unfortunately, testing signed gems with a provided CA is extremely difficult +# as 'gem cert' is currently the only way to add CAs to the system. + +RSpec.describe "policies with unsigned gems" do + before do + build_security_repo + gemfile <<-G + source "file://#{security_repo}" + gem "rack" + gem "signed_gem" + G + end + + it "will work after you try to deploy without a lock" do + bundle "install --deployment" + bundle :install + expect(exitstatus).to eq(0) if exitstatus + expect(the_bundle).to include_gems "rack 1.0", "signed_gem 1.0" + end + + it "will fail when given invalid security policy" do + bundle "install --trust-policy=InvalidPolicyName" + expect(out).to include("Rubygems doesn't know about trust policy") + end + + it "will fail with High Security setting due to presence of unsigned gem" do + bundle "install --trust-policy=HighSecurity" + expect(out).to include("security policy didn't allow") + end + + # This spec will fail on Rubygems 2 rc1 due to a bug in policy.rb. the bug is fixed in rc3. + it "will fail with Medium Security setting due to presence of unsigned gem", :unless => ENV["RGV"] == "v2.0.0.rc.1" do + bundle "install --trust-policy=MediumSecurity" + expect(out).to include("security policy didn't allow") + end + + it "will succeed with no policy" do + bundle "install" + expect(exitstatus).to eq(0) if exitstatus + end +end + +RSpec.describe "policies with signed gems and no CA" do + before do + build_security_repo + gemfile <<-G + source "file://#{security_repo}" + gem "signed_gem" + G + end + + it "will fail with High Security setting, gem is self-signed" do + bundle "install --trust-policy=HighSecurity" + expect(out).to include("security policy didn't allow") + end + + it "will fail with Medium Security setting, gem is self-signed" do + bundle "install --trust-policy=MediumSecurity" + expect(out).to include("security policy didn't allow") + end + + it "will succeed with Low Security setting, low security accepts self signed gem" do + bundle "install --trust-policy=LowSecurity" + expect(exitstatus).to eq(0) if exitstatus + expect(the_bundle).to include_gems "signed_gem 1.0" + end + + it "will succeed with no policy" do + bundle "install" + expect(exitstatus).to eq(0) if exitstatus + expect(the_bundle).to include_gems "signed_gem 1.0" + end +end diff --git a/spec/bundler/install/yanked_spec.rb b/spec/bundler/install/yanked_spec.rb new file mode 100644 index 0000000000..d42978ce4c --- /dev/null +++ b/spec/bundler/install/yanked_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.context "when installing a bundle that includes yanked gems" do + before(:each) do + build_repo4 do + build_gem "foo", "9.0.0" + end + end + + it "throws an error when the original gem version is yanked" do + lockfile <<-L + GEM + remote: file://#{gem_repo4} + specs: + foo (10.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo (= 10.0.0) + + L + + install_gemfile <<-G + source "file://#{gem_repo4}" + gem "foo", "10.0.0" + G + + expect(out).to include("Your bundle is locked to foo (10.0.0)") + end + + it "throws the original error when only the Gemfile specifies a gem version that doesn't exist" do + install_gemfile <<-G + source "file://#{gem_repo4}" + gem "foo", "10.0.0" + G + + expect(out).not_to include("Your bundle is locked to foo (10.0.0)") + expect(out).to include("Could not find gem 'foo (= 10.0.0)' in any of the gem sources") + end +end + +RSpec.context "when using gem before installing" do + it "does not suggest the author has yanked the gem" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "0.9.1" + G + + lockfile <<-L + GEM + remote: file://#{gem_repo1} + specs: + rack (0.9.1) + + PLATFORMS + ruby + + DEPENDENCIES + rack (= 0.9.1) + L + + bundle :list + + expect(out).to include("Could not find rack-0.9.1 in any of the sources") + expect(out).to_not include("Your bundle is locked to rack (0.9.1), but that version could not be found in any of the sources listed in your Gemfile.") + expect(out).to_not include("If you haven't changed sources, that means the author of rack (0.9.1) has removed it.") + expect(out).to_not include("You'll need to update your bundle to a different version of rack (0.9.1) that hasn't been removed in order to install.") + end +end diff --git a/spec/bundler/lock/git_spec.rb b/spec/bundler/lock/git_spec.rb new file mode 100644 index 0000000000..b36f61338d --- /dev/null +++ b/spec/bundler/lock/git_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle lock with git gems" do + before :each do + build_git "foo" + + install_gemfile <<-G + gem 'foo', :git => "#{lib_path("foo-1.0")}" + G + end + + it "doesn't break right after running lock" do + expect(the_bundle).to include_gems "foo 1.0.0" + end + + it "locks a git source to the current ref" do + update_git "foo" + bundle :install + + run <<-RUBY + require 'foo' + puts "WIN" unless defined?(FOO_PREV_REF) + RUBY + + expect(out).to eq("WIN") + end + + it "provides correct #full_gem_path" do + run <<-RUBY + puts Bundler.rubygems.find_name('foo').first.full_gem_path + RUBY + expect(out).to eq(bundle("show foo")) + end +end diff --git a/spec/bundler/lock/lockfile_spec.rb b/spec/bundler/lock/lockfile_spec.rb new file mode 100644 index 0000000000..968c969a55 --- /dev/null +++ b/spec/bundler/lock/lockfile_spec.rb @@ -0,0 +1,1381 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "the lockfile format" do + include Bundler::GemHelpers + + it "generates a simple lockfile for a single source, gem" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "updates the lockfile's bundler version if current ver. is newer" do + lockfile <<-L + GIT + remote: git://github.com/nex3/haml.git + revision: 8a2271f + specs: + + GEM + remote: file://#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + omg! + rack + + BUNDLED WITH + 1.8.2 + L + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not update the lockfile's bundler version if nothing changed during bundle install" do + lockfile <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + 1.10.0 + L + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + 1.10.0 + G + end + + it "updates the lockfile's bundler version if not present" do + lockfile <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + L + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "> 0" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack (> 0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "outputs a warning if the current is older than lockfile's bundler version" do + lockfile <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + 9999999.1.0 + L + + simulate_bundler_version "9999999.0.0" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + end + + warning_message = "the running version of Bundler (9999999.0.0) is older " \ + "than the version that created the lockfile (9999999.1.0)" + expect(out.scan(warning_message).size).to eq(1) + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + 9999999.1.0 + G + end + + it "errors if the current is a major version older than lockfile's bundler version" do + lockfile <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + 9999999.0.0 + L + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + + expect(exitstatus > 0) if exitstatus + expect(out).to include("You must use Bundler 9999999 or greater with this lockfile.") + end + + it "shows a friendly error when running with a new bundler 2 lockfile" do + lockfile <<-L + GEM + remote: https://rails-assets.org/ + specs: + rails-assets-bootstrap (3.3.4) + rails-assets-jquery (>= 1.9.1) + rails-assets-jquery (2.1.4) + + GEM + remote: https://rubygems.org/ + specs: + rake (10.4.2) + + PLATFORMS + ruby + + DEPENDENCIES + rails-assets-bootstrap! + rake + + BUNDLED WITH + 9999999.0.0 + L + + install_gemfile <<-G + source 'https://rubygems.org' + gem 'rake' + + source 'https://rails-assets.org' do + gem 'rails-assets-bootstrap' + end + G + + expect(exitstatus > 0) if exitstatus + expect(out).to include("You must use Bundler 9999999 or greater with this lockfile.") + end + + it "warns when updating bundler major version" do + lockfile <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + 1.10.0 + L + + simulate_bundler_version "9999999.0.0" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + end + + expect(out).to include("Warning: the lockfile is being updated to Bundler " \ + "9999999, after which you will be unable to return to Bundler 1.") + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + + BUNDLED WITH + 9999999.0.0 + G + end + + it "generates a simple lockfile for a single source, gem with dependencies" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack-obama" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + rack-obama (1.0) + rack + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack-obama + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "generates a simple lockfile for a single source, gem with a version requirement" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack-obama", ">= 1.0" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + rack-obama (1.0) + rack + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack-obama (>= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "generates a lockfile wihout credentials for a configured source" do + bundle "config http://localgemserver.test/ user:pass" + + install_gemfile(<<-G, :artifice => "endpoint_strict_basic_authentication", :quiet => true) + source "http://localgemserver.test/" + source "http://user:pass@othergemserver.test/" + + gem "rack-obama", ">= 1.0" + G + + lockfile_should_be <<-G + GEM + remote: http://localgemserver.test/ + remote: http://user:pass@othergemserver.test/ + specs: + rack (1.0.0) + rack-obama (1.0) + rack + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack-obama (>= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "generates lockfiles with multiple requirements" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "net-sftp" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + net-sftp (1.1.1) + net-ssh (>= 1.0.0, < 1.99.0) + net-ssh (1.0) + + PLATFORMS + ruby + + DEPENDENCIES + net-sftp + + BUNDLED WITH + #{Bundler::VERSION} + G + + expect(the_bundle).to include_gems "net-sftp 1.1.1", "net-ssh 1.0.0" + end + + it "generates a simple lockfile for a single pinned source, gem with a version requirement" do + git = build_git "foo" + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + lockfile_should_be <<-G + GIT + remote: #{lib_path("foo-1.0")} + revision: #{git.ref_for("master")} + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not asplode when a platform specific dependency is present and the Gemfile has not been resolved on that platform" do + build_lib "omg", :path => lib_path("omg") + + gemfile <<-G + source "file://#{gem_repo1}" + + platforms :#{not_local_tag} do + gem "omg", :path => "#{lib_path("omg")}" + end + + gem "rack" + G + + lockfile <<-L + GIT + remote: git://github.com/nex3/haml.git + revision: 8a2271f + specs: + + GEM + remote: file://#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{not_local} + + DEPENDENCIES + omg! + rack + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install" + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "serializes global git sources" do + git = build_git "foo" + + install_gemfile <<-G + git "#{lib_path("foo-1.0")}" do + gem "foo" + end + G + + lockfile_should_be <<-G + GIT + remote: #{lib_path("foo-1.0")} + revision: #{git.ref_for("master")} + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "generates a lockfile with a ref for a single pinned source, git gem with a branch requirement" do + git = build_git "foo" + update_git "foo", :branch => "omg" + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "omg" + G + + lockfile_should_be <<-G + GIT + remote: #{lib_path("foo-1.0")} + revision: #{git.ref_for("omg")} + branch: omg + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "generates a lockfile with a ref for a single pinned source, git gem with a tag requirement" do + git = build_git "foo" + update_git "foo", :tag => "omg" + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}", :tag => "omg" + G + + lockfile_should_be <<-G + GIT + remote: #{lib_path("foo-1.0")} + revision: #{git.ref_for("omg")} + tag: omg + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "serializes pinned path sources to the lockfile" do + build_lib "foo" + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo-1.0")}" + G + + lockfile_should_be <<-G + PATH + remote: #{lib_path("foo-1.0")} + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "serializes pinned path sources to the lockfile even when packaging" do + build_lib "foo" + + install_gemfile! <<-G + gem "foo", :path => "#{lib_path("foo-1.0")}" + G + + bundle! "package --all" + bundle! "install --local" + + lockfile_should_be <<-G + PATH + remote: #{lib_path("foo-1.0")} + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "sorts serialized sources by type" do + build_lib "foo" + bar = build_git "bar" + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + gem "foo", :path => "#{lib_path("foo-1.0")}" + gem "bar", :git => "#{lib_path("bar-1.0")}" + G + + lockfile_should_be <<-G + GIT + remote: #{lib_path("bar-1.0")} + revision: #{bar.ref_for("master")} + specs: + bar (1.0) + + PATH + remote: #{lib_path("foo-1.0")} + specs: + foo (1.0) + + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + bar! + foo! + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "lists gems alphabetically" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "thin" + gem "actionpack" + gem "rack-obama" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + actionpack (2.3.2) + activesupport (= 2.3.2) + activesupport (2.3.2) + rack (1.0.0) + rack-obama (1.0) + rack + thin (1.0) + rack + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + actionpack + rack-obama + thin + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "orders dependencies' dependencies in alphabetical order" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rails" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + actionmailer (2.3.2) + activesupport (= 2.3.2) + actionpack (2.3.2) + activesupport (= 2.3.2) + activerecord (2.3.2) + activesupport (= 2.3.2) + activeresource (2.3.2) + activesupport (= 2.3.2) + activesupport (2.3.2) + rails (2.3.2) + actionmailer (= 2.3.2) + actionpack (= 2.3.2) + activerecord (= 2.3.2) + activeresource (= 2.3.2) + rake (= 10.0.2) + rake (10.0.2) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rails + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "orders dependencies by version" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem 'double_deps' + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + double_deps (1.0) + net-ssh + net-ssh (>= 1.0.0) + net-ssh (1.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + double_deps + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add the :require option to the lockfile" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack-obama", ">= 1.0", :require => "rack/obama" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + rack-obama (1.0) + rack + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack-obama (>= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add the :group option to the lockfile" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack-obama", ">= 1.0", :group => :test + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + rack-obama (1.0) + rack + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack-obama (>= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "stores relative paths when the path is provided in a relative fashion and in Gemfile dir" do + build_lib "foo", :path => bundled_app("foo") + + install_gemfile <<-G + path "foo" + gem "foo" + G + + lockfile_should_be <<-G + PATH + remote: foo + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "stores relative paths when the path is provided in a relative fashion and is above Gemfile dir" do + build_lib "foo", :path => bundled_app(File.join("..", "foo")) + + install_gemfile <<-G + path "../foo" + gem "foo" + G + + lockfile_should_be <<-G + PATH + remote: ../foo + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "stores relative paths when the path is provided in an absolute fashion but is relative" do + build_lib "foo", :path => bundled_app("foo") + + install_gemfile <<-G + path File.expand_path("../foo", __FILE__) + gem "foo" + G + + lockfile_should_be <<-G + PATH + remote: foo + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "stores relative paths when the path is provided for gemspec" do + build_lib("foo", :path => tmp.join("foo")) + + install_gemfile <<-G + gemspec :path => "../foo" + G + + lockfile_should_be <<-G + PATH + remote: ../foo + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "keeps existing platforms in the lockfile" do + lockfile <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + java + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + + platforms = ["java", generic_local_platform.to_s].sort + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{platforms[0]} + #{platforms[1]} + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "persists the spec's platform to the lockfile" do + build_gem "platform_specific", "1.0.0", :to_system => true do |s| + s.platform = Gem::Platform.new("universal-java-16") + end + + simulate_platform "universal-java-16" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "platform_specific" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + platform_specific (1.0-java) + + PLATFORMS + java + + DEPENDENCIES + platform_specific + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add duplicate gems" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + activesupport (2.3.5) + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + activesupport + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add duplicate dependencies" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add duplicate dependencies with versions" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0" + gem "rack", "1.0" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + rack (= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "does not add duplicate dependencies in different groups" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0", :group => :one + gem "rack", "1.0", :group => :two + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + rack (= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "raises if two different versions are used" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0" + gem "rack", "1.1" + G + + expect(bundled_app("Gemfile.lock")).not_to exist + expect(out).to include "rack (= 1.0) and rack (= 1.1)" + end + + it "raises if two different sources are used" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rack", :git => "git://hubz.com" + G + + expect(bundled_app("Gemfile.lock")).not_to exist + expect(out).to include "rack (>= 0) should come from an unspecified source and git://hubz.com (at master)" + end + + it "works correctly with multiple version dependencies" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "> 0.9", "< 1.0" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (0.9.1) + + PLATFORMS + ruby + + DEPENDENCIES + rack (> 0.9, < 1.0) + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "captures the Ruby version in the lockfile" do + install_gemfile <<-G + source "file://#{gem_repo1}" + ruby '#{RUBY_VERSION}' + gem "rack", "> 0.9", "< 1.0" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (0.9.1) + + PLATFORMS + ruby + + DEPENDENCIES + rack (> 0.9, < 1.0) + + RUBY VERSION + ruby #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + # Some versions of the Bundler 1.1 RC series introduced corrupted + # lockfiles. There were two major problems: + # + # * multiple copies of the same GIT section appeared in the lockfile + # * when this happened, those sections got multiple copies of gems + # in those sections. + it "fixes corrupted lockfiles" do + build_git "omg", :path => lib_path("omg") + revision = revision_for(lib_path("omg")) + + gemfile <<-G + source "file://#{gem_repo1}" + gem "omg", :git => "#{lib_path("omg")}", :branch => 'master' + G + + bundle "install --path vendor" + expect(the_bundle).to include_gems "omg 1.0" + + # Create a Gemfile.lock that has duplicate GIT sections + lockfile <<-L + GIT + remote: #{lib_path("omg")} + revision: #{revision} + branch: master + specs: + omg (1.0) + + GIT + remote: #{lib_path("omg")} + revision: #{revision} + branch: master + specs: + omg (1.0) + + GEM + remote: file:#{gem_repo1}/ + specs: + + PLATFORMS + #{local} + + DEPENDENCIES + omg! + + BUNDLED WITH + #{Bundler::VERSION} + L + + FileUtils.rm_rf(bundled_app("vendor")) + bundle "install" + expect(the_bundle).to include_gems "omg 1.0" + + # Confirm that duplicate specs do not appear + expect(File.read(bundled_app("Gemfile.lock"))).to eq(strip_whitespace(<<-L)) + GIT + remote: #{lib_path("omg")} + revision: #{revision} + branch: master + specs: + omg (1.0) + + GEM + remote: file:#{gem_repo1}/ + specs: + + PLATFORMS + #{local} + + DEPENDENCIES + omg! + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "raises a helpful error message when the lockfile is missing deps" do + lockfile <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + rack_middleware (1.0) + + PLATFORMS + #{local} + + DEPENDENCIES + rack_middleware + L + + install_gemfile <<-G + source "file:#{gem_repo1}" + gem "rack_middleware" + G + + expect(out).to include("Downloading rack_middleware-1.0 revealed dependencies not in the API or the lockfile (#{Gem::Dependency.new("rack", "= 0.9.1")})."). + and include("Either installing with `--full-index` or running `bundle update rack_middleware` should fix the problem.") + end + + describe "a line ending" do + def set_lockfile_mtime_to_known_value + time = Time.local(2000, 1, 1, 0, 0, 0) + File.utime(time, time, bundled_app("Gemfile.lock")) + end + before(:each) do + build_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "rack" + G + set_lockfile_mtime_to_known_value + end + + it "generates Gemfile.lock with \\n line endings" do + expect(File.read(bundled_app("Gemfile.lock"))).not_to match("\r\n") + expect(the_bundle).to include_gems "rack 1.0" + end + + context "during updates" do + it "preserves Gemfile.lock \\n line endings" do + update_repo2 + + expect { bundle "update" }.to change { File.mtime(bundled_app("Gemfile.lock")) } + expect(File.read(bundled_app("Gemfile.lock"))).not_to match("\r\n") + expect(the_bundle).to include_gems "rack 1.2" + end + + it "preserves Gemfile.lock \\n\\r line endings" do + update_repo2 + win_lock = File.read(bundled_app("Gemfile.lock")).gsub(/\n/, "\r\n") + File.open(bundled_app("Gemfile.lock"), "wb") {|f| f.puts(win_lock) } + set_lockfile_mtime_to_known_value + + expect { bundle "update" }.to change { File.mtime(bundled_app("Gemfile.lock")) } + expect(File.read(bundled_app("Gemfile.lock"))).to match("\r\n") + expect(the_bundle).to include_gems "rack 1.2" + end + end + + context "when nothing changes" do + it "preserves Gemfile.lock \\n line endings" do + expect do + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup + RUBY + end.not_to change { File.mtime(bundled_app("Gemfile.lock")) } + end + + it "preserves Gemfile.lock \\n\\r line endings" do + win_lock = File.read(bundled_app("Gemfile.lock")).gsub(/\n/, "\r\n") + File.open(bundled_app("Gemfile.lock"), "wb") {|f| f.puts(win_lock) } + set_lockfile_mtime_to_known_value + + expect do + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup + RUBY + end.not_to change { File.mtime(bundled_app("Gemfile.lock")) } + end + end + end + + it "refuses to install if Gemfile.lock contains conflict markers" do + lockfile <<-L + GEM + remote: file://#{gem_repo1}/ + specs: + <<<<<<< + rack (1.0.0) + ======= + rack (1.0.1) + >>>>>>> + + PLATFORMS + ruby + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + L + + error = install_gemfile(<<-G) + source "file://#{gem_repo1}" + gem "rack" + G + + expect(error).to match(/your Gemfile.lock contains merge conflicts/i) + expect(error).to match(/git checkout HEAD -- Gemfile.lock/i) + end +end diff --git a/spec/bundler/other/bundle_ruby_spec.rb b/spec/bundler/other/bundle_ruby_spec.rb new file mode 100644 index 0000000000..09fa2c223b --- /dev/null +++ b/spec/bundler/other/bundle_ruby_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle_ruby" do + context "without patchlevel" do + it "returns the ruby version" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.9.3", :engine => 'ruby', :engine_version => '1.9.3' + + gem "foo" + G + + bundle_ruby + + expect(out).to include("ruby 1.9.3") + end + + it "engine defaults to MRI" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.9.3" + + gem "foo" + G + + bundle_ruby + + expect(out).to include("ruby 1.9.3") + end + + it "handles jruby" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine => 'jruby', :engine_version => '1.6.5' + + gem "foo" + G + + bundle_ruby + + expect(out).to include("ruby 1.8.7 (jruby 1.6.5)") + end + + it "handles rbx" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine => 'rbx', :engine_version => '1.2.4' + + gem "foo" + G + + bundle_ruby + + expect(out).to include("ruby 1.8.7 (rbx 1.2.4)") + end + + it "raises an error if engine is used but engine version is not" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine => 'rbx' + + gem "foo" + G + + bundle_ruby + expect(exitstatus).not_to eq(0) if exitstatus + + bundle_ruby + expect(out).to include("Please define :engine_version") + end + + it "raises an error if engine_version is used but engine is not" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine_version => '1.2.4' + + gem "foo" + G + + bundle_ruby + expect(exitstatus).not_to eq(0) if exitstatus + + bundle_ruby + expect(out).to include("Please define :engine") + end + + it "raises an error if engine version doesn't match ruby version for MRI" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine => 'ruby', :engine_version => '1.2.4' + + gem "foo" + G + + bundle_ruby + expect(exitstatus).not_to eq(0) if exitstatus + + bundle_ruby + expect(out).to include("ruby_version must match the :engine_version for MRI") + end + + it "should print if no ruby version is specified" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo" + G + + bundle_ruby + + expect(out).to include("No ruby version specified") + end + end + + context "when using patchlevel" do + it "returns the ruby version" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.9.3", :patchlevel => '429', :engine => 'ruby', :engine_version => '1.9.3' + + gem "foo" + G + + bundle_ruby + + expect(out).to include("ruby 1.9.3p429") + end + + it "handles an engine" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.9.3", :patchlevel => '392', :engine => 'jruby', :engine_version => '1.7.4' + + gem "foo" + G + + bundle_ruby + + expect(out).to include("ruby 1.9.3p392 (jruby 1.7.4)") + end + end +end diff --git a/spec/bundler/other/cli_dispatch_spec.rb b/spec/bundler/other/cli_dispatch_spec.rb new file mode 100644 index 0000000000..8b34a457ef --- /dev/null +++ b/spec/bundler/other/cli_dispatch_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle command names" do + it "work when given fully" do + bundle "install" + expect(err).to lack_errors + expect(out).not_to match(/Ambiguous command/) + end + + it "work when not ambiguous" do + bundle "ins" + expect(err).to lack_errors + expect(out).not_to match(/Ambiguous command/) + end + + it "print a friendly error when ambiguous" do + bundle "in" + expect(err).to lack_errors + expect(out).to match(/Ambiguous command/) + end +end diff --git a/spec/bundler/other/ext_spec.rb b/spec/bundler/other/ext_spec.rb new file mode 100644 index 0000000000..2d6ab941b8 --- /dev/null +++ b/spec/bundler/other/ext_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "Gem::Specification#match_platform" do + it "does not match platforms other than the gem platform" do + darwin = gem "lol", "1.0", "platform_specific-1.0-x86-darwin-10" + expect(darwin.match_platform(pl("java"))).to eq(false) + end + + context "when platform is a string" do + it "matches when platform is a string" do + lazy_spec = Bundler::LazySpecification.new("lol", "1.0", "universal-mingw32") + expect(lazy_spec.match_platform(pl("x86-mingw32"))).to eq(true) + expect(lazy_spec.match_platform(pl("x64-mingw32"))).to eq(true) + end + end +end + +RSpec.describe "Bundler::GemHelpers#generic" do + include Bundler::GemHelpers + + it "converts non-windows platforms into ruby" do + expect(generic(pl("x86-darwin-10"))).to eq(pl("ruby")) + expect(generic(pl("ruby"))).to eq(pl("ruby")) + end + + it "converts java platform variants into java" do + expect(generic(pl("universal-java-17"))).to eq(pl("java")) + expect(generic(pl("java"))).to eq(pl("java")) + end + + it "converts mswin platform variants into x86-mswin32" do + expect(generic(pl("mswin32"))).to eq(pl("x86-mswin32")) + expect(generic(pl("i386-mswin32"))).to eq(pl("x86-mswin32")) + expect(generic(pl("x86-mswin32"))).to eq(pl("x86-mswin32")) + end + + it "converts 32-bit mingw platform variants into x86-mingw32" do + expect(generic(pl("mingw32"))).to eq(pl("x86-mingw32")) + expect(generic(pl("i386-mingw32"))).to eq(pl("x86-mingw32")) + expect(generic(pl("x86-mingw32"))).to eq(pl("x86-mingw32")) + end + + it "converts 64-bit mingw platform variants into x64-mingw32" do + expect(generic(pl("x64-mingw32"))).to eq(pl("x64-mingw32")) + expect(generic(pl("x86_64-mingw32"))).to eq(pl("x64-mingw32")) + end +end + +RSpec.describe "Gem::SourceIndex#refresh!" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + it "does not explode when called", :rubygems => "1.7" do + run "Gem.source_index.refresh!" + run "Gem::SourceIndex.new([]).refresh!" + end + + it "does not explode when called", :rubygems => "< 1.7" do + run "Gem.source_index.refresh!" + run "Gem::SourceIndex.from_gems_in([]).refresh!" + end +end diff --git a/spec/bundler/other/major_deprecation_spec.rb b/spec/bundler/other/major_deprecation_spec.rb new file mode 100644 index 0000000000..465d769538 --- /dev/null +++ b/spec/bundler/other/major_deprecation_spec.rb @@ -0,0 +1,248 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "major deprecations" do + let(:warnings) { out } # change to err in 2.0 + + context "in a .99 version" do + before do + simulate_bundler_version "1.99.1" + bundle "config --delete major_deprecations" + end + + it "prints major deprecations without being configured" do + ruby <<-R + require "bundler" + Bundler::SharedHelpers.major_deprecation(Bundler::VERSION) + R + + expect(warnings).to have_major_deprecation("1.99.1") + end + end + + before do + bundle "config major_deprecations true" + + install_gemfile <<-G + source "file:#{gem_repo1}" + ruby #{RUBY_VERSION.dump} + gem "rack" + G + end + + describe "bundle_ruby" do + it "prints a deprecation" do + bundle_ruby + out.gsub! "\nruby #{RUBY_VERSION}", "" + expect(warnings).to have_major_deprecation "the bundle_ruby executable has been removed in favor of `bundle platform --ruby`" + end + end + + describe "Bundler" do + describe ".clean_env" do + it "is deprecated in favor of .original_env" do + source = "Bundler.clean_env" + bundle "exec ruby -e #{source.dump}" + expect(warnings).to have_major_deprecation "`Bundler.clean_env` has weird edge cases, use `.original_env` instead" + end + end + + describe ".environment" do + it "is deprecated in favor of .load" do + source = "Bundler.environment" + bundle "exec ruby -e #{source.dump}" + expect(warnings).to have_major_deprecation "Bundler.environment has been removed in favor of Bundler.load" + end + end + + shared_examples_for "environmental deprecations" do |trigger| + describe "ruby version", :ruby => "< 2.0" do + it "requires a newer ruby version" do + instance_eval(&trigger) + expect(warnings).to have_major_deprecation "Bundler will only support ruby >= 2.0, you are running #{RUBY_VERSION}" + end + end + + describe "rubygems version", :rubygems => "< 2.0" do + it "requires a newer rubygems version" do + instance_eval(&trigger) + expect(warnings).to have_major_deprecation "Bundler will only support rubygems >= 2.0, you are running #{Gem::VERSION}" + end + end + end + + describe "-rbundler/setup" do + it_behaves_like "environmental deprecations", proc { ruby "require 'bundler/setup'" } + end + + describe "Bundler.setup" do + it_behaves_like "environmental deprecations", proc { ruby "require 'bundler'; Bundler.setup" } + end + + describe "bundle check" do + it_behaves_like "environmental deprecations", proc { bundle :check } + end + + describe "bundle update --quiet" do + it "does not print any deprecations" do + bundle :update, :quiet => true + expect(warnings).not_to have_major_deprecation + end + end + + describe "bundle install --binstubs" do + it "should output a deprecation warning" do + gemfile <<-G + gem 'rack' + G + + bundle :install, :binstubs => true + expect(warnings).to have_major_deprecation a_string_including("The --binstubs option will be removed") + end + end + end + + context "when bundle is run" do + it "should not warn about gems.rb" do + create_file "gems.rb", <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install + expect(err).not_to have_major_deprecation + expect(out).not_to have_major_deprecation + end + + it "should print a Gemfile deprecation warning" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + expect(warnings).to have_major_deprecation("gems.rb and gems.locked will be preferred to Gemfile and Gemfile.lock.") + end + + context "with flags" do + it "should print a deprecation warning about autoremembering flags" do + install_gemfile <<-G, :path => "vendor/bundle" + source "file://#{gem_repo1}" + gem "rack" + G + + expect(warnings).to have_major_deprecation a_string_including( + "flags passed to commands will no longer be automatically remembered." + ) + end + end + end + + context "when Bundler.setup is run in a ruby script" do + it "should print a single deprecation warning" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :group => :test + G + + ruby <<-RUBY + require 'rubygems' + require 'bundler' + require 'bundler/vendored_thor' + + Bundler.ui = Bundler::UI::Shell.new + Bundler.setup + Bundler.setup + RUBY + + expect(warnings).to have_major_deprecation("gems.rb and gems.locked will be preferred to Gemfile and Gemfile.lock.") + end + end + + context "when `bundler/deployment` is required in a ruby script" do + it "should print a capistrano deprecation warning" do + ruby(<<-RUBY) + require 'bundler/deployment' + RUBY + + expect(warnings).to have_major_deprecation("Bundler no longer integrates " \ + "with Capistrano, but Capistrano provides " \ + "its own integration with Bundler via the " \ + "capistrano-bundler gem. Use it instead.") + end + end + + describe Bundler::Dsl do + before do + @rubygems = double("rubygems") + allow(Bundler::Source::Rubygems).to receive(:new) { @rubygems } + end + + context "with github gems" do + it "warns about the https change" do + msg = "The :github option uses the git: protocol, which is not secure. " \ + "Bundler 2.0 will use the https: protocol, which is secure. Enable this change now by " \ + "running `bundle config github.https true`." + expect(Bundler::SharedHelpers).to receive(:major_deprecation).with(msg) + subject.gem("sparks", :github => "indirect/sparks") + end + + it "upgrades to https on request" do + Bundler.settings["github.https"] = true + subject.gem("sparks", :github => "indirect/sparks") + expect(Bundler::SharedHelpers).to receive(:major_deprecation).never + github_uri = "https://github.com/indirect/sparks.git" + expect(subject.dependencies.first.source.uri).to eq(github_uri) + end + end + + context "with bitbucket gems" do + it "warns about removal" do + allow(Bundler.ui).to receive(:deprecate) + msg = "The :bitbucket git source is deprecated, and will be removed " \ + "in Bundler 2.0. Add this code to your Gemfile to ensure it " \ + "continues to work:\n git_source(:bitbucket) do |repo_name|\n " \ + " \"https://\#{user_name}@bitbucket.org/\#{user_name}/\#{repo_name}" \ + ".git\"\n end\n" + expect(Bundler::SharedHelpers).to receive(:major_deprecation).with(msg) + subject.gem("not-really-a-gem", :bitbucket => "mcorp/flatlab-rails") + end + end + + context "with gist gems" do + it "warns about removal" do + allow(Bundler.ui).to receive(:deprecate) + msg = "The :gist git source is deprecated, and will be removed " \ + "in Bundler 2.0. Add this code to your Gemfile to ensure it " \ + "continues to work:\n git_source(:gist) do |repo_name|\n " \ + " \"https://gist.github.com/\#{repo_name}.git\"\n" \ + " end\n" + expect(Bundler::SharedHelpers).to receive(:major_deprecation).with(msg) + subject.gem("not-really-a-gem", :gist => "1234") + end + end + end + + context "bundle list" do + it "prints a deprecation warning" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :list + + out.gsub!(/gems included.*?\[DEPRECATED/im, "[DEPRECATED") + + expect(warnings).to have_major_deprecation("use `bundle show` instead of `bundle list`") + end + end + + context "bundle console" do + it "prints a deprecation warning" do + bundle "console" + + expect(warnings).to have_major_deprecation \ + "bundle console will be replaced by `bin/console` generated by `bundle gem <name>`" + end + end +end diff --git a/spec/bundler/other/platform_spec.rb b/spec/bundler/other/platform_spec.rb new file mode 100644 index 0000000000..6adbcef111 --- /dev/null +++ b/spec/bundler/other/platform_spec.rb @@ -0,0 +1,1292 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle platform" do + context "without flags" do + it "returns all the output" do + gemfile <<-G + source "file://#{gem_repo1}" + + #{ruby_version_correct} + + gem "foo" + G + + bundle "platform" + expect(out).to eq(<<-G.chomp) +Your platform is: #{RUBY_PLATFORM} + +Your app has gems that work on these platforms: +* ruby + +Your Gemfile specifies a Ruby version requirement: +* ruby #{RUBY_VERSION} + +Your current platform satisfies the Ruby version requirement. +G + end + + it "returns all the output including the patchlevel" do + gemfile <<-G + source "file://#{gem_repo1}" + + #{ruby_version_correct_patchlevel} + + gem "foo" + G + + bundle "platform" + expect(out).to eq(<<-G.chomp) +Your platform is: #{RUBY_PLATFORM} + +Your app has gems that work on these platforms: +* ruby + +Your Gemfile specifies a Ruby version requirement: +* ruby #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} + +Your current platform satisfies the Ruby version requirement. +G + end + + it "doesn't print ruby version requirement if it isn't specified" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo" + G + + bundle "platform" + expect(out).to eq(<<-G.chomp) +Your platform is: #{RUBY_PLATFORM} + +Your app has gems that work on these platforms: +* ruby + +Your Gemfile does not specify a Ruby version requirement. +G + end + + it "doesn't match the ruby version requirement" do + gemfile <<-G + source "file://#{gem_repo1}" + + #{ruby_version_incorrect} + + gem "foo" + G + + bundle "platform" + expect(out).to eq(<<-G.chomp) +Your platform is: #{RUBY_PLATFORM} + +Your app has gems that work on these platforms: +* ruby + +Your Gemfile specifies a Ruby version requirement: +* ruby #{not_local_ruby_version} + +Your Ruby version is #{RUBY_VERSION}, but your Gemfile specified #{not_local_ruby_version} +G + end + end + + context "--ruby" do + it "returns ruby version when explicit" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.9.3", :engine => 'ruby', :engine_version => '1.9.3' + + gem "foo" + G + + bundle "platform --ruby" + + expect(out).to eq("ruby 1.9.3") + end + + it "defaults to MRI" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.9.3" + + gem "foo" + G + + bundle "platform --ruby" + + expect(out).to eq("ruby 1.9.3") + end + + it "handles jruby" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine => 'jruby', :engine_version => '1.6.5' + + gem "foo" + G + + bundle "platform --ruby" + + expect(out).to eq("ruby 1.8.7 (jruby 1.6.5)") + end + + it "handles rbx" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine => 'rbx', :engine_version => '1.2.4' + + gem "foo" + G + + bundle "platform --ruby" + + expect(out).to eq("ruby 1.8.7 (rbx 1.2.4)") + end + + it "raises an error if engine is used but engine version is not" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine => 'rbx' + + gem "foo" + G + + bundle "platform" + + expect(exitstatus).not_to eq(0) if exitstatus + end + + it "raises an error if engine_version is used but engine is not" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine_version => '1.2.4' + + gem "foo" + G + + bundle "platform" + + expect(exitstatus).not_to eq(0) if exitstatus + end + + it "raises an error if engine version doesn't match ruby version for MRI" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.8.7", :engine => 'ruby', :engine_version => '1.2.4' + + gem "foo" + G + + bundle "platform" + + expect(exitstatus).not_to eq(0) if exitstatus + end + + it "should print if no ruby version is specified" do + gemfile <<-G + source "file://#{gem_repo1}" + + gem "foo" + G + + bundle "platform --ruby" + + expect(out).to eq("No ruby version specified") + end + + it "handles when there is a locked requirement" do + gemfile <<-G + ruby "< 1.8.7" + G + + lockfile <<-L + GEM + specs: + + PLATFORMS + ruby + + DEPENDENCIES + + RUBY VERSION + ruby 1.0.0p127 + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle! "platform --ruby" + expect(out).to eq("ruby 1.0.0p127") + end + + it "handles when there is a requirement in the gemfile" do + gemfile <<-G + ruby ">= 1.8.7" + G + + bundle! "platform --ruby" + expect(out).to eq("ruby 1.8.7") + end + + it "handles when there are multiple requirements in the gemfile" do + gemfile <<-G + ruby ">= 1.8.7", "< 2.0.0" + G + + bundle! "platform --ruby" + expect(out).to eq("ruby 1.8.7") + end + end + + let(:ruby_version_correct) { "ruby \"#{RUBY_VERSION}\", :engine => \"#{local_ruby_engine}\", :engine_version => \"#{local_engine_version}\"" } + let(:ruby_version_correct_engineless) { "ruby \"#{RUBY_VERSION}\"" } + let(:ruby_version_correct_patchlevel) { "#{ruby_version_correct}, :patchlevel => '#{RUBY_PATCHLEVEL}'" } + let(:ruby_version_incorrect) { "ruby \"#{not_local_ruby_version}\", :engine => \"#{local_ruby_engine}\", :engine_version => \"#{not_local_ruby_version}\"" } + let(:engine_incorrect) { "ruby \"#{RUBY_VERSION}\", :engine => \"#{not_local_tag}\", :engine_version => \"#{RUBY_VERSION}\"" } + let(:engine_version_incorrect) { "ruby \"#{RUBY_VERSION}\", :engine => \"#{local_ruby_engine}\", :engine_version => \"#{not_local_engine_version}\"" } + let(:patchlevel_incorrect) { "#{ruby_version_correct}, :patchlevel => '#{not_local_patchlevel}'" } + let(:patchlevel_fixnum) { "#{ruby_version_correct}, :patchlevel => #{RUBY_PATCHLEVEL}1" } + + def should_be_ruby_version_incorrect + expect(exitstatus).to eq(18) if exitstatus + expect(out).to be_include("Your Ruby version is #{RUBY_VERSION}, but your Gemfile specified #{not_local_ruby_version}") + end + + def should_be_engine_incorrect + expect(exitstatus).to eq(18) if exitstatus + expect(out).to be_include("Your Ruby engine is #{local_ruby_engine}, but your Gemfile specified #{not_local_tag}") + end + + def should_be_engine_version_incorrect + expect(exitstatus).to eq(18) if exitstatus + expect(out).to be_include("Your #{local_ruby_engine} version is #{local_engine_version}, but your Gemfile specified #{local_ruby_engine} #{not_local_engine_version}") + end + + def should_be_patchlevel_incorrect + expect(exitstatus).to eq(18) if exitstatus + expect(out).to be_include("Your Ruby patchlevel is #{RUBY_PATCHLEVEL}, but your Gemfile specified #{not_local_patchlevel}") + end + + def should_be_patchlevel_fixnum + expect(exitstatus).to eq(18) if exitstatus + expect(out).to be_include("The Ruby patchlevel in your Gemfile must be a string") + end + + context "bundle install" do + it "installs fine when the ruby version matches" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{ruby_version_correct} + G + + expect(bundled_app("Gemfile.lock")).to exist + end + + it "installs fine with any engine" do + simulate_ruby_engine "jruby" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{ruby_version_correct_engineless} + G + + expect(bundled_app("Gemfile.lock")).to exist + end + end + + it "installs fine when the patchlevel matches" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{ruby_version_correct_patchlevel} + G + + expect(bundled_app("Gemfile.lock")).to exist + end + + it "doesn't install when the ruby version doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{ruby_version_incorrect} + G + + expect(bundled_app("Gemfile.lock")).not_to exist + should_be_ruby_version_incorrect + end + + it "doesn't install when engine doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{engine_incorrect} + G + + expect(bundled_app("Gemfile.lock")).not_to exist + should_be_engine_incorrect + end + + it "doesn't install when engine version doesn't match" do + simulate_ruby_engine "jruby" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{engine_version_incorrect} + G + + expect(bundled_app("Gemfile.lock")).not_to exist + should_be_engine_version_incorrect + end + end + + it "doesn't install when patchlevel doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{patchlevel_incorrect} + G + + expect(bundled_app("Gemfile.lock")).not_to exist + should_be_patchlevel_incorrect + end + end + + context "bundle check" do + it "checks fine when the ruby version matches" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{ruby_version_correct} + G + + bundle :check + expect(exitstatus).to eq(0) if exitstatus + expect(out).to eq("Resolving dependencies...\nThe Gemfile's dependencies are satisfied") + end + + it "checks fine with any engine" do + simulate_ruby_engine "jruby" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{ruby_version_correct_engineless} + G + + bundle :check + expect(exitstatus).to eq(0) if exitstatus + expect(out).to eq("Resolving dependencies...\nThe Gemfile's dependencies are satisfied") + end + end + + it "fails when ruby version doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{ruby_version_incorrect} + G + + bundle :check + should_be_ruby_version_incorrect + end + + it "fails when engine doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{engine_incorrect} + G + + bundle :check + should_be_engine_incorrect + end + + it "fails when engine version doesn't match" do + simulate_ruby_engine "ruby" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{engine_version_incorrect} + G + + bundle :check + should_be_engine_version_incorrect + end + end + + it "fails when patchlevel doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{patchlevel_incorrect} + G + + bundle :check + should_be_patchlevel_incorrect + end + end + + context "bundle update" do + before do + build_repo2 + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + gem "rack-obama" + G + end + + it "updates successfully when the ruby version matches" do + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + gem "rack-obama" + + #{ruby_version_correct} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle "update" + expect(the_bundle).to include_gems "rack 1.2", "rack-obama 1.0", "activesupport 3.0" + end + + it "updates fine with any engine" do + simulate_ruby_engine "jruby" do + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + gem "rack-obama" + + #{ruby_version_correct_engineless} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle "update" + expect(the_bundle).to include_gems "rack 1.2", "rack-obama 1.0", "activesupport 3.0" + end + end + + it "fails when ruby version doesn't match" do + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + gem "rack-obama" + + #{ruby_version_incorrect} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle :update + should_be_ruby_version_incorrect + end + + it "fails when ruby engine doesn't match" do + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + gem "rack-obama" + + #{engine_incorrect} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle :update + should_be_engine_incorrect + end + + it "fails when ruby engine version doesn't match" do + simulate_ruby_engine "jruby" do + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport" + gem "rack-obama" + + #{engine_version_incorrect} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle :update + should_be_engine_version_incorrect + end + end + + it "fails when patchlevel doesn't match" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{patchlevel_incorrect} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle :update + should_be_patchlevel_incorrect + end + end + + context "bundle show" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + end + + it "prints path if ruby version is correct" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + + #{ruby_version_correct} + G + + bundle "show rails" + expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s) + end + + it "prints path if ruby version is correct for any engine" do + simulate_ruby_engine "jruby" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + + #{ruby_version_correct_engineless} + G + + bundle "show rails" + expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s) + end + end + + it "fails if ruby version doesn't match" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + + #{ruby_version_incorrect} + G + + bundle "show rails" + should_be_ruby_version_incorrect + end + + it "fails if engine doesn't match" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + + #{engine_incorrect} + G + + bundle "show rails" + should_be_engine_incorrect + end + + it "fails if engine version doesn't match" do + simulate_ruby_engine "jruby" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + + #{engine_version_incorrect} + G + + bundle "show rails" + should_be_engine_version_incorrect + end + end + + it "fails when patchlevel doesn't match" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{patchlevel_incorrect} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle "show rails" + should_be_patchlevel_incorrect + end + end + + context "bundle cache" do + before do + gemfile <<-G + gem 'rack' + G + + system_gems "rack-1.0.0" + end + + it "copies the .gem file to vendor/cache when ruby version matches" do + gemfile <<-G + gem 'rack' + + #{ruby_version_correct} + G + + bundle :cache + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + end + + it "copies the .gem file to vendor/cache when ruby version matches for any engine" do + simulate_ruby_engine "jruby" do + gemfile <<-G + gem 'rack' + + #{ruby_version_correct_engineless} + G + + bundle :cache + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + end + end + + it "fails if the ruby version doesn't match" do + gemfile <<-G + gem 'rack' + + #{ruby_version_incorrect} + G + + bundle :cache + should_be_ruby_version_incorrect + end + + it "fails if the engine doesn't match" do + gemfile <<-G + gem 'rack' + + #{engine_incorrect} + G + + bundle :cache + should_be_engine_incorrect + end + + it "fails if the engine version doesn't match" do + simulate_ruby_engine "jruby" do + gemfile <<-G + gem 'rack' + + #{engine_version_incorrect} + G + + bundle :cache + should_be_engine_version_incorrect + end + end + + it "fails when patchlevel doesn't match" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{patchlevel_incorrect} + G + + bundle :cache + should_be_patchlevel_incorrect + end + end + + context "bundle pack" do + before do + gemfile <<-G + gem 'rack' + G + + system_gems "rack-1.0.0" + end + + it "copies the .gem file to vendor/cache when ruby version matches" do + gemfile <<-G + gem 'rack' + + #{ruby_version_correct} + G + + bundle :pack + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + end + + it "copies the .gem file to vendor/cache when ruby version matches any engine" do + simulate_ruby_engine "jruby" do + gemfile <<-G + gem 'rack' + + #{ruby_version_correct_engineless} + G + + bundle :pack + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + end + end + + it "fails if the ruby version doesn't match" do + gemfile <<-G + gem 'rack' + + #{ruby_version_incorrect} + G + + bundle :pack + should_be_ruby_version_incorrect + end + + it "fails if the engine doesn't match" do + gemfile <<-G + gem 'rack' + + #{engine_incorrect} + G + + bundle :pack + should_be_engine_incorrect + end + + it "fails if the engine version doesn't match" do + simulate_ruby_engine "jruby" do + gemfile <<-G + gem 'rack' + + #{engine_version_incorrect} + G + + bundle :pack + should_be_engine_version_incorrect + end + end + + it "fails when patchlevel doesn't match" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{patchlevel_incorrect} + G + + bundle :pack + should_be_patchlevel_incorrect + end + end + + context "bundle exec" do + before do + ENV["BUNDLER_FORCE_TTY"] = "true" + system_gems "rack-1.0.0", "rack-0.9.1" + end + + it "activates the correct gem when ruby version matches" do + gemfile <<-G + gem "rack", "0.9.1" + + #{ruby_version_correct} + G + + bundle "exec rackup" + expect(out).to eq("0.9.1") + end + + it "activates the correct gem when ruby version matches any engine" do + simulate_ruby_engine "jruby" do + gemfile <<-G + gem "rack", "0.9.1" + + #{ruby_version_correct_engineless} + G + + bundle "exec rackup" + expect(out).to eq("0.9.1") + end + end + + it "fails when the ruby version doesn't match" do + gemfile <<-G + gem "rack", "0.9.1" + + #{ruby_version_incorrect} + G + + bundle "exec rackup" + should_be_ruby_version_incorrect + end + + it "fails when the engine doesn't match" do + gemfile <<-G + gem "rack", "0.9.1" + + #{engine_incorrect} + G + + bundle "exec rackup" + should_be_engine_incorrect + end + + # it "fails when the engine version doesn't match" do + # simulate_ruby_engine "jruby" do + # gemfile <<-G + # gem "rack", "0.9.1" + # + # #{engine_version_incorrect} + # G + # + # bundle "exec rackup" + # should_be_engine_version_incorrect + # end + # end + + it "fails when patchlevel doesn't match" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + + #{patchlevel_incorrect} + G + + bundle "exec rackup" + should_be_patchlevel_incorrect + end + end + + context "bundle console" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + G + end + + it "starts IRB with the default group loaded when ruby version matches" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + + #{ruby_version_correct} + G + + bundle "console" do |input, _, _| + input.puts("puts RACK") + input.puts("exit") + end + expect(out).to include("0.9.1") + end + + it "starts IRB with the default group loaded when ruby version matches any engine" do + simulate_ruby_engine "jruby" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + + #{ruby_version_correct_engineless} + G + + bundle "console" do |input, _, _| + input.puts("puts RACK") + input.puts("exit") + end + expect(out).to include("0.9.1") + end + end + + it "fails when ruby version doesn't match" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + + #{ruby_version_incorrect} + G + + bundle "console" + should_be_ruby_version_incorrect + end + + it "fails when engine doesn't match" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + + #{engine_incorrect} + G + + bundle "console" + should_be_engine_incorrect + end + + it "fails when engine version doesn't match" do + simulate_ruby_engine "jruby" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + + #{engine_version_incorrect} + G + + bundle "console" + should_be_engine_version_incorrect + end + end + + it "fails when patchlevel doesn't match" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "activesupport", :group => :test + gem "rack_middleware", :group => :development + + #{patchlevel_incorrect} + G + + bundle "console" + should_be_patchlevel_incorrect + end + end + + context "Bundler.setup" do + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + gem "rack", :group => :test + G + + ENV["BUNDLER_FORCE_TTY"] = "true" + end + + it "makes a Gemfile.lock if setup succeeds" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + gem "rack" + + #{ruby_version_correct} + G + + FileUtils.rm(bundled_app("Gemfile.lock")) + + run "1" + expect(bundled_app("Gemfile.lock")).to exist + end + + it "makes a Gemfile.lock if setup succeeds for any engine" do + simulate_ruby_engine "jruby" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + gem "rack" + + #{ruby_version_correct_engineless} + G + + FileUtils.rm(bundled_app("Gemfile.lock")) + + run "1" + expect(bundled_app("Gemfile.lock")).to exist + end + end + + it "fails when ruby version doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + gem "rack" + + #{ruby_version_incorrect} + G + + FileUtils.rm(bundled_app("Gemfile.lock")) + + ruby <<-R + require 'rubygems' + require 'bundler/setup' + R + + expect(bundled_app("Gemfile.lock")).not_to exist + should_be_ruby_version_incorrect + end + + it "fails when engine doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + gem "rack" + + #{engine_incorrect} + G + + FileUtils.rm(bundled_app("Gemfile.lock")) + + ruby <<-R + require 'rubygems' + require 'bundler/setup' + R + + expect(bundled_app("Gemfile.lock")).not_to exist + should_be_engine_incorrect + end + + it "fails when engine version doesn't match" do + simulate_ruby_engine "jruby" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + gem "rack" + + #{engine_version_incorrect} + G + + FileUtils.rm(bundled_app("Gemfile.lock")) + + ruby <<-R + require 'rubygems' + require 'bundler/setup' + R + + expect(bundled_app("Gemfile.lock")).not_to exist + should_be_engine_version_incorrect + end + end + + it "fails when patchlevel doesn't match" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + gem "rack" + + #{patchlevel_incorrect} + G + + FileUtils.rm(bundled_app("Gemfile.lock")) + + ruby <<-R + require 'rubygems' + require 'bundler/setup' + R + + expect(bundled_app("Gemfile.lock")).not_to exist + should_be_patchlevel_incorrect + end + end + + context "bundle outdated" do + before do + build_repo2 do + build_git "foo", :path => lib_path("foo") + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + G + end + + it "returns list of outdated gems when the ruby version matches" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{ruby_version_correct} + G + + bundle "outdated" + expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5") + expect(out).to include("foo (newest 1.0") + end + + it "returns list of outdated gems when the ruby version matches for any engine" do + simulate_ruby_engine "jruby" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{ruby_version_correct_engineless} + G + + bundle "outdated" + expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5)") + expect(out).to include("foo (newest 1.0") + end + end + + it "fails when the ruby version doesn't match" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{ruby_version_incorrect} + G + + bundle "outdated" + should_be_ruby_version_incorrect + end + + it "fails when the engine doesn't match" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{engine_incorrect} + G + + bundle "outdated" + should_be_engine_incorrect + end + + it "fails when the engine version doesn't match" do + simulate_ruby_engine "jruby" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{engine_version_incorrect} + G + + bundle "outdated" + should_be_engine_version_incorrect + end + end + + it "fails when the patchlevel doesn't match" do + simulate_ruby_engine "jruby" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{patchlevel_incorrect} + G + + bundle "outdated" + should_be_patchlevel_incorrect + end + end + + it "fails when the patchlevel is a fixnum" do + simulate_ruby_engine "jruby" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", :path => lib_path("foo") + end + + gemfile <<-G + source "file://#{gem_repo2}" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{patchlevel_fixnum} + G + + bundle "outdated" + should_be_patchlevel_fixnum + end + end + end +end diff --git a/spec/bundler/other/ssl_cert_spec.rb b/spec/bundler/other/ssl_cert_spec.rb new file mode 100644 index 0000000000..2de4dfdd0c --- /dev/null +++ b/spec/bundler/other/ssl_cert_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +require "spec_helper" +require "bundler/ssl_certs/certificate_manager" + +RSpec.describe "SSL Certificates", :rubygems_master do + hosts = %w( + rubygems.org + index.rubygems.org + rubygems.global.ssl.fastly.net + staging.rubygems.org + ) + + hosts.each do |host| + it "can securely connect to #{host}", :realworld do + Bundler::SSLCerts::CertificateManager.new.connect_to(host) + end + end +end diff --git a/spec/bundler/plugins/command_spec.rb b/spec/bundler/plugins/command_spec.rb new file mode 100644 index 0000000000..6ad782b758 --- /dev/null +++ b/spec/bundler/plugins/command_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "command plugins" do + before do + build_repo2 do + build_plugin "command-mah" do |s| + s.write "plugins.rb", <<-RUBY + module Mah + class Plugin < Bundler::Plugin::API + command "mahcommand" # declares the command + + def exec(command, args) + puts "MahHello" + end + end + end + RUBY + end + end + + bundle "plugin install command-mah --source file://#{gem_repo2}" + end + + it "executes without arguments" do + expect(out).to include("Installed plugin command-mah") + + bundle "mahcommand" + expect(out).to eq("MahHello") + end + + it "accepts the arguments" do + build_repo2 do + build_plugin "the-echoer" do |s| + s.write "plugins.rb", <<-RUBY + module Resonance + class Echoer + # Another method to declare the command + Bundler::Plugin::API.command "echo", self + + def exec(command, args) + puts "You gave me \#{args.join(", ")}" + end + end + end + RUBY + end + end + + bundle "plugin install the-echoer --source file://#{gem_repo2}" + expect(out).to include("Installed plugin the-echoer") + + bundle "echo tacos tofu lasange", "no-color" => false + expect(out).to eq("You gave me tacos, tofu, lasange") + end + + it "raises error on redeclaration of command" do + build_repo2 do + build_plugin "copycat" do |s| + s.write "plugins.rb", <<-RUBY + module CopyCat + class Cheater < Bundler::Plugin::API + command "mahcommand", self + + def exec(command, args) + end + end + end + RUBY + end + end + + bundle "plugin install copycat --source file://#{gem_repo2}" + + expect(out).not_to include("Installed plugin copycat") + + expect(out).to include("Failed to install plugin") + + expect(out).to include("Command(s) `mahcommand` declared by copycat are already registered.") + end +end diff --git a/spec/bundler/plugins/hook_spec.rb b/spec/bundler/plugins/hook_spec.rb new file mode 100644 index 0000000000..9850d850ac --- /dev/null +++ b/spec/bundler/plugins/hook_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "hook plugins" do + before do + build_repo2 do + build_plugin "before-install-plugin" do |s| + s.write "plugins.rb", <<-RUBY + Bundler::Plugin::API.hook "before-install-all" do |deps| + puts "gems to be installed \#{deps.map(&:name).join(", ")}" + end + RUBY + end + end + + bundle "plugin install before-install-plugin --source file://#{gem_repo2}" + end + + it "runs after a rubygem is installed" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rake" + gem "rack" + G + + expect(out).to include "gems to be installed rake, rack" + end +end diff --git a/spec/bundler/plugins/install_spec.rb b/spec/bundler/plugins/install_spec.rb new file mode 100644 index 0000000000..e2d351181c --- /dev/null +++ b/spec/bundler/plugins/install_spec.rb @@ -0,0 +1,258 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundler plugin install" do + before do + build_repo2 do + build_plugin "foo" + build_plugin "kung-foo" + end + end + + it "shows proper message when gem in not found in the source" do + bundle "plugin install no-foo --source file://#{gem_repo1}" + + expect(out).to include("Could not find") + plugin_should_not_be_installed("no-foo") + end + + it "installs from rubygems source" do + bundle "plugin install foo --source file://#{gem_repo2}" + + expect(out).to include("Installed plugin foo") + plugin_should_be_installed("foo") + end + + it "installs multiple plugins" do + bundle "plugin install foo kung-foo --source file://#{gem_repo2}" + + expect(out).to include("Installed plugin foo") + expect(out).to include("Installed plugin kung-foo") + + plugin_should_be_installed("foo", "kung-foo") + end + + it "uses the same version for multiple plugins" do + update_repo2 do + build_plugin "foo", "1.1" + build_plugin "kung-foo", "1.1" + end + + bundle "plugin install foo kung-foo --version '1.0' --source file://#{gem_repo2}" + + expect(out).to include("Installing foo 1.0") + expect(out).to include("Installing kung-foo 1.0") + plugin_should_be_installed("foo", "kung-foo") + end + + it "works with different load paths" do + build_repo2 do + build_plugin "testing" do |s| + s.write "plugins.rb", <<-RUBY + require "fubar" + class Test < Bundler::Plugin::API + command "check2" + + def exec(command, args) + puts "mate" + end + end + RUBY + s.require_paths = %w(lib src) + s.write("src/fubar.rb") + end + end + bundle "plugin install testing --source file://#{gem_repo2}" + + bundle "check2", "no-color" => false + expect(out).to eq("mate") + end + + context "malformatted plugin" do + it "fails when plugins.rb is missing" do + update_repo2 do + build_plugin "foo", "1.1" + build_plugin "kung-foo", "1.1" + end + + bundle "plugin install foo kung-foo --version '1.0' --source file://#{gem_repo2}" + + expect(out).to include("Installing foo 1.0") + expect(out).to include("Installing kung-foo 1.0") + plugin_should_be_installed("foo", "kung-foo") + + build_repo2 do + build_gem "charlie" + end + + bundle "plugin install charlie --source file://#{gem_repo2}" + + expect(out).to include("plugins.rb was not found") + + expect(global_plugin_gem("charlie-1.0")).not_to be_directory + + plugin_should_be_installed("foo", "kung-foo") + plugin_should_not_be_installed("charlie") + end + + it "fails when plugins.rb throws exception on load" do + build_repo2 do + build_plugin "chaplin" do |s| + s.write "plugins.rb", <<-RUBY + raise "I got you man" + RUBY + end + end + + bundle "plugin install chaplin --source file://#{gem_repo2}" + + expect(global_plugin_gem("chaplin-1.0")).not_to be_directory + + plugin_should_not_be_installed("chaplin") + end + end + + context "git plugins" do + it "installs form a git source" do + build_git "foo" do |s| + s.write "plugins.rb" + end + + bundle "plugin install foo --git file://#{lib_path("foo-1.0")}" + + expect(out).to include("Installed plugin foo") + plugin_should_be_installed("foo") + end + end + + context "Gemfile eval" do + it "installs plugins listed in gemfile" do + gemfile <<-G + source 'file://#{gem_repo2}' + plugin 'foo' + gem 'rack', "1.0.0" + G + + bundle "install" + + expect(out).to include("Installed plugin foo") + + expect(out).to include("Bundle complete!") + + expect(the_bundle).to include_gems("rack 1.0.0") + plugin_should_be_installed("foo") + end + + it "accepts plugin version" do + update_repo2 do + build_plugin "foo", "1.1.0" + end + + install_gemfile <<-G + source 'file://#{gem_repo2}' + plugin 'foo', "1.0" + G + + bundle "install" + + expect(out).to include("Installing foo 1.0") + + plugin_should_be_installed("foo") + + expect(out).to include("Bundle complete!") + end + + it "accepts git sources" do + build_git "ga-plugin" do |s| + s.write "plugins.rb" + end + + install_gemfile <<-G + plugin 'ga-plugin', :git => "#{lib_path("ga-plugin-1.0")}" + G + + expect(out).to include("Installed plugin ga-plugin") + plugin_should_be_installed("ga-plugin") + end + end + + context "inline gemfiles" do + it "installs the listed plugins" do + code = <<-RUBY + require "bundler/inline" + + gemfile do + source 'file://#{gem_repo2}' + plugin 'foo' + end + RUBY + + ruby code + expect(local_plugin_gem("foo-1.0", "plugins.rb")).to exist + end + end + + describe "local plugin" do + it "is installed when inside an app" do + gemfile "" + bundle "plugin install foo --source file://#{gem_repo2}" + + plugin_should_be_installed("foo") + expect(local_plugin_gem("foo-1.0")).to be_directory + end + + context "conflict with global plugin" do + before do + update_repo2 do + build_plugin "fubar" do |s| + s.write "plugins.rb", <<-RUBY + class Fubar < Bundler::Plugin::API + command "shout" + + def exec(command, args) + puts "local_one" + end + end + RUBY + end + end + + # inside the app + gemfile "source 'file://#{gem_repo2}'\nplugin 'fubar'" + bundle "install" + + update_repo2 do + build_plugin "fubar", "1.1" do |s| + s.write "plugins.rb", <<-RUBY + class Fubar < Bundler::Plugin::API + command "shout" + + def exec(command, args) + puts "global_one" + end + end + RUBY + end + end + + # outside the app + Dir.chdir tmp + bundle "plugin install fubar --source file://#{gem_repo2}" + end + + it "inside the app takes precedence over global plugin" do + Dir.chdir bundled_app + + bundle "shout" + expect(out).to eq("local_one") + end + + it "outside the app global plugin is used" do + Dir.chdir tmp + + bundle "shout" + expect(out).to eq("global_one") + end + end + end +end diff --git a/spec/bundler/plugins/source/example_spec.rb b/spec/bundler/plugins/source/example_spec.rb new file mode 100644 index 0000000000..2ae34caf73 --- /dev/null +++ b/spec/bundler/plugins/source/example_spec.rb @@ -0,0 +1,446 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "real source plugins" do + context "with a minimal source plugin" do + before do + build_repo2 do + build_plugin "bundler-source-mpath" do |s| + s.write "plugins.rb", <<-RUBY + require "fileutils" + require "bundler-source-mpath" + + class MPath < Bundler::Plugin::API + source "mpath" + + attr_reader :path + + def initialize(opts) + super + + @path = Pathname.new options["uri"] + end + + def fetch_gemspec_files + @spec_files ||= begin + glob = "{,*,*/*}.gemspec" + if installed? + search_path = install_path + else + search_path = path + end + Dir["\#{search_path.to_s}/\#{glob}"] + end + end + + def install(spec, opts) + mkdir_p(install_path.parent) + FileUtils.cp_r(path, install_path) + + post_install(spec) + + nil + end + end + RUBY + end # build_plugin + end + + build_lib "a-path-gem" + + gemfile <<-G + source "file://#{gem_repo2}" # plugin source + source "#{lib_path("a-path-gem-1.0")}", :type => :mpath do + gem "a-path-gem" + end + G + end + + it "installs" do + bundle "install" + + expect(out).to include("Bundle complete!") + + expect(the_bundle).to include_gems("a-path-gem 1.0") + end + + it "writes to lock file" do + bundle "install" + + lockfile_should_be <<-G + PLUGIN SOURCE + remote: #{lib_path("a-path-gem-1.0")} + type: mpath + specs: + a-path-gem (1.0) + + GEM + remote: file:#{gem_repo2}/ + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + a-path-gem! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "provides correct #full_gem_path" do + bundle "install" + run <<-RUBY + puts Bundler.rubygems.find_name('a-path-gem').first.full_gem_path + RUBY + expect(out).to eq(bundle("show a-path-gem")) + end + + it "installs the gem executables" do + build_lib "gem-with-bin" do |s| + s.executables = ["foo"] + end + + install_gemfile <<-G + source "file://#{gem_repo2}" # plugin source + source "#{lib_path("gem-with-bin-1.0")}", :type => :mpath do + gem "gem-with-bin" + end + G + + bundle "exec foo" + expect(out).to eq("1.0") + end + + describe "bundle cache/package" do + let(:uri_hash) { Digest::SHA1.hexdigest(lib_path("a-path-gem-1.0").to_s) } + it "copies repository to vendor cache and uses it" do + bundle "install" + bundle "cache --all" + + expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist + expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}/.git")).not_to exist + expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}/.bundlecache")).to be_file + + FileUtils.rm_rf lib_path("a-path-gem-1.0") + expect(the_bundle).to include_gems("a-path-gem 1.0") + end + + it "copies repository to vendor cache and uses it even when installed with bundle --path" do + bundle "install --path vendor/bundle" + bundle "cache --all" + + expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist + + FileUtils.rm_rf lib_path("a-path-gem-1.0") + expect(the_bundle).to include_gems("a-path-gem 1.0") + end + + it "bundler package copies repository to vendor cache" do + bundle "install --path vendor/bundle" + bundle "package --all" + + expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist + + FileUtils.rm_rf lib_path("a-path-gem-1.0") + expect(the_bundle).to include_gems("a-path-gem 1.0") + end + end + + context "with lockfile" do + before do + lockfile <<-G + PLUGIN SOURCE + remote: #{lib_path("a-path-gem-1.0")} + type: mpath + specs: + a-path-gem (1.0) + + GEM + remote: file:#{gem_repo2}/ + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + a-path-gem! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "installs" do + bundle "install" + + expect(the_bundle).to include_gems("a-path-gem 1.0") + end + end + end + + context "with a more elaborate source plugin" do + before do + build_repo2 do + build_plugin "bundler-source-gitp" do |s| + s.write "plugins.rb", <<-RUBY + class SPlugin < Bundler::Plugin::API + source "gitp" + + attr_reader :ref + + def initialize(opts) + super + + @ref = options["ref"] || options["branch"] || options["tag"] || "master" + @unlocked = false + end + + def eql?(other) + other.is_a?(self.class) && uri == other.uri && ref == other.ref + end + + alias_method :==, :eql? + + def fetch_gemspec_files + @spec_files ||= begin + glob = "{,*,*/*}.gemspec" + if !cached? + cache_repo + end + + if installed? && !@unlocked + path = install_path + else + path = cache_path + end + + Dir["\#{path}/\#{glob}"] + end + end + + def install(spec, opts) + mkdir_p(install_path.dirname) + rm_rf(install_path) + `git clone --no-checkout --quiet "\#{cache_path}" "\#{install_path}"` + Dir.chdir install_path do + `git reset --hard \#{revision}` + end + + post_install(spec) + + nil + end + + def options_to_lock + opts = {"revision" => revision} + opts["ref"] = ref if ref != "master" + opts + end + + def unlock! + @unlocked = true + @revision = latest_revision + end + + def app_cache_dirname + "\#{base_name}-\#{shortref_for_path(revision)}" + end + + private + + def cache_path + @cache_path ||= cache_dir.join("gitp", base_name) + end + + def cache_repo + `git clone --quiet \#{@options["uri"]} \#{cache_path}` + end + + def cached? + File.directory?(cache_path) + end + + def locked_revision + options["revision"] + end + + def revision + @revision ||= locked_revision || latest_revision + end + + def latest_revision + if !cached? || @unlocked + rm_rf(cache_path) + cache_repo + end + + Dir.chdir cache_path do + `git rev-parse --verify \#{@ref}`.strip + end + end + + def base_name + File.basename(uri.sub(%r{^(\w+://)?([^/:]+:)?(//\w*/)?(\w*/)*}, ""), ".git") + end + + def shortref_for_path(ref) + ref[0..11] + end + + def install_path + @install_path ||= begin + git_scope = "\#{base_name}-\#{shortref_for_path(revision)}" + + path = gem_install_dir.join(git_scope) + + if !path.exist? && requires_sudo? + user_bundle_path.join(ruby_scope).join(git_scope) + else + path + end + end + end + + def installed? + File.directory?(install_path) + end + end + RUBY + end + end + + build_git "ma-gitp-gem" + + gemfile <<-G + source "file://#{gem_repo2}" # plugin source + source "file://#{lib_path("ma-gitp-gem-1.0")}", :type => :gitp do + gem "ma-gitp-gem" + end + G + end + + it "handles the source option" do + bundle "install" + expect(out).to include("Bundle complete!") + expect(the_bundle).to include_gems("ma-gitp-gem 1.0") + end + + it "writes to lock file" do + revision = revision_for(lib_path("ma-gitp-gem-1.0")) + bundle "install" + + lockfile_should_be <<-G + PLUGIN SOURCE + remote: file://#{lib_path("ma-gitp-gem-1.0")} + type: gitp + revision: #{revision} + specs: + ma-gitp-gem (1.0) + + GEM + remote: file:#{gem_repo2}/ + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + ma-gitp-gem! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + context "with lockfile" do + before do + revision = revision_for(lib_path("ma-gitp-gem-1.0")) + lockfile <<-G + PLUGIN SOURCE + remote: file://#{lib_path("ma-gitp-gem-1.0")} + type: gitp + revision: #{revision} + specs: + ma-gitp-gem (1.0) + + GEM + remote: file:#{gem_repo2}/ + specs: + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + ma-gitp-gem! + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "installs" do + bundle "install" + expect(the_bundle).to include_gems("ma-gitp-gem 1.0") + end + + it "uses the locked ref" do + update_git "ma-gitp-gem" + bundle "install" + + run <<-RUBY + require 'ma-gitp-gem' + puts "WIN" unless defined?(MAGITPGEM_PREV_REF) + RUBY + expect(out).to eq("WIN") + end + + it "updates the deps on bundler update" do + update_git "ma-gitp-gem" + bundle "update ma-gitp-gem" + + run <<-RUBY + require 'ma-gitp-gem' + puts "WIN" if defined?(MAGITPGEM_PREV_REF) + RUBY + expect(out).to eq("WIN") + end + + it "updates the deps on change in gemfile" do + update_git "ma-gitp-gem", "1.1", :path => lib_path("ma-gitp-gem-1.0"), :gemspec => true + gemfile <<-G + source "file://#{gem_repo2}" # plugin source + source "file://#{lib_path("ma-gitp-gem-1.0")}", :type => :gitp do + gem "ma-gitp-gem", "1.1" + end + G + bundle "install" + + expect(the_bundle).to include_gems("ma-gitp-gem 1.1") + end + end + + describe "bundle cache with gitp" do + it "copies repository to vendor cache and uses it" do + git = build_git "foo" + ref = git.ref_for("master", 11) + + install_gemfile <<-G + source "file://#{gem_repo2}" # plugin source + source '#{lib_path("foo-1.0")}', :type => :gitp do + gem "foo" + end + G + + bundle "cache --all" + expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist + expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.git")).not_to exist + expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.bundlecache")).to be_file + + FileUtils.rm_rf lib_path("foo-1.0") + expect(the_bundle).to include_gems "foo 1.0" + end + end + end +end diff --git a/spec/bundler/plugins/source_spec.rb b/spec/bundler/plugins/source_spec.rb new file mode 100644 index 0000000000..0448bc409a --- /dev/null +++ b/spec/bundler/plugins/source_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundler source plugin" do + describe "plugins dsl eval for #source with :type option" do + before do + update_repo2 do + build_plugin "bundler-source-psource" do |s| + s.write "plugins.rb", <<-RUBY + class OPSource < Bundler::Plugin::API + source "psource" + end + RUBY + end + end + end + + it "installs bundler-source-* gem when no handler for source is present" do + install_gemfile <<-G + source "file://#{gem_repo2}" + source "file://#{lib_path("gitp")}", :type => :psource do + end + G + + plugin_should_be_installed("bundler-source-psource") + end + + it "enables the plugin to require a lib path" do + update_repo2 do + build_plugin "bundler-source-psource" do |s| + s.write "plugins.rb", <<-RUBY + require "bundler-source-psource" + class PSource < Bundler::Plugin::API + source "psource" + end + RUBY + end + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + source "file://#{lib_path("gitp")}", :type => :psource do + end + G + + expect(out).to include("Bundle complete!") + end + + context "with an explicit handler" do + before do + update_repo2 do + build_plugin "another-psource" do |s| + s.write "plugins.rb", <<-RUBY + class Cheater < Bundler::Plugin::API + source "psource" + end + RUBY + end + end + end + + context "explicit presence in gemfile" do + before do + install_gemfile <<-G + source "file://#{gem_repo2}" + + plugin "another-psource" + + source "file://#{lib_path("gitp")}", :type => :psource do + end + G + end + + it "completes successfully" do + expect(out).to include("Bundle complete!") + end + + it "installs the explicit one" do + plugin_should_be_installed("another-psource") + end + + it "doesn't install the default one" do + plugin_should_not_be_installed("bundler-source-psource") + end + end + + context "explicit default source" do + before do + install_gemfile <<-G + source "file://#{gem_repo2}" + + plugin "bundler-source-psource" + + source "file://#{lib_path("gitp")}", :type => :psource do + end + G + end + + it "completes successfully" do + expect(out).to include("Bundle complete!") + end + + it "installs the default one" do + plugin_should_be_installed("bundler-source-psource") + end + end + end + end +end diff --git a/spec/bundler/quality_spec.rb b/spec/bundler/quality_spec.rb new file mode 100644 index 0000000000..b87b4a0731 --- /dev/null +++ b/spec/bundler/quality_spec.rb @@ -0,0 +1,263 @@ +# frozen_string_literal: true +require "spec_helper" + +if defined?(Encoding) && Encoding.default_external.name != "UTF-8" + # Poor man's ruby -E UTF-8, since it works on 1.8.7 + Encoding.default_external = Encoding.find("UTF-8") +end + +RSpec.describe "The library itself" do + def check_for_spec_defs_with_single_quotes(filename) + failing_lines = [] + + File.readlines(filename).each_with_index do |line, number| + failing_lines << number + 1 if line =~ /^ *(describe|it|context) {1}'{1}/ + end + + return if failing_lines.empty? + "#{filename} uses inconsistent single quotes on lines #{failing_lines.join(", ")}" + end + + def check_for_debugging_mechanisms(filename) + debugging_mechanisms_regex = / + (binding\.pry)| + (debugger)| + (sleep\s*\(?\d+)| + (fit\s*\(?("|\w)) + /x + + failing_lines = [] + File.readlines(filename).each_with_index do |line, number| + if line =~ debugging_mechanisms_regex && !line.end_with?("# ignore quality_spec\n") + failing_lines << number + 1 + end + end + + return if failing_lines.empty? + "#{filename} has debugging mechanisms (like binding.pry, sleep, debugger, rspec focusing, etc.) on lines #{failing_lines.join(", ")}" + end + + def check_for_git_merge_conflicts(filename) + merge_conflicts_regex = / + <<<<<<<| + =======| + >>>>>>> + /x + + failing_lines = [] + File.readlines(filename).each_with_index do |line, number| + failing_lines << number + 1 if line =~ merge_conflicts_regex + end + + return if failing_lines.empty? + "#{filename} has unresolved git merge conflicts on lines #{failing_lines.join(", ")}" + end + + def check_for_tab_characters(filename) + failing_lines = [] + File.readlines(filename).each_with_index do |line, number| + failing_lines << number + 1 if line =~ /\t/ + end + + return if failing_lines.empty? + "#{filename} has tab characters on lines #{failing_lines.join(", ")}" + end + + def check_for_extra_spaces(filename) + failing_lines = [] + File.readlines(filename).each_with_index do |line, number| + next if line =~ /^\s+#.*\s+\n$/ + next if %w(LICENCE.md).include?(line) + failing_lines << number + 1 if line =~ /\s+\n$/ + end + + return if failing_lines.empty? + "#{filename} has spaces on the EOL on lines #{failing_lines.join(", ")}" + end + + def check_for_expendable_words(filename) + failing_line_message = [] + useless_words = %w( + actually + basically + clearly + just + obviously + really + simply + ) + pattern = /\b#{Regexp.union(useless_words)}\b/i + + File.readlines(filename).each_with_index do |line, number| + next unless word_found = pattern.match(line) + failing_line_message << "#{filename} has '#{word_found}' on line #{number + 1}. Avoid using these kinds of weak modifiers." + end + + failing_line_message unless failing_line_message.empty? + end + + def check_for_specific_pronouns(filename) + failing_line_message = [] + specific_pronouns = /\b(he|she|his|hers|him|her|himself|herself)\b/i + + File.readlines(filename).each_with_index do |line, number| + next unless word_found = specific_pronouns.match(line) + failing_line_message << "#{filename} has '#{word_found}' on line #{number + 1}. Use more generic pronouns in documentation." + end + + failing_line_message unless failing_line_message.empty? + end + + RSpec::Matchers.define :be_well_formed do + match(&:empty?) + + failure_message do |actual| + actual.join("\n") + end + end + + it "has no malformed whitespace", :ruby_repo do + exempt = /\.gitmodules|\.marshal|fixtures|vendor|ssl_certs|LICENSE/ + error_messages = [] + Dir.chdir(File.expand_path("../..", __FILE__)) do + `git ls-files -z`.split("\x0").each do |filename| + next if filename =~ exempt + error_messages << check_for_tab_characters(filename) + error_messages << check_for_extra_spaces(filename) + end + end + expect(error_messages.compact).to be_well_formed + end + + it "uses double-quotes consistently in specs", :ruby_repo do + included = /spec/ + error_messages = [] + Dir.chdir(File.expand_path("../", __FILE__)) do + `git ls-files -z`.split("\x0").each do |filename| + next unless filename =~ included + error_messages << check_for_spec_defs_with_single_quotes(filename) + end + end + expect(error_messages.compact).to be_well_formed + end + + it "does not include any leftover debugging or development mechanisms", :ruby_repo do + exempt = %r{quality_spec.rb|support/helpers} + error_messages = [] + Dir.chdir(File.expand_path("../", __FILE__)) do + `git ls-files -z`.split("\x0").each do |filename| + next if filename =~ exempt + error_messages << check_for_debugging_mechanisms(filename) + end + end + expect(error_messages.compact).to be_well_formed + end + + it "does not include any unresolved merge conflicts", :ruby_repo do + error_messages = [] + exempt = %r{lock/lockfile_spec|quality_spec} + Dir.chdir(File.expand_path("../", __FILE__)) do + `git ls-files -z`.split("\x0").each do |filename| + next if filename =~ exempt + error_messages << check_for_git_merge_conflicts(filename) + end + end + expect(error_messages.compact).to be_well_formed + end + + it "maintains language quality of the documentation", :ruby_repo do + included = /ronn/ + error_messages = [] + Dir.chdir(File.expand_path("../../man", __FILE__)) do + `git ls-files -z`.split("\x0").each do |filename| + next unless filename =~ included + error_messages << check_for_expendable_words(filename) + error_messages << check_for_specific_pronouns(filename) + end + end + expect(error_messages.compact).to be_well_formed + end + + it "maintains language quality of sentences used in source code", :ruby_repo do + error_messages = [] + exempt = /vendor/ + Dir.chdir(File.expand_path("../../lib", __FILE__)) do + `git ls-files -z`.split("\x0").each do |filename| + next if filename =~ exempt + error_messages << check_for_expendable_words(filename) + error_messages << check_for_specific_pronouns(filename) + end + end + expect(error_messages.compact).to be_well_formed + end + + it "documents all used settings", :ruby_repo do + exemptions = %w( + gem.coc + gem.mit + inline + warned_version + ) + + all_settings = Hash.new {|h, k| h[k] = [] } + documented_settings = exemptions + + Bundler::Settings::BOOL_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::BOOL_KEYS" } + Bundler::Settings::NUMBER_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::NUMBER_KEYS" } + + Dir.chdir(File.expand_path("../../lib", __FILE__)) do + key_pattern = /([a-z\._-]+)/i + `git ls-files -z`.split("\x0").each do |filename| + File.readlines(filename).each_with_index do |line, number| + line.scan(/Bundler\.settings\[:#{key_pattern}\]/).flatten.each {|s| all_settings[s] << "referenced at `lib/#{filename}:#{number.succ}`" } + end + end + documented_settings = File.read("../man/bundle-config.ronn").scan(/^\* `#{key_pattern}`/).flatten + end + + documented_settings.each {|s| all_settings.delete(s) } + exemptions.each {|s| all_settings.delete(s) } + error_messages = all_settings.map do |setting, refs| + "The `#{setting}` setting is undocumented\n\t- #{refs.join("\n\t- ")}\n" + end + + expect(error_messages.sort).to be_well_formed + end + + it "can still be built", :ruby_repo do + Dir.chdir(root) do + begin + gem_command! :build, "bundler.gemspec" + if Bundler.rubygems.provides?(">= 2.4") + # older rubygems have weird warnings, and we won't actually be using them + # to build the gem for releases anyways + expect(err).to be_empty, "bundler should build as a gem without warnings, but\n#{err}" + end + ensure + # clean up the .gem generated + FileUtils.rm("bundler-#{Bundler::VERSION}.gem") + end + end + end + + it "does not contain any warnings", :ruby_repo do + Dir.chdir(root.join("lib")) do + exclusions = %w( + bundler/capistrano.rb + bundler/gem_tasks.rb + bundler/vlad.rb + ) + lib_files = `git ls-files -z`.split("\x0").grep(/\.rb$/) - exclusions + lib_files.reject! {|f| f.start_with?("bundler/vendor") } + lib_files.map! {|f| f.chomp(".rb") } + sys_exec!("ruby -w -I.") do |input, _, _| + lib_files.each do |f| + input.puts "require '#{f}'" + end + end + + expect(@err.split("\n")).to be_well_formed + expect(@out.split("\n")).to be_well_formed + end + end +end diff --git a/spec/bundler/realworld/dependency_api_spec.rb b/spec/bundler/realworld/dependency_api_spec.rb new file mode 100644 index 0000000000..468fa3644c --- /dev/null +++ b/spec/bundler/realworld/dependency_api_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "gemcutter's dependency API", :realworld => true do + context "when Gemcutter API takes too long to respond" do + before do + require_rack + + port = find_unused_port + @server_uri = "http://127.0.0.1:#{port}" + + require File.expand_path("../../support/artifice/endpoint_timeout", __FILE__) + require "thread" + @t = Thread.new do + server = Rack::Server.start(:app => EndpointTimeout, + :Host => "0.0.0.0", + :Port => port, + :server => "webrick", + :AccessLog => [], + :Logger => Spec::SilentLogger.new) + server.start + end + @t.run + + wait_for_server("127.0.0.1", port) + end + + after do + Artifice.deactivate + @t.kill + @t.join + end + + it "times out and falls back on the modern index" do + gemfile <<-G + source "#{@server_uri}" + gem "rack" + + old_v, $VERBOSE = $VERBOSE, nil + Bundler::Fetcher.api_timeout = 1 + $VERBOSE = old_v + G + + bundle :install + expect(out).to include("Fetching source index from #{@server_uri}/") + expect(the_bundle).to include_gems "rack 1.0.0" + end + end +end diff --git a/spec/bundler/realworld/edgecases_spec.rb b/spec/bundler/realworld/edgecases_spec.rb new file mode 100644 index 0000000000..302fd57cf0 --- /dev/null +++ b/spec/bundler/realworld/edgecases_spec.rb @@ -0,0 +1,382 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "real world edgecases", :realworld => true, :sometimes => true do + def rubygems_version(name, requirement) + require "bundler/source/rubygems/remote" + require "bundler/fetcher" + source = Bundler::Source::Rubygems::Remote.new(URI("https://rubygems.org")) + fetcher = Bundler::Fetcher.new(source) + index = fetcher.specs([name], nil) + rubygem = index.search(Gem::Dependency.new(name, requirement)).last + if rubygem.nil? + raise "Could not find #{name} (#{requirement}) on rubygems.org!\n" \ + "Found specs:\n#{index.send(:specs).inspect}" + end + "#{name} (#{rubygem.version})" + end + + # there is no rbx-relative-require gem that will install on 1.9 + it "ignores extra gems with bad platforms", :ruby => "~> 1.8.7" do + gemfile <<-G + source "https://rubygems.org" + gem "linecache", "0.46" + G + bundle :lock + expect(err).to lack_errors + expect(exitstatus).to eq(0) if exitstatus + end + + # https://github.com/bundler/bundler/issues/1202 + it "bundle cache works with rubygems 1.3.7 and pre gems", + :ruby => "~> 1.8.7", :rubygems => "~> 1.3.7" do + install_gemfile <<-G + source "https://rubygems.org" + gem "rack", "1.3.0.beta2" + gem "will_paginate", "3.0.pre2" + G + bundle :cache + expect(out).not_to include("Removing outdated .gem files from vendor/cache") + end + + # https://github.com/bundler/bundler/issues/1486 + # this is a hash collision that only manifests on 1.8.7 + it "finds the correct child versions", :ruby => "~> 1.8.7" do + gemfile <<-G + source "https://rubygems.org" + + gem 'i18n', '~> 0.6.0' + gem 'activesupport', '~> 3.0.5' + gem 'activerecord', '~> 3.0.5' + gem 'builder', '~> 2.1.2' + G + bundle :lock + expect(lockfile).to include("activemodel (3.0.5)") + end + + it "resolves dependencies correctly", :ruby => "1.9.3" do + gemfile <<-G + source "https://rubygems.org" + + gem 'rails', '~> 3.0' + gem 'capybara', '~> 2.2.0' + gem 'rack-cache', '1.2.0' # last version that works on Ruby 1.9 + G + bundle! :lock + expect(lockfile).to include(rubygems_version("rails", "~> 3.0")) + expect(lockfile).to include("capybara (2.2.1)") + end + + it "installs the latest version of gxapi_rails", :ruby => "1.9.3" do + gemfile <<-G + source "https://rubygems.org" + + gem "sass-rails" + gem "rails", "~> 3" + gem "gxapi_rails", "< 0.1.0" # 0.1.0 was released way after the test was written + gem 'rack-cache', '1.2.0' # last version that works on Ruby 1.9 + G + bundle :lock + expect(lockfile).to include("gxapi_rails (0.0.6)") + end + + it "installs the latest version of i18n" do + gemfile <<-G + source "https://rubygems.org" + + gem "i18n", "~> 0.6.0" + gem "activesupport", "~> 3.0" + gem "activerecord", "~> 3.0" + gem "builder", "~> 2.1.2" + G + bundle :lock + expect(lockfile).to include(rubygems_version("i18n", "~> 0.6.0")) + expect(lockfile).to include(rubygems_version("activesupport", "~> 3.0")) + end + + it "is able to update a top-level dependency when there is a conflict on a shared transitive child", :ruby => "2.1" do + # from https://github.com/bundler/bundler/issues/5031 + + gemfile <<-G + source "https://rubygems.org" + gem 'rails', '~> 4.2.7.1' + gem 'paperclip', '~> 5.1.0' + G + + lockfile <<-L + GEM + remote: https://rubygems.org/ + specs: + actionmailer (4.2.7.1) + actionpack (= 4.2.7.1) + actionview (= 4.2.7.1) + activejob (= 4.2.7.1) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 1.0, >= 1.0.5) + actionpack (4.2.7.1) + actionview (= 4.2.7.1) + activesupport (= 4.2.7.1) + rack (~> 1.6) + rack-test (~> 0.6.2) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (4.2.7.1) + activesupport (= 4.2.7.1) + builder (~> 3.1) + erubis (~> 2.7.0) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + activejob (4.2.7.1) + activesupport (= 4.2.7.1) + globalid (>= 0.3.0) + activemodel (4.2.7.1) + activesupport (= 4.2.7.1) + builder (~> 3.1) + activerecord (4.2.7.1) + activemodel (= 4.2.7.1) + activesupport (= 4.2.7.1) + arel (~> 6.0) + activesupport (4.2.7.1) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + arel (6.0.3) + builder (3.2.2) + climate_control (0.0.3) + activesupport (>= 3.0) + cocaine (0.5.8) + climate_control (>= 0.0.3, < 1.0) + concurrent-ruby (1.0.2) + erubis (2.7.0) + globalid (0.3.7) + activesupport (>= 4.1.0) + i18n (0.7.0) + json (1.8.3) + loofah (2.0.3) + nokogiri (>= 1.5.9) + mail (2.6.4) + mime-types (>= 1.16, < 4) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + mimemagic (0.3.2) + mini_portile2 (2.1.0) + minitest (5.9.1) + nokogiri (1.6.8) + mini_portile2 (~> 2.1.0) + pkg-config (~> 1.1.7) + paperclip (5.1.0) + activemodel (>= 4.2.0) + activesupport (>= 4.2.0) + cocaine (~> 0.5.5) + mime-types + mimemagic (~> 0.3.0) + pkg-config (1.1.7) + rack (1.6.4) + rack-test (0.6.3) + rack (>= 1.0) + rails (4.2.7.1) + actionmailer (= 4.2.7.1) + actionpack (= 4.2.7.1) + actionview (= 4.2.7.1) + activejob (= 4.2.7.1) + activemodel (= 4.2.7.1) + activerecord (= 4.2.7.1) + activesupport (= 4.2.7.1) + bundler (>= 1.3.0, < 2.0) + railties (= 4.2.7.1) + sprockets-rails + rails-deprecated_sanitizer (1.0.3) + activesupport (>= 4.2.0.alpha) + rails-dom-testing (1.0.7) + activesupport (>= 4.2.0.beta, < 5.0) + nokogiri (~> 1.6.0) + rails-deprecated_sanitizer (>= 1.0.1) + rails-html-sanitizer (1.0.3) + loofah (~> 2.0) + railties (4.2.7.1) + actionpack (= 4.2.7.1) + activesupport (= 4.2.7.1) + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + rake (11.3.0) + sprockets (3.7.0) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.0) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + thor (0.19.1) + thread_safe (0.3.5) + tzinfo (1.2.2) + thread_safe (~> 0.1) + + PLATFORMS + ruby + + DEPENDENCIES + paperclip (~> 5.1.0) + rails (~> 4.2.7.1) + + BUNDLED WITH + 1.13.1 + L + + bundle! "lock --update paperclip" + + expect(lockfile).to include(rubygems_version("paperclip", "~> 5.1.0")) + end + + # https://github.com/bundler/bundler/issues/1500 + it "does not fail install because of gem plugins" do + realworld_system_gems("open_gem --version 1.4.2", "rake --version 0.9.2") + gemfile <<-G + source "https://rubygems.org" + + gem 'rack', '1.0.1' + G + + bundle "install --path vendor/bundle" + expect(err).not_to include("Could not find rake") + expect(err).to lack_errors + end + + it "checks out git repos when the lockfile is corrupted" do + gemfile <<-G + source "https://rubygems.org" + + gem 'activerecord', :github => 'carlhuda/rails-bundler-test', :branch => 'master' + gem 'activesupport', :github => 'carlhuda/rails-bundler-test', :branch => 'master' + gem 'actionpack', :github => 'carlhuda/rails-bundler-test', :branch => 'master' + G + + lockfile <<-L + GIT + remote: git://github.com/carlhuda/rails-bundler-test.git + revision: 369e28a87419565f1940815219ea9200474589d4 + branch: master + specs: + actionpack (3.2.2) + activemodel (= 3.2.2) + activesupport (= 3.2.2) + builder (~> 3.0.0) + erubis (~> 2.7.0) + journey (~> 1.0.1) + rack (~> 1.4.0) + rack-cache (~> 1.2) + rack-test (~> 0.6.1) + sprockets (~> 2.1.2) + activemodel (3.2.2) + activesupport (= 3.2.2) + builder (~> 3.0.0) + activerecord (3.2.2) + activemodel (= 3.2.2) + activesupport (= 3.2.2) + arel (~> 3.0.2) + tzinfo (~> 0.3.29) + activesupport (3.2.2) + i18n (~> 0.6) + multi_json (~> 1.0) + + GIT + remote: git://github.com/carlhuda/rails-bundler-test.git + revision: 369e28a87419565f1940815219ea9200474589d4 + branch: master + specs: + actionpack (3.2.2) + activemodel (= 3.2.2) + activesupport (= 3.2.2) + builder (~> 3.0.0) + erubis (~> 2.7.0) + journey (~> 1.0.1) + rack (~> 1.4.0) + rack-cache (~> 1.2) + rack-test (~> 0.6.1) + sprockets (~> 2.1.2) + activemodel (3.2.2) + activesupport (= 3.2.2) + builder (~> 3.0.0) + activerecord (3.2.2) + activemodel (= 3.2.2) + activesupport (= 3.2.2) + arel (~> 3.0.2) + tzinfo (~> 0.3.29) + activesupport (3.2.2) + i18n (~> 0.6) + multi_json (~> 1.0) + + GIT + remote: git://github.com/carlhuda/rails-bundler-test.git + revision: 369e28a87419565f1940815219ea9200474589d4 + branch: master + specs: + actionpack (3.2.2) + activemodel (= 3.2.2) + activesupport (= 3.2.2) + builder (~> 3.0.0) + erubis (~> 2.7.0) + journey (~> 1.0.1) + rack (~> 1.4.0) + rack-cache (~> 1.2) + rack-test (~> 0.6.1) + sprockets (~> 2.1.2) + activemodel (3.2.2) + activesupport (= 3.2.2) + builder (~> 3.0.0) + activerecord (3.2.2) + activemodel (= 3.2.2) + activesupport (= 3.2.2) + arel (~> 3.0.2) + tzinfo (~> 0.3.29) + activesupport (3.2.2) + i18n (~> 0.6) + multi_json (~> 1.0) + + GEM + remote: https://rubygems.org/ + specs: + arel (3.0.2) + builder (3.0.0) + erubis (2.7.0) + hike (1.2.1) + i18n (0.6.0) + journey (1.0.3) + multi_json (1.1.0) + rack (1.4.1) + rack-cache (1.2) + rack (>= 0.4) + rack-test (0.6.1) + rack (>= 1.0) + sprockets (2.1.2) + hike (~> 1.2) + rack (~> 1.0) + tilt (~> 1.1, != 1.3.0) + tilt (1.3.3) + tzinfo (0.3.32) + + PLATFORMS + ruby + + DEPENDENCIES + actionpack! + activerecord! + activesupport! + L + + bundle :lock + expect(err).to eq("") + expect(exitstatus).to eq(0) if exitstatus + end + + it "outputs a helpful error message when gems have invalid gemspecs" do + install_gemfile <<-G, :standalone => true + source 'https://rubygems.org' + gem "resque-scheduler", "2.2.0" + G + expect(out).to include("You have one or more invalid gemspecs that need to be fixed.") + expect(out).to include("resque-scheduler 2.2.0 has an invalid gemspec") + end +end diff --git a/spec/bundler/realworld/gemfile_source_header_spec.rb b/spec/bundler/realworld/gemfile_source_header_spec.rb new file mode 100644 index 0000000000..ba888d43bd --- /dev/null +++ b/spec/bundler/realworld/gemfile_source_header_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true +require "spec_helper" +require "thread" + +RSpec.describe "fetching dependencies with a mirrored source", :realworld => true, :rubygems => ">= 2.0" do + let(:mirror) { "https://server.example.org" } + let(:original) { "http://127.0.0.1:#{@port}" } + + before do + setup_server + bundle "config --local mirror.#{mirror} #{original}" + end + + after do + Artifice.deactivate + @t.kill + @t.join + end + + it "sets the 'X-Gemfile-Source' header and bundles successfully" do + gemfile <<-G + source "#{mirror}" + gem 'weakling' + G + + bundle :install + + expect(out).to include("Installing weakling") + expect(out).to include("Bundle complete") + expect(the_bundle).to include_gems "weakling 0.0.3" + end + + private + + def setup_server + require_rack + @port = find_unused_port + @server_uri = "http://127.0.0.1:#{@port}" + + require File.expand_path("../../support/artifice/endpoint_mirror_source", __FILE__) + + @t = Thread.new do + Rack::Server.start(:app => EndpointMirrorSource, + :Host => "0.0.0.0", + :Port => @port, + :server => "webrick", + :AccessLog => [], + :Logger => Spec::SilentLogger.new) + end.run + + wait_for_server("127.0.0.1", @port) + end +end diff --git a/spec/bundler/realworld/mirror_probe_spec.rb b/spec/bundler/realworld/mirror_probe_spec.rb new file mode 100644 index 0000000000..93dca0c173 --- /dev/null +++ b/spec/bundler/realworld/mirror_probe_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true +require "spec_helper" +require "thread" + +RSpec.describe "fetching dependencies with a not available mirror", :realworld => true do + let(:mirror) { @mirror_uri } + let(:original) { @server_uri } + let(:server_port) { @server_port } + let(:host) { "127.0.0.1" } + + before do + require_rack + setup_server + setup_mirror + end + + after do + Artifice.deactivate + @server_thread.kill + @server_thread.join + end + + context "with a specific fallback timeout" do + before do + global_config("BUNDLE_MIRROR__HTTP://127__0__0__1:#{server_port}/__FALLBACK_TIMEOUT/" => "true", + "BUNDLE_MIRROR__HTTP://127__0__0__1:#{server_port}/" => mirror) + end + + it "install a gem using the original uri when the mirror is not responding" do + gemfile <<-G + source "#{original}" + gem 'weakling' + G + + bundle :install + + expect(out).to include("Installing weakling") + expect(out).to include("Bundle complete") + expect(the_bundle).to include_gems "weakling 0.0.3" + end + end + + context "with a global fallback timeout" do + before do + global_config("BUNDLE_MIRROR__ALL__FALLBACK_TIMEOUT/" => "1", + "BUNDLE_MIRROR__ALL" => mirror) + end + + it "install a gem using the original uri when the mirror is not responding" do + gemfile <<-G + source "#{original}" + gem 'weakling' + G + + bundle :install + + expect(out).to include("Installing weakling") + expect(out).to include("Bundle complete") + expect(the_bundle).to include_gems "weakling 0.0.3" + end + end + + context "with a specific mirror without a fallback timeout" do + before do + global_config("BUNDLE_MIRROR__HTTP://127__0__0__1:#{server_port}/" => mirror) + end + + it "fails to install the gem with a timeout error" do + gemfile <<-G + source "#{original}" + gem 'weakling' + G + + bundle :install + + expect(out).to include("Fetching source index from #{mirror}") + expect(out).to include("Retrying fetcher due to error (2/4): Bundler::HTTPError Could not fetch specs from #{mirror}") + expect(out).to include("Retrying fetcher due to error (3/4): Bundler::HTTPError Could not fetch specs from #{mirror}") + expect(out).to include("Retrying fetcher due to error (4/4): Bundler::HTTPError Could not fetch specs from #{mirror}") + expect(out).to include("Could not fetch specs from #{mirror}") + end + + it "prints each error and warning on a new line" do + gemfile <<-G + source "#{original}" + gem 'weakling' + G + + bundle :install + + expect(out).to eq "Fetching source index from #{mirror}/ + +Retrying fetcher due to error (2/4): Bundler::HTTPError Could not fetch specs from #{mirror}/ +Retrying fetcher due to error (3/4): Bundler::HTTPError Could not fetch specs from #{mirror}/ +Retrying fetcher due to error (4/4): Bundler::HTTPError Could not fetch specs from #{mirror}/ +Could not fetch specs from #{mirror}/" + end + end + + context "with a global mirror without a fallback timeout" do + before do + global_config("BUNDLE_MIRROR__ALL" => mirror) + end + + it "fails to install the gem with a timeout error" do + gemfile <<-G + source "#{original}" + gem 'weakling' + G + + bundle :install + + expect(out).to include("Fetching source index from #{mirror}") + expect(out).to include("Retrying fetcher due to error (2/4): Bundler::HTTPError Could not fetch specs from #{mirror}") + expect(out).to include("Retrying fetcher due to error (3/4): Bundler::HTTPError Could not fetch specs from #{mirror}") + expect(out).to include("Retrying fetcher due to error (4/4): Bundler::HTTPError Could not fetch specs from #{mirror}") + expect(out).to include("Could not fetch specs from #{mirror}") + end + end + + def setup_server + @server_port = find_unused_port + @server_uri = "http://#{host}:#{@server_port}" + + require File.expand_path("../../support/artifice/endpoint", __FILE__) + + @server_thread = Thread.new do + Rack::Server.start(:app => Endpoint, + :Host => host, + :Port => @server_port, + :server => "webrick", + :AccessLog => [], + :Logger => Spec::SilentLogger.new) + end.run + + wait_for_server(host, @server_port) + end + + def setup_mirror + mirror_port = find_unused_port + @mirror_uri = "http://#{host}:#{mirror_port}" + end +end diff --git a/spec/bundler/realworld/parallel_spec.rb b/spec/bundler/realworld/parallel_spec.rb new file mode 100644 index 0000000000..6950bead19 --- /dev/null +++ b/spec/bundler/realworld/parallel_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "parallel", :realworld => true, :sometimes => true do + it "installs" do + gemfile <<-G + source "https://rubygems.org" + gem 'activesupport', '~> 3.2.13' + gem 'faker', '~> 1.1.2' + gem 'i18n', '~> 0.6.0' # Because 0.7+ requires Ruby 1.9.3+ + G + + bundle :install, :jobs => 4, :env => { "DEBUG" => "1" } + + if Bundler.rubygems.provides?(">= 2.1.0") + expect(out).to match(/[1-3]: /) + else + expect(out).to include("is not threadsafe") + end + + bundle "show activesupport" + expect(out).to match(/activesupport/) + + bundle "show faker" + expect(out).to match(/faker/) + + bundle "config jobs" + expect(out).to match(/: "4"/) + end + + it "updates" do + install_gemfile <<-G + source "https://rubygems.org" + gem 'activesupport', '3.2.12' + gem 'faker', '~> 1.1.2' + G + + gemfile <<-G + source "https://rubygems.org" + gem 'activesupport', '~> 3.2.12' + gem 'faker', '~> 1.1.2' + gem 'i18n', '~> 0.6.0' # Because 0.7+ requires Ruby 1.9.3+ + G + + bundle :update, :jobs => 4, :env => { "DEBUG" => "1" } + + if Bundler.rubygems.provides?(">= 2.1.0") + expect(out).to match(/[1-3]: /) + else + expect(out).to include("is not threadsafe") + end + + bundle "show activesupport" + expect(out).to match(/activesupport-3\.2\.\d+/) + + bundle "show faker" + expect(out).to match(/faker/) + + bundle "config jobs" + expect(out).to match(/: "4"/) + end + + it "works with --standalone" do + gemfile <<-G, :standalone => true + source "https://rubygems.org" + gem "diff-lcs" + G + + bundle :install, :standalone => true, :jobs => 4 + + ruby <<-RUBY, :no_lib => true + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + require "diff/lcs" + puts Diff::LCS + RUBY + + expect(out).to eq("Diff::LCS") + end +end diff --git a/spec/bundler/resolver/basic_spec.rb b/spec/bundler/resolver/basic_spec.rb new file mode 100644 index 0000000000..9e93847ab5 --- /dev/null +++ b/spec/bundler/resolver/basic_spec.rb @@ -0,0 +1,258 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "Resolving" do + before :each do + @index = an_awesome_index + end + + it "resolves a single gem" do + dep "rack" + + should_resolve_as %w(rack-1.1) + end + + it "resolves a gem with dependencies" do + dep "actionpack" + + should_resolve_as %w(actionpack-2.3.5 activesupport-2.3.5 rack-1.0) + end + + it "resolves a conflicting index" do + @index = a_conflict_index + dep "my_app" + should_resolve_as %w(activemodel-3.2.11 builder-3.0.4 grape-0.2.6 my_app-1.0.0) + end + + it "resolves a complex conflicting index" do + @index = a_complex_conflict_index + dep "my_app" + should_resolve_as %w(a-1.4.0 b-0.3.5 c-3.2 d-0.9.8 my_app-1.1.0) + end + + it "resolves a index with conflict on child" do + @index = index_with_conflict_on_child + dep "chef_app" + should_resolve_as %w(berkshelf-2.0.7 chef-10.26 chef_app-1.0.0 json-1.7.7) + end + + it "resolves a index with root level conflict on child" do + @index = a_index_with_root_conflict_on_child + dep "i18n", "~> 0.4" + dep "activesupport", "~> 3.0" + dep "activerecord", "~> 3.0" + dep "builder", "~> 2.1.2" + should_resolve_as %w(activesupport-3.0.5 i18n-0.4.2 builder-2.1.2 activerecord-3.0.5 activemodel-3.0.5) + end + + it "raises an exception if a child dependency is not resolved" do + @index = a_unresovable_child_index + dep "chef_app_error" + expect do + resolve + end.to raise_error(Bundler::VersionConflict) + end + + it "raises an exception with the minimal set of conflicting dependencies" do + @index = build_index do + %w(0.9 1.0 2.0).each {|v| gem("a", v) } + gem("b", "1.0") { dep "a", ">= 2" } + gem("c", "1.0") { dep "a", "< 1" } + end + dep "a" + dep "b" + dep "c" + expect do + resolve + end.to raise_error(Bundler::VersionConflict, <<-E.strip) +Bundler could not find compatible versions for gem "a": + In Gemfile: + b was resolved to 1.0, which depends on + a (>= 2) + + c was resolved to 1.0, which depends on + a (< 1) + E + end + + it "should throw error in case of circular dependencies" do + @index = a_circular_index + dep "circular_app" + + expect do + resolve + end.to raise_error(Bundler::CyclicDependencyError, /please remove either gem 'bar' or gem 'foo'/i) + end + + # Issue #3459 + it "should install the latest possible version of a direct requirement with no constraints given" do + @index = a_complicated_index + dep "foo" + should_resolve_and_include %w(foo-3.0.5) + end + + # Issue #3459 + it "should install the latest possible version of a direct requirement with constraints given" do + @index = a_complicated_index + dep "foo", ">= 3.0.0" + should_resolve_and_include %w(foo-3.0.5) + end + + it "takes into account required_ruby_version" do + @index = build_index do + gem "foo", "1.0.0" do + dep "bar", ">= 0" + end + + gem "foo", "2.0.0" do |s| + dep "bar", ">= 0" + s.required_ruby_version = "~> 2.0.0" + end + + gem "bar", "1.0.0" + + gem "bar", "2.0.0" do |s| + s.required_ruby_version = "~> 2.0.0" + end + + gem "ruby\0", "1.8.7" + end + dep "foo" + dep "ruby\0", "1.8.7" + + deps = [] + @deps.each do |d| + deps << Bundler::DepProxy.new(d, "ruby") + end + + should_resolve_and_include %w(foo-1.0.0 bar-1.0.0), [{}, []] + end + + context "conservative" do + before :each do + @index = build_index do + gem("foo", "1.3.7") { dep "bar", "~> 2.0" } + gem("foo", "1.3.8") { dep "bar", "~> 2.0" } + gem("foo", "1.4.3") { dep "bar", "~> 2.0" } + gem("foo", "1.4.4") { dep "bar", "~> 2.0" } + gem("foo", "1.4.5") { dep "bar", "~> 2.1" } + gem("foo", "1.5.0") { dep "bar", "~> 2.1" } + gem("foo", "1.5.1") { dep "bar", "~> 3.0" } + gem("foo", "2.0.0") { dep "bar", "~> 3.0" } + gem "bar", %w(2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0) + end + dep "foo" + + # base represents declared dependencies in the Gemfile that are still satisfied by the lockfile + @base = Bundler::SpecSet.new([]) + + # locked represents versions in lockfile + @locked = locked(%w(foo 1.4.3), %w(bar 2.0.3)) + end + + it "resolves all gems to latest patch" do + # strict is not set, so bar goes up a minor version due to dependency from foo 1.4.5 + should_conservative_resolve_and_include :patch, [], %w(foo-1.4.5 bar-2.1.1) + end + + it "resolves all gems to latest patch strict" do + # strict is set, so foo can only go up to 1.4.4 to avoid bar going up a minor version, and bar can go up to 2.0.5 + should_conservative_resolve_and_include [:patch, :strict], [], %w(foo-1.4.4 bar-2.0.5) + end + + it "resolves foo only to latest patch - same dependency case" do + @locked = locked(%w(foo 1.3.7), %w(bar 2.0.3)) + # bar is locked, and the lock holds here because the dependency on bar doesn't change on the matching foo version. + should_conservative_resolve_and_include :patch, ["foo"], %w(foo-1.3.8 bar-2.0.3) + end + + it "resolves foo only to latest patch - changing dependency not declared case" do + # foo is the only gem being requested for update, therefore bar is locked, but bar is NOT + # declared as a dependency in the Gemfile. In this case, locks don't apply to _changing_ + # dependencies and since the dependency of the selected foo gem changes, the latest matching + # dependency of "bar", "~> 2.1" -- bar-2.1.1 -- is selected. This is not a bug and follows + # the long-standing documented Conservative Updating behavior of bundle install. + # http://bundler.io/v1.12/man/bundle-install.1.html#CONSERVATIVE-UPDATING + should_conservative_resolve_and_include :patch, ["foo"], %w(foo-1.4.5 bar-2.1.1) + end + + it "resolves foo only to latest patch - changing dependency declared case" do + # bar is locked AND a declared dependency in the Gemfile, so it will not move, and therefore + # foo can only move up to 1.4.4. + @base << build_spec("bar", "2.0.3").first + should_conservative_resolve_and_include :patch, ["foo"], %w(foo-1.4.4 bar-2.0.3) + end + + it "resolves foo only to latest patch strict" do + # adding strict helps solve the possibly unexpected behavior of bar changing in the prior test case, + # because no versions will be returned for bar ~> 2.1, so the engine falls back to ~> 2.0 (turn on + # debugging to see this happen). + should_conservative_resolve_and_include [:patch, :strict], ["foo"], %w(foo-1.4.4 bar-2.0.3) + end + + it "resolves bar only to latest patch" do + # bar is locked, so foo can only go up to 1.4.4 + should_conservative_resolve_and_include :patch, ["bar"], %w(foo-1.4.3 bar-2.0.5) + end + + it "resolves all gems to latest minor" do + # strict is not set, so bar goes up a major version due to dependency from foo 1.4.5 + should_conservative_resolve_and_include :minor, [], %w(foo-1.5.1 bar-3.0.0) + end + + it "resolves all gems to latest minor strict" do + # strict is set, so foo can only go up to 1.5.0 to avoid bar going up a major version + should_conservative_resolve_and_include [:minor, :strict], [], %w(foo-1.5.0 bar-2.1.1) + end + + it "resolves all gems to latest major" do + should_conservative_resolve_and_include :major, [], %w(foo-2.0.0 bar-3.0.0) + end + + it "resolves all gems to latest major strict" do + should_conservative_resolve_and_include [:major, :strict], [], %w(foo-2.0.0 bar-3.0.0) + end + + # Why would this happen in real life? If bar 2.2 has a bug that the author of foo wants to bypass + # by reverting the dependency, the author of foo could release a new gem with an older requirement. + context "revert to previous" do + before :each do + @index = build_index do + gem("foo", "1.4.3") { dep "bar", "~> 2.2" } + gem("foo", "1.4.4") { dep "bar", "~> 2.1.0" } + gem("foo", "1.5.0") { dep "bar", "~> 2.0.0" } + gem "bar", %w(2.0.5 2.1.1 2.2.3) + end + dep "foo" + + # base represents declared dependencies in the Gemfile that are still satisfied by the lockfile + @base = Bundler::SpecSet.new([]) + + # locked represents versions in lockfile + @locked = locked(%w(foo 1.4.3), %w(bar 2.2.3)) + end + + it "could revert to a previous version level patch" do + should_conservative_resolve_and_include :patch, [], %w(foo-1.4.4 bar-2.1.1) + end + + it "cannot revert to a previous version in strict mode level patch" do + # the strict option removes the version required to match, so a version conflict results + expect do + should_conservative_resolve_and_include [:patch, :strict], [], %w(foo-1.4.3 bar-2.1.1) + end.to raise_error Bundler::VersionConflict, /#{Regexp.escape("Could not find gem 'bar (~> 2.1.0)'")}/ + end + + it "could revert to a previous version level minor" do + should_conservative_resolve_and_include :minor, [], %w(foo-1.5.0 bar-2.0.5) + end + + it "cannot revert to a previous version in strict mode level minor" do + # the strict option removes the version required to match, so a version conflict results + expect do + should_conservative_resolve_and_include [:minor, :strict], [], %w(foo-1.4.3 bar-2.1.1) + end.to raise_error Bundler::VersionConflict, /#{Regexp.escape("Could not find gem 'bar (~> 2.0.0)'")}/ + end + end + end +end diff --git a/spec/bundler/resolver/platform_spec.rb b/spec/bundler/resolver/platform_spec.rb new file mode 100644 index 0000000000..90d6f637ce --- /dev/null +++ b/spec/bundler/resolver/platform_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "Resolving platform craziness" do + describe "with cross-platform gems" do + before :each do + @index = an_awesome_index + end + + it "resolves a simple multi platform gem" do + dep "nokogiri" + platforms "ruby", "java" + + should_resolve_as %w(nokogiri-1.4.2 nokogiri-1.4.2-java weakling-0.0.3) + end + + it "doesn't pull gems that don't exist for the current platform" do + dep "nokogiri" + platforms "ruby" + + should_resolve_as %w(nokogiri-1.4.2) + end + + it "doesn't pull gems when the version is available for all requested platforms" do + dep "nokogiri" + platforms "mswin32" + + should_resolve_as %w(nokogiri-1.4.2.1-x86-mswin32) + end + end + + describe "with mingw32" do + before :each do + @index = build_index do + platforms "mingw32 mswin32 x64-mingw32" do |platform| + gem "thin", "1.2.7", platform + end + gem "win32-api", "1.5.1", "universal-mingw32" + end + end + + it "finds mswin gems" do + # win32 is hardcoded to get CPU x86 in rubygems + platforms "mswin32" + dep "thin" + should_resolve_as %w(thin-1.2.7-x86-mswin32) + end + + it "finds mingw gems" do + # mingw is _not_ hardcoded to add CPU x86 in rubygems + platforms "x86-mingw32" + dep "thin" + should_resolve_as %w(thin-1.2.7-mingw32) + end + + it "finds x64-mingw gems" do + platforms "x64-mingw32" + dep "thin" + should_resolve_as %w(thin-1.2.7-x64-mingw32) + end + + it "finds universal-mingw gems on x86-mingw" do + platform "x86-mingw32" + dep "win32-api" + should_resolve_as %w(win32-api-1.5.1-universal-mingw32) + end + + it "finds universal-mingw gems on x64-mingw" do + platform "x64-mingw32" + dep "win32-api" + should_resolve_as %w(win32-api-1.5.1-universal-mingw32) + end + end + + describe "with conflicting cases" do + before :each do + @index = build_index do + gem "foo", "1.0.0" do + dep "bar", ">= 0" + end + + gem "bar", "1.0.0" do + dep "baz", "~> 1.0.0" + end + + gem "bar", "1.0.0", "java" do + dep "baz", " ~> 1.1.0" + end + + gem "baz", %w(1.0.0 1.1.0 1.2.0) + end + end + + it "reports on the conflict" do + platforms "ruby", "java" + dep "foo" + + should_conflict_on "baz" + end + end +end diff --git a/spec/bundler/runtime/executable_spec.rb b/spec/bundler/runtime/executable_spec.rb new file mode 100644 index 0000000000..ff27d0b415 --- /dev/null +++ b/spec/bundler/runtime/executable_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "Running bin/* commands" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + it "runs the bundled command when in the bundle" do + bundle "install --binstubs" + + build_gem "rack", "2.0", :to_system => true do |s| + s.executables = "rackup" + end + + gembin "rackup" + expect(out).to eq("1.0.0") + end + + it "allows the location of the gem stubs to be specified" do + bundle "install --binstubs gbin" + + expect(bundled_app("bin")).not_to exist + expect(bundled_app("gbin/rackup")).to exist + + gembin bundled_app("gbin/rackup") + expect(out).to eq("1.0.0") + end + + it "allows absolute paths as a specification of where to install bin stubs" do + bundle "install --binstubs #{tmp}/bin" + + gembin tmp("bin/rackup") + expect(out).to eq("1.0.0") + end + + it "uses the default ruby install name when shebang is not specified" do + bundle "install --binstubs" + expect(File.open("bin/rackup").gets).to eq("#!/usr/bin/env #{RbConfig::CONFIG["ruby_install_name"]}\n") + end + + it "allows the name of the shebang executable to be specified" do + bundle "install --binstubs --shebang ruby-foo" + expect(File.open("bin/rackup").gets).to eq("#!/usr/bin/env ruby-foo\n") + end + + it "runs the bundled command when out of the bundle" do + bundle "install --binstubs" + + build_gem "rack", "2.0", :to_system => true do |s| + s.executables = "rackup" + end + + Dir.chdir(tmp) do + gembin "rackup" + expect(out).to eq("1.0.0") + end + end + + it "works with gems in path" do + build_lib "rack", :path => lib_path("rack") do |s| + s.executables = "rackup" + end + + gemfile <<-G + gem "rack", :path => "#{lib_path("rack")}" + G + + bundle "install --binstubs" + + build_gem "rack", "2.0", :to_system => true do |s| + s.executables = "rackup" + end + + gembin "rackup" + expect(out).to eq("1.0") + end + + it "don't bundle da bundla" do + build_gem "bundler", Bundler::VERSION, :to_system => true do |s| + s.executables = "bundle" + end + + gemfile <<-G + source "file://#{gem_repo1}" + gem "bundler" + G + + bundle "install --binstubs" + + expect(bundled_app("bin/bundle")).not_to exist + end + + it "does not generate bin stubs if the option was not specified" do + bundle "install" + + expect(bundled_app("bin/rackup")).not_to exist + end + + it "allows you to stop installing binstubs" do + bundle "install --binstubs bin/" + bundled_app("bin/rackup").rmtree + bundle "install --binstubs \"\"" + + expect(bundled_app("bin/rackup")).not_to exist + + bundle "config bin" + expect(out).to include("You have not configured a value for `bin`") + end + + it "remembers that the option was specified" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + G + + bundle "install --binstubs" + + gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + gem "rack" + G + + bundle "install" + + expect(bundled_app("bin/rackup")).to exist + end + + it "rewrites bins on --binstubs (to maintain backwards compatibility)" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "install --binstubs bin/" + + File.open(bundled_app("bin/rackup"), "wb") do |file| + file.print "OMG" + end + + bundle "install" + + expect(bundled_app("bin/rackup").read).to_not eq("OMG") + end +end diff --git a/spec/bundler/runtime/gem_tasks_spec.rb b/spec/bundler/runtime/gem_tasks_spec.rb new file mode 100644 index 0000000000..7cb0f32c0c --- /dev/null +++ b/spec/bundler/runtime/gem_tasks_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "require 'bundler/gem_tasks'" do + before :each do + bundled_app("foo.gemspec").open("w") do |f| + f.write <<-GEMSPEC + Gem::Specification.new do |s| + s.name = "foo" + end + GEMSPEC + end + bundled_app("Rakefile").open("w") do |f| + f.write <<-RAKEFILE + $:.unshift("#{bundler_path}") + require "bundler/gem_tasks" + RAKEFILE + end + end + + it "includes the relevant tasks" do + with_gem_path_as(Spec::Path.base_system_gems.to_s) do + sys_exec "#{rake} -T" + end + + expect(err).to eq("") + expected_tasks = [ + "rake build", + "rake clean", + "rake clobber", + "rake install", + "rake release[remote]", + ] + tasks = out.lines.to_a.map {|s| s.split("#").first.strip } + expect(tasks & expected_tasks).to eq(expected_tasks) + expect(exitstatus).to eq(0) if exitstatus + end + + it "adds 'pkg' to rake/clean's CLOBBER" do + require "bundler/gem_tasks" + expect(CLOBBER).to include("pkg") + end +end diff --git a/spec/bundler/runtime/inline_spec.rb b/spec/bundler/runtime/inline_spec.rb new file mode 100644 index 0000000000..e816799d08 --- /dev/null +++ b/spec/bundler/runtime/inline_spec.rb @@ -0,0 +1,268 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundler/inline#gemfile" do + def script(code, options = {}) + requires = ["bundler/inline"] + requires.unshift File.expand_path("../../support/artifice/" + options.delete(:artifice) + ".rb", __FILE__) if options.key?(:artifice) + requires = requires.map {|r| "require '#{r}'" }.join("\n") + @out = ruby("#{requires}\n\n" + code, options) + end + + before :each do + build_lib "one", "1.0.0" do |s| + s.write "lib/baz.rb", "puts 'baz'" + s.write "lib/qux.rb", "puts 'qux'" + end + + build_lib "two", "1.0.0" do |s| + s.write "lib/two.rb", "puts 'two'" + s.add_dependency "three", "= 1.0.0" + end + + build_lib "three", "1.0.0" do |s| + s.write "lib/three.rb", "puts 'three'" + s.add_dependency "seven", "= 1.0.0" + end + + build_lib "four", "1.0.0" do |s| + s.write "lib/four.rb", "puts 'four'" + end + + build_lib "five", "1.0.0", :no_default => true do |s| + s.write "lib/mofive.rb", "puts 'five'" + end + + build_lib "six", "1.0.0" do |s| + s.write "lib/six.rb", "puts 'six'" + end + + build_lib "seven", "1.0.0" do |s| + s.write "lib/seven.rb", "puts 'seven'" + end + + build_lib "eight", "1.0.0" do |s| + s.write "lib/eight.rb", "puts 'eight'" + end + + build_lib "four", "1.0.0" do |s| + s.write "lib/four.rb", "puts 'four'" + end + end + + it "requires the gems" do + script <<-RUBY + gemfile do + path "#{lib_path}" + gem "two" + end + RUBY + + expect(out).to eq("two") + expect(exitstatus).to be_zero if exitstatus + + script <<-RUBY + gemfile do + path "#{lib_path}" + gem "eleven" + end + + puts "success" + RUBY + + expect(err).to include "Could not find gem 'eleven'" + expect(out).not_to include "success" + + script <<-RUBY + gemfile(true) do + source "file://#{gem_repo1}" + gem "rack" + end + RUBY + + expect(out).to include("Rack's post install message") + expect(exitstatus).to be_zero if exitstatus + + script <<-RUBY, :artifice => "endpoint" + gemfile(true) do + source "https://notaserver.com" + gem "activesupport", :require => true + end + RUBY + + expect(out).to include("Installing activesupport") + err.gsub! %r{.*lib/sinatra/base\.rb:\d+: warning: constant ::Fixnum is deprecated$}, "" + err.strip! + expect(err).to lack_errors + expect(exitstatus).to be_zero if exitstatus + end + + it "lets me use my own ui object" do + script <<-RUBY, :artifice => "endpoint" + require 'bundler' + class MyBundlerUI < Bundler::UI::Silent + def confirm(msg, newline = nil) + puts "CONFIRMED!" + end + end + gemfile(true, :ui => MyBundlerUI.new) do + source "https://notaserver.com" + gem "activesupport", :require => true + end + RUBY + + expect(out).to eq("CONFIRMED!\nCONFIRMED!") + expect(exitstatus).to be_zero if exitstatus + end + + it "raises an exception if passed unknown arguments" do + script <<-RUBY + gemfile(true, :arglebargle => true) do + path "#{lib_path}" + gem "two" + end + + puts "success" + RUBY + expect(err).to include "Unknown options: arglebargle" + expect(out).not_to include "success" + end + + it "does not mutate the option argument" do + script <<-RUBY + require 'bundler' + options = { :ui => Bundler::UI::Shell.new } + gemfile(false, options) do + path "#{lib_path}" + gem "two" + end + puts "OKAY" if options.key?(:ui) + RUBY + + expect(out).to match("OKAY") + expect(exitstatus).to be_zero if exitstatus + end + + it "installs quietly if necessary when the install option is not set" do + script <<-RUBY + gemfile do + source "file://#{gem_repo1}" + gem "rack" + end + + puts RACK + RUBY + + expect(out).to eq("1.0.0") + expect(err).to be_empty + expect(exitstatus).to be_zero if exitstatus + end + + it "installs quietly from git if necessary when the install option is not set" do + build_git "foo", "1.0.0" + baz_ref = build_git("baz", "2.0.0").ref_for("HEAD") + script <<-RUBY + gemfile do + gem "foo", :git => #{lib_path("foo-1.0.0").to_s.dump} + gem "baz", :git => #{lib_path("baz-2.0.0").to_s.dump}, :ref => #{baz_ref.dump} + end + + puts FOO + puts BAZ + RUBY + + expect(out).to eq("1.0.0\n2.0.0") + expect(err).to be_empty + expect(exitstatus).to be_zero if exitstatus + end + + it "allows calling gemfile twice" do + script <<-RUBY + gemfile do + path "#{lib_path}" do + gem "two" + end + end + + gemfile do + path "#{lib_path}" do + gem "four" + end + end + RUBY + + expect(out).to eq("two\nfour") + expect(err).to be_empty + expect(exitstatus).to be_zero if exitstatus + end + + it "installs inline gems when a Gemfile.lock is present" do + gemfile <<-G + source "https://notaserver.com" + gem "rake" + G + + lockfile <<-G + GEM + remote: https://rubygems.org/ + specs: + rake (11.3.0) + + PLATFORMS + ruby + + DEPENDENCIES + rake + + BUNDLED WITH + 1.13.6 + G + + in_app_root do + script <<-RUBY + gemfile do + source "file://#{gem_repo1}" + gem "rack" + end + + puts RACK + RUBY + end + + expect(err).to be_empty + expect(exitstatus).to be_zero if exitstatus + end + + it "installs inline gems when BUNDLE_GEMFILE is set to an empty string" do + ENV["BUNDLE_GEMFILE"] = "" + + in_app_root do + script <<-RUBY + gemfile do + source "file://#{gem_repo1}" + gem "rack" + end + + puts RACK + RUBY + end + + expect(err).to be_empty + expect(exitstatus).to be_zero if exitstatus + end + + it "installs inline gems when BUNDLE_BIN is set" do + ENV["BUNDLE_BIN"] = "/usr/local/bundle/bin" + + script <<-RUBY + gemfile do + source "file://#{gem_repo1}" + gem "rack" # has the rackup executable + end + + puts RACK + RUBY + expect(exitstatus).to eq(0) if exitstatus + expect(out).to eq "1.0.0" + end +end diff --git a/spec/bundler/runtime/load_spec.rb b/spec/bundler/runtime/load_spec.rb new file mode 100644 index 0000000000..d0e308ed3e --- /dev/null +++ b/spec/bundler/runtime/load_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "Bundler.load" do + before :each do + system_gems "rack-1.0.0" + end + + describe "with a gemfile" do + before(:each) do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + it "provides a list of the env dependencies" do + expect(Bundler.load.dependencies).to have_dep("rack", ">= 0") + end + + it "provides a list of the resolved gems" do + expect(Bundler.load.gems).to have_gem("rack-1.0.0", "bundler-#{Bundler::VERSION}") + end + + it "ignores blank BUNDLE_GEMFILEs" do + expect do + ENV["BUNDLE_GEMFILE"] = "" + Bundler.load + end.not_to raise_error + end + end + + describe "with a gems.rb file" do + before(:each) do + create_file "gems.rb", <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + it "provides a list of the env dependencies" do + expect(Bundler.load.dependencies).to have_dep("rack", ">= 0") + end + + it "provides a list of the resolved gems" do + expect(Bundler.load.gems).to have_gem("rack-1.0.0", "bundler-#{Bundler::VERSION}") + end + end + + describe "without a gemfile" do + it "raises an exception if the default gemfile is not found" do + expect do + Bundler.load + end.to raise_error(Bundler::GemfileNotFound, /could not locate gemfile/i) + end + + it "raises an exception if a specified gemfile is not found" do + expect do + ENV["BUNDLE_GEMFILE"] = "omg.rb" + Bundler.load + end.to raise_error(Bundler::GemfileNotFound, /omg\.rb/) + end + + it "does not find a Gemfile above the testing directory" do + bundler_gemfile = tmp.join("../Gemfile") + unless File.exist?(bundler_gemfile) + FileUtils.touch(bundler_gemfile) + @remove_bundler_gemfile = true + end + begin + expect { Bundler.load }.to raise_error(Bundler::GemfileNotFound) + ensure + bundler_gemfile.rmtree if @remove_bundler_gemfile + end + end + end + + describe "when called twice" do + it "doesn't try to load the runtime twice" do + system_gems "rack-1.0.0", "activesupport-2.3.5" + gemfile <<-G + gem "rack" + gem "activesupport", :group => :test + G + + ruby <<-RUBY + require "bundler" + Bundler.setup :default + Bundler.require :default + puts RACK + begin + require "activesupport" + rescue LoadError + puts "no activesupport" + end + RUBY + + expect(out.split("\n")).to eq(["1.0.0", "no activesupport"]) + end + end + + describe "not hurting brittle rubygems" do + it "does not inject #source into the generated YAML of the gem specs" do + system_gems "activerecord-2.3.2", "activesupport-2.3.2" + gemfile <<-G + gem "activerecord" + G + + Bundler.load.specs.each do |spec| + expect(spec.to_yaml).not_to match(/^\s+source:/) + expect(spec.to_yaml).not_to match(/^\s+groups:/) + end + end + end +end diff --git a/spec/bundler/runtime/platform_spec.rb b/spec/bundler/runtime/platform_spec.rb new file mode 100644 index 0000000000..4df934e71f --- /dev/null +++ b/spec/bundler/runtime/platform_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "Bundler.setup with multi platform stuff" do + it "raises a friendly error when gems are missing locally" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + lockfile <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0) + + PLATFORMS + #{local_tag} + + DEPENDENCIES + rack + G + + ruby <<-R + begin + require 'bundler' + Bundler.setup + rescue Bundler::GemNotFound => e + puts "WIN" + end + R + + expect(out).to eq("WIN") + end + + it "will resolve correctly on the current platform when the lockfile was targetted for a different one" do + lockfile <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + nokogiri (1.4.2-java) + weakling (= 0.0.3) + weakling (0.0.3) + + PLATFORMS + java + + DEPENDENCIES + nokogiri + G + + system_gems "nokogiri-1.4.2" + + simulate_platform "x86-darwin-10" + gemfile <<-G + source "file://#{gem_repo1}" + gem "nokogiri" + G + + expect(the_bundle).to include_gems "nokogiri 1.4.2" + end + + it "will add the resolve for the current platform" do + lockfile <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + nokogiri (1.4.2-java) + weakling (= 0.0.3) + weakling (0.0.3) + + PLATFORMS + java + + DEPENDENCIES + nokogiri + G + + simulate_platform "x86-darwin-100" + + system_gems "nokogiri-1.4.2", "platform_specific-1.0-x86-darwin-100" + + gemfile <<-G + source "file://#{gem_repo1}" + gem "nokogiri" + gem "platform_specific" + G + + expect(the_bundle).to include_gems "nokogiri 1.4.2", "platform_specific 1.0 x86-darwin-100" + end + + it "allows specifying only-ruby-platform" do + simulate_platform "java" + + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "nokogiri" + gem "platform_specific" + G + + bundle! "config force_ruby_platform true" + + bundle! "install" + + expect(the_bundle).to include_gems "nokogiri 1.4.2", "platform_specific 1.0 RUBY" + end + + it "allows specifying only-ruby-platform on windows with dependency platforms" do + simulate_windows do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "nokogiri", :platforms => [:mingw, :mswin, :x64_mingw, :jruby] + gem "platform_specific" + G + + bundle! "config force_ruby_platform true" + + bundle! "install" + + expect(the_bundle).to include_gems "platform_specific 1.0 RUBY" + end + end +end diff --git a/spec/bundler/runtime/require_spec.rb b/spec/bundler/runtime/require_spec.rb new file mode 100644 index 0000000000..b68313726b --- /dev/null +++ b/spec/bundler/runtime/require_spec.rb @@ -0,0 +1,442 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "Bundler.require" do + before :each do + build_lib "one", "1.0.0" do |s| + s.write "lib/baz.rb", "puts 'baz'" + s.write "lib/qux.rb", "puts 'qux'" + end + + build_lib "two", "1.0.0" do |s| + s.write "lib/two.rb", "puts 'two'" + s.add_dependency "three", "= 1.0.0" + end + + build_lib "three", "1.0.0" do |s| + s.write "lib/three.rb", "puts 'three'" + s.add_dependency "seven", "= 1.0.0" + end + + build_lib "four", "1.0.0" do |s| + s.write "lib/four.rb", "puts 'four'" + end + + build_lib "five", "1.0.0", :no_default => true do |s| + s.write "lib/mofive.rb", "puts 'five'" + end + + build_lib "six", "1.0.0" do |s| + s.write "lib/six.rb", "puts 'six'" + end + + build_lib "seven", "1.0.0" do |s| + s.write "lib/seven.rb", "puts 'seven'" + end + + build_lib "eight", "1.0.0" do |s| + s.write "lib/eight.rb", "puts 'eight'" + end + + build_lib "nine", "1.0.0" do |s| + s.write "lib/nine.rb", "puts 'nine'" + end + + build_lib "ten", "1.0.0" do |s| + s.write "lib/ten.rb", "puts 'ten'" + end + + gemfile <<-G + path "#{lib_path}" + gem "one", :group => :bar, :require => %w[baz qux] + gem "two" + gem "three", :group => :not + gem "four", :require => false + gem "five" + gem "six", :group => "string" + gem "seven", :group => :not + gem "eight", :require => true, :group => :require_true + env "BUNDLER_TEST" => "nine" do + gem "nine", :require => true + end + gem "ten", :install_if => lambda { ENV["BUNDLER_TEST"] == "ten" } + G + end + + it "requires the gems" do + # default group + run "Bundler.require" + expect(out).to eq("two") + + # specific group + run "Bundler.require(:bar)" + expect(out).to eq("baz\nqux") + + # default and specific group + run "Bundler.require(:default, :bar)" + expect(out).to eq("baz\nqux\ntwo") + + # specific group given as a string + run "Bundler.require('bar')" + expect(out).to eq("baz\nqux") + + # specific group declared as a string + run "Bundler.require(:string)" + expect(out).to eq("six") + + # required in resolver order instead of gemfile order + run("Bundler.require(:not)") + expect(out.split("\n").sort).to eq(%w(seven three)) + + # test require: true + run "Bundler.require(:require_true)" + expect(out).to eq("eight") + end + + it "allows requiring gems with non standard names explicitly" do + run "Bundler.require ; require 'mofive'" + expect(out).to eq("two\nfive") + end + + it "allows requiring gems which are scoped by env" do + ENV["BUNDLER_TEST"] = "nine" + run "Bundler.require" + expect(out).to eq("two\nnine") + end + + it "allows requiring gems which are scoped by install_if" do + ENV["BUNDLER_TEST"] = "ten" + run "Bundler.require" + expect(out).to eq("two\nten") + end + + it "raises an exception if a require is specified but the file does not exist" do + gemfile <<-G + path "#{lib_path}" + gem "two", :require => 'fail' + G + + load_error_run <<-R, "fail" + Bundler.require + R + + expect(err).to eq_err("ZOMG LOAD ERROR") + end + + it "displays a helpful message if the required gem throws an error" do + build_lib "faulty", "1.0.0" do |s| + s.write "lib/faulty.rb", "raise RuntimeError.new(\"Gem Internal Error Message\")" + end + + gemfile <<-G + path "#{lib_path}" + gem "faulty" + G + + run "Bundler.require" + expect(err).to match("error while trying to load the gem 'faulty'") + expect(err).to match("Gem Internal Error Message") + end + + it "doesn't swallow the error when the library has an unrelated error" do + build_lib "loadfuuu", "1.0.0" do |s| + s.write "lib/loadfuuu.rb", "raise LoadError.new(\"cannot load such file -- load-bar\")" + end + + gemfile <<-G + path "#{lib_path}" + gem "loadfuuu" + G + + cmd = <<-RUBY + begin + Bundler.require + rescue LoadError => e + $stderr.puts "ZOMG LOAD ERROR: \#{e.message}" + end + RUBY + run(cmd) + + expect(err).to eq_err("ZOMG LOAD ERROR: cannot load such file -- load-bar") + end + + describe "with namespaced gems" do + before :each do + build_lib "jquery-rails", "1.0.0" do |s| + s.write "lib/jquery/rails.rb", "puts 'jquery/rails'" + end + lib_path("jquery-rails-1.0.0/lib/jquery-rails.rb").rmtree + end + + it "requires gem names that are namespaced" do + gemfile <<-G + path '#{lib_path}' + gem 'jquery-rails' + G + + run "Bundler.require" + expect(out).to eq("jquery/rails") + end + + it "silently passes if the require fails" do + build_lib "bcrypt-ruby", "1.0.0", :no_default => true do |s| + s.write "lib/brcrypt.rb", "BCrypt = '1.0.0'" + end + gemfile <<-G + path "#{lib_path}" + gem "bcrypt-ruby" + G + + cmd = <<-RUBY + require 'bundler' + Bundler.require + RUBY + ruby(cmd) + + expect(err).to lack_errors + end + + it "does not mangle explicitly given requires" do + gemfile <<-G + path "#{lib_path}" + gem 'jquery-rails', :require => 'jquery-rails' + G + + load_error_run <<-R, "jquery-rails" + Bundler.require + R + expect(err).to eq_err("ZOMG LOAD ERROR") + end + + it "handles the case where regex fails" do + build_lib "load-fuuu", "1.0.0" do |s| + s.write "lib/load-fuuu.rb", "raise LoadError.new(\"Could not open library 'libfuuu-1.0': libfuuu-1.0: cannot open shared object file: No such file or directory.\")" + end + + gemfile <<-G + path "#{lib_path}" + gem "load-fuuu" + G + + cmd = <<-RUBY + begin + Bundler.require + rescue LoadError => e + $stderr.puts "ZOMG LOAD ERROR" if e.message.include?("Could not open library 'libfuuu-1.0'") + end + RUBY + run(cmd) + + expect(err).to eq_err("ZOMG LOAD ERROR") + end + + it "doesn't swallow the error when the library has an unrelated error" do + build_lib "load-fuuu", "1.0.0" do |s| + s.write "lib/load/fuuu.rb", "raise LoadError.new(\"cannot load such file -- load-bar\")" + end + lib_path("load-fuuu-1.0.0/lib/load-fuuu.rb").rmtree + + gemfile <<-G + path "#{lib_path}" + gem "load-fuuu" + G + + cmd = <<-RUBY + begin + Bundler.require + rescue LoadError => e + $stderr.puts "ZOMG LOAD ERROR: \#{e.message}" + end + RUBY + run(cmd) + + expect(err).to eq_err("ZOMG LOAD ERROR: cannot load such file -- load-bar") + end + end + + describe "using bundle exec" do + it "requires the locked gems" do + bundle "exec ruby -e 'Bundler.require'" + expect(out).to eq("two") + + bundle "exec ruby -e 'Bundler.require(:bar)'" + expect(out).to eq("baz\nqux") + + bundle "exec ruby -e 'Bundler.require(:default, :bar)'" + expect(out).to eq("baz\nqux\ntwo") + end + end + + describe "order" do + before(:each) do + build_lib "one", "1.0.0" do |s| + s.write "lib/one.rb", <<-ONE + if defined?(Two) + Two.two + else + puts "two_not_loaded" + end + puts 'one' + ONE + end + + build_lib "two", "1.0.0" do |s| + s.write "lib/two.rb", <<-TWO + module Two + def self.two + puts 'module_two' + end + end + puts 'two' + TWO + end + end + + it "works when the gems are in the Gemfile in the correct order" do + gemfile <<-G + path "#{lib_path}" + gem "two" + gem "one" + G + + run "Bundler.require" + expect(out).to eq("two\nmodule_two\none") + end + + describe "a gem with different requires for different envs" do + before(:each) do + build_gem "multi_gem", :to_system => true do |s| + s.write "lib/one.rb", "puts 'ONE'" + s.write "lib/two.rb", "puts 'TWO'" + end + + install_gemfile <<-G + gem "multi_gem", :require => "one", :group => :one + gem "multi_gem", :require => "two", :group => :two + G + end + + it "requires both with Bundler.require(both)" do + run "Bundler.require(:one, :two)" + expect(out).to eq("ONE\nTWO") + end + + it "requires one with Bundler.require(:one)" do + run "Bundler.require(:one)" + expect(out).to eq("ONE") + end + + it "requires :two with Bundler.require(:two)" do + run "Bundler.require(:two)" + expect(out).to eq("TWO") + end + end + + it "fails when the gems are in the Gemfile in the wrong order" do + gemfile <<-G + path "#{lib_path}" + gem "one" + gem "two" + G + + run "Bundler.require" + expect(out).to eq("two_not_loaded\none\ntwo") + end + + describe "with busted gems" do + it "should be busted" do + build_gem "busted_require", :to_system => true do |s| + s.write "lib/busted_require.rb", "require 'no_such_file_omg'" + end + + install_gemfile <<-G + gem "busted_require" + G + + load_error_run <<-R, "no_such_file_omg" + Bundler.require + R + expect(err).to eq_err("ZOMG LOAD ERROR") + end + end + end + + it "does not load rubygems gemspecs that are used", :rubygems => ">= 2.5.2" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + run! <<-R + path = File.join(Gem.dir, "specifications", "rack-1.0.0.gemspec") + contents = File.read(path) + contents = contents.lines.to_a.insert(-2, "\n raise 'broken gemspec'\n").join + File.open(path, "w") do |f| + f.write contents + end + R + + run! <<-R + Bundler.require + puts "WIN" + R + + expect(out).to eq("WIN") + end + + it "does not load git gemspecs that are used", :rubygems => ">= 2.5.2" do + build_git "foo" + + install_gemfile! <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run! <<-R + path = Gem.loaded_specs["foo"].loaded_from + contents = File.read(path) + contents = contents.lines.to_a.insert(-2, "\n raise 'broken gemspec'\n").join + File.open(path, "w") do |f| + f.write contents + end + R + + run! <<-R + Bundler.require + puts "WIN" + R + + expect(out).to eq("WIN") + end +end + +RSpec.describe "Bundler.require with platform specific dependencies" do + it "does not require the gems that are pinned to other platforms" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + platforms :#{not_local_tag} do + gem "fail", :require => "omgomg" + end + + gem "rack", "1.0.0" + G + + run "Bundler.require" + expect(err).to lack_errors + end + + it "requires gems pinned to multiple platforms, including the current one" do + install_gemfile <<-G + source "file://#{gem_repo1}" + + platforms :#{not_local_tag}, :#{local_tag} do + gem "rack", :require => "rack" + end + G + + run "Bundler.require; puts RACK" + + expect(out).to eq("1.0.0") + expect(err).to lack_errors + end +end diff --git a/spec/bundler/runtime/setup_spec.rb b/spec/bundler/runtime/setup_spec.rb new file mode 100644 index 0000000000..dc7af07188 --- /dev/null +++ b/spec/bundler/runtime/setup_spec.rb @@ -0,0 +1,1289 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "Bundler.setup" do + describe "with no arguments" do + it "makes all groups available" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :group => :test + G + + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup + + require 'rack' + puts RACK + RUBY + expect(err).to lack_errors + expect(out).to eq("1.0.0") + end + end + + describe "when called with groups" do + before(:each) do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + gem "rack", :group => :test + G + end + + it "doesn't make all groups available" do + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup(:default) + + begin + require 'rack' + rescue LoadError + puts "WIN" + end + RUBY + expect(err).to lack_errors + expect(out).to eq("WIN") + end + + it "accepts string for group name" do + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup(:default, 'test') + + require 'rack' + puts RACK + RUBY + expect(err).to lack_errors + expect(out).to eq("1.0.0") + end + + it "leaves all groups available if they were already" do + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup + Bundler.setup(:default) + + require 'rack' + puts RACK + RUBY + expect(err).to lack_errors + expect(out).to eq("1.0.0") + end + + it "leaves :default available if setup is called twice" do + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup(:default) + Bundler.setup(:default, :test) + + begin + require 'yard' + puts "WIN" + rescue LoadError + puts "FAIL" + end + RUBY + expect(err).to lack_errors + expect(out).to match("WIN") + end + + it "handles multiple non-additive invocations" do + ruby <<-RUBY + require 'bundler' + Bundler.setup(:default, :test) + Bundler.setup(:default) + require 'rack' + + puts "FAIL" + RUBY + + expect(err).to match("rack") + expect(err).to match("LoadError") + expect(out).not_to match("FAIL") + end + end + + context "load order" do + def clean_load_path(lp) + without_bundler_load_path = ruby!("puts $LOAD_PATH").split("\n") + lp = lp - [ + bundler_path.to_s, + bundler_path.join("gems/bundler-#{Bundler::VERSION}/lib").to_s, + tmp("rubygems/lib").to_s, + root.join("../lib").expand_path.to_s, + ] - without_bundler_load_path + lp.map! {|p| p.sub(/^#{system_gem_path}/, "") } + end + + it "puts loaded gems after -I and RUBYLIB", :ruby_repo do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + ENV["RUBYOPT"] = "-Idash_i_dir" + ENV["RUBYLIB"] = "rubylib_dir" + + ruby <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup + puts $LOAD_PATH + RUBY + + load_path = out.split("\n") + rack_load_order = load_path.index {|path| path.include?("rack") } + + expect(err).to eq("") + expect(load_path[1]).to include "dash_i_dir" + expect(load_path[2]).to include "rubylib_dir" + expect(rack_load_order).to be > 0 + end + + it "orders the load path correctly when there are dependencies", :ruby_repo do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + G + + ruby! <<-RUBY + require 'rubygems' + require 'bundler' + Bundler.setup + puts $LOAD_PATH + RUBY + + load_path = clean_load_path(out.split("\n")) + + expect(load_path).to start_with( + "/gems/rails-2.3.2/lib", + "/gems/activeresource-2.3.2/lib", + "/gems/activerecord-2.3.2/lib", + "/gems/actionpack-2.3.2/lib", + "/gems/actionmailer-2.3.2/lib", + "/gems/activesupport-2.3.2/lib", + "/gems/rake-10.0.2/lib" + ) + end + + it "falls back to order the load path alphabetically for backwards compatibility" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "weakling" + gem "duradura" + gem "terranova" + G + + ruby! <<-RUBY + require 'rubygems' + require 'bundler/setup' + puts $LOAD_PATH + RUBY + + load_path = clean_load_path(out.split("\n")) + + expect(load_path).to start_with( + "/gems/weakling-0.0.3/lib", + "/gems/terranova-8/lib", + "/gems/duradura-7.0/lib" + ) + end + end + + it "raises if the Gemfile was not yet installed" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + ruby <<-R + require 'rubygems' + require 'bundler' + + begin + Bundler.setup + puts "FAIL" + rescue Bundler::GemNotFound + puts "WIN" + end + R + + expect(out).to eq("WIN") + end + + it "doesn't create a Gemfile.lock if the setup fails" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + ruby <<-R + require 'rubygems' + require 'bundler' + + Bundler.setup + R + + expect(bundled_app("Gemfile.lock")).not_to exist + end + + it "doesn't change the Gemfile.lock if the setup fails" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + lockfile = File.read(bundled_app("Gemfile.lock")) + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "nosuchgem", "10.0" + G + + ruby <<-R + require 'rubygems' + require 'bundler' + + Bundler.setup + R + + expect(File.read(bundled_app("Gemfile.lock"))).to eq(lockfile) + end + + it "makes a Gemfile.lock if setup succeeds" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + File.read(bundled_app("Gemfile.lock")) + + FileUtils.rm(bundled_app("Gemfile.lock")) + + run "1" + expect(bundled_app("Gemfile.lock")).to exist + end + + it "uses BUNDLE_GEMFILE to locate the gemfile if present" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + gemfile bundled_app("4realz"), <<-G + source "file://#{gem_repo1}" + gem "activesupport", "2.3.5" + G + + ENV["BUNDLE_GEMFILE"] = bundled_app("4realz").to_s + bundle :install + + expect(the_bundle).to include_gems "activesupport 2.3.5" + end + + it "prioritizes gems in BUNDLE_PATH over gems in GEM_HOME" do + ENV["BUNDLE_PATH"] = bundled_app(".bundle").to_s + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0.0" + G + + build_gem "rack", "1.0", :to_system => true do |s| + s.write "lib/rack.rb", "RACK = 'FAIL'" + end + + expect(the_bundle).to include_gems "rack 1.0.0" + end + + describe "integrate with rubygems" do + describe "by replacing #gem" do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "0.9.1" + G + end + + it "replaces #gem but raises when the gem is missing" do + run <<-R + begin + gem "activesupport" + puts "FAIL" + rescue LoadError + puts "WIN" + end + R + + expect(out).to eq("WIN") + end + + it "version_requirement is now deprecated in rubygems 1.4.0+ when gem is missing" do + run <<-R + begin + gem "activesupport" + puts "FAIL" + rescue LoadError + puts "WIN" + end + R + + expect(err).to lack_errors + end + + it "replaces #gem but raises when the version is wrong" do + run <<-R + begin + gem "rack", "1.0.0" + puts "FAIL" + rescue LoadError + puts "WIN" + end + R + + expect(out).to eq("WIN") + end + + it "version_requirement is now deprecated in rubygems 1.4.0+ when the version is wrong" do + run <<-R + begin + gem "rack", "1.0.0" + puts "FAIL" + rescue LoadError + puts "WIN" + end + R + + expect(err).to lack_errors + end + end + + describe "by hiding system gems" do + before :each do + system_gems "activesupport-2.3.5" + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "yard" + G + end + + it "removes system gems from Gem.source_index" do + run "require 'yard'" + expect(out).to eq("bundler-#{Bundler::VERSION}\nyard-1.0") + end + + context "when the ruby stdlib is a substring of Gem.path" do + it "does not reject the stdlib from $LOAD_PATH" do + substring = "/" + $LOAD_PATH.find {|p| p =~ /vendor_ruby/ }.split("/")[2] + run "puts 'worked!'", :env => { "GEM_PATH" => substring } + expect(out).to eq("worked!") + end + end + end + end + + describe "with paths" do + it "activates the gems in the path source" do + system_gems "rack-1.0.0" + + build_lib "rack", "1.0.0" do |s| + s.write "lib/rack.rb", "puts 'WIN'" + end + + gemfile <<-G + path "#{lib_path("rack-1.0.0")}" + source "file://#{gem_repo1}" + gem "rack" + G + + run "require 'rack'" + expect(out).to eq("WIN") + end + end + + describe "with git" do + before do + build_git "rack", "1.0.0" + + gemfile <<-G + gem "rack", :git => "#{lib_path("rack-1.0.0")}" + G + end + + it "provides a useful exception when the git repo is not checked out yet" do + run "1" + expect(err).to match(/the git source #{lib_path('rack-1.0.0')} is not yet checked out. Please run `bundle install`/i) + end + + it "does not hit the git binary if the lockfile is available and up to date" do + bundle "install" + + break_git! + + ruby <<-R + require 'rubygems' + require 'bundler' + + begin + Bundler.setup + puts "WIN" + rescue Exception => e + puts "FAIL" + end + R + + expect(out).to eq("WIN") + end + + it "provides a good exception if the lockfile is unavailable" do + bundle "install" + + FileUtils.rm(bundled_app("Gemfile.lock")) + + break_git! + + ruby <<-R + require "rubygems" + require "bundler" + + begin + Bundler.setup + puts "FAIL" + rescue Bundler::GitError => e + puts e.message + end + R + + run "puts 'FAIL'" + + expect(err).not_to include "This is not the git you are looking for" + end + + it "works even when the cache directory has been deleted" do + bundle "install --path vendor/bundle" + FileUtils.rm_rf vendored_gems("cache") + expect(the_bundle).to include_gems "rack 1.0.0" + end + + it "does not randomly change the path when specifying --path and the bundle directory becomes read only" do + bundle "install --path vendor/bundle" + + with_read_only("**/*") do + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + + it "finds git gem when default bundle path becomes read only" do + bundle "install" + + with_read_only("#{Bundler.bundle_path}/**/*") do + expect(the_bundle).to include_gems "rack 1.0.0" + end + end + end + + describe "when specifying local override" do + it "explodes if given path does not exist on runtime" do + build_git "rack", "0.8" + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle :install + expect(out).to match(/at #{lib_path('local-rack')}/) + + FileUtils.rm_rf(lib_path("local-rack")) + run "require 'rack'" + expect(err).to match(/Cannot use local override for rack-0.8 because #{Regexp.escape(lib_path('local-rack').to_s)} does not exist/) + end + + it "explodes if branch is not given on runtime" do + build_git "rack", "0.8" + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle :install + expect(out).to match(/at #{lib_path('local-rack')}/) + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}" + G + + run "require 'rack'" + expect(err).to match(/because :branch is not specified in Gemfile/) + end + + it "explodes on different branches on runtime" do + build_git "rack", "0.8" + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle :install + expect(out).to match(/at #{lib_path('local-rack')}/) + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "changed" + G + + run "require 'rack'" + expect(err).to match(/is using branch master but Gemfile specifies changed/) + end + + it "explodes on refs with different branches on runtime" do + build_git "rack", "0.8" + + FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :ref => "master", :branch => "master" + G + + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :ref => "master", :branch => "nonexistant" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + run "require 'rack'" + expect(err).to match(/is using branch master but Gemfile specifies nonexistant/) + end + end + + describe "when excluding groups" do + it "doesn't change the resolve if --without is used" do + install_gemfile <<-G, :without => :rails + source "file://#{gem_repo1}" + gem "activesupport" + + group :rails do + gem "rails", "2.3.2" + end + G + + install_gems "activesupport-2.3.5" + + expect(the_bundle).to include_gems "activesupport 2.3.2", :groups => :default + end + + it "remembers --without and does not bail on bare Bundler.setup" do + install_gemfile <<-G, :without => :rails + source "file://#{gem_repo1}" + gem "activesupport" + + group :rails do + gem "rails", "2.3.2" + end + G + + install_gems "activesupport-2.3.5" + + expect(the_bundle).to include_gems "activesupport 2.3.2" + end + + it "remembers --without and does not include groups passed to Bundler.setup" do + install_gemfile <<-G, :without => :rails + source "file://#{gem_repo1}" + gem "activesupport" + + group :rack do + gem "rack" + end + + group :rails do + gem "rails", "2.3.2" + end + G + + expect(the_bundle).not_to include_gems "activesupport 2.3.2", :groups => :rack + expect(the_bundle).to include_gems "rack 1.0.0", :groups => :rack + end + end + + # Unfortunately, gem_prelude does not record the information about + # activated gems, so this test cannot work on 1.9 :( + if RUBY_VERSION < "1.9" + describe "preactivated gems" do + it "raises an exception if a pre activated gem conflicts with the bundle" do + system_gems "thin-1.0", "rack-1.0.0" + build_gem "thin", "1.1", :to_system => true do |s| + s.add_dependency "rack" + end + + gemfile <<-G + gem "thin", "1.0" + G + + ruby <<-R + require 'rubygems' + gem "thin" + require 'bundler' + begin + Bundler.setup + puts "FAIL" + rescue Gem::LoadError => e + puts e.message + end + R + + expect(out).to eq("You have already activated thin 1.1, but your Gemfile requires thin 1.0. Prepending `bundle exec` to your command may solve this.") + end + + it "version_requirement is now deprecated in rubygems 1.4.0+" do + system_gems "thin-1.0", "rack-1.0.0" + build_gem "thin", "1.1", :to_system => true do |s| + s.add_dependency "rack" + end + + gemfile <<-G + gem "thin", "1.0" + G + + ruby <<-R + require 'rubygems' + gem "thin" + require 'bundler' + begin + Bundler.setup + puts "FAIL" + rescue Gem::LoadError => e + puts e.message + end + R + + expect(err).to lack_errors + end + end + end + + # Rubygems returns loaded_from as a string + it "has loaded_from as a string on all specs" do + build_git "foo" + build_git "no-gemspec", :gemspec => false + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "foo", :git => "#{lib_path("foo-1.0")}" + gem "no-gemspec", "1.0", :git => "#{lib_path("no-gemspec-1.0")}" + G + + run <<-R + Gem.loaded_specs.each do |n, s| + puts "FAIL" unless s.loaded_from.is_a?(String) + end + R + + expect(out).to be_empty + end + + it "does not load all gemspecs", :rubygems => ">= 2.3" do + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + run! <<-R + File.open(File.join(Gem.dir, "specifications", "broken.gemspec"), "w") do |f| + f.write <<-RUBY +# -*- encoding: utf-8 -*- +# stub: broken 1.0.0 ruby lib + +Gem::Specification.new do |s| + s.name = "broken" + s.version = "1.0.0" + raise "BROKEN GEMSPEC" +end + RUBY + end + R + + run! <<-R + puts "WIN" + R + + expect(out).to eq("WIN") + end + + it "ignores empty gem paths" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + ENV["GEM_HOME"] = "" + bundle %(exec ruby -e "require 'set'") + + expect(err).to lack_errors + end + + it "should prepend gemspec require paths to $LOAD_PATH in order" do + update_repo2 do + build_gem("requirepaths") do |s| + s.write("lib/rq.rb", "puts 'yay'") + s.write("src/rq.rb", "puts 'nooo'") + s.require_paths = %w(lib src) + end + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + gem "requirepaths", :require => nil + G + + run "require 'rq'" + expect(out).to eq("yay") + end + + it "should clean $LOAD_PATH properly", :ruby_repo do + gem_name = "very_simple_binary" + full_gem_name = gem_name + "-1.0" + ext_dir = File.join(tmp "extenstions", full_gem_name) + + install_gem full_gem_name + + install_gemfile <<-G + source "file://#{gem_repo1}" + G + + ruby <<-R + if Gem::Specification.method_defined? :extension_dir + s = Gem::Specification.find_by_name '#{gem_name}' + s.extension_dir = '#{ext_dir}' + + # Don't build extensions. + s.class.send(:define_method, :build_extensions) { nil } + end + + require 'bundler' + gem '#{gem_name}' + + puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} >= 2 + + Bundler.setup + + puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} == 0 + R + + expect(out).to eq("true\ntrue") + end + + it "stubs out Gem.refresh so it does not reveal system gems" do + system_gems "rack-1.0.0" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + G + + run <<-R + puts Bundler.rubygems.find_name("rack").inspect + Gem.refresh + puts Bundler.rubygems.find_name("rack").inspect + R + + expect(out).to eq("[]\n[]") + end + + describe "when a vendored gem specification uses the :path option" do + it "should resolve paths relative to the Gemfile" do + path = bundled_app(File.join("vendor", "foo")) + build_lib "foo", :path => path + + # If the .gemspec exists, then Bundler handles the path differently. + # See Source::Path.load_spec_files for details. + FileUtils.rm(File.join(path, "foo.gemspec")) + + install_gemfile <<-G + gem 'foo', '1.2.3', :path => 'vendor/foo' + G + + Dir.chdir(bundled_app.parent) do + run <<-R, :env => { "BUNDLE_GEMFILE" => bundled_app("Gemfile") } + require 'foo' + R + end + expect(err).to lack_errors + end + + it "should make sure the Bundler.root is really included in the path relative to the Gemfile" do + relative_path = File.join("vendor", Dir.pwd[1..-1], "foo") + absolute_path = bundled_app(relative_path) + FileUtils.mkdir_p(absolute_path) + build_lib "foo", :path => absolute_path + + # If the .gemspec exists, then Bundler handles the path differently. + # See Source::Path.load_spec_files for details. + FileUtils.rm(File.join(absolute_path, "foo.gemspec")) + + gemfile <<-G + gem 'foo', '1.2.3', :path => '#{relative_path}' + G + + bundle :install + + Dir.chdir(bundled_app.parent) do + run <<-R, :env => { "BUNDLE_GEMFILE" => bundled_app("Gemfile") } + require 'foo' + R + end + + expect(err).to lack_errors + end + end + + describe "with git gems that don't have gemspecs" do + before :each do + build_git "no-gemspec", :gemspec => false + + install_gemfile <<-G + gem "no-gemspec", "1.0", :git => "#{lib_path("no-gemspec-1.0")}" + G + end + + it "loads the library via a virtual spec" do + run <<-R + require 'no-gemspec' + puts NOGEMSPEC + R + + expect(out).to eq("1.0") + end + end + + describe "with bundled and system gems" do + before :each do + system_gems "rack-1.0.0" + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "activesupport", "2.3.5" + G + end + + it "does not pull in system gems" do + run <<-R + require 'rubygems' + + begin; + require 'rack' + rescue LoadError + puts 'WIN' + end + R + + expect(out).to eq("WIN") + end + + it "provides a gem method" do + run <<-R + gem 'activesupport' + require 'activesupport' + puts ACTIVESUPPORT + R + + expect(out).to eq("2.3.5") + end + + it "raises an exception if gem is used to invoke a system gem not in the bundle" do + run <<-R + begin + gem 'rack' + rescue LoadError => e + puts e.message + end + R + + expect(out).to eq("rack is not part of the bundle. Add it to your Gemfile.") + end + + it "sets GEM_HOME appropriately" do + run "puts ENV['GEM_HOME']" + expect(out).to eq(default_bundle_path.to_s) + end + end + + describe "with system gems in the bundle" do + before :each do + system_gems "rack-1.0.0" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0.0" + gem "activesupport", "2.3.5" + G + end + + it "sets GEM_PATH appropriately" do + run "puts Gem.path" + paths = out.split("\n") + expect(paths).to include(system_gem_path.to_s) + expect(paths).to include(default_bundle_path.to_s) + end + end + + describe "with a gemspec that requires other files" do + before :each do + build_git "bar", :gemspec => false do |s| + s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0') + s.write "bar.gemspec", <<-G + lib = File.expand_path('../lib/', __FILE__) + $:.unshift lib unless $:.include?(lib) + require 'bar/version' + + Gem::Specification.new do |s| + s.name = 'bar' + s.version = BAR_VERSION + s.summary = 'Bar' + s.files = Dir["lib/**/*.rb"] + s.author = 'no one' + end + G + end + + gemfile <<-G + gem "bar", :git => "#{lib_path("bar-1.0")}" + G + end + + it "evals each gemspec in the context of its parent directory" do + bundle :install + run "require 'bar'; puts BAR" + expect(out).to eq("1.0") + end + + it "error intelligently if the gemspec has a LoadError" do + ref = update_git "bar", :gemspec => false do |s| + s.write "bar.gemspec", "require 'foobarbaz'" + end.ref_for("HEAD") + bundle :install + + expect(out.lines.map(&:chomp)).to include( + a_string_starting_with("[!] There was an error while loading `bar.gemspec`:"), + RUBY_VERSION >= "1.9" ? a_string_starting_with("Does it try to require a relative path? That's been removed in Ruby 1.9.") : "", + " # from #{default_bundle_path "bundler", "gems", "bar-1.0-#{ref[0, 12]}", "bar.gemspec"}:1", + " > require 'foobarbaz'" + ) + end + + it "evals each gemspec with a binding from the top level" do + bundle "install" + + ruby <<-RUBY + require 'bundler' + def Bundler.require(path) + raise "LOSE" + end + Bundler.load + RUBY + + expect(err).to lack_errors + expect(out).to eq("") + end + end + + describe "when Bundler is bundled" do + it "doesn't blow up" do + install_gemfile <<-G + gem "bundler", :path => "#{File.expand_path("..", lib)}" + G + + bundle %(exec ruby -e "require 'bundler'; Bundler.setup") + expect(err).to lack_errors + end + end + + describe "when BUNDLED WITH" do + def lock_with(bundler_version = nil) + lock = <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + L + + if bundler_version + lock += "\n BUNDLED WITH\n #{bundler_version}\n" + end + + lock + end + + before do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + end + + context "is not present" do + it "does not change the lock" do + lockfile lock_with(nil) + ruby "require 'bundler/setup'" + lockfile_should_be lock_with(nil) + end + end + + context "is newer" do + it "does not change the lock or warn" do + lockfile lock_with(Bundler::VERSION.succ) + ruby "require 'bundler/setup'" + expect(out).to eq("") + expect(err).to eq("") + lockfile_should_be lock_with(Bundler::VERSION.succ) + end + end + + context "is older" do + it "does not change the lock" do + lockfile lock_with("1.10.1") + ruby "require 'bundler/setup'" + lockfile_should_be lock_with("1.10.1") + end + end + end + + describe "when RUBY VERSION" do + let(:ruby_version) { nil } + + def lock_with(ruby_version = nil) + lock = <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rack + L + + if ruby_version + lock += "\n RUBY VERSION\n ruby #{ruby_version}\n" + end + + lock += <<-L + + BUNDLED WITH + #{Bundler::VERSION} + L + + lock + end + + before do + install_gemfile <<-G + ruby ">= 0" + source "file:#{gem_repo1}" + gem "rack" + G + lockfile lock_with(ruby_version) + end + + context "is not present" do + it "does not change the lock" do + expect { ruby! "require 'bundler/setup'" }.not_to change { lockfile } + end + end + + context "is newer" do + let(:ruby_version) { "5.5.5" } + it "does not change the lock or warn" do + expect { ruby! "require 'bundler/setup'" }.not_to change { lockfile } + expect(out).to eq("") + expect(err).to eq("") + end + end + + context "is older" do + let(:ruby_version) { "1.0.0" } + it "does not change the lock" do + expect { ruby! "require 'bundler/setup'" }.not_to change { lockfile } + end + end + end + + describe "with gemified standard libraries" do + it "does not load Psych", :ruby => "~> 2.2" do + gemfile "" + ruby <<-RUBY + require 'bundler/setup' + puts defined?(Psych::VERSION) ? Psych::VERSION : "undefined" + require 'psych' + puts Psych::VERSION + RUBY + pre_bundler, post_bundler = out.split("\n") + expect(pre_bundler).to eq("undefined") + expect(post_bundler).to match(/\d+\.\d+\.\d+/) + end + + it "does not load openssl" do + install_gemfile! "" + ruby! <<-RUBY + require "bundler/setup" + puts defined?(OpenSSL) || "undefined" + require "openssl" + puts defined?(OpenSSL) || "undefined" + RUBY + expect(out).to eq("undefined\nconstant") + end + + describe "default gem activation", :ruby_repo do + let(:exemptions) do + if Gem::Version.new(Gem::VERSION) >= Gem::Version.new("2.7") || ENV["RGV"] == "master" + [] + else + %w(io-console openssl) + end << "bundler" + end + + let(:code) { strip_whitespace(<<-RUBY) } + require "rubygems" + + if Gem::Specification.instance_methods.map(&:to_sym).include?(:activate) + Gem::Specification.send(:alias_method, :bundler_spec_activate, :activate) + Gem::Specification.send(:define_method, :activate) do + unless #{exemptions.inspect}.include?(name) + warn '-' * 80 + warn "activating \#{full_name}" + warn *caller + warn '*' * 80 + end + bundler_spec_activate + end + end + + require "bundler/setup" + require "pp" + loaded_specs = Gem.loaded_specs.dup + #{exemptions.inspect}.each {|s| loaded_specs.delete(s) } + pp loaded_specs + + # not a default gem, but harmful to have loaded + open_uri = $LOADED_FEATURES.grep(/open.uri/) + unless open_uri.empty? + warn "open_uri: \#{open_uri}" + end + RUBY + + it "activates no gems with -rbundler/setup" do + install_gemfile! "" + ruby!(code) + expect(err).to eq("") + expect(out).to eq("{}") + end + + it "activates no gems with bundle exec" do + install_gemfile! "" + create_file("script.rb", code) + bundle! "exec ruby ./script.rb" + expect(err).to eq("") + expect(out).to eq("{}") + end + + it "activates no gems with bundle exec that is loaded" do + # TODO: remove once https://github.com/erikhuda/thor/pull/539 is released + exemptions << "io-console" + + install_gemfile! "" + create_file("script.rb", "#!/usr/bin/env ruby\n\n#{code}") + FileUtils.chmod(0o777, bundled_app("script.rb")) + bundle! "exec ./script.rb", :artifice => nil + expect(err).to eq("") + expect(out).to eq("{}") + end + + let(:default_gems) do + ruby!(<<-RUBY).split("\n") + if Gem::Specification.is_a?(Enumerable) + puts Gem::Specification.select(&:default_gem?).map(&:name) + end + RUBY + end + + it "activates newer versions of default gems" do + build_repo4 do + default_gems.each do |g| + build_gem g, "999999" + end + end + + install_gemfile! <<-G + source "file:#{gem_repo4}" + #{default_gems}.each do |g| + gem g, "999999" + end + G + + expect(the_bundle).to include_gems(*default_gems.map {|g| "#{g} 999999" }) + end + + it "activates older versions of default gems" do + build_repo4 do + default_gems.each do |g| + build_gem g, "0.0.0.a" + end + end + + default_gems.reject! {|g| exemptions.include?(g) } + + install_gemfile! <<-G + source "file:#{gem_repo4}" + #{default_gems}.each do |g| + gem g, "0.0.0.a" + end + G + + expect(the_bundle).to include_gems(*default_gems.map {|g| "#{g} 0.0.0.a" }) + end + end + end + + describe "after setup" do + it "allows calling #gem on random objects" do + install_gemfile <<-G + source "file:#{gem_repo1}" + gem "rack" + G + ruby! <<-RUBY + require "bundler/setup" + Object.new.gem "rack" + puts Gem.loaded_specs["rack"].full_name + RUBY + expect(out).to eq("rack-1.0.0") + end + end +end diff --git a/spec/bundler/runtime/with_clean_env_spec.rb b/spec/bundler/runtime/with_clean_env_spec.rb new file mode 100644 index 0000000000..d18a0de485 --- /dev/null +++ b/spec/bundler/runtime/with_clean_env_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "Bundler.with_env helpers" do + describe "Bundler.original_env" do + before do + gemfile "" + bundle "install --path vendor/bundle" + end + + it "should return the PATH present before bundle was activated", :ruby_repo do + code = "print Bundler.original_env['PATH']" + path = `getconf PATH`.strip + "#{File::PATH_SEPARATOR}/foo" + with_path_as(path) do + result = bundle("exec ruby -e #{code.dump}") + expect(result).to eq(path) + end + end + + it "should return the GEM_PATH present before bundle was activated" do + code = "print Bundler.original_env['GEM_PATH']" + gem_path = ENV["GEM_PATH"] + ":/foo" + with_gem_path_as(gem_path) do + result = bundle("exec ruby -e #{code.inspect}") + expect(result).to eq(gem_path) + end + end + + it "works with nested bundle exec invocations", :ruby_repo do + create_file("exe.rb", <<-'RB') + count = ARGV.first.to_i + exit if count < 0 + STDERR.puts "#{count} #{ENV["PATH"].end_with?(":/foo")}" + if count == 2 + ENV["PATH"] = "#{ENV["PATH"]}:/foo" + end + exec("ruby", __FILE__, (count - 1).to_s) + RB + path = `getconf PATH`.strip + File::PATH_SEPARATOR + File.dirname(Gem.ruby) + with_path_as(path) do + bundle!("exec ruby #{bundled_app("exe.rb")} 2") + end + expect(err).to eq <<-EOS.strip +2 false +1 true +0 true + EOS + end + end + + describe "Bundler.clean_env" do + before do + gemfile "" + bundle "install --path vendor/bundle" + end + + it "should delete BUNDLE_PATH" do + code = "print Bundler.clean_env.has_key?('BUNDLE_PATH')" + ENV["BUNDLE_PATH"] = "./foo" + result = bundle("exec ruby -e #{code.inspect}") + expect(result).to eq("false") + end + + it "should remove '-rbundler/setup' from RUBYOPT" do + code = "print Bundler.clean_env['RUBYOPT']" + ENV["RUBYOPT"] = "-W2 -rbundler/setup" + result = bundle("exec ruby -e #{code.inspect}") + expect(result).not_to include("-rbundler/setup") + end + + it "should clean up RUBYLIB", :ruby_repo do + code = "print Bundler.clean_env['RUBYLIB']" + ENV["RUBYLIB"] = root.join("lib").to_s + File::PATH_SEPARATOR + "/foo" + result = bundle("exec ruby -e #{code.inspect}") + expect(result).to eq("/foo") + end + + it "should restore the original MANPATH" do + code = "print Bundler.clean_env['MANPATH']" + ENV["MANPATH"] = "/foo" + ENV["BUNDLER_ORIG_MANPATH"] = "/foo-original" + result = bundle("exec ruby -e #{code.inspect}") + expect(result).to eq("/foo-original") + end + end + + describe "Bundler.with_original_env" do + it "should set ENV to original_env in the block" do + expected = Bundler.original_env + actual = Bundler.with_original_env { ENV.to_hash } + expect(actual).to eq(expected) + end + + it "should restore the environment after execution" do + Bundler.with_original_env do + ENV["FOO"] = "hello" + end + + expect(ENV).not_to have_key("FOO") + end + end + + describe "Bundler.with_clean_env" do + it "should set ENV to clean_env in the block" do + expected = Bundler.clean_env + actual = Bundler.with_clean_env { ENV.to_hash } + expect(actual).to eq(expected) + end + + it "should restore the environment after execution" do + Bundler.with_clean_env do + ENV["FOO"] = "hello" + end + + expect(ENV).not_to have_key("FOO") + end + end + + describe "Bundler.clean_system", :ruby => ">= 1.9" do + it "runs system inside with_clean_env" do + Bundler.clean_system(%(echo 'if [ "$BUNDLE_PATH" = "" ]; then exit 42; else exit 1; fi' | /bin/sh)) + expect($?.exitstatus).to eq(42) + end + end + + describe "Bundler.clean_exec", :ruby => ">= 1.9" do + it "runs exec inside with_clean_env" do + pid = Kernel.fork do + Bundler.clean_exec(%(echo 'if [ "$BUNDLE_PATH" = "" ]; then exit 42; else exit 1; fi' | /bin/sh)) + end + Process.wait(pid) + expect($?.exitstatus).to eq(42) + end + end +end diff --git a/spec/bundler/spec_helper.rb b/spec/bundler/spec_helper.rb new file mode 100644 index 0000000000..7293f0e57b --- /dev/null +++ b/spec/bundler/spec_helper.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true +$:.unshift File.expand_path("..", __FILE__) +$:.unshift File.expand_path("../../lib", __FILE__) + +require "bundler/psyched_yaml" +require "fileutils" +require "uri" +require "digest/sha1" +require File.expand_path("../support/path.rb", __FILE__) + +begin + require "rubygems" + spec = Gem::Specification.load(Spec::Path.gemspec.to_s) + rspec = spec.dependencies.find {|d| d.name == "rspec" } + gem "rspec", rspec.requirement.to_s + require "rspec" +rescue LoadError + abort "Run rake spec:deps to install development dependencies" +end + +if File.expand_path(__FILE__) =~ %r{([^\w/\.])} + abort "The bundler specs cannot be run from a path that contains special characters (particularly #{$1.inspect})" +end + +require "bundler" + +# Require the correct version of popen for the current platform +if RbConfig::CONFIG["host_os"] =~ /mingw|mswin/ + begin + require "win32/open3" + rescue LoadError + abort "Run `gem install win32-open3` to be able to run specs" + end +else + require "open3" +end + +Dir["#{File.expand_path("../support", __FILE__)}/*.rb"].each do |file| + require file unless file.end_with?("hax.rb") +end + +$debug = false + +Spec::Rubygems.setup +FileUtils.rm_rf(Spec::Path.gem_repo1) +ENV["RUBYOPT"] = "#{ENV["RUBYOPT"]} -r#{Spec::Path.spec_dir}/support/hax.rb" +ENV["BUNDLE_SPEC_RUN"] = "true" +ENV["BUNDLE_PLUGINS"] = "true" + +# Don't wrap output in tests +ENV["THOR_COLUMNS"] = "10000" + +Spec::CodeClimate.setup + +module Gem + def self.ruby= ruby + @ruby = ruby + end +end + +RSpec.configure do |config| + config.include Spec::Builders + config.include Spec::Helpers + config.include Spec::Indexes + config.include Spec::Matchers + config.include Spec::Path + config.include Spec::Rubygems + config.include Spec::Platforms + config.include Spec::Sudo + config.include Spec::Permissions + + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = ".rspec_status" + + config.disable_monkey_patching! + + # Since failures cause us to keep a bunch of long strings in memory, stop + # once we have a large number of failures (indicative of core pieces of + # bundler being broken) so that running the full test suite doesn't take + # forever due to memory constraints + config.fail_fast ||= 25 + + if ENV["BUNDLER_SUDO_TESTS"] && Spec::Sudo.present? + config.filter_run :sudo => true + else + config.filter_run_excluding :sudo => true + end + + if ENV["BUNDLER_REALWORLD_TESTS"] + config.filter_run :realworld => true + else + config.filter_run_excluding :realworld => true + end + + git_version = Bundler::Source::Git::GitProxy.new(nil, nil, nil).version + + config.filter_run_excluding :ruby => LessThanProc.with(RUBY_VERSION) + config.filter_run_excluding :rubygems => LessThanProc.with(Gem::VERSION) + config.filter_run_excluding :git => LessThanProc.with(git_version) + config.filter_run_excluding :rubygems_master => (ENV["RGV"] != "master") + config.filter_run_excluding :ruby_repo => !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + + config.filter_run_when_matching :focus unless ENV["CI"] + + original_wd = Dir.pwd + original_env = ENV.to_hash + + config.expect_with :rspec do |c| + c.syntax = :expect + end + + config.before :suite do + @orig_ruby = if ENV['BUNDLE_RUBY'] + ruby = Gem.ruby + Gem.ruby = ENV['BUNDLE_RUBY'] + ruby + end + end + + config.before :all do + build_repo1 + # HACK: necessary until rspec-mocks > 3.5.0 is used + # see https://github.com/bundler/bundler/pull/5363#issuecomment-278089256 + if RUBY_VERSION < "1.9" + FileUtils.module_eval do + alias_method :mkpath, :mkdir_p + module_function :mkpath + end + end + end + + config.before :each do + reset! + system_gems [] + in_app_root + @all_output = String.new + end + + config.after :each do |example| + @all_output.strip! + if example.exception && !@all_output.empty? + warn @all_output unless config.formatters.grep(RSpec::Core::Formatters::DocumentationFormatter).empty? + message = example.exception.message + "\n\nCommands:\n#{@all_output}" + (class << example.exception; self; end).send(:define_method, :message) do + message + end + end + + Dir.chdir(original_wd) + ENV.replace(original_env) + end + + config.after :suite do + Gem.ruby = @orig_ruby + end +end diff --git a/spec/bundler/support/artifice/compact_index.rb b/spec/bundler/support/artifice/compact_index.rb new file mode 100644 index 0000000000..9111ed8211 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +$LOAD_PATH.unshift Dir[base_system_gems.join("gems/compact_index*/lib")].first.to_s +require "compact_index" + +class CompactIndexAPI < Endpoint + helpers do + def load_spec(name, version, platform, gem_repo) + full_name = "#{name}-#{version}" + full_name += "-#{platform}" if platform != "ruby" + Marshal.load(Gem.inflate(File.open(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")).read)) + end + + def etag_response + response_body = yield + checksum = Digest::MD5.hexdigest(response_body) + return if not_modified?(checksum) + headers "ETag" => quote(checksum) + headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" + content_type "text/plain" + requested_range_for(response_body) + rescue => e + puts e + puts e.backtrace + raise + end + + def not_modified?(checksum) + etags = parse_etags(request.env["HTTP_IF_NONE_MATCH"]) + + return unless etags.include?(checksum) + headers "ETag" => quote(checksum) + status 304 + body "" + end + + def requested_range_for(response_body) + ranges = Rack::Utils.byte_ranges(env, response_body.bytesize) + + if ranges + status 206 + body ranges.map! {|range| slice_body(response_body, range) }.join + else + status 200 + body response_body + end + end + + def quote(string) + %("#{string}") + end + + def parse_etags(value) + value ? value.split(/, ?/).select {|s| s.sub!(/"(.*)"/, '\1') } : [] + end + + def slice_body(body, range) + if body.respond_to?(:byteslice) + body.byteslice(range) + else # pre-1.9.3 + body.unpack("@#{range.first}a#{range.end + 1}").first + end + end + + def gems(gem_repo = GEM_REPO) + @gems ||= {} + @gems[gem_repo] ||= begin + specs = Bundler::Deprecate.skip_during do + %w(specs.4.8 prerelease_specs.4.8).map do |filename| + Marshal.load(File.open(gem_repo.join(filename)).read).map do |name, version, platform| + load_spec(name, version, platform, gem_repo) + end + end.flatten + end + + specs.group_by(&:name).map do |name, versions| + gem_versions = versions.map do |spec| + deps = spec.dependencies.select {|d| d.type == :runtime }.map do |d| + reqs = d.requirement.requirements.map {|r| r.join(" ") }.join(", ") + CompactIndex::Dependency.new(d.name, reqs) + end + checksum = begin + Digest::SHA256.file("#{GEM_REPO}/gems/#{spec.original_name}.gem").base64digest + rescue + nil + end + CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, checksum, nil, + deps, spec.required_ruby_version, spec.required_rubygems_version) + end + CompactIndex::Gem.new(name, gem_versions) + end + end + end + end + + get "/names" do + etag_response do + CompactIndex.names(gems.map(&:name)) + end + end + + get "/versions" do + etag_response do + file = tmp("versions.list") + file.delete if file.file? + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + file.contents + end + end + + get "/info/:name" do + etag_response do + gem = gems.find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end +end + +Artifice.activate_with(CompactIndexAPI) diff --git a/spec/bundler/support/artifice/compact_index_api_missing.rb b/spec/bundler/support/artifice/compact_index_api_missing.rb new file mode 100644 index 0000000000..6d15b54b85 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_api_missing.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexApiMissing < CompactIndexAPI + get "/fetch/actual/gem/:id" do + $stderr.puts params[:id] + if params[:id] == "rack-1.0.gemspec.rz" + halt 404 + else + File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + end +end + +Artifice.activate_with(CompactIndexApiMissing) diff --git a/spec/bundler/support/artifice/compact_index_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_basic_authentication.rb new file mode 100644 index 0000000000..bffb5b9e2b --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_basic_authentication.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexBasicAuthentication < CompactIndexAPI + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + end +end + +Artifice.activate_with(CompactIndexBasicAuthentication) diff --git a/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb new file mode 100644 index 0000000000..4ac328736c --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexChecksumMismatch < CompactIndexAPI + get "/versions" do + headers "ETag" => quote("123") + headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" + content_type "text/plain" + body "" + end +end + +Artifice.activate_with(CompactIndexChecksumMismatch) diff --git a/spec/bundler/support/artifice/compact_index_concurrent_download.rb b/spec/bundler/support/artifice/compact_index_concurrent_download.rb new file mode 100644 index 0000000000..b788a852cf --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_concurrent_download.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexConcurrentDownload < CompactIndexAPI + get "/versions" do + versions = File.join(Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions") + + # Verify the original (empty) content hasn't been deleted, e.g. on a retry + File.read(versions) == "" || raise("Original file should be present and empty") + + # Verify this is only requested once for a partial download + env["HTTP_RANGE"] || raise("Missing Range header for expected partial download") + + # Overwrite the file in parallel, which should be then overwritten + # after a successful download to prevent corruption + File.open(versions, "w") {|f| f.puts "another process" } + + etag_response do + file = tmp("versions.list") + file.delete if file.file? + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + file.contents + end + end +end + +Artifice.activate_with(CompactIndexConcurrentDownload) diff --git a/spec/bundler/support/artifice/compact_index_creds_diff_host.rb b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb new file mode 100644 index 0000000000..0c417f0580 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexCredsDiffHost < CompactIndexAPI + helpers do + def auth + @auth ||= Rack::Auth::Basic::Request.new(request.env) + end + + def authorized? + auth.provided? && auth.basic? && auth.credentials && auth.credentials == %w(user pass) + end + + def protected! + return if authorized? + response["WWW-Authenticate"] = %(Basic realm="Testing HTTP Auth") + throw(:halt, [401, "Not authorized\n"]) + end + end + + before do + protected! unless request.path_info.include?("/no/creds/") + end + + get "/gems/:id" do + redirect "http://diffhost.com/no/creds/#{params[:id]}" + end + + get "/no/creds/:id" do + if request.host.include?("diffhost") && !auth.provided? + File.read("#{gem_repo1}/gems/#{params[:id]}") + end + end +end + +Artifice.activate_with(CompactIndexCredsDiffHost) diff --git a/spec/bundler/support/artifice/compact_index_extra.rb b/spec/bundler/support/artifice/compact_index_extra.rb new file mode 100644 index 0000000000..8a87fc4343 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_extra.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexExtra < CompactIndexAPI + get "/extra/versions" do + halt 404 + end + + get "/extra/api/v1/dependencies" do + halt 404 + end + + get "/extra/specs.4.8.gz" do + File.read("#{gem_repo2}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.read("#{gem_repo2}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.read("#{gem_repo2}/gems/#{params[:id]}") + end +end + +Artifice.activate_with(CompactIndexExtra) diff --git a/spec/bundler/support/artifice/compact_index_extra_api.rb b/spec/bundler/support/artifice/compact_index_extra_api.rb new file mode 100644 index 0000000000..844a9ca9f2 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_extra_api.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexExtraApi < CompactIndexAPI + get "/extra/names" do + etag_response do + CompactIndex.names(gems(gem_repo4).map(&:name)) + end + end + + get "/extra/versions" do + etag_response do + file = tmp("versions.list") + file.delete if file.file? + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems(gem_repo4)) + file.contents + end + end + + get "/extra/info/:name" do + etag_response do + gem = gems(gem_repo4).find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end + + get "/extra/specs.4.8.gz" do + File.read("#{gem_repo4}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.read("#{gem_repo4}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.read("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.read("#{gem_repo4}/gems/#{params[:id]}") + end +end + +Artifice.activate_with(CompactIndexExtraApi) diff --git a/spec/bundler/support/artifice/compact_index_extra_missing.rb b/spec/bundler/support/artifice/compact_index_extra_missing.rb new file mode 100644 index 0000000000..2af5ce9c27 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_extra_missing.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index_extra", __FILE__) + +Artifice.deactivate + +class CompactIndexExtraMissing < CompactIndexExtra + get "/extra/fetch/actual/gem/:id" do + if params[:id] == "missing-1.0.gemspec.rz" + halt 404 + else + File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + end +end + +Artifice.activate_with(CompactIndexExtraMissing) diff --git a/spec/bundler/support/artifice/compact_index_forbidden.rb b/spec/bundler/support/artifice/compact_index_forbidden.rb new file mode 100644 index 0000000000..b25eea94e7 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_forbidden.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexForbidden < CompactIndexAPI + get "/versions" do + halt 403 + end +end + +Artifice.activate_with(CompactIndexForbidden) diff --git a/spec/bundler/support/artifice/compact_index_host_redirect.rb b/spec/bundler/support/artifice/compact_index_host_redirect.rb new file mode 100644 index 0000000000..6c1ab2def6 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_host_redirect.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexHostRedirect < CompactIndexAPI + get "/fetch/actual/gem/:id", :host_name => "localgemserver.test" do + redirect "http://bundler.localgemserver.test#{request.path_info}" + end + + get "/versions" do + status 404 + end + + get "/api/v1/dependencies" do + status 404 + end +end + +Artifice.activate_with(CompactIndexHostRedirect) diff --git a/spec/bundler/support/artifice/compact_index_partial_update.rb b/spec/bundler/support/artifice/compact_index_partial_update.rb new file mode 100644 index 0000000000..bf6feab877 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_partial_update.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexPartialUpdate < CompactIndexAPI + # Stub the server to never return 304s. This simulates the behaviour of + # Fastly / Rubygems ignoring ETag headers. + def not_modified?(_checksum) + false + end + + get "/versions" do + cached_versions_path = File.join( + Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions" + ) + + # Verify a cached copy of the versions file exists + unless File.read(cached_versions_path).start_with?("created_at: ") + raise("Cached versions file should be present and have content") + end + + # Verify that a partial request is made, starting from the index of the + # final byte of the cached file. + unless env["HTTP_RANGE"] == "bytes=#{File.read(cached_versions_path).bytesize - 1}-" + raise("Range header should be present, and start from the index of the final byte of the cache.") + end + + etag_response do + # Return the exact contents of the cache. + File.read(cached_versions_path) + end + end +end + +Artifice.activate_with(CompactIndexPartialUpdate) diff --git a/spec/bundler/support/artifice/compact_index_redirects.rb b/spec/bundler/support/artifice/compact_index_redirects.rb new file mode 100644 index 0000000000..ff1d3e43bc --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_redirects.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexRedirect < CompactIndexAPI + get "/fetch/actual/gem/:id" do + redirect "/fetch/actual/gem/#{params[:id]}" + end + + get "/versions" do + status 404 + end + + get "/api/v1/dependencies" do + status 404 + end +end + +Artifice.activate_with(CompactIndexRedirect) diff --git a/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb new file mode 100644 index 0000000000..49a072d2b9 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexStrictBasicAuthentication < CompactIndexAPI + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + + # Only accepts password == "password" + unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz" + halt 403, "Authentication failed" + end + end +end + +Artifice.activate_with(CompactIndexStrictBasicAuthentication) diff --git a/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb new file mode 100644 index 0000000000..25935f5e5d --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexWrongDependencies < CompactIndexAPI + get "/info/:name" do + etag_response do + gem = gems.find {|g| g.name == params[:name] } + gem.versions.each {|gv| gv.dependencies.clear } if gem + CompactIndex.info(gem ? gem.versions : []) + end + end +end + +Artifice.activate_with(CompactIndexWrongDependencies) diff --git a/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb new file mode 100644 index 0000000000..3a12a59ae7 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexWrongGemChecksum < CompactIndexAPI + get "/info/:name" do + etag_response do + name = params[:name] + gem = gems.find {|g| g.name == name } + checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") { "ab" * 22 } + versions = gem ? gem.versions : [] + versions.each {|v| v.checksum = checksum } + CompactIndex.info(versions) + end + end +end + +Artifice.activate_with(CompactIndexWrongGemChecksum) diff --git a/spec/bundler/support/artifice/endopint_marshal_fail_basic_authentication.rb b/spec/bundler/support/artifice/endopint_marshal_fail_basic_authentication.rb new file mode 100644 index 0000000000..f1f8dc5700 --- /dev/null +++ b/spec/bundler/support/artifice/endopint_marshal_fail_basic_authentication.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint_marshal_fail", __FILE__) + +Artifice.deactivate + +class EndpointMarshalFailBasicAuthentication < EndpointMarshalFail + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + end +end + +Artifice.activate_with(EndpointMarshalFailBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint.rb b/spec/bundler/support/artifice/endpoint.rb new file mode 100644 index 0000000000..771d431f22 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true +require File.expand_path("../../path.rb", __FILE__) +require Spec::Path.root.join("lib/bundler/deprecate") +include Spec::Path + +$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gems.join("gems/{artifice,rack,tilt,sinatra}-*/lib")].map(&:to_s)) +require "artifice" +require "sinatra/base" + +class Endpoint < Sinatra::Base + GEM_REPO = Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"] || Spec::Path.gem_repo1) + set :raise_errors, true + set :show_exceptions, false + + helpers do + def dependencies_for(gem_names, gem_repo = GEM_REPO) + return [] if gem_names.nil? || gem_names.empty? + + require "rubygems" + require "bundler" + Bundler::Deprecate.skip_during do + all_specs = %w(specs.4.8 prerelease_specs.4.8).map do |filename| + Marshal.load(File.open(gem_repo.join(filename)).read) + end.inject(:+) + + all_specs.map do |name, version, platform| + spec = load_spec(name, version, platform, gem_repo) + next unless gem_names.include?(spec.name) + { + :name => spec.name, + :number => spec.version.version, + :platform => spec.platform.to_s, + :dependencies => spec.dependencies.select {|dep| dep.type == :runtime }.map do |dep| + [dep.name, dep.requirement.requirements.map {|a| a.join(" ") }.join(", ")] + end + } + end.compact + end + end + + def load_spec(name, version, platform, gem_repo) + full_name = "#{name}-#{version}" + full_name += "-#{platform}" if platform != "ruby" + Marshal.load(Gem.inflate(File.open(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")).read)) + end + end + + get "/quick/Marshal.4.8/:id" do + redirect "/fetch/actual/gem/#{params[:id]}" + end + + get "/fetch/actual/gem/:id" do + File.read("#{GEM_REPO}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/gems/:id" do + File.read("#{GEM_REPO}/gems/#{params[:id]}") + end + + get "/api/v1/dependencies" do + Marshal.dump(dependencies_for(params[:gems])) + end + + get "/specs.4.8.gz" do + File.read("#{GEM_REPO}/specs.4.8.gz") + end + + get "/prerelease_specs.4.8.gz" do + File.read("#{GEM_REPO}/prerelease_specs.4.8.gz") + end +end + +Artifice.activate_with(Endpoint) diff --git a/spec/bundler/support/artifice/endpoint_500.rb b/spec/bundler/support/artifice/endpoint_500.rb new file mode 100644 index 0000000000..993630b79e --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_500.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +require File.expand_path("../../path.rb", __FILE__) +include Spec::Path + +$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gems.join("gems/{artifice,rack,tilt,sinatra}-*/lib")].map(&:to_s)) + +require "artifice" +require "sinatra/base" + +Artifice.deactivate + +class Endpoint500 < Sinatra::Base + before do + halt 500 + end +end + +Artifice.activate_with(Endpoint500) diff --git a/spec/bundler/support/artifice/endpoint_api_forbidden.rb b/spec/bundler/support/artifice/endpoint_api_forbidden.rb new file mode 100644 index 0000000000..21ad9117ed --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_api_forbidden.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointApiForbidden < Endpoint + get "/api/v1/dependencies" do + halt 403 + end +end + +Artifice.activate_with(EndpointApiForbidden) diff --git a/spec/bundler/support/artifice/endpoint_api_missing.rb b/spec/bundler/support/artifice/endpoint_api_missing.rb new file mode 100644 index 0000000000..6f5b5f1323 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_api_missing.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointApiMissing < Endpoint + get "/fetch/actual/gem/:id" do + $stderr.puts params[:id] + if params[:id] == "rack-1.0.gemspec.rz" + halt 404 + else + File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + end +end + +Artifice.activate_with(EndpointApiMissing) diff --git a/spec/bundler/support/artifice/endpoint_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_basic_authentication.rb new file mode 100644 index 0000000000..9fafd51a3d --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_basic_authentication.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointBasicAuthentication < Endpoint + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + end +end + +Artifice.activate_with(EndpointBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_creds_diff_host.rb b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb new file mode 100644 index 0000000000..cd152848fe --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointCredsDiffHost < Endpoint + helpers do + def auth + @auth ||= Rack::Auth::Basic::Request.new(request.env) + end + + def authorized? + auth.provided? && auth.basic? && auth.credentials && auth.credentials == %w(user pass) + end + + def protected! + return if authorized? + response["WWW-Authenticate"] = %(Basic realm="Testing HTTP Auth") + throw(:halt, [401, "Not authorized\n"]) + end + end + + before do + protected! unless request.path_info.include?("/no/creds/") + end + + get "/gems/:id" do + redirect "http://diffhost.com/no/creds/#{params[:id]}" + end + + get "/no/creds/:id" do + if request.host.include?("diffhost") && !auth.provided? + File.read("#{gem_repo1}/gems/#{params[:id]}") + end + end +end + +Artifice.activate_with(EndpointCredsDiffHost) diff --git a/spec/bundler/support/artifice/endpoint_extra.rb b/spec/bundler/support/artifice/endpoint_extra.rb new file mode 100644 index 0000000000..ed4e87e65f --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_extra.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointExtra < Endpoint + get "/extra/api/v1/dependencies" do + halt 404 + end + + get "/extra/specs.4.8.gz" do + File.read("#{gem_repo2}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.read("#{gem_repo2}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.read("#{gem_repo2}/gems/#{params[:id]}") + end +end + +Artifice.activate_with(EndpointExtra) diff --git a/spec/bundler/support/artifice/endpoint_extra_api.rb b/spec/bundler/support/artifice/endpoint_extra_api.rb new file mode 100644 index 0000000000..1e9e1dc60d --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_extra_api.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointExtraApi < Endpoint + get "/extra/api/v1/dependencies" do + deps = dependencies_for(params[:gems], gem_repo4) + Marshal.dump(deps) + end + + get "/extra/specs.4.8.gz" do + File.read("#{gem_repo4}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.read("#{gem_repo4}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.read("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.read("#{gem_repo4}/gems/#{params[:id]}") + end +end + +Artifice.activate_with(EndpointExtraApi) diff --git a/spec/bundler/support/artifice/endpoint_extra_missing.rb b/spec/bundler/support/artifice/endpoint_extra_missing.rb new file mode 100644 index 0000000000..dc79705a26 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_extra_missing.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint_extra", __FILE__) + +Artifice.deactivate + +class EndpointExtraMissing < EndpointExtra + get "/extra/fetch/actual/gem/:id" do + if params[:id] == "missing-1.0.gemspec.rz" + halt 404 + else + File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + end +end + +Artifice.activate_with(EndpointExtraMissing) diff --git a/spec/bundler/support/artifice/endpoint_fallback.rb b/spec/bundler/support/artifice/endpoint_fallback.rb new file mode 100644 index 0000000000..8a85a41784 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_fallback.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointFallback < Endpoint + DEPENDENCY_LIMIT = 60 + + get "/api/v1/dependencies" do + if params[:gems] && params[:gems].size <= DEPENDENCY_LIMIT + Marshal.dump(dependencies_for(params[:gems])) + else + halt 413, "Too many gems to resolve, please request less than #{DEPENDENCY_LIMIT} gems" + end + end +end + +Artifice.activate_with(EndpointFallback) diff --git a/spec/bundler/support/artifice/endpoint_host_redirect.rb b/spec/bundler/support/artifice/endpoint_host_redirect.rb new file mode 100644 index 0000000000..250416d8cc --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_host_redirect.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointHostRedirect < Endpoint + get "/fetch/actual/gem/:id", :host_name => "localgemserver.test" do + redirect "http://bundler.localgemserver.test#{request.path_info}" + end + + get "/api/v1/dependencies" do + status 404 + end +end + +Artifice.activate_with(EndpointHostRedirect) diff --git a/spec/bundler/support/artifice/endpoint_marshal_fail.rb b/spec/bundler/support/artifice/endpoint_marshal_fail.rb new file mode 100644 index 0000000000..0fb75ebf31 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_marshal_fail.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint_fallback", __FILE__) + +Artifice.deactivate + +class EndpointMarshalFail < EndpointFallback + get "/api/v1/dependencies" do + "f0283y01hasf" + end +end + +Artifice.activate_with(EndpointMarshalFail) diff --git a/spec/bundler/support/artifice/endpoint_mirror_source.rb b/spec/bundler/support/artifice/endpoint_mirror_source.rb new file mode 100644 index 0000000000..9fb58ecb29 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_mirror_source.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +class EndpointMirrorSource < Endpoint + get "/gems/:id" do + if request.env["HTTP_X_GEMFILE_SOURCE"] == "https://server.example.org/" + File.read("#{gem_repo1}/gems/#{params[:id]}") + else + halt 500 + end + end +end + +Artifice.activate_with(EndpointMirrorSource) diff --git a/spec/bundler/support/artifice/endpoint_redirect.rb b/spec/bundler/support/artifice/endpoint_redirect.rb new file mode 100644 index 0000000000..f80d7600c2 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_redirect.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointRedirect < Endpoint + get "/fetch/actual/gem/:id" do + redirect "/fetch/actual/gem/#{params[:id]}" + end + + get "/api/v1/dependencies" do + status 404 + end +end + +Artifice.activate_with(EndpointRedirect) diff --git a/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb new file mode 100644 index 0000000000..4b32cbbf5b --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointStrictBasicAuthentication < Endpoint + before do + unless env["HTTP_AUTHORIZATION"] + halt 401, "Authentication info not supplied" + end + + # Only accepts password == "password" + unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz" + halt 403, "Authentication failed" + end + end +end + +Artifice.activate_with(EndpointStrictBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_timeout.rb b/spec/bundler/support/artifice/endpoint_timeout.rb new file mode 100644 index 0000000000..b15650f226 --- /dev/null +++ b/spec/bundler/support/artifice/endpoint_timeout.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require File.expand_path("../endpoint_fallback", __FILE__) + +Artifice.deactivate + +class EndpointTimeout < EndpointFallback + SLEEP_TIMEOUT = 15 + + get "/api/v1/dependencies" do + sleep(SLEEP_TIMEOUT) + end +end + +Artifice.activate_with(EndpointTimeout) diff --git a/spec/bundler/support/artifice/fail.rb b/spec/bundler/support/artifice/fail.rb new file mode 100644 index 0000000000..1059c6df4e --- /dev/null +++ b/spec/bundler/support/artifice/fail.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "net/http" +begin + require "net/https" +rescue LoadError + nil # net/https or openssl +end + +# We can't use artifice here because it uses rack + +module Artifice; end # for < 2.0, Net::HTTP::Persistent::SSLReuse + +class Fail < Net::HTTP + # Net::HTTP uses a @newimpl instance variable to decide whether + # to use a legacy implementation. Since we are subclassing + # Net::HTTP, we must set it + @newimpl = true + + def request(req, body = nil, &block) + raise(exception(req)) + end + + # Ensure we don't start a connect here + def connect + end + + def exception(req) + name = ENV.fetch("BUNDLER_SPEC_EXCEPTION") { "Errno::ENETUNREACH" } + const = name.split("::").reduce(Object) {|mod, sym| mod.const_get(sym) } + const.new("host down: Bundler spec artifice fail! #{req["PATH_INFO"]}") + end +end + +# Replace Net::HTTP with our failing subclass +::Net.class_eval do + remove_const(:HTTP) + const_set(:HTTP, ::Fail) +end diff --git a/spec/bundler/support/artifice/windows.rb b/spec/bundler/support/artifice/windows.rb new file mode 100644 index 0000000000..c18ca454ec --- /dev/null +++ b/spec/bundler/support/artifice/windows.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true +require File.expand_path("../../path.rb", __FILE__) +include Spec::Path + +$LOAD_PATH.unshift Dir[base_system_gems.join("gems/artifice*/lib")].first.to_s +$LOAD_PATH.unshift(*Dir[base_system_gems.join("gems/rack-*/lib")]) +$LOAD_PATH.unshift Dir[base_system_gems.join("gems/tilt*/lib")].first.to_s +$LOAD_PATH.unshift Dir[base_system_gems.join("gems/sinatra*/lib")].first.to_s +require "artifice" +require "sinatra/base" + +Artifice.deactivate + +class Windows < Sinatra::Base + set :raise_errors, true + set :show_exceptions, false + + helpers do + def gem_repo + Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"] || Spec::Path.gem_repo1) + end + end + + files = ["specs.4.8.gz", + "prerelease_specs.4.8.gz", + "quick/Marshal.4.8/rcov-1.0-mswin32.gemspec.rz", + "gems/rcov-1.0-mswin32.gem"] + + files.each do |file| + get "/#{file}" do + File.read gem_repo.join(file) + end + end + + get "/gems/rcov-1.0-x86-mswin32.gem" do + halt 404 + end + + get "/api/v1/dependencies" do + halt 404 + end + + get "/versions" do + halt 500 + end +end + +Artifice.activate_with(Windows) diff --git a/spec/bundler/support/builders.rb b/spec/bundler/support/builders.rb new file mode 100644 index 0000000000..db128d497b --- /dev/null +++ b/spec/bundler/support/builders.rb @@ -0,0 +1,806 @@ +# frozen_string_literal: true +require "bundler/shared_helpers" +require "shellwords" + +module Spec + module Builders + def self.constantize(name) + name.delete("-").upcase + end + + def v(version) + Gem::Version.new(version) + end + + def pl(platform) + Gem::Platform.new(platform) + end + + def build_repo1 + build_repo gem_repo1 do + build_gem "rack", %w(0.9.1 1.0.0) do |s| + s.executables = "rackup" + s.post_install_message = "Rack's post install message" + end + + build_gem "thin" do |s| + s.add_dependency "rack" + s.post_install_message = "Thin's post install message" + end + + build_gem "rack-obama" do |s| + s.add_dependency "rack" + s.post_install_message = "Rack-obama's post install message" + end + + build_gem "rack_middleware", "1.0" do |s| + s.add_dependency "rack", "0.9.1" + end + + build_gem "rails", "2.3.2" do |s| + s.executables = "rails" + s.add_dependency "rake", "10.0.2" + s.add_dependency "actionpack", "2.3.2" + s.add_dependency "activerecord", "2.3.2" + s.add_dependency "actionmailer", "2.3.2" + s.add_dependency "activeresource", "2.3.2" + end + build_gem "actionpack", "2.3.2" do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "activerecord", ["2.3.1", "2.3.2"] do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "actionmailer", "2.3.2" do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "activeresource", "2.3.2" do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "activesupport", %w(1.2.3 2.3.2 2.3.5) + + build_gem "activemerchant" do |s| + s.add_dependency "activesupport", ">= 2.0.0" + end + + build_gem "rails_fail" do |s| + s.add_dependency "activesupport", "= 1.2.3" + end + + build_gem "missing_dep" do |s| + s.add_dependency "not_here" + end + + build_gem "rspec", "1.2.7", :no_default => true do |s| + s.write "lib/spec.rb", "SPEC = '1.2.7'" + end + + build_gem "rack-test", :no_default => true do |s| + s.write "lib/rack/test.rb", "RACK_TEST = '1.0'" + end + + build_gem "platform_specific" do |s| + s.platform = Bundler.local_platform + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 #{Bundler.local_platform}'" + end + + build_gem "platform_specific" do |s| + s.platform = "java" + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 JAVA'" + end + + build_gem "platform_specific" do |s| + s.platform = "ruby" + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 RUBY'" + end + + build_gem "platform_specific" do |s| + s.platform = "x86-mswin32" + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 MSWIN'" + end + + build_gem "platform_specific" do |s| + s.platform = "x86-darwin-100" + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 x86-darwin-100'" + end + + build_gem "only_java", "1.0" do |s| + s.platform = "java" + s.write "lib/only_java.rb", "ONLY_JAVA = '1.0.0 JAVA'" + end + + build_gem "only_java", "1.1" do |s| + s.platform = "java" + s.write "lib/only_java.rb", "ONLY_JAVA = '1.1.0 JAVA'" + end + + build_gem "nokogiri", "1.4.2" + build_gem "nokogiri", "1.4.2" do |s| + s.platform = "java" + s.write "lib/nokogiri.rb", "NOKOGIRI = '1.4.2 JAVA'" + s.add_dependency "weakling", ">= 0.0.3" + end + + build_gem "laduradura", "5.15.2" + build_gem "laduradura", "5.15.2" do |s| + s.platform = "java" + s.write "lib/laduradura.rb", "LADURADURA = '5.15.2 JAVA'" + end + build_gem "laduradura", "5.15.3" do |s| + s.platform = "java" + s.write "lib/laduradura.rb", "LADURADURA = '5.15.2 JAVA'" + end + + build_gem "weakling", "0.0.3" + + build_gem "terranova", "8" + + build_gem "duradura", "7.0" + + build_gem "multiple_versioned_deps" do |s| + s.add_dependency "weakling", ">= 0.0.1", "< 0.1" + end + + build_gem "not_released", "1.0.pre" + + build_gem "has_prerelease", "1.0" + build_gem "has_prerelease", "1.1.pre" + + build_gem "with_development_dependency" do |s| + s.add_development_dependency "activesupport", "= 2.3.5" + end + + build_gem "with_license" do |s| + s.license = "MIT" + end + + build_gem "with_implicit_rake_dep" do |s| + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("../lib", __FILE__) + FileUtils.mkdir_p(path) + File.open("\#{path}/implicit_rake_dep.rb", "w") do |f| + f.puts "IMPLICIT_RAKE_DEP = 'YES'" + end + end + RUBY + end + + build_gem "another_implicit_rake_dep" do |s| + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("../lib", __FILE__) + FileUtils.mkdir_p(path) + File.open("\#{path}/another_implicit_rake_dep.rb", "w") do |f| + f.puts "ANOTHER_IMPLICIT_RAKE_DEP = 'YES'" + end + end + RUBY + end + + build_gem "very_simple_binary", &:add_c_extension + + build_gem "bundler", "0.9" do |s| + s.executables = "bundle" + s.write "bin/bundle", "puts 'FAIL'" + end + + # The bundler 0.8 gem has a rubygems plugin that always loads :( + build_gem "bundler", "0.8.1" do |s| + s.write "lib/bundler/omg.rb", "" + s.write "lib/rubygems_plugin.rb", "require 'bundler/omg' ; puts 'FAIL'" + end + + build_gem "bundler_dep" do |s| + s.add_dependency "bundler" + end + + # The yard gem iterates over Gem.source_index looking for plugins + build_gem "yard" do |s| + s.write "lib/yard.rb", <<-Y + if Gem::Version.new(Gem::VERSION) >= Gem::Version.new("1.8.10") + specs = Gem::Specification + else + specs = Gem.source_index.find_name('') + end + specs.sort_by(&:name).each do |gem| + puts gem.full_name + end + Y + end + + # The rcov gem is platform mswin32, but has no arch + build_gem "rcov" do |s| + s.platform = Gem::Platform.new([nil, "mswin32", nil]) + s.write "lib/rcov.rb", "RCOV = '1.0.0'" + end + + build_gem "net-ssh" + build_gem "net-sftp", "1.1.1" do |s| + s.add_dependency "net-ssh", ">= 1.0.0", "< 1.99.0" + end + + # Test complicated gem dependencies for install + build_gem "net_a" do |s| + s.add_dependency "net_b" + s.add_dependency "net_build_extensions" + end + + build_gem "net_b" + + build_gem "net_build_extensions" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("../lib", __FILE__) + FileUtils.mkdir_p(path) + File.open("\#{path}/net_build_extensions.rb", "w") do |f| + f.puts "NET_BUILD_EXTENSIONS = 'YES'" + end + end + RUBY + end + + build_gem "net_c" do |s| + s.add_dependency "net_a" + s.add_dependency "net_d" + end + + build_gem "net_d" + + build_gem "net_e" do |s| + s.add_dependency "net_d" + end + + # Capistrano did this (at least until version 2.5.10) + # Rubygems 2.2 doesn't allow the specifying of a dependency twice + # See https://github.com/rubygems/rubygems/commit/03dbac93a3396a80db258d9bc63500333c25bd2f + build_gem "double_deps", "1.0", :skip_validation => true do |s| + s.add_dependency "net-ssh", ">= 1.0.0" + s.add_dependency "net-ssh" + end + + build_gem "foo" + + # A minimal fake pry console + build_gem "pry" do |s| + s.write "lib/pry.rb", <<-RUBY + class Pry + class << self + def toplevel_binding + unless defined?(@toplevel_binding) && @toplevel_binding + TOPLEVEL_BINDING.eval %{ + def self.__pry__; binding; end + Pry.instance_variable_set(:@toplevel_binding, __pry__) + class << self; undef __pry__; end + } + end + @toplevel_binding.eval('private') + @toplevel_binding + end + + def __pry__ + while line = gets + begin + puts eval(line, toplevel_binding).inspect.sub(/^"(.*)"$/, '=> \\1') + rescue Exception => e + puts "\#{e.class}: \#{e.message}" + puts e.backtrace.first + end + end + end + alias start __pry__ + end + end + RUBY + end + end + end + + def build_repo2(&blk) + FileUtils.rm_rf gem_repo2 + FileUtils.cp_r gem_repo1, gem_repo2 + update_repo2(&blk) if block_given? + end + + def build_repo3 + build_repo gem_repo3 do + build_gem "rack" + end + FileUtils.rm_rf Dir[gem_repo3("prerelease*")] + end + + # A repo that has no pre-installed gems included. (The caller completely + # determines the contents with the block.) + def build_repo4(&blk) + FileUtils.rm_rf gem_repo4 + build_repo(gem_repo4, &blk) + end + + def update_repo4(&blk) + update_repo(gem_repo4, &blk) + end + + def update_repo2 + update_repo gem_repo2 do + build_gem "rack", "1.2" do |s| + s.executables = "rackup" + end + yield if block_given? + end + end + + def build_security_repo + build_repo security_repo do + build_gem "rack" + + build_gem "signed_gem" do |s| + cert = "signing-cert.pem" + pkey = "signing-pkey.pem" + s.write cert, TEST_CERT + s.write pkey, TEST_PKEY + s.signing_key = pkey + s.cert_chain = [cert] + end + end + end + + def build_repo(path, &blk) + return if File.directory?(path) + rake_path = Dir["#{Path.base_system_gems}/**/rake*.gem"].first + + if rake_path.nil? + Spec::Path.base_system_gems.rmtree + Spec::Rubygems.setup + rake_path = Dir["#{Path.base_system_gems}/**/rake*.gem"].first + end + + if rake_path + FileUtils.mkdir_p("#{path}/gems") + FileUtils.cp rake_path, "#{path}/gems/" + else + abort "Your test gems are missing! Run `rm -rf #{tmp}` and try again." + end + + update_repo(path, &blk) + end + + def update_repo(path) + if path == gem_repo1 && caller.first.split(" ").last == "`build_repo`" + raise "Updating gem_repo1 is unsupported -- use gem_repo2 instead" + end + return unless block_given? + @_build_path = "#{path}/gems" + @_build_repo = File.basename(path) + yield + with_gem_path_as Path.base_system_gems do + Dir.chdir(path) { gem_command! :generate_index } + end + ensure + @_build_path = nil + @_build_repo = nil + end + + def build_index(&block) + index = Bundler::Index.new + IndexBuilder.run(index, &block) if block_given? + index + end + + def build_spec(name, version, platform = nil, &block) + Array(version).map do |v| + Gem::Specification.new do |s| + s.name = name + s.version = Gem::Version.new(v) + s.platform = platform + s.authors = ["no one in particular"] + s.summary = "a gemspec used only for testing" + DepBuilder.run(s, &block) if block_given? + end + end + end + + def build_dep(name, requirements = Gem::Requirement.default, type = :runtime) + Bundler::Dependency.new(name, :version => requirements) + end + + def build_lib(name, *args, &blk) + build_with(LibBuilder, name, args, &blk) + end + + def build_gem(name, *args, &blk) + build_with(GemBuilder, name, args, &blk) + end + + def build_git(name, *args, &block) + opts = args.last.is_a?(Hash) ? args.last : {} + builder = opts[:bare] ? GitBareBuilder : GitBuilder + spec = build_with(builder, name, args, &block) + GitReader.new(opts[:path] || lib_path(spec.full_name)) + end + + def update_git(name, *args, &block) + opts = args.last.is_a?(Hash) ? args.last : {} + spec = build_with(GitUpdater, name, args, &block) + GitReader.new(opts[:path] || lib_path(spec.full_name)) + end + + def build_plugin(name, *args, &blk) + build_with(PluginBuilder, name, args, &blk) + end + + private + + def build_with(builder, name, args, &blk) + @_build_path ||= nil + @_build_repo ||= nil + options = args.last.is_a?(Hash) ? args.pop : {} + versions = args.last || "1.0" + spec = nil + + options[:path] ||= @_build_path + options[:source] ||= @_build_repo + + Array(versions).each do |version| + spec = builder.new(self, name, version) + spec.authors = ["no one"] if !spec.authors || spec.authors.empty? + yield spec if block_given? + spec._build(options) + end + + spec + end + + class IndexBuilder + include Builders + + def self.run(index, &block) + new(index).run(&block) + end + + def initialize(index) + @index = index + end + + def run(&block) + instance_eval(&block) + end + + def gem(*args, &block) + build_spec(*args, &block).each do |s| + @index << s + end + end + + def platforms(platforms) + platforms.split(/\s+/).each do |platform| + platform.gsub!(/^(mswin32)$/, 'x86-\1') + yield Gem::Platform.new(platform) + end + end + + def versions(versions) + versions.split(/\s+/).each {|version| yield v(version) } + end + end + + class DepBuilder + include Builders + + def self.run(spec, &block) + new(spec).run(&block) + end + + def initialize(spec) + @spec = spec + end + + def run(&block) + instance_eval(&block) + end + + def runtime(name, requirements) + @spec.add_runtime_dependency(name, requirements) + end + + def development(name, requirements) + @spec.add_development_dependency(name, requirements) + end + + def required_ruby_version=(*reqs) + @spec.required_ruby_version = *reqs + end + + alias_method :dep, :runtime + end + + class LibBuilder + def initialize(context, name, version) + @context = context + @name = name + @spec = Gem::Specification.new do |s| + s.name = name + s.version = version + s.summary = "This is just a fake gem for testing" + s.description = "This is a completely fake gem, for testing purposes." + s.author = "no one" + s.email = "foo@bar.baz" + s.homepage = "http://example.com" + s.license = "MIT" + end + @files = {} + end + + def method_missing(*args, &blk) + @spec.send(*args, &blk) + end + + def write(file, source = "") + @files[file] = source + end + + def executables=(val) + @spec.executables = Array(val) + @spec.executables.each do |file| + executable = "#{@spec.bindir}/#{file}" + shebang = if Bundler.current_ruby.jruby? + "#!/usr/bin/env jruby\n" + else + "#!/usr/bin/env ruby\n" + end + @spec.files << executable + write executable, "#{shebang}require '#{@name}' ; puts #{Builders.constantize(@name)}" + end + end + + def add_c_extension + require_paths << "ext" + extensions << "ext/extconf.rb" + write "ext/extconf.rb", <<-RUBY + require "mkmf" + + + # exit 1 unless with_config("simple") + + extension_name = "very_simple_binary_c" + if extra_lib_dir = with_config("ext-lib") + # add extra libpath if --with-ext-lib is + # passed in as a build_arg + dir_config extension_name, nil, extra_lib_dir + else + dir_config extension_name + end + create_makefile extension_name + RUBY + write "ext/very_simple_binary.c", <<-C + #include "ruby.h" + + void Init_very_simple_binary_c() { + rb_define_module("VerySimpleBinaryInC"); + } + C + end + + def _build(options) + path = options[:path] || _default_path + + if options[:rubygems_version] + @spec.rubygems_version = options[:rubygems_version] + def @spec.mark_version; end + + def @spec.validate; end + end + + case options[:gemspec] + when false + # do nothing + when :yaml + @files["#{name}.gemspec"] = @spec.to_yaml + else + @files["#{name}.gemspec"] = @spec.to_ruby + end + + unless options[:no_default] + gem_source = options[:source] || "path@#{path}" + @files = _default_files. + merge("lib/#{name}/source.rb" => "#{Builders.constantize(name)}_SOURCE = #{gem_source.to_s.dump}"). + merge(@files) + end + + @spec.authors = ["no one"] + + @files.each do |file, source| + file = Pathname.new(path).join(file) + FileUtils.mkdir_p(file.dirname) + File.open(file, "w") {|f| f.puts source } + end + @spec.files = @files.keys + path + end + + def _default_files + @_default_files ||= begin + platform_string = " #{@spec.platform}" unless @spec.platform == Gem::Platform::RUBY + { "lib/#{name}.rb" => "#{Builders.constantize(name)} = '#{version}#{platform_string}'" } + end + end + + def _default_path + @context.tmp("libs", @spec.full_name) + end + end + + class GitBuilder < LibBuilder + def _build(options) + path = options[:path] || _default_path + source = options[:source] || "git@#{path}" + super(options.merge(:path => path, :source => source)) + Dir.chdir(path) do + `git init` + `git add *` + `git config user.email "lol@wut.com"` + `git config user.name "lolwut"` + `git commit -m 'OMG INITIAL COMMIT'` + end + end + end + + class GitBareBuilder < LibBuilder + def _build(options) + path = options[:path] || _default_path + super(options.merge(:path => path)) + Dir.chdir(path) do + `git init --bare` + end + end + end + + class GitUpdater < LibBuilder + def silently(str) + `#{str} 2>#{Bundler::NULL}` + end + + def _build(options) + libpath = options[:path] || _default_path + update_gemspec = options[:gemspec] || false + source = options[:source] || "git@#{libpath}" + + Dir.chdir(libpath) do + silently "git checkout master" + + if branch = options[:branch] + raise "You can't specify `master` as the branch" if branch == "master" + escaped_branch = Shellwords.shellescape(branch) + + if `git branch | grep #{escaped_branch}`.empty? + silently("git branch #{escaped_branch}") + end + + silently("git checkout #{escaped_branch}") + elsif tag = options[:tag] + `git tag #{Shellwords.shellescape(tag)}` + elsif options[:remote] + silently("git remote add origin file://#{options[:remote]}") + elsif options[:push] + silently("git push origin #{options[:push]}") + end + + current_ref = `git rev-parse HEAD`.strip + _default_files.keys.each do |path| + _default_files[path] += "\n#{Builders.constantize(name)}_PREV_REF = '#{current_ref}'" + end + super(options.merge(:path => libpath, :gemspec => update_gemspec, :source => source)) + `git add *` + `git commit -m "BUMP"` + end + end + end + + class GitReader + attr_reader :path + + def initialize(path) + @path = path + end + + def ref_for(ref, len = nil) + ref = git "rev-parse #{ref}" + ref = ref[0..len] if len + ref + end + + private + + def git(cmd) + Bundler::SharedHelpers.with_clean_git_env do + Dir.chdir(@path) { `git #{cmd}`.strip } + end + end + end + + class GemBuilder < LibBuilder + def _build(opts) + lib_path = super(opts.merge(:path => @context.tmp(".tmp/#{@spec.full_name}"), :no_default => opts[:no_default])) + Dir.chdir(lib_path) do + destination = opts[:path] || _default_path + FileUtils.mkdir_p(destination) + + @spec.authors = ["that guy"] if !@spec.authors || @spec.authors.empty? + + Bundler.rubygems.build(@spec, opts[:skip_validation]) + if opts[:to_system] + `gem install --ignore-dependencies --no-ri --no-rdoc #{@spec.full_name}.gem` + else + FileUtils.mv("#{@spec.full_name}.gem", opts[:path] || _default_path) + end + end + end + + def _default_path + @context.gem_repo1("gems") + end + end + + class PluginBuilder < GemBuilder + def _default_files + @_default_files ||= super.merge("plugins.rb" => "") + end + end + + TEST_CERT = <<-CERT.gsub(/^\s*/, "") + -----BEGIN CERTIFICATE----- + MIIDMjCCAhqgAwIBAgIBATANBgkqhkiG9w0BAQUFADAnMQwwCgYDVQQDDAN5b3Ux + FzAVBgoJkiaJk/IsZAEZFgdleGFtcGxlMB4XDTE1MDIwODAwMTIyM1oXDTQyMDYy + NTAwMTIyM1owJzEMMAoGA1UEAwwDeW91MRcwFQYKCZImiZPyLGQBGRYHZXhhbXBs + ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANlvFdpN43c4DMS9Jo06 + m0a7k3bQ3HWQ1yrYhZMi77F1F73NpBknYHIzDktQpGn6hs/4QFJT4m4zNEBF47UL + jHU5nTK5rjkS3niGYUjvh3ZEzVeo9zHUlD/UwflDo4ALl3TSo2KY/KdPS/UTdLXL + ajkQvaVJtEDgBPE3DPhlj5whp+Ik3mDHej7qpV6F502leAwYaFyOtlEG/ZGNG+nZ + L0clH0j77HpP42AylHDi+vakEM3xcjo9BeWQ6Vkboic93c9RTt6CWBWxMQP7Nol1 + MOebz9XOSQclxpxWteXNfPRtMdAhmRl76SMI8ywzThNPpa4EH/yz34ftebVOgKyM + nd0CAwEAAaNpMGcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFA7D + n9qo0np23qi3aOYuAAPn/5IdMBYGA1UdEQQPMA2BC3lvdUBleGFtcGxlMBYGA1Ud + EgQPMA2BC3lvdUBleGFtcGxlMA0GCSqGSIb3DQEBBQUAA4IBAQA7Gyk62sWOUX/N + vk4tJrgKESph6Ns8+E36A7n3jt8zCep8ldzMvwTWquf9iqhsC68FilEoaDnUlWw7 + d6oNuaFkv7zfrWGLlvqQJC+cu2X5EpcCksg5oRp8VNbwJysJ6JgwosxzROII8eXc + R+j1j6mDvQYqig2QOnzf480pjaqbP+tspfDFZbhKPrgM3Blrb3ZYuFpv4zkqI7aB + 6fuk2DUhNO1CuwrJA84TqC+jGo73bDKaT5hrIDiaJRrN5+zcWja2uEWrj5jSbep4 + oXdEdyH73hOHMBP40uds3PqnUsxEJhzjB2sCCe1geV24kw9J4m7EQXPVkUKDgKrt + LlpDmOoo + -----END CERTIFICATE----- + CERT + + TEST_PKEY = <<-PKEY.gsub(/^\s*/, "") + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEA2W8V2k3jdzgMxL0mjTqbRruTdtDcdZDXKtiFkyLvsXUXvc2k + GSdgcjMOS1CkafqGz/hAUlPibjM0QEXjtQuMdTmdMrmuORLeeIZhSO+HdkTNV6j3 + MdSUP9TB+UOjgAuXdNKjYpj8p09L9RN0tctqORC9pUm0QOAE8TcM+GWPnCGn4iTe + YMd6PuqlXoXnTaV4DBhoXI62UQb9kY0b6dkvRyUfSPvsek/jYDKUcOL69qQQzfFy + Oj0F5ZDpWRuiJz3dz1FO3oJYFbExA/s2iXUw55vP1c5JByXGnFa15c189G0x0CGZ + GXvpIwjzLDNOE0+lrgQf/LPfh+15tU6ArIyd3QIDAQABAoIBACbDqz20TS1gDMa2 + gj0DidNedbflHKjJHdNBru7Ad8NHgOgR1YO2hXdWquG6itVqGMbTF4SV9/R1pIcg + 7qvEV1I+50u31tvOBWOvcYCzU48+TO2n7gowQA3xPHPYHzog1uu48fAOHl0lwgD7 + av9OOK3b0jO5pC08wyTOD73pPWU0NrkTh2+N364leIi1pNuI1z4V+nEuIIm7XpVd + 5V4sXidMTiEMJwE6baEDfTjHKaoRndXrrPo3ryIXmcX7Ag1SwAQwF5fBCRToCgIx + dszEZB1bJD5gA6r+eGnJLB/F60nK607az5o3EdguoB2LKa6q6krpaRCmZU5svvoF + J7xgBPECgYEA8RIzHAQ3zbaibKdnllBLIgsqGdSzebTLKheFuigRotEV3Or/z5Lg + k/nVnThWVkTOSRqXTNpJAME6a4KTdcVSxYP+SdZVO1esazHrGb7xPVb7MWSE1cqp + WEk3Yy8OUOPoPQMc4dyGzd30Mi8IBB6gnFIYOTrpUo0XtkBv8rGGhfsCgYEA5uYn + 6QgL4NqNT84IXylmMb5ia3iBt6lhxI/A28CDtQvfScl4eYK0IjBwdfG6E1vJgyzg + nJzv3xEVo9bz+Kq7CcThWpK5JQaPnsV0Q74Wjk0ShHet15txOdJuKImnh5F6lylC + GTLR9gnptytfMH/uuw4ws0Q2kcg4l5NHKOWOnAcCgYEAvAwIVkhsB0n59Wu4gCZu + FUZENxYWUk/XUyQ6KnZrG2ih90xQ8+iMyqFOIm/52R2fFKNrdoWoALC6E3ct8+ZS + pMRLrelFXx8K3it4SwMJR2H8XBEfFW4bH0UtsW7Zafv+AunUs9LETP5gKG1LgXsq + qgXX43yy2LQ61O365YPZfdUCgYBVbTvA3MhARbvYldrFEnUL3GtfZbNgdxuD9Mee + xig0eJMBIrgfBLuOlqtVB70XYnM4xAbKCso4loKSHnofO1N99siFkRlM2JOUY2tz + kMWZmmxKdFjuF0WZ5f/5oYxI/QsFGC+rUQEbbWl56mMKd5qkvEhKWudxoklF0yiV + ufC8SwKBgDWb8iWqWN5a/kfvKoxFcDM74UHk/SeKMGAL+ujKLf58F+CbweM5pX9C + EUsxeoUEraVWTiyFVNqD81rCdceus9TdBj0ZIK1vUttaRZyrMAwF0uQSfjtxsOpd + l69BkyvzjgDPkmOHVGiSZDLi3YDvypbUpo6LOy4v5rVg5U2F/A0v + -----END RSA PRIVATE KEY----- + PKEY + end +end diff --git a/spec/bundler/support/code_climate.rb b/spec/bundler/support/code_climate.rb new file mode 100644 index 0000000000..8f1fb35bcd --- /dev/null +++ b/spec/bundler/support/code_climate.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +module Spec + module CodeClimate + def self.setup + require "codeclimate-test-reporter" + ::CodeClimate::TestReporter.start + configure_exclusions + rescue LoadError + # it's fine if CodeClimate isn't set up + nil + end + + def self.configure_exclusions + SimpleCov.start do + add_filter "/bin/" + add_filter "/lib/bundler/man/" + add_filter "/lib/bundler/vendor/" + add_filter "/man/" + add_filter "/pkg/" + add_filter "/spec/" + add_filter "/tmp/" + end + end + end +end diff --git a/spec/bundler/support/hax.rb b/spec/bundler/support/hax.rb new file mode 100644 index 0000000000..663d3527c5 --- /dev/null +++ b/spec/bundler/support/hax.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true +require "rubygems" + +module Gem + class Platform + @local = new(ENV["BUNDLER_SPEC_PLATFORM"]) if ENV["BUNDLER_SPEC_PLATFORM"] + end + @platforms = [Gem::Platform::RUBY, Gem::Platform.local] +end + +if ENV["BUNDLER_SPEC_VERSION"] + module Bundler + remove_const(:VERSION) if const_defined?(:VERSION) + VERSION = ENV["BUNDLER_SPEC_VERSION"].dup + end +end + +if ENV["BUNDLER_SPEC_WINDOWS"] == "true" + require "bundler/constants" + + module Bundler + remove_const :WINDOWS if defined?(WINDOWS) + WINDOWS = true + end +end + +class Object + if ENV["BUNDLER_SPEC_RUBY_ENGINE"] + if defined?(RUBY_ENGINE) && RUBY_ENGINE != "jruby" && ENV["BUNDLER_SPEC_RUBY_ENGINE"] == "jruby" + begin + # this has to be done up front because psych will try to load a .jar + # if it thinks its on jruby + require "psych" + rescue LoadError + nil + end + end + + remove_const :RUBY_ENGINE if defined?(RUBY_ENGINE) + RUBY_ENGINE = ENV["BUNDLER_SPEC_RUBY_ENGINE"] + + if RUBY_ENGINE == "jruby" + remove_const :JRUBY_VERSION if defined?(JRUBY_VERSION) + JRUBY_VERSION = ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] + end + end +end diff --git a/spec/bundler/support/helpers.rb b/spec/bundler/support/helpers.rb new file mode 100644 index 0000000000..1a3fec1960 --- /dev/null +++ b/spec/bundler/support/helpers.rb @@ -0,0 +1,504 @@ +# frozen_string_literal: true + +module Spec + module Helpers + def reset! + Dir.glob("#{tmp}/{gems/*,*}", File::FNM_DOTMATCH).each do |dir| + next if %w(base remote1 gems rubygems . ..).include?(File.basename(dir)) + if ENV["BUNDLER_SUDO_TESTS"] + `sudo rm -rf "#{dir}"` + else + FileUtils.rm_rf(dir) + end + end + FileUtils.mkdir_p(home) + FileUtils.mkdir_p(tmpdir) + Bundler.reset! + Bundler.ui = nil + Bundler.ui # force it to initialize + end + + def self.bang(method) + define_method("#{method}!") do |*args, &blk| + send(method, *args, &blk).tap do + if exitstatus && exitstatus != 0 + error = out + "\n" + err + error.strip! + raise RuntimeError, + "Invoking #{method}!(#{args.map(&:inspect).join(", ")}) failed:\n#{error}", + caller.drop_while {|bt| bt.start_with?(__FILE__) } + end + end + end + end + + attr_reader :out, :err, :exitstatus + + def the_bundle(*args) + TheBundle.new(*args) + end + + def in_app_root(&blk) + Dir.chdir(bundled_app, &blk) + end + + def in_app_root2(&blk) + Dir.chdir(bundled_app2, &blk) + end + + def in_app_root_custom(root, &blk) + Dir.chdir(root, &blk) + end + + def run(cmd, *args) + opts = args.last.is_a?(Hash) ? args.pop : {} + groups = args.map(&:inspect).join(", ") + setup = "require 'rubygems' ; require 'bundler' ; Bundler.setup(#{groups})\n" + @out = ruby(setup + cmd, opts) + end + bang :run + + def load_error_run(ruby, name, *args) + cmd = <<-RUBY + begin + #{ruby} + rescue LoadError => e + $stderr.puts "ZOMG LOAD ERROR" if e.message.include?("-- #{name}") + end + RUBY + opts = args.last.is_a?(Hash) ? args.pop : {} + args += [opts] + run(cmd, *args) + end + + def lib + root.join("lib") + end + + def spec + spec_dir.to_s + end + + def bundle(cmd, options = {}) + with_sudo = options.delete(:sudo) + sudo = with_sudo == :preserve_env ? "sudo -E" : "sudo" if with_sudo + + options["no-color"] = true unless options.key?("no-color") || cmd.to_s =~ /\A(e|ex|exe|exec|conf|confi|config)(\s|\z)/ + + bundle_bin = options.delete("bundle_bin") || bindir.join("bundle") + + if system_bundler = options.delete(:system_bundler) + bundle_bin = "-S bundle" + end + + requires = options.delete(:requires) || [] + if artifice = options.delete(:artifice) { "fail" unless RSpec.current_example.metadata[:realworld] } + requires << File.expand_path("../artifice/#{artifice}.rb", __FILE__) + end + requires << "support/hax" + requires_str = requires.map {|r| "-r#{r}" }.join(" ") + + load_path = [] + load_path << lib unless system_bundler + load_path << spec + load_path_str = "-I#{load_path.join(File::PATH_SEPARATOR)}" + + env = (options.delete(:env) || {}).map {|k, v| "#{k}='#{v}'" }.join(" ") + env["PATH"].gsub!("#{Path.root}/exe", "") if env["PATH"] && system_bundler + args = options.map do |k, v| + v == true ? " --#{k}" : " --#{k} #{v}" if v + end.join + + cmd = "#{env} #{sudo} #{Gem.ruby} #{load_path_str} #{requires_str} #{bundle_bin} #{cmd}#{args}" + sys_exec(cmd) {|i, o, thr| yield i, o, thr if block_given? } + end + bang :bundle + + def bundler(cmd, options = {}) + options["bundle_bin"] = bindir.join("bundler") + bundle(cmd, options) + end + + def bundle_ruby(options = {}) + options["bundle_bin"] = bindir.join("bundle_ruby") + bundle("", options) + end + + def ruby(ruby, options = {}) + env = (options.delete(:env) || {}).map {|k, v| "#{k}='#{v}' " }.join + ruby = ruby.gsub(/["`\$]/) {|m| "\\#{m}" } + lib_option = options[:no_lib] ? "" : " -I#{lib}" + sys_exec(%(#{env}#{Gem.ruby}#{lib_option} -e "#{ruby}")) + end + bang :ruby + + def load_error_ruby(ruby, name, opts = {}) + ruby(<<-R) + begin + #{ruby} + rescue LoadError => e + $stderr.puts "ZOMG LOAD ERROR"# if e.message.include?("-- #{name}") + end + R + end + + def gembin(cmd) + lib = File.expand_path("../../../lib", __FILE__) + old = ENV["RUBYOPT"] + ENV["RUBYOPT"] = "#{ENV["RUBYOPT"]} -I#{lib}" + cmd = bundled_app("bin/#{cmd}") unless cmd.to_s.include?("/") + sys_exec(cmd.to_s) + ensure + ENV["RUBYOPT"] = old + end + + def gem_command(command, args = "", options = {}) + if command == :exec && !options[:no_quote] + args = args.gsub(/(?=")/, "\\") + args = %("#{args}") + end + gem = ENV['BUNDLE_GEM'] || "#{Gem.ruby} -rubygems -S gem --backtrace" + sys_exec("#{gem} #{command} #{args}") + end + bang :gem_command + + def rake + if ENV['BUNDLE_RUBY'] && ENV['BUNDLE_GEM'] + "#{ENV['BUNDLE_RUBY']} #{ENV['GEM_PATH']}/bin/rake" + else + 'rake' + end + end + + def sys_exec(cmd) + Open3.popen3(cmd.to_s) do |stdin, stdout, stderr, wait_thr| + yield stdin, stdout, wait_thr if block_given? + stdin.close + + @exitstatus = wait_thr && wait_thr.value.exitstatus + @out = Thread.new { stdout.read }.value.strip + @err = Thread.new { stderr.read }.value.strip + end + + (@all_output ||= String.new) << [ + "$ #{cmd.to_s.strip}", + out, + err, + @exitstatus ? "# $? => #{@exitstatus}" : "", + "\n", + ].reject(&:empty?).join("\n") + + @out + end + bang :sys_exec + + def config(config = nil, path = bundled_app(".bundle/config")) + return YAML.load_file(path) unless config + FileUtils.mkdir_p(File.dirname(path)) + File.open(path, "w") do |f| + f.puts config.to_yaml + end + config + end + + def global_config(config = nil) + config(config, home(".bundle/config")) + end + + def create_file(*args) + path = bundled_app(args.shift) + path = args.shift if args.first.is_a?(Pathname) + str = args.shift || "" + path.dirname.mkpath + File.open(path.to_s, "w") do |f| + f.puts strip_whitespace(str) + end + end + + def gemfile(*args) + if args.empty? + File.open("Gemfile", "r", &:read) + else + create_file("Gemfile", *args) + end + end + + def lockfile(*args) + if args.empty? + File.open("Gemfile.lock", "r", &:read) + else + create_file("Gemfile.lock", *args) + end + end + + def strip_whitespace(str) + # Trim the leading spaces + spaces = str[/\A\s+/, 0] || "" + str.gsub(/^#{spaces}/, "") + end + + def install_gemfile(*args) + gemfile(*args) + opts = args.last.is_a?(Hash) ? args.last : {} + opts[:retry] ||= 0 + bundle :install, opts + end + bang :install_gemfile + + def lock_gemfile(*args) + gemfile(*args) + opts = args.last.is_a?(Hash) ? args.last : {} + opts[:retry] ||= 0 + bundle :lock, opts + end + + def install_gems(*gems) + options = gems.last.is_a?(Hash) ? gems.pop : {} + gem_repo = options.fetch(:gem_repo) { gem_repo1 } + gems.each do |g| + path = if g == :bundler + Dir.chdir(root) { gem_command! :build, gemspec.to_s } + bundler_path = root + "bundler-#{Bundler::VERSION}.gem" + elsif g.to_s =~ %r{\A/.*\.gem\z} + g + else + "#{gem_repo}/gems/#{g}.gem" + end + + raise "OMG `#{path}` does not exist!" unless File.exist?(path) + + gem_command! :install, "--no-rdoc --no-ri --ignore-dependencies '#{path}'" + bundler_path && bundler_path.rmtree + end + end + + alias_method :install_gem, :install_gems + + def with_gem_path_as(path) + backup = ENV.to_hash + ENV["GEM_HOME"] = path.to_s + ENV["GEM_PATH"] = path.to_s + ENV["BUNDLER_ORIG_GEM_PATH"] = nil + yield + ensure + ENV.replace(backup) + end + + def with_path_as(path) + backup = ENV.to_hash + ENV["PATH"] = path.to_s + ENV["BUNDLER_ORIG_PATH"] = nil + yield + ensure + ENV.replace(backup) + end + + def with_path_added(path) + with_path_as(path.to_s + ":" + ENV["PATH"]) do + yield + end + end + + def break_git! + FileUtils.mkdir_p(tmp("broken_path")) + File.open(tmp("broken_path/git"), "w", 0o755) do |f| + f.puts "#!/usr/bin/env ruby\nSTDERR.puts 'This is not the git you are looking for'\nexit 1" + end + + ENV["PATH"] = "#{tmp("broken_path")}:#{ENV["PATH"]}" + end + + def with_fake_man + FileUtils.mkdir_p(tmp("fake_man")) + File.open(tmp("fake_man/man"), "w", 0o755) do |f| + f.puts "#!/usr/bin/env ruby\nputs ARGV.inspect\n" + end + with_path_added(tmp("fake_man")) { yield } + end + + def system_gems(*gems) + gems = gems.flatten + + FileUtils.rm_rf(system_gem_path) + FileUtils.mkdir_p(system_gem_path) + + Gem.clear_paths + + env_backup = ENV.to_hash + ENV["GEM_HOME"] = system_gem_path.to_s + ENV["GEM_PATH"] = system_gem_path.to_s + ENV["BUNDLER_ORIG_GEM_PATH"] = nil + + install_gems(*gems) + return unless block_given? + begin + yield + ensure + ENV.replace(env_backup) + end + end + + def realworld_system_gems(*gems) + gems = gems.flatten + + FileUtils.rm_rf(system_gem_path) + FileUtils.mkdir_p(system_gem_path) + + Gem.clear_paths + + gem_home = ENV["GEM_HOME"] + gem_path = ENV["GEM_PATH"] + path = ENV["PATH"] + ENV["GEM_HOME"] = system_gem_path.to_s + ENV["GEM_PATH"] = system_gem_path.to_s + + gems.each do |gem| + gem_command :install, "--no-rdoc --no-ri #{gem}" + end + return unless block_given? + begin + yield + ensure + ENV["GEM_HOME"] = gem_home + ENV["GEM_PATH"] = gem_path + ENV["PATH"] = path + end + end + + def cache_gems(*gems) + gems = gems.flatten + + FileUtils.rm_rf("#{bundled_app}/vendor/cache") + FileUtils.mkdir_p("#{bundled_app}/vendor/cache") + + gems.each do |g| + path = "#{gem_repo1}/gems/#{g}.gem" + raise "OMG `#{path}` does not exist!" unless File.exist?(path) + FileUtils.cp(path, "#{bundled_app}/vendor/cache") + end + end + + def simulate_new_machine + system_gems [] + FileUtils.rm_rf default_bundle_path + FileUtils.rm_rf bundled_app(".bundle") + end + + def simulate_platform(platform) + old = ENV["BUNDLER_SPEC_PLATFORM"] + ENV["BUNDLER_SPEC_PLATFORM"] = platform.to_s + yield if block_given? + ensure + ENV["BUNDLER_SPEC_PLATFORM"] = old if block_given? + end + + def simulate_ruby_version(version) + return if version == RUBY_VERSION + old = ENV["BUNDLER_SPEC_RUBY_VERSION"] + ENV["BUNDLER_SPEC_RUBY_VERSION"] = version + yield if block_given? + ensure + ENV["BUNDLER_SPEC_RUBY_VERSION"] = old if block_given? + end + + def simulate_ruby_engine(engine, version = "1.6.0") + return if engine == local_ruby_engine + + old = ENV["BUNDLER_SPEC_RUBY_ENGINE"] + ENV["BUNDLER_SPEC_RUBY_ENGINE"] = engine + old_version = ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] + ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] = version + yield if block_given? + ensure + ENV["BUNDLER_SPEC_RUBY_ENGINE"] = old if block_given? + ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] = old_version if block_given? + end + + def simulate_bundler_version(version) + old = ENV["BUNDLER_SPEC_VERSION"] + ENV["BUNDLER_SPEC_VERSION"] = version.to_s + yield if block_given? + ensure + ENV["BUNDLER_SPEC_VERSION"] = old if block_given? + end + + def simulate_windows + old = ENV["BUNDLER_SPEC_WINDOWS"] + ENV["BUNDLER_SPEC_WINDOWS"] = "true" + simulate_platform mswin do + yield + end + ensure + ENV["BUNDLER_SPEC_WINDOWS"] = old + end + + def revision_for(path) + Dir.chdir(path) { `git rev-parse HEAD`.strip } + end + + def capture_output + capture(:stdout) + end + + def with_read_only(pattern) + chmod = lambda do |dirmode, filemode| + lambda do |f| + mode = File.directory?(f) ? dirmode : filemode + File.chmod(mode, f) + end + end + + Dir[pattern].each(&chmod[0o555, 0o444]) + yield + ensure + Dir[pattern].each(&chmod[0o755, 0o644]) + end + + def process_file(pathname) + changed_lines = pathname.readlines.map do |line| + yield line + end + File.open(pathname, "w") {|file| file.puts(changed_lines.join) } + end + + def with_env_vars(env_hash, &block) + current_values = {} + env_hash.each do |k, v| + current_values[k] = ENV[k] + ENV[k] = v + end + block.call if block_given? + env_hash.each do |k, _| + ENV[k] = current_values[k] + end + end + + def require_rack + # need to hack, so we can require rack + old_gem_home = ENV["GEM_HOME"] + ENV["GEM_HOME"] = Spec::Path.base_system_gems.to_s + require "rack" + ENV["GEM_HOME"] = old_gem_home + end + + def wait_for_server(host, port, seconds = 15) + tries = 0 + sleep 0.5 + TCPSocket.new(host, port) + rescue => e + raise(e) if tries > (seconds * 2) + tries += 1 + retry + end + + def find_unused_port + port = 21_453 + begin + port += 1 while TCPSocket.new("127.0.0.1", port) + rescue + false + end + port + end + end +end diff --git a/spec/bundler/support/indexes.rb b/spec/bundler/support/indexes.rb new file mode 100644 index 0000000000..29780014fc --- /dev/null +++ b/spec/bundler/support/indexes.rb @@ -0,0 +1,365 @@ +# frozen_string_literal: true +module Spec + module Indexes + def dep(name, reqs = nil) + @deps ||= [] + @deps << Bundler::Dependency.new(name, reqs) + end + + def platform(*args) + @platforms ||= [] + @platforms.concat args.map {|p| Gem::Platform.new(p) } + end + + alias_method :platforms, :platform + + def resolve(args = []) + @platforms ||= ["ruby"] + deps = [] + @deps.each do |d| + @platforms.each do |p| + deps << Bundler::DepProxy.new(d, p) + end + end + Bundler::Resolver.resolve(deps, @index, *args) + end + + def should_resolve_as(specs) + got = resolve + got = got.map(&:full_name).sort + expect(got).to eq(specs.sort) + end + + def should_resolve_and_include(specs, args = []) + got = resolve(args) + got = got.map(&:full_name).sort + specs.each do |s| + expect(got).to include(s) + end + end + + def should_conflict_on(names) + got = resolve + flunk "The resolve succeeded with: #{got.map(&:full_name).sort.inspect}" + rescue Bundler::VersionConflict => e + expect(Array(names).sort).to eq(e.conflicts.sort) + end + + def gem(*args, &blk) + build_spec(*args, &blk).first + end + + def locked(*args) + Bundler::SpecSet.new(args.map do |name, version| + gem(name, version) + end) + end + + def should_conservative_resolve_and_include(opts, unlock, specs) + # empty unlock means unlock all + opts = Array(opts) + search = Bundler::GemVersionPromoter.new(@locked, unlock).tap do |s| + s.level = opts.first + s.strict = opts.include?(:strict) + end + should_resolve_and_include specs, [{}, @base, search] + end + + def an_awesome_index + build_index do + gem "rack", %w(0.8 0.9 0.9.1 0.9.2 1.0 1.1) + gem "rack-mount", %w(0.4 0.5 0.5.1 0.5.2 0.6) + + # --- Rails + versions "1.2.3 2.2.3 2.3.5 3.0.0.beta 3.0.0.beta1" do |version| + gem "activesupport", version + gem "actionpack", version do + dep "activesupport", version + if version >= v("3.0.0.beta") + dep "rack", "~> 1.1" + dep "rack-mount", ">= 0.5" + elsif version > v("2.3") then dep "rack", "~> 1.0.0" + elsif version > v("2.0.0") then dep "rack", "~> 0.9.0" + end + end + gem "activerecord", version do + dep "activesupport", version + dep "arel", ">= 0.2" if version >= v("3.0.0.beta") + end + gem "actionmailer", version do + dep "activesupport", version + dep "actionmailer", version + end + if version < v("3.0.0.beta") + gem "railties", version do + dep "activerecord", version + dep "actionpack", version + dep "actionmailer", version + dep "activesupport", version + end + else + gem "railties", version + gem "rails", version do + dep "activerecord", version + dep "actionpack", version + dep "actionmailer", version + dep "activesupport", version + dep "railties", version + end + end + end + + versions "1.0 1.2 1.2.1 1.2.2 1.3 1.3.0.1 1.3.5 1.4.0 1.4.2 1.4.2.1" do |version| + platforms "ruby java mswin32 mingw32 x64-mingw32" do |platform| + next if version == v("1.4.2.1") && platform != pl("x86-mswin32") + next if version == v("1.4.2") && platform == pl("x86-mswin32") + gem "nokogiri", version, platform do + dep "weakling", ">= 0.0.3" if platform =~ pl("java") + end + end + end + + versions "0.0.1 0.0.2 0.0.3" do |version| + gem "weakling", version + end + + # --- Rails related + versions "1.2.3 2.2.3 2.3.5" do |version| + gem "activemerchant", version do + dep "activesupport", ">= #{version}" + end + end + end + end + + # Builder 3.1.4 will activate first, but if all + # goes well, it should resolve to 3.0.4 + def a_conflict_index + build_index do + gem "builder", %w(3.0.4 3.1.4) + gem("grape", "0.2.6") do + dep "builder", ">= 0" + end + + versions "3.2.8 3.2.9 3.2.10 3.2.11" do |version| + gem("activemodel", version) do + dep "builder", "~> 3.0.0" + end + end + + gem("my_app", "1.0.0") do + dep "activemodel", ">= 0" + dep "grape", ">= 0" + end + end + end + + def a_complex_conflict_index + build_index do + gem("a", %w(1.0.2 1.1.4 1.2.0 1.4.0)) do + dep "d", ">= 0" + end + + gem("d", %w(1.3.0 1.4.1)) do + dep "x", ">= 0" + end + + gem "d", "0.9.8" + + gem("b", "0.3.4") do + dep "a", ">= 1.5.0" + end + + gem("b", "0.3.5") do + dep "a", ">= 1.2" + end + + gem("b", "0.3.3") do + dep "a", "> 1.0" + end + + versions "3.2 3.3" do |version| + gem("c", version) do + dep "a", "~> 1.0" + end + end + + gem("my_app", "1.3.0") do + dep "c", ">= 4.0" + dep "b", ">= 0" + end + + gem("my_app", "1.2.0") do + dep "c", "~> 3.3.0" + dep "b", "0.3.4" + end + + gem("my_app", "1.1.0") do + dep "c", "~> 3.2.0" + dep "b", "0.3.5" + end + end + end + + def index_with_conflict_on_child + build_index do + gem "json", %w(1.6.5 1.7.7 1.8.0) + + gem("chef", "10.26") do + dep "json", [">= 1.4.4", "<= 1.7.7"] + end + + gem("berkshelf", "2.0.7") do + dep "json", ">= 1.7.7" + end + + gem("chef_app", "1.0.0") do + dep "berkshelf", "~> 2.0" + dep "chef", "~> 10.26" + end + end + end + + # Issue #3459 + def a_complicated_index + build_index do + gem "foo", %w(3.0.0 3.0.5) do + dep "qux", ["~> 3.1"] + dep "baz", ["< 9.0", ">= 5.0"] + dep "bar", ["~> 1.0"] + dep "grault", ["~> 3.1"] + end + + gem "foo", "1.2.1" do + dep "baz", ["~> 4.2"] + dep "bar", ["~> 1.0"] + dep "qux", ["~> 3.1"] + dep "grault", ["~> 2.0"] + end + + gem "bar", "1.0.5" do + dep "grault", ["~> 3.1"] + dep "baz", ["< 9", ">= 4.2"] + end + + gem "bar", "1.0.3" do + dep "baz", ["< 9", ">= 4.2"] + dep "grault", ["~> 2.0"] + end + + gem "baz", "8.2.10" do + dep "grault", ["~> 3.0"] + dep "garply", [">= 0.5.1", "~> 0.5"] + end + + gem "baz", "5.0.2" do + dep "grault", ["~> 2.0"] + dep "garply", [">= 0.3.1"] + end + + gem "baz", "4.2.0" do + dep "grault", ["~> 2.0"] + dep "garply", [">= 0.3.1"] + end + + gem "grault", %w(2.6.3 3.1.1) + + gem "garply", "0.5.1" do + dep "waldo", ["~> 0.1.3"] + end + + gem "waldo", "0.1.5" do + dep "plugh", ["~> 0.6.0"] + end + + gem "plugh", %w(0.6.3 0.6.11 0.7.0) + + gem "qux", "3.2.21" do + dep "plugh", [">= 0.6.4", "~> 0.6"] + dep "corge", ["~> 1.0"] + end + + gem "corge", "1.10.1" + end + end + + def a_unresovable_child_index + build_index do + gem "json", %w(1.8.0) + + gem("chef", "10.26") do + dep "json", [">= 1.4.4", "<= 1.7.7"] + end + + gem("berkshelf", "2.0.7") do + dep "json", ">= 1.7.7" + end + + gem("chef_app_error", "1.0.0") do + dep "berkshelf", "~> 2.0" + dep "chef", "~> 10.26" + end + end + end + + def a_index_with_root_conflict_on_child + build_index do + gem "builder", %w(2.1.2 3.0.1 3.1.3) + gem "i18n", %w(0.4.1 0.4.2) + + gem "activesupport", %w(3.0.0 3.0.1 3.0.5 3.1.7) + + gem("activemodel", "3.0.5") do + dep "activesupport", "= 3.0.5" + dep "builder", "~> 2.1.2" + dep "i18n", "~> 0.4" + end + + gem("activemodel", "3.0.0") do + dep "activesupport", "= 3.0.0" + dep "builder", "~> 2.1.2" + dep "i18n", "~> 0.4.1" + end + + gem("activemodel", "3.1.3") do + dep "activesupport", "= 3.1.3" + dep "builder", "~> 2.1.2" + dep "i18n", "~> 0.5" + end + + gem("activerecord", "3.0.0") do + dep "activesupport", "= 3.0.0" + dep "activemodel", "= 3.0.0" + end + + gem("activerecord", "3.0.5") do + dep "activesupport", "= 3.0.5" + dep "activemodel", "= 3.0.5" + end + + gem("activerecord", "3.0.9") do + dep "activesupport", "= 3.1.5" + dep "activemodel", "= 3.1.5" + end + end + end + + def a_circular_index + build_index do + gem "rack", "1.0.1" + gem("foo", "0.2.6") do + dep "bar", ">= 0" + end + + gem("bar", "1.0.0") do + dep "foo", ">= 0" + end + + gem("circular_app", "1.0.0") do + dep "foo", ">= 0" + dep "bar", ">= 0" + end + end + end + end +end diff --git a/spec/bundler/support/less_than_proc.rb b/spec/bundler/support/less_than_proc.rb new file mode 100644 index 0000000000..27966aa6ed --- /dev/null +++ b/spec/bundler/support/less_than_proc.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +class LessThanProc < Proc + attr_accessor :present + + def self.with(present) + provided = Gem::Version.new(present.dup) + new do |required| + if required =~ /[=><~]/ + !Gem::Requirement.new(required).satisfied_by?(provided) + else + provided < Gem::Version.new(required) + end + end.tap {|l| l.present = present } + end + + def inspect + "\"=< #{present}\"" + end +end diff --git a/spec/bundler/support/matchers.rb b/spec/bundler/support/matchers.rb new file mode 100644 index 0000000000..9248360639 --- /dev/null +++ b/spec/bundler/support/matchers.rb @@ -0,0 +1,222 @@ +# frozen_string_literal: true +require "forwardable" +require "support/the_bundle" +module Spec + module Matchers + extend RSpec::Matchers + + class Precondition + include RSpec::Matchers::Composable + extend Forwardable + def_delegators :failing_matcher, + :failure_message, + :actual, + :description, + :diffable?, + :expected, + :failure_message_when_negated + + def initialize(matcher, preconditions) + @matcher = with_matchers_cloned(matcher) + @preconditions = with_matchers_cloned(preconditions) + @failure_index = nil + end + + def matches?(target, &blk) + return false if @failure_index = @preconditions.index {|pc| !pc.matches?(target, &blk) } + @matcher.matches?(target, &blk) + end + + def does_not_match?(target, &blk) + return false if @failure_index = @preconditions.index {|pc| !pc.matches?(target, &blk) } + if @matcher.respond_to?(:does_not_match?) + @matcher.does_not_match?(target, &blk) + else + !@matcher.matches?(target, &blk) + end + end + + def expects_call_stack_jump? + @matcher.expects_call_stack_jump? || @preconditions.any?(&:expects_call_stack_jump) + end + + def supports_block_expectations? + @matcher.supports_block_expectations? || @preconditions.any?(&:supports_block_expectations) + end + + def failing_matcher + @failure_index ? @preconditions[@failure_index] : @matcher + end + end + + def self.define_compound_matcher(matcher, preconditions, &declarations) + raise "Must have preconditions to define a compound matcher" if preconditions.empty? + define_method(matcher) do |*expected, &block_arg| + Precondition.new( + RSpec::Matchers::DSL::Matcher.new(matcher, declarations, self, *expected, &block_arg), + preconditions + ) + end + end + + MAJOR_DEPRECATION = /^\[DEPRECATED FOR 2\.0\]\s*/ + + RSpec::Matchers.define :lack_errors do + diffable + match do |actual| + actual.gsub(/#{MAJOR_DEPRECATION}.+[\n]?/, "") == "" + end + end + + RSpec::Matchers.define :eq_err do |expected| + diffable + match do |actual| + actual.gsub(/#{MAJOR_DEPRECATION}.+[\n]?/, "") == expected + end + end + + RSpec::Matchers.define :have_major_deprecation do |expected| + diffable + match do |actual| + actual.split(MAJOR_DEPRECATION).any? do |d| + !d.empty? && values_match?(expected, d.strip) + end + end + end + + RSpec::Matchers.define :have_dep do |*args| + dep = Bundler::Dependency.new(*args) + + match do |actual| + actual.length == 1 && actual.all? {|d| d == dep } + end + end + + RSpec::Matchers.define :have_gem do |*args| + match do |actual| + actual.length == args.length && actual.all? {|a| args.include?(a.full_name) } + end + end + + RSpec::Matchers.define :have_rubyopts do |*args| + args = args.flatten + args = args.first.split(/\s+/) if args.size == 1 + + match do |actual| + actual = actual.split(/\s+/) if actual.is_a?(String) + args.all? {|arg| actual.include?(arg) } && actual.uniq.size == actual.size + end + end + + define_compound_matcher :read_as, [exist] do |file_contents| + diffable + + match do |actual| + @actual = Bundler.read_file(actual) + values_match?(file_contents, @actual) + end + end + + def indent(string, padding = 4, indent_character = " ") + string.to_s.gsub(/^/, indent_character * padding).gsub("\t", " ") + end + + define_compound_matcher :include_gems, [be_an_instance_of(Spec::TheBundle)] do |*names| + match do + opts = names.last.is_a?(Hash) ? names.pop : {} + source = opts.delete(:source) + groups = Array(opts[:groups]) + groups << opts + @errors = names.map do |name| + name, version, platform = name.split(/\s+/) + version_const = name == "bundler" ? "Bundler::VERSION" : Spec::Builders.constantize(name) + begin + run! "require '#{name}.rb'; puts #{version_const}", *groups + rescue => e + next "#{name} is not installed:\n#{indent(e)}" + end + out.gsub!(/#{MAJOR_DEPRECATION}.*$/, "") + actual_version, actual_platform = out.strip.split(/\s+/, 2) + unless Gem::Version.new(actual_version) == Gem::Version.new(version) + next "#{name} was expected to be at version #{version} but was #{actual_version}" + end + unless actual_platform == platform + next "#{name} was expected to be of platform #{platform} but was #{actual_platform}" + end + next unless source + begin + source_const = "#{Spec::Builders.constantize(name)}_SOURCE" + run! "require '#{name}/source'; puts #{source_const}", *groups + rescue + next "#{name} does not have a source defined:\n#{indent(e)}" + end + out.gsub!(/#{MAJOR_DEPRECATION}.*$/, "") + unless out.strip == source + next "Expected #{name} (#{version}) to be installed from `#{source}`, was actually from `#{out}`" + end + end.compact + + @errors.empty? + end + + match_when_negated do + opts = names.last.is_a?(Hash) ? names.pop : {} + groups = Array(opts[:groups]) || [] + @errors = names.map do |name| + name, version = name.split(/\s+/, 2) + begin + run <<-R, *(groups + [opts]) + begin + require '#{name}' + puts #{Spec::Builders.constantize(name)} + rescue LoadError, NameError + puts "WIN" + end + R + rescue => e + next "checking for #{name} failed:\n#{e}" + end + next if out == "WIN" + next "expected #{name} to not be installed, but it was" if version.nil? + if Gem::Version.new(out) == Gem::Version.new(version) + next "expected #{name} (#{version}) not to be installed, but it was" + end + end.compact + + @errors.empty? + end + + failure_message do + super() + " but:\n" + @errors.map {|e| indent(e) }.join("\n") + end + + failure_message_when_negated do + super() + " but:\n" + @errors.map {|e| indent(e) }.join("\n") + end + end + RSpec::Matchers.define_negated_matcher :not_include_gems, :include_gems + RSpec::Matchers.alias_matcher :include_gem, :include_gems + + def have_lockfile(expected) + read_as(strip_whitespace(expected)) + end + + def plugin_should_be_installed(*names) + names.each do |name| + expect(Bundler::Plugin).to be_installed(name) + path = Pathname.new(Bundler::Plugin.installed?(name)) + expect(path + "plugins.rb").to exist + end + end + + def plugin_should_not_be_installed(*names) + names.each do |name| + expect(Bundler::Plugin).not_to be_installed(name) + end + end + + def lockfile_should_be(expected) + expect(bundled_app("Gemfile.lock")).to read_as(strip_whitespace(expected)) + end + end +end diff --git a/spec/bundler/support/path.rb b/spec/bundler/support/path.rb new file mode 100644 index 0000000000..f28d660e83 --- /dev/null +++ b/spec/bundler/support/path.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true +require "pathname" + +module Spec + module Path + def root + if !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + # for Ruby Core + root_path = File.expand_path("../../../..", __FILE__) + else + root_path = File.expand_path("../../..", __FILE__) + end + @root ||= Pathname.new(root_path) + end + + def gemspec + if !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + # for Ruby Core + gemspec_path = File.expand_path(root.join("lib/bundler.gemspec"), __FILE__) + else + gemspec_path = File.expand_path(root.join("bundler.gemspec"), __FILE__) + end + @gemspec ||= Pathname.new(gemspec_path) + end + + def bindir + if !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + # for Ruby Core + bin_path = File.expand_path(root.join("bin"), __FILE__) + else + bin_path = File.expand_path(root.join("exe"), __FILE__) + end + @bindir ||= Pathname.new(bin_path) + end + + def spec_dir + if !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) + # for Ruby Core + spec_path = File.expand_path(root.join("spec/bundler"), __FILE__) + else + spec_path = File.expand_path(root.join("spec"), __FILE__) + end + @spec_dir ||= Pathname.new(spec_path) + end + + def tmp(*path) + root.join("tmp", *path) + end + + def home(*path) + tmp.join("home", *path) + end + + def default_bundle_path(*path) + system_gem_path(*path) + end + + def bundled_app(*path) + root = tmp.join("bundled_app") + FileUtils.mkdir_p(root) + root.join(*path) + end + + alias_method :bundled_app1, :bundled_app + + def bundled_app2(*path) + root = tmp.join("bundled_app2") + FileUtils.mkdir_p(root) + root.join(*path) + end + + def vendored_gems(path = nil) + bundled_app(*["vendor/bundle", Gem.ruby_engine, Gem::ConfigMap[:ruby_version], path].compact) + end + + def cached_gem(path) + bundled_app("vendor/cache/#{path}.gem") + end + + def base_system_gems + tmp.join("gems/base") + end + + def gem_repo1(*args) + tmp("gems/remote1", *args) + end + + def gem_repo_missing(*args) + tmp("gems/missing", *args) + end + + def gem_repo2(*args) + tmp("gems/remote2", *args) + end + + def gem_repo3(*args) + tmp("gems/remote3", *args) + end + + def gem_repo4(*args) + tmp("gems/remote4", *args) + end + + def security_repo(*args) + tmp("gems/security_repo", *args) + end + + def system_gem_path(*path) + tmp("gems/system", *path) + end + + def lib_path(*args) + tmp("libs", *args) + end + + def bundler_path + Pathname.new(File.expand_path(root.join("lib"), __FILE__)) + end + + def global_plugin_gem(*args) + home ".bundle", "plugin", "gems", *args + end + + def local_plugin_gem(*args) + bundled_app ".bundle", "plugin", "gems", *args + end + + def tmpdir(*args) + tmp "tmpdir", *args + end + + extend self + end +end diff --git a/spec/bundler/support/permissions.rb b/spec/bundler/support/permissions.rb new file mode 100644 index 0000000000..f5636dd70a --- /dev/null +++ b/spec/bundler/support/permissions.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +module Spec + module Permissions + def with_umask(new_umask) + old_umask = File.umask(new_umask) + yield if block_given? + ensure + File.umask(old_umask) + end + end +end diff --git a/spec/bundler/support/platforms.rb b/spec/bundler/support/platforms.rb new file mode 100644 index 0000000000..a2a3afba00 --- /dev/null +++ b/spec/bundler/support/platforms.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true +module Spec + module Platforms + include Bundler::GemHelpers + + def rb + Gem::Platform::RUBY + end + + def mac + Gem::Platform.new("x86-darwin-10") + end + + def x64_mac + Gem::Platform.new("x86_64-darwin-15") + end + + def java + Gem::Platform.new([nil, "java", nil]) + end + + def linux + Gem::Platform.new(["x86", "linux", nil]) + end + + def mswin + Gem::Platform.new(["x86", "mswin32", nil]) + end + + def mingw + Gem::Platform.new(["x86", "mingw32", nil]) + end + + def x64_mingw + Gem::Platform.new(["x64", "mingw32", nil]) + end + + def all_platforms + [rb, java, linux, mswin, mingw, x64_mingw] + end + + def local + generic_local_platform + end + + def not_local + all_platforms.find {|p| p != generic_local_platform } + end + + def local_tag + if RUBY_PLATFORM == "java" + :jruby + else + :ruby + end + end + + def not_local_tag + [:ruby, :jruby].find {|tag| tag != local_tag } + end + + def local_ruby_engine + ENV["BUNDLER_SPEC_RUBY_ENGINE"] || (defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby") + end + + def local_engine_version + return ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] if ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] + + case local_ruby_engine + when "ruby" + RUBY_VERSION + when "rbx" + Rubinius::VERSION + when "jruby" + JRUBY_VERSION + else + raise BundlerError, "That RUBY_ENGINE is not recognized" + end + end + + def not_local_engine_version + case not_local_tag + when :ruby + not_local_ruby_version + when :jruby + "1.6.1" + end + end + + def not_local_ruby_version + "1.12" + end + + def not_local_patchlevel + 9999 + end + end +end diff --git a/spec/bundler/support/rubygems_ext.rb b/spec/bundler/support/rubygems_ext.rb new file mode 100644 index 0000000000..b484d63eab --- /dev/null +++ b/spec/bundler/support/rubygems_ext.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true +require "rubygems/user_interaction" +require "support/path" unless defined?(Spec::Path) + +module Spec + module Rubygems + DEPS = begin + deps = { + # rack 2.x requires Ruby version >= 2.2.2. + # artifice doesn't support rack 2.x now. + "rack" => "< 2", + # rack-test 0.7.0 dropped 1.8.7 support + # https://github.com/rack-test/rack-test/issues/193#issuecomment-314230318 + "rack-test" => "< 0.7.0", + "artifice" => "~> 0.6.0", + "compact_index" => "~> 0.11.0", + "sinatra" => "~> 1.4.7", + # Rake version has to be consistent for tests to pass + "rake" => "10.0.2", + # 3.0.0 breaks 1.9.2 specs + "builder" => "2.1.2", + "bundler" => "1.12.0", + } + # ruby-graphviz is used by the viz tests + deps["ruby-graphviz"] = nil if RUBY_VERSION >= "1.9.3" + deps + end + + def self.setup + Gem.clear_paths + + ENV["BUNDLE_PATH"] = nil + ENV["GEM_HOME"] = ENV["GEM_PATH"] = Path.base_system_gems.to_s + ENV["PATH"] = ["#{Path.root}/exe", "#{Path.system_gem_path}/bin", ENV["PATH"]].join(File::PATH_SEPARATOR) + + manifest = DEPS.to_a.sort_by(&:first).map {|k, v| "#{k} => #{v}\n" } + manifest_path = "#{Path.base_system_gems}/manifest.txt" + # it's OK if there are extra gems + if !File.exist?(manifest_path) || !(manifest - File.readlines(manifest_path)).empty? + FileUtils.rm_rf(Path.base_system_gems) + FileUtils.mkdir_p(Path.base_system_gems) + puts "installing gems for the tests to use..." + install_gems(DEPS) + File.open(manifest_path, "w") {|f| f << manifest.join } + end + + ENV["HOME"] = Path.home.to_s + ENV["TMPDIR"] = Path.tmpdir.to_s + + Gem::DefaultUserInteraction.ui = Gem::SilentUI.new + end + + def self.install_gems(gems) + reqs, no_reqs = gems.partition {|_, req| !req.nil? && !req.split(" ").empty? } + reqs = reqs.sort_by {|name, _| name == "rack" ? 0 : 1 } # TODO: remove when we drop ruby 1.8.7 support + no_reqs.map!(&:first) + reqs.map! {|name, req| "'#{name}:#{req}'" } + deps = reqs.concat(no_reqs).join(" ") + cmd = "gem install #{deps} --no-rdoc --no-ri --conservative" + puts cmd + system(cmd) || raise("Installing gems #{deps} for the tests to use failed!") + end + end +end diff --git a/spec/bundler/support/silent_logger.rb b/spec/bundler/support/silent_logger.rb new file mode 100644 index 0000000000..1a8f91b3ba --- /dev/null +++ b/spec/bundler/support/silent_logger.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +require "logger" +module Spec + class SilentLogger + (::Logger.instance_methods - Object.instance_methods).each do |logger_instance_method| + define_method(logger_instance_method) {|*args, &blk| } + end + end +end diff --git a/spec/bundler/support/sometimes.rb b/spec/bundler/support/sometimes.rb new file mode 100644 index 0000000000..6a50f5ff4c --- /dev/null +++ b/spec/bundler/support/sometimes.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true +module Sometimes + def run_with_retries(example_to_run, retries) + example = RSpec.current_example + example.metadata[:retries] ||= retries + + retries.times do |t| + example.metadata[:retried] = t + 1 + example.instance_variable_set(:@exception, nil) + example_to_run.run + break unless example.exception + end + + if e = example.exception + new_exception = e.exception(e.message + "[Retried #{retries} times]") + new_exception.set_backtrace e.backtrace + example.instance_variable_set(:@exception, new_exception) + end + end +end + +RSpec.configure do |config| + config.include Sometimes + config.alias_example_to :sometimes, :sometimes => true + config.add_setting :sometimes_retry_count, :default => 5 + + config.around(:each, :sometimes => true) do |example| + retries = example.metadata[:retries] || RSpec.configuration.sometimes_retry_count + run_with_retries(example, retries) + end + + config.after(:suite) do + message = proc do |color, text| + colored = RSpec::Core::Formatters::ConsoleCodes.wrap(text, color) + notification = RSpec::Core::Notifications::MessageNotification.new(colored) + formatter = RSpec.configuration.formatters.first + formatter.message(notification) if formatter.respond_to?(:message) + end + + retried_examples = RSpec.world.example_groups.map do |g| + g.descendants.map do |d| + d.filtered_examples.select do |e| + e.metadata[:sometimes] && e.metadata.fetch(:retried, 1) > 1 + end + end + end.flatten + + message.call(retried_examples.empty? ? :green : :yellow, "\n\nRetried examples: #{retried_examples.count}") + + retried_examples.each do |e| + message.call(:cyan, " #{e.full_description}") + path = RSpec::Core::Metadata.relative_path(e.location) + message.call(:cyan, " [#{e.metadata[:retried]}/#{e.metadata[:retries]}] " + path) + end + end +end diff --git a/spec/bundler/support/streams.rb b/spec/bundler/support/streams.rb new file mode 100644 index 0000000000..561b29092b --- /dev/null +++ b/spec/bundler/support/streams.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +require "stringio" + +def capture(*streams) + streams.map!(&:to_s) + begin + result = StringIO.new + streams.each {|stream| eval "$#{stream} = result" } + yield + ensure + streams.each {|stream| eval("$#{stream} = #{stream.upcase}") } + end + result.string +end diff --git a/spec/bundler/support/sudo.rb b/spec/bundler/support/sudo.rb new file mode 100644 index 0000000000..8c82bb8c0f --- /dev/null +++ b/spec/bundler/support/sudo.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +module Spec + module Sudo + def self.present? + @which_sudo ||= Bundler.which("sudo") + end + + def sudo(cmd) + raise "sudo not present" unless Sudo.present? + sys_exec("sudo #{cmd}") + end + + def chown_system_gems_to_root + sudo "chown -R root #{system_gem_path}" + end + end +end diff --git a/spec/bundler/support/the_bundle.rb b/spec/bundler/support/the_bundle.rb new file mode 100644 index 0000000000..742d393425 --- /dev/null +++ b/spec/bundler/support/the_bundle.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true +require "support/helpers" +require "support/path" + +module Spec + class TheBundle + include Spec::Helpers + include Spec::Path + + attr_accessor :bundle_dir + + def initialize(opts = {}) + opts = opts.dup + @bundle_dir = Pathname.new(opts.delete(:bundle_dir) { bundled_app }) + raise "Too many options! #{opts}" unless opts.empty? + end + + def to_s + "the bundle" + end + alias_method :inspect, :to_s + + def locked? + lockfile.file? + end + + def lockfile + bundle_dir.join("Gemfile.lock") + end + + def locked_gems + raise "Cannot read lockfile if it doesn't exist" unless locked? + Bundler::LockfileParser.new(lockfile.read) + end + end +end diff --git a/spec/bundler/update/gems/post_install_spec.rb b/spec/bundler/update/gems/post_install_spec.rb new file mode 100644 index 0000000000..5a4fe7f321 --- /dev/null +++ b/spec/bundler/update/gems/post_install_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle update" do + let(:config) {} + + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack', "< 1.0" + gem 'thin' + G + + bundle! "config #{config}" if config + + bundle! :install + end + + shared_examples "a config observer" do + context "when ignore post-install messages for gem is set" do + let(:config) { "ignore_messages.rack true" } + + it "doesn't display gem's post-install message" do + expect(out).not_to include("Rack's post install message") + end + end + + context "when ignore post-install messages for all gems" do + let(:config) { "ignore_messages true" } + + it "doesn't display any post-install messages" do + expect(out).not_to include("Post-install message") + end + end + end + + shared_examples "a post-install message outputter" do + it "should display post-install messages for updated gems" do + expect(out).to include("Post-install message from rack:") + expect(out).to include("Rack's post install message") + end + + it "should not display the post-install message for non-updated gems" do + expect(out).not_to include("Thin's post install message") + end + end + + context "when listed gem is updated" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack' + gem 'thin' + G + + bundle! :update + end + + it_behaves_like "a post-install message outputter" + it_behaves_like "a config observer" + end + + context "when dependency triggers update" do + before do + gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack-obama' + gem 'thin' + G + + bundle! :update + end + + it_behaves_like "a post-install message outputter" + it_behaves_like "a config observer" + end +end diff --git a/spec/bundler/update/git_spec.rb b/spec/bundler/update/git_spec.rb new file mode 100644 index 0000000000..021c8c942b --- /dev/null +++ b/spec/bundler/update/git_spec.rb @@ -0,0 +1,333 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "bundle update" do + describe "git sources" do + it "floats on a branch when :branch is used" do + build_git "foo", "1.0" + update_git "foo", :branch => "omg" + + install_gemfile <<-G + git "#{lib_path("foo-1.0")}", :branch => "omg" do + gem 'foo' + end + G + + update_git "foo", :branch => "omg" do |s| + s.write "lib/foo.rb", "FOO = '1.1'" + end + + bundle "update" + + expect(the_bundle).to include_gems "foo 1.1" + end + + it "updates correctly when you have like craziness" do + build_lib "activesupport", "3.0", :path => lib_path("rails/activesupport") + build_git "rails", "3.0", :path => lib_path("rails") do |s| + s.add_dependency "activesupport", "= 3.0" + end + + install_gemfile <<-G + gem "rails", :git => "#{lib_path("rails")}" + G + + bundle "update rails" + expect(out).to include("Using activesupport 3.0 from #{lib_path("rails")} (at master@#{revision_for(lib_path("rails"))[0..6]})") + expect(the_bundle).to include_gems "rails 3.0", "activesupport 3.0" + end + + it "floats on a branch when :branch is used and the source is specified in the update" do + build_git "foo", "1.0", :path => lib_path("foo") + update_git "foo", :branch => "omg", :path => lib_path("foo") + + install_gemfile <<-G + git "#{lib_path("foo")}", :branch => "omg" do + gem 'foo' + end + G + + update_git "foo", :branch => "omg", :path => lib_path("foo") do |s| + s.write "lib/foo.rb", "FOO = '1.1'" + end + + bundle "update --source foo" + + expect(the_bundle).to include_gems "foo 1.1" + end + + it "floats on master when updating all gems that are pinned to the source even if you have child dependencies" do + build_git "foo", :path => lib_path("foo") + build_gem "bar", :to_system => true do |s| + s.add_dependency "foo" + end + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo")}" + gem "bar" + G + + update_git "foo", :path => lib_path("foo") do |s| + s.write "lib/foo.rb", "FOO = '1.1'" + end + + bundle "update foo" + + expect(the_bundle).to include_gems "foo 1.1" + end + + it "notices when you change the repo url in the Gemfile" do + build_git "foo", :path => lib_path("foo_one") + build_git "foo", :path => lib_path("foo_two") + + install_gemfile <<-G + gem "foo", "1.0", :git => "#{lib_path("foo_one")}" + G + + FileUtils.rm_rf lib_path("foo_one") + + install_gemfile <<-G + gem "foo", "1.0", :git => "#{lib_path("foo_two")}" + G + + expect(err).to lack_errors + expect(out).to include("Fetching #{lib_path}/foo_two") + expect(out).to include("Bundle complete!") + end + + it "fetches tags from the remote" do + build_git "foo" + @remote = build_git("bar", :bare => true) + update_git "foo", :remote => @remote.path + update_git "foo", :push => "master" + + install_gemfile <<-G + gem 'foo', :git => "#{@remote.path}" + G + + # Create a new tag on the remote that needs fetching + update_git "foo", :tag => "fubar" + update_git "foo", :push => "fubar" + + gemfile <<-G + gem 'foo', :git => "#{@remote.path}", :tag => "fubar" + G + + bundle "update" + expect(exitstatus).to eq(0) if exitstatus + end + + describe "with submodules" do + before :each do + build_gem "submodule", :to_system => true do |s| + s.write "lib/submodule.rb", "puts 'GEM'" + end + + build_git "submodule", "1.0" do |s| + s.write "lib/submodule.rb", "puts 'GIT'" + end + + build_git "has_submodule", "1.0" do |s| + s.add_dependency "submodule" + end + + Dir.chdir(lib_path("has_submodule-1.0")) do + sys_exec "git submodule add #{lib_path("submodule-1.0")} submodule-1.0" + `git commit -m "submodulator"` + end + end + + it "it unlocks the source when submodules are added to a git source" do + install_gemfile <<-G + git "#{lib_path("has_submodule-1.0")}" do + gem "has_submodule" + end + G + + run "require 'submodule'" + expect(out).to eq("GEM") + + install_gemfile <<-G + git "#{lib_path("has_submodule-1.0")}", :submodules => true do + gem "has_submodule" + end + G + + run "require 'submodule'" + expect(out).to eq("GIT") + end + + it "unlocks the source when submodules are removed from git source", :git => ">= 2.9.0" do + install_gemfile <<-G + git "#{lib_path("has_submodule-1.0")}", :submodules => true do + gem "has_submodule" + end + G + + run "require 'submodule'" + expect(out).to eq("GIT") + + install_gemfile <<-G + git "#{lib_path("has_submodule-1.0")}" do + gem "has_submodule" + end + G + + run "require 'submodule'" + expect(out).to eq("GEM") + end + end + + it "errors with a message when the .git repo is gone" do + build_git "foo", "1.0" + + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + lib_path("foo-1.0").join(".git").rmtree + + bundle :update + expect(out).to include(lib_path("foo-1.0").to_s) + end + + it "should not explode on invalid revision on update of gem by name" do + build_git "rack", "0.8" + + build_git "rack", "0.8", :path => lib_path("local-rack") do |s| + s.write "lib/rack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + G + + bundle %(config local.rack #{lib_path("local-rack")}) + bundle "update rack" + expect(out).to include("Bundle updated!") + end + + it "shows the previous version of the gem" do + build_git "rails", "3.0", :path => lib_path("rails") + + install_gemfile <<-G + gem "rails", :git => "#{lib_path("rails")}" + G + + lockfile <<-G + GIT + remote: #{lib_path("rails")} + specs: + rails (2.3.2) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + rails! + G + + bundle "update" + expect(out).to include("Using rails 3.0 (was 2.3.2) from #{lib_path("rails")} (at master@#{revision_for(lib_path("rails"))[0..6]})") + end + end + + describe "with --source flag" do + before :each do + build_repo2 + @git = build_git "foo", :path => lib_path("foo") do |s| + s.executables = "foobar" + end + + install_gemfile <<-G + source "file://#{gem_repo2}" + git "#{lib_path("foo")}" do + gem 'foo' + end + gem 'rack' + G + end + + it "updates the source" do + update_git "foo", :path => @git.path + + bundle "update --source foo" + + in_app_root do + run <<-RUBY + require 'foo' + puts "WIN" if defined?(FOO_PREV_REF) + RUBY + + expect(out).to eq("WIN") + end + end + + it "unlocks gems that were originally pulled in by the source" do + update_git "foo", "2.0", :path => @git.path + + bundle "update --source foo" + expect(the_bundle).to include_gems "foo 2.0" + end + + it "leaves all other gems frozen" do + update_repo2 + update_git "foo", :path => @git.path + + bundle "update --source foo" + expect(the_bundle).to include_gems "rack 1.0" + end + end + + context "when the gem and the repository have different names" do + before :each do + build_repo2 + @git = build_git "foo", :path => lib_path("bar") + + install_gemfile <<-G + source "file://#{gem_repo2}" + git "#{lib_path("bar")}" do + gem 'foo' + end + gem 'rack' + G + end + + it "the --source flag updates version of gems that were originally pulled in by the source" do + spec_lines = lib_path("bar/foo.gemspec").read.split("\n") + spec_lines[5] = "s.version = '2.0'" + + update_git "foo", "2.0", :path => @git.path do |s| + s.write "foo.gemspec", spec_lines.join("\n") + end + + ref = @git.ref_for "master" + + bundle "update --source bar" + + lockfile_should_be <<-G + GIT + remote: #{@git.path} + revision: #{ref} + specs: + foo (2.0) + + GEM + remote: file:#{gem_repo2}/ + specs: + rack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo! + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + end +end diff --git a/spec/bundler/update/path_spec.rb b/spec/bundler/update/path_spec.rb new file mode 100644 index 0000000000..5ac4f7b1fe --- /dev/null +++ b/spec/bundler/update/path_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe "path sources" do + describe "bundle update --source" do + it "shows the previous version of the gem when updated from path source" do + build_lib "activesupport", "2.3.5", :path => lib_path("rails/activesupport") + + install_gemfile <<-G + gem "activesupport", :path => "#{lib_path("rails/activesupport")}" + G + + build_lib "activesupport", "3.0", :path => lib_path("rails/activesupport") + + bundle "update --source activesupport" + expect(out).to include("Using activesupport 3.0 (was 2.3.5) from source at `#{lib_path("rails/activesupport")}`") + end + end +end |