diff options
Diffstat (limited to 'spec/bundler')
292 files changed, 38555 insertions, 14968 deletions
diff --git a/spec/bundler/bundler/build_metadata_spec.rb b/spec/bundler/bundler/build_metadata_spec.rb index afa2d1716f..2e69821f68 100644 --- a/spec/bundler/bundler/build_metadata_spec.rb +++ b/spec/bundler/bundler/build_metadata_spec.rb @@ -6,18 +6,20 @@ require "bundler/build_metadata" RSpec.describe Bundler::BuildMetadata do before do allow(Time).to receive(:now).and_return(Time.at(0)) - Bundler::BuildMetadata.instance_variable_set(:@built_at, nil) + Bundler::BuildMetadata.instance_variable_set(:@timestamp, nil) end - describe "#built_at" do - it "returns %Y-%m-%d formatted time" do - expect(Bundler::BuildMetadata.built_at).to eq "1970-01-01" + describe "#timestamp" do + it "returns %Y-%m-%d formatted current time if built_at not set" do + Bundler::BuildMetadata.instance_variable_set(:@built_at, nil) + expect(Bundler::BuildMetadata.timestamp).to eq "1970-01-01" end - end - describe "#release?" do - it "returns false as default" do - expect(Bundler::BuildMetadata.release?).to be_falsey + it "returns %Y-%m-%d formatted current time if built_at not set" do + Bundler::BuildMetadata.instance_variable_set(:@built_at, "2025-01-01") + expect(Bundler::BuildMetadata.timestamp).to eq "2025-01-01" + ensure + Bundler::BuildMetadata.instance_variable_set(:@built_at, nil) end end @@ -40,10 +42,9 @@ RSpec.describe Bundler::BuildMetadata do describe "#to_h" do subject { Bundler::BuildMetadata.to_h } - it "returns a hash includes Built At, Git SHA and Released Version" do - expect(subject["Built At"]).to eq "1970-01-01" + it "returns a hash includes Timestamp, and Git SHA" do + expect(subject["Timestamp"]).to eq "1970-01-01" expect(subject["Git SHA"]).to be_instance_of(String) - expect(subject["Released Version"]).to be_falsey end end end diff --git a/spec/bundler/bundler/bundler_spec.rb b/spec/bundler/bundler/bundler_spec.rb index 247838600b..bddcbdaef3 100644 --- a/spec/bundler/bundler/bundler_spec.rb +++ b/spec/bundler/bundler/bundler_spec.rb @@ -4,6 +4,71 @@ require "bundler" require "tmpdir" RSpec.describe Bundler do + describe "#load_marshal" do + it "is a private method and raises an error" do + data = Marshal.dump(Bundler) + expect { Bundler.load_marshal(data) }.to raise_error(NoMethodError, /private method [`']load_marshal' called/) + end + + it "loads any data" do + data = Marshal.dump(Bundler) + expect(Bundler.send(:load_marshal, data)).to eq(Bundler) + end + end + + describe "#safe_load_marshal" do + it "fails on unexpected class" do + data = Marshal.dump(Bundler) + expect { Bundler.safe_load_marshal(data) }.to raise_error(Bundler::MarshalError) + end + + it "loads simple structure" do + simple_structure = { "name" => [:development] } + data = Marshal.dump(simple_structure) + expect(Bundler.safe_load_marshal(data)).to eq(simple_structure) + end + + it "loads Gem::Specification" do + gem_spec = Gem::Specification.new do |s| + s.name = "bundler" + s.version = Gem::Version.new("2.4.7") + s.installed_by_version = Gem::Version.new("0") + s.authors = ["André Arko", + "Samuel Giddins", + "Colby Swandale", + "Hiroshi Shibata", + "David Rodríguez", + "Grey Baker", + "Stephanie Morillo", + "Chris Morris", + "James Wen", + "Tim Moore", + "André Medeiros", + "Jessica Lynn Suttles", + "Terence Lee", + "Carl Lerche", + "Yehuda Katz"] + s.date = Time.utc(2023, 2, 15) + s.description = "Bundler manages an application's dependencies through its entire life, across many machines, systematically and repeatably" + s.email = ["team@bundler.io"] + s.homepage = "https://bundler.io" + s.metadata = { "bug_tracker_uri" => "https://github.com/ruby/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3ABundler", + "changelog_uri" => "https://github.com/ruby/rubygems/blob/master/bundler/CHANGELOG.md", + "homepage_uri" => "https://bundler.io/", + "source_code_uri" => "https://github.com/ruby/rubygems/tree/master/bundler" } + s.require_paths = ["lib"] + s.required_ruby_version = Gem::Requirement.new([">= 2.6.0"]) + s.required_rubygems_version = Gem::Requirement.new([">= 3.0.1"]) + s.rubygems_version = "3.4.7" + s.specification_version = 4 + s.summary = "The best way to manage your application's dependencies" + s.license = false + end + data = Marshal.dump(gem_spec) + expect(Bundler.safe_load_marshal(data)).to eq(gem_spec) + end + end + describe "#load_gemspec_uncached" do let(:app_gemspec_path) { tmp("test.gemspec") } subject { Bundler.load_gemspec_uncached(app_gemspec_path) } @@ -11,7 +76,7 @@ RSpec.describe Bundler do context "with incorrect YAML file" do before do File.open(app_gemspec_path, "wb") do |f| - f.write strip_whitespace(<<-GEMSPEC) + f.write <<~GEMSPEC --- {:!00 ao=gu\g1= 7~f GEMSPEC @@ -21,33 +86,9 @@ RSpec.describe Bundler do 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 + 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 @@ -58,7 +99,7 @@ RSpec.describe Bundler do $VERBOSE = verbose File.open(app_gemspec_path, "wb") do |file| - file.puts <<-GEMSPEC.gsub(/^\s+/, "") + file.puts <<~GEMSPEC # -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.author = "André the Giant" @@ -98,7 +139,7 @@ RSpec.describe Bundler do end GEMSPEC end - expect(Bundler.rubygems).to receive(:validate).with have_attributes(:name => "validated") + expect(Bundler.rubygems).to receive(:validate).with have_attributes(name: "validated") subject end end @@ -106,7 +147,7 @@ RSpec.describe Bundler do context "with gemspec containing local variables" do before do File.open(app_gemspec_path, "wb") do |f| - f.write strip_whitespace(<<-GEMSPEC) + f.write <<~GEMSPEC must_not_leak = true Gem::Specification.new do |gem| gem.name = "leak check" @@ -123,68 +164,45 @@ RSpec.describe Bundler do 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 + it "can detect relative path" do + script_path = bundled_app("tmp/test_command") + create_file(script_path, "#!/usr/bin/env ruby\n") - subject { described_class.which(executable) } + result = Dir.chdir script_path.dirname.dirname do + Bundler.which("test_command") + end + expect(result).to eq(nil) - shared_examples_for "it returns the correct executable" do - it "returns the expected file" do - expect(subject).to eq(expected) + result = Dir.chdir script_path.dirname do + Bundler.which("test_command") end + + expect(result).to eq("test_command") unless Gem.win_platform? + expect(result).to eq("test_command.bat") if Gem.win_platform? end - it_behaves_like "it returns the correct executable" + it "can detect absolute path" do + create_file("test_command", "#!/usr/bin/env ruby\n") - context "when the executable in inside a quoted path" do - let(:expected) { "/e/executable" } - it_behaves_like "it returns the correct executable" + ENV["PATH"] = bundled_app("test_command").parent.to_s + + result = Bundler.which("test_command") + expect(result).to eq(bundled_app("test_command").to_s) unless Gem.win_platform? + expect(result).to eq(bundled_app("test_command.bat").to_s) if Gem.win_platform? end - context "when the executable is not found" do - let(:expected) { nil } - it_behaves_like "it returns the correct executable" + it "returns nil when not found" do + result = Bundler.which("test_command") + expect(result).to eq(nil) end end describe "configuration" do context "disable_shared_gems" do it "should unset GEM_PATH with empty string" do - env = {} expect(Bundler).to receive(:use_system_gems?).and_return(false) - Bundler.send(:configure_gem_path, env) - expect(env.keys).to include("GEM_PATH") - expect(env["GEM_PATH"]).to eq "" - end - end - end - - describe "#rm_rf" do - context "the directory is world writable" do - let(:bundler_ui) { Bundler.ui } - it "should raise a friendly error" do - allow(File).to receive(:exist?).and_return(true) - allow(::Bundler::FileUtils).to receive(:remove_entry_secure).and_raise(ArgumentError) - allow(File).to receive(:world_writable?).and_return(true) - message = <<EOF -It is a security vulnerability to allow your home directory to be world-writable, and bundler can not continue. -You should probably consider fixing this issue by running `chmod o-w ~` on *nix. -Please refer to https://ruby-doc.org/stdlib-2.1.2/libdoc/fileutils/rdoc/FileUtils.html#method-c-remove_entry_secure for details. -EOF - expect(bundler_ui).to receive(:warn).with(message) - expect { Bundler.send(:rm_rf, bundled_app) }.to raise_error(Bundler::PathError) + Bundler.send(:configure_gem_path) + expect(ENV["GEM_PATH"]).to eq "" end end end @@ -192,28 +210,14 @@ EOF describe "#mkdir_p" do it "creates a folder at the given path" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - Bundler.mkdir_p(bundled_app.join("foo", "bar")) - expect(bundled_app.join("foo", "bar")).to exist - end - - context "when mkdir_p requires sudo" do - it "creates a new folder using sudo" do - expect(Bundler).to receive(:requires_sudo?).and_return(true) - expect(Bundler).to receive(:sudo).and_return true - Bundler.mkdir_p(bundled_app.join("foo")) - end - end + allow(Bundler).to receive(:root).and_return(bundled_app) - context "with :no_sudo option" do - it "forces mkdir_p to not use sudo" do - expect(Bundler).to receive(:requires_sudo?).and_return(true) - expect(Bundler).to_not receive(:sudo) - Bundler.mkdir_p(bundled_app.join("foo"), :no_sudo => true) - end + Bundler.mkdir_p(bundled_app("foo", "bar")) + expect(bundled_app("foo", "bar")).to exist end end @@ -233,11 +237,8 @@ EOF allow(Bundler.rubygems).to receive(:user_home).and_return(path) allow(File).to receive(:directory?).with(path).and_return false allow(Bundler).to receive(:tmp).and_return(Pathname.new("/tmp/trulyrandom")) - message = <<EOF -`/home/oggy` is not a directory. -Bundler will use `/tmp/trulyrandom' as your home directory temporarily. -EOF - expect(Bundler.ui).to receive(:warn).with(message) + expect(Bundler.ui).to receive(:warn).with("`/home/oggy` is not a directory.\n") + expect(Bundler.ui).to receive(:warn).with("Bundler will use `/tmp/trulyrandom' as your home directory temporarily.\n") expect(Bundler.user_home).to eq(Pathname("/tmp/trulyrandom")) end end @@ -249,14 +250,12 @@ EOF it "should issue a warning and return a temporary user home" do allow(Bundler.rubygems).to receive(:user_home).and_return(path) allow(File).to receive(:directory?).with(path).and_return true + allow(File).to receive(:writable?).and_call_original allow(File).to receive(:writable?).with(path).and_return false allow(File).to receive(:directory?).with(dotbundle).and_return false allow(Bundler).to receive(:tmp).and_return(Pathname.new("/tmp/trulyrandom")) - message = <<EOF -`/home/oggy` is not writable. -Bundler will use `/tmp/trulyrandom' as your home directory temporarily. -EOF - expect(Bundler.ui).to receive(:warn).with(message) + expect(Bundler.ui).to receive(:warn).with("`/home/oggy` is not writable.\n") + expect(Bundler.ui).to receive(:warn).with("Bundler will use `/tmp/trulyrandom' as your home directory temporarily.\n") expect(Bundler.user_home).to eq(Pathname("/tmp/trulyrandom")) end @@ -277,128 +276,13 @@ EOF it "should issue warning and return a temporary user home" do allow(Bundler.rubygems).to receive(:user_home).and_return(nil) allow(Bundler).to receive(:tmp).and_return(Pathname.new("/tmp/trulyrandom")) - message = <<EOF -Your home directory is not set. -Bundler will use `/tmp/trulyrandom' as your home directory temporarily. -EOF - expect(Bundler.ui).to receive(:warn).with(message) + expect(Bundler.ui).to receive(:warn).with("Your home directory is not set.\n") + expect(Bundler.ui).to receive(:warn).with("Bundler will use `/tmp/trulyrandom' as your home directory temporarily.\n") expect(Bundler.user_home).to eq(Pathname("/tmp/trulyrandom")) end end end - describe "#requires_sudo?" do - let!(:tmpdir) { Dir.mktmpdir } - let(:bundle_path) { Pathname("#{tmpdir}/bundle") } - - def clear_cached_requires_sudo - return unless Bundler.instance_variable_defined?(:@requires_sudo_ran) - Bundler.remove_instance_variable(:@requires_sudo_ran) - Bundler.remove_instance_variable(:@requires_sudo) - end - - before do - clear_cached_requires_sudo - allow(Bundler).to receive(:which).with("sudo").and_return("/usr/bin/sudo") - allow(Bundler).to receive(:bundle_path).and_return(bundle_path) - end - - after do - FileUtils.rm_rf(tmpdir) - clear_cached_requires_sudo - end - - subject { Bundler.requires_sudo? } - - context "bundle_path doesn't exist" do - it { should be false } - - context "and parent dir can't be written" do - before do - FileUtils.chmod(0o500, tmpdir) - end - - it { should be true } - end - - context "with unwritable files in a parent dir" do - # Regression test for https://github.com/bundler/bundler/pull/6316 - # It doesn't matter if there are other unwritable files so long as - # bundle_path can be created - before do - file = File.join(tmpdir, "unrelated_file") - FileUtils.touch(file) - FileUtils.chmod(0o400, file) - end - - it { should be false } - end - end - - context "bundle_path exists" do - before do - FileUtils.mkdir_p(bundle_path) - end - - it { should be false } - - context "and is unwritable" do - before do - FileUtils.chmod(0o500, bundle_path) - end - - it { should be true } - end - end - - context "path writability" do - before do - FileUtils.mkdir_p("tmp/vendor/bundle") - FileUtils.mkdir_p("tmp/vendor/bin_dir") - end - after do - FileUtils.rm_rf("tmp/vendor/bundle") - FileUtils.rm_rf("tmp/vendor/bin_dir") - end - context "writable paths" do - it "should return false and display nothing" do - allow(Bundler).to receive(:bundle_path).and_return(Pathname("tmp/vendor/bundle")) - expect(Bundler.ui).to_not receive(:warn) - expect(Bundler.requires_sudo?).to eq(false) - end - end - context "unwritable paths" do - before do - FileUtils.touch("tmp/vendor/bundle/unwritable1.txt") - FileUtils.touch("tmp/vendor/bundle/unwritable2.txt") - FileUtils.touch("tmp/vendor/bin_dir/unwritable3.txt") - FileUtils.chmod(0o400, "tmp/vendor/bundle/unwritable1.txt") - FileUtils.chmod(0o400, "tmp/vendor/bundle/unwritable2.txt") - FileUtils.chmod(0o400, "tmp/vendor/bin_dir/unwritable3.txt") - end - it "should return true and display warn message" do - allow(Bundler).to receive(:bundle_path).and_return(Pathname("tmp/vendor/bundle")) - bin_dir = Pathname("tmp/vendor/bin_dir/") - - # allow File#writable? to be called with args other than the stubbed on below - allow(File).to receive(:writable?).and_call_original - - # fake make the directory unwritable - allow(File).to receive(:writable?).with(bin_dir).and_return(false) - allow(Bundler).to receive(:system_bindir).and_return(Pathname("tmp/vendor/bin_dir/")) - message = <<-MESSAGE.chomp -Following files may not be writable, so sudo is needed: - tmp/vendor/bin_dir/ - tmp/vendor/bundle/unwritable1.txt - tmp/vendor/bundle/unwritable2.txt -MESSAGE - expect(Bundler.ui).to receive(:warn).with(message) - expect(Bundler.requires_sudo?).to eq(true) - end - end - end - end - context "user cache dir" do let(:home_path) { Pathname.new(ENV["HOME"]) } diff --git a/spec/bundler/bundler/ci_detector_spec.rb b/spec/bundler/bundler/ci_detector_spec.rb new file mode 100644 index 0000000000..299d8005e8 --- /dev/null +++ b/spec/bundler/bundler/ci_detector_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::CIDetector do + # This is properly tested in rubygems, under the name Gem::CIDetector + # But the test that confirms that our version _stays in sync_ with that version + # will live here. + + it "stays in sync with the rubygems implementation" do + rubygems_implementation_path = File.join(git_root, "lib", "rubygems", "ci_detector.rb") + expect(File.exist?(rubygems_implementation_path)).to be_truthy + rubygems_code = File.read(rubygems_implementation_path) + denamespaced_rubygems_code = rubygems_code.sub("Gem", "NAMESPACE") + + bundler_implementation_path = File.join(source_lib_dir, "bundler", "ci_detector.rb") + expect(File.exist?(bundler_implementation_path)).to be_truthy + bundler_code = File.read(bundler_implementation_path) + denamespaced_bundler_code = bundler_code.sub("Bundler", "NAMESPACE") + + expect(denamespaced_bundler_code).to eq(denamespaced_rubygems_code) + end +end diff --git a/spec/bundler/bundler/cli_common_spec.rb b/spec/bundler/bundler/cli_common_spec.rb new file mode 100644 index 0000000000..015894b3a1 --- /dev/null +++ b/spec/bundler/bundler/cli_common_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "bundler/cli" + +RSpec.describe Bundler::CLI::Common do + describe "gem_not_found_message" do + it "should suggest alternate gem names" do + message = subject.gem_not_found_message("ralis", ["BOGUS"]) + expect(message).to match("Could not find gem 'ralis'.$") + message = subject.gem_not_found_message("ralis", ["rails"]) + expect(message).to match("Did you mean 'rails'?") + message = subject.gem_not_found_message("Rails", ["rails"]) + expect(message).to match("Did you mean 'rails'?") + message = subject.gem_not_found_message("meail", %w[email fail eval]) + expect(message).to match("Did you mean 'email'?") + message = subject.gem_not_found_message("nokogri", %w[nokogiri rails sidekiq dog]) + expect(message).to match("Did you mean 'nokogiri'?") + message = subject.gem_not_found_message("methosd", %w[method methods bogus]) + expect(message).to match(/Did you mean 'method(|s)' or 'method(|s)'?/) + end + end +end diff --git a/spec/bundler/bundler/cli_spec.rb b/spec/bundler/bundler/cli_spec.rb index ddcd699d6c..56caf9937e 100644 --- a/spec/bundler/bundler/cli_spec.rb +++ b/spec/bundler/bundler/cli_spec.rb @@ -4,16 +4,18 @@ 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 + bundle "--invalid_argument", raise_on_error: false + expect(exitstatus).to_not be_zero end it "returns non-zero exit status when passed unrecognized task" do - bundle "unrecognized-task" - expect(exitstatus).to_not be_zero if exitstatus + bundle "unrecognized-task", raise_on_error: false + expect(exitstatus).to_not be_zero end it "looks for a binary and executes it if it's named bundler-<task>" do + skip "Could not find command testtasks, probably because not a windows friendly executable" if Gem.win_platform? + File.open(tmp("bundler-testtasks"), "w", 0o755) do |f| ruby = ENV["RUBY"] || "/usr/bin/env ruby" f.puts "#!#{ruby}\nputs 'Hello, world'\n" @@ -23,7 +25,6 @@ RSpec.describe "bundle executable" do bundle "testtasks" end - expect(exitstatus).to be_zero if exitstatus expect(out).to eq("Hello, world") end @@ -31,125 +32,187 @@ RSpec.describe "bundle executable" do it "aliases e to exec" do bundle "e --help" - expect(out).to include("BUNDLE-EXEC") + expect(out_with_macos_man_workaround).to include("bundle-exec") end it "aliases ex to exec" do bundle "ex --help" - expect(out).to include("BUNDLE-EXEC") + expect(out_with_macos_man_workaround).to include("bundle-exec") end it "aliases exe to exec" do bundle "exe --help" - expect(out).to include("BUNDLE-EXEC") + expect(out_with_macos_man_workaround).to include("bundle-exec") end it "aliases c to check" do bundle "c --help" - expect(out).to include("BUNDLE-CHECK") + expect(out_with_macos_man_workaround).to include("bundle-check") end it "aliases i to install" do bundle "i --help" - expect(out).to include("BUNDLE-INSTALL") + expect(out_with_macos_man_workaround).to include("bundle-install") end it "aliases ls to list" do bundle "ls --help" - expect(out).to include("BUNDLE-LIST") + expect(out_with_macos_man_workaround).to include("bundle-list") end it "aliases package to cache" do bundle "package --help" - expect(out).to include("BUNDLE-CACHE") + expect(out_with_macos_man_workaround).to include("bundle-cache") end it "aliases pack to cache" do bundle "pack --help" - expect(out).to include("BUNDLE-CACHE") + expect(out_with_macos_man_workaround).to include("bundle-cache") + end + + private + + # Some `man` (e.g., on macOS) always highlights the output even to + # non-tty. + def out_with_macos_man_workaround + out.gsub(/.[\b]/, "") end end context "with no arguments" do - it "prints a concise help message", :bundler => "3" do - bundle! "" - expect(err).to be_empty + it "tries to installs by default but print help on missing Gemfile" do + bundle "", raise_on_error: false + expect(err).to include("Could not locate Gemfile") + expect(out).to include("In a future version of Bundler") + expect(out).to include("Bundler version #{Bundler::VERSION}"). and include("\n\nBundler commands:\n\n"). and include("\n\n Primary commands:\n"). and include("\n\n Utilities:\n"). and include("\n\nOptions:\n") end + + it "runs bundle install when default_cli_command set to install" do + bundle_config "default_cli_command install" + bundle "", raise_on_error: false + expect(out).to_not include("In a future version of Bundler") + expect(err).to include("Could not locate Gemfile") + expect(exitstatus).to_not be_zero + end end context "when ENV['BUNDLE_GEMFILE'] is set to an empty string" do it "ignores it" do - gemfile bundled_app("Gemfile"), <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + gemfile bundled_app_gemfile, <<-G + source "https://gem.repo1" + gem 'myrack' G - bundle :install, :env => { "BUNDLE_GEMFILE" => "" } + bundle :install, env: { "BUNDLE_GEMFILE" => "" } - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 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_uri_for(gem_repo1)}" - gem 'rack' - G + context "with --verbose" do + before do + gemfile "source 'https://gem.repo1'" + end - bundle :install, :env => { "RUBYGEMS_GEMDEPS" => "foo" } - expect(err).to include("RUBYGEMS_GEMDEPS") - expect(err).to include("conflict with Bundler") + it "prints the running command" do + bundle "info bundler", verbose: true + expect(out).to start_with("Running `bundle info bundler --verbose` with bundler #{Bundler::VERSION}") + + bundle "install", verbose: true + expect(out).to start_with("Running `bundle install --verbose` with bundler #{Bundler::VERSION}") + end - bundle :install, :env => { "RUBYGEMS_GEMDEPS" => "" } - expect(err).not_to include("RUBYGEMS_GEMDEPS") + it "prints the simulated version too when setting is enabled" do + bundle "config set simulate_version 4", verbose: true + bundle "info bundler", verbose: true + expect(out).to start_with("Running `bundle info bundler --verbose` with bundler #{Bundler::VERSION} (simulating Bundler 4)") end end - context "with --verbose" do + context "with verbose configuration" do + before do + bundle_config "verbose true" + end + it "prints the running command" do - gemfile "" - bundle! "info bundler", :verbose => true - expect(out).to start_with("Running `bundle info bundler --verbose` with bundler #{Bundler::VERSION}") + gemfile "source 'https://gem.repo1'" + bundle "info bundler" + expect(out).to start_with("Running `bundle info bundler` with bundler #{Bundler::VERSION}") end + end + + describe "bundle outdated" do + let(:run_command) do + bundle "install" - it "doesn't print defaults" do - install_gemfile! "", :verbose => true - expect(out).to start_with("Running `bundle install --retry 0 --verbose` with bundler #{Bundler::VERSION}") + bundle "outdated #{flags}", raise_on_error: false end - it "doesn't print defaults" do - install_gemfile! "", :verbose => true - expect(out).to start_with("Running `bundle install --retry 0 --verbose` with bundler #{Bundler::VERSION}") + before do + gemfile <<-G + source "https://gem.repo1" + gem "myrack", '0.9.1' + G + end + + context "with --groups flag" do + let(:flags) { "--groups" } + + it "prints a message when there are outdated gems" do + run_command + + expect(out).to include("Gem Current Latest Requested Groups") + expect(out).to include("myrack 0.9.1 1.0.0 = 0.9.1 default") + end + end + + context "with --parseable" do + let(:flags) { "--parseable" } + + it "prints a message when there are outdated gems" do + run_command + + expect(out).to include("myrack (newest 1.0.0, installed 0.9.1, requested = 0.9.1)") + end + end + + context "with --groups and --parseable" do + let(:flags) { "--groups --parseable" } + + it "prints a simplified message when there are outdated gems" do + run_command + + expect(out).to include("myrack (newest 1.0.0, installed 0.9.1, requested = 0.9.1)") + end end end describe "printing the outdated warning" do shared_examples_for "no warning" do it "prints no warning" do - bundle "fail" - expect(last_command.stdboth).to eq("Could not find command \"fail\".") + bundle "fail", env: { "BUNDLER_VERSION" => bundler_version }, raise_on_error: false + expect(stdboth).to eq("Could not find command \"fail\".") end end - let(:bundler_version) { "1.1" } + let(:bundler_version) { "2.0" } let(:latest_version) { nil } before do - bundle! "config set --global disable_version_check false" + bundle_config_global "disable_version_check false" - simulate_bundler_version(bundler_version) + pristine_system_gems "bundler-#{bundler_version}" if latest_version info_path = home(".bundle/cache/compact_index/rubygems.org.443.29b0360b937aa4d161703e6160654e47/info/bundler") info_path.parent.mkpath @@ -174,35 +237,37 @@ RSpec.describe "bundle executable" do context "when the latest version is greater than the current version" do let(:latest_version) { "222.0" } it "prints the version warning" do - bundle "fail" + bundle "fail", env: { "BUNDLER_VERSION" => bundler_version }, raise_on_error: false expect(err).to start_with(<<-EOS.strip) The latest bundler is #{latest_version}, but you are currently running #{bundler_version}. -To install the latest version, run `gem install bundler` +To update to the most recent version, run `bundle update --bundler` EOS end context "and disable_version_check is set" do - before { bundle! "config set disable_version_check true" } + before { bundle "config set disable_version_check true", env: { "BUNDLER_VERSION" => bundler_version } } include_examples "no warning" end context "running a parseable command" do it "prints no warning" do - bundle! "config get --parseable foo" - expect(last_command.stdboth).to eq "" + bundle "config set foo value", env: { "BUNDLER_VERSION" => bundler_version } + bundle "config get --parseable foo", env: { "BUNDLER_VERSION" => bundler_version } + expect(out).to eq "foo=value" + expect(err).to eq "" - bundle "platform --ruby" - expect(last_command.stdboth).to eq "Could not locate Gemfile" + bundle "platform --ruby", env: { "BUNDLER_VERSION" => bundler_version }, raise_on_error: false + expect(stdboth).to eq "Could not locate Gemfile" end end context "and is a pre-release" do let(:latest_version) { "222.0.0.pre.4" } it "prints the version warning" do - bundle "fail" + bundle "fail", env: { "BUNDLER_VERSION" => bundler_version }, raise_on_error: false expect(err).to start_with(<<-EOS.strip) The latest bundler is #{latest_version}, but you are currently running #{bundler_version}. -To install the latest version, run `gem install bundler --pre` +To update to the most recent version, run `bundle update --bundler` EOS end end @@ -211,13 +276,23 @@ To install the latest version, run `gem install bundler --pre` end RSpec.describe "bundler executable" do - it "shows the bundler version just as the `bundle` executable does", :bundler => "< 3" do + it "shows the bundler version just as the `bundle` executable does" do bundler "--version" - expect(out).to eq("Bundler version #{Bundler::VERSION}") - end + expect(out).to eq(Bundler::VERSION.to_s) - it "shows the bundler version just as the `bundle` executable does", :bundler => "3" do + bundle_config "simulate_version 5" bundler "--version" - expect(out).to eq(Bundler::VERSION) + expect(out).to eq("#{Bundler::VERSION} (simulating Bundler 5)") + end + + it "shows cli_help when bundler install and no Gemfile is found" do + bundler "install", raise_on_error: false + expect(err).to include("Could not locate Gemfile") + + expect(out).to include("Bundler version #{Bundler::VERSION}"). + and include("\n\nBundler commands:\n\n"). + and include("\n\n Primary commands:\n"). + and include("\n\n Utilities:\n"). + and include("\n\nOptions:\n") end end diff --git a/spec/bundler/bundler/compact_index_client/parser_spec.rb b/spec/bundler/bundler/compact_index_client/parser_spec.rb new file mode 100644 index 0000000000..6aa867f058 --- /dev/null +++ b/spec/bundler/bundler/compact_index_client/parser_spec.rb @@ -0,0 +1,249 @@ +# frozen_string_literal: true + +require "bundler/compact_index_client" +require "bundler/compact_index_client/parser" + +TestCompactIndexClient = Struct.new(:names, :versions, :info_data) do + # Requiring the checksum to match the input data helps ensure + # that we are parsing the correct checksum from the versions file + def info(name, checksum) + info_data.dig(name, checksum) + end + + def set_info_data(name, value) + info_data[name] = value + end +end + +RSpec.describe Bundler::CompactIndexClient::Parser do + subject(:parser) { described_class.new(compact_index) } + + let(:compact_index) { TestCompactIndexClient.new(names, versions, info_data) } + let(:names) { "a\nb\nc\n" } + let(:versions) { <<~VERSIONS.dup } + created_at: 2024-05-01T00:00:04Z + --- + a 1.0.0,1.0.1,1.1.0 aaa111 + b 2.0.0,2.0.0-java bbb222 + c 3.0.0,3.0.3,3.3.3 ccc333 + c -3.0.3 ccc333yanked + VERSIONS + let(:info_data) do + { + "a" => { "aaa111" => a_info }, + "b" => { "bbb222" => b_info }, + "c" => { "ccc333yanked" => c_info }, + } + end + let(:a_info) { <<~INFO.dup } + --- + 1.0.0 |checksum:aaa1,ruby:>= 3.0.0,rubygems:>= 3.2.3 + 1.0.1 |checksum:aaa2,ruby:>= 3.0.0,rubygems:>= 3.2.3 + 1.1.0 |checksum:aaa3,ruby:>= 3.0.0,rubygems:>= 3.2.3 + INFO + let(:b_info) { <<~INFO } + 2.0.0 a:~> 1.0&<= 3.0|checksum:bbb1 + 2.0.0-java a:~> 1.0&<= 3.0|checksum:bbb2 + INFO + let(:c_info) { <<~INFO } + 3.0.0 a:= 1.0.0,b:~> 2.0|checksum:ccc1,ruby:>= 2.7.0,rubygems:>= 3.0.0 + 3.3.3 a:>= 1.1.0,b:~> 2.0|checksum:ccc3,ruby:>= 3.0.0,rubygems:>= 3.2.3,created_at:2026-05-12T10:00:00Z + INFO + + describe "#available?" do + it "returns true versions are available" do + expect(parser).to be_available + end + + it "returns true when versions has only one gem" do + compact_index.versions = +"a 1.0.0 aaa1\n" + expect(parser).to be_available + end + + it "returns true when versions has a gem and a header" do + compact_index.versions = +"---\na 1.0.0 aaa1\n" + expect(parser).to be_available + end + + it "returns true when versions has a gem and a header with header data" do + compact_index.versions = +"created_at: 2024-05-01T00:00:04Z\n---\na 1.0.0 aaa1\n" + expect(parser).to be_available + end + + it "returns false when versions has only the header" do + compact_index.versions = +"---\n" + expect(parser).not_to be_available + end + + it "returns false when versions has only the header with header data" do + compact_index.versions = +"created_at: 2024-05-01T00:00:04Z\n---\n" + expect(parser).not_to be_available + end + + it "returns false when versions index is not available" do + compact_index.versions = nil + expect(parser).not_to be_available + end + + it "returns false when versions is empty" do + compact_index.versions = +"" + expect(parser).not_to be_available + end + end + + describe "#names" do + it "returns the names" do + expect(parser.names).to eq(%w[a b c]) + end + + it "returns an empty array when names is empty" do + compact_index.names = "" + expect(parser.names).to eq([]) + end + + it "returns an empty array when names is not readable" do + compact_index.names = nil + expect(parser.names).to eq([]) + end + end + + describe "#versions" do + it "returns the versions" do + expect(parser.versions).to eq( + "a" => [ + ["a", "1.0.0"], + ["a", "1.0.1"], + ["a", "1.1.0"], + ], + "b" => [ + ["b", "2.0.0"], + ["b", "2.0.0", "java"], + ], + "c" => [ + ["c", "3.0.0"], + ["c", "3.3.3"], + ], + ) + end + + it "returns an empty hash when versions is empty" do + compact_index.versions = "" + expect(parser.versions).to eq({}) + end + + it "returns an empty hash when versions is not readable" do + compact_index.versions = nil + expect(parser.versions).to eq({}) + end + end + + describe "#info" do + let(:a_result) do + [ + [ + "a", + "1.0.0", + nil, + [], + [["checksum", ["aaa1"]], ["ruby", [">= 3.0.0"]], ["rubygems", [">= 3.2.3"]]], + ], + [ + "a", + "1.0.1", + nil, + [], + [["checksum", ["aaa2"]], ["ruby", [">= 3.0.0"]], ["rubygems", [">= 3.2.3"]]], + ], + [ + "a", + "1.1.0", + nil, + [], + [["checksum", ["aaa3"]], ["ruby", [">= 3.0.0"]], ["rubygems", [">= 3.2.3"]]], + ], + ] + end + let(:b_result) do + [ + [ + "b", + "2.0.0", + nil, + [["a", ["~> 1.0", "<= 3.0"]]], + [["checksum", ["bbb1"]]], + ], + [ + "b", + "2.0.0", + "java", + [["a", ["~> 1.0", "<= 3.0"]]], + [["checksum", ["bbb2"]]], + ], + ] + end + let(:c_result) do + [ + [ + "c", + "3.0.0", + nil, + [["a", ["= 1.0.0"]], ["b", ["~> 2.0"]]], + [["checksum", ["ccc1"]], ["ruby", [">= 2.7.0"]], ["rubygems", [">= 3.0.0"]]], + ], + [ + "c", + "3.3.3", + nil, + [["a", [">= 1.1.0"]], ["b", ["~> 2.0"]]], + [["checksum", ["ccc3"]], ["ruby", [">= 3.0.0"]], ["rubygems", [">= 3.2.3"]], ["created_at", ["2026-05-12T10:00:00Z"]]], + ], + ] + end + + it "returns the info for example gem 'a' which has no deps" do + expect(parser.info("a")).to eq(a_result) + end + + it "returns the info for example gem 'b' which has platform and compound deps" do + expect(parser.info("b")).to eq(b_result) + end + + it "returns the info for example gem 'c' which has deps and yanked version (requires use of correct info checksum)" do + expect(parser.info("c")).to eq(c_result) + end + + it "returns an empty array when the info is empty" do + compact_index.set_info_data("a", {}) + expect(parser.info("a")).to eq([]) + end + + it "returns an empty array when the info is not readable" do + expect(parser.info("d")).to eq([]) + end + + it "handles empty lines in the versions file (Artifactory bug that they have yet to fix)" do + compact_index.versions = +<<~VERSIONS + created_at: 2024-05-01T00:00:04Z + --- + a 1.0.0,1.0.1,1.1.0 aaa111 + b 2.0.0,2.0.0-java bbb222 + + c 3.0.0,3.0.3,3.3.3 ccc333 + c -3.0.3 ccc333yanked + VERSIONS + expect(parser.info("a")).to eq(a_result) + end + + it "handles lines without a checksum" do + compact_index.versions = <<~VERSIONS + created_at: 2024-05-01T00:00:04Z + --- + a 1.0.0,1.0.1,1.1.0 aaa111 + b 2.0.0,2.0.0-java + c 3.0.0,3.0.3,3.3.3 ccc333 + VERSIONS + + expect(parser.info("a")).to eq(a_result) + end + end +end diff --git a/spec/bundler/bundler/compact_index_client/updater_spec.rb b/spec/bundler/bundler/compact_index_client/updater_spec.rb index fd554a7b0d..fd63a652a4 100644 --- a/spec/bundler/bundler/compact_index_client/updater_spec.rb +++ b/spec/bundler/bundler/compact_index_client/updater_spec.rb @@ -1,55 +1,243 @@ # frozen_string_literal: true -require "net/http" +require "bundler/vendored_net_http" require "bundler/compact_index_client" require "bundler/compact_index_client/updater" +require "tmpdir" RSpec.describe Bundler::CompactIndexClient::Updater do + subject(:updater) { described_class.new(fetcher) } + let(:fetcher) { double(:fetcher) } - let(:local_path) { Pathname("/tmp/localpath") } + let(:local_path) { Pathname.new(Dir.mktmpdir("localpath")).join("versions") } + let(:etag_path) { Pathname.new(Dir.mktmpdir("localpath-etags")).join("versions.etag") } let(:remote_path) { double(:remote_path) } - subject(:updater) { described_class.new(fetcher) } + let(:full_body) { "abc123" } + let(:response) { double(:response, body: full_body, is_a?: false) } + let(:digest) { Digest::SHA256.base64digest(full_body) } - context "when the ETag header is missing" do - # Regression test for https://github.com/bundler/bundler/issues/5463 + context "when the local path does not exist" do + before do + allow(response).to receive(:[]).with("Repr-Digest") { nil } + allow(response).to receive(:[]).with("Digest") { nil } + allow(response).to receive(:[]).with("ETag") { '"thisisanetag"' } + end + + it "downloads the file without attempting append" do + expect(fetcher).to receive(:call).once.with(remote_path, {}) { response } + + updater.update(remote_path, local_path, etag_path) - let(:response) { double(:response, :body => "") } + expect(local_path.read).to eq(full_body) + expect(etag_path.read).to eq("thisisanetag") + end - 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 } + it "fails immediately on bad checksum" do + expect(fetcher).to receive(:call).once.with(remote_path, {}) { response } + allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:baddigest:" } expect do - updater.update(local_path, remote_path) - end.to raise_error(Bundler::CompactIndexClient::Updater::MisMatchedChecksumError) + updater.update(remote_path, local_path, etag_path) + end.to raise_error(Bundler::CompactIndexClient::Updater::MismatchedChecksumError) + end + end + + context "when the local path exists" do + let(:local_body) { "abc" } + + before do + local_path.open("w") {|f| f.write(local_body) } + end + + context "with an etag" do + before do + etag_path.open("w") {|f| f.write("LocalEtag") } + end + + let(:headers) do + { + "If-None-Match" => '"LocalEtag"', + "Range" => "bytes=2-", + } + end + + it "does nothing if etags match" do + expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response) + allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { false } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified) { true } + + updater.update(remote_path, local_path, etag_path) + + expect(local_path.read).to eq("abc") + expect(etag_path.read).to eq("LocalEtag") + end + + it "appends the file if etags do not match" do + expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response) + allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" } + allow(response).to receive(:[]).with("ETag") { '"NewEtag"' } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified) { false } + allow(response).to receive(:body) { "c123" } + + updater.update(remote_path, local_path, etag_path) + + expect(local_path.read).to eq(full_body) + expect(etag_path.read).to eq("NewEtag") + end + + it "replaces the file if response ignores range" do + expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response) + allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" } + allow(response).to receive(:[]).with("ETag") { '"NewEtag"' } + allow(response).to receive(:body) { full_body } + + updater.update(remote_path, local_path, etag_path) + + expect(local_path.read).to eq(full_body) + expect(etag_path.read).to eq("NewEtag") + end + + it "tries the request again if the partial response fails digest check" do + allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:baddigest:" } + allow(response).to receive(:body) { "the beginning of the file changed" } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true } + expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response) + + full_response = double(:full_response, body: full_body, is_a?: false) + allow(full_response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" } + allow(full_response).to receive(:[]).with("ETag") { '"NewEtag"' } + expect(fetcher).to receive(:call).once.with(remote_path, { "If-None-Match" => '"LocalEtag"' }).and_return(full_response) + + updater.update(remote_path, local_path, etag_path) + + expect(local_path.read).to eq(full_body) + expect(etag_path.read).to eq("NewEtag") + end + + it "tries the request again if the partial response is blank" do + allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:baddigest:" } + allow(response).to receive(:body) { "" } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true } + expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response) + + full_response = double(:full_response, body: full_body, is_a?: false) + allow(full_response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" } + allow(full_response).to receive(:[]).with("ETag") { '"NewEtag"' } + expect(fetcher).to receive(:call).once.with(remote_path, { "If-None-Match" => '"LocalEtag"' }).and_return(full_response) + + updater.update(remote_path, local_path, etag_path) + + expect(local_path.read).to eq(full_body) + expect(etag_path.read).to eq("NewEtag") + end + end + + context "without an etag file" do + let(:headers) do + { "Range" => "bytes=2-" } + end + + it "appends the file" do + expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response) + allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" } + allow(response).to receive(:[]).with("ETag") { '"OpaqueEtag"' } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified) { false } + allow(response).to receive(:body) { "c123" } + + updater.update(remote_path, local_path, etag_path) + + expect(local_path.read).to eq(full_body) + expect(etag_path.read).to eq("OpaqueEtag") + end + + it "replaces the file on full file response that ignores range request" do + expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response) + allow(response).to receive(:[]).with("Repr-Digest") { nil } + allow(response).to receive(:[]).with("Digest") { nil } + allow(response).to receive(:[]).with("ETag") { '"OpaqueEtag"' } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { false } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified) { false } + allow(response).to receive(:body) { full_body } + + updater.update(remote_path, local_path, etag_path) + + expect(local_path.read).to eq(full_body) + expect(etag_path.read).to eq("OpaqueEtag") + end + + it "tries the request again if the partial response fails digest check" do + allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:baddigest:" } + allow(response).to receive(:body) { "the beginning of the file changed" } + allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true } + expect(fetcher).to receive(:call).once.with(remote_path, headers) do + # During the failed first request, we simulate another process writing the etag. + # This ensures the second request doesn't generate the md5 etag again but just uses whatever is written. + etag_path.open("w") {|f| f.write("LocalEtag") } + response + end + + full_response = double(:full_response, body: full_body, is_a?: false) + allow(full_response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" } + allow(full_response).to receive(:[]).with("ETag") { '"NewEtag"' } + expect(fetcher).to receive(:call).once.with(remote_path, { "If-None-Match" => '"LocalEtag"' }).and_return(full_response) + + updater.update(remote_path, local_path, etag_path) + + expect(local_path.read).to eq(full_body) + expect(etag_path.read).to eq("NewEtag") + end + end + end + + context "when the ETag header is missing" do + # Regression test for https://github.com/rubygems/bundler/issues/5463 + let(:response) { double(:response, body: full_body) } + + it "treats the response as an update" do + allow(response).to receive(:[]).with("Repr-Digest") { nil } + allow(response).to receive(:[]).with("Digest") { nil } + allow(response).to receive(:[]).with("ETag") { nil } + expect(fetcher).to receive(:call) { response } + + updater.update(remote_path, local_path, etag_path) end end context "when the download is corrupt" do - let(:response) { double(:response, :body => "") } + let(:response) { double(:response, body: "") } it "raises HTTPError" do - expect(response).to receive(:[]).with("Content-Encoding") { "gzip" } - expect(fetcher).to receive(:call) { response } + expect(fetcher).to receive(:call).and_raise(Zlib::GzipFile::Error) expect do - updater.update(local_path, remote_path) + updater.update(remote_path, local_path, etag_path) end.to raise_error(Bundler::HTTPError) end end - context "when bundler doesn't have permissions on Dir.tmpdir" do - let(:response) { double(:response, :body => "") } + context "when receiving non UTF-8 data and default internal encoding set to ASCII" do + let(:response) { double(:response, body: "\x8B".b) } - it "Errno::EACCES is raised" do - allow(Dir).to receive(:mktmpdir) { raise Errno::EACCES } + it "works just fine" do + old_verbose = $VERBOSE + previous_internal_encoding = Encoding.default_internal - expect do - updater.update(local_path, remote_path) - end.to raise_error(Bundler::PermissionError) + begin + $VERBOSE = false + Encoding.default_internal = "ASCII" + allow(response).to receive(:[]).with("Repr-Digest") { nil } + allow(response).to receive(:[]).with("Digest") { nil } + allow(response).to receive(:[]).with("ETag") { nil } + expect(fetcher).to receive(:call) { response } + + updater.update(remote_path, local_path, etag_path) + ensure + Encoding.default_internal = previous_internal_encoding + $VERBOSE = old_verbose + end end end end diff --git a/spec/bundler/bundler/current_ruby_spec.rb b/spec/bundler/bundler/current_ruby_spec.rb new file mode 100644 index 0000000000..79eb802aa5 --- /dev/null +++ b/spec/bundler/bundler/current_ruby_spec.rb @@ -0,0 +1,157 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::CurrentRuby do + describe "PLATFORM_MAP" do + subject { described_class::PLATFORM_MAP } + + # rubocop:disable Naming/VariableNumber + let(:platforms) do + { ruby: Gem::Platform::RUBY, + ruby_18: Gem::Platform::RUBY, + ruby_19: Gem::Platform::RUBY, + ruby_20: Gem::Platform::RUBY, + ruby_21: Gem::Platform::RUBY, + ruby_22: Gem::Platform::RUBY, + ruby_23: Gem::Platform::RUBY, + ruby_24: Gem::Platform::RUBY, + ruby_25: Gem::Platform::RUBY, + ruby_26: Gem::Platform::RUBY, + ruby_27: Gem::Platform::RUBY, + ruby_30: Gem::Platform::RUBY, + ruby_31: Gem::Platform::RUBY, + ruby_32: Gem::Platform::RUBY, + ruby_33: Gem::Platform::RUBY, + ruby_34: Gem::Platform::RUBY, + ruby_40: Gem::Platform::RUBY, + ruby_41: Gem::Platform::RUBY, + mri: Gem::Platform::RUBY, + mri_18: Gem::Platform::RUBY, + mri_19: Gem::Platform::RUBY, + mri_20: Gem::Platform::RUBY, + mri_21: Gem::Platform::RUBY, + mri_22: Gem::Platform::RUBY, + mri_23: Gem::Platform::RUBY, + mri_24: Gem::Platform::RUBY, + mri_25: Gem::Platform::RUBY, + mri_26: Gem::Platform::RUBY, + mri_27: Gem::Platform::RUBY, + mri_30: Gem::Platform::RUBY, + mri_31: Gem::Platform::RUBY, + mri_32: Gem::Platform::RUBY, + mri_33: Gem::Platform::RUBY, + mri_34: Gem::Platform::RUBY, + mri_40: Gem::Platform::RUBY, + mri_41: Gem::Platform::RUBY, + rbx: Gem::Platform::RUBY, + truffleruby: Gem::Platform::RUBY, + jruby: Gem::Platform::JAVA, + jruby_18: Gem::Platform::JAVA, + jruby_19: Gem::Platform::JAVA, + windows: Gem::Platform::WINDOWS, + windows_18: Gem::Platform::WINDOWS, + windows_19: Gem::Platform::WINDOWS, + windows_20: Gem::Platform::WINDOWS, + windows_21: Gem::Platform::WINDOWS, + windows_22: Gem::Platform::WINDOWS, + windows_23: Gem::Platform::WINDOWS, + windows_24: Gem::Platform::WINDOWS, + windows_25: Gem::Platform::WINDOWS, + windows_26: Gem::Platform::WINDOWS, + windows_27: Gem::Platform::WINDOWS, + windows_30: Gem::Platform::WINDOWS, + windows_31: Gem::Platform::WINDOWS, + windows_32: Gem::Platform::WINDOWS, + windows_33: Gem::Platform::WINDOWS, + windows_34: Gem::Platform::WINDOWS, + windows_40: Gem::Platform::WINDOWS, + windows_41: Gem::Platform::WINDOWS } + end + + let(:deprecated) do + { mswin: Gem::Platform::MSWIN, + mswin_18: Gem::Platform::MSWIN, + mswin_19: Gem::Platform::MSWIN, + mswin_20: Gem::Platform::MSWIN, + mswin_21: Gem::Platform::MSWIN, + mswin_22: Gem::Platform::MSWIN, + mswin_23: Gem::Platform::MSWIN, + mswin_24: Gem::Platform::MSWIN, + mswin_25: Gem::Platform::MSWIN, + mswin_26: Gem::Platform::MSWIN, + mswin_27: Gem::Platform::MSWIN, + mswin_30: Gem::Platform::MSWIN, + mswin_31: Gem::Platform::MSWIN, + mswin_32: Gem::Platform::MSWIN, + mswin_33: Gem::Platform::MSWIN, + mswin_34: Gem::Platform::MSWIN, + mswin_40: Gem::Platform::MSWIN, + mswin_41: Gem::Platform::MSWIN, + mswin64: Gem::Platform::MSWIN64, + mswin64_19: Gem::Platform::MSWIN64, + mswin64_20: Gem::Platform::MSWIN64, + mswin64_21: Gem::Platform::MSWIN64, + mswin64_22: Gem::Platform::MSWIN64, + mswin64_23: Gem::Platform::MSWIN64, + mswin64_24: Gem::Platform::MSWIN64, + mswin64_25: Gem::Platform::MSWIN64, + mswin64_26: Gem::Platform::MSWIN64, + mswin64_27: Gem::Platform::MSWIN64, + mswin64_30: Gem::Platform::MSWIN64, + mswin64_31: Gem::Platform::MSWIN64, + mswin64_32: Gem::Platform::MSWIN64, + mswin64_33: Gem::Platform::MSWIN64, + mswin64_34: Gem::Platform::MSWIN64, + mswin64_40: Gem::Platform::MSWIN64, + mswin64_41: Gem::Platform::MSWIN64, + mingw: Gem::Platform::UNIVERSAL_MINGW, + mingw_18: Gem::Platform::UNIVERSAL_MINGW, + mingw_19: Gem::Platform::UNIVERSAL_MINGW, + mingw_20: Gem::Platform::UNIVERSAL_MINGW, + mingw_21: Gem::Platform::UNIVERSAL_MINGW, + mingw_22: Gem::Platform::UNIVERSAL_MINGW, + mingw_23: Gem::Platform::UNIVERSAL_MINGW, + mingw_24: Gem::Platform::UNIVERSAL_MINGW, + mingw_25: Gem::Platform::UNIVERSAL_MINGW, + mingw_26: Gem::Platform::UNIVERSAL_MINGW, + mingw_27: Gem::Platform::UNIVERSAL_MINGW, + mingw_30: Gem::Platform::UNIVERSAL_MINGW, + mingw_31: Gem::Platform::UNIVERSAL_MINGW, + mingw_32: Gem::Platform::UNIVERSAL_MINGW, + mingw_33: Gem::Platform::UNIVERSAL_MINGW, + mingw_34: Gem::Platform::UNIVERSAL_MINGW, + mingw_40: Gem::Platform::UNIVERSAL_MINGW, + mingw_41: Gem::Platform::UNIVERSAL_MINGW, + x64_mingw: Gem::Platform::UNIVERSAL_MINGW, + x64_mingw_20: Gem::Platform::UNIVERSAL_MINGW, + x64_mingw_21: Gem::Platform::UNIVERSAL_MINGW, + x64_mingw_22: Gem::Platform::UNIVERSAL_MINGW, + x64_mingw_23: Gem::Platform::UNIVERSAL_MINGW, + x64_mingw_24: Gem::Platform::UNIVERSAL_MINGW, + x64_mingw_25: Gem::Platform::UNIVERSAL_MINGW, + x64_mingw_26: Gem::Platform::UNIVERSAL_MINGW, + x64_mingw_27: Gem::Platform::UNIVERSAL_MINGW, + x64_mingw_30: Gem::Platform::UNIVERSAL_MINGW, + x64_mingw_31: Gem::Platform::UNIVERSAL_MINGW, + x64_mingw_32: Gem::Platform::UNIVERSAL_MINGW, + x64_mingw_33: Gem::Platform::UNIVERSAL_MINGW, + x64_mingw_34: Gem::Platform::UNIVERSAL_MINGW, + x64_mingw_40: Gem::Platform::UNIVERSAL_MINGW, + x64_mingw_41: Gem::Platform::UNIVERSAL_MINGW } + end + # rubocop:enable Naming/VariableNumber + + it "includes all platforms" do + expect(subject).to eq(platforms.merge(deprecated)) + end + end + + describe "Deprecated platform" do + it "outputs an error and aborts when calling maglev?" do + expect { Bundler.current_ruby.maglev? }.to raise_error(Bundler::RemovedError, /`CurrentRuby#maglev\?` was removed with no replacement./) + end + + it "outputs an error and aborts when calling maglev_31?" do + expect { Bundler.current_ruby.maglev_31? }.to raise_error(Bundler::RemovedError, /`CurrentRuby#maglev_31\?` was removed with no replacement./) + end + end +end diff --git a/spec/bundler/bundler/definition_spec.rb b/spec/bundler/bundler/definition_spec.rb index 92f836299d..8c4a5a0331 100644 --- a/spec/bundler/bundler/definition_spec.rb +++ b/spec/bundler/bundler/definition_spec.rb @@ -3,190 +3,216 @@ require "bundler/definition" RSpec.describe Bundler::Definition do + describe "#overrides" do + before do + allow(Bundler::SharedHelpers).to receive(:find_gemfile) { bundled_app_gemfile } + end + + subject { Bundler::Definition.new(bundled_app_lock, [], Bundler::SourceList.new, {}) } + + it "defaults to an empty array" do + expect(subject.overrides).to eq([]) + end + + it "is writable" do + override = Bundler::Override.new("rails", :version, ">= 8.0") + subject.overrides = [override] + expect(subject.overrides).to eq([override]) + end + end + describe "#lock" do before do - allow(Bundler).to receive(:settings) { Bundler::Settings.new(".") } - allow(Bundler::SharedHelpers).to receive(:find_gemfile) { Pathname.new("Gemfile") } - allow(Bundler).to receive(:ui) { double("UI", :info => "", :debug => "") } + allow(Bundler::SharedHelpers).to receive(:find_gemfile) { bundled_app_gemfile } + allow(Bundler).to receive(:ui) { double("UI", info: "", debug: "") } end - context "when it's not possible to write to the file" do - subject { Bundler::Definition.new(nil, [], Bundler::SourceList.new, []) } + subject { Bundler::Definition.new(bundled_app_lock, [], Bundler::SourceList.new, {}) } + + context "when it's not possible to write to the file" do it "raises an PermissionError with explanation" do allow(File).to receive(:open).and_call_original - expect(File).to receive(:open).with("Gemfile.lock", "wb"). - and_raise(Errno::EACCES) - expect { subject.lock("Gemfile.lock") }. + expect(File).to receive(:open).with(bundled_app_lock, "wb"). + and_raise(Errno::EACCES.new(bundled_app_lock.to_s)) + expect { subject.lock }. to raise_error(Bundler::PermissionError, /Gemfile\.lock/) end end context "when a temporary resource access issue occurs" do - subject { Bundler::Definition.new(nil, [], Bundler::SourceList.new, []) } - it "raises a TemporaryResourceError with explanation" do allow(File).to receive(:open).and_call_original - expect(File).to receive(:open).with("Gemfile.lock", "wb"). + expect(File).to receive(:open).with(bundled_app_lock, "wb"). and_raise(Errno::EAGAIN) - expect { subject.lock("Gemfile.lock") }. + expect { subject.lock }. to raise_error(Bundler::TemporaryResourceError, /temporarily unavailable/) end end + context "when Bundler::Definition.no_lock is set to true" do + before { Bundler::Definition.no_lock = true } + after { Bundler::Definition.no_lock = false } + + it "does not create a lockfile" do + subject.lock + expect(bundled_app_lock).not_to be_file + end + end end describe "detects changes" do - it "for a path gem with changes", :bundler => "< 3" do - build_lib "foo", "1.0", :path => lib_path("foo") + it "for a path gem with changes" do + build_lib "foo", "1.0", path: lib_path("foo") install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://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" + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack", "1.0" end - bundle :install, :env => { "DEBUG" => 1 } + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo1, "myrack", "1.0.0" + end + + bundle :install, env: { "DEBUG" => "1" } expect(out).to match(/re-resolving dependencies/) - lockfile_should_be <<-G + expect(lockfile).to eq <<~G PATH remote: #{lib_path("foo")} specs: foo (1.0) - rack (= 1.0) + myrack (= 1.0) GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES foo! - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end - it "for a path gem with changes", :bundler => "3" do - build_lib "foo", "1.0", :path => lib_path("foo") - - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "foo", :path => "#{lib_path("foo")}" - G - - build_lib "foo", "1.0", :path => lib_path("foo") do |s| - s.add_dependency "rack", "1.0" + it "with an explicit update" do + build_repo4 do + build_gem("ffi", "1.9.23") {|s| s.platform = "java" } + build_gem("ffi", "1.9.23") end - 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_uri_for(gem_repo1)}/ - specs: - rack (1.0.0) + gemfile <<-G + source "https://gem.repo4" + gem "ffi" + G - PLATFORMS - #{lockfile_platforms} + bundle "lock --add-platform java" - DEPENDENCIES - foo! + bundle "update ffi", env: { "DEBUG" => "1" } - BUNDLED WITH - #{Bundler::VERSION} - G + expect(out).to match(/because bundler is unlocking gems: \(ffi\)/) end it "for a path gem with deps and no changes" do - build_lib "foo", "1.0", :path => lib_path("foo") do |s| - s.add_dependency "rack", "1.0" + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack", "1.0" s.add_development_dependency "net-ssh", "1.0" end + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo1, "myrack", "1.0.0" + end + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "foo", :path => "#{lib_path("foo")}" G - bundle :check, :env => { "DEBUG" => 1 } - - expect(out).to match(/using resolution from the lockfile/) - lockfile_should_be <<-G + expected_lockfile = <<~G PATH remote: #{lib_path("foo")} specs: foo (1.0) - rack (= 1.0) + myrack (= 1.0) GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES foo! - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G + + expect(lockfile).to eq(expected_lockfile) + + bundle :check, env: { "DEBUG" => "1" } + + expect(out).to match(/using resolution from the lockfile/) + expect(lockfile).to eq(expected_lockfile) end it "for a locked gem for another platform" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "only_java", platform: :jruby G + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo1, "only_java", "1.1", "java" + end + bundle "lock --add-platform java" - bundle :check, :env => { "DEBUG" => 1 } + bundle :check, env: { "DEBUG" => "1" } expect(out).to match(/using resolution from the lockfile/) - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: only_java (1.1-java) PLATFORMS - java - #{lockfile_platforms} + #{lockfile_platforms("java")} DEPENDENCIES only_java - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end it "for a rubygems gem" do + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo1, "foo", "1.0" + end + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "foo" G - bundle :check, :env => { "DEBUG" => 1 } + bundle :check, env: { "DEBUG" => "1" } expect(out).to match(/using resolution from the lockfile/) - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: foo (1.0) @@ -195,48 +221,25 @@ RSpec.describe Bundler::Definition do DEPENDENCIES foo - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end end describe "initialize" do context "gem version promoter" do - context "with lockfile" do - before do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "foo" - G - end - - it "should get a locked specs list when updating all" do - definition = Bundler::Definition.new(bundled_app("Gemfile.lock"), [], Bundler::SourceList.new, true) - locked_specs = definition.gem_version_promoter.locked_specs - expect(locked_specs.to_a.map(&:name)).to eq ["foo"] - expect(definition.instance_variable_get("@locked_specs").empty?).to eq true - end - end - - context "without gemfile or lockfile" do - it "should not attempt to parse empty lockfile contents" do - definition = Bundler::Definition.new(nil, [], mock_source_list, true) - expect(definition.gem_version_promoter.locked_specs.to_a).to eq [] - end - end - context "eager unlock" do let(:source_list) do Bundler::SourceList.new.tap do |source_list| - source_list.global_rubygems_source = file_uri_for(gem_repo4) + source_list.add_global_rubygems_remote("https://gem.repo4") end end before do gemfile <<-G - source "#{file_uri_for(gem_repo4)}" + source "https://gem.repo4" gem 'isolated_owner' gem 'shared_owner_a' @@ -245,7 +248,7 @@ RSpec.describe Bundler::Definition do lockfile <<-L GEM - remote: #{file_uri_for(gem_repo4)} + remote: https://gem.repo4 specs: isolated_dep (2.0.1) isolated_owner (1.0.1) @@ -267,6 +270,8 @@ RSpec.describe Bundler::Definition do BUNDLED WITH 1.13.0 L + + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) end it "should not eagerly unlock shared dependency with bundle install conservative updating behavior" do @@ -275,7 +280,7 @@ RSpec.describe Bundler::Definition do Bundler::Dependency.new("shared_owner_b", ">= 0")] unlock_hash_for_bundle_install = {} definition = Bundler::Definition.new( - bundled_app("Gemfile.lock"), + bundled_app_lock, updated_deps_in_gemfile, source_list, unlock_hash_for_bundle_install @@ -289,10 +294,10 @@ RSpec.describe Bundler::Definition do Bundler::Dependency.new("shared_owner_a", ">= 0"), Bundler::Dependency.new("shared_owner_b", ">= 0")] definition = Bundler::Definition.new( - bundled_app("Gemfile.lock"), + bundled_app_lock, updated_deps_in_gemfile, source_list, - :gems => ["shared_owner_a"], :lock_shared_dependencies => true + gems: ["shared_owner_a"], conservative: true ) locked = definition.send(:converge_locked_specs).map(&:name) expect(locked).to eq %w[isolated_dep isolated_owner shared_dep shared_owner_b] @@ -302,31 +307,55 @@ RSpec.describe Bundler::Definition do 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" + describe "#precompute_source_requirements_for_indirect_dependencies?" do + before do + allow(Bundler::SharedHelpers).to receive(:find_gemfile) { Pathname.new("Gemfile") } 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) } + let(:sources) { Bundler::SourceList.new } + subject { Bundler::Definition.new(nil, [], sources, []) } - 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 + before do + allow(sources).to receive(:non_global_rubygems_sources).and_return(non_global_rubygems_sources) end - end - def build_stub_spec(name, version) - Bundler::StubSpecification.new(name, version, nil, nil) + context "when all the scoped sources implement a dependency API" do + let(:non_global_rubygems_sources) do + [ + double("non-global-source-0", "dependency_api_available?":true, to_s:"a"), + double("non-global-source-1", "dependency_api_available?":true, to_s:"b"), + ] + end + + it "returns true without warning" do + expect(subject).not_to receive(:non_dependency_api_warning) + + expect(subject.send(:precompute_source_requirements_for_indirect_dependencies?)).to be_truthy + end + end + + context "when some scoped sources do not implement a dependency API" do + let(:non_global_rubygems_sources) do + [ + double("non-global-source-0", "dependency_api_available?":true, to_s:"a"), + double("non-global-source-1", "dependency_api_available?":false, to_s:"b"), + double("non-global-source-2", "dependency_api_available?":false, to_s:"c"), + ] + end + + it "returns false and warns about the non-API sources" do + expect(Bundler.ui).to receive(:warn).with(<<-W.strip) +Your Gemfile contains scoped sources that don't implement a dependency API, namely: + + * b + * c + +Using the above gem servers may result in installing unexpected gems. To resolve this warning, make sure you use gem servers that implement dependency APIs, such as gemstash or geminabox gem servers. + W + + expect(subject.send(:precompute_source_requirements_for_indirect_dependencies?)).to be_falsy + end + end end def mock_source_list @@ -339,10 +368,6 @@ RSpec.describe Bundler::Definition do [] end - def rubygems_remotes - [] - end - def replace_sources!(arg) nil end diff --git a/spec/bundler/bundler/dep_proxy_spec.rb b/spec/bundler/bundler/dep_proxy_spec.rb deleted file mode 100644 index 0f8d6b1076..0000000000 --- a/spec/bundler/bundler/dep_proxy_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Bundler::DepProxy do - let(:dep) { Bundler::Dependency.new("rake", ">= 0") } - subject { described_class.new(dep, Gem::Platform::RUBY) } - let(:same) { subject } - let(:other) { subject.dup } - let(:different) { described_class.new(dep, Gem::Platform::JAVA) } - - describe "#eql?" do - it { expect(subject.eql?(same)).to be true } - it { expect(subject.eql?(other)).to be true } - it { expect(subject.eql?(different)).to be false } - it { expect(subject.eql?(nil)).to be false } - it { expect(subject.eql?("foobar")).to be false } - end - - describe "#hash" do - it { expect(subject.hash).to eq(same.hash) } - it { expect(subject.hash).to eq(other.hash) } - end -end diff --git a/spec/bundler/bundler/dependency_spec.rb b/spec/bundler/bundler/dependency_spec.rb new file mode 100644 index 0000000000..f930459571 --- /dev/null +++ b/spec/bundler/bundler/dependency_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Dependency do + let(:options) do + {} + end + let(:dependency) do + described_class.new( + "test_gem", + "1.0.0", + options + ) + end + + describe "to_lock" do + it "returns formatted string" do + expect(dependency.to_lock).to eq(" test_gem (= 1.0.0)") + end + + it "matches format of Gem::Dependency#to_lock" do + gem_dependency = Gem::Dependency.new("test_gem", "1.0.0") + expect(dependency.to_lock).to eq(gem_dependency.to_lock) + end + + context "when source is passed" do + let(:options) do + { + "source" => Bundler::Source::Git.new({}), + } + end + + it "returns formatted string with exclamation mark" do + expect(dependency.to_lock).to eq(" test_gem (= 1.0.0)!") + end + end + end + + it "is on the current platform" do + engine = Gem.win_platform? ? "windows" : RUBY_ENGINE + + dep = described_class.new( + "test_gem", + "1.0.0", + { "platforms" => "#{engine}_#{RbConfig::CONFIG["MAJOR"]}#{RbConfig::CONFIG["MINOR"]}" }, + ) + + expect(dep.current_platform?).to be_truthy + end +end diff --git a/spec/bundler/bundler/digest_spec.rb b/spec/bundler/bundler/digest_spec.rb new file mode 100644 index 0000000000..f876827964 --- /dev/null +++ b/spec/bundler/bundler/digest_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "openssl" +require "bundler/digest" + +RSpec.describe Bundler::Digest do + context "SHA1" do + subject { Bundler::Digest } + let(:stdlib) { OpenSSL::Digest::SHA1 } + + it "is compatible with stdlib" do + random_strings = ["foo", "skfjsdlkfjsdf", "3924m", "ldskfj"] + + # https://www.rfc-editor.org/rfc/rfc3174#section-7.3 + rfc3174_test_cases = ["abc", "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", "a", "01234567" * 8] + + (random_strings + rfc3174_test_cases).each do |payload| + sha1 = subject.sha1(payload) + sha1_stdlib = stdlib.hexdigest(payload) + expect(sha1).to be == sha1_stdlib, "#{payload}'s sha1 digest (#{sha1}) did not match stlib's result (#{sha1_stdlib})" + end + end + end +end diff --git a/spec/bundler/bundler/dsl_spec.rb b/spec/bundler/bundler/dsl_spec.rb index 40739a431b..b6e67a312c 100644 --- a/spec/bundler/bundler/dsl_spec.rb +++ b/spec/bundler/bundler/dsl_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Bundler::Dsl do it "registers custom hosts" do subject.git_source(:example) {|repo_name| "git@git.example.com:#{repo_name}.git" } subject.git_source(:foobar) {|repo_name| "git@foobar.com:#{repo_name}.git" } - subject.gem("dobry-pies", :example => "strzalek/dobry-pies") + subject.gem("dobry-pies", example: "strzalek/dobry-pies") example_uri = "git@git.example.com:strzalek/dobry-pies.git" expect(subject.dependencies.first.source.uri).to eq(example_uri) end @@ -25,54 +25,144 @@ RSpec.describe Bundler::Dsl do expect { subject.git_source(:example) }.to raise_error(Bundler::InvalidOption) end - context "default hosts", :bundler => "2" do + it "converts :github PR to URI using https" do + subject.gem("sparks", github: "https://github.com/indirect/sparks/pull/5") + github_uri = "https://github.com/indirect/sparks.git" + expect(subject.dependencies.first.source.uri).to eq(github_uri) + expect(subject.dependencies.first.source.ref).to eq("refs/pull/5/head") + end + + it "converts :gitlab PR to URI using https" do + subject.gem("sparks", gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5") + gitlab_uri = "https://gitlab.com/indirect/sparks.git" + expect(subject.dependencies.first.source.uri).to eq(gitlab_uri) + expect(subject.dependencies.first.source.ref).to eq("refs/merge-requests/5/head") + end + + it "rejects :github PR URI with a branch, ref or tag" do + expect do + subject.gem("sparks", github: "https://github.com/indirect/sparks/pull/5", branch: "foo") + end.to raise_error( + Bundler::GemfileError, + %(The :branch option can't be used with `github: "https://github.com/indirect/sparks/pull/5"`), + ) + + expect do + subject.gem("sparks", github: "https://github.com/indirect/sparks/pull/5", ref: "foo") + end.to raise_error( + Bundler::GemfileError, + %(The :ref option can't be used with `github: "https://github.com/indirect/sparks/pull/5"`), + ) + + expect do + subject.gem("sparks", github: "https://github.com/indirect/sparks/pull/5", tag: "foo") + end.to raise_error( + Bundler::GemfileError, + %(The :tag option can't be used with `github: "https://github.com/indirect/sparks/pull/5"`), + ) + end + + it "rejects :gitlab PR URI with a branch, ref or tag" do + expect do + subject.gem("sparks", gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5", branch: "foo") + end.to raise_error( + Bundler::GemfileError, + %(The :branch option can't be used with `gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5"`), + ) + + expect do + subject.gem("sparks", gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5", ref: "foo") + end.to raise_error( + Bundler::GemfileError, + %(The :ref option can't be used with `gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5"`), + ) + + expect do + subject.gem("sparks", gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5", tag: "foo") + end.to raise_error( + Bundler::GemfileError, + %(The :tag option can't be used with `gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5"`), + ) + end + + it "rejects :github with :git" do + expect do + subject.gem("sparks", github: "indirect/sparks", git: "https://github.com/indirect/sparks.git") + end.to raise_error( + Bundler::GemfileError, + %(The :git option can't be used with `github: "indirect/sparks"`), + ) + end + + it "rejects :gitlab with :git" do + expect do + subject.gem("sparks", gitlab: "indirect/sparks", git: "https://gitlab.com/indirect/sparks.git") + end.to raise_error( + Bundler::GemfileError, + %(The :git option can't be used with `gitlab: "indirect/sparks"`), + ) + end + + context "default hosts" do it "converts :github to URI using https" do - subject.gem("sparks", :github => "indirect/sparks") + subject.gem("sparks", github: "indirect/sparks") github_uri = "https://github.com/indirect/sparks.git" expect(subject.dependencies.first.source.uri).to eq(github_uri) end it "converts :github shortcut to URI using https" do - subject.gem("sparks", :github => "rails") + subject.gem("sparks", github: "rails") github_uri = "https://github.com/rails/rails.git" expect(subject.dependencies.first.source.uri).to eq(github_uri) end + it "converts :gitlab to URI using https" do + subject.gem("sparks", gitlab: "indirect/sparks") + gitlab_uri = "https://gitlab.com/indirect/sparks.git" + expect(subject.dependencies.first.source.uri).to eq(gitlab_uri) + end + + it "converts :gitlab shortcut to URI using https" do + subject.gem("sparks", gitlab: "rails") + gitlab_uri = "https://gitlab.com/rails/rails.git" + expect(subject.dependencies.first.source.uri).to eq(gitlab_uri) + end + it "converts numeric :gist to :git" do - subject.gem("not-really-a-gem", :gist => 2_859_988) + subject.gem("not-really-a-gem", gist: 2_859_988) github_uri = "https://gist.github.com/2859988.git" expect(subject.dependencies.first.source.uri).to eq(github_uri) end it "converts :gist to :git" do - subject.gem("not-really-a-gem", :gist => "2859988") + subject.gem("not-really-a-gem", gist: "2859988") github_uri = "https://gist.github.com/2859988.git" expect(subject.dependencies.first.source.uri).to eq(github_uri) end it "converts :bitbucket to :git" do - subject.gem("not-really-a-gem", :bitbucket => "mcorp/flatlab-rails") + subject.gem("not-really-a-gem", bitbucket: "mcorp/flatlab-rails") bitbucket_uri = "https://mcorp@bitbucket.org/mcorp/flatlab-rails.git" expect(subject.dependencies.first.source.uri).to eq(bitbucket_uri) end it "converts 'mcorp' to 'mcorp/mcorp'" do - subject.gem("not-really-a-gem", :bitbucket => "mcorp") + subject.gem("not-really-a-gem", bitbucket: "mcorp") bitbucket_uri = "https://mcorp@bitbucket.org/mcorp/mcorp.git" expect(subject.dependencies.first.source.uri).to eq(bitbucket_uri) end end - context "default git sources", :bundler => "3" do - it "has none" do - expect(subject.instance_variable_get(:@git_sources)).to eq({}) + context "default git sources" do + it "has bitbucket, gist, github, and gitlab" do + expect(subject.instance_variable_get(:@git_sources).keys.sort).to eq(%w[bitbucket gist github gitlab]) end end end describe "#method_missing" do it "raises an error for unknown DSL methods" do - expect(Bundler).to receive(:read_file).with(bundled_app("Gemfile").to_s). + expect(Bundler).to receive(:read_file).with(git_root.join("Gemfile").to_s). and_return("unknown") error_msg = "There was an error parsing `Gemfile`: Undefined local variable or method `unknown' for Gemfile. Bundler cannot continue." @@ -83,33 +173,59 @@ RSpec.describe Bundler::Dsl do describe "#eval_gemfile" do it "handles syntax errors with a useful message" do - expect(Bundler).to receive(:read_file).with(bundled_app("Gemfile").to_s).and_return("}") + expect(Bundler).to receive(:read_file).with(git_root.join("Gemfile").to_s).and_return("}") expect { subject.eval_gemfile("Gemfile") }. - to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`: (syntax error, unexpected tSTRING_DEND|(compile error - )?syntax error, unexpected '\}'). Bundler cannot continue./) + to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`: (syntax error, unexpected tSTRING_DEND|(compile error - )?syntax error, unexpected '\}'|.+?unexpected '}', ignoring it\n). Bundler cannot continue./m) end it "distinguishes syntax errors from evaluation errors" do - expect(Bundler).to receive(:read_file).with(bundled_app("Gemfile").to_s).and_return( + expect(Bundler).to receive(:read_file).with(git_root.join("Gemfile").to_s).and_return( "ruby '2.1.5', :engine => 'ruby', :engine_version => '1.2.4'" ) expect { subject.eval_gemfile("Gemfile") }. to raise_error(Bundler::GemfileError, /There was an error evaluating `Gemfile`: ruby_version must match the :engine_version for MRI/) end + + it "populates __dir__ and __FILE__ correctly" do + abs_path = git_root.join("../fragment.rb").to_s + expect(Bundler).to receive(:read_file).with(abs_path).and_return(<<~RUBY) + @fragment_dir = __dir__ + @fragment_file = __FILE__ + RUBY + subject.eval_gemfile("../fragment.rb") + expect(subject.instance_variable_get(:@fragment_dir)).to eq(git_root.dirname.to_s) + expect(subject.instance_variable_get(:@fragment_file)).to eq(abs_path) + end end describe "#gem" do - [:ruby, :ruby_18, :ruby_19, :ruby_20, :ruby_21, :ruby_22, :ruby_23, :ruby_24, :ruby_25, :ruby_26, :mri, :mri_18, :mri_19, - :mri_20, :mri_21, :mri_22, :mri_23, :mri_24, :mri_25, :mri_26, :jruby, :rbx, :truffleruby].each do |platform| + # rubocop:disable Naming/VariableNumber + [:ruby, :ruby_18, :ruby_19, :ruby_20, :ruby_21, :ruby_22, :ruby_23, :ruby_24, :ruby_25, :ruby_26, :ruby_27, + :ruby_30, :ruby_31, :ruby_32, :ruby_33, :ruby_34, :ruby_40, :mri, :mri_18, :mri_19, :mri_20, :mri_21, :mri_22, :mri_23, :mri_24, + :mri_25, :mri_26, :mri_27, :mri_30, :mri_31, :mri_32, :mri_33, :mri_34, :mri_40, :jruby, :rbx, :truffleruby].each do |platform| it "allows #{platform} as a valid platform" do - subject.gem("foo", :platform => platform) + subject.gem("foo", platform: platform) end end + # rubocop:enable Naming/VariableNumber + + it "allows platforms matching the running Ruby version" do + platform = "ruby_#{RbConfig::CONFIG["MAJOR"]}#{RbConfig::CONFIG["MINOR"]}" + + expect { subject.gem("foo", platform: platform) }.not_to raise_error + expect(Bundler.current_ruby.respond_to?("#{platform}?")).to be_truthy + end it "rejects invalid platforms" do - expect { subject.gem("foo", :platform => :bogus) }. + expect { subject.gem("foo", platform: :bogus) }. to raise_error(Bundler::GemfileError, /is not a valid platform/) end + it "warn for legacy windows platforms" do + expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with(/\APlatform :mswin, :x64_mingw will be removed in the future./) + subject.gem("foo", platforms: [:mswin, :jruby, :x64_mingw]) + end + it "rejects empty gem name" do expect { subject.gem("") }. to raise_error(Bundler::GemfileError, /an empty gem name is not valid/) @@ -156,54 +272,29 @@ RSpec.describe Bundler::Dsl do end it "rejects branch option on non-git gems" do - expect { subject.gem("foo", :branch => "test") }. + expect { subject.gem("foo", branch: "test") }. to raise_error(Bundler::GemfileError, /The `branch` option for `gem 'foo'` is not allowed. Only gems with a git source can specify a branch/) end it "allows specifying a branch on git gems" do - subject.gem("foo", :branch => "test", :git => "http://mytestrepo") + subject.gem("foo", branch: "test", git: "http://mytestrepo") dep = subject.dependencies.last expect(dep.name).to eq "foo" end it "allows specifying a branch on git gems with a git_source" do subject.git_source(:test_source) {|n| "https://github.com/#{n}" } - subject.gem("foo", :branch => "test", :test_source => "bundler/bundler") + subject.gem("foo", branch: "test", test_source: "bundler/bundler") dep = subject.dependencies.last expect(dep.name).to eq "foo" end 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]) + describe "#platforms" do + it "warn for legacy windows platforms" do + expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with(/\APlatform :mswin64, :mingw will be removed in the future./) + subject.platforms(:mswin64, :jruby, :mingw) do + subject.gem("foo") end end end @@ -229,7 +320,7 @@ RSpec.describe Bundler::Dsl do # gem 'spree_api' # gem 'spree_backend' # end - describe "#github", :bundler => "< 3" do + describe "#github" do it "from github" do spree_gems = %w[spree_core spree_api spree_backend] subject.github "spree" do @@ -241,44 +332,20 @@ RSpec.describe Bundler::Dsl do end end end - - describe "#github", :bundler => "2" do - it "from github" do - spree_gems = %w[spree_core spree_api spree_backend] - subject.github "spree" do - spree_gems.each {|spree_gem| subject.send :gem, spree_gem } - end - - subject.dependencies.each do |d| - expect(d.source.uri).to eq("https://github.com/spree/spree.git") - end - end - end - - describe "#github", :bundler => "3" do - it "from github" do - expect do - spree_gems = %w[spree_core spree_api spree_backend] - subject.github "spree" do - spree_gems.each {|spree_gem| subject.send :gem, spree_gem } - end - end.to raise_error(Bundler::DeprecatedError, /github method has been removed/) - end - end end describe "syntax errors" do it "will raise a Bundler::GemfileError" do gemfile "gem 'foo', :path => /unquoted/string/syntax/error" - expect { Bundler::Dsl.evaluate(bundled_app("Gemfile"), nil, true) }. - to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`:( compile error -)? unknown regexp options - trg.+ Bundler cannot continue./) + 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./m) end end describe "Runtime errors" do it "will raise a Bundler::GemfileError" do gemfile "raise RuntimeError, 'foo'" - expect { Bundler::Dsl.evaluate(bundled_app("Gemfile"), nil, true) }. + expect { Bundler::Dsl.evaluate(bundled_app_gemfile, nil, true) }. to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`: foo. Bundler cannot continue./i) end end @@ -291,7 +358,7 @@ RSpec.describe Bundler::Dsl do allow(Bundler).to receive(:default_gemfile).and_return(Pathname.new("./Gemfile")) subject.source("https://other-source.org") do - subject.gem("dobry-pies", :path => "foo") + subject.gem("dobry-pies", path: "foo") subject.gem("foo") end @@ -299,4 +366,193 @@ RSpec.describe Bundler::Dsl do end end end + + describe "#source with cooldown" do + before do + allow(@rubygems).to receive(:add_remote) + end + + it "accepts a non-negative integer" do + expect do + subject.source("https://rubygems.org", cooldown: 7) + end.not_to raise_error + end + + it "accepts 0 as an explicit disable" do + expect do + subject.source("https://rubygems.org", cooldown: 0) + end.not_to raise_error + end + + it "rejects a string" do + expect do + subject.source("https://rubygems.org", cooldown: "7") + end.to raise_error(Bundler::InvalidOption, /non-negative integer/) + end + + it "rejects a float" do + expect do + subject.source("https://rubygems.org", cooldown: 7.5) + end.to raise_error(Bundler::InvalidOption, /non-negative integer/) + end + + it "rejects a negative integer" do + expect do + subject.source("https://rubygems.org", cooldown: -7) + end.to raise_error(Bundler::InvalidOption, /non-negative integer/) + end + + it "rejects an array" do + expect do + subject.source("https://rubygems.org", cooldown: [7]) + end.to raise_error(Bundler::InvalidOption, /non-negative integer/) + end + end + + describe "#override" do + it "stores an Override for a gem with a version: operation" do + subject.override("rails", version: ">= 8.0") + + expect(subject.overrides.size).to eq(1) + override = subject.overrides.first + expect(override.target).to eq("rails") + expect(override.field).to eq(:version) + expect(override.operation).to eq(">= 8.0") + end + + it "accepts :ignore_upper as the operation" do + subject.override("nokogiri", version: :ignore_upper) + expect(subject.overrides.first.operation).to eq(:ignore_upper) + end + + it "accepts nil as the operation" do + subject.override("legacy", version: nil) + expect(subject.overrides.first.operation).to be_nil + end + + it "appends to overrides across multiple statements" do + subject.override("rails", version: ">= 8.0") + subject.override("nokogiri", version: :ignore_upper) + expect(subject.overrides.map(&:target)).to eq(["rails", "nokogiri"]) + end + + it "is empty by default" do + expect(subject.overrides).to eq([]) + end + + it "raises ArgumentError when target is :all and version: is given" do + expect do + subject.override(:all, version: ">= 8.0") + end.to raise_error(ArgumentError, /`override :all, version:` is not allowed/) + end + + it "rejects :all + version: even when other fields are also given" do + expect do + subject.override(:all, required_ruby_version: :ignore_upper, version: ">= 8.0") + end.to raise_error(ArgumentError, /`override :all, version:` is not allowed/) + end + + it "does not record any override when :all + version: is rejected" do + expect do + subject.override(:all, version: ">= 8.0") + end.to raise_error(ArgumentError) + expect(subject.overrides).to eq([]) + end + + it "raises ArgumentError when target is neither :all nor a string" do + expect do + subject.override(:rails, version: ">= 8.0") + end.to raise_error(ArgumentError, /target must be :all or a gem name string/) + end + + it "raises ArgumentError for an unsupported field" do + expect do + subject.override("rails", as: "y") + end.to raise_error(ArgumentError, /unsupported override field `as:`/) + end + + it "stores an Override for a gem with a required_ruby_version: operation" do + subject.override("rails", required_ruby_version: :ignore_upper) + override = subject.overrides.first + expect(override.target).to eq("rails") + expect(override.field).to eq(:required_ruby_version) + expect(override.operation).to eq(:ignore_upper) + end + + it "stores an Override for a gem with a required_rubygems_version: operation" do + subject.override("rails", required_rubygems_version: nil) + override = subject.overrides.first + expect(override.field).to eq(:required_rubygems_version) + expect(override.operation).to be_nil + end + + it "stores an Override targeting :all with a metadata field" do + subject.override(:all, required_ruby_version: :ignore_upper) + override = subject.overrides.first + expect(override.target).to eq(:all) + expect(override.field).to eq(:required_ruby_version) + expect(override.operation).to eq(:ignore_upper) + end + + it "stores an Override targeting :all with required_rubygems_version" do + subject.override(:all, required_rubygems_version: nil) + override = subject.overrides.first + expect(override.target).to eq(:all) + expect(override.field).to eq(:required_rubygems_version) + end + + it "raises ArgumentError for a non-string, non-symbol, non-nil operation" do + expect do + subject.override("rails", version: 42) + end.to raise_error(ArgumentError, /override operation must be a String, Symbol, or nil/) + end + + it "raises ArgumentError for an unsupported symbol operation" do + expect do + subject.override("rails", version: :explode) + end.to raise_error(ArgumentError, /unsupported override operation/) + end + + it "raises ArgumentError for an unparsable version string" do + expect do + subject.override("rails", version: "not a version") + end.to raise_error(ArgumentError, /invalid override version requirement/) + end + + it "does not record an override when the version string is invalid" do + expect do + subject.override("rails", version: "not a version") + end.to raise_error(ArgumentError) + expect(subject.overrides).to eq([]) + end + + it "rejects atomically when one field in a multi-field call is invalid" do + expect do + subject.override("rails", version: ">= 8.0", as: "y") + end.to raise_error(ArgumentError, /unsupported override field/) + expect(subject.overrides).to eq([]) + end + + it "raises ArgumentError when the same target and field are overridden twice" do + subject.override("rails", version: ">= 8.0") + expect do + subject.override("rails", version: :ignore_upper) + end.to raise_error(ArgumentError, /duplicate override for "rails" `version:`/) + end + + it "keeps the original override when a duplicate is rejected" do + subject.override("rails", version: ">= 8.0") + expect do + subject.override("rails", version: :ignore_upper) + end.to raise_error(ArgumentError) + expect(subject.overrides.size).to eq(1) + expect(subject.overrides.first.operation).to eq(">= 8.0") + end + + it "allows different targets with the same field" do + subject.override("rails", version: ">= 8.0") + subject.override("nokogiri", version: :ignore_upper) + expect(subject.overrides.size).to eq(2) + end + end end diff --git a/spec/bundler/bundler/endpoint_specification_spec.rb b/spec/bundler/bundler/endpoint_specification_spec.rb index a9371f6617..4fbd59d48f 100644 --- a/spec/bundler/bundler/endpoint_specification_spec.rb +++ b/spec/bundler/bundler/endpoint_specification_spec.rb @@ -5,9 +5,10 @@ RSpec.describe Bundler::EndpointSpecification do let(:version) { "1.0.0" } let(:platform) { Gem::Platform::RUBY } let(:dependencies) { [] } + let(:spec_fetcher) { double(:spec_fetcher) } let(:metadata) { nil } - subject(:spec) { described_class.new(name, version, platform, dependencies, metadata) } + subject(:spec) { described_class.new(name, version, platform, spec_fetcher, dependencies, metadata) } describe "#build_dependency" do let(:name) { "foo" } @@ -32,22 +33,6 @@ RSpec.describe Bundler::EndpointSpecification do ) 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 @@ -57,14 +42,81 @@ RSpec.describe Bundler::EndpointSpecification 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"}')) + and(a_string_including("The metadata was #{{ "rubygems" => ">\n" }.inspect}")) ) end end + + context "when the metadata has created_at" do + let(:metadata) { { "created_at" => ["2026-05-12T10:00:00Z"] } } + + it "parses created_at as a Time" do + expect(subject.created_at).to eq(Time.utc(2026, 5, 12, 10, 0, 0)) + end + end + + context "when the metadata has a string created_at (older rubygems shape)" do + let(:metadata) { { "created_at" => "2026-05-12T10:00:00Z" } } + + it "still parses created_at" do + expect(subject.created_at).to eq(Time.utc(2026, 5, 12, 10, 0, 0)) + end + end + + context "when created_at is truncated (older rubygems splits on colons)" do + let(:metadata) { { "created_at" => "2026-05-12T10" } } + + it "leaves created_at as nil instead of raising" do + expect(subject.created_at).to be_nil + end + end + + context "when the metadata has no created_at" do + let(:metadata) { { "checksum" => ["abc"] } } + let(:spec_fetcher) { double(:spec_fetcher, uri: "https://rubygems.org") } + + it "leaves created_at as nil" do + allow(Bundler::Checksum).to receive(:from_api).and_return(nil) + expect(subject.created_at).to be_nil + end + end + + context "when the metadata is nil" do + it "leaves created_at as nil" do + expect(subject.created_at).to be_nil + end + end + end + + describe "#required_ruby_version" do + context "required_ruby_version is already set on endpoint specification" do + existing_value = "already set value" + let(:required_ruby_version) { existing_value } + + it "should return the current value when already set on endpoint specification" do + expect(spec.required_ruby_version). eql?(existing_value) + end + end + + it "should return the remote spec value when not set on endpoint specification and remote spec has one" do + remote_value = "remote_value" + remote_spec = double(:remote_spec, required_ruby_version: remote_value, required_rubygems_version: nil) + allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec) + + expect(spec.required_ruby_version). eql?(remote_value) + end + + it "should use the default Gem Requirement value when not set on endpoint specification and not set on remote spec" do + remote_spec = double(:remote_spec, required_ruby_version: nil, required_rubygems_version: nil) + allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec) + expect(spec.required_ruby_version). eql?(Gem::Requirement.default) + end end it "supports equality comparison" do - other_spec = described_class.new("bar", version, platform, dependencies, metadata) + remote_spec = double(:remote_spec, required_ruby_version: nil, required_rubygems_version: nil) + allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec) + other_spec = described_class.new("bar", version, platform, spec_fetcher, dependencies, metadata) expect(spec).to eql(spec) expect(spec).to_not eql(other_spec) end diff --git a/spec/bundler/bundler/env_spec.rb b/spec/bundler/bundler/env_spec.rb index f0ab5d5f35..2b7dbde217 100644 --- a/spec/bundler/bundler/env_spec.rb +++ b/spec/bundler/bundler/env_spec.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require "openssl" require "bundler/settings" +require "openssl" RSpec.describe Bundler::Env do - let(:git_proxy_stub) { Bundler::Source::Git::GitProxy.new(nil, nil, nil) } + let(:git_proxy_stub) { Bundler::Source::Git::GitProxy.new(nil, nil) } describe "#report" do it "prints the environment" do @@ -27,9 +27,9 @@ RSpec.describe Bundler::Env do end it "prints gem path" do - with_clear_paths("GEM_PATH", "/a/b/c:/d/e/f") do + with_clear_paths("GEM_PATH", "/a/b/c#{File::PATH_SEPARATOR}d/e/f") do out = described_class.report - expect(out).to include("Gem Path /a/b/c:/d/e/f") + expect(out).to include("Gem Path /a/b/c#{File::PATH_SEPARATOR}d/e/f") end end @@ -42,6 +42,8 @@ RSpec.describe Bundler::Env do it "prints user path" do with_clear_paths("HOME", "/a/b/c") do + allow(File).to receive(:exist?) + allow(File).to receive(:exist?).with("/a/b/c/.gem").and_return(true) out = described_class.report expect(out).to include("User Path /a/b/c/.gem") end @@ -54,7 +56,7 @@ RSpec.describe Bundler::Env do end end - private + private def with_clear_paths(env_var, env_value) old_env_var = ENV[env_var] @@ -68,46 +70,76 @@ RSpec.describe Bundler::Env do context "when there is a Gemfile and a lockfile and print_gemfile is true" do before do - gemfile "gem 'rack', '1.0.0'" + gemfile "source 'https://gem.repo1'; gem 'myrack', '1.0.0'" lockfile <<-L GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: - rack (1.0.0) + myrack (1.0.0) DEPENDENCIES - rack + myrack BUNDLED WITH 1.10.0 L + + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) end - let(:output) { described_class.report(:print_gemfile => true) } + let(:output) { described_class.report(print_gemfile: true) } it "prints the Gemfile" do expect(output).to include("Gemfile") - expect(output).to include("'rack', '1.0.0'") + expect(output).to include("'myrack', '1.0.0'") end it "prints the lockfile" do expect(output).to include("Gemfile.lock") - expect(output).to include("rack (1.0.0)") + expect(output).to include("myrack (1.0.0)") end end context "when there no Gemfile and print_gemfile is true" do - let(:output) { described_class.report(:print_gemfile => true) } + let(:output) { described_class.report(print_gemfile: true) } it "prints the environment" do expect(output).to start_with("## Environment") end end + context "when there's bundler config with credentials" do + before do + bundle "config set https://localgemserver.test/ user:pass" + end + + let(:output) { described_class.report(print_gemfile: true) } + + it "prints the config with redacted values" do + expect(output).to include("https://localgemserver.test") + expect(output).to include("user:[REDACTED]") + expect(output).to_not include("user:pass") + end + end + + context "when there's bundler config with OAuth token credentials" do + before do + bundle "config set https://localgemserver.test/ api_token:x-oauth-basic" + end + + let(:output) { described_class.report(print_gemfile: true) } + + it "prints the config with redacted values" do + expect(output).to include("https://localgemserver.test") + expect(output).to include("[REDACTED]:x-oauth-basic") + expect(output).to_not include("api_token:x-oauth-basic") + end + end + context "when Gemfile contains a gemspec and print_gemspecs is true" do let(:gemspec) do - strip_whitespace(<<-GEMSPEC) + <<~GEMSPEC Gem::Specification.new do |gem| gem.name = "foo" gem.author = "Fumofu" @@ -116,15 +148,17 @@ RSpec.describe Bundler::Env do end before do - gemfile("gemspec") + gemfile("source 'https://gem.repo1'; gemspec") - File.open(bundled_app.join("foo.gemspec"), "wb") do |f| + File.open(bundled_app("foo.gemspec"), "wb") do |f| f.write(gemspec) end + + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) end it "prints the gemspec" do - output = described_class.report(:print_gemspecs => true) + output = described_class.report(print_gemspecs: true) expect(output).to include("foo.gemspec") expect(output).to include(gemspec) @@ -133,28 +167,30 @@ RSpec.describe Bundler::Env do context "when eval_gemfile is used" do it "prints all gemfiles" do - create_file "other/Gemfile-other", "gem 'rack'" - create_file "other/Gemfile", "eval_gemfile 'Gemfile-other'" - create_file "Gemfile-alt", <<-G - source "#{file_uri_for(gem_repo1)}" + gemfile bundled_app("other/Gemfile-other"), "gem 'myrack'" + gemfile bundled_app("other/Gemfile"), "eval_gemfile 'Gemfile-other'" + gemfile bundled_app("Gemfile-alt"), <<-G + source "https://gem.repo1" eval_gemfile "other/Gemfile" G - gemfile "eval_gemfile #{File.expand_path("Gemfile-alt").dump}" + gemfile "eval_gemfile #{bundled_app("Gemfile-alt").to_s.dump}" + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + allow(Bundler::SharedHelpers).to receive(:pwd).and_return(bundled_app) - output = described_class.report(:print_gemspecs => true) - expect(output).to include(strip_whitespace(<<-ENV)) + output = described_class.report(print_gemspecs: true) + expect(output).to include(<<~ENV) ## Gemfile ### Gemfile ```ruby - eval_gemfile #{File.expand_path("Gemfile-alt").dump} + eval_gemfile #{bundled_app("Gemfile-alt").to_s.dump} ``` ### Gemfile-alt ```ruby - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" eval_gemfile "other/Gemfile" ``` @@ -167,13 +203,13 @@ RSpec.describe Bundler::Env do ### other/Gemfile-other ```ruby - gem 'rack' + gem 'myrack' ``` ### Gemfile.lock ``` - <No #{bundled_app("Gemfile.lock")} found> + <No #{bundled_app_lock} found> ``` ENV end @@ -181,20 +217,21 @@ RSpec.describe Bundler::Env do context "when the git version is OS specific" do it "includes OS specific information with the version number" do - expect(git_proxy_stub).to receive(:git).with("--version"). - and_return("git version 1.2.3 (Apple Git-BS)") + status = double("success?" => true) + expect(Open3).to receive(:capture3).with("git", "--version"). + and_return(["git version 1.2.3 (Apple Git-BS)", "", status]) expect(Bundler::Source::Git::GitProxy).to receive(:new).and_return(git_proxy_stub) - expect(described_class.report).to include("Git 1.2.3 (Apple Git-BS)") + expect(described_class.report).to include("Git 1.2.3 (Apple Git-BS)") end end - end - describe ".version_of" do - let(:parsed_version) { described_class.send(:version_of, "ruby") } - - it "strips version of new line characters" do - expect(parsed_version).to_not end_with("\n") + it "no longer reports the Tools section or external tool versions" do + report = described_class.report + expect(report).not_to include("Tools") + ["rbenv", "RVM", "chruby"].each do |tool| + expect(report).not_to include(tool) + end end end end diff --git a/spec/bundler/bundler/environment_preserver_spec.rb b/spec/bundler/bundler/environment_preserver_spec.rb index 530ca6f835..6c7066d0c6 100644 --- a/spec/bundler/bundler/environment_preserver_spec.rb +++ b/spec/bundler/bundler/environment_preserver_spec.rb @@ -27,8 +27,12 @@ RSpec.describe Bundler::EnvironmentPreserver do context "when a key is empty" do let(:env) { { "foo" => "" } } - it "should not create backup entries" do - expect(subject).not_to have_key "BUNDLER_ORIG_foo" + it "should keep the original entry" do + expect(subject["foo"]).to be_empty + end + + it "should still create backup entries" do + expect(subject).to have_key "BUNDLER_ORIG_foo" end end @@ -71,8 +75,12 @@ RSpec.describe Bundler::EnvironmentPreserver do context "when the original key is empty" do let(:env) { { "foo" => "my-foo", "BUNDLER_ORIG_foo" => "" } } - it "should keep the current value" do - expect(subject["foo"]).to eq("my-foo") + it "should restore the original value" do + expect(subject["foo"]).to be_empty + end + + it "should delete the backup value" do + expect(subject.key?("BUNDLER_ORIG_foo")).to eq(false) end end end diff --git a/spec/bundler/bundler/errors_spec.rb b/spec/bundler/bundler/errors_spec.rb new file mode 100644 index 0000000000..b62d85d32b --- /dev/null +++ b/spec/bundler/bundler/errors_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::IncorrectLockfileDependencies do + describe "#message" do + let(:spec) do + double("LazySpecification", full_name: "rubocop-1.82.0") + end + + context "without dependency details" do + subject { described_class.new(spec) } + + it "provides a basic error message" do + expect(subject.message).to include("Bundler found incorrect dependencies in the lockfile for rubocop-1.82.0") + expect(subject.message).to include("Please run `bundle install` to regenerate the lockfile.") + end + end + + context "with dependency details" do + let(:actual_dependencies) do + [ + Gem::Dependency.new("json", [">= 2.3", "< 4.0"]), + Gem::Dependency.new("parallel", ["~> 1.10"]), + Gem::Dependency.new("parser", [">= 3.3.0.2"]), + ] + end + + let(:lockfile_dependencies) do + [ + Gem::Dependency.new("json", [">= 2.3", "< 3.0"]), + Gem::Dependency.new("parallel", ["~> 1.10"]), + Gem::Dependency.new("parser", [">= 3.2.0.0"]), + ] + end + + subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) } + + it "shows only mismatched dependencies" do + message = subject.message + + expect(message).to include("json: gemspec specifies") + expect(message).to include("parser: gemspec specifies") + expect(message).not_to include("parallel") + end + end + + context "when gemspec has dependencies but lockfile has none" do + let(:actual_dependencies) do + [ + Gem::Dependency.new("myrack-test", ["~> 1.0"]), + ] + end + + let(:lockfile_dependencies) { [] } + + subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) } + + it "shows the dependency as not in lockfile" do + message = subject.message + + expect(message).to include("myrack-test: gemspec specifies ~> 1.0, not in lockfile") + end + end + + context "when gemspec has no dependencies but lockfile has some" do + let(:actual_dependencies) { [] } + + let(:lockfile_dependencies) do + [ + Gem::Dependency.new("unexpected", ["~> 1.0"]), + ] + end + + subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) } + + it "shows the dependency as not in gemspec" do + message = subject.message + + expect(message).to include("unexpected: not in gemspec, lockfile has ~> 1.0") + end + end + end + + describe "#status_code" do + let(:spec) { double("LazySpecification", full_name: "test-1.0.0") } + subject { described_class.new(spec) } + + it "returns 41" do + expect(subject.status_code).to eq(41) + end + end +end diff --git a/spec/bundler/bundler/fetcher/base_spec.rb b/spec/bundler/bundler/fetcher/base_spec.rb index df1245d44d..b8c6b57b10 100644 --- a/spec/bundler/bundler/fetcher/base_spec.rb +++ b/spec/bundler/bundler/fetcher/base_spec.rb @@ -4,15 +4,16 @@ RSpec.describe Bundler::Fetcher::Base do let(:downloader) { double(:downloader) } let(:remote) { double(:remote) } let(:display_uri) { "http://sample_uri.com" } + let(:gem_remote_fetcher) { nil } class TestClass < described_class; end - subject { TestClass.new(downloader, remote, display_uri) } + subject { TestClass.new(downloader, remote, display_uri, gem_remote_fetcher) } describe "#initialize" do context "with the abstract Base class" do it "should raise an error" do - expect { described_class.new(downloader, remote, display_uri) }.to raise_error(RuntimeError, "Abstract class") + expect { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) }.to raise_error(RuntimeError, "Abstract class") end end @@ -36,7 +37,7 @@ RSpec.describe Bundler::Fetcher::Base do end describe "#fetch_uri" do - let(:remote_uri_obj) { URI("http://rubygems.org") } + let(:remote_uri_obj) { Gem::URI("http://rubygems.org") } before { allow(subject).to receive(:remote_uri).and_return(remote_uri_obj) } @@ -49,10 +50,10 @@ RSpec.describe Bundler::Fetcher::Base do end context "when the remote uri's host is not rubygems.org" do - let(:remote_uri_obj) { URI("http://otherhost.org") } + let(:remote_uri_obj) { Gem::URI("http://otherhost.org") } it "should return the remote uri" do - expect(subject.fetch_uri).to eq(URI("http://otherhost.org")) + expect(subject.fetch_uri).to eq(Gem::URI("http://otherhost.org")) end end diff --git a/spec/bundler/bundler/fetcher/compact_index_spec.rb b/spec/bundler/bundler/fetcher/compact_index_spec.rb index f5ae6f4d77..aa536673d9 100644 --- a/spec/bundler/bundler/fetcher/compact_index_spec.rb +++ b/spec/bundler/bundler/fetcher/compact_index_spec.rb @@ -1,13 +1,21 @@ # frozen_string_literal: true +# load CompactIndexClient upfront to prevent thread safety issues during parallel specs +require "bundler/compact_index_client" + 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) } + let(:response) { double(:response) } + let(:downloader) { double(:downloader, fetch: response) } + let(:display_uri) { Gem::URI("http://sampleuri.com") } + let(:remote) { double(:remote, cache_slug: "lsjdf", uri: display_uri) } + let(:gem_remote_fetcher) { nil } + let(:compact_index) { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) } + let(:compact_index_client) { double(:compact_index_client, available?: true, info: [["lskdjf", "1", nil, [], []]]) } before do + allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified).and_return(true) allow(compact_index).to receive(:log_specs) {} + allow(compact_index).to receive(:compact_index_client).and_return(compact_index_client) end describe "#specs_for_names" do @@ -28,11 +36,6 @@ RSpec.describe Bundler::Fetcher::CompactIndex do 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 @@ -62,7 +65,7 @@ RSpec.describe Bundler::Fetcher::CompactIndex do context "when FIPS-mode is active" do before do - allow(OpenSSL::Digest::MD5).to receive(:digest). + allow(OpenSSL::Digest).to receive(:digest).with("MD5", ""). and_raise(OpenSSL::Digest::DigestError) end diff --git a/spec/bundler/bundler/fetcher/dependency_spec.rb b/spec/bundler/bundler/fetcher/dependency_spec.rb index 081fdff34d..501bc269a5 100644 --- a/spec/bundler/bundler/fetcher/dependency_spec.rb +++ b/spec/bundler/bundler/fetcher/dependency_spec.rb @@ -2,10 +2,11 @@ RSpec.describe Bundler::Fetcher::Dependency do let(:downloader) { double(:downloader) } - let(:remote) { double(:remote, :uri => URI("http://localhost:5000")) } + let(:remote) { double(:remote, uri: Gem::URI("http://localhost:5000")) } let(:display_uri) { "http://sample_uri.com" } + let(:gem_remote_fetcher) { nil } - subject { described_class.new(downloader, remote, display_uri) } + subject { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) } describe "#available?" do let(:dependency_api_uri) { double(:dependency_api_uri) } @@ -155,9 +156,9 @@ RSpec.describe Bundler::Fetcher::Dependency do end end - shared_examples_for "the error suggests retrying with the full index" do - it "should log the inability to fetch from API at debug level" do - expect(Bundler).to receive_message_chain(:ui, :debug).with("could not fetch from the dependency API\nit's suggested to retry using the full index via `bundle install --full-index`") + shared_examples_for "the error is logged" do + it "should log the inability to fetch from API at debug level, and mention retrying" do + expect(Bundler).to receive_message_chain(:ui, :debug).with("could not fetch from the dependency API, trying the full index") subject.specs(gem_names, full_dependency_list, last_spec_list) end end @@ -166,25 +167,21 @@ RSpec.describe Bundler::Fetcher::Dependency do before { allow(subject).to receive(:dependency_specs) { raise Bundler::HTTPError.new } } it_behaves_like "the error is properly handled" - it_behaves_like "the error suggests retrying with the full index" + it_behaves_like "the error is logged" end context "when a GemspecError occurs" do before { allow(subject).to receive(:dependency_specs) { raise Bundler::GemspecError.new } } it_behaves_like "the error is properly handled" - it_behaves_like "the error suggests retrying with the full index" + it_behaves_like "the error is logged" end context "when a MarshalError occurs" do before { allow(subject).to receive(:dependency_specs) { raise Bundler::MarshalError.new } } it_behaves_like "the error is properly handled" - - it "should log the inability to fetch from API and mention retrying" do - expect(Bundler).to receive_message_chain(:ui, :debug).with("could not fetch from the dependency API, trying the full index") - subject.specs(gem_names, full_dependency_list, last_spec_list) - end + it_behaves_like "the error is logged" end end @@ -214,37 +211,49 @@ RSpec.describe Bundler::Fetcher::Dependency do let(:gem_names) { [%w[foo bar], %w[bundler rubocop]] } let(:dep_api_uri) { double(:dep_api_uri) } let(:unmarshalled_gems) { double(:unmarshalled_gems) } - let(:fetch_response) { double(:fetch_response, :body => double(:body)) } - let(:rubygems_limit) { 50 } + let(:fetch_response) { double(:fetch_response, body: double(:body)) } + let(:rubygems_limit) { 100 } 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(Bundler).to receive(:safe_load_marshal).with(fetch_response.body).and_return([unmarshalled_gems]) expect(subject.unmarshalled_dep_gems(gem_names)).to eq([unmarshalled_gems]) end + + it "should fetch as many dependencies as specified" do + allow(subject).to receive(:dependency_api_uri).with([%w[foo bar]]).and_return(dep_api_uri) + allow(subject).to receive(:dependency_api_uri).with([%w[bundler rubocop]]).and_return(dep_api_uri) + + expect(downloader).to receive(:fetch).twice.with(dep_api_uri).and_return(fetch_response) + expect(Bundler).to receive(:safe_load_marshal).twice.with(fetch_response.body).and_return([unmarshalled_gems]) + + Bundler.settings.temporary(api_request_size: 1) do + expect(subject.unmarshalled_dep_gems(gem_names)).to eq([unmarshalled_gems, unmarshalled_gems]) + end + end end describe "#get_formatted_specs_and_deps" do let(:gem_list) do [ { - :dependencies => { + dependencies: { "resque" => "req3,req4", }, - :name => "typhoeus", - :number => "1.0.1", - :platform => "ruby", + name: "typhoeus", + number: "1.0.1", + platform: "ruby", }, { - :dependencies => { + dependencies: { "faraday" => "req1,req2", }, - :name => "grape", - :number => "2.0.2", - :platform => "jruby", + name: "grape", + number: "2.0.2", + platform: "jruby", }, ] end @@ -258,7 +267,7 @@ RSpec.describe Bundler::Fetcher::Dependency do end describe "#dependency_api_uri" do - let(:uri) { URI("http://gem-api.com") } + let(:uri) { Gem::URI("http://gem-api.com") } context "with gem names" do let(:gem_names) { %w[foo bar bundler rubocop] } diff --git a/spec/bundler/bundler/fetcher/downloader_spec.rb b/spec/bundler/bundler/fetcher/downloader_spec.rb index f985b88982..edf426328a 100644 --- a/spec/bundler/bundler/fetcher/downloader_spec.rb +++ b/spec/bundler/bundler/fetcher/downloader_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Bundler::Fetcher::Downloader do let(:connection) { double(:connection) } let(:redirect_limit) { 5 } - let(:uri) { URI("http://www.uri-to-fetch.com/api/v2/endpoint") } + let(:uri) { Gem::URI("http://www.uri-to-fetch.com/api/v2/endpoint") } let(:options) { double(:options) } subject { described_class.new(connection, redirect_limit) } @@ -27,7 +27,7 @@ RSpec.describe Bundler::Fetcher::Downloader do end context "logging" do - let(:http_response) { Net::HTTPSuccess.new("1.1", 200, "Success") } + let(:http_response) { Gem::Net::HTTPSuccess.new("1.1", 200, "Success") } it "should log the HTTP response code and message to debug" do expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP 200 Success #{uri}") @@ -35,56 +35,61 @@ RSpec.describe Bundler::Fetcher::Downloader do end end - context "when the request response is a Net::HTTPRedirection" do - let(:http_response) { Net::HTTPRedirection.new(httpv, 308, "Moved") } + context "when the request response is a Gem::Net::HTTPRedirection" do + let(:http_response) { Gem::Net::HTTPRedirection.new(httpv, 308, "Moved") } before { http_response["location"] = "http://www.redirect-uri.com/api/v2/endpoint" } it "should try to fetch the redirect uri and iterate the # requests counter" do - expect(subject).to receive(:fetch).with(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) + expect(subject).to receive(:fetch).with(Gem::URI("http://www.uri-to-fetch.com/api/v2/endpoint"), options, 0).and_call_original + expect(subject).to receive(:fetch).with(Gem::URI("http://www.redirect-uri.com/api/v2/endpoint"), options, 1) subject.fetch(uri, options, counter) end context "when the redirect uri and original uri are the same" do - let(:uri) { URI("ssh://username:password@www.uri-to-fetch.com/api/v2/endpoint") } + let(:uri) { Gem::URI("ssh://username:password@www.uri-to-fetch.com/api/v2/endpoint") } before { http_response["location"] = "ssh://www.uri-to-fetch.com/api/v1/endpoint" } it "should set the same user and password for the redirect uri" do - expect(subject).to receive(:fetch).with(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) + expect(subject).to receive(:fetch).with(Gem::URI("ssh://username:password@www.uri-to-fetch.com/api/v2/endpoint"), options, 0).and_call_original + expect(subject).to receive(:fetch).with(Gem::URI("ssh://username:password@www.uri-to-fetch.com/api/v1/endpoint"), options, 1) subject.fetch(uri, options, counter) end end end - context "when the request response is a Net::HTTPSuccess" do - let(:http_response) { Net::HTTPSuccess.new("1.1", 200, "Success") } + context "when the request response is a Gem::Net::HTTPSuccess" do + let(:http_response) { Gem::Net::HTTPSuccess.new("1.1", 200, "Success") } it "should return the response body" do expect(subject.fetch(uri, options, counter)).to eq(http_response) end end - context "when the request response is a Net::HTTPRequestEntityTooLarge" do - let(:http_response) { Net::HTTPRequestEntityTooLarge.new("1.1", 413, "Too Big") } + context "when the request response is a Gem::Net::HTTPRequestEntityTooLarge" do + let(:http_response) { Gem::Net::HTTPRequestEntityTooLarge.new("1.1", 413, "Too Big") } it "should raise a Bundler::Fetcher::FallbackError with the response body" do expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::FallbackError, "Body with info") end end - context "when the request response is a Net::HTTPUnauthorized" do - let(:http_response) { Net::HTTPUnauthorized.new("1.1", 401, "Unauthorized") } + context "when the request response is a Gem::Net::HTTPUnauthorized" do + let(:http_response) { Gem::Net::HTTPUnauthorized.new("1.1", 401, "Unauthorized") } it "should raise a Bundler::Fetcher::AuthenticationRequiredError with the uri host" do expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError, /Authentication is required for www.uri-to-fetch.com/) end - context "when the there are credentials provided in the request" do - let(:uri) { URI("http://user:password@www.uri-to-fetch.com") } + it "should raise a Bundler::Fetcher::AuthenticationRequiredError with advice" do + expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError, + /`bundle config set --global www\.uri-to-fetch\.com username:password`.*`BUNDLE_WWW__URI___TO___FETCH__COM`/m) + end + + context "when there are credentials provided in the request" do + let(:uri) { Gem::URI("http://user:password@www.uri-to-fetch.com") } it "should raise a Bundler::Fetcher::BadAuthenticationError that doesn't contain the password" do expect { subject.fetch(uri, options, counter) }. @@ -93,29 +98,71 @@ RSpec.describe Bundler::Fetcher::Downloader do end end - context "when the request response is a Net::HTTPNotFound" do - let(:http_response) { Net::HTTPNotFound.new("1.1", 404, "Not Found") } + context "when the request response is a Gem::Net::HTTPForbidden" do + let(:http_response) { Gem::Net::HTTPForbidden.new("1.1", 403, "Forbidden") } + let(:uri) { Gem::URI("http://user:password@www.uri-to-fetch.com") } + + it "should raise a Bundler::Fetcher::AuthenticationForbiddenError with the uri host" do + expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::AuthenticationForbiddenError, + /Access token could not be authenticated for www.uri-to-fetch.com/) + end + end + + context "when the request response is a Gem::Net::HTTPNotFound" do + let(:http_response) { Gem::Net::HTTPNotFound.new("1.1", 404, "Not Found") } - it "should raise a Bundler::Fetcher::FallbackError with Net::HTTPNotFound" do + it "should raise a Bundler::Fetcher::FallbackError with Gem::Net::HTTPNotFound" do expect { subject.fetch(uri, options, counter) }. - to raise_error(Bundler::Fetcher::FallbackError, "Net::HTTPNotFound: http://www.uri-to-fetch.com/api/v2/endpoint") + to raise_error(Bundler::Fetcher::FallbackError, "Gem::Net::HTTPNotFound: http://www.uri-to-fetch.com/api/v2/endpoint") end - context "when the there are credentials provided in the request" do - let(:uri) { URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") } + context "when there are credentials provided in the request" do + let(:uri) { Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") } it "should raise a Bundler::Fetcher::FallbackError that doesn't contain the password" do expect { subject.fetch(uri, options, counter) }. - to raise_error(Bundler::Fetcher::FallbackError, "Net::HTTPNotFound: http://username@www.uri-to-fetch.com/api/v2/endpoint") + to raise_error(Bundler::Fetcher::FallbackError, "Gem::Net::HTTPNotFound: http://username@www.uri-to-fetch.com/api/v2/endpoint") end end end + context "when the request response is a Gem::Net::HTTPRequestedRangeNotSatisfiable" do + let(:http_response) { Gem::Net::HTTPRequestedRangeNotSatisfiable.new("1.1", 416, "Range Not Satisfiable") } + let(:success_response) { Gem::Net::HTTPSuccess.new("1.1", 200, "Success") } + let(:options) { { "Range" => "bytes=1000-", "If-None-Match" => "some-etag" } } + + before do + # First request returns 416, retry request returns success + allow(subject).to receive(:request).with(uri, options).and_return(http_response) + allow(subject).to receive(:request).with(uri, { "If-None-Match" => "some-etag" }).and_return(success_response) + end + + # The 416 handler removes the Range header and retries without incrementing the counter. + # Importantly, it does NOT add Accept-Encoding header, which would break Ruby's + # automatic gzip decompression (see issue #9271 for details on that bug). + it "should retry the request without the Range header" do + expect(subject).to receive(:request).with(uri, options).ordered + expect(subject).to receive(:request).with(uri, hash_excluding("Range", "Accept-Encoding")).ordered + subject.fetch(uri, options, counter) + end + + it "should preserve other headers on retry" do + expect(subject).to receive(:request).with(uri, options).ordered + expect(subject).to receive(:request).with(uri, hash_including("If-None-Match" => "some-etag")).ordered + subject.fetch(uri, options, counter) + end + + it "should return the successful response" do + result = subject.fetch(uri, options, counter) + expect(result).to eq(success_response) + end + end + context "when the request response is some other type" do - let(:http_response) { Net::HTTPBadGateway.new("1.1", 500, "Fatal Error") } + let(:http_response) { Gem::Net::HTTPBadGateway.new("1.1", 500, "Fatal Error") } it "should raise a Bundler::HTTPError with the response class and body" do - expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::HTTPError, "Net::HTTPBadGateway: Body with info") + expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::HTTPError, "Gem::Net::HTTPBadGateway: Body with info") end end end @@ -125,7 +172,7 @@ RSpec.describe Bundler::Fetcher::Downloader do let(:response) { double(:response) } before do - allow(Net::HTTP::Get).to receive(:new).with("/api/v2/endpoint", options).and_return(net_http_get) + allow(Gem::Net::HTTP::Get).to receive(:new).with("/api/v2/endpoint", options).and_return(net_http_get) allow(connection).to receive(:request).with(uri, net_http_get).and_return(response) end @@ -137,62 +184,46 @@ RSpec.describe Bundler::Fetcher::Downloader do context "when there is a user provided in the request" do context "and there is also a password provided" do context "that contains cgi escaped characters" do - let(:uri) { URI("http://username:password%24@www.uri-to-fetch.com/api/v2/endpoint") } + let(:uri) { Gem::URI("http://username:password%24@www.uri-to-fetch.com/api/v2/endpoint") } - it "should request basic authentication with the username and password" do + it "should request basic authentication with the username and password, and log the HTTP GET request to debug, without the password" do expect(net_http_get).to receive(:basic_auth).with("username", "password$") + expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://username@www.uri-to-fetch.com/api/v2/endpoint") 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 + let(:uri) { Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") } + it "should request basic authentication with the username and proper cgi compliant password, and log the HTTP GET request to debug, without the password" do expect(net_http_get).to receive(:basic_auth).with("username", "password") + expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://username@www.uri-to-fetch.com/api/v2/endpoint") 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") } + context "and it's used as the authentication token" do + let(:uri) { Gem::URI("http://username@www.uri-to-fetch.com/api/v2/endpoint") } - it "should request basic authentication with just the user" do + it "should request basic authentication with just the user, and log the HTTP GET request to debug, without the token" do expect(net_http_get).to receive(:basic_auth).with("username", nil) + 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 end - context "that contains cgi escaped characters" do - let(:uri) { URI("http://username%24@www.uri-to-fetch.com/api/v2/endpoint") } + context "and it's used as the authentication token, and contains cgi escaped characters" do + let(:uri) { Gem::URI("http://username%24@www.uri-to-fetch.com/api/v2/endpoint") } - it "should request basic authentication with the proper cgi compliant password user" do + it "should request basic authentication with the proper cgi compliant password user, and log the HTTP GET request to debug, without the token" do expect(net_http_get).to receive(:basic_auth).with("username$", nil) + 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 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 } } @@ -202,66 +233,68 @@ RSpec.describe Bundler::Fetcher::Downloader do end end - context "when the request response causes an error included in HTTP_ERRORS" do - let(:message) { nil } - let(:error) { RuntimeError.new(message) } + context "when the request response causes an HTTP error" do + let(:message) { "error about network" } + let(:error) { error_class.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" } + context "that it's retryable" do + let(:error_class) { Gem::Timeout::Error } - 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/) + 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 - 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)") + "Network error while fetching http://www.uri-to-fetch.com/api/v2/endpoint (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") } + context "when there are credentials provided in the request" do + let(:uri) { Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") } before do allow(net_http_get).to receive(:basic_auth).with("username", "password") end 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)") + "Network error while fetching http://username@www.uri-to-fetch.com/api/v2/endpoint (error about network)") end end end - context "when error message is about no route to host" do + context "when error is about the host being down" do + let(:error_class) { Gem::Net::HTTP::Persistent::Error } + 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 is about connection refused" do + let(:error_class) { Gem::Net::HTTP::Persistent::Error } + let(:message) { "connection refused 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 is about no route to host" do + let(:error_class) { SocketError } 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})") + 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 end diff --git a/spec/bundler/bundler/fetcher/gem_remote_fetcher_spec.rb b/spec/bundler/bundler/fetcher/gem_remote_fetcher_spec.rb new file mode 100644 index 0000000000..df1a58d843 --- /dev/null +++ b/spec/bundler/bundler/fetcher/gem_remote_fetcher_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require "rubygems/remote_fetcher" +require "bundler/fetcher/gem_remote_fetcher" +require_relative "../../support/artifice/helpers/artifice" +require "bundler/vendored_persistent.rb" + +RSpec.describe Bundler::Fetcher::GemRemoteFetcher do + describe "Parallel download" do + it "download using multiple connections from the pool" do + unless Bundler.rubygems.provides?(">= 4.0.0.dev") + skip "This example can only run when RubyGems supports multiple http connection pool" + end + + require_relative "../../support/artifice/helpers/endpoint" + concurrent_ruby_path = Dir[scoped_base_system_gem_path.join("gems/concurrent-ruby-*/lib/concurrent-ruby")].first + $LOAD_PATH.unshift(concurrent_ruby_path) + require "concurrent-ruby" + + require_rack_test + responses = [] + + latch1 = Concurrent::CountDownLatch.new + latch2 = Concurrent::CountDownLatch.new + previous_client = Gem::Request::ConnectionPools.client + dummy_endpoint = Class.new(Endpoint) do + get "/foo" do + latch2.count_down + latch1.wait + + responses << "foo" + end + + get "/bar" do + responses << "bar" + + latch1.count_down + end + end + + Artifice.activate_with(dummy_endpoint) + Gem::Request::ConnectionPools.client = Gem::Net::HTTP + + first_request = Thread.new do + subject.fetch_path("https://example.org/foo") + end + second_request = Thread.new do + latch2.wait + subject.fetch_path("https://example.org/bar") + end + + [first_request, second_request].each(&:join) + + expect(responses).to eq(["bar", "foo"]) + ensure + Artifice.deactivate + Gem::Request::ConnectionPools.client = previous_client + end + end +end diff --git a/spec/bundler/bundler/fetcher/index_spec.rb b/spec/bundler/bundler/fetcher/index_spec.rb index d5ededae3e..a6a18efd98 100644 --- a/spec/bundler/bundler/fetcher/index_spec.rb +++ b/spec/bundler/bundler/fetcher/index_spec.rb @@ -1,13 +1,18 @@ # frozen_string_literal: true +require "rubygems/remote_fetcher" + RSpec.describe Bundler::Fetcher::Index do let(:downloader) { nil } - let(:remote) { nil } + let(:remote) { double(:remote, uri: remote_uri) } + let(:remote_uri) { Gem::URI("http://#{userinfo}remote-uri.org") } + let(:userinfo) { "" } let(:display_uri) { "http://sample_uri.com" } let(:rubygems) { double(:rubygems) } let(:gem_names) { %w[foo bar] } + let(:gem_remote_fetcher) { nil } - subject { described_class.new(downloader, remote, display_uri) } + subject { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) } before { allow(Bundler).to receive(:rubygems).and_return(rubygems) } @@ -17,100 +22,54 @@ RSpec.describe Bundler::Fetcher::Index do 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" } - - 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 + before do + allow(rubygems).to receive(:fetch_all_remote_specs) { raise Gem::RemoteFetcher::FetchError.new(error_message, display_uri) } + end - context "and there was no userinfo" do - let(:userinfo) { nil } + context "when certificate verify failed" do + let(:error_message) { "certificate verify failed" } - 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 + 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 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 } + 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 + 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 - 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)) } + context "and there was userinfo" do + let(:userinfo) { "user:pass@" } - 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") + 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://user@remote-uri.org}) 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) } } + context "when a 403 response occurs" do + let(:error_message) { "403" } - it_behaves_like "the error is properly handled" + it "should raise a Bundler::Fetcher::AuthenticationForbiddenError" do + expect { subject.specs(gem_names) }.to raise_error(Bundler::Fetcher::AuthenticationForbiddenError, + %r{Access token could not be authenticated for http://remote-uri.org}) + end end - context "when a OpenSSL::SSL::SSLError occurs" do - before { allow(rubygems).to receive(:fetch_all_remote_specs) { raise OpenSSL::SSL::SSLError.new(error_message) } } + context "any other message is returned" do + let(:error_message) { "You get an error, you get an error!" } - 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) } } + before { allow(Bundler).to receive(:ui).and_return(double(trace: nil)) } - it_behaves_like "the error is properly handled" + it "should raise a Bundler::HTTPError" do + expect { subject.specs(gem_names) }.to raise_error(Bundler::HTTPError, "Could not fetch specs from http://sample_uri.com due to underlying error <You get an error, you get an error! (http://sample_uri.com)>") + end end end end diff --git a/spec/bundler/bundler/fetcher_spec.rb b/spec/bundler/bundler/fetcher_spec.rb index 184b9efa64..e20f7e7c48 100644 --- a/spec/bundler/bundler/fetcher_spec.rb +++ b/spec/bundler/bundler/fetcher_spec.rb @@ -3,8 +3,8 @@ require "bundler/fetcher" RSpec.describe Bundler::Fetcher do - let(:uri) { URI("https://example.com") } - let(:remote) { double("remote", :uri => uri, :original_uri => nil) } + let(:uri) { Gem::URI("https://example.com") } + let(:remote) { double("remote", uri: uri, original_uri: nil) } subject(:fetcher) { Bundler::Fetcher.new(remote) } @@ -26,7 +26,7 @@ RSpec.describe Bundler::Fetcher do context "when Gem.configuration specifies http_proxy " do let(:proxy) { "http://proxy-example2.com" } before do - allow(Bundler.rubygems.configuration).to receive(:[]).with(:http_proxy).and_return(proxy) + allow(Gem.configuration).to receive(:[]).with(:http_proxy).and_return(proxy) end it "consider Gem.configuration when determine proxy" do expect(fetcher.http_proxy).to match("http://proxy-example2.com") @@ -45,9 +45,9 @@ RSpec.describe Bundler::Fetcher do end context "when a rubygems source mirror is set" do - let(:orig_uri) { URI("http://zombo.com") } + let(:orig_uri) { Gem::URI("http://zombo.com") } let(:remote_with_mirror) do - double("remote", :uri => uri, :original_uri => orig_uri, :anonymized_uri => uri) + double("remote", uri: uri, original_uri: orig_uri, anonymized_uri: uri) end let(:fetcher) { Bundler::Fetcher.new(remote_with_mirror) } @@ -61,7 +61,7 @@ RSpec.describe Bundler::Fetcher do context "when there is no rubygems source mirror set" do let(:remote_no_mirror) do - double("remote", :uri => uri, :original_uri => nil, :anonymized_uri => uri) + double("remote", uri: uri, original_uri: nil, anonymized_uri: uri) end let(:fetcher) { Bundler::Fetcher.new(remote_no_mirror) } @@ -113,10 +113,10 @@ RSpec.describe Bundler::Fetcher do context "when gem ssl configuration is set" do before do - allow(Bundler.rubygems.configuration).to receive_messages( - :http_proxy => nil, - :ssl_client_cert => "cert", - :ssl_ca_cert => "ca" + allow(Gem.configuration).to receive_messages( + http_proxy: nil, + ssl_client_cert: "cert", + ssl_ca_cert: "ca" ) expect(File).to receive(:read).and_return("") expect(OpenSSL::X509::Certificate).to receive(:new).and_return("cert") @@ -143,19 +143,118 @@ RSpec.describe Bundler::Fetcher do 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") + with_env_vars("CI" => nil, "JENKINS_URL" => "foo") do + ci_part = fetcher.user_agent.split(" ").find {|x| x.start_with?("ci/") } + cis = ci_part.split("/").last.split(",") + expect(cis).to include("jenkins") + expect(cis).not_to include("ci") end end it "from many CI" do - with_env_vars("TRAVIS" => "foo", "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") + with_env_vars("CI" => "true", "SEMAPHORE" => nil, "TRAVIS" => "foo", "GITLAB_CI" => "gitlab", "CI_NAME" => "MY_ci") do + ci_part = fetcher.user_agent.split(" ").find {|x| x.start_with?("ci/") } + cis = ci_part.split("/").last.split(",") + expect(cis).to include("ci", "gitlab", "my_ci", "travis") + expect(cis).not_to include("semaphore") end end end end + + describe "#fetch_spec" do + let(:name) { "name" } + let(:version) { "1.3.17" } + let(:platform) { "platform" } + let(:downloader) { double("downloader") } + let(:body) { double(Gem::Net::HTTP::Get, body: downloaded_data) } + + context "when attempting to load a Gem::Specification" do + let(:spec) { Gem::Specification.new(name, version) } + let(:downloaded_data) { Zlib::Deflate.deflate(Marshal.dump(spec)) } + + it "returns the spec" do + expect(Bundler::Fetcher::Downloader).to receive(:new).and_return(downloader) + expect(downloader).to receive(:fetch).once.and_return(body) + result = fetcher.fetch_spec([name, version, platform]) + expect(result).to eq(spec) + end + end + + context "when attempting to load an unexpected class" do + let(:downloaded_data) { Zlib::Deflate.deflate(Marshal.dump(3)) } + + it "raises a HTTPError error" do + expect(Bundler::Fetcher::Downloader).to receive(:new).and_return(downloader) + expect(downloader).to receive(:fetch).once.and_return(body) + expect { fetcher.fetch_spec([name, version, platform]) }.to raise_error(Bundler::HTTPError, /Gemspec .* contained invalid data/i) + end + end + end + + describe "#specs_with_retry" do + let(:downloader) { double(:downloader) } + let(:remote) { double(:remote, cache_slug: "slug", uri: uri, original_uri: nil, anonymized_uri: uri) } + let(:compact_index) { double(Bundler::Fetcher::CompactIndex, available?: true, api_fetcher?: true) } + let(:dependency) { double(Bundler::Fetcher::Dependency, available?: true, api_fetcher?: true) } + let(:index) { double(Bundler::Fetcher::Index, available?: true, api_fetcher?: false) } + + before do + allow(Bundler::Fetcher::CompactIndex).to receive(:new).and_return(compact_index) + allow(Bundler::Fetcher::Dependency).to receive(:new).and_return(dependency) + allow(Bundler::Fetcher::Index).to receive(:new).and_return(index) + end + + it "picks the first fetcher that works" do + expect(compact_index).to receive(:specs).with("name").and_return([["name", "1.2.3", "ruby"]]) + expect(dependency).not_to receive(:specs) + expect(index).not_to receive(:specs) + fetcher.specs_with_retry("name", double(Bundler::Source::Rubygems)) + end + + context "when APIs are not available" do + before do + allow(compact_index).to receive(:available?).and_return(false) + allow(dependency).to receive(:available?).and_return(false) + end + + it "uses the index" do + expect(compact_index).not_to receive(:specs) + expect(dependency).not_to receive(:specs) + expect(index).to receive(:specs).with("name").and_return([["name", "1.2.3", "ruby"]]) + + fetcher.specs_with_retry("name", double(Bundler::Source::Rubygems)) + end + end + end + + describe "#api_fetcher?" do + let(:downloader) { double(:downloader) } + let(:remote) { double(:remote, cache_slug: "slug", uri: uri, original_uri: nil, anonymized_uri: uri) } + let(:compact_index) { double(Bundler::Fetcher::CompactIndex, available?: false, api_fetcher?: true) } + let(:dependency) { double(Bundler::Fetcher::Dependency, available?: false, api_fetcher?: true) } + let(:index) { double(Bundler::Fetcher::Index, available?: true, api_fetcher?: false) } + + before do + allow(Bundler::Fetcher::CompactIndex).to receive(:new).and_return(compact_index) + allow(Bundler::Fetcher::Dependency).to receive(:new).and_return(dependency) + allow(Bundler::Fetcher::Index).to receive(:new).and_return(index) + end + + context "when an api fetcher is available" do + before do + allow(compact_index).to receive(:available?).and_return(true) + end + + it "is truthy" do + expect(fetcher).to be_api_fetcher + end + end + + context "when only the index fetcher is available" do + it "is falsey" do + expect(fetcher).not_to be_api_fetcher + end + end + end end diff --git a/spec/bundler/bundler/friendly_errors_spec.rb b/spec/bundler/bundler/friendly_errors_spec.rb index 47e7a5d3cd..426e3c856d 100644 --- a/spec/bundler/bundler/friendly_errors_spec.rb +++ b/spec/bundler/bundler/friendly_errors_spec.rb @@ -2,30 +2,30 @@ require "bundler" require "bundler/friendly_errors" -require "cgi" +require "cgi/escape" +require "cgi/util" unless defined?(CGI::EscapeExt) 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| + File.open(home(".gemrc"), "w") do |f| f.write "invalid: yaml: hah" end end after do - FileUtils.rm(Gem.configuration.config_file_name) + FileUtils.rm(home(".gemrc")) end it "reports a relevant friendly error message" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle :install, :env => { "DEBUG" => true } + bundle :install, env: { "DEBUG" => "true" } expect(err).to include("Failed to load #{home(".gemrc")}") - expect(exitstatus).to eq(0) if exitstatus end end @@ -102,35 +102,15 @@ RSpec.describe Bundler, "friendly errors" do context "BundlerError" do it "Bundler.ui receive error" do error = Bundler::BundlerError.new - expect(Bundler.ui).to receive(:error).with(error.message, :wrap => true) + expect(Bundler.ui).to receive(:error).with(error.message, wrap: true) Bundler::FriendlyErrors.log_error(error) end - it_behaves_like "Bundler.ui receive trace", Bundler::BundlerError.new end context "Thor::Error" do it_behaves_like "Bundler.ui receive error", Bundler::Thor::Error.new end - context "LoadError" do - let(:error) { LoadError.new("cannot load such file -- openssl") } - - 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...") @@ -142,7 +122,7 @@ RSpec.describe Bundler, "friendly errors" do context "Gem::InvalidSpecificationException" do it "Bundler.ui receive error" do error = Gem::InvalidSpecificationException.new - expect(Bundler.ui).to receive(:error).with(error.message, :wrap => true) + expect(Bundler.ui).to receive(:error).with(error.message, wrap: true) Bundler::FriendlyErrors.log_error(error) end end @@ -151,17 +131,13 @@ RSpec.describe Bundler, "friendly errors" do # Does nothing end - context "Java::JavaLang::OutOfMemoryError" do - module Java - module JavaLang - class OutOfMemoryError < StandardError; end - end - end - + context "Java::JavaLang::OutOfMemoryError", :jruby_only do 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) + install_gemfile <<-G, raise_on_error: false, env: { "JRUBY_OPTS" => "-J-Xmx32M" }, artifice: nil + source "https://gem.repo1" + G + + expect(err).to include("JVM has run out of memory") end end @@ -200,9 +176,9 @@ RSpec.describe Bundler, "friendly errors" do 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) + expect(Bundler.ui).not_to receive(:info) + expect(Bundler.ui).to receive(:error).exactly(3).times + expect(Bundler.ui).not_to receive(:warn) Bundler::FriendlyErrors.request_issue_report_for(StandardError.new) end @@ -221,7 +197,7 @@ RSpec.describe Bundler, "friendly errors" 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") + expect(Bundler::FriendlyErrors.issues_url(exception)).to eq("https://github.com/ruby/rubygems/search?q=Exception+message&type=Issues") end it "generates a search URL for only the first line of a multi-line exception message" do @@ -230,7 +206,7 @@ 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") + expect(Bundler::FriendlyErrors.issues_url(exception)).to eq("https://github.com/ruby/rubygems/search?q=First+line+of+the+exception+message&type=Issues") end it "generates the url without colons" do @@ -239,7 +215,7 @@ 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") + expect(issues_url).to eq("https://github.com/ruby/rubygems/search?q=#{CGI.escape("Exception with colons ")}&type=Issues") end it "removes information after - for Errono::EACCES" do @@ -249,7 +225,7 @@ 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") + expect(issues_url).to eq("https://github.com/ruby/rubygems/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 index c01d65d5dd..b4ae2abdc5 100644 --- a/spec/bundler/bundler/gem_helper_spec.rb +++ b/spec/bundler/bundler/gem_helper_spec.rb @@ -9,7 +9,13 @@ RSpec.describe Bundler::GemHelper do 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_config_global "gem.mit false" + bundle_config_global "gem.test false" + bundle_config_global "gem.coc false" + bundle_config_global "gem.linter false" + bundle_config_global "gem.ci false" + bundle_config_global "gem.changelog false" + git("config --global init.defaultBranch main") bundle "gem #{app_name}" prepare_gemspec(app_gemspec_path) end @@ -60,10 +66,20 @@ RSpec.describe Bundler::GemHelper do mock_confirm_message message end + def mock_checksum_message(name, version) + message = "#{name} #{version} checksum written to checksums/#{name}-#{version}.gem.sha512." + mock_confirm_message message + end + + def sha512_hexdigest(path) + Digest::SHA512.file(path).hexdigest + end + subject! { Bundler::GemHelper.new(app_path) } let(:app_version) { "0.1.0" } let(:app_gem_dir) { app_path.join("pkg") } let(:app_gem_path) { app_gem_dir.join("#{app_name}-#{app_version}.gem") } + let(:app_sha_path) { app_path.join("checksums", "#{app_name}-#{app_version}.gem.sha512") } let(:app_gemspec_content) { File.read(app_gemspec_path) } before(:each) do @@ -97,6 +113,7 @@ RSpec.describe Bundler::GemHelper do context "before installation" do it "raises an error with appropriate message" do task_names.each do |name| + skip "Rake::FileTask '#{name}' exists" if File.exist?(name) expect { Rake.application[name] }. to raise_error(/^Don't know how to build task '#{name}'/) end @@ -138,6 +155,68 @@ RSpec.describe Bundler::GemHelper do expect(app_gem_path).to exist end end + + context "when building in the current working directory" do + it "creates .gem file" do + mock_build_message app_name, app_version + Dir.chdir app_path do + Bundler::GemHelper.new.build_gem + end + expect(app_gem_path).to exist + end + end + + context "when building in a location relative to the current working directory" do + it "creates .gem file" do + mock_build_message app_name, app_version + Dir.chdir File.dirname(app_path) do + Bundler::GemHelper.new(File.basename(app_path)).build_gem + end + expect(app_gem_path).to exist + end + end + end + + describe "#build_checksum" do + it "calculates SHA512 of the content" do + FileUtils.mkdir_p(app_gem_dir) + File.write(app_gem_path, "") + mock_checksum_message app_name, app_version + subject.build_checksum(app_gem_path) + expect(File.read(app_sha_path).chomp).to eql(Digest::SHA512.hexdigest("")) + end + + context "when build was successful" do + it "creates .sha512 file" do + mock_build_message app_name, app_version + mock_checksum_message app_name, app_version + subject.build_checksum + expect(app_sha_path).to exist + expect(File.read(app_sha_path).chomp).to eql(sha512_hexdigest(app_gem_path)) + end + end + context "when building in the current working directory" do + it "creates a .sha512 file" do + mock_build_message app_name, app_version + mock_checksum_message app_name, app_version + Dir.chdir app_path do + Bundler::GemHelper.new.build_checksum + end + expect(app_sha_path).to exist + expect(File.read(app_sha_path).chomp).to eql(sha512_hexdigest(app_gem_path)) + end + end + context "when building in a location relative to the current working directory" do + it "creates a .sha512 file" do + mock_build_message app_name, app_version + mock_checksum_message app_name, app_version + Dir.chdir File.dirname(app_path) do + Bundler::GemHelper.new(File.basename(app_path)).build_checksum + end + expect(app_sha_path).to exist + expect(File.read(app_sha_path).chomp).to eql(sha512_hexdigest(app_gem_path)) + end + end end describe "#install_gem" do @@ -147,7 +226,7 @@ RSpec.describe Bundler::GemHelper do mock_confirm_message "#{app_name} (#{app_version}) installed." subject.install_gem(nil, :local) expect(app_gem_path).to exist - gem_command! :list + installed_gems_list expect(out).to include("#{app_name} (#{app_version})") end end @@ -160,7 +239,7 @@ RSpec.describe Bundler::GemHelper do FileUtils.touch app_gem_path app_gem_path end - expect { subject.install_gem }.to raise_error(/Couldn't install gem/) + expect { subject.install_gem }.to raise_error(/Running `#{gem_bin} install #{app_gem_path}` failed/) end end end @@ -178,13 +257,11 @@ RSpec.describe Bundler::GemHelper do end before do - Dir.chdir(app_path) do - `git init` - `git config user.email "you@example.com"` - `git config user.name "name"` - `git config commit.gpgsign false` - `git config push.default simple` - end + git("init", app_path) + git("config user.email \"you@example.com\"", app_path) + git("config user.name \"name\"", app_path) + git("config commit.gpgsign false", app_path) + git("config push.default simple", app_path) # silence messages allow(Bundler.ui).to receive(:confirm) @@ -198,34 +275,32 @@ RSpec.describe Bundler::GemHelper do end it "when there are uncommitted files" do - Dir.chdir(app_path) { `git add .` } + git("add .", app_path) expect { Rake.application["release"].invoke }. to raise_error("There are files that need to be committed first.") end it "when there is no git remote" do - Dir.chdir(app_path) { `git commit -a -m "initial commit"` } + git("commit -a -m \"initial commit\"", app_path) expect { Rake.application["release"].invoke }.to raise_error(RuntimeError) end end context "succeeds" do - let(:repo) { build_git("foo", :bare => true) } + let(:repo) { build_git("foo", bare: true) } before do - Dir.chdir(app_path) do - sys_exec("git remote add origin #{file_uri_for(repo.path)}") - sys_exec('git commit -a -m "initial commit"') - end + git("remote add origin #{repo.path}", app_path) + git('commit -a -m "initial commit"', app_path) end context "on releasing" do before do mock_build_message app_name, app_version mock_confirm_message "Tagged v#{app_version}." - mock_confirm_message "Pushed git commits and tags." + mock_confirm_message "Pushed git commits and release tag." - Dir.chdir(app_path) { sys_exec("git push -u origin master") } + git("push -u origin main", app_path) end it "calls rubygem_push with proper arguments" do @@ -235,7 +310,43 @@ RSpec.describe Bundler::GemHelper do end it "uses Kernel.system" do - expect(Kernel).to receive(:system).with("gem", "push", app_gem_path.to_s, "--host", "http://example.org").and_return(true) + cmd = gem_bin.shellsplit + expect(Kernel).to receive(:system).with(*cmd, "push", app_gem_path.to_s, "--host", "http://example.org").and_return(true) + + Rake.application["release"].invoke + end + + it "also works when releasing from an ambiguous reference" do + # Create a branch with the same name as the tag + git("checkout -b v#{app_version}", app_path) + git("push -u origin v#{app_version}", app_path) + + expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s) + + Rake.application["release"].invoke + end + + it "also works with releasing from a branch not yet pushed" do + git("checkout -b module_function", app_path) + + expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s) + + Rake.application["release"].invoke + end + end + + context "on releasing with a custom tag prefix" do + before do + Bundler::GemHelper.tag_prefix = "foo-" + mock_build_message app_name, app_version + mock_confirm_message "Pushed git commits and release tag." + + git("push -u origin main", app_path) + expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s) + end + + it "prepends the custom prefix to the tag" do + mock_confirm_message "Tagged foo-v#{app_version}." Rake.application["release"].invoke end @@ -246,9 +357,7 @@ RSpec.describe Bundler::GemHelper do mock_confirm_message "Tag v#{app_version} has already been created." expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s) - Dir.chdir(app_path) do - `git tag -a -m \"Version #{app_version}\" v#{app_version}` - end + git("tag -a -m \"Version #{app_version}\" v#{app_version}", app_path) Rake.application["release"].invoke end @@ -269,12 +378,10 @@ RSpec.describe Bundler::GemHelper do 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 + git("init", app_path) + git("config user.email \"you@example.com\"", app_path) + git("config user.name \"name\"", app_path) + git("config push.gpgsign simple", app_path) # silence messages allow(Bundler.ui).to receive(:confirm) @@ -283,6 +390,7 @@ RSpec.describe Bundler::GemHelper do credentials = double("credentials", "file?" => true) allow(Bundler.user_home).to receive(:join). with(".gem/credentials").and_return(credentials) + allow(Bundler.user_home).to receive(:join).and_call_original end describe "success messaging" do diff --git a/spec/bundler/bundler/gem_version_promoter_spec.rb b/spec/bundler/bundler/gem_version_promoter_spec.rb index 01e0232fba..0e1b7c9cc8 100644 --- a/spec/bundler/bundler/gem_version_promoter_spec.rb +++ b/spec/bundler/bundler/gem_version_promoter_spec.rb @@ -1,178 +1,162 @@ # frozen_string_literal: true RSpec.describe Bundler::GemVersionPromoter do - context "conservative resolver" do - def versions(result) - result.flatten.map(&:version).map(&:to_s) + let(:gvp) { described_class.new } + + # Rightmost (highest array index) in result is most preferred. + # Leftmost (lowest array index) in result is least preferred. + # `build_candidates` has all versions of gem in index. + # `build_spec` is the version currently in the .lock file. + # + # In default (not strict) mode, all versions in the index will + # be returned, allowing Bundler the best chance to resolve all + # dependencies, but sometimes resulting in upgrades that some + # would not consider conservative. + + describe "#sort_versions" do + def build_candidates(versions) + versions.map do |v| + Bundler::Resolver::Candidate.new(v) + end end - def make_instance(*args) - @gvp = Bundler::GemVersionPromoter.new(*args).tap do |gvp| - gvp.class.class_eval { public :filter_dep_specs, :sort_dep_specs } - end + def build_package(name, version, unlock) + Bundler::Resolver::Package.new(name, [], locked_specs: Bundler::SpecSet.new(build_spec(name, version)), unlock: unlock) end - def unlocking(options) - make_instance(Bundler::SpecSet.new([]), ["foo"]).tap do |p| - p.level = options[:level] if options[:level] - p.strict = options[:strict] if options[:strict] - end + def sorted_versions(candidates:, current:, unlock: true) + gvp.sort_versions( + build_package("foo", current, unlock), + build_candidates(candidates) + ).flatten.map(&:version).map(&:to_s) end - def keep_locked(options) - make_instance(Bundler::SpecSet.new([]), ["bar"]).tap do |p| - p.level = options[:level] if options[:level] - p.strict = options[:strict] if options[:strict] - end + it "numerically sorts versions" do + versions = sorted_versions(candidates: %w[1.7.7 1.7.8 1.7.9 1.7.15 1.8.0], current: "1.7.8") + expect(versions).to eq %w[1.8.0 1.7.15 1.7.9 1.7.8 1.7.7] end - def build_spec_groups(name, versions) - versions.map do |v| - Bundler::Resolver::SpecGroup.new(build_spec(name, v)) + context "with no options" do + it "defaults to level=:major, strict=false, pre=false" do + versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") + expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0] end end - # Rightmost (highest array index) in result is most preferred. - # Leftmost (lowest array index) in result is least preferred. - # `build_spec_groups` has all versions of gem in index. - # `build_spec` is the version currently in the .lock file. - # - # In default (not strict) mode, all versions in the index will - # be returned, allowing Bundler the best chance to resolve all - # dependencies, but sometimes resulting in upgrades that some - # would not consider conservative. - context "filter specs (strict) level patch" do - it "when keeping build_spec, keep current, next release" do - keep_locked(:level => :patch) - res = @gvp.filter_dep_specs( - build_spec_groups("foo", %w[1.7.8 1.7.9 1.8.0]), - build_spec("foo", "1.7.8").first - ) - expect(versions(res)).to eq %w[1.7.9 1.7.8] - end + context "when strict" do + before { gvp.strict = true } - it "when unlocking prefer next release first" do - unlocking(:level => :patch) - res = @gvp.filter_dep_specs( - build_spec_groups("foo", %w[1.7.8 1.7.9 1.8.0]), - build_spec("foo", "1.7.8").first - ) - expect(versions(res)).to eq %w[1.7.8 1.7.9] - end + context "when level is major" do + before { gvp.level = :major } - it "when unlocking keep current when already at latest release" do - unlocking(:level => :patch) - res = @gvp.filter_dep_specs( - build_spec_groups("foo", %w[1.7.9 1.8.0 2.0.0]), - build_spec("foo", "1.7.9").first - ) - expect(versions(res)).to eq %w[1.7.9] + it "keeps downgrades" do + versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") + expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0] + end end - end - context "filter specs (strict) level minor" do - it "when unlocking favor next releases, remove minor and major increases" do - unlocking(:level => :minor) - res = @gvp.filter_dep_specs( - build_spec_groups("foo", %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.0 2.0.1]), - build_spec("foo", "0.2.0").first - ) - expect(versions(res)).to eq %w[0.2.0 0.3.0 0.3.1 0.9.0] + context "when level is minor" do + before { gvp.level = :minor } + + it "sorts highest minor within same major in first position" do + versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") + expect(versions).to eq %w[0.9.0 0.3.1 0.3.0 1.0.0 2.1.0 2.0.1 0.2.0] + end end - it "when keep locked, keep current, then favor next release, remove minor and major increases" do - keep_locked(:level => :minor) - res = @gvp.filter_dep_specs( - build_spec_groups("foo", %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.0 2.0.1]), - build_spec("foo", "0.2.0").first - ) - expect(versions(res)).to eq %w[0.3.0 0.3.1 0.9.0 0.2.0] + context "when level is patch" do + before { gvp.level = :patch } + + it "sorts highest patch within same minor in first position" do + versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") + expect(versions).to eq %w[0.3.1 0.3.0 0.9.0 1.0.0 2.0.1 2.1.0 0.2.0] + end end end - context "sort specs (not strict) level patch" do - it "when not unlocking, same order but make sure build_spec version is most preferred to stay put" do - keep_locked(:level => :patch) - res = @gvp.sort_dep_specs( - build_spec_groups("foo", %w[1.5.4 1.6.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 2.0.0 2.0.1]), - build_spec("foo", "1.7.7").first - ) - expect(versions(res)).to eq %w[1.5.4 1.6.5 1.7.6 2.0.0 2.0.1 1.8.0 1.8.1 1.7.8 1.7.9 1.7.7] - end + context "when not strict" do + before { gvp.strict = false } - it "when unlocking favor next release, then current over minor increase" do - unlocking(:level => :patch) - res = @gvp.sort_dep_specs( - build_spec_groups("foo", %w[1.7.7 1.7.8 1.7.9 1.8.0]), - build_spec("foo", "1.7.8").first - ) - expect(versions(res)).to eq %w[1.7.7 1.8.0 1.7.8 1.7.9] + context "when level is major" do + before { gvp.level = :major } + + it "orders by version" do + versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") + expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0] + end end - it "when unlocking do proper integer comparison, not string" do - unlocking(:level => :patch) - res = @gvp.sort_dep_specs( - build_spec_groups("foo", %w[1.7.7 1.7.8 1.7.9 1.7.15 1.8.0]), - build_spec("foo", "1.7.8").first - ) - expect(versions(res)).to eq %w[1.7.7 1.8.0 1.7.8 1.7.9 1.7.15] + context "when level is minor" do + before { gvp.level = :minor } + + it "favors minor upgrades, then patch upgrades, then major upgrades, then downgrades" do + versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") + expect(versions).to eq %w[0.9.0 0.3.1 0.3.0 1.0.0 2.1.0 2.0.1 0.2.0] + end end - it "leave current when unlocking but already at latest release" do - unlocking(:level => :patch) - res = @gvp.sort_dep_specs( - build_spec_groups("foo", %w[1.7.9 1.8.0 2.0.0]), - build_spec("foo", "1.7.9").first - ) - expect(versions(res)).to eq %w[2.0.0 1.8.0 1.7.9] + context "when level is patch" do + before { gvp.level = :patch } + + it "favors patch upgrades, then minor upgrades, then major upgrades, then downgrades" do + versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") + expect(versions).to eq %w[0.3.1 0.3.0 0.9.0 1.0.0 2.0.1 2.1.0 0.2.0] + end end end - context "sort specs (not strict) level minor" do - it "when unlocking favor next release, then minor increase over current" do - unlocking(:level => :minor) - res = @gvp.sort_dep_specs( - build_spec_groups("foo", %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.0 2.0.1]), - build_spec("foo", "0.2.0").first - ) - expect(versions(res)).to eq %w[2.0.0 2.0.1 1.0.0 0.2.0 0.3.0 0.3.1 0.9.0] + context "when pre" do + before { gvp.pre = true } + + it "sorts regardless of prerelease status" do + versions = sorted_versions(candidates: %w[1.7.7.pre 1.8.0 1.8.1.pre 1.8.1 2.0.0.pre 2.0.0], current: "1.8.0") + expect(versions).to eq %w[2.0.0 2.0.0.pre 1.8.1 1.8.1.pre 1.8.0 1.7.7.pre] end end - context "level error handling" do - subject { Bundler::GemVersionPromoter.new } + context "when not pre" do + before { gvp.pre = false } - it "should raise if not major, minor or patch is passed" do - expect { subject.level = :minjor }.to raise_error ArgumentError + it "deprioritizes prerelease gems" do + versions = sorted_versions(candidates: %w[1.7.7.pre 1.8.0 1.8.1.pre 1.8.1 2.0.0.pre 2.0.0], current: "1.8.0") + expect(versions).to eq %w[2.0.0 1.8.1 1.8.0 2.0.0.pre 1.8.1.pre 1.7.7.pre] end + end - it "should raise if invalid classes passed" do - [123, nil].each do |value| - expect { subject.level = value }.to raise_error ArgumentError - end + context "when locking and not major" do + before { gvp.level = :minor } + + it "keeps the current version first" do + versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.1.0 2.0.1], current: "0.3.0", unlock: []) + expect(versions.first).to eq("0.3.0") end + end + end - it "should accept major, minor patch symbols" do - [:major, :minor, :patch].each do |value| - subject.level = value - expect(subject.level).to eq value - end + describe "#level=" do + subject { described_class.new } + + it "should raise if not major, minor or patch is passed" do + expect { subject.level = :minjor }.to raise_error ArgumentError + end + + it "should raise if invalid classes passed" do + [123, nil].each do |value| + expect { subject.level = value }.to raise_error ArgumentError end + end - it "should accept major, minor patch strings" do - %w[major minor patch].each do |value| - subject.level = value - expect(subject.level).to eq value.to_sym - end + it "should accept major, minor patch symbols" do + [:major, :minor, :patch].each do |value| + subject.level = value + expect(subject.level).to eq value end end - context "debug output" do - it "should not kerblooie on its own debug output" do - gvp = unlocking(:level => :patch) - dep = Bundler::DepProxy.new(dep("foo", "1.2.0").first, "ruby") - result = gvp.send(:debug_format_result, dep, build_spec_groups("foo", %w[1.2.0 1.3.0])) - expect(result.class).to eq Array + it "should accept major, minor patch strings" do + %w[major minor patch].each do |value| + subject.level = value + expect(subject.level).to eq value.to_sym end end end diff --git a/spec/bundler/bundler/installer/gem_installer_spec.rb b/spec/bundler/bundler/installer/gem_installer_spec.rb index 8f8d1c6d15..dbd4e1d2c8 100644 --- a/spec/bundler/bundler/installer/gem_installer_spec.rb +++ b/spec/bundler/bundler/installer/gem_installer_spec.rb @@ -3,37 +3,44 @@ require "bundler/installer/gem_installer" RSpec.describe Bundler::GemInstaller do - let(:installer) { instance_double("Installer") } + let(:definition) { instance_double("Definition", locked_gems: nil) } + let(:installer) { instance_double("Installer", definition: definition) } let(:spec_source) { instance_double("SpecSource") } - let(:spec) { instance_double("Specification", :name => "dummy", :version => "0.0.1", :loaded_from => "dummy", :source => spec_source) } + let(:spec) { instance_double("Specification", name: "dummy", version: "0.0.1", loaded_from: "dummy", source: spec_source) } + let(:base_options) { { force: false, local: false, previous_spec: nil } } subject { described_class.new(spec, installer) } context "spec_settings is nil" do it "invokes install method with empty build_args" do - allow(spec_source).to receive(:install).with(spec, :force => false, :ensure_builtin_gems_cached => false, :build_args => []) + allow(spec_source).to receive(:install).with( + spec, + base_options.merge(build_args: []) + ) subject.install_from_spec end end context "spec_settings is build option" do it "invokes install method with build_args" do - allow(Bundler.settings).to receive(:[]).with(:bin) - allow(Bundler.settings).to receive(:[]).with(:inline) - allow(Bundler.settings).to receive(:[]).with(:forget_cli_options) + allow(Bundler.settings).to receive(:[]) allow(Bundler.settings).to receive(:[]).with("build.dummy").and_return("--with-dummy-config=dummy") - expect(spec_source).to receive(:install).with(spec, :force => false, :ensure_builtin_gems_cached => false, :build_args => ["--with-dummy-config=dummy"]) + expect(spec_source).to receive(:install).with( + spec, + base_options.merge(build_args: ["--with-dummy-config=dummy"]) + ) subject.install_from_spec end end context "spec_settings is build option with spaces" do it "invokes install method with build_args" do - allow(Bundler.settings).to receive(:[]).with(:bin) - allow(Bundler.settings).to receive(:[]).with(:inline) - allow(Bundler.settings).to receive(:[]).with(:forget_cli_options) + allow(Bundler.settings).to receive(:[]) allow(Bundler.settings).to receive(:[]).with("build.dummy").and_return("--with-dummy-config=dummy --with-another-dummy-config") - expect(spec_source).to receive(:install).with(spec, :force => false, :ensure_builtin_gems_cached => false, :build_args => ["--with-dummy-config=dummy", "--with-another-dummy-config"]) + expect(spec_source).to receive(:install).with( + spec, + base_options.merge(build_args: ["--with-dummy-config=dummy", "--with-another-dummy-config"]) + ) subject.install_from_spec end end diff --git a/spec/bundler/bundler/installer/parallel_installer_spec.rb b/spec/bundler/bundler/installer/parallel_installer_spec.rb index ace5c1a23a..49bcb5310b 100644 --- a/spec/bundler/bundler/installer/parallel_installer_spec.rb +++ b/spec/bundler/bundler/installer/parallel_installer_spec.rb @@ -1,47 +1,79 @@ # frozen_string_literal: true require "bundler/installer/parallel_installer" +require "bundler/rubygems_gem_installer" +require "rubygems/remote_fetcher" +require "bundler" 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 + describe "priority queue" do + before do + require "support/artifice/compact_index" + + @previous_client = Gem::Request::ConnectionPools.client + Gem::Request::ConnectionPools.client = Gem::Net::HTTP + Gem::RemoteFetcher.fetcher.close_all + + build_repo2 do + build_gem "gem_with_extension", &:add_c_extension + build_gem "gem_without_extension" + end + + gemfile <<~G + source "https://gem.repo2" + + gem "gem_with_extension" + gem "gem_without_extension" + G + lockfile <<~L + GEM + remote: https://gem.repo2/ + specs: + gem_with_extension (1.0) + gem_without_extension (1.0) + + DEPENDENCIES + gem_with_extension + gem_without_extension + L + + @old_ui = Bundler.ui + Bundler.ui = Bundler::UI::Silent.new 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 + after do + Bundler.ui = @old_ui + Gem::Request::ConnectionPools.client = @previous_client + Artifice.deactivate 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) + let(:definition) do + allow(Bundler).to receive(:root) { bundled_app } + + definition = Bundler::Definition.build(bundled_app.join("Gemfile"), bundled_app.join("Gemfile.lock"), false) + definition.tap(&:setup_domain!) + end + let(:installer) { Bundler::Installer.new(bundled_app, definition) } + + it "queues native extensions in priority" do + parallel_installer = Bundler::ParallelInstaller.new(installer, definition.specs, 2, false, true) + worker_pool = parallel_installer.send(:worker_pool) + expected = 6 # Enqueue to download bundler and the 2 gems. Enqueue to install Bundler and the 2 gems. + + expect(worker_pool).to receive(:enq).exactly(expected).times.and_wrap_original do |original_enq, spec, opts| + unless opts.nil? # Enqueued for download, no priority + if spec.name == "gem_with_extension" + expect(opts).to eq({ priority: true }) + else + expect(opts).to eq({ priority: false }) + end + end + + opts ||= {} + original_enq.call(spec, **opts) end + + parallel_installer.call end end end diff --git a/spec/bundler/bundler/installer/spec_installation_spec.rb b/spec/bundler/bundler/installer/spec_installation_spec.rb index a9cf09a372..57868766d9 100644 --- a/spec/bundler/bundler/installer/spec_installation_spec.rb +++ b/spec/bundler/bundler/installer/spec_installation_spec.rb @@ -3,14 +3,17 @@ 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 + def build_spec(name, extensions: []) + spec = Object.new + spec.define_singleton_method(:name) { name } + spec.define_singleton_method(:full_name) { "#{name}-1.0" } + spec.define_singleton_method(:extensions) { extensions } + spec.define_singleton_method(:dependencies) { [] } + spec end + let!(:dep) { build_spec("I like tests") } + describe "#ready_to_enqueue?" do context "when in enqueued state" do it "is falsey" do @@ -35,27 +38,51 @@ RSpec.describe Bundler::ParallelInstaller::SpecInstallation do 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)] + it "returns true when all dependencies are installed" do + alpha = described_class.new(build_spec("alpha")) + alpha.dependencies = [] + + beta = described_class.new(build_spec("beta")) + beta.dependencies = [alpha] + + gamma = described_class.new(build_spec("gamma")) + gamma.dependencies = [beta] + + expect(gamma.dependencies_installed?({})).to be_falsey + expect(gamma.dependencies_installed?({ "beta" => true })).to be_falsey + expect(gamma.dependencies_installed?({ "alpha" => true, "beta" => true })).to be_truthy + end + end + + describe "#ready_to_install?" do + context "when spec has no extensions" do + it "returns true regardless of dependencies" do + beta = described_class.new(build_spec("beta")) + beta.dependencies = [] + spec = described_class.new(dep) - allow(spec).to receive(:all_dependencies).and_return(dependencies) - expect(spec.dependencies_installed?(all_specs)).to be_truthy + spec.state = :downloaded + spec.dependencies = [beta] + + expect(spec.ready_to_install?({})).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 + context "when spec has extensions" do + it "returns true when all dependencies are installed" do + alpha = described_class.new(build_spec("alpha")) + alpha.dependencies = [] + + beta = described_class.new(build_spec("beta")) + beta.dependencies = [alpha] + + gamma = described_class.new(build_spec("gamma", extensions: ["ext/Rakefile"])) + gamma.state = :downloaded + gamma.dependencies = [beta] + + expect(gamma.ready_to_install?({})).to be_falsey + expect(gamma.ready_to_install?({ "beta" => true })).to be_falsey + expect(gamma.ready_to_install?({ "alpha" => true, "beta" => true })).to be_truthy end end end diff --git a/spec/bundler/bundler/lockfile_parser_spec.rb b/spec/bundler/bundler/lockfile_parser_spec.rb index 3a6d61336f..7364ab98e5 100644 --- a/spec/bundler/bundler/lockfile_parser_spec.rb +++ b/spec/bundler/bundler/lockfile_parser_spec.rb @@ -3,7 +3,7 @@ require "bundler/lockfile_parser" RSpec.describe Bundler::LockfileParser do - let(:lockfile_contents) { strip_whitespace(<<-L) } + let(:lockfile_contents) { <<~L } GIT remote: https://github.com/alloy/peiji-san.git revision: eca485d8dc95f12aaec1a434b49d295c7e91844b @@ -22,6 +22,9 @@ RSpec.describe Bundler::LockfileParser do peiji-san! rake + CHECKSUMS + rake (10.3.2) sha256=814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8 + RUBY VERSION ruby 2.1.3p242 @@ -33,13 +36,13 @@ RSpec.describe Bundler::LockfileParser do it "returns the attributes" do attributes = described_class.sections_in_lockfile(lockfile_contents) expect(attributes).to contain_exactly( - "BUNDLED WITH", "DEPENDENCIES", "GEM", "GIT", "PLATFORMS", "RUBY VERSION" + "BUNDLED WITH", "CHECKSUMS", "DEPENDENCIES", "GEM", "GIT", "PLATFORMS", "RUBY VERSION" ) end end describe ".unknown_sections_in_lockfile" do - let(:lockfile_contents) { strip_whitespace(<<-L) } + let(:lockfile_contents) { <<~L } UNKNOWN ATTR UNKNOWN ATTR 2 @@ -60,7 +63,7 @@ RSpec.describe Bundler::LockfileParser do it "returns the same as > 1.0" do expect(subject).to contain_exactly( - described_class::BUNDLED, described_class::RUBY, described_class::PLUGIN + described_class::BUNDLED, described_class::CHECKSUMS, described_class::RUBY, described_class::PLUGIN ) end end @@ -70,7 +73,7 @@ RSpec.describe Bundler::LockfileParser do it "returns the same as for the release version" do expect(subject).to contain_exactly( - described_class::RUBY, described_class::PLUGIN + described_class::CHECKSUMS, described_class::RUBY, described_class::PLUGIN ) end end @@ -108,16 +111,25 @@ RSpec.describe Bundler::LockfileParser do end let(:specs) do [ - Bundler::LazySpecification.new("peiji-san", v("1.2.0"), rb), - Bundler::LazySpecification.new("rake", v("10.3.2"), rb), + Bundler::LazySpecification.new("peiji-san", v("1.2.0"), Gem::Platform::RUBY), + Bundler::LazySpecification.new("rake", v("10.3.2"), Gem::Platform::RUBY), ] end - let(:platforms) { [rb] } + let(:platforms) { [Gem::Platform::RUBY] } let(:bundler_version) { Gem::Version.new("1.12.0.rc.2") } let(:ruby_version) { "ruby 2.1.3p242" } + let(:lockfile_path) { Bundler.default_lockfile.relative_path_from(Dir.pwd) } + let(:rake_sha256_checksum) do + Bundler::Checksum.from_lock( + "sha256=814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8", + "#{lockfile_path}:20:17" + ) + end + let(:rake_checksums) { [rake_sha256_checksum] } shared_examples_for "parsing" do it "parses correctly" do + expect(subject.valid?).to be(true) expect(subject.sources).to eq sources expect(subject.dependencies).to eq dependencies expect(subject.specs).to eq specs @@ -125,6 +137,9 @@ RSpec.describe Bundler::LockfileParser do expect(subject.platforms).to eq platforms expect(subject.bundler_version).to eq bundler_version expect(subject.ruby_version).to eq ruby_version + rake_spec = specs.last + checksums = subject.sources.last.checksum_store.to_lock(specs.last) + expect(checksums).to eq("#{rake_spec.lock_name} #{rake_checksums.map(&:to_lock).sort.join(",")}") end end @@ -149,5 +164,140 @@ RSpec.describe Bundler::LockfileParser do let(:lockfile_contents) { super().sub("peiji-san!", "peiji-san!\n foo: bar") } include_examples "parsing" end + + context "when the checksum is urlsafe base64 encoded" do + let(:lockfile_contents) do + super().sub( + "sha256=814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8", + "sha256=gUgow08TFdfnt-gpUYRXfMTpabrWFWrAadAtY_WNgug=" + ) + end + include_examples "parsing" + end + + context "when the checksum is of an unknown algorithm" do + let(:rake_sha512_checksum) do + Bundler::Checksum.from_lock( + "sha512=pVDn9GLmcFkz8vj1ueiVxj5uGKkAyaqYjEX8zG6L5O4BeVg3wANaKbQdpj/B82Nd/MHVszy6polHcyotUdwilQ==", + "#{lockfile_path}:20:17" + ) + end + let(:lockfile_contents) do + super().sub( + "sha256=", + "sha512=pVDn9GLmcFkz8vj1ueiVxj5uGKkAyaqYjEX8zG6L5O4BeVg3wANaKbQdpj/B82Nd/MHVszy6polHcyotUdwilQ==,sha256=" + ) + end + let(:rake_checksums) { [rake_sha256_checksum, rake_sha512_checksum] } + include_examples "parsing" + end + + context "when the content does not contain any recognized lockfile sections" do + let(:lockfile_contents) { "hello world\nlorem ipsum\n" } + + it "does not raise, is not valid, and deprecates" do + expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with( + /does not appear to be a valid lockfile.*future version of Bundler/m + ) + parser = described_class.new(lockfile_contents) + expect(parser.valid?).to be(false) + expect(parser.specs).to eq([]) + expect(parser.dependencies).to eq({}) + end + + it "does not raise when strict: true, and still deprecates" do + expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with( + /does not appear to be a valid lockfile.*future version of Bundler/m + ) + parser = described_class.new(lockfile_contents, strict: true) + expect(parser.valid?).to be(false) + expect(parser.specs).to eq([]) + expect(parser.dependencies).to eq({}) + end + end + + context "when the content looks like a Gemfile DSL" do + let(:lockfile_contents) { <<~G } + source "https://rubygems.org" + gem "rake" + G + + it "does not raise, is not valid, and deprecates" do + expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with( + /does not appear to be a valid lockfile.*future version of Bundler/m + ) + parser = described_class.new(lockfile_contents) + expect(parser.valid?).to be(false) + expect(parser.specs).to eq([]) + expect(parser.dependencies).to eq({}) + end + + it "does not raise when strict: true, and still deprecates" do + expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with( + /does not appear to be a valid lockfile.*future version of Bundler/m + ) + parser = described_class.new(lockfile_contents, strict: true) + expect(parser.valid?).to be(false) + expect(parser.specs).to eq([]) + expect(parser.dependencies).to eq({}) + end + end + + context "when the content is empty" do + let(:lockfile_contents) { "" } + + it "does not raise and is valid" do + expect { subject }.not_to raise_error + expect(subject.valid?).to be(true) + end + end + + context "when lockfile_path is given" do + it "uses the provided path in error messages instead of looking up Bundler.default_lockfile" do + expect(Bundler::SharedHelpers).not_to receive(:relative_lockfile_path) + parser = described_class.new(lockfile_contents, lockfile_path: "custom/path.lock") + expect(parser.valid?).to be(true) + rake_spec = parser.specs.last + checksums = parser.sources.last.checksum_store.to_lock(rake_spec) + expected_checksum = Bundler::Checksum.from_lock( + "sha256=814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8", + "custom/path.lock:20:17" + ) + expect(checksums).to eq("#{rake_spec.lock_name} #{expected_checksum.to_lock}") + end + + it "raises with the provided path when the lockfile contains merge conflicts" do + expect do + described_class.new("<<<<<<<\n", lockfile_path: "custom/path.lock") + end.to raise_error(Bundler::LockfileError, %r{custom/path\.lock contains merge conflicts}) + end + end + + context "when CHECKSUMS has duplicate checksums in the lockfile that don't match" do + let(:bad_checksum) { "sha256=c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11" } + let(:lockfile_contents) { super().split(/(?<=CHECKSUMS\n)/m).insert(1, " rake (10.3.2) #{bad_checksum}\n").join } + + it "raises a security error" do + expect { subject }.to raise_error(Bundler::SecurityError) do |e| + expect(e.message).to match <<~MESSAGE + Bundler found mismatched checksums. This is a potential security risk. + rake (10.3.2) #{bad_checksum} + from the lockfile CHECKSUMS at #{lockfile_path}:20:17 + rake (10.3.2) #{rake_sha256_checksum.to_lock} + from the lockfile CHECKSUMS at #{lockfile_path}:21:17 + + To resolve this issue you can either: + 1. remove the matching checksum in #{lockfile_path}:21:17 + 2. run `bundle install` + or if you are sure that the new checksum from the lockfile CHECKSUMS at #{lockfile_path}:21:17 is correct: + 1. remove the matching checksum in #{lockfile_path}:20:17 + 2. run `bundle install` + + To ignore checksum security warnings, disable checksum validation with + `bundle config set --local disable_checksum_validation true` + MESSAGE + end + end + end end end diff --git a/spec/bundler/bundler/mirror_spec.rb b/spec/bundler/bundler/mirror_spec.rb index 76b697c4d2..ba1c6ed413 100644 --- a/spec/bundler/bundler/mirror_spec.rb +++ b/spec/bundler/bundler/mirror_spec.rb @@ -36,12 +36,12 @@ RSpec.describe Bundler::Settings::Mirror do it "takes a string for the uri but returns an uri object" do mirror.uri = "http://localhost:9292" - expect(mirror.uri).to eq(URI("http://localhost:9292")) + expect(mirror.uri).to eq(Gem::URI("http://localhost:9292")) end it "takes an uri object for the uri" do - mirror.uri = URI("http://localhost:9293") - expect(mirror.uri).to eq(URI("http://localhost:9293")) + mirror.uri = Gem::URI("http://localhost:9293") + expect(mirror.uri).to eq(Gem::URI("http://localhost:9293")) end context "without a uri" do @@ -145,7 +145,7 @@ RSpec.describe Bundler::Settings::Mirror do end RSpec.describe Bundler::Settings::Mirrors do - let(:localhost_uri) { URI("http://localhost:9292") } + let(:localhost_uri) { Gem::URI("http://localhost:9292") } context "with a just created mirror" do let(:mirrors) do @@ -260,7 +260,7 @@ RSpec.describe Bundler::Settings::Mirrors do before { mirrors.parse("mirror.all.fallback_timeout", "true") } it "returns the source uri, not localhost" do - expect(mirrors.for("http://whatever.com").uri).to eq(URI("http://whatever.com/")) + expect(mirrors.for("http://whatever.com").uri).to eq(Gem::URI("http://whatever.com/")) end end end @@ -270,7 +270,7 @@ RSpec.describe Bundler::Settings::Mirrors do context "without a fallback timeout" do it "returns the uri that is not mirrored" do - expect(mirrors.for("http://whatever.com").uri).to eq(URI("http://whatever.com/")) + expect(mirrors.for("http://whatever.com").uri).to eq(Gem::URI("http://whatever.com/")) end it "returns localhost for rubygems.org" do @@ -282,11 +282,11 @@ RSpec.describe Bundler::Settings::Mirrors do before { mirrors.parse("mirror.http://rubygems.org/.fallback_timeout", "true") } it "returns the uri that is not mirrored" do - expect(mirrors.for("http://whatever.com").uri).to eq(URI("http://whatever.com/")) + expect(mirrors.for("http://whatever.com").uri).to eq(Gem::URI("http://whatever.com/")) end it "returns rubygems.org for rubygems.org" do - expect(mirrors.for("http://rubygems.org/").uri).to eq(URI("http://rubygems.org/")) + expect(mirrors.for("http://rubygems.org/").uri).to eq(Gem::URI("http://rubygems.org/")) end end end @@ -305,6 +305,8 @@ RSpec.describe Bundler::Settings::TCPSocketProbe do end it "probes the server correctly" do + skip "obscure error" if Gem.win_platform? + with_server_and_mirror do |server, mirror| expect(server.closed?).to be_falsey expect(probe.replies?(mirror)).to be_truthy diff --git a/spec/bundler/bundler/override_spec.rb b/spec/bundler/bundler/override_spec.rb new file mode 100644 index 0000000000..ad8be75520 --- /dev/null +++ b/spec/bundler/bundler/override_spec.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true + +RSpec.describe "MatchMetadata override-aware checks" do + let(:spec_class) do + Class.new do + include Bundler::MatchMetadata + attr_accessor :name + def initialize(name, ruby_req, rubygems_req) + @name = name + @required_ruby_version = ruby_req + @required_rubygems_version = rubygems_req + end + end + end + + it "matches_current_metadata? ignores overrides (strict path)" do + spec = spec_class.new("rails", Gem::Requirement.new("< #{Gem.ruby_version}"), Gem::Requirement.default) + overrides = [Bundler::Override.new("rails", :required_ruby_version, :ignore_upper)] + # Strict method MUST NOT apply overrides; guards SelfManager and other generic callers. + expect(spec.matches_current_metadata?).to be(false) + expect(spec.matches_current_metadata_with_overrides?(overrides)).to be(true) + end + + it "matches_current_ruby_with_overrides? returns the strict result for an empty override list" do + spec = spec_class.new("rails", Gem::Requirement.new(">= #{Gem.ruby_version}"), Gem::Requirement.default) + expect(spec.matches_current_ruby_with_overrides?([])).to be(true) + expect(spec.matches_current_ruby_with_overrides?(nil)).to be(true) + end + + it "matches_current_rubygems_with_overrides? honors :all override" do + spec = spec_class.new("rails", Gem::Requirement.default, Gem::Requirement.new("< #{Gem.rubygems_version}")) + overrides = [Bundler::Override.new(:all, :required_rubygems_version, :ignore_upper)] + expect(spec.matches_current_rubygems_with_overrides?(overrides)).to be(true) + end +end + +RSpec.describe "LazySpecification override propagation" do + let(:overrides) { [Bundler::Override.new("rails", :required_ruby_version, :ignore_upper)] } + + it "carries overrides forward from a source LazySpec via from_spec" do + src = Bundler::LazySpecification.new("rails", "8.0", Gem::Platform::RUBY) + src.overrides = overrides + derived = Bundler::LazySpecification.from_spec(src) + expect(derived.overrides).to eq(overrides) + end + + it "does not call respond_to? on the source spec, avoiding gemspec lazy load" do + # If from_spec used respond_to?(:overrides), a RemoteSpec source would + # force-load the backing gemspec. Use a stand-in object whose + # respond_to? raises to prove it is never asked. + src = Object.new + src.define_singleton_method(:name) { "rails" } + src.define_singleton_method(:version) { Gem::Version.new("8.0") } + src.define_singleton_method(:platform) { Gem::Platform::RUBY } + src.define_singleton_method(:source) { nil } + src.define_singleton_method(:runtime_dependencies) { [] } + src.define_singleton_method(:required_ruby_version) { Gem::Requirement.default } + src.define_singleton_method(:required_rubygems_version) { Gem::Requirement.default } + src.define_singleton_method(:respond_to?) {|*| raise "from_spec must not call respond_to?" } + expect { Bundler::LazySpecification.from_spec(src) }.not_to raise_error + end +end + +RSpec.describe Bundler::Override do + describe ".find_for" do + it "returns the matching override by target and field" do + a = described_class.new("rails", :version, ">= 8.0") + b = described_class.new("nokogiri", :version, :ignore_upper) + expect(described_class.find_for([a, b], "rails", :version)).to be(a) + end + + it "returns nil when no override matches the target" do + a = described_class.new("rails", :version, ">= 8.0") + expect(described_class.find_for([a], "sinatra", :version)).to be_nil + end + + it "returns nil when no override matches the field" do + a = described_class.new("rails", :version, ">= 8.0") + expect(described_class.find_for([a], "rails", :required_ruby_version)).to be_nil + end + + it "returns nil for an empty overrides list" do + expect(described_class.find_for([], "rails", :version)).to be_nil + end + + it "falls back to an :all override on the same field" do + a = described_class.new(:all, :required_ruby_version, :ignore_upper) + expect(described_class.find_for([a], "rails", :required_ruby_version)).to be(a) + end + + it "prefers a per-gem override over a matching :all override" do + per_gem = described_class.new("rails", :required_ruby_version, ">= 3.4") + all_target = described_class.new(:all, :required_ruby_version, :ignore_upper) + expect(described_class.find_for([all_target, per_gem], "rails", :required_ruby_version)).to be(per_gem) + end + + it "does not fall back to :all when the field differs" do + a = described_class.new(:all, :required_ruby_version, :ignore_upper) + expect(described_class.find_for([a], "rails", :required_rubygems_version)).to be_nil + end + end + + describe "#apply_to" do + context "when operation is a version spec string" do + it "replaces the existing requirement entirely" do + override = described_class.new("rails", :version, ">= 8.0") + result = override.apply_to(Gem::Requirement.new(">= 1.0", "< 2.0")) + expect(result).to eq(Gem::Requirement.new(">= 8.0")) + end + + it "ignores the existing requirement regardless of its content" do + override = described_class.new("rails", :version, "= 1.0") + result = override.apply_to(Gem::Requirement.new(">= 99.0")) + expect(result).to eq(Gem::Requirement.new("= 1.0")) + end + end + + context "when operation is :ignore_upper" do + it "removes < and <= operators" do + override = described_class.new("rails", :version, :ignore_upper) + result = override.apply_to(Gem::Requirement.new(">= 1.0", "< 2.0")) + expect(result).to eq(Gem::Requirement.new(">= 1.0")) + end + + it "keeps >, >=, = operators" do + override = described_class.new("rails", :version, :ignore_upper) + result = override.apply_to(Gem::Requirement.new("> 1.0", "<= 2.0")) + expect(result).to eq(Gem::Requirement.new("> 1.0")) + end + + it "converts ~> to >= preserving the lower bound" do + override = described_class.new("rails", :version, :ignore_upper) + result = override.apply_to(Gem::Requirement.new("~> 1.5")) + expect(result).to eq(Gem::Requirement.new(">= 1.5")) + end + + it "preserves != exclusion constraints" do + override = described_class.new("rails", :version, :ignore_upper) + result = override.apply_to(Gem::Requirement.new(">= 1.0", "!= 1.5.0", "< 2.0")) + expect(result).to eq(Gem::Requirement.new(">= 1.0", "!= 1.5.0")) + end + + it "returns the default requirement when only upper bounds remain" do + override = described_class.new("rails", :version, :ignore_upper) + result = override.apply_to(Gem::Requirement.new("< 2.0")) + expect(result).to eq(Gem::Requirement.default) + end + + it "returns the default requirement when the input is nil" do + override = described_class.new("rails", :version, :ignore_upper) + expect(override.apply_to(nil)).to eq(Gem::Requirement.default) + end + + it "returns the default requirement when the input is already the default" do + override = described_class.new("rails", :version, :ignore_upper) + expect(override.apply_to(Gem::Requirement.default)).to eq(Gem::Requirement.default) + end + end + + context "when operation is nil" do + it "returns the default requirement" do + override = described_class.new("rails", :version, nil) + result = override.apply_to(Gem::Requirement.new(">= 1.0", "< 2.0")) + expect(result).to eq(Gem::Requirement.default) + end + end + + context "when operation is unsupported" do + it "raises ArgumentError" do + override = described_class.new("rails", :version, 42) + expect { override.apply_to(Gem::Requirement.default) }.to raise_error(ArgumentError, /unsupported override operation/) + end + end + end +end diff --git a/spec/bundler/bundler/plugin/api/source_spec.rb b/spec/bundler/bundler/plugin/api/source_spec.rb index 2c50ff56a4..ae02e08bea 100644 --- a/spec/bundler/bundler/plugin/api/source_spec.rb +++ b/spec/bundler/bundler/plugin/api/source_spec.rb @@ -51,7 +51,7 @@ RSpec.describe Bundler::Plugin::API::Source do context "to_lock" do it "returns the string with remote and type" do - expected = strip_whitespace <<-L + expected = <<~L PLUGIN SOURCE remote: #{uri} type: #{type} @@ -67,7 +67,7 @@ RSpec.describe Bundler::Plugin::API::Source do end it "includes them" do - expected = strip_whitespace <<-L + expected = <<~L PLUGIN SOURCE remote: #{uri} type: #{type} @@ -79,4 +79,10 @@ RSpec.describe Bundler::Plugin::API::Source do end end end + + describe "to_s" do + it "returns the string with type and uri" do + expect(source.to_s).to eq("plugin source for spec_type with uri uri://to/test") + end + end end diff --git a/spec/bundler/bundler/plugin/dsl_spec.rb b/spec/bundler/bundler/plugin/dsl_spec.rb index be23db3bba..235a549735 100644 --- a/spec/bundler/bundler/plugin/dsl_spec.rb +++ b/spec/bundler/bundler/plugin/dsl_spec.rb @@ -23,16 +23,16 @@ RSpec.describe Bundler::Plugin::DSL do it "adds #source with :type to list and also inferred_plugins list" do expect(dsl).to receive(:plugin).with("bundler-source-news").once - dsl.source("some_random_url", :type => "news") {} + dsl.source("some_random_url", type: "news") {} expect(dsl.inferred_plugins).to eq(["bundler-source-news"]) end - it "registers a source type plugin only once for multiple declataions" do + it "registers a source type plugin only once for multiple declarations" do expect(dsl).to receive(:plugin).with("bundler-source-news").and_call_original.once - dsl.source("some_random_url", :type => "news") {} - dsl.source("another_random_url", :type => "news") {} + dsl.source("some_random_url", type: "news") {} + dsl.source("another_random_url", type: "news") {} end end end diff --git a/spec/bundler/bundler/plugin/events_spec.rb b/spec/bundler/bundler/plugin/events_spec.rb index 28d70c6fdd..77e5fdb74c 100644 --- a/spec/bundler/bundler/plugin/events_spec.rb +++ b/spec/bundler/bundler/plugin/events_spec.rb @@ -2,7 +2,17 @@ RSpec.describe Bundler::Plugin::Events do context "plugin events" do - before { Bundler::Plugin::Events.send :reset } + before do + @old_constants = Bundler::Plugin::Events.constants.map {|name| [name, Bundler::Plugin::Events.const_get(name)] } + Bundler::Plugin::Events.send :reset + end + + after do + Bundler::Plugin::Events.send(:reset) + Hash[@old_constants].each do |name, value| + Bundler::Plugin::Events.send(:define, name, value) + end + end describe "#define" do it "raises when redefining a constant" do diff --git a/spec/bundler/bundler/plugin/index_spec.rb b/spec/bundler/bundler/plugin/index_spec.rb index e18e960fb8..a28934269b 100644 --- a/spec/bundler/bundler/plugin/index_spec.rb +++ b/spec/bundler/bundler/plugin/index_spec.rb @@ -4,7 +4,8 @@ RSpec.describe Bundler::Plugin::Index do Index = Bundler::Plugin::Index before do - gemfile "" + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + gemfile "source 'https://gem.repo1'" path = lib_path(plugin_name) index.register_plugin("new-plugin", path.to_s, [path.join("lib").to_s], commands, sources, hooks) end @@ -21,7 +22,7 @@ RSpec.describe Bundler::Plugin::Index do expect(index.plugin_path(plugin_name)).to eq(lib_path(plugin_name)) end - it "load_paths is available for retrival" do + it "load_paths is available for retrieval" do expect(index.load_paths(plugin_name)).to eq([lib_path(plugin_name).join("lib").to_s]) end @@ -97,7 +98,13 @@ RSpec.describe Bundler::Plugin::Index do expect(index.hook_plugins("after-bar")).to eq([plugin_name]) end - context "that are not registered", :focused do + it "is gone after unregistration" do + expect(index.index_file.read).to include("after-bar:\n - \"new-plugin\"\n") + index.unregister_plugin(plugin_name) + expect(index.index_file.read).to_not include("after-bar:\n - \n") + end + + context "that are not registered" do let(:file) { double("index-file") } before do @@ -117,11 +124,11 @@ RSpec.describe Bundler::Plugin::Index do 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 + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(nil) + + Bundler::Plugin.reset! + path = lib_path("gplugin") + index.register_plugin("gplugin", path.to_s, [path.join("lib").to_s], [], ["glb_source"], []) end it "skips sources" do @@ -133,7 +140,7 @@ RSpec.describe Bundler::Plugin::Index do describe "after conflict" do let(:commands) { ["foo"] } let(:sources) { ["bar"] } - let(:hooks) { ["hoook"] } + let(:hooks) { ["thehook"] } shared_examples "it cleans up" do it "the path" do @@ -149,7 +156,7 @@ RSpec.describe Bundler::Plugin::Index do end it "the hook" do - expect(index.hook_plugins("xhoook")).to be_empty + expect(index.hook_plugins("xthehook")).to be_empty end end @@ -157,7 +164,7 @@ RSpec.describe Bundler::Plugin::Index do before do expect do path = lib_path("cplugin") - index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["xbar"], ["xhoook"]) + index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["xbar"], ["xthehook"]) end.to raise_error(Index::CommandConflict) end @@ -168,7 +175,7 @@ RSpec.describe Bundler::Plugin::Index do before do expect do path = lib_path("cplugin") - index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["xfoo"], ["bar"], ["xhoook"]) + index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["xfoo"], ["bar"], ["xthehook"]) end.to raise_error(Index::SourceConflict) end @@ -179,7 +186,7 @@ RSpec.describe Bundler::Plugin::Index do before do expect do path = lib_path("cplugin") - index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["bar"], ["xhoook"]) + index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["bar"], ["xthehook"]) end.to raise_error(Index::CommandConflict) end @@ -187,11 +194,82 @@ RSpec.describe Bundler::Plugin::Index do end end - describe "readonly disk without home" do - it "ignores being unable to create temp home dir" do - expect_any_instance_of(Bundler::Plugin::Index).to receive(:global_index_file). - and_raise(Bundler::GenericSystemCallError.new("foo", "bar")) - Bundler::Plugin::Index.new + describe "relative plugin paths" do + let(:plugin_name) { "relative-plugin" } + + before do + Bundler::Plugin.reset! + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + + plugin_root = Bundler::Plugin.root + FileUtils.mkdir_p(plugin_root) + + path = plugin_root.join(plugin_name) + FileUtils.mkdir_p(path.join("lib")) + + index.register_plugin(plugin_name, path.to_s, [path.join("lib").to_s], [], [], []) + end + + it "stores plugin paths relative to the plugin root" do + require "yaml" + data = YAML.load_file(index.index_file) + + expect(data["plugin_paths"][plugin_name]).to eq(plugin_name) + expect(data["load_paths"][plugin_name]).to eq([File.join(plugin_name, "lib")]) + end + + it "expands relative paths to absolute on load" do + require "bundler/yaml_serializer" + + plugin_root = Bundler::Plugin.root + + relative_index = { + "commands" => {}, + "hooks" => {}, + "load_paths" => { plugin_name => [File.join(plugin_name, "lib")] }, + "plugin_paths" => { plugin_name => plugin_name }, + "sources" => {}, + } + + File.open(index.index_file, "w") {|f| f.puts Bundler::YAMLSerializer.dump(relative_index) } + + new_index = Index.new + expect(new_index.plugin_path(plugin_name)).to eq(plugin_root.join(plugin_name)) + expect(new_index.load_paths(plugin_name)).to eq([plugin_root.join(plugin_name, "lib").to_s]) + end + + it "keeps paths outside the plugin root as absolute" do + outside_path = tmp.join("outside", "external-plugin") + FileUtils.mkdir_p(outside_path.join("lib")) + + index.register_plugin("external-plugin", outside_path.to_s, [outside_path.join("lib").to_s], [], [], []) + + require "yaml" + data = YAML.load_file(index.index_file) + + expect(data["plugin_paths"]["external-plugin"]).to eq(outside_path.to_s) + expect(data["load_paths"]["external-plugin"]).to eq([outside_path.join("lib").to_s]) + end + + it "reads legacy index files with absolute paths" do + require "bundler/yaml_serializer" + + plugin_root = Bundler::Plugin.root + absolute_path = plugin_root.join(plugin_name).to_s + + legacy_index = { + "commands" => {}, + "hooks" => {}, + "load_paths" => { plugin_name => [File.join(absolute_path, "lib")] }, + "plugin_paths" => { plugin_name => absolute_path }, + "sources" => {}, + } + + File.open(index.index_file, "w") {|f| f.puts Bundler::YAMLSerializer.dump(legacy_index) } + + new_index = Index.new + expect(new_index.plugin_path(plugin_name)).to eq(Pathname.new(absolute_path)) + expect(new_index.load_paths(plugin_name)).to eq([File.join(absolute_path, "lib")]) end end end diff --git a/spec/bundler/bundler/plugin/installer_spec.rb b/spec/bundler/bundler/plugin/installer_spec.rb index e89720f6f7..c200a98afa 100644 --- a/spec/bundler/bundler/plugin/installer_spec.rb +++ b/spec/bundler/bundler/plugin/installer_spec.rb @@ -6,8 +6,7 @@ RSpec.describe Bundler::Plugin::Installer do describe "cli install" do it "uses Gem.sources when non of the source is provided" do sources = double(:sources) - Bundler.settings # initialize it before we have to touch rubygems.ext_lock - allow(Bundler).to receive_message_chain("rubygems.sources") { sources } + allow(Gem).to receive(:sources) { sources } allow(installer).to receive(:install_rubygems). with("new-plugin", [">= 0"], sources).once @@ -21,15 +20,15 @@ RSpec.describe Bundler::Plugin::Installer do allow(installer).to receive(:install_git). and_return("new-plugin" => spec) - expect(installer.install(["new-plugin"], :git => "https://some.ran/dom")). + expect(installer.install(["new-plugin"], git: "https://some.ran/dom")). to eq("new-plugin" => spec) end it "returns the installed spec after installing local git plugins" do - allow(installer).to receive(:install_local_git). + allow(installer).to receive(:install_git). and_return("new-plugin" => spec) - expect(installer.install(["new-plugin"], :local_git => "/phony/path/repo")). + expect(installer.install(["new-plugin"], git: "/phony/path/repo")). to eq("new-plugin" => spec) end @@ -37,7 +36,7 @@ RSpec.describe Bundler::Plugin::Installer do allow(installer).to receive(:install_rubygems). and_return("new-plugin" => spec) - expect(installer.install(["new-plugin"], :source => "https://some.ran/dom")). + expect(installer.install(["new-plugin"], source: "https://some.ran/dom")). to eq("new-plugin" => spec) end end @@ -48,17 +47,24 @@ RSpec.describe Bundler::Plugin::Installer do build_plugin "re-plugin" build_plugin "ma-plugin" end + + @previous_ui = Bundler.ui + Bundler.ui = Bundler::UI::Silent.new + end + + after do + Bundler.ui = @previous_ui end context "git plugins" do before do - build_git "ga-plugin", :path => lib_path("ga-plugin") do |s| + build_git "ga-plugin", path: lib_path("ga-plugin") do |s| s.write "plugins.rb" end end let(:result) do - installer.install(["ga-plugin"], :git => file_uri_for(lib_path("ga-plugin"))) + installer.install(["ga-plugin"], git: lib_path("ga-plugin").to_s) end it "returns the installed spec after installing" do @@ -75,13 +81,13 @@ RSpec.describe Bundler::Plugin::Installer do context "local git plugins" do before do - build_git "ga-plugin", :path => lib_path("ga-plugin") do |s| + build_git "ga-plugin", path: lib_path("ga-plugin") do |s| s.write "plugins.rb" end end let(:result) do - installer.install(["ga-plugin"], :local_git => lib_path("ga-plugin").to_s) + installer.install(["ga-plugin"], git: lib_path("ga-plugin").to_s) end it "returns the installed spec after installing" do @@ -98,7 +104,7 @@ RSpec.describe Bundler::Plugin::Installer do context "rubygems plugins" do let(:result) do - installer.install(["re-plugin"], :source => file_uri_for(gem_repo2)) + installer.install(["re-plugin"], source: file_uri_for(gem_repo2)) end it "returns the installed spec after installing " do @@ -113,7 +119,7 @@ RSpec.describe Bundler::Plugin::Installer do context "multiple plugins" do let(:result) do - installer.install(["re-plugin", "ma-plugin"], :source => file_uri_for(gem_repo2)) + installer.install(["re-plugin", "ma-plugin"], source: file_uri_for(gem_repo2)) end it "returns the installed spec after installing " do diff --git a/spec/bundler/bundler/plugin_spec.rb b/spec/bundler/bundler/plugin_spec.rb index e0e2e9afdf..b379594c6f 100644 --- a/spec/bundler/bundler/plugin_spec.rb +++ b/spec/bundler/bundler/plugin_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative "../support/streams" - RSpec.describe Bundler::Plugin do Plugin = Bundler::Plugin @@ -11,11 +9,11 @@ RSpec.describe Bundler::Plugin do let(:spec2) { double(:spec2) } before do - build_lib "new-plugin", :path => lib_path("new-plugin") do |s| + build_lib "new-plugin", path: lib_path("new-plugin") do |s| s.write "plugins.rb" end - build_lib "another-plugin", :path => lib_path("another-plugin") do |s| + build_lib "another-plugin", path: lib_path("another-plugin") do |s| s.write "plugins.rb" end @@ -67,6 +65,8 @@ RSpec.describe Bundler::Plugin do end it "passes the name and options to installer" do + allow(index).to receive(:up_to_date?). + with(spec) allow(installer).to receive(:install).with(["new-plugin"], opts) do { "new-plugin" => spec } end.once @@ -75,6 +75,8 @@ RSpec.describe Bundler::Plugin do end it "validates the installed plugin" do + allow(index).to receive(:up_to_date?). + with(spec) allow(subject). to receive(:validate_plugin!).with(lib_path("new-plugin")).once @@ -82,6 +84,8 @@ RSpec.describe Bundler::Plugin do end it "registers the plugin with index" do + allow(index).to receive(:up_to_date?). + with(spec) 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 @@ -98,6 +102,7 @@ RSpec.describe Bundler::Plugin do end.once allow(subject).to receive(:validate_plugin!).twice + allow(index).to receive(:up_to_date?).twice allow(index).to receive(:register_plugin).twice subject.install ["new-plugin", "another-plugin"], opts end @@ -107,11 +112,12 @@ RSpec.describe Bundler::Plugin do describe "evaluate gemfile for plugins" do let(:definition) { double("definition") } let(:builder) { double("builder") } - let(:gemfile) { bundled_app("Gemfile") } + 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(:check_primary_source_safety) allow(builder).to receive(:to_definition) { definition } allow(builder).to receive(:inferred_plugins) { [] } end @@ -132,7 +138,7 @@ RSpec.describe Bundler::Plugin do end before do - allow(index).to receive(:installed?) { nil } + allow(index).to receive(:up_to_date?) { 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 @@ -219,7 +225,7 @@ RSpec.describe Bundler::Plugin do end end - describe "#source_from_lock" do + describe "#from_lock" do it "returns instance of registered class initialized with locked opts" do opts = { "type" => "l_source", "remote" => "xyz", "other" => "random" } allow(index).to receive(:source_plugin).with("l_source") { "plugin_name" } @@ -230,25 +236,28 @@ RSpec.describe Bundler::Plugin do expect(SClass).to receive(:new). with(hash_including("type" => "l_source", "uri" => "xyz", "other" => "random")) { s_instance } - expect(subject.source_from_lock(opts)).to be(s_instance) + expect(subject.from_lock(opts)).to be(s_instance) end end describe "#root" do context "in app dir" do before do - gemfile "" + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) end it "returns plugin dir in app .bundle path" do - expect(subject.root).to eq(bundled_app.join(".bundle/plugin")) + expect(subject.root).to eq(bundled_app(".bundle/plugin")) end end context "outside app dir" do + before do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(nil) + end + it "returns plugin dir in global bundle path" do - Dir.chdir tmp - expect(subject.root).to eq(home.join(".bundle/plugin")) + expect(subject.root).to eq(home(".bundle/plugin")) end end end @@ -266,22 +275,30 @@ RSpec.describe Bundler::Plugin do describe "#hook" do before do path = lib_path("foo-plugin") - build_lib "foo-plugin", :path => path do |s| + build_lib "foo-plugin", path: path do |s| s.write "plugins.rb", code end + @old_constants = Bundler::Plugin::Events.constants.map {|name| [name, Bundler::Plugin::Events.const_get(name)] } Bundler::Plugin::Events.send(:reset) - Bundler::Plugin::Events.send(:define, :EVENT_1, "event-1") - Bundler::Plugin::Events.send(:define, :EVENT_2, "event-2") + Bundler::Plugin::Events.send(:define, :EVENT1, "event-1") + Bundler::Plugin::Events.send(:define, :EVENT2, "event-2") - allow(index).to receive(:hook_plugins).with(Bundler::Plugin::Events::EVENT_1). - and_return(["foo-plugin"]) - allow(index).to receive(:hook_plugins).with(Bundler::Plugin::Events::EVENT_2). + allow(index).to receive(:hook_plugins).with(Bundler::Plugin::Events::EVENT1). + and_return(["foo-plugin", "", nil]) + allow(index).to receive(:hook_plugins).with(Bundler::Plugin::Events::EVENT2). and_return(["foo-plugin"]) allow(index).to receive(:plugin_path).with("foo-plugin").and_return(path) allow(index).to receive(:load_paths).with("foo-plugin").and_return([]) end + after do + Bundler::Plugin::Events.send(:reset) + Hash[@old_constants].each do |name, value| + Bundler::Plugin::Events.send(:define, name, value) + end + end + let(:code) { <<-RUBY } Bundler::Plugin::API.hook("event-1") { puts "hook for event 1" } RUBY @@ -293,41 +310,58 @@ RSpec.describe Bundler::Plugin do end it "executes the hook" do - out = capture(:stdout) do - Plugin.hook(Bundler::Plugin::Events::EVENT_1) - end.strip - - expect(out).to eq("hook for event 1") + expect do + Plugin.hook(Bundler::Plugin::Events::EVENT1) + end.to output("hook for event 1\n").to_stdout end context "single plugin declaring more than one hook" do let(:code) { <<-RUBY } - Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT_1) {} - Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT_2) {} + Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT1) {} + Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT2) {} puts "loaded" RUBY it "evals plugins.rb once" do - out = capture(:stdout) do - Plugin.hook(Bundler::Plugin::Events::EVENT_1) - Plugin.hook(Bundler::Plugin::Events::EVENT_2) - end.strip - - expect(out).to eq("loaded") + expect do + Plugin.hook(Bundler::Plugin::Events::EVENT1) + Plugin.hook(Bundler::Plugin::Events::EVENT2) + end.to output("loaded\n").to_stdout end end context "a block is passed" do let(:code) { <<-RUBY } - Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT_1) { |&blk| blk.call } + Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT1) { |&blk| blk.call } RUBY it "is passed to the hook" do - out = capture(:stdout) do - Plugin.hook(Bundler::Plugin::Events::EVENT_1) { puts "win" } - end.strip + expect do + Plugin.hook(Bundler::Plugin::Events::EVENT1) { puts "win" } + end.to output("win\n").to_stdout + end + end + + context "the plugin load_path is invalid" do + before do + allow(index).to receive(:load_paths).with("foo-plugin"). + and_return(["invalid-file-name1", "invalid-file-name2"]) + end + + it "outputs a useful warning" do + msg = + "The following plugin paths don't exist: invalid-file-name1, invalid-file-name2.\n\n" \ + "This can happen if the plugin was " \ + "installed with a different version of Ruby that has since been uninstalled.\n\n" \ + "If you would like to reinstall the plugin, run:\n\n" \ + "bundler plugin uninstall foo-plugin && bundler plugin install foo-plugin\n\n" \ + "Continuing without installing plugin foo-plugin.\n" + + expect(Bundler.ui).to receive(:warn).with(msg) + + Plugin.hook(Bundler::Plugin::Events::EVENT1) - expect(out).to eq("win") + expect(subject.loaded?("foo-plugin")).to be_falsey end end end diff --git a/spec/bundler/bundler/psyched_yaml_spec.rb b/spec/bundler/bundler/psyched_yaml_spec.rb deleted file mode 100644 index d5d68c5cc3..0000000000 --- a/spec/bundler/bundler/psyched_yaml_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require "bundler/psyched_yaml" - -RSpec.describe "Bundler::YamlLibrarySyntaxError" do - it "is raised on YAML parse errors" do - expect { YAML.parse "{foo" }.to raise_error(Bundler::YamlLibrarySyntaxError) - end -end diff --git a/spec/bundler/bundler/remote_specification_spec.rb b/spec/bundler/bundler/remote_specification_spec.rb index 8115e026d8..f35b231d58 100644 --- a/spec/bundler/bundler/remote_specification_spec.rb +++ b/spec/bundler/bundler/remote_specification_spec.rb @@ -17,7 +17,7 @@ RSpec.describe Bundler::RemoteSpecification do end describe "#fetch_platform" do - let(:remote_spec) { double(:remote_spec, :platform => "jruby") } + let(:remote_spec) { double(:remote_spec, platform: "jruby") } before { allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec) } @@ -45,7 +45,7 @@ RSpec.describe Bundler::RemoteSpecification do let(:platform) { "jruby" } it "should return the spec name, version, and platform" do - expect(subject.full_name).to eq("foo-1.0.0-jruby") + expect(subject.full_name).to eq("foo-1.0.0-java") end end end @@ -113,7 +113,7 @@ RSpec.describe Bundler::RemoteSpecification do context "comparing a non sortable object" do let(:other) { Object.new } - let(:remote_spec) { double(:remote_spec, :platform => "jruby") } + let(:remote_spec) { double(:remote_spec, platform: "jruby") } before do allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec) @@ -127,8 +127,8 @@ RSpec.describe Bundler::RemoteSpecification do end describe "#__swap__" do - let(:spec) { double(:spec, :dependencies => []) } - let(:new_spec) { double(:new_spec, :dependencies => [], :runtime_dependencies => []) } + let(:spec) { double(:spec, dependencies: []) } + let(:new_spec) { double(:new_spec, dependencies: [], runtime_dependencies: []) } before { subject.instance_variable_set(:@_remote_specification, spec) } @@ -157,7 +157,7 @@ RSpec.describe Bundler::RemoteSpecification do describe "method missing" do context "and is present in Gem::Specification" do - let(:remote_spec) { double(:remote_spec, :authors => "abcd") } + let(:remote_spec) { double(:remote_spec, authors: "abcd") } before do allow(subject).to receive(:_remote_specification).and_return(remote_spec) @@ -172,7 +172,7 @@ RSpec.describe Bundler::RemoteSpecification do describe "respond to missing?" do context "and is present in Gem::Specification" do - let(:remote_spec) { double(:remote_spec, :authors => "abcd") } + let(:remote_spec) { double(:remote_spec, authors: "abcd") } before do allow(subject).to receive(:_remote_specification).and_return(remote_spec) diff --git a/spec/bundler/bundler/resolver/candidate_spec.rb b/spec/bundler/bundler/resolver/candidate_spec.rb new file mode 100644 index 0000000000..aefad3316e --- /dev/null +++ b/spec/bundler/bundler/resolver/candidate_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Resolver::Candidate do + it "compares fine" do + version1 = described_class.new("1.12.5", priority: -1) + version2 = described_class.new("1.12.5", priority: 1) + + expect(version2 > version1).to be true + + version1 = described_class.new("1.12.5") + version2 = described_class.new("1.12.5") + + expect(version2 == version1).to be true + + version1 = described_class.new("1.12.5", priority: 1) + version2 = described_class.new("1.12.5", priority: -1) + + expect(version2 < version1).to be true + end +end diff --git a/spec/bundler/bundler/resolver/cooldown_spec.rb b/spec/bundler/bundler/resolver/cooldown_spec.rb new file mode 100644 index 0000000000..37ec158cba --- /dev/null +++ b/spec/bundler/bundler/resolver/cooldown_spec.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Resolver do + let(:resolver) { described_class.allocate } + + def remote(cooldown:) + instance_double(Bundler::Source::Rubygems::Remote, effective_cooldown: cooldown) + end + + def spec(created_at:, remote:, name: "myrack", version: "1.0.0") + Struct.new(:name, :version, :created_at, :remote).new(name, Gem::Version.new(version), created_at, remote) + end + + describe "#filter_cooldown" do + let(:now) { Time.now } + + context "with a 7-day cooldown" do + let(:r) { remote(cooldown: 7) } + + it "rejects versions published within the window" do + recent = spec(version: "1.1.0", created_at: now - (2 * 86_400), remote: r) + old = spec(version: "1.0.0", created_at: now - (30 * 86_400), remote: r) + + expect(resolver.send(:filter_cooldown, [recent, old])).to eq([old]) + end + + it "keeps versions published exactly at the threshold" do + boundary = spec(created_at: now - (7 * 86_400), remote: r) + + expect(resolver.send(:filter_cooldown, [boundary])).to eq([boundary]) + end + + it "leaves rolling-delay history intact" do + # 7-day cooldown with frequent releases must still expose an older candidate. + in_cooldown = spec(version: "1.2.0", created_at: now - 86_400, remote: r) + also_in_cooldown = spec(version: "1.1.0", created_at: now - (3 * 86_400), remote: r) + eligible = spec(version: "1.0.0", created_at: now - (10 * 86_400), remote: r) + + result = resolver.send(:filter_cooldown, [in_cooldown, also_in_cooldown, eligible]) + + expect(result).to eq([eligible]) + end + + it "drops every spec sharing an excluded [name, version] tuple" do + # The cooldown check is by version, not per-spec: a StubSpecification for an + # in-cooldown release would otherwise slip through on local install paths. + endpoint = spec(version: "2.0.0", created_at: now - 86_400, remote: r) + local_stub = Struct.new(:name, :version).new("myrack", Gem::Version.new("2.0.0")) + eligible = spec(version: "1.0.0", created_at: now - (30 * 86_400), remote: r) + + result = resolver.send(:filter_cooldown, [endpoint, local_stub, eligible]) + + expect(result).to eq([eligible]) + end + + it "keeps stub-only versions that no endpoint marks as in cooldown" do + # If no remote spec carries created_at for a version, cooldown cannot judge it; + # the stub stays in. + local_only = Struct.new(:name, :version).new("myrack", Gem::Version.new("2.0.0")) + eligible = spec(version: "1.0.0", created_at: now - (30 * 86_400), remote: r) + + result = resolver.send(:filter_cooldown, [local_only, eligible]) + + expect(result).to eq([local_only, eligible]) + end + end + + context "when created_at is missing (blank metadata)" do + it "keeps the spec regardless of cooldown" do + s = spec(created_at: nil, remote: remote(cooldown: 7)) + + expect(resolver.send(:filter_cooldown, [s])).to eq([s]) + end + end + + context "when the remote has no cooldown" do + it "keeps every spec" do + s = spec(created_at: now - 3600, remote: remote(cooldown: nil)) + + expect(resolver.send(:filter_cooldown, [s])).to eq([s]) + end + end + + context "when cooldown is 0" do + it "keeps every spec (escape hatch)" do + s = spec(created_at: now - 3600, remote: remote(cooldown: 0)) + + expect(resolver.send(:filter_cooldown, [s])).to eq([s]) + end + end + + context "when the spec does not respond to created_at" do + it "keeps the spec" do + bare = Struct.new(:version).new("1.0.0") + + expect(resolver.send(:filter_cooldown, [bare])).to eq([bare]) + end + end + + context "when the spec has no remote" do + it "keeps the spec" do + s = spec(created_at: now - 86_400, remote: nil) + + expect(resolver.send(:filter_cooldown, [s])).to eq([s]) + end + end + + it "returns the same array when input is empty" do + expect(resolver.send(:filter_cooldown, [])).to eq([]) + end + end + + describe "#cooldown_hint" do + let(:now) { Time.now } + let(:r) { remote(cooldown: 7) } + + it "returns nil when no spec is excluded" do + expect(resolver.send(:cooldown_hint, [])).to be_nil + end + + it "returns nil when every spec is outside the cooldown window" do + eligible = [spec(created_at: now - (30 * 86_400), remote: r)] + + expect(resolver.send(:cooldown_hint, eligible)).to be_nil + end + + it "mentions the count and the bypass flag for one excluded version" do + excluded = [spec(created_at: now - 86_400, remote: r)] + + hint = resolver.send(:cooldown_hint, excluded) + + expect(hint).to match(/1 version excluded by the cooldown setting/) + expect(hint).to match(/--cooldown 0/) + end + + it "uses plural wording when multiple versions are excluded" do + excluded = %w[1.0.0 1.1.0 1.2.0].map {|v| spec(version: v, created_at: now - 86_400, remote: r) } + + expect(resolver.send(:cooldown_hint, excluded)).to match(/3 versions excluded/) + end + + it "counts each unique version once even when multiple spec instances share it" do + duplicates = Array.new(3) { spec(created_at: now - 86_400, remote: r) } + + expect(resolver.send(:cooldown_hint, duplicates)).to match(/1 version excluded/) + end + end +end diff --git a/spec/bundler/bundler/retry_spec.rb b/spec/bundler/bundler/retry_spec.rb index b893580d72..5c84d0bea5 100644 --- a/spec/bundler/bundler/retry_spec.rb +++ b/spec/bundler/bundler/retry_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Bundler::Retry do end it "returns the first valid result" do - jobs = [proc { raise "foo" }, proc { :bar }, proc { raise "foo" }] + jobs = [proc { raise "job 1 failed" }, proc { :bar }, proc { raise "job 2 failed" }] attempts = 0 result = Bundler::Retry.new(nil, nil, 3).attempt do attempts += 1 @@ -68,7 +68,7 @@ RSpec.describe Bundler::Retry 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(Bundler.ui).to receive(:warn).with(failure_message, true) expect do Bundler::Retry.new("test", [], 1).attempt do @@ -78,4 +78,113 @@ RSpec.describe Bundler::Retry do end end end + + context "exponential backoff" do + it "can be disabled by setting base_delay to 0" do + attempts = 0 + expect do + Bundler::Retry.new("test", [], 2, base_delay: 0).attempt do + attempts += 1 + raise "error" + end + end.to raise_error(StandardError) + + # Verify no sleep was called (implicitly - if sleep was called, timing would be different) + expect(attempts).to eq(3) + end + + it "is enabled by default with 1 second base delay" do + original_base_delay = Bundler::Retry.default_base_delay + Bundler::Retry.default_base_delay = 1.0 + + attempts = 0 + sleep_times = [] + + allow_any_instance_of(Bundler::Retry).to receive(:sleep) do |_instance, delay| + sleep_times << delay + end + + expect do + Bundler::Retry.new("test", [], 2, jitter: 0).attempt do + attempts += 1 + raise "error" + end + end.to raise_error(StandardError) + + expect(attempts).to eq(3) + expect(sleep_times.length).to eq(2) + # First retry: 1.0 * 2^0 = 1.0 + expect(sleep_times[0]).to eq(1.0) + # Second retry: 1.0 * 2^1 = 2.0 + expect(sleep_times[1]).to eq(2.0) + ensure + Bundler::Retry.default_base_delay = original_base_delay + end + + it "sleeps with exponential backoff when base_delay is set" do + attempts = 0 + sleep_times = [] + + allow_any_instance_of(Bundler::Retry).to receive(:sleep) do |_instance, delay| + sleep_times << delay + end + + expect do + Bundler::Retry.new("test", [], 2, base_delay: 1.0, jitter: 0).attempt do + attempts += 1 + raise "error" + end + end.to raise_error(StandardError) + + expect(attempts).to eq(3) + expect(sleep_times.length).to eq(2) + # First retry: 1.0 * 2^0 = 1.0 + expect(sleep_times[0]).to eq(1.0) + # Second retry: 1.0 * 2^1 = 2.0 + expect(sleep_times[1]).to eq(2.0) + end + + it "respects max_delay" do + sleep_times = [] + + allow_any_instance_of(Bundler::Retry).to receive(:sleep) do |_instance, delay| + sleep_times << delay + end + + expect do + Bundler::Retry.new("test", [], 3, base_delay: 10.0, max_delay: 15.0, jitter: 0).attempt do + raise "error" + end + end.to raise_error(StandardError) + + # First retry: 10.0 * 2^0 = 10.0 + expect(sleep_times[0]).to eq(10.0) + # Second retry: 10.0 * 2^1 = 20.0, capped at 15.0 + expect(sleep_times[1]).to eq(15.0) + # Third retry: 10.0 * 2^2 = 40.0, capped at 15.0 + expect(sleep_times[2]).to eq(15.0) + end + + it "adds jitter to delay" do + sleep_times = [] + + allow_any_instance_of(Bundler::Retry).to receive(:sleep) do |_instance, delay| + sleep_times << delay + end + + expect do + Bundler::Retry.new("test", [], 2, base_delay: 1.0, jitter: 0.5).attempt do + raise "error" + end + end.to raise_error(StandardError) + + expect(sleep_times.length).to eq(2) + # First retry should be between 1.0 and 1.5 (base + jitter) + expect(sleep_times[0]).to be >= 1.0 + expect(sleep_times[0]).to be <= 1.5 + # Second retry should be between 2.0 and 2.5 + expect(sleep_times[1]).to be >= 2.0 + expect(sleep_times[1]).to be <= 2.5 + end + end end diff --git a/spec/bundler/bundler/ruby_dsl_spec.rb b/spec/bundler/bundler/ruby_dsl_spec.rb index bc1ca98457..45a37c5795 100644 --- a/spec/bundler/bundler/ruby_dsl_spec.rb +++ b/spec/bundler/bundler/ruby_dsl_spec.rb @@ -7,28 +7,37 @@ RSpec.describe Bundler::RubyDsl do include Bundler::RubyDsl attr_reader :ruby_version + attr_accessor :gemfile end let(:dsl) { MockDSL.new } let(:ruby_version) { "2.0.0" } + let(:ruby_version_arg) { ruby_version } let(:version) { "2.0.0" } let(:engine) { "jruby" } let(:engine_version) { "9000" } let(:patchlevel) { "100" } let(:options) do - { :patchlevel => patchlevel, - :engine => engine, - :engine_version => engine_version } + { patchlevel: patchlevel, + engine: engine, + engine_version: engine_version } end + let(:project_root) { Pathname.new("/path/to/project") } + let(:gemfile) { project_root.join("Gemfile") } + before { allow(Bundler).to receive(:root).and_return(project_root) } let(:invoke) do proc do - args = Array(ruby_version) + [options] + args = [] + args << ruby_version_arg if ruby_version_arg + args << options + dsl.ruby(*args) end end subject do + dsl.gemfile = gemfile invoke.call dsl.ruby_version end @@ -59,10 +68,19 @@ RSpec.describe Bundler::RubyDsl do it_behaves_like "it stores the ruby version" end + context "with a preview version" do + let(:ruby_version) { "3.3.0-preview2" } + + it "stores the version" do + expect(subject.versions).to eq(Array("3.3.0.preview2")) + expect(subject.gem_version.version).to eq("3.3.0.preview2") + end + end + context "with two requirements in the same string" do let(:ruby_version) { ">= 2.0.0, < 3.0" } it "raises an error" do - expect { subject }.to raise_error(ArgumentError) + expect { subject }.to raise_error(Bundler::InvalidArgumentError) end end @@ -91,5 +109,140 @@ RSpec.describe Bundler::RubyDsl do it_behaves_like "it stores the ruby version" end end + + context "with a file option" do + let(:file) { ".ruby-version" } + let(:ruby_version_file_path) { gemfile.dirname.join(file) } + let(:options) do + { file: file, + patchlevel: patchlevel, + engine: engine, + engine_version: engine_version } + end + let(:ruby_version_arg) { nil } + let(:file_content) { "#{version}\n" } + + before do + allow(Bundler).to receive(:read_file) do |path| + raise Errno::ENOENT, <<~ERROR unless path == ruby_version_file_path + #{file} not found in specs: + expected: #{ruby_version_file_path} + received: #{path} + ERROR + file_content + end + end + + it_behaves_like "it stores the ruby version" + + context "with the Gemfile ruby file: path is relative to the Gemfile in a subdir" do + let(:gemfile) { project_root.join("subdir", "Gemfile") } + let(:file) { "../.ruby-version" } + let(:ruby_version_file_path) { gemfile.dirname.join(file) } + + it_behaves_like "it stores the ruby version" + end + + context "with bundler root in a subdir of the project" do + let(:project_root) { Pathname.new("/path/to/project/subdir") } + let(:gemfile) { project_root.parent.join("Gemfile") } + + it_behaves_like "it stores the ruby version" + end + + context "with the ruby- prefix in the file" do + let(:file_content) { "ruby-#{version}\n" } + + it_behaves_like "it stores the ruby version" + end + + context "and a version" do + let(:ruby_version_arg) { version } + + it "raises an error" do + expect { subject }.to raise_error(Bundler::GemfileError, "Do not pass version argument when using :file option") + end + end + + context "with a @gemset" do + let(:file_content) { "ruby-#{version}@gemset\n" } + + it "raises an error" do + expect { subject }.to raise_error(Bundler::InvalidArgumentError, "2.0.0@gemset is not a valid requirement on the Ruby version") + end + end + + context "with a mise.toml file format" do + let(:file) { "mise.toml" } + let(:ruby_version_arg) { nil } + let(:file_content) do + <<~TOML + [tools] + ruby = #{quote}#{version}#{quote} + TOML + end + + context "with double quotes" do + let(:quote) { '"' } + + it_behaves_like "it stores the ruby version" + end + + context "with single quotes" do + let(:quote) { "'" } + + it_behaves_like "it stores the ruby version" + end + + context "with mismatched quotes" do + let(:file_content) do + <<~TOML + [tools] + ruby = "#{version}' + TOML + end + + it "raises an error" do + expect { subject }.to raise_error(Bundler::InvalidArgumentError, "= is not a valid requirement on the Ruby version") + end + end + end + + context "with a .tool-versions file format" do + let(:file) { ".tool-versions" } + let(:ruby_version_arg) { nil } + let(:file_content) do + <<~TOOLS + nodejs 18.16.0 + ruby #{version} # This is a comment + pnpm 8.6.12 + TOOLS + end + + it_behaves_like "it stores the ruby version" + + context "with extra spaces and a very cozy comment" do + let(:file_content) do + <<~TOOLS + nodejs 18.16.0 + ruby #{version}# This is a cozy comment + pnpm 8.6.12 + TOOLS + end + + it_behaves_like "it stores the ruby version" + end + end + + context "when the file does not exist" do + let(:ruby_version_file_path) { nil } + let(:ruby_version_arg) { nil } + let(:file) { "nonexistent.txt" } + + it "raises an error" do + expect { subject }.to raise_error(Bundler::GemfileError, /Could not find version file nonexistent.txt/) + end + end + end end end diff --git a/spec/bundler/bundler/ruby_version_spec.rb b/spec/bundler/bundler/ruby_version_spec.rb index 8c6c071d7f..0d41ec9901 100644 --- a/spec/bundler/bundler/ruby_version_spec.rb +++ b/spec/bundler/bundler/ruby_version_spec.rb @@ -100,7 +100,7 @@ RSpec.describe "Bundler::RubyVersion and its subclasses" do 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)") + expect(subject.to_s).to eq("ruby 2.0.0 (jruby 2.0.1)") end context "no patchlevel" do @@ -115,7 +115,7 @@ RSpec.describe "Bundler::RubyVersion and its subclasses" 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") + expect(subject.to_s).to eq("ruby 2.0.0") end end @@ -137,7 +137,7 @@ RSpec.describe "Bundler::RubyVersion and its subclasses" do end end - context "the versions, pathlevels, engines, and engine_versions match" do + shared_examples_for "the versions, engines, and engine_versions match" do it "should return true" do expect(subject).to eq(other_ruby_version) end @@ -152,7 +152,7 @@ RSpec.describe "Bundler::RubyVersion and its subclasses" do context "the patchlevels do not match" do let(:other_patchlevel) { "21" } - it_behaves_like "two ruby versions are not equal" + it_behaves_like "the versions, engines, and engine_versions match" end context "the engines do not match" do @@ -228,9 +228,9 @@ RSpec.describe "Bundler::RubyVersion and its subclasses" do 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]) + shared_examples_for "even there is a difference in the patchlevels" do + it "should return nil" do + expect(ruby_version.diff(other_ruby_version)).to be_nil end end @@ -287,10 +287,10 @@ RSpec.describe "Bundler::RubyVersion and its subclasses" do it_behaves_like "there is a difference in the engine versions" end - context "detects patchlevel discrepancies last" do + context "ignores patchlevel discrepancies last" do let(:other_patchlevel) { "643" } - it_behaves_like "there is a difference in the patchlevels" + it_behaves_like "even there is a difference in the patchlevels" end context "successfully matches gem requirements" do @@ -355,7 +355,7 @@ RSpec.describe "Bundler::RubyVersion and its subclasses" do let(:other_engine) { "ruby" } let(:other_engine_version) { "2.0.5" } - it_behaves_like "there is a difference in the patchlevels" + it_behaves_like "even there is a difference in the patchlevels" end context "successfully detects bad gem requirements with engine versions" do @@ -389,7 +389,7 @@ RSpec.describe "Bundler::RubyVersion and its subclasses" do 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" + it_behaves_like "even there is a difference in the patchlevels" end end end @@ -400,19 +400,19 @@ RSpec.describe "Bundler::RubyVersion and its subclasses" do let(:bundler_system_ruby_version) { subject } around do |example| - if Bundler::RubyVersion.instance_variable_defined?("@ruby_version") + if Bundler::RubyVersion.instance_variable_defined?("@system") begin - old_ruby_version = Bundler::RubyVersion.instance_variable_get("@ruby_version") - Bundler::RubyVersion.remove_instance_variable("@ruby_version") + old_ruby_version = Bundler::RubyVersion.instance_variable_get("@system") + Bundler::RubyVersion.remove_instance_variable("@system") example.run ensure - Bundler::RubyVersion.instance_variable_set("@ruby_version", old_ruby_version) + Bundler::RubyVersion.instance_variable_set("@system", old_ruby_version) end else begin example.run ensure - Bundler::RubyVersion.remove_instance_variable("@ruby_version") + Bundler::RubyVersion.remove_instance_variable("@system") end end end @@ -427,9 +427,8 @@ RSpec.describe "Bundler::RubyVersion and its subclasses" do end describe "#version" do - it "should return a copy of the value of RUBY_VERSION" do - expect(subject.versions).to eq([RUBY_VERSION]) - expect(subject.versions.first).to_not be(RUBY_VERSION) + it "should return the value of Gem.ruby_version as a string" do + expect(subject.versions).to eq([Gem.ruby_version.to_s]) end end @@ -446,13 +445,12 @@ RSpec.describe "Bundler::RubyVersion and its subclasses" do describe "#engine_version" do context "engine is ruby" do before do - stub_const("RUBY_ENGINE_VERSION", "2.2.4") + allow(Gem).to receive(:ruby_version).and_return(Gem::Version.new("2.2.4")) stub_const("RUBY_ENGINE", "ruby") end - it "should return a copy of the value of RUBY_ENGINE_VERSION" do + it "should return the value of Gem.ruby_version as a string" do expect(bundler_system_ruby_version.engine_versions).to eq(["2.2.4"]) - expect(bundler_system_ruby_version.engine_versions.first).to_not be(RUBY_ENGINE_VERSION) end end @@ -498,31 +496,5 @@ RSpec.describe "Bundler::RubyVersion and its subclasses" do end end end - - describe "#to_gem_version_with_patchlevel" do - shared_examples_for "the patchlevel is omitted" do - it "does not include a patch level" do - expect(subject.to_gem_version_with_patchlevel.to_s).to eq(version) - end - end - - context "with nil patch number" do - let(:patchlevel) { nil } - - it_behaves_like "the patchlevel is omitted" - end - - context "with negative patch number" do - let(:patchlevel) { -1 } - - it_behaves_like "the patchlevel is omitted" - end - - context "with a valid patch number" do - it "uses the specified patchlevel as patchlevel" do - expect(subject.to_gem_version_with_patchlevel.to_s).to eq("#{version}.#{patchlevel}") - end - end - end end end diff --git a/spec/bundler/bundler/rubygems_ext_spec.rb b/spec/bundler/bundler/rubygems_ext_spec.rb new file mode 100644 index 0000000000..0fc528f78c --- /dev/null +++ b/spec/bundler/bundler/rubygems_ext_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "bundler/rubygems_ext" + +RSpec.describe Gem::SplitCompactIndexEntryOnFirstColon do + # Reproduces the RubyGems < 4.0.13 `Gem::Resolver::APISet::GemParser` that + # split each compact index entry on every colon, corrupting metadata values + # that themselves contain colons. + let(:legacy_parser_class) do + Class.new do + def parse_dependency(string) + dependency = string.split(":") + dependency[-1] = dependency[-1].split("&") if dependency.size > 1 + dependency[0] = -dependency[0] + dependency + end + end + end + + before { legacy_parser_class.prepend(described_class) } + + it "preserves colon-bearing metadata values such as created_at timestamps" do + parser = legacy_parser_class.new + + expect(parser.send(:parse_dependency, "created_at:2026-05-12T10:00:00Z")).to eq(["created_at", ["2026-05-12T10:00:00Z"]]) + end + + it "still parses ordinary name:requirement entries" do + parser = legacy_parser_class.new + + expect(parser.send(:parse_dependency, "myrack:>= 1.0")).to eq(["myrack", [">= 1.0"]]) + end + + it "keeps parse_dependency private" do + parser = legacy_parser_class.new + + expect { parser.parse_dependency("created_at:x") }.to raise_error(NoMethodError, /private method/) + end +end diff --git a/spec/bundler/bundler/rubygems_integration_spec.rb b/spec/bundler/bundler/rubygems_integration_spec.rb index 26cbaa630b..a2c63a7ca0 100644 --- a/spec/bundler/bundler/rubygems_integration_spec.rb +++ b/spec/bundler/bundler/rubygems_integration_spec.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true RSpec.describe Bundler::RubygemsIntegration do - it "uses the same chdir lock as rubygems" do - expect(Bundler.rubygems.ext_lock).to eq(Gem::Ext::Builder::CHDIR_MONITOR) - end - context "#validate" do let(:spec) do Gem::Specification.new do |s| @@ -15,14 +11,14 @@ RSpec.describe Bundler::RubygemsIntegration do end subject { Bundler.rubygems.validate(spec) } - it "validates with packaging mode disabled" do - expect(spec).to receive(:validate).with(false) + it "validates for resolution" do + expect(spec).to receive(:validate_for_resolution) subject end context "with an invalid spec" do before do - expect(spec).to receive(:validate).with(false). + expect(spec).to receive(:validate_for_resolution). and_raise(Gem::InvalidSpecificationException.new("TODO is not an author")) end @@ -34,69 +30,97 @@ RSpec.describe Bundler::RubygemsIntegration do end end - describe "#configuration" do - it "handles Gem::SystemExitException errors" do - allow(Gem).to receive(:configuration) { raise Gem::SystemExitException.new(1) } - expect { Bundler.rubygems.configuration }.to raise_error(Gem::SystemExitException) - end - end - describe "#download_gem" do let(:bundler_retry) { double(Bundler::Retry) } - let(:retry) { double("Bundler::Retry") } - let(:uri) { URI.parse("https://foo.bar") } - let(:path) { Gem.path.first } + let(:cache_dir) { "#{Gem.path.first}/cache" } let(:spec) do - spec = Bundler::RemoteSpecification.new("Foo", Gem::Version.new("2.5.2"), - Gem::Platform::RUBY, nil) + spec = Gem::Specification.new("Foo", Gem::Version.new("2.5.2")) spec.remote = Bundler::Source::Rubygems::Remote.new(uri.to_s) spec end let(:fetcher) { double("gem_remote_fetcher") } - it "successfully downloads gem with retries" do - expect(Bundler.rubygems).to receive(:gem_remote_fetcher).and_return(fetcher) - expect(fetcher).to receive(:headers=).with("X-Gemfile-Source" => "https://foo.bar") - expect(Bundler::Retry).to receive(:new).with("download gem from #{uri}/"). - and_return(bundler_retry) - expect(bundler_retry).to receive(:attempts).and_yield - expect(fetcher).to receive(:download).with(spec, uri, path) + context "when uri is public" do + let(:uri) { Gem::URI.parse("https://foo.bar") } + + it "successfully downloads gem with retries" do + 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(:cache_update_path) - Bundler.rubygems.download_gem(spec, uri, path) + Bundler.rubygems.download_gem(spec, uri, cache_dir, fetcher) + end + end + + context "when uri contains userinfo part" do + let(:uri) { Gem::URI.parse("https://#{userinfo}@foo.bar") } + + context "with user and password" do + let(:userinfo) { "user:password" } + + it "successfully downloads gem with retries with filtered log" do + expect(Bundler::Retry).to receive(:new).with("download gem from https://user:REDACTED@foo.bar/"). + and_return(bundler_retry) + expect(bundler_retry).to receive(:attempts).and_yield + expect(fetcher).to receive(:cache_update_path) + + Bundler.rubygems.download_gem(spec, uri, cache_dir, fetcher) + end + end + + context "with token [as user]" do + let(:userinfo) { "token" } + + it "successfully downloads gem with retries with filtered log" do + expect(Bundler::Retry).to receive(:new).with("download gem from https://REDACTED@foo.bar/"). + and_return(bundler_retry) + expect(bundler_retry).to receive(:attempts).and_yield + expect(fetcher).to receive(:cache_update_path) + + Bundler.rubygems.download_gem(spec, uri, cache_dir, fetcher) + end + end end end describe "#fetch_all_remote_specs" do - let(:uri) { URI("https://example.com") } + let(: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) } + let(:orig_uri) { Gem::URI("http://zombo.com") } + let(:remote_with_mirror) { double("remote", uri: uri, original_uri: orig_uri) } it "sets the 'X-Gemfile-Source' header containing the original source" do - expect(Bundler.rubygems).to receive(:gem_remote_fetcher).twice.and_return(fetcher) - expect(fetcher).to receive(:headers=).with("X-Gemfile-Source" => "http://zombo.com").twice expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(specs_response) expect(fetcher).to receive(:fetch_path).with(uri + "prerelease_specs.4.8.gz").and_return(prerelease_specs_response) - result = Bundler.rubygems.fetch_all_remote_specs(remote_with_mirror) + result = Bundler.rubygems.fetch_all_remote_specs(remote_with_mirror, fetcher) expect(result).to eq(%w[specs prerelease_specs]) end end context "when there is no rubygems source mirror set" do - let(:remote_no_mirror) { double("remote", :uri => uri, :original_uri => nil) } + let(:remote_no_mirror) { double("remote", uri: uri, original_uri: nil) } it "does not set the 'X-Gemfile-Source' header" do - expect(Bundler.rubygems).to receive(:gem_remote_fetcher).twice.and_return(fetcher) - expect(fetcher).to_not receive(:headers=) expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(specs_response) expect(fetcher).to receive(:fetch_path).with(uri + "prerelease_specs.4.8.gz").and_return(prerelease_specs_response) - result = Bundler.rubygems.fetch_all_remote_specs(remote_no_mirror) + result = Bundler.rubygems.fetch_all_remote_specs(remote_no_mirror, fetcher) expect(result).to eq(%w[specs prerelease_specs]) end end + + context "when loading an unexpected class" do + let(:remote_no_mirror) { double("remote", uri: uri, original_uri: nil) } + let(:unexpected_specs_response) { Marshal.dump(3) } + + it "raises a MarshalError error" do + expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(unexpected_specs_response) + expect { Bundler.rubygems.fetch_all_remote_specs(remote_no_mirror, fetcher) }.to raise_error(Bundler::MarshalError, /unexpected class/i) + end + end end end diff --git a/spec/bundler/bundler/settings/validator_spec.rb b/spec/bundler/bundler/settings/validator_spec.rb index e4ffd89435..b252ba59a0 100644 --- a/spec/bundler/bundler/settings/validator_spec.rb +++ b/spec/bundler/bundler/settings/validator_spec.rb @@ -44,14 +44,14 @@ RSpec.describe Bundler::Settings::Validator do validate!("without", "b:c", "BUNDLE_WITH" => "a") end.not_to raise_error - expect { validate!("with", "b:c", "BUNDLE_WITHOUT" => "c:d") }.to raise_error Bundler::InvalidOption, strip_whitespace(<<-EOS).strip + expect { validate!("with", "b:c", "BUNDLE_WITHOUT" => "c:d") }.to raise_error Bundler::InvalidOption, <<~EOS.strip Setting `with` to "b:c" failed: - a group cannot be in both `with` & `without` simultaneously - `without` is current set to [:c, :d] - the `c` groups conflict EOS - expect { validate!("without", "b:c", "BUNDLE_WITH" => "c:d") }.to raise_error Bundler::InvalidOption, strip_whitespace(<<-EOS).strip + expect { validate!("without", "b:c", "BUNDLE_WITH" => "c:d") }.to raise_error Bundler::InvalidOption, <<~EOS.strip Setting `without` to "b:c" failed: - a group cannot be in both `with` & `without` simultaneously - `with` is current set to [:c, :d] @@ -74,7 +74,7 @@ RSpec.describe Bundler::Settings::Validator do describe "#fail!" do it "raises with a helpful message" do - expect { subject.fail!("key", "value", "reason1", "reason2") }.to raise_error Bundler::InvalidOption, strip_whitespace(<<-EOS).strip + expect { subject.fail!("key", "value", "reason1", "reason2") }.to raise_error Bundler::InvalidOption, <<~EOS.strip Setting `key` to "value" failed: - rule description - reason1 diff --git a/spec/bundler/bundler/settings_spec.rb b/spec/bundler/bundler/settings_spec.rb index 2a285fdcf3..5e1aaaa555 100644 --- a/spec/bundler/bundler/settings_spec.rb +++ b/spec/bundler/bundler/settings_spec.rb @@ -6,12 +6,18 @@ 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 + context "root is nil" 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") + before do + allow(Pathname).to receive(:new).and_call_original + allow(Pathname).to receive(:new).with(".bundle").and_return home(".bundle") + end + + it "works" do + subject.set_local("foo", "bar") + + expect(subject["foo"]).to eq("bar") end end end @@ -27,7 +33,7 @@ RSpec.describe Bundler::Settings do "gem.mit" => "false", "gem.test" => "minitest", "thingy" => <<-EOS.tr("\n", " "), ---asdf --fdsa --ty=oh man i hope this doesnt break bundler because +--asdf --fdsa --ty=oh man i hope this doesn't break bundler because that would suck --ehhh=oh geez it looks like i might have broken bundler somehow --very-important-option=DontDeleteRoo --very-important-option=DontDeleteRoo @@ -64,13 +70,10 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow 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(Bundler).to receive(:tmp).twice.and_raise(Errno::EROFS, "Read-only file system @ dir_s_mkdir - /tmp/bundler") + it "does not raise" do + expect(Bundler.rubygems).to receive(:user_home).twice.and_return(nil) - expect(subject.send(:global_config_file)).to be_nil - end + expect(subject.send(:global_config_file)).to be_nil end end end @@ -116,23 +119,31 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow settings.set_local :ssl_verify_mode, "1" expect(settings[:ssl_verify_mode]).to be 1 end + + it "coerces cooldown to integer" do + settings.set_local :cooldown, "7" + expect(settings[:cooldown]).to be 7 + end end - context "when it's not possible to write to the file" do + context "when it's not possible to create the settings directory" do it "raises an PermissionError with explanation" do - expect(::Bundler::FileUtils).to receive(:mkdir_p).with(settings.send(:local_config_file).dirname). - and_raise(Errno::EACCES) + settings_dir = settings.send(:local_config_file).dirname + expect(::Bundler::FileUtils).to receive(:mkdir_p).with(settings_dir). + and_raise(Errno::EACCES.new(settings_dir.to_s)) expect { settings.set_local :frozen, "1" }. - to raise_error(Bundler::PermissionError, /config/) + to raise_error(Bundler::PermissionError, /#{settings_dir}/) end end end describe "#temporary" do it "reset after used" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + Bundler.settings.set_command_option :no_install, true - Bundler.settings.temporary(:no_install => false) do + Bundler.settings.temporary(no_install: false) do expect(Bundler.settings[:no_install]).to eq false end @@ -148,23 +159,24 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow context "when called without a block" do it "leaves the setting changed" do - Bundler.settings.temporary(:foo => :random) + Bundler.settings.temporary(foo: :random) expect(Bundler.settings[:foo]).to eq "random" end it "returns nil" do - expect(Bundler.settings.temporary(:foo => :bar)).to be_nil + expect(Bundler.settings.temporary(foo: :bar)).to be_nil end end end describe "#set_global" do - context "when it's not possible to write to the file" do + context "when it's not possible to write to create the settings directory" do it "raises an PermissionError with explanation" do - expect(::Bundler::FileUtils).to receive(:mkdir_p).with(settings.send(:global_config_file).dirname). - and_raise(Errno::EACCES) + settings_dir = settings.send(:global_config_file).dirname + expect(::Bundler::FileUtils).to receive(:mkdir_p).with(settings_dir). + and_raise(Errno::EACCES.new(settings_dir.to_s)) expect { settings.set_global(:frozen, "1") }. - to raise_error(Bundler::PermissionError, %r{\.bundle/config}) + to raise_error(Bundler::PermissionError, /#{settings_dir}/) end end end @@ -180,7 +192,7 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow end describe "#mirror_for" do - let(:uri) { URI("https://rubygems.org/") } + let(:uri) { Gem::URI("https://rubygems.org/") } context "with no configured mirror" do it "returns the original URI" do @@ -193,7 +205,7 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow end context "with a configured mirror" do - let(:mirror_uri) { URI("https://rubygems-mirror.org/") } + let(:mirror_uri) { Gem::URI("https://example-mirror.rubygems.org/") } before { settings.set_local "mirror.https://rubygems.org/", mirror_uri.to_s } @@ -214,7 +226,7 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow end context "with a file URI" do - let(:mirror_uri) { URI("file:/foo/BAR/baz/qUx/") } + let(:mirror_uri) { Gem::URI("file:/foo/BAR/baz/qUx/") } it "returns the mirror URI" do expect(settings.mirror_for(uri)).to eq(mirror_uri) @@ -232,7 +244,7 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow end describe "#credentials_for" do - let(:uri) { URI("https://gemserver.example.org/") } + let(:uri) { Gem::URI("https://gemserver.example.org/") } let(:credentials) { "username:password" } context "with no configured credentials" do @@ -270,12 +282,12 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow end it "normalizes HTTP URIs in mirror configuration" do - settings.set_local "mirror.http://rubygems.org", "http://rubygems-mirror.org" + settings.set_local "mirror.http://rubygems.org", "http://example-mirror.rubygems.org" expect(settings.all).to include("mirror.http://rubygems.org/") end it "normalizes HTTPS URIs in mirror configuration" do - settings.set_local "mirror.https://rubygems.org", "http://rubygems-mirror.org" + settings.set_local "mirror.https://rubygems.org", "http://example-mirror.rubygems.org" expect(settings.all).to include("mirror.https://rubygems.org/") end @@ -290,9 +302,9 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow end it "reads older keys without trailing slashes" do - settings.set_local "mirror.https://rubygems.org", "http://rubygems-mirror.org" + settings.set_local "mirror.https://rubygems.org", "http://example-mirror.rubygems.org" expect(settings.mirror_for("https://rubygems.org/")).to eq( - URI("http://rubygems-mirror.org/") + Gem::URI("http://example-mirror.rubygems.org/") ) end @@ -310,19 +322,59 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow 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") + it "converts older keys without double underscore" do + bundle_config("BUNDLE_MY__PERSONAL.MYRACK" => "~/Work/git/myrack") + expect(settings["my.personal.myrack"]).to eq("~/Work/git/myrack") + end + + it "converts older keys without trailing slashes and double underscore" do + bundle_config("BUNDLE_MIRROR__HTTPS://RUBYGEMS.ORG" => "http://example-mirror.rubygems.org") + expect(settings["mirror.https://rubygems.org/"]).to eq("http://example-mirror.rubygems.org") 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") + it "ignores commented out keys" do + create_file bundled_app(".bundle/config"), <<~C + # BUNDLE_MY-PERSONAL-SERVER__ORG: my-personal-server.org + C + + expect(Bundler.ui).not_to receive(:warn) + expect(settings.all).to be_empty + end + + it "converts older keys with dashes" do + bundle_config("BUNDLE_MY-PERSONAL-SERVER__ORG" => "my-personal-server.org") + expect(Bundler.ui).to receive(:warn).with( + "Your #{bundled_app(".bundle/config")} config includes `BUNDLE_MY-PERSONAL-SERVER__ORG`, which contains the dash character (`-`).\n" \ + "This is deprecated, because configuration through `ENV` should be possible, but `ENV` keys cannot include dashes.\n" \ + "Please edit #{bundled_app(".bundle/config")} and replace any dashes in configuration keys with a triple underscore (`___`)." + ) + expect(settings["my-personal-server.org"]).to eq("my-personal-server.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") + bundle_config("BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/" => "http://example-mirror.rubygems.org") + expect(settings["mirror.https://rubygems.org/"]).to eq("http://example-mirror.rubygems.org") + end + end + + describe "default_cli_command validation" do + it "accepts 'install' as a valid value" do + expect { settings.set_local("default_cli_command", "install") }.not_to raise_error + end + + it "accepts 'cli_help' as a valid value" do + expect { settings.set_local("default_cli_command", "cli_help") }.not_to raise_error + end + + it "rejects invalid values" do + expect { settings.set_local("default_cli_command", "invalid") }.to raise_error( + Bundler::InvalidOption, + /Setting `default_cli_command` to "invalid" failed:\n - default_cli_command must be either 'install' or 'cli_help'\n - must be one of: install, cli_help/ + ) + end + + it "accepts nil values" do + expect { settings.set_local("default_cli_command", nil) }.not_to raise_error end end end diff --git a/spec/bundler/bundler/shared_helpers_spec.rb b/spec/bundler/bundler/shared_helpers_spec.rb index 4530a9a5cd..41115aa667 100644 --- a/spec/bundler/bundler/shared_helpers_spec.rb +++ b/spec/bundler/bundler/shared_helpers_spec.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true RSpec.describe Bundler::SharedHelpers do - let(:ext_lock_double) { double(:ext_lock) } - before do - allow(Bundler.rubygems).to receive(:ext_lock).and_return(ext_lock_double) - allow(ext_lock_double).to receive(:synchronize) {|&block| block.call } + pwd_stub end + let(:pwd_stub) { allow(subject).to receive(:pwd).and_return(bundled_app) } + subject { Bundler::SharedHelpers } describe "#default_gemfile" do @@ -60,7 +59,7 @@ RSpec.describe Bundler::SharedHelpers do before { allow(subject).to receive(:default_gemfile).and_return(gemfile_path) } - it "returns the lock file path" do + it "returns the lockfile path" do expect(subject.default_lockfile).to eq(expected_lockfile_path) end end @@ -77,7 +76,7 @@ RSpec.describe Bundler::SharedHelpers do let(:global_rubygems_dir) { Pathname.new(bundled_app) } before do - Dir.mkdir ".bundle" + Dir.mkdir bundled_app(".bundle") allow(Bundler.rubygems).to receive(:user_home).and_return(global_rubygems_dir) end @@ -91,7 +90,7 @@ RSpec.describe Bundler::SharedHelpers do let(:expected_bundle_dir_path) { Pathname.new("#{bundled_app}/.bundle") } before do - Dir.mkdir ".bundle" + Dir.mkdir bundled_app(".bundle") allow(Bundler.rubygems).to receive(:user_home).and_return(global_rubygems_dir) end @@ -109,7 +108,8 @@ RSpec.describe Bundler::SharedHelpers do 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") } + before { FileUtils.touch(bundled_app_gemfile) } + after { FileUtils.rm(bundled_app_gemfile) } it "returns path of the bundle Gemfile" do expect(subject.in_bundle?).to eq("#{bundled_app}/Gemfile") @@ -147,22 +147,24 @@ RSpec.describe Bundler::SharedHelpers do describe "#chdir" do let(:op_block) { proc { Dir.mkdir "nested_dir" } } - before { Dir.mkdir "chdir_test_dir" } + before { Dir.mkdir bundled_app("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 + subject.chdir(bundled_app("chdir_test_dir"), &op_block) + expect(bundled_app("chdir_test_dir/nested_dir")).to exist end end describe "#pwd" do + let(:pwd_stub) { nil } + it "returns the current absolute path" do - expect(subject.pwd).to eq(bundled_app) + expect(subject.pwd).to eq(git_root.to_s) end end describe "#with_clean_git_env" do - let(:with_clean_git_env_block) { proc { Dir.mkdir "with_clean_git_env_test_dir" } } + let(:with_clean_git_env_block) { proc { Dir.mkdir bundled_app("with_clean_git_env_test_dir") } } before do ENV["GIT_DIR"] = "ORIGINAL_ENV_GIT_DIR" @@ -171,20 +173,20 @@ RSpec.describe Bundler::SharedHelpers do 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 + expect(bundled_app("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? + Dir.mkdir bundled_app("git_dir_test_dir") unless ENV["GIT_DIR"].nil? + Dir.mkdir bundled_app("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 + expect(bundled_app("git_dir_test_dir")).to_not exist + expect(bundled_app("git_work_tree_test_dir")).to_not exist end end @@ -224,7 +226,7 @@ RSpec.describe Bundler::SharedHelpers do end shared_examples_for "ENV['PATH'] gets set correctly" do - before { Dir.mkdir ".bundle" } + before { Dir.mkdir bundled_app(".bundle") } it "ensures bundle bin path is in ENV['PATH']" do subject.set_bundle_environment @@ -236,7 +238,14 @@ RSpec.describe Bundler::SharedHelpers do shared_examples_for "ENV['RUBYOPT'] gets set correctly" do it "ensures -rbundler/setup is at the beginning of ENV['RUBYOPT']" do subject.set_bundle_environment - expect(ENV["RUBYOPT"].split(" ")).to start_with("-r#{lib_dir}/bundler/setup") + expect(ENV["RUBYOPT"].split(" ")).to start_with("-r#{install_path}/bundler/setup") + end + end + + shared_examples_for "ENV['BUNDLER_SETUP'] gets set correctly" do + it "ensures bundler/setup is set in ENV['BUNDLER_SETUP']" do + subject.set_bundle_environment + expect(ENV["BUNDLER_SETUP"]).to eq("#{source_lib_dir}/bundler/setup") end end @@ -244,13 +253,12 @@ RSpec.describe Bundler::SharedHelpers do let(:ruby_lib_path) { "stubbed_ruby_lib_dir" } before do - allow(Bundler::SharedHelpers).to receive(:bundler_ruby_lib).and_return(ruby_lib_path) + allow(subject).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) + expect(rubylib).to include(ruby_lib_path) end end @@ -263,19 +271,18 @@ RSpec.describe Bundler::SharedHelpers do end it "ignores if bundler_ruby_lib is same as rubylibdir" do - allow(Bundler::SharedHelpers).to receive(:bundler_ruby_lib).and_return(RbConfig::CONFIG["rubylibdir"]) + allow(subject).to receive(:bundler_ruby_lib).and_return(RbConfig::CONFIG["rubylibdir"]) subject.set_bundle_environment - paths = (ENV["RUBYLIB"]).split(File::PATH_SEPARATOR) - expect(paths.count(RbConfig::CONFIG["rubylibdir"])).to eq(0) + expect(rubylib.count(RbConfig::CONFIG["rubylibdir"])).to eq(0) end it "exits if bundle path contains the unix-like path separator" do if Gem.respond_to?(:path_separator) allow(Gem).to receive(:path_separator).and_return(":") else - stub_const("File::PATH_SEPARATOR", ":".freeze) + stub_const("File::PATH_SEPARATOR", ":") end allow(Bundler).to receive(:bundle_path) { Pathname.new("so:me/dir/bin") } expect { subject.send(:validate_bundle_path) }.to raise_error( @@ -347,25 +354,46 @@ RSpec.describe Bundler::SharedHelpers do it "ENV['PATH'] should only contain one instance of bundle bin path" do subject.set_bundle_environment - paths = (ENV["PATH"]).split(File::PATH_SEPARATOR) + 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") } + context "when bundler install path is standard" do + let(:install_path) { source_lib_dir } - it_behaves_like "ENV['RUBYOPT'] gets set correctly" - end + context "ENV['RUBYOPT'] does not exist" do + before { ENV.delete("RUBYOPT") } - context "ENV['RUBYOPT'] exists without -rbundler/setup" do - before { ENV["RUBYOPT"] = "-I/some_app_path/lib" } + it_behaves_like "ENV['RUBYOPT'] gets set correctly" + end - it_behaves_like "ENV['RUBYOPT'] gets set correctly" + context "ENV['RUBYOPT'] exists without -rbundler/setup" do + before { ENV["RUBYOPT"] = "-I/some_app_path/lib" } + + it_behaves_like "ENV['RUBYOPT'] gets set correctly" + end + + context "ENV['RUBYOPT'] exists and contains -rbundler/setup" do + before { ENV["RUBYOPT"] = "-rbundler/setup" } + + it_behaves_like "ENV['RUBYOPT'] gets set correctly" + end end - context "ENV['RUBYOPT'] exists and contains -rbundler/setup" do - before { ENV["RUBYOPT"] = "-rbundler/setup" } + context "when bundler install path contains special characters" do + let(:install_path) { "/opt/ruby3.3.0-preview2/lib/ruby/3.3.0+0" } + + before do + ENV["RUBYOPT"] = "-r#{install_path}/bundler/setup" + allow(File).to receive(:expand_path).and_return("#{install_path}/bundler/setup") + allow(Gem).to receive(:bin_path).and_return("#{install_path}/bundler/setup") + end + + it "ensures -rbundler/setup is not duplicated" do + subject.set_bundle_environment + expect(ENV["RUBYOPT"].split(" ").grep(%r{-r.*/bundler/setup}).length).to eq(1) + end it_behaves_like "ENV['RUBYOPT'] gets set correctly" end @@ -395,7 +423,7 @@ RSpec.describe Bundler::SharedHelpers do it "sets BUNDLE_BIN_PATH to the bundle executable file" do subject.set_bundle_environment bin_path = ENV["BUNDLE_BIN_PATH"] - expect(bin_path).to eq(bindir.join("bundle").to_s) + expect(bin_path).to eq(exedir.join("bundle").to_s) expect(File.exist?(bin_path)).to be true end end @@ -411,8 +439,7 @@ RSpec.describe Bundler::SharedHelpers do 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) + expect(rubylib.count(ruby_lib_path)).to eq(1) end end end @@ -422,13 +449,13 @@ RSpec.describe Bundler::SharedHelpers 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 + subject.filesystem_access(bundled_app("test_dir"), &file_op_block) + expect(bundled_app("test_dir")).to exist end end context "system throws Errno::EACESS" do - let(:file_op_block) { proc {|_path| raise Errno::EACCES } } + let(:file_op_block) { proc {|_path| raise Errno::EACCES.new("/path") } } it "raises a PermissionError" do expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error( @@ -483,33 +510,9 @@ RSpec.describe Bundler::SharedHelpers do it "raises a GenericSystemCallError" do expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error( - Bundler::GenericSystemCallError, /error accessing.+underlying.+Shields down/m + Bundler::GenericSystemCallError, /error creating.+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 index c18490233d..1f10ca4b07 100644 --- a/spec/bundler/bundler/source/git/git_proxy_spec.rb +++ b/spec/bundler/bundler/source/git/git_proxy_spec.rb @@ -2,40 +2,98 @@ RSpec.describe Bundler::Source::Git::GitProxy do let(:path) { Pathname("path") } - let(:uri) { "https://github.com/bundler/bundler.git" } - let(:ref) { "HEAD" } + let(:uri) { "https://github.com/ruby/rubygems.git" } + let(:ref) { nil } + let(:branch) { nil } + let(:tag) { nil } + let(:options) { { "ref" => ref, "branch" => branch, "tag" => tag }.compact } let(:revision) { nil } let(:git_source) { nil } - subject { described_class.new(path, uri, ref, revision, git_source) } + let(:clone_result) { double(Process::Status, success?: true) } + let(:fail_result) { double(Process::Status, success?: false) } + let(:base_clone_args) { ["clone", "--bare", "--no-hardlinks", "--quiet", "--no-tags", "--depth", "1", "--single-branch"] } + let(:base_fetch_args) { ["fetch", "--force", "--quiet", "--no-tags", "--depth", "1"] } + subject(:git_proxy) { described_class.new(path, uri, options, revision, git_source) } + + context "with explicit ref" do + context "with branch only" do + let(:branch) { "main" } + it "sets explicit ref to branch" do + expect(git_proxy.explicit_ref).to eq(branch) + end + end + + context "with ref only" do + let(:ref) { "HEAD" } + it "sets explicit ref to ref" do + expect(git_proxy.explicit_ref).to eq(ref) + end + end + + context "with tag only" do + let(:tag) { "v1.0" } + it "sets explicit ref to ref" do + expect(git_proxy.explicit_ref).to eq(tag) + end + end + + context "with tag and branch" do + let(:tag) { "v1.0" } + let(:branch) { "main" } + it "raises error" do + expect { git_proxy }.to raise_error(Bundler::Source::Git::AmbiguousGitReference) + end + end + + context "with tag and ref" do + let(:tag) { "v1.0" } + let(:ref) { "HEAD" } + it "raises error" do + expect { git_proxy }.to raise_error(Bundler::Source::Git::AmbiguousGitReference) + end + end + + context "with branch and ref" do + let(:branch) { "main" } + let(:ref) { "HEAD" } + it "honors ref over branch" do + expect(git_proxy.explicit_ref).to eq(ref) + end + end + end context "with configured credentials" do it "adds username and password to URI" do Bundler.settings.temporary(uri => "u:p") do - expect(subject).to receive(:git_retry).with(match("https://u:p@github.com/bundler/bundler.git")) + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", "https://u:p@github.com/ruby/rubygems.git", path.to_s], nil).and_return(["", "", clone_result]) subject.checkout end end it "adds username and password to URI for host" do Bundler.settings.temporary("github.com" => "u:p") do - expect(subject).to receive(:git_retry).with(match("https://u:p@github.com/bundler/bundler.git")) + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", "https://u:p@github.com/ruby/rubygems.git", path.to_s], nil).and_return(["", "", clone_result]) subject.checkout end end it "does not add username and password to mismatched URI" do - Bundler.settings.temporary("https://u:p@github.com/bundler/bundler-mismatch.git" => "u:p") do - expect(subject).to receive(:git_retry).with(match(uri)) + Bundler.settings.temporary("https://u:p@github.com/ruby/rubygems-mismatch.git" => "u:p") do + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", uri, path.to_s], nil).and_return(["", "", clone_result]) subject.checkout end end it "keeps original userinfo" do Bundler.settings.temporary("github.com" => "u:p") do - 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 + original = "https://orig:info@github.com/ruby/rubygems.git" + git_proxy = described_class.new(Pathname("path"), original, options) + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", original, path.to_s], nil).and_return(["", "", clone_result]) + git_proxy.checkout end end end @@ -43,46 +101,46 @@ RSpec.describe Bundler::Source::Git::GitProxy do describe "#version" do context "with a normal version number" do before do - expect(subject).to receive(:git).with("--version"). + expect(described_class).to receive(:full_version). and_return("git version 1.2.3") end it "returns the git version number" do - expect(subject.version).to eq("1.2.3") + expect(git_proxy.version).to eq("1.2.3") end it "does not raise an error when passed into Gem::Version.create" do - expect { Gem::Version.create subject.version }.not_to raise_error + expect { Gem::Version.create git_proxy.version }.not_to raise_error end end context "with a OSX version number" do before do - expect(subject).to receive(:git).with("--version"). + expect(described_class).to receive(:full_version). and_return("git version 1.2.3 (Apple Git-BS)") end it "strips out OSX specific additions in the version string" do - expect(subject.version).to eq("1.2.3") + expect(git_proxy.version).to eq("1.2.3") end it "does not raise an error when passed into Gem::Version.create" do - expect { Gem::Version.create subject.version }.not_to raise_error + expect { Gem::Version.create git_proxy.version }.not_to raise_error end end context "with a msysgit version number" do before do - expect(subject).to receive(:git).with("--version"). + expect(described_class).to receive(:full_version). and_return("git version 1.2.3.msysgit.0") end it "strips out msysgit specific additions in the version string" do - expect(subject.version).to eq("1.2.3") + expect(git_proxy.version).to eq("1.2.3") end it "does not raise an error when passed into Gem::Version.create" do - expect { Gem::Version.create subject.version }.not_to raise_error + expect { Gem::Version.create git_proxy.version }.not_to raise_error end end end @@ -90,58 +148,206 @@ RSpec.describe Bundler::Source::Git::GitProxy do describe "#full_version" do context "with a normal version number" do before do - expect(subject).to receive(:git).with("--version"). - and_return("git version 1.2.3") + status = double("success?" => true) + expect(Open3).to receive(:capture3).with("git", "--version"). + and_return(["git version 1.2.3", "", status]) end it "returns the git version number" do - expect(subject.full_version).to eq("1.2.3") + expect(git_proxy.full_version).to eq("1.2.3") end end context "with a OSX version number" do before do - expect(subject).to receive(:git).with("--version"). - and_return("git version 1.2.3 (Apple Git-BS)") + status = double("success?" => true) + expect(Open3).to receive(:capture3).with("git", "--version"). + and_return(["git version 1.2.3 (Apple Git-BS)", "", status]) end it "does not strip out OSX specific additions in the version string" do - expect(subject.full_version).to eq("1.2.3 (Apple Git-BS)") + expect(git_proxy.full_version).to eq("1.2.3 (Apple Git-BS)") end end context "with a msysgit version number" do before do - expect(subject).to receive(:git).with("--version"). - and_return("git version 1.2.3.msysgit.0") + status = double("success?" => true) + expect(Open3).to receive(:capture3).with("git", "--version"). + and_return(["git version 1.2.3.msysgit.0", "", status]) end it "does not strip out msysgit specific additions in the version string" do - expect(subject.full_version).to eq("1.2.3.msysgit.0") + expect(git_proxy.full_version).to eq("1.2.3.msysgit.0") end end end - describe "#copy_to" do - let(:destination) { tmpdir("copy_to_path") } - let(:submodules) { false } - - context "when given a SHA as a revision" do - let(:revision) { "abcd" * 10 } - let(:command) { "reset --hard #{revision}" } - - it "fails gracefully when resetting to the revision fails" do - expect(subject).to receive(:git_retry).with(start_with("clone ")) { destination.mkpath } - expect(subject).to receive(:git_retry).with(start_with("fetch ")) - expect(subject).to receive(:git).with(command).and_raise(Bundler::Source::Git::GitCommandError, command) - expect(subject).not_to receive(:git) - - expect { subject.copy_to(destination, submodules) }. - to raise_error( - Bundler::Source::Git::MissingGitRevisionError, - "Git error: command `git #{command}` in directory #{destination} has failed.\n" \ - "Revision #{revision} does not exist in the repository #{uri}. Maybe you misspelled it?" \ - ) + it "doesn't allow arbitrary code execution through Gemfile uris with a leading dash" do + gemfile <<~G + gem "poc", git: "-u./pay:load.sh" + G + + file = bundled_app("pay:load.sh") + + create_file file, <<~RUBY + #!/bin/sh + + touch #{bundled_app("canary")} + RUBY + + FileUtils.chmod("+x", file) + + bundle :lock, raise_on_error: false + + expect(Pathname.new(bundled_app("canary"))).not_to exist + end + + context "URI is HTTP" do + let(:uri) { "http://github.com/ruby/rubygems.git" } + let(:clone_args_without_depth) { ["clone", "--bare", "--no-hardlinks", "--quiet", "--no-tags", "--single-branch"] } + + it "retries clone without --depth when dumb http transport fails" do + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", uri, path.to_s], nil).and_return(["", "dumb http transport does not support shallow capabilities", fail_result]) + expect(git_proxy).to receive(:capture).with([*clone_args_without_depth, "--", uri, path.to_s], nil).and_return(["", "", clone_result]) + + subject.checkout + end + end + + describe "#installed_to?" do + let(:destination) { "install/dir" } + let(:destination_dir_exists) { true } + let(:children) { ["gem.gemspec", "README.me", ".git", "Rakefile"] } + + before do + allow(Dir).to receive(:exist?).with(destination).and_return(destination_dir_exists) + allow(Dir).to receive(:children).with(destination).and_return(children) + end + + context "when destination dir exists with children other than just .git" do + it "returns true" do + expect(git_proxy.installed_to?(destination)).to be true + end + end + + context "when destination dir does not exist" do + let(:destination_dir_exists) { false } + + it "returns false" do + expect(git_proxy.installed_to?(destination)).to be false + end + end + + context "when destination dir is empty" do + let(:children) { [] } + + it "returns false" do + expect(git_proxy.installed_to?(destination)).to be false + end + end + + context "when destination dir has only .git directory" do + let(:children) { [".git"] } + + it "returns false" do + expect(git_proxy.installed_to?(destination)).to be false + end + end + end + + describe "#checkout" do + context "when the repository isn't cloned" do + before do + allow(path).to receive(:exist?).and_return(false) + end + + it "clones the repository" do + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", uri, path.to_s], nil).and_return(["", "", clone_result]) + subject.checkout + end + end + + context "when the repository is cloned" do + before do + allow(path).to receive(:exist?).and_return(true) + end + + context "with a locked revision" do + let(:revision) { Digest::SHA1.hexdigest("ruby") } + + context "when the revision exists locally" do + it "uses the cached revision" do + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:git).with("cat-file", "-e", revision, dir: path).and_return(true) + subject.checkout + end + end + + context "when the revision doesn't exist locally" do + it "fetches the specific revision" do + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:git).with("cat-file", "-e", revision, dir: path).and_raise(Bundler::GitError) + expect(git_proxy).to receive(:capture).with(["fetch", "--force", "--quiet", "--no-tags", "--depth", "1", "--", uri, "#{revision}:refs/#{revision}-sha"], path).and_return(["", "", clone_result]) + subject.checkout + end + end + end + + context "with no explicit ref" do + it "fetches the HEAD revision" do + parsed_revision = Digest::SHA1.hexdigest("ruby") + allow(git_proxy).to receive(:git_local).with("rev-parse", "--abbrev-ref", "HEAD", dir: path).and_return(parsed_revision) + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:capture).with(["fetch", "--force", "--quiet", "--no-tags", "--depth", "1", "--", uri, "refs/heads/#{parsed_revision}:refs/heads/#{parsed_revision}"], path).and_return(["", "", clone_result]) + subject.checkout + end + end + + context "with a commit ref" do + let(:ref) { Digest::SHA1.hexdigest("ruby") } + + context "when the revision exists locally" do + it "uses the cached revision" do + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:git).with("cat-file", "-e", ref, dir: path).and_return(true) + subject.checkout + end + end + + context "when the revision doesn't exist locally" do + it "fetches the specific revision" do + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:git).with("cat-file", "-e", ref, dir: path).and_raise(Bundler::GitError) + expect(git_proxy).to receive(:capture).with(["fetch", "--force", "--quiet", "--no-tags", "--depth", "1", "--", uri, "#{ref}:refs/#{ref}-sha"], path).and_return(["", "", clone_result]) + subject.checkout + end + end + end + + context "with a non-commit ref" do + let(:ref) { "HEAD" } + + it "fetches all revisions" do + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:capture).with(["fetch", "--force", "--quiet", "--no-tags", "--", uri, "refs/*:refs/*"], path).and_return(["", "", clone_result]) + subject.checkout + end + end + + context "URI is HTTP" do + let(:uri) { "http://github.com/ruby/rubygems.git" } + + it "retries fetch without --depth when dumb http transport fails" do + parsed_revision = Digest::SHA1.hexdigest("ruby") + allow(git_proxy).to receive(:git_local).with("rev-parse", "--abbrev-ref", "HEAD", dir: path).and_return(parsed_revision) + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:capture).with([*base_fetch_args, "--", uri, "refs/heads/#{parsed_revision}:refs/heads/#{parsed_revision}"], path).and_return(["", "dumb http transport does not support shallow capabilities", fail_result]) + expect(git_proxy).to receive(:capture).with(["fetch", "--force", "--quiet", "--no-tags", "--", uri, "refs/heads/#{parsed_revision}:refs/heads/#{parsed_revision}"], path).and_return(["", "", clone_result]) + subject.checkout + end end end end diff --git a/spec/bundler/bundler/source/git_spec.rb b/spec/bundler/bundler/source/git_spec.rb index f7475a35aa..14e91c6bdc 100644 --- a/spec/bundler/bundler/source/git_spec.rb +++ b/spec/bundler/bundler/source/git_spec.rb @@ -14,14 +14,109 @@ RSpec.describe Bundler::Source::Git do describe "#to_s" do it "returns a description" do - expect(subject.to_s).to eq "https://github.com/foo/bar.git (at master)" + expect(subject.to_s).to eq "https://github.com/foo/bar.git" end context "when the URI contains credentials" do let(:uri) { "https://my-secret-token:x-oauth-basic@github.com/foo/bar.git" } it "filters credentials" do - expect(subject.to_s).to eq "https://x-oauth-basic@github.com/foo/bar.git (at master)" + expect(subject.to_s).to eq "https://x-oauth-basic@github.com/foo/bar.git" + end + end + + context "when the source has a glob specifier" do + let(:glob) { "bar/baz/*.gemspec" } + let(:options) do + { "uri" => uri, "glob" => glob } + end + + it "includes it" do + expect(subject.to_s).to eq "https://github.com/foo/bar.git (glob: bar/baz/*.gemspec)" + end + end + + context "when the source has a reference" do + let(:git_proxy_stub) do + instance_double(Bundler::Source::Git::GitProxy, revision: "123abc", branch: "v1.0.0") + end + let(:options) do + { "uri" => uri, "ref" => "v1.0.0" } + end + + before do + allow(Bundler::Source::Git::GitProxy).to receive(:new).and_return(git_proxy_stub) + end + + it "includes it" do + expect(subject.to_s).to eq "https://github.com/foo/bar.git (at v1.0.0@123abc)" + end + end + + context "when the source has both reference and glob specifiers" do + let(:git_proxy_stub) do + instance_double(Bundler::Source::Git::GitProxy, revision: "123abc", branch: "v1.0.0") + end + let(:options) do + { "uri" => uri, "ref" => "v1.0.0", "glob" => "gems/foo/*.gemspec" } + end + + before do + allow(Bundler::Source::Git::GitProxy).to receive(:new).and_return(git_proxy_stub) + end + + it "includes both" do + expect(subject.to_s).to eq "https://github.com/foo/bar.git (at v1.0.0@123abc, glob: gems/foo/*.gemspec)" + end + end + end + + describe "#locked_revision_checked_out?" do + let(:revision) { "abc" } + let(:git_proxy_revision) { revision } + let(:git_proxy_installed) { true } + let(:git_proxy) { subject.send(:git_proxy) } + let(:options) do + { + "uri" => uri, + "revision" => revision, + } + end + + before do + allow(git_proxy).to receive(:revision).and_return(git_proxy_revision) + allow(git_proxy).to receive(:installed_to?).with(subject.install_path).and_return(git_proxy_installed) + end + + context "when the locked revision is checked out" do + it "returns true" do + expect(subject.send(:locked_revision_checked_out?)).to be true + end + end + + context "when no revision is provided" do + let(:options) do + { "uri" => uri } + end + + it "returns falsey value" do + expect(subject.send(:locked_revision_checked_out?)).to be_falsey + end + end + + context "when the git proxy revision is different than the git revision" do + let(:git_proxy_revision) { revision.next } + + it "returns falsey value" do + expect(subject.send(:locked_revision_checked_out?)).to be_falsey + end + end + + context "when the gem hasn't been installed" do + let(:git_proxy_installed) { false } + + it "returns falsey value" do + expect(subject.send(:locked_revision_checked_out?)).to be_falsey end end end diff --git a/spec/bundler/bundler/source/rubygems/remote_spec.rb b/spec/bundler/bundler/source/rubygems/remote_spec.rb index 52fb4e7f1c..27430d4a3b 100644 --- a/spec/bundler/bundler/source/rubygems/remote_spec.rb +++ b/spec/bundler/bundler/source/rubygems/remote_spec.rb @@ -11,8 +11,8 @@ RSpec.describe Bundler::Source::Rubygems::Remote do allow(Digest(:MD5)).to receive(:hexdigest).with(duck_type(:to_s)) {|string| "MD5HEX(#{string})" } end - let(:uri_no_auth) { URI("https://gems.example.com") } - let(:uri_with_auth) { URI("https://#{credentials}@gems.example.com") } + let(:uri_no_auth) { Gem::URI("https://gems.example.com") } + let(:uri_with_auth) { Gem::URI("https://#{credentials}@gems.example.com") } let(:credentials) { "username:password" } context "when the original URI has no credentials" do @@ -89,11 +89,11 @@ RSpec.describe Bundler::Source::Rubygems::Remote do end context "when the original URI has only a username" do - let(:uri) { URI("https://SeCrEt-ToKeN@gem.fury.io/me/") } + let(:uri) { Gem::URI("https://SeCrEt-ToKeN@gem.fury.io/me/") } describe "#anonymized_uri" do it "returns the URI without username and password" do - expect(remote(uri).anonymized_uri).to eq(URI("https://gem.fury.io/me/")) + expect(remote(uri).anonymized_uri).to eq(Gem::URI("https://gem.fury.io/me/")) end end @@ -105,9 +105,9 @@ RSpec.describe Bundler::Source::Rubygems::Remote do end context "when a mirror with inline credentials is configured for the URI" do - let(:uri) { 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/") } + let(:uri) { Gem::URI("https://rubygems.org/") } + let(:mirror_uri_with_auth) { Gem::URI("https://username:password@example-mirror.rubygems.org/") } + let(:mirror_uri_no_auth) { Gem::URI("https://example-mirror.rubygems.org/") } before { Bundler.settings.temporary("mirror.https://rubygems.org/" => mirror_uri_with_auth.to_s) } @@ -131,9 +131,9 @@ RSpec.describe Bundler::Source::Rubygems::Remote do end context "when a mirror with configured credentials is configured for the URI" do - let(:uri) { 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/") } + let(:uri) { Gem::URI("https://rubygems.org/") } + let(:mirror_uri_with_auth) { Gem::URI("https://#{credentials}@example-mirror.rubygems.org/") } + let(:mirror_uri_no_auth) { Gem::URI("https://example-mirror.rubygems.org/") } before do Bundler.settings.temporary("mirror.https://rubygems.org/" => mirror_uri_no_auth.to_s) @@ -169,4 +169,39 @@ RSpec.describe Bundler::Source::Rubygems::Remote do end end end + + describe "#cooldown" do + it "is nil by default" do + expect(remote(uri_no_auth).cooldown).to be_nil + end + + it "returns the value passed to the constructor" do + r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7) + expect(r.cooldown).to eq(7) + end + end + + describe "#effective_cooldown" do + it "returns the per-remote value when no override is set" do + r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7) + expect(r.effective_cooldown).to eq(7) + end + + it "returns nil when neither override nor per-remote value is set" do + expect(remote(uri_no_auth).effective_cooldown).to be_nil + end + + it "settings override per-remote value" do + r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7) + Bundler.settings.temporary(cooldown: 14) do + expect(r.effective_cooldown).to eq(14) + end + end + + it "settings override even when per-remote value is absent" do + Bundler.settings.temporary(cooldown: 14) do + expect(remote(uri_no_auth).effective_cooldown).to eq(14) + end + end + end end diff --git a/spec/bundler/bundler/source/rubygems_spec.rb b/spec/bundler/bundler/source/rubygems_spec.rb index 7c457a7265..feb787498e 100644 --- a/spec/bundler/bundler/source/rubygems_spec.rb +++ b/spec/bundler/bundler/source/rubygems_spec.rb @@ -30,4 +30,75 @@ RSpec.describe Bundler::Source::Rubygems do end end end + + describe "#no_remotes?" do + context "when no remote provided" do + it "returns a truthy value" do + expect(described_class.new("remotes" => []).no_remotes?).to be_truthy + end + end + + context "when a remote provided" do + it "returns a falsey value" do + expect(described_class.new("remotes" => ["https://rubygems.org"]).no_remotes?).to be_falsey + end + end + end + + describe "#clear_cache" do + it "invalidates memoized indexes so subsequent reads rebuild them" do + source = described_class.new + + first_specs = source.specs + first_installed = source.send(:installed_specs) + first_default = source.send(:default_specs) + first_cached = source.send(:cached_specs) + + expect(source.specs).to equal(first_specs) + expect(source.send(:installed_specs)).to equal(first_installed) + expect(source.send(:default_specs)).to equal(first_default) + expect(source.send(:cached_specs)).to equal(first_cached) + + source.clear_cache + + expect(source.specs).not_to equal(first_specs) + expect(source.send(:installed_specs)).not_to equal(first_installed) + expect(source.send(:default_specs)).not_to equal(first_default) + expect(source.send(:cached_specs)).not_to equal(first_cached) + end + + it "reflects newly-discovered installed gems after clear_cache" do + source = described_class.new + foo = Gem::Specification.new("foo", "1.0.0") + bar = Gem::Specification.new("bar", "1.0.0") + + allow(Bundler.rubygems).to receive(:installed_specs).and_return([foo]) + expect(source.send(:installed_specs).search("bar")).to be_empty + + allow(Bundler.rubygems).to receive(:installed_specs).and_return([foo, bar]) + expect(source.send(:installed_specs).search("bar")).to be_empty + + source.clear_cache + + expect(source.send(:installed_specs).search("bar")).not_to be_empty + end + end + + describe "log debug information" do + it "log the time spent downloading and installing a gem" do + build_repo2 do + build_gem "warning" + end + + gemfile_content = <<~G + source "https://gem.repo2" + gem "warning" + G + + stdout = install_gemfile(gemfile_content, env: { "DEBUG" => "1" }) + + expect(stdout).to match(/Downloaded warning in: \d+\.\d+s/) + expect(stdout).to match(/Installed warning in: \d+\.\d+s/) + end + end end diff --git a/spec/bundler/bundler/source_list_spec.rb b/spec/bundler/bundler/source_list_spec.rb index a78b80ec3b..61bd99b063 100644 --- a/spec/bundler/bundler/source_list_spec.rb +++ b/spec/bundler/bundler/source_list_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Bundler::SourceList do subject(:source_list) { Bundler::SourceList.new } - let(:rubygems_aggregate) { Bundler::Source::Rubygems.new } + let(:global_rubygems_source) { Bundler::Source::Rubygems.new } let(:metadata_source) { Bundler::Source::Metadata.new } describe "adding sources" do @@ -75,7 +75,7 @@ RSpec.describe Bundler::SourceList 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 set git.allow_insecure true`, " \ + "Disable this warning with `bundle config set --local git.allow_insecure true`, " \ "or switch to the `https` protocol to keep your data secure." end @@ -85,7 +85,7 @@ RSpec.describe Bundler::SourceList do end it "ignores git protocols on request" do - Bundler.settings.temporary(:"git.allow_insecure" => true) + Bundler.settings.temporary("git.allow_insecure": true) expect(Bundler.ui).to_not receive(:warn).with(msg) source_list.add_git_source("uri" => "git://existing-git.org/path.git") end @@ -115,20 +115,26 @@ RSpec.describe Bundler::SourceList do end end - describe "#add_rubygems_remote", :bundler => "< 3" do - let!(:returned_source) { source_list.add_rubygems_remote("https://rubygems.org/") } + describe "#add_global_rubygems_remote" do + let!(:returned_source) { source_list.add_global_rubygems_remote("https://rubygems.org/") } - it "returns the aggregate rubygems source" do + it "returns the global 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") + it "adds the provided remote to the beginning of the global source" do + source_list.add_global_rubygems_remote("https://othersource.org") expect(returned_source.remotes).to eq [ - URI("https://othersource.org/"), - URI("https://rubygems.org/"), + Gem::URI("https://othersource.org/"), + Gem::URI("https://rubygems.org/"), ] end + + it "records the per-remote cooldown when supplied" do + source_list.add_global_rubygems_remote("https://othersource.org", cooldown: 7) + expect(returned_source.cooldown_for(Gem::URI("https://othersource.org/"))).to eq(7) + expect(returned_source.cooldown_for(Gem::URI("https://rubygems.org/"))).to be_nil + end end describe "#add_plugin_source" do @@ -156,21 +162,21 @@ RSpec.describe Bundler::SourceList do end describe "#all_sources" do - it "includes the aggregate rubygems source when rubygems sources have been added" do + it "includes the global 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 + expect(source_list.all_sources).to include global_rubygems_source end - it "includes the aggregate rubygems source when no rubygems sources have been added" do + it "includes the global 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 + expect(source_list.all_sources).to include global_rubygems_source end it "returns sources of the same type in the reverse order that they were added" do @@ -204,7 +210,7 @@ RSpec.describe Bundler::SourceList do 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, + global_rubygems_source, metadata_source, ] end @@ -212,22 +218,22 @@ RSpec.describe Bundler::SourceList do 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_global_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_global_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_global_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_global_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_global_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_global_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 [ @@ -240,7 +246,7 @@ RSpec.describe Bundler::SourceList do 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_global_rubygems_remote("https://rubygems.org") source_list.add_path_source("path" => "/path/to/gem") expect(source_list.git_sources).to be_empty @@ -248,15 +254,15 @@ RSpec.describe Bundler::SourceList do 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_global_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_global_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_global_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_global_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_global_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 [ @@ -269,7 +275,7 @@ RSpec.describe Bundler::SourceList do 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_global_rubygems_remote("https://rubygems.org") source_list.add_path_source("path" => "/path/to/gem") expect(source_list.plugin_sources).to be_empty @@ -279,13 +285,13 @@ RSpec.describe Bundler::SourceList 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_global_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_global_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_global_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_global_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 [ @@ -297,19 +303,19 @@ RSpec.describe Bundler::SourceList do end describe "#rubygems_sources" do - it "includes the aggregate rubygems source when rubygems sources have been added" do + it "includes the global 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 + expect(source_list.rubygems_sources).to include global_rubygems_source end - it "returns only the aggregate rubygems source when no rubygems sources have been added" do + it "returns only the global 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] + expect(source_list.rubygems_sources).to eq [global_rubygems_source] end it "returns rubygems sources in the reverse order that they were added" do @@ -331,7 +337,7 @@ RSpec.describe Bundler::SourceList do 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, + global_rubygems_source, ] end end @@ -339,7 +345,7 @@ RSpec.describe Bundler::SourceList do 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") } + before { @equal_source = source_list.add_global_rubygems_remote("https://rubygems.org") } it "returns the equal source" do expect(source_list.get(rubygems_source)).to be @equal_source @@ -372,26 +378,7 @@ RSpec.describe Bundler::SourceList do source_list.add_git_source("uri" => "git://first-git.org/path.git") end - it "combines the rubygems sources into a single instance, removing duplicate remotes from the end", :bundler => "< 3" do - expect(source_list.lock_sources).to eq [ - Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"), - Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"), - Bundler::Source::Git.new("uri" => "git://third-git.org/path.git"), - ASourcePlugin.new("uri" => "https://second-plugin.org/random"), - ASourcePlugin.new("uri" => "https://third-bar.org/foo"), - Bundler::Source::Path.new("path" => "/first/path/to/gem"), - Bundler::Source::Path.new("path" => "/second/path/to/gem"), - Bundler::Source::Path.new("path" => "/third/path/to/gem"), - Bundler::Source::Rubygems.new("remotes" => [ - "https://duplicate-rubygems.org", - "https://first-rubygems.org", - "https://second-rubygems.org", - "https://third-rubygems.org", - ]), - ] - end - - it "returns all sources, without combining rubygems sources", :bundler => "3" do + it "returns all sources, without combining rubygems sources" do expect(source_list.lock_sources).to eq [ Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"), Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"), @@ -460,4 +447,29 @@ RSpec.describe Bundler::SourceList do source_list.remote! end end + + describe "#clear_cache" do + let(:rubygems_source) { source_list.add_rubygems_source("remotes" => ["https://rubygems.org"]) } + + it "calls #clear_cache on all rubygems sources" do + expect(rubygems_source).to receive(:clear_cache) + expect(source_list.global_rubygems_source).to receive(:clear_cache) + source_list.clear_cache + end + end + + describe "implicit_global_source?" do + context "when a global rubygem source provided" do + it "returns a falsy value" do + source_list.add_global_rubygems_remote("https://rubygems.org") + + expect(source_list.implicit_global_source?).to be_falsey + end + end + context "when no global rubygem source provided" do + it "returns a truthy value" do + expect(source_list.implicit_global_source?).to be_truthy + end + end + end end diff --git a/spec/bundler/bundler/source_spec.rb b/spec/bundler/bundler/source_spec.rb index 0c35c27fdf..01b57ce9e8 100644 --- a/spec/bundler/bundler/source_spec.rb +++ b/spec/bundler/bundler/source_spec.rb @@ -21,7 +21,7 @@ RSpec.describe Bundler::Source do end describe "#version_message" do - let(:spec) { double(:spec, :name => "nokogiri", :version => ">= 1.6", :platform => rb) } + let(:spec) { double(:spec, name: "nokogiri", version: ">= 1.6", platform: Gem::Platform::RUBY) } shared_examples_for "the lockfile specs are not relevant" do it "should return a string with the spec name and version" do @@ -30,31 +30,21 @@ RSpec.describe Bundler::Source do end context "when there are locked gems" do - let(:locked_gems) { double(:locked_gems) } - - before { allow(Bundler).to receive(:locked_gems).and_return(locked_gems) } - context "that contain the relevant gem spec" do - before do - specs = double(:specs) - allow(locked_gems).to receive(:specs).and_return(specs) - allow(specs).to receive(:find).and_return(locked_gem) - end - context "without a version" do - let(:locked_gem) { double(:locked_gem, :name => "nokogiri", :version => nil) } + let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: nil) } it_behaves_like "the lockfile specs are not relevant" end context "with the same version" do - let(:locked_gem) { double(:locked_gem, :name => "nokogiri", :version => ">= 1.6") } + let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: ">= 1.6") } it_behaves_like "the lockfile specs are not relevant" end context "with a different version" do - let(:locked_gem) { double(:locked_gem, :name => "nokogiri", :version => "< 1.5") } + let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: "< 1.5") } context "with color", :no_color_tty do before do @@ -62,7 +52,7 @@ RSpec.describe Bundler::Source do end it "should return a string with the spec name and version and locked spec version" do - expect(subject.version_message(spec)).to eq("nokogiri >= 1.6\e[32m (was < 1.5)\e[0m") + expect(subject.version_message(spec, locked_gem)).to eq("nokogiri >= 1.6\e[32m (was < 1.5)\e[0m") end end @@ -74,14 +64,14 @@ RSpec.describe Bundler::Source do end it "should return a string with the spec name and version and locked spec version" do - expect(subject.version_message(spec)).to eq("nokogiri >= 1.6 (was < 1.5)") + expect(subject.version_message(spec, locked_gem)).to eq("nokogiri >= 1.6 (was < 1.5)") end end end context "with a more recent version" do - let(:spec) { double(:spec, :name => "nokogiri", :version => "1.6.1", :platform => rb) } - let(:locked_gem) { double(:locked_gem, :name => "nokogiri", :version => "1.7.0") } + let(:spec) { double(:spec, name: "nokogiri", version: "1.6.1", platform: Gem::Platform::RUBY) } + let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: "1.7.0") } context "with color", :no_color_tty do before do @@ -89,7 +79,7 @@ RSpec.describe Bundler::Source do end it "should return a string with the locked spec version in yellow" do - expect(subject.version_message(spec)).to eq("nokogiri 1.6.1\e[33m (was 1.7.0)\e[0m") + expect(subject.version_message(spec, locked_gem)).to eq("nokogiri 1.6.1\e[33m (was 1.7.0)\e[0m") end end @@ -101,14 +91,14 @@ RSpec.describe Bundler::Source do end it "should return a string with the locked spec version in yellow" do - expect(subject.version_message(spec)).to eq("nokogiri 1.6.1 (was 1.7.0)") + expect(subject.version_message(spec, locked_gem)).to eq("nokogiri 1.6.1 (was 1.7.0)") end end end context "with an older version" do - let(:spec) { double(:spec, :name => "nokogiri", :version => "1.7.1", :platform => rb) } - let(:locked_gem) { double(:locked_gem, :name => "nokogiri", :version => "1.7.0") } + let(:spec) { double(:spec, name: "nokogiri", version: "1.7.1", platform: Gem::Platform::RUBY) } + let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: "1.7.0") } context "with color", :no_color_tty do before do @@ -116,7 +106,7 @@ RSpec.describe Bundler::Source do end it "should return a string with the locked spec version in green" do - expect(subject.version_message(spec)).to eq("nokogiri 1.7.1\e[32m (was 1.7.0)\e[0m") + expect(subject.version_message(spec, locked_gem)).to eq("nokogiri 1.7.1\e[32m (was 1.7.0)\e[0m") end end @@ -128,33 +118,17 @@ RSpec.describe Bundler::Source do end it "should return a string with the locked spec version in yellow" do - expect(subject.version_message(spec)).to eq("nokogiri 1.7.1 (was 1.7.0)") + expect(subject.version_message(spec, locked_gem)).to eq("nokogiri 1.7.1 (was 1.7.0)") end end end end - - context "that do not contain the relevant gem spec" do - before do - specs = double(:specs) - allow(locked_gems).to receive(:specs).and_return(specs) - allow(specs).to receive(:find).and_return(nil) - end - - it_behaves_like "the lockfile specs are not relevant" - end - end - - context "when there are no locked gems" do - before { allow(Bundler).to receive(:locked_gems).and_return(nil) } - - it_behaves_like "the lockfile specs are not relevant" end end describe "#can_lock?" do context "when the passed spec's source is equivalent" do - let(:spec) { double(:spec, :source => subject) } + let(:spec) { double(:spec, source: subject) } it "should return true" do expect(subject.can_lock?(spec)).to be_truthy @@ -162,7 +136,7 @@ RSpec.describe Bundler::Source do end context "when the passed spec's source is not equivalent" do - let(:spec) { double(:spec, :source => double(:other_source)) } + let(:spec) { double(:spec, source: double(:other_source)) } it "should return false" do expect(subject.can_lock?(spec)).to be_falsey @@ -188,7 +162,7 @@ RSpec.describe Bundler::Source do end end -private + private def with_ui(ui) old_ui = Bundler.ui diff --git a/spec/bundler/bundler/spec_set_spec.rb b/spec/bundler/bundler/spec_set_spec.rb index 6fedd38b50..1e1ceadf26 100644 --- a/spec/bundler/bundler/spec_set_spec.rb +++ b/spec/bundler/bundler/spec_set_spec.rb @@ -43,23 +43,29 @@ RSpec.describe Bundler::SpecSet do spec = described_class.new(specs).find_by_name_and_platform("b", platform) expect(spec).to eq platform_spec end - end - describe "#merge" do - let(:other_specs) do - [ - build_spec("f", "1.0"), - build_spec("g", "2.0"), - ].flatten + it "returns nil when the name is not present" do + spec = described_class.new(specs).find_by_name_and_platform("missing", platform) + expect(spec).to be_nil end - let(:other_spec_set) { described_class.new(other_specs) } + it "returns nil when the name exists but no spec is installable on the requested platform" do + incompatible_platform = Gem::Platform.new("java") + incompatible_spec = build_spec("a", "1.0", incompatible_platform).first - it "merges the items in each gemspec" do - new_spec_set = subject.merge(other_spec_set) - specs = new_spec_set.to_a.map(&:full_name) - expect(specs).to include("a-1.0") - expect(specs).to include("f-1.0") + spec = described_class.new([incompatible_spec]).find_by_name_and_platform("a", platform) + expect(spec).to be_nil + end + + it "returns the first installable spec for the given name in insertion order" do + later_platform_spec = build_spec("b", "3.0", platform).first + specs = [ + platform_spec, + later_platform_spec, + ] + + spec = described_class.new(specs).find_by_name_and_platform("b", platform) + expect(spec).to eq platform_spec end end @@ -73,5 +79,70 @@ RSpec.describe Bundler::SpecSet do d-2.0 ] end + + it "puts rake first when present" do + specs = [ + build_spec("a", "1.0") {|s| s.dep "rake", ">= 0" }, + build_spec("rake", "13.0"), + ].flatten + + expect(described_class.new(specs).to_a.map(&:full_name)).to eq %w[ + rake-13.0 + a-1.0 + ] + end + end + + describe "#complete_platform" do + let(:platform) { Gem::Platform.new("x86_64-linux") } + + let(:platform_variant) do + build_spec("needs_old_ruby", "1.0", platform).first.tap do |s| + s.required_ruby_version = Gem::Requirement.new("< #{Gem.ruby_version}") + end + end + + let(:lazy_spec) do + lazy = Bundler::LazySpecification.new("needs_old_ruby", Gem::Version.new("1.0"), Gem::Platform::RUBY) + lazy.required_ruby_version = Gem::Requirement.new("< #{Gem.ruby_version}") + source = double("source") + source_specs = double("source_specs") + allow(source).to receive(:specs).and_return(source_specs) + allow(source_specs).to receive(:search). + with(["needs_old_ruby", Gem::Version.new("1.0")]).and_return([platform_variant]) + lazy.source = source + lazy + end + + it "rejects a platform variant whose strict metadata is incompatible when no override is attached" do + set = described_class.new([lazy_spec]) + expect(set.send(:complete_platform, platform)).to be(false) + end + + it "accepts a platform variant when the LazySpec carries an override that allows it" do + lazy_spec.overrides = [Bundler::Override.new("needs_old_ruby", :required_ruby_version, :ignore_upper)] + set = described_class.new([lazy_spec]) + expect(set.send(:complete_platform, platform)).to be(true) + end + + it "carries overrides onto a synthesized LazySpec so a follow-up complete_platform still honors them" do + override = Bundler::Override.new("needs_old_ruby", :required_ruby_version, :ignore_upper) + lazy_spec.overrides = [override] + second_platform = Gem::Platform.new("aarch64-linux") + second_variant = build_spec("needs_old_ruby", "1.0", second_platform).first.tap do |s| + s.required_ruby_version = Gem::Requirement.new("< #{Gem.ruby_version}") + end + allow(lazy_spec.source.specs).to receive(:search). + with(["needs_old_ruby", Gem::Version.new("1.0")]).and_return([platform_variant, second_variant]) + + set = described_class.new([lazy_spec]) + expect(set.send(:complete_platform, platform)).to be(true) + # The synthesized x86_64-linux variant is now in the set. If lookup + # picks it as exemplar for the next platform check, the override list + # must still be reachable via its overrides accessor. + synthesized = set.to_a.find {|s| s.platform == platform } + expect(synthesized.overrides).to eq([override]) + expect(set.send(:complete_platform, second_platform)).to be(true) + end end end diff --git a/spec/bundler/bundler/specifications/foo.gemspec b/spec/bundler/bundler/specifications/foo.gemspec new file mode 100644 index 0000000000..19b7724e81 --- /dev/null +++ b/spec/bundler/bundler/specifications/foo.gemspec @@ -0,0 +1,13 @@ +# rubocop:disable Style/FrozenStringLiteralComment +# stub: foo 1.0.0 ruby lib + +# The first line would be '# -*- encoding: utf-8 -*-' in a real stub gemspec + +Gem::Specification.new do |s| + s.name = "foo" + s.version = "1.0.0" + s.loaded_from = __FILE__ + s.extensions = "ext/foo" + s.required_ruby_version = ">= 3.2.0" +end +# rubocop:enable Style/FrozenStringLiteralComment diff --git a/spec/bundler/bundler/stub_specification_spec.rb b/spec/bundler/bundler/stub_specification_spec.rb index 7495b5d661..f2faa2ea64 100644 --- a/spec/bundler/bundler/stub_specification_spec.rb +++ b/spec/bundler/bundler/stub_specification_spec.rb @@ -1,15 +1,14 @@ # frozen_string_literal: true RSpec.describe Bundler::StubSpecification do - let(:gemspec) do - Gem::Specification.new do |s| + let(:with_bundler_stub_spec) do + gemspec = Gem::Specification.new do |s| s.name = "gemname" s.version = "1.0.0" s.loaded_from = __FILE__ + s.extensions = "ext/gemname" end - end - let(:with_bundler_stub_spec) do described_class.from_stub(gemspec) end @@ -19,4 +18,65 @@ RSpec.describe Bundler::StubSpecification do expect(stub).to be(with_bundler_stub_spec) end end + + describe "#gem_build_complete_path" do + it "StubSpecification should have equal gem_build_complete_path as Specification" do + spec_path = File.join(File.dirname(__FILE__), "specifications", "foo.gemspec") + spec = Gem::Specification.load(spec_path) + gem_stub = Gem::StubSpecification.new(spec_path, File.dirname(__FILE__),"","") + + stub = described_class.from_stub(gem_stub) + expect(stub.gem_build_complete_path).to eq spec.gem_build_complete_path + end + end + + describe "#manually_installed?" do + it "returns true if installed_by_version is nil or 0" do + stub = described_class.from_stub(with_bundler_stub_spec) + expect(stub.manually_installed?).to be true + end + + it "returns false if installed_by_version is greater than 0" do + stub = described_class.from_stub(with_bundler_stub_spec) + stub.installed_by_version = Gem::Version.new(1) + expect(stub.manually_installed?).to be false + end + end + + describe "#missing_extensions?" do + it "returns false if manually_installed?" do + stub = described_class.from_stub(with_bundler_stub_spec) + expect(stub.missing_extensions?).to be false + end + + it "returns #{RUBY_ENGINE == "jruby" ? "false" : "true"} if not manually_installed?" do + stub = described_class.from_stub(with_bundler_stub_spec) + stub.installed_by_version = Gem::Version.new(1) + if RUBY_ENGINE == "jruby" + expect(stub.missing_extensions?).to be false + else + expect(stub.missing_extensions?).to be true + end + end + end + + describe "#activated?" do + it "returns true after activation" do + stub = described_class.from_stub(with_bundler_stub_spec) + + expect(stub.activated?).to be_falsey + stub.activated = true + expect(stub.activated?).to be true + end + + it "returns true after activation if the underlying stub is a `Gem::StubSpecification`" do + spec_path = File.join(File.dirname(__FILE__), "specifications", "foo.gemspec") + gem_stub = Gem::StubSpecification.new(spec_path, File.dirname(__FILE__),"","") + stub = described_class.from_stub(gem_stub) + + expect(stub.activated?).to be_falsey + stub.activated = true + expect(stub.activated?).to be true + end + end end diff --git a/spec/bundler/bundler/ui/shell_spec.rb b/spec/bundler/bundler/ui/shell_spec.rb index 536014c6aa..83f147191e 100644 --- a/spec/bundler/bundler/ui/shell_spec.rb +++ b/spec/bundler/bundler/ui/shell_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative "../../support/streams" - RSpec.describe Bundler::UI::Shell do subject { described_class.new } @@ -12,6 +10,13 @@ RSpec.describe Bundler::UI::Shell do it "prints to stdout" do expect { subject.info("info") }.to output("info\n").to_stdout end + + context "when output_stream is :stderr" do + before { subject.output_stream = :stderr } + it "prints to stderr" do + expect { subject.info("info") }.to output("info\n").to_stderr + end + end end describe "#confirm" do @@ -19,19 +24,36 @@ RSpec.describe Bundler::UI::Shell do it "prints to stdout" do expect { subject.confirm("confirm") }.to output("confirm\n").to_stdout end + + context "when output_stream is :stderr" do + before { subject.output_stream = :stderr } + it "prints to stderr" do + expect { subject.confirm("confirm") }.to output("confirm\n").to_stderr + end + end end describe "#warn" do before { subject.level = "warn" } - it "prints to stderr" do + it "prints to stderr, implicitly adding a newline" do expect { subject.warn("warning") }.to output("warning\n").to_stderr end + it "can be told not to emit a newline" do + expect { subject.warn("warning", false) }.to output("warning").to_stderr + end end describe "#debug" do it "prints to stdout" do expect { subject.debug("debug") }.to output("debug\n").to_stdout end + + context "when output_stream is :stderr" do + before { subject.output_stream = :stderr } + it "prints to stderr" do + expect { subject.debug("debug") }.to output("debug\n").to_stderr + end + end end describe "#error" do @@ -43,11 +65,48 @@ RSpec.describe Bundler::UI::Shell do context "when stderr is closed" do it "doesn't report anything" do - output = capture(:stderr, :closed => true) do - subject.error("Something went wrong") + output = begin + result = StringIO.new + result.close + + $stderr = result + + subject.error("Something went wrong") + + result.string + ensure + $stderr = STDERR + end + expect(output).to_not eq("Something went wrong") + end + end + end + + describe "threads" do + it "is thread safe when using with_level" do + stop_thr1 = false + stop_thr2 = false + + expect(subject.level).to eq("debug") + + thr1 = Thread.new do + subject.silence do + sleep(0.1) until stop_thr1 + end + + stop_thr2 = true + end + + thr2 = Thread.new do + subject.silence do + stop_thr1 = true + sleep(0.1) until stop_thr2 end - expect(output).to_not eq("Something went wrong\n") end + + [thr1, thr2].each(&:join) + + expect(subject.level).to eq("debug") end end end diff --git a/spec/bundler/bundler/uri_credentials_filter_spec.rb b/spec/bundler/bundler/uri_credentials_filter_spec.rb index fe52d16306..641f0addb4 100644 --- a/spec/bundler/bundler/uri_credentials_filter_spec.rb +++ b/spec/bundler/bundler/uri_credentials_filter_spec.rb @@ -16,7 +16,7 @@ RSpec.describe Bundler::URICredentialsFilter do let(:credentials) { "oauth_token:x-oauth-basic@" } it "returns the uri without the oauth token" do - expect(subject.credential_filtered_uri(uri).to_s).to eq(URI("https://x-oauth-basic@github.com/company/private-repo").to_s) + expect(subject.credential_filtered_uri(uri).to_s).to eq(Gem::URI("https://x-oauth-basic@github.com/company/private-repo").to_s) end it_behaves_like "original type of uri is maintained" @@ -26,7 +26,17 @@ RSpec.describe Bundler::URICredentialsFilter do let(:credentials) { "oauth_token:x@" } it "returns the uri without the oauth token" do - expect(subject.credential_filtered_uri(uri).to_s).to eq(URI("https://x@github.com/company/private-repo").to_s) + expect(subject.credential_filtered_uri(uri).to_s).to eq(Gem::URI("https://x@github.com/company/private-repo").to_s) + end + + it_behaves_like "original type of uri is maintained" + end + + context "specified without empty username" do + let(:credentials) { "oauth_token@" } + + it "returns the uri without the oauth token" do + expect(subject.credential_filtered_uri(uri).to_s).to eq(Gem::URI("https://github.com/company/private-repo").to_s) end it_behaves_like "original type of uri is maintained" @@ -37,7 +47,7 @@ RSpec.describe Bundler::URICredentialsFilter do let(:credentials) { "username1:hunter3@" } it "returns the uri without the password" do - expect(subject.credential_filtered_uri(uri).to_s).to eq(URI("https://username1@github.com/company/private-repo").to_s) + expect(subject.credential_filtered_uri(uri).to_s).to eq(Gem::URI("https://username1@github.com/company/private-repo").to_s) end it_behaves_like "original type of uri is maintained" @@ -55,7 +65,7 @@ RSpec.describe Bundler::URICredentialsFilter do end context "uri is a uri object" do - let(:uri) { URI("https://#{credentials}github.com/company/private-repo") } + let(:uri) { Gem::URI("https://#{credentials}github.com/company/private-repo") } it_behaves_like "sensitive credentials in uri are filtered out" end @@ -90,7 +100,7 @@ RSpec.describe Bundler::URICredentialsFilter do describe "#credential_filtered_string" do let(:str_to_filter) { "This is a git message containing a uri #{uri}!" } let(:credentials) { "" } - let(:uri) { URI("https://#{credentials}github.com/company/private-repo") } + let(:uri) { Gem::URI("https://#{credentials}github.com/company/private-repo") } context "with a uri that contains credentials" do let(:credentials) { "oauth_token:x-oauth-basic@" } diff --git a/spec/bundler/bundler/uri_normalizer_spec.rb b/spec/bundler/bundler/uri_normalizer_spec.rb new file mode 100644 index 0000000000..1308e86014 --- /dev/null +++ b/spec/bundler/bundler/uri_normalizer_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::URINormalizer do + describe ".normalize_suffix" do + context "when trailing_slash is true" do + it "adds a trailing slash when missing" do + expect(described_class.normalize_suffix("https://example.com", trailing_slash: true)).to eq("https://example.com/") + end + + it "keeps the trailing slash when present" do + expect(described_class.normalize_suffix("https://example.com/", trailing_slash: true)).to eq("https://example.com/") + end + end + + context "when trailing_slash is false" do + it "removes a trailing slash when present" do + expect(described_class.normalize_suffix("https://example.com/", trailing_slash: false)).to eq("https://example.com") + end + + it "keeps the value unchanged when no trailing slash exists" do + expect(described_class.normalize_suffix("https://example.com", trailing_slash: false)).to eq("https://example.com") + end + end + end +end diff --git a/spec/bundler/bundler/vendored_persistent_spec.rb b/spec/bundler/bundler/vendored_persistent_spec.rb deleted file mode 100644 index b4d68c2ea0..0000000000 --- a/spec/bundler/bundler/vendored_persistent_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -require "bundler/vendored_persistent" - -RSpec.describe Bundler::PersistentHTTP do - describe "#warn_old_tls_version_rubygems_connection" do - let(:uri) { "https://index.rubygems.org" } - let(:connection) { instance_double(Bundler::Persistent::Net::HTTP::Persistent::Connection) } - let(:tls_version) { "TLSv1.2" } - let(:socket) { double("Socket") } - let(:socket_io) { double("SocketIO") } - - before do - allow(connection).to receive_message_chain(:http, :use_ssl?).and_return(!tls_version.nil?) - allow(socket).to receive(:io).and_return(socket_io) if socket - connection.instance_variable_set(:@socket, socket) - - if tls_version - allow(socket_io).to receive(:ssl_version).and_return(tls_version) - end - end - - shared_examples_for "does not warn" do - it "does not warn" do - allow(Bundler.ui).to receive(:warn).never - subject.warn_old_tls_version_rubygems_connection(URI(uri), connection) - end - end - - shared_examples_for "does warn" do |*expected| - it "warns" do - expect(Bundler.ui).to receive(:warn).with(*expected) - subject.warn_old_tls_version_rubygems_connection(URI(uri), connection) - end - end - - context "an HTTPS uri with TLSv1.2" do - include_examples "does not warn" - end - - context "without SSL" do - let(:tls_version) { nil } - - include_examples "does not warn" - end - - context "without a socket" do - let(:socket) { nil } - - include_examples "does not warn" - end - - context "with a different TLD" do - let(:uri) { "https://foo.bar" } - include_examples "does not warn" - - context "and an outdated TLS version" do - let(:tls_version) { "TLSv1" } - include_examples "does not warn" - end - end - - context "with a nonsense TLS version" do - let(:tls_version) { "BlahBlah2.0Blah" } - include_examples "does not warn" - end - - context "with an outdated TLS version" do - let(:tls_version) { "TLSv1" } - include_examples "does warn", - "Warning: Your Ruby version is compiled against a copy of OpenSSL that is very old. " \ - "Starting in January 2018, RubyGems.org will refuse connection requests from these very old versions of OpenSSL. " \ - "If you will need to continue installing gems after January 2018, please follow this guide to upgrade: http://ruby.to/tls-outdated.", - :wrap => true - end - end -end diff --git a/spec/bundler/bundler/version_ranges_spec.rb b/spec/bundler/bundler/version_ranges_spec.rb deleted file mode 100644 index bca044b0c0..0000000000 --- a/spec/bundler/bundler/version_ranges_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require "bundler/version_ranges" - -RSpec.describe Bundler::VersionRanges do - describe ".empty?" do - shared_examples_for "empty?" do |exp, *req| - it "returns #{exp} for #{req}" do - r = Gem::Requirement.new(*req) - ranges = described_class.for(r) - expect(described_class.empty?(*ranges)).to eq(exp), "expected `#{r}` #{exp ? "" : "not "}to be empty" - end - end - - include_examples "empty?", false - include_examples "empty?", false, "!= 1" - include_examples "empty?", false, "!= 1", "= 2" - include_examples "empty?", false, "!= 1", "> 1" - include_examples "empty?", false, "!= 1", ">= 1" - include_examples "empty?", false, "= 1", ">= 0.1", "<= 1.1" - include_examples "empty?", false, "= 1", ">= 1", "<= 1" - include_examples "empty?", false, "= 1", "~> 1" - include_examples "empty?", false, ">= 0.z", "= 0" - include_examples "empty?", false, ">= 0" - include_examples "empty?", false, ">= 1.0.0", "< 2.0.0" - include_examples "empty?", false, "~> 1" - include_examples "empty?", false, "~> 2.0", "~> 2.1" - include_examples "empty?", true, ">= 4.1.0", "< 5.0", "= 5.2.1" - include_examples "empty?", true, "< 5.0", "< 5.3", "< 6.0", "< 6", "= 5.2.0", "> 2", ">= 3.0", ">= 3.1", ">= 3.2", ">= 4.0.0", ">= 4.1.0", ">= 4.2.0", ">= 4.2", ">= 4" - include_examples "empty?", true, "!= 1", "< 2", "> 2" - include_examples "empty?", true, "!= 1", "<= 1", ">= 1" - include_examples "empty?", true, "< 2", "> 2" - include_examples "empty?", true, "< 2", "> 2", "= 2" - include_examples "empty?", true, "= 1", "!= 1" - include_examples "empty?", true, "= 1", "= 2" - include_examples "empty?", true, "= 1", "~> 2" - include_examples "empty?", true, ">= 0", "<= 0.a" - include_examples "empty?", true, "~> 2.0", "~> 3" - end -end diff --git a/spec/bundler/bundler/worker_spec.rb b/spec/bundler/bundler/worker_spec.rb index 2e5642709d..2ad2845e37 100644 --- a/spec/bundler/bundler/worker_spec.rb +++ b/spec/bundler/bundler/worker_spec.rb @@ -19,4 +19,71 @@ RSpec.describe Bundler::Worker do end end end + + describe "priority queue" do + it "process elements from the priority queue first" do + processed_elements = [] + + function = proc do |element, _| + processed_elements << element + end + + worker = described_class.new(1, "Spec Worker", function) + worker.instance_variable_set(:@threads, []) # Prevent the enqueueing from starting work. + worker.enq("Normal element") + worker.enq("Priority element", priority: true) + worker.send(:create_threads) + + worker.stop + + expect(processed_elements).to eq(["Priority element", "Normal element"]) + end + end + + describe "handling interrupts" do + let(:status) do + pid = Process.fork do + $stderr.reopen File.new("/dev/null", "w") + Signal.trap "INT", previous_interrupt_handler + subject.enq "a" + subject.stop unless interrupt_before_stopping + Process.kill "INT", Process.pid + end + + Process.wait2(pid).last + end + + before do + skip "requires Process.fork" unless Process.respond_to?(:fork) + end + + context "when interrupted before stopping" do + let(:interrupt_before_stopping) { true } + let(:previous_interrupt_handler) { ->(*) { exit 0 } } + + it "aborts" do + expect(status.exitstatus).to eq(1) + end + end + + context "when interrupted after stopping" do + let(:interrupt_before_stopping) { false } + + context "when the previous interrupt handler was the default" do + let(:previous_interrupt_handler) { "DEFAULT" } + + it "uses the default interrupt handler" do + expect(status).to be_signaled + end + end + + context "when the previous interrupt handler was customized" do + let(:previous_interrupt_handler) { ->(*) { exit 42 } } + + it "restores the custom interrupt handler after stopping" do + expect(status.exitstatus).to eq(42) + end + end + end + end end diff --git a/spec/bundler/bundler/yaml_serializer_spec.rb b/spec/bundler/bundler/yaml_serializer_spec.rb index 1241c74bbf..9ff1579b76 100644 --- a/spec/bundler/bundler/yaml_serializer_spec.rb +++ b/spec/bundler/bundler/yaml_serializer_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Bundler::YAMLSerializer do it "works for simple hash" do hash = { "Q" => "Where does Thursday come before Wednesday? In the dictionary. :P" } - expected = strip_whitespace <<-YAML + expected = <<~YAML --- Q: "Where does Thursday come before Wednesday? In the dictionary. :P" YAML @@ -24,7 +24,7 @@ RSpec.describe Bundler::YAMLSerializer do }, } - expected = strip_whitespace <<-YAML + expected = <<~YAML --- nice-one: read_ahead: "All generalizations are false, including this one" @@ -45,7 +45,7 @@ RSpec.describe Bundler::YAMLSerializer do }, } - expected = strip_whitespace <<-YAML + expected = <<~YAML --- nested_hash: contains_array: @@ -57,11 +57,24 @@ RSpec.describe Bundler::YAMLSerializer do expect(serializer.dump(hash)).to eq(expected) end + + it "handles empty array" do + hash = { + "empty_array" => [], + } + + expected = <<~YAML + --- + empty_array: [] + YAML + + expect(serializer.dump(hash)).to eq(expected) + end end describe "#load" do it "works for simple hash" do - yaml = strip_whitespace <<-YAML + yaml = <<~YAML --- Jon: "Air is free dude!" Jack: "Yes.. until you buy a bag of chips!" @@ -76,7 +89,7 @@ RSpec.describe Bundler::YAMLSerializer do end it "works for nested hash" do - yaml = strip_whitespace <<-YAML + yaml = <<~YAML --- baa: baa: "black sheep" @@ -98,15 +111,15 @@ RSpec.describe Bundler::YAMLSerializer do end it "handles colon in key/value" do - yaml = strip_whitespace <<-YAML - BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/: http://rubygems-mirror.org + yaml = <<~YAML + BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/: http://example-mirror.rubygems.org YAML - expect(serializer.load(yaml)).to eq("BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/" => "http://rubygems-mirror.org") + expect(serializer.load(yaml)).to eq("BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/" => "http://example-mirror.rubygems.org") end it "handles arrays inside hashes" do - yaml = strip_whitespace <<-YAML + yaml = <<~YAML --- nested_hash: contains_array: @@ -127,7 +140,7 @@ RSpec.describe Bundler::YAMLSerializer do end it "handles windows-style CRLF line endings" do - yaml = strip_whitespace(<<-YAML).gsub("\n", "\r\n") + yaml = <<~YAML.gsub("\n", "\r\n") --- nested_hash: contains_array: @@ -148,6 +161,34 @@ RSpec.describe Bundler::YAMLSerializer do expect(serializer.load(yaml)).to eq(hash) end + + it "handles empty array" do + yaml = <<~YAML + --- + empty_array: [] + YAML + + hash = { + "empty_array" => [], + } + + expect(serializer.load(yaml)).to eq(hash) + end + + it "skip commented out words" do + yaml = <<~YAML + --- + foo: bar + buzz: foo # bar + YAML + + hash = { + "foo" => "bar", + "buzz" => "foo", + } + + expect(serializer.load(yaml)).to eq(hash) + end end describe "against yaml lib" do diff --git a/spec/bundler/cache/cache_path_spec.rb b/spec/bundler/cache/cache_path_spec.rb index 12385427b1..2a280ea858 100644 --- a/spec/bundler/cache/cache_path_spec.rb +++ b/spec/bundler/cache/cache_path_spec.rb @@ -3,30 +3,30 @@ RSpec.describe "bundle package" do before do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G end context "with --cache-path" do it "caches gems at given path" do bundle :cache, "cache-path" => "vendor/cache-foo" - expect(bundled_app("vendor/cache-foo/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache-foo/myrack-1.0.0.gem")).to exist end end context "with config cache_path" do it "caches gems at given path" do - bundle "config set cache_path vendor/cache-foo" + bundle_config "cache_path vendor/cache-foo" bundle :cache - expect(bundled_app("vendor/cache-foo/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache-foo/myrack-1.0.0.gem")).to exist end end context "with absolute --cache-path" do it "caches gems at given path" do - bundle :cache, "cache-path" => "/tmp/cache-foo" - expect(bundled_app("/tmp/cache-foo/rack-1.0.0.gem")).to exist + bundle :cache, "cache-path" => bundled_app("vendor/cache-foo") + expect(bundled_app("vendor/cache-foo/myrack-1.0.0.gem")).to exist end end end diff --git a/spec/bundler/cache/gems_spec.rb b/spec/bundler/cache/gems_spec.rb index 89d6d41570..198279d84c 100644 --- a/spec/bundler/cache/gems_spec.rb +++ b/spec/bundler/cache/gems_spec.rb @@ -4,22 +4,23 @@ RSpec.describe "bundle cache" do shared_examples_for "when there are only gemsources" do before :each do gemfile <<-G - gem 'rack' + source "https://gem.repo1" + gem 'myrack' G - system_gems "rack-1.0.0", :path => :bundle_path - bundle! :cache + system_gems "myrack-1.0.0", path: path + bundle :cache end it "copies the .gem file to vendor/cache" do - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/myrack-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") + build_gem "omg", path: bundled_app("vendor/cache") install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "omg" G @@ -27,131 +28,167 @@ RSpec.describe "bundle cache" do end it "uses the cache as a source when installing gems with --local" do - system_gems [], :path => :bundle_path + system_gems [], path: default_bundle_path bundle "install --local" - expect(the_bundle).to include_gems("rack 1.0.0") + expect(the_bundle).to include_gems("myrack 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'" + build_gem "myrack", "1.0.0", path: bundled_app("vendor/cache") do |s| + s.write "lib/myrack.rb", "MYRACK = 'FAIL'" end install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G - expect(the_bundle).to include_gems("rack 1.0.0") + expect(the_bundle).to include_gems("myrack 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", :path => :bundle_path + system_gems "myrack-1.0.0", path: default_bundle_path gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G - build_gem "rack", "1.0.0", :path => bundled_app("vendor/cache") do |s| - s.write "lib/rack.rb", "RACK = 'FAIL'" + build_gem "myrack", "1.0.0", path: bundled_app("vendor/cache") do |s| + s.write "lib/myrack.rb", "MYRACK = 'FAIL'" end - bundle! :install, :local => true - expect(the_bundle).to include_gems("rack 1.0.0") + bundle :install, local: true + expect(the_bundle).to include_gems("myrack 1.0.0") end it "creates a lockfile" do - cache_gems "rack-1.0.0" + cache_gems "myrack-1.0.0" gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G bundle "cache" - expect(bundled_app("Gemfile.lock")).to exist + expect(bundled_app_lock).to exist end end context "using system gems" do - before { bundle! "config set path.system true" } + before { bundle_config "path.system true" } + let(:path) { system_gem_path } it_behaves_like "when there are only gemsources" end context "installing into a local path" do - before { bundle! "config set path ./.bundle" } + before { bundle_config "path ./.bundle" } + let(:path) { local_gem_path } it_behaves_like "when there are only gemsources" end - describe "when there is a built-in gem" do + describe "when there is a built-in gem", :ruby_repo do + let(:default_json_version) { ruby "gem 'json'; require 'json'; puts JSON::VERSION" } + before :each do - build_repo2 do - build_gem "builtin_gem", "1.0.2" + build_gem "json", default_json_version, to_system: true, default: true + end + + context "when a remote gem is available for caching" do + before do + build_repo2 do + build_gem "json", default_json_version + end end - build_gem "builtin_gem", "1.0.2", :to_system => true do |s| - s.summary = "This builtin_gem is bundled with Ruby" + it "uses remote gems when installing" do + install_gemfile %(source "https://gem.repo2"; gem 'json', '#{default_json_version}'), verbose: true + expect(out).to include("Installing json #{default_json_version}") end - FileUtils.rm("#{system_gem_path}/cache/builtin_gem-1.0.2.gem") - end + it "does not use remote gems when installing with --local flag" do + install_gemfile %(source "https://gem.repo2"; gem 'json', '#{default_json_version}'), verbose: true, local: true + expect(out).to include("Using json #{default_json_version}") + end - it "uses builtin gems when installing to system gems" do - bundle! "config set path.system true" - install_gemfile %(gem 'builtin_gem', '1.0.2') - expect(the_bundle).to include_gems("builtin_gem 1.0.2") - end + it "does not use remote gems when installing with --prefer-local flag" do + install_gemfile %(source "https://gem.repo2"; gem 'json', '#{default_json_version}'), verbose: true, "prefer-local": true + expect(out).to include("Using json #{default_json_version}") + end - it "caches remote and builtin gems" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem 'builtin_gem', '1.0.2' - gem 'rack', '1.0.0' - G + it "caches remote and builtin gems" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'json', '#{default_json_version}' + gem 'myrack', '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 + bundle :cache + expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/json-#{default_json_version}.gem")).to exist + end + + it "caches builtin gems when cache_all_platforms is set" do + gemfile <<-G + source "https://gem.repo2" + gem "json" + G + + bundle_config "cache_all_platforms true" - 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" + bundle :cache + expect(bundled_app("vendor/cache/json-#{default_json_version}.gem")).to exist end - install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem 'builtin_gem_2', '1.0.2' - G + it "doesn't make remote request after caching the gem" do + build_gem "builtin_gem_2", "1.0.2", path: bundled_app("vendor/cache"), default: true - bundle "install --local" - expect(the_bundle).to include_gems("builtin_gem_2 1.0.2") + install_gemfile <<-G + source "https://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 end - it "errors if the builtin gem isn't available to cache" do - bundle! "config set path.system true" + context "when a remote gem is not available for caching" do + it "warns, but uses builtin gems when installing to system gems" do + bundle_config "path.system true" + install_gemfile %(source "https://gem.repo1"; gem 'json', '#{default_json_version}'), verbose: true + expect(err).to include("json-#{default_json_version} is built in to Ruby, and can't be cached") + expect(out).to include("Using json #{default_json_version}") + end - install_gemfile <<-G - gem 'builtin_gem', '1.0.2' - G + it "errors when explicitly caching" do + bundle_config "path.system true" - bundle :cache - expect(exitstatus).to_not eq(0) if exitstatus - expect(err).to include("builtin_gem-1.0.2 is built in to Ruby, and can't be cached") + install_gemfile <<-G + source "https://gem.repo1" + gem 'json', '#{default_json_version}' + G + + bundle :cache, raise_on_error: false + expect(last_command).to be_failure + expect(err).to include("json-#{default_json_version} is built in to Ruby, and can't be cached") + end end end describe "when there are also git sources" do before do build_git "foo" - system_gems "rack-1.0.0" + system_gems "myrack-1.0.0" install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" git "#{lib_path("foo-1.0")}" do gem 'foo' end - gem 'rack' + gem 'myrack' G end @@ -161,49 +198,57 @@ RSpec.describe "bundle cache" do system_gems [] bundle "install --local" - expect(the_bundle).to include_gems("rack 1.0.0", "foo 1.0") + expect(the_bundle).to include_gems("myrack 1.0.0", "foo 1.0") end it "should not explode if the lockfile is not present" do - FileUtils.rm(bundled_app("Gemfile.lock")) + FileUtils.rm(bundled_app_lock) bundle :cache - expect(bundled_app("Gemfile.lock")).to exist + expect(bundled_app_lock).to exist end end describe "when previously cached" do - before :each do + let :setup_main_repo do build_repo2 install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack" + source "https://gem.repo2" + gem "myrack" gem "actionpack" G bundle :cache - expect(cached_gem("rack-1.0.0")).to exist + expect(cached_gem("myrack-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 + setup_main_repo + FileUtils.rm_rf cached_gem("myrack-1.0.0") bundle :install expect(out).to include("Updating files in vendor/cache") - expect(cached_gem("rack-1.0.0")).to exist + expect(cached_gem("myrack-1.0.0")).to exist end it "adds and removes when gems are updated" do - update_repo2 - bundle "update", :all => true - expect(cached_gem("rack-1.2")).to exist - expect(cached_gem("rack-1.0.0")).not_to exist + setup_main_repo + update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + end + + bundle "update", all: true + expect(cached_gem("myrack-1.2")).to exist + expect(cached_gem("myrack-1.0.0")).not_to exist end it "adds new gems and dependencies" do + setup_main_repo install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "rails" G expect(cached_gem("rails-2.3.2")).to exist @@ -211,24 +256,26 @@ RSpec.describe "bundle cache" do end it "removes .gems for removed gems and dependencies" do + setup_main_repo install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack" + source "https://gem.repo2" + gem "myrack" G - expect(cached_gem("rack-1.0.0")).to exist + expect(cached_gem("myrack-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" + setup_main_repo + build_git "myrack" install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack", :git => "#{lib_path("rack-1.0")}" + source "https://gem.repo2" + gem "myrack", :git => "#{lib_path("myrack-1.0")}" gem "actionpack" G - expect(cached_gem("rack-1.0.0")).not_to exist + expect(cached_gem("myrack-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 @@ -236,7 +283,7 @@ RSpec.describe "bundle cache" do it "doesn't remove gems that are for another platform" do simulate_platform "java" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "platform_specific" G @@ -244,37 +291,111 @@ RSpec.describe "bundle cache" do expect(cached_gem("platform_specific-1.0-java")).to exist end - simulate_new_machine - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "platform_specific" - G + pristine_system_gems + + simulate_platform "x86-darwin-100" do + install_gemfile <<-G + source "https://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 + expect(cached_gem("platform_specific-1.0-x86-darwin-100")).to exist + expect(cached_gem("platform_specific-1.0-java")).to exist + end 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 + it "doesn't remove gems cached gems that don't match their remote counterparts, but also refuses to install and prints an error" do + setup_main_repo + cached_myrack = cached_gem("myrack-1.0.0") + FileUtils.rm_rf cached_myrack + build_gem "myrack", "1.0.0", + path: cached_myrack.parent, + rubygems_version: "1.3.2" + FileUtils.rm_r default_bundle_path + default_system_gems + + FileUtils.rm bundled_app_lock + bundle :install, raise_on_error: false + + expect(err).to eq <<~E.strip + Bundler found mismatched checksums. This is a potential security risk. + #{checksum_to_lock(gem_repo2, "myrack", "1.0.0")} + from the API at https://gem.repo2/ + #{checksum_from_package(cached_myrack, "myrack", "1.0.0")} + from the gem at #{cached_myrack} + + If you trust the API at https://gem.repo2/, to resolve this issue you can: + 1. remove the gem at #{cached_myrack} + 2. run `bundle install` + + To ignore checksum security warnings, disable checksum validation with + `bundle config set --local disable_checksum_validation true` + E + + expect(cached_gem("myrack-1.0.0")).to exist + end + + it "raises an error when a cached gem is altered and produces a different checksum than the remote gem" do + setup_main_repo + FileUtils.rm_rf cached_gem("myrack-1.0.0") + build_gem "myrack", "1.0.0", path: bundled_app("vendor/cache") + + checksums = checksums_section do |c| + c.checksum gem_repo1, "myrack", "1.0.0" + end + + FileUtils.rm_r default_bundle_path + default_system_gems + + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + myrack (1.0.0) + #{checksums} + L + + bundle :install, raise_on_error: false + expect(exitstatus).to eq(37) + expect(err).to include("Bundler found mismatched checksums.") + expect(err).to include("1. remove the gem at #{cached_gem("myrack-1.0.0")}") + + expect(cached_gem("myrack-1.0.0")).to exist + FileUtils.rm_rf cached_gem("myrack-1.0.0") bundle :install - expect(cached_gem("rack-1.0.0")).to exist + expect(cached_gem("myrack-1.0.0")).to exist + end + + it "installs a modified gem with a non-matching checksum when the API implementation does not provide checksums" do + setup_main_repo + FileUtils.rm_rf cached_gem("myrack-1.0.0") + build_gem "myrack", "1.0.0", path: bundled_app("vendor/cache") + pristine_system_gems + + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + myrack (1.0.0) + L + + bundle :install, artifice: "endpoint", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s } + expect(cached_gem("myrack-1.0.0")).to exist end it "handles directories and non .gem files in the cache" do + setup_main_repo 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 + setup_main_repo install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G bundle "cache" bundle "install" @@ -282,9 +403,10 @@ RSpec.describe "bundle cache" do end it "does not warn about all if it doesn't have any git/path dependency" do + setup_main_repo install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G bundle "cache" expect(out).not_to match(/\-\-all/) @@ -292,9 +414,10 @@ RSpec.describe "bundle cache" do 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") + path: bundled_app("vendor/cache") install_gemfile <<-G + source "https://gem.repo1" gem "foo-bundler" G diff --git a/spec/bundler/cache/git_spec.rb b/spec/bundler/cache/git_spec.rb index 75525d405b..f0976ecac7 100644 --- a/spec/bundler/cache/git_spec.rb +++ b/spec/bundler/cache/git_spec.rb @@ -13,105 +13,121 @@ RSpec.describe "git base name" do end RSpec.describe "bundle cache with git" do + it "does not copy repository to vendor cache when cache_all set to false" do + git = build_git "foo" + ref = git.ref_for("main", 11) + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + bundle_config "cache_all false" + bundle :cache + expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).not_to exist + + expect(the_bundle).to include_gems "foo 1.0" + end + it "copies repository to vendor cache and uses it" do git = build_git "foo" - ref = git.ref_for("master", 11) + ref = git.ref_for("main", 11) install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => '#{lib_path("foo-1.0")}' G - bundle "config set cache_all true" bundle :cache 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") + FileUtils.rm_r 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 + it "copies repository to vendor cache and uses it even when configured with `path`" do git = build_git "foo" - ref = git.ref_for("master", 11) + ref = git.ref_for("main", 11) install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => '#{lib_path("foo-1.0")}' G - bundle "install --path vendor/bundle" - bundle "config set cache_all true" + bundle_config "path vendor/bundle" + bundle "install" bundle :cache 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") + FileUtils.rm_r 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 + install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => '#{lib_path("foo-1.0")}' G - bundle "config set cache_all true" - bundle! :cache - bundle! :cache + bundle :cache + bundle :cache expect(out).to include "Updating files in vendor/cache" - FileUtils.rm_rf lib_path("foo-1.0") + FileUtils.rm_r 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) + old_ref = git.ref_for("main", 11) install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => '#{lib_path("foo-1.0")}' G - bundle "config set cache_all true" bundle :cache update_git "foo" do |s| s.write "lib/foo.rb", "puts :CACHE" end - ref = git.ref_for("master", 11) + ref = git.ref_for("main", 11) expect(ref).not_to eq(old_ref) - bundle! "update", :all => true - bundle "config set cache_all true" - bundle! :cache + bundle "update", all: true + bundle :cache 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'" + FileUtils.rm_r 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) + old_ref = git.ref_for("main", 11) install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => '#{lib_path("foo-1.0")}' G - bundle "config set cache_all true" - bundle! :cache + bundle :cache update_git "foo" do |s| s.write "lib/foo.rb", "puts :CACHE" end - ref = git.ref_for("master", 11) + ref = git.ref_for("main", 11) expect(ref).not_to eq(old_ref) bundle "update foo" @@ -119,22 +135,22 @@ RSpec.describe "bundle cache with git" do 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") + FileUtils.rm_r 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) + ref = git.ref_for("main", 11) gemfile <<-G - gem "foo", :git => '#{lib_path("foo-invalid")}', :branch => :master + source "https://gem.repo1" + gem "foo", :git => '#{lib_path("foo-invalid")}', :branch => :main G bundle %(config set local.foo #{lib_path("foo-1.0")}) bundle "install" - bundle "config set cache_all true" bundle :cache expect(bundled_app("vendor/cache/foo-invalid-#{ref}")).to exist @@ -148,57 +164,193 @@ RSpec.describe "bundle cache with git" do expect(out).to eq("LOCAL") end - it "copies repository to vendor cache, including submodules" do - build_git "submodule", "1.0" + it "can use gems after copying install folder to a different machine with git not installed" do + build_git "foo" - git = build_git "has_submodule", "1.0" do |s| - s.add_dependency "submodule" - end + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + bundle_config "path vendor/bundle" + bundle :install - 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"` + pristine_system_gems + with_path_as "" do + bundle_config "deployment true" + bundle "install --local" + expect(the_bundle).to include_gem "foo 1.0" end + end - install_gemfile <<-G - git "#{lib_path("has_submodule-1.0")}", :submodules => true do - gem "has_submodule" - end + it "can install after bundle cache without cloning remote repositories" do + build_git "foo" + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => '#{lib_path("foo-1.0")}' G + bundle :cache, "all-platforms" => true - ref = git.ref_for("master", 11) - bundle "config set cache_all true" - bundle :cache + pristine_system_gems + bundle_config "frozen true" + bundle "install --local --verbose" + expect(out).to_not include("Fetching") + expect(the_bundle).to include_gem "foo 1.0" + end - 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" + it "can install after bundle cache without cloning remote repositories even without the original cache" do + build_git "foo" + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + bundle :cache, "all-platforms" => true + + pristine_system_gems + bundle_config "frozen true" + bundle "install --local --verbose" + expect(out).to_not include("Fetching") + expect(the_bundle).to include_gem "foo 1.0" end - it "displays warning message when detecting git repo in Gemfile", :bundler => "< 3" do + it "can install after bundle cache without cloning remote repositories with only git tracked files" do build_git "foo" - install_gemfile <<-G + gemfile <<-G + source "https://gem.repo1" gem "foo", :git => '#{lib_path("foo-1.0")}' G + bundle :cache, "all-platforms" => true - bundle :cache + pristine_system_gems + bundle_config "frozen true" - expect(err).to include("Your Gemfile contains path and git dependencies.") + # Remove untracked files (including the empty refs dir in the cache) + Dir.chdir(bundled_app) do + system(*%W[git init --quiet]) + system(*%W[git add --all]) + system(*%W[git clean -d --force --quiet]) + end + + bundle "install --local --verbose" + expect(out).to_not include("Fetching") + expect(the_bundle).to include_gem "foo 1.0" end - it "does not display warning message if cache_all is set in bundle config" do - build_git "foo" + it "installs properly a bundler 2.5.17-2.5.23 cache as a bare repository without cloning remote repositories" do + git = build_git "foo" - install_gemfile <<-G + short_ref = git.ref_for("main", 11) + cache_dir = bundled_app("vendor/cache/foo-1.0-#{short_ref}") + + gemfile <<-G + source "https://gem.repo1" gem "foo", :git => '#{lib_path("foo-1.0")}' G + bundle_config "global_gem_cache false" + bundle_config "path vendor/bundle" + bundle :install - bundle "config set cache_all true" - bundle :cache + # Simulate old cache by copying the real cache folder to vendor/cache + FileUtils.mkdir_p bundled_app("vendor/cache") + FileUtils.cp_r "#{Dir.glob(vendored_gems("cache/bundler/git/foo-1.0-*")).first}/.", cache_dir + FileUtils.rm_r bundled_app("vendor/bundle") + + bundle "install --local --verbose" + expect(err).to include("Installing from cache in old \"bare repository\" format for compatibility") + + expect(out).to_not include("Fetching") + + # leaves old cache alone + expect(cache_dir.join("lib/foo.rb")).not_to exist + expect(cache_dir.join("HEAD")).to exist + + expect(the_bundle).to include_gem "foo 1.0" + end + + it "migrates a bundler 2.5.17-2.5.23 cache as a bare repository when not running with --local" do + git = build_git "foo" + + short_ref = git.ref_for("main", 11) + cache_dir = bundled_app("vendor/cache/foo-1.0-#{short_ref}") + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + bundle_config "global_gem_cache false" + bundle_config "path vendor/bundle" + bundle :install + + # Simulate old cache by copying the real cache folder to vendor/cache + FileUtils.mkdir_p bundled_app("vendor/cache") + FileUtils.cp_r "#{Dir.glob(vendored_gems("cache/bundler/git/foo-1.0-*")).first}/.", cache_dir + FileUtils.rm_r bundled_app("vendor/bundle") + + bundle "install --verbose" + expect(out).to include("Fetching") + + # migrates old cache alone + expect(cache_dir.join("lib/foo.rb")).to exist + expect(cache_dir.join("HEAD")).not_to exist + + expect(the_bundle).to include_gem "foo 1.0" + end + + it "migrates a bundler 2.5.17-2.5.23 cache as a bare repository when running `bundle cache`, even if gems already installed" do + git = build_git "foo" + + short_ref = git.ref_for("main", 11) + cache_dir = bundled_app("vendor/cache/foo-1.0-#{short_ref}") + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + bundle_config "global_gem_cache false" + bundle_config "path vendor/bundle" + bundle :install + + # Simulate old cache by copying the real cache folder to vendor/cache + FileUtils.mkdir_p bundled_app("vendor/cache") + FileUtils.cp_r "#{Dir.glob(vendored_gems("cache/bundler/git/foo-1.0-*")).first}/.", cache_dir + + bundle "cache" + + # migrates old cache alone + expect(cache_dir.join("lib/foo.rb")).to exist + expect(cache_dir.join("HEAD")).not_to exist + + expect(the_bundle).to include_gem "foo 1.0" + end + + it "copies repository to vendor cache, including submodules" do + # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ + system(*%W[git config --global protocol.file.allow always]) + + build_git "submodule", "1.0" + + git = build_git "has_submodule", "1.0" do |s| + s.add_dependency "submodule" + end + + git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") + git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("has_submodule-1.0")}", :submodules => true do + gem "has_submodule" + end + G + + ref = git.ref_for("main", 11) bundle :cache - expect(err).not_to include("Your Gemfile contains path and git dependencies.") + 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 "caches pre-evaluated gemspecs" do @@ -210,12 +362,12 @@ RSpec.describe "bundle cache with git" do update_git("foo") {|s| s.write "foo.gemspec", spec_lines.join("\n") } install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => '#{lib_path("foo-1.0")}' G - bundle "config set cache_all true" bundle :cache - ref = git.ref_for("master", 11) + ref = git.ref_for("main", 11) gemspec = bundled_app("vendor/cache/foo-1.0-#{ref}/foo.gemspec").read expect(gemspec).to_not match("`echo bob`") end @@ -224,16 +376,136 @@ RSpec.describe "bundle cache with git" do build_git "foo" gemfile <<-G + source "https://gem.repo1" gem "foo", :git => '#{lib_path("foo-1.0")}' G - bundle! "config set cache_all true" - bundle! :cache, "all-platforms" => true, :install => false, :path => "./vendor/cache" + bundle :cache, "all-platforms" => true, :install => false - simulate_new_machine + pristine_system_gems with_path_as "" do - bundle! "config set deployment true" - bundle! :install, :local => true + bundle_config "deployment true" + bundle :install, local: true expect(the_bundle).to include_gem "foo 1.0" end end + + it "can install after bundle cache generated with an older Bundler that kept checkouts in the cache" do + git = build_git("foo") + locked_revision = git.ref_for("main") + path_revision = git.ref_for("main", 11) + + git_path = lib_path("foo-1.0") + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => '#{git_path}' + G + lockfile <<~L + GIT + remote: #{git_path}/ + revision: #{locked_revision} + specs: + foo (1.0) + + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + L + + # Simulate an old incorrect situation where vendor/cache would be the install location of git gems + FileUtils.mkdir_p bundled_app("vendor/cache") + FileUtils.cp_r git_path, bundled_app("vendor/cache/foo-1.0-#{path_revision}") + FileUtils.rm_r bundled_app("vendor/cache/foo-1.0-#{path_revision}/.git") + + bundle :install, env: { "BUNDLE_DEPLOYMENT" => "true", "BUNDLE_CACHE_ALL" => "true" } + end + + it "respects the --no-install flag" do + git = build_git "foo", &:add_c_extension + ref = git.ref_for("main", 11) + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + # The algorithm for the cache location for a git checkout is + # in Bundle::Source::Git#cache_path + cache_path_name = "foo-1.0-#{Digest(:SHA1).hexdigest(lib_path("foo-1.0").to_s)}" + + # Run this test twice. This is because materially different codepaths + # will get hit the second time around. + # The first time, Bundler::Sources::Git#install_path is set to the system + # wide cache directory bundler/gems; the second time, it's set to the + # vendor/cache directory. We don't want the native extension to appear in + # either of these places, so run the `bundle cache` command twice. + 2.times do + bundle :cache, "all-platforms" => true, :install => false + + # it did _NOT_ actually install the gem - neither in $GEM_HOME (bundler 2 mode), + # nor in .bundle (bundler 4 mode) + expect(Pathname.new(File.join(default_bundle_path, "gems/foo-1.0-#{ref}"))).to_not exist + # it _did_ cache the gem in vendor/ + expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist + # it did _NOT_ build the gems extensions in the vendor/ dir + expect(Dir[bundled_app("vendor/cache/foo-1.0-#{ref}/lib/foo_c*")]).to be_empty + # it _did_ cache the git checkout + expect(default_cache_path("git", cache_path_name)).to exist + # And the checkout is a bare checkout + expect(default_cache_path("git", cache_path_name, "HEAD")).to exist + end + + # Subsequently installing the gem should compile it. + # _currently_, the gem gets compiled in vendor/cache, and vendor/cache is added + # to the $LOAD_PATH for git extensions, so it all kind of "works". However, in the + # future we would like to stop adding vendor/cache to the $LOAD_PATH for git extensions + # and instead treat them identically to normal gems (where the gem install location, + # not the cache location, is added to $LOAD_PATH). + # Verify that the compilation worked and the result is in $LOAD_PATH by simply attempting + # to require it; that should make sure this spec does not break if the load path behaviour + # is changed. + bundle :install, local: true + ruby <<~R, raise_on_error: false + require 'bundler/setup' + require 'foo_c' + R + expect(last_command).to_not be_failure + end + + it "doesn't fail when git gem has extensions and an empty cache folder is present before bundle install" do + build_git "puma" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.executables = "puma" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("../lib", __FILE__) + FileUtils.mkdir_p(path) + File.open("\#{path}/puma.rb", "w") do |f| + f.puts "PUMA = 'YES'" + end + end + RUBY + end + + FileUtils.mkdir_p(bundled_app("vendor/cache")) + + install_gemfile <<-G + source "https://gem.repo1" + gem "puma", :git => "#{lib_path("puma-1.0")}" + G + + bundle "exec puma" + + expect(out).to eq("YES") + end end diff --git a/spec/bundler/cache/path_spec.rb b/spec/bundler/cache/path_spec.rb index 79e8b4a82b..42648aea1f 100644 --- a/spec/bundler/cache/path_spec.rb +++ b/spec/bundler/cache/path_spec.rb @@ -2,13 +2,13 @@ RSpec.describe "bundle cache with path" do it "is no-op when the path is within the bundle" do - build_lib "foo", :path => bundled_app("lib/foo") + build_lib "foo", path: bundled_app("lib/foo") install_gemfile <<-G + source "https://gem.repo1" gem "foo", :path => '#{bundled_app("lib/foo")}' G - bundle "config set cache_all true" bundle :cache expect(bundled_app("vendor/cache/foo-1.0")).not_to exist expect(the_bundle).to include_gems "foo 1.0" @@ -18,34 +18,32 @@ RSpec.describe "bundle cache with path" do build_lib "foo" install_gemfile <<-G + source "https://gem.repo1" gem "foo", :path => '#{lib_path("foo-1.0")}' G - bundle "config set cache_all true" bundle :cache 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) + libname = File.basename(bundled_app) + "_gem" + libpath = File.join(File.dirname(bundled_app), libname) - build_lib libname, :path => libpath + build_lib libname, path: libpath install_gemfile <<-G + source "https://gem.repo1" gem "#{libname}", :path => '#{libpath}' G - bundle "config set cache_all true" bundle :cache 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 @@ -53,10 +51,10 @@ RSpec.describe "bundle cache with path" do build_lib "foo" install_gemfile <<-G + source "https://gem.repo1" gem "foo", :path => '#{lib_path("foo-1.0")}' G - bundle "config set cache_all true" bundle :cache build_lib "foo" do |s| @@ -66,7 +64,6 @@ RSpec.describe "bundle cache with path" do bundle :cache 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") @@ -76,69 +73,49 @@ RSpec.describe "bundle cache with path" do build_lib "foo" install_gemfile <<-G + source "https://gem.repo1" gem "foo", :path => '#{lib_path("foo-1.0")}' G - bundle "config set cache_all true" bundle :cache - install_gemfile <<-G - gem "bar", :path => '#{lib_path("bar-1.0")}' - G - - bundle :cache - expect(bundled_app("vendor/cache/bar-1.0")).not_to exist - end + expect(bundled_app("vendor/cache/foo-1.0")).to exist - it "raises a warning without --all", :bundler => "< 3" do - build_lib "foo" + build_lib "bar" install_gemfile <<-G - gem "foo", :path => '#{lib_path("foo-1.0")}' + source "https://gem.repo1" + gem "bar", :path => '#{lib_path("bar-1.0")}' G bundle :cache - expect(err).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 + it "does not cache path gems if cache_all is set to false" do build_lib "foo" install_gemfile <<-G + source "https://gem.repo1" gem "foo", :path => '#{lib_path("foo-1.0")}' G + bundle_config "cache_all false" - bundle "config set cache_all true" bundle :cache - build_lib "bar" - - install_gemfile <<-G - gem "foo", :path => '#{lib_path("foo-1.0")}' - gem "bar", :path => '#{lib_path("bar-1.0")}' - G - - bundle :cache - expect(bundled_app("vendor/cache/bar-1.0")).to exist + expect(err).to be_empty + expect(bundled_app("vendor/cache/foo-1.0")).not_to exist end - it "can rewind chosen configuration" do + it "caches path gems by default" do build_lib "foo" install_gemfile <<-G + source "https://gem.repo1" gem "foo", :path => '#{lib_path("foo-1.0")}' G - bundle "config set cache_all true" bundle :cache - build_lib "baz" - - gemfile <<-G - gem "foo", :path => '#{lib_path("foo-1.0")}' - gem "baz", :path => '#{lib_path("baz-1.0")}' - G - - bundle "cache --no-all" - expect(bundled_app("vendor/cache/baz-1.0")).not_to exist + expect(err).to be_empty + expect(bundled_app("vendor/cache/foo-1.0")).to exist end end diff --git a/spec/bundler/cache/platform_spec.rb b/spec/bundler/cache/platform_spec.rb index b65bb06ae6..71c0eaee8e 100644 --- a/spec/bundler/cache/platform_spec.rb +++ b/spec/bundler/cache/platform_spec.rb @@ -3,10 +3,10 @@ RSpec.describe "bundle cache with multiple platforms" do before :each do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" platforms :mri, :rbx do - gem "rack", "1.0.0" + gem "myrack", "1.0.0" end platforms :jruby do @@ -16,9 +16,9 @@ RSpec.describe "bundle cache with multiple platforms" do lockfile <<-G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: - rack (1.0.0) + myrack (1.0.0) activesupport (2.3.5) PLATFORMS @@ -26,24 +26,24 @@ RSpec.describe "bundle cache with multiple platforms" do java DEPENDENCIES - rack (1.0.0) + myrack (1.0.0) activesupport (2.3.5) G - cache_gems "rack-1.0.0", "activesupport-2.3.5" + cache_gems "myrack-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" + bundle "install" - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/myrack-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", :all => true + bundle "update", all: true - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/myrack-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 index 35fd43d3d2..162650f2e5 100644 --- a/spec/bundler/commands/add_spec.rb +++ b/spec/bundler/commands/add_spec.rb @@ -9,189 +9,386 @@ RSpec.describe "bundle add" do build_gem "bar", "0.12.3" build_gem "cat", "0.12.3.pre" build_gem "dog", "1.1.3.pre" + build_gem "lemur", "3.1.1.pre.2023.1.1" end build_git "foo", "2.0" - install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + gemfile <<-G + source "https://gem.repo2" gem "weakling", "~> 0.0.1" G end context "when no gems are specified" do it "shows error" do - bundle "add" + bundle "add", raise_on_error: false expect(err).to include("Please specify gems to add") end end + context "when Gemfile is empty, and frozen mode is set" do + it "shows error" do + gemfile 'source "https://gem.repo2"' + bundle "add bar", raise_on_error: false, env: { "BUNDLE_FROZEN" => "true" } + + expect(err).to include("Frozen mode is set, but there's no lockfile") + end + end + describe "without version specified" do - it "version requirement becomes ~> major.minor.patch when resolved version is < 1.0" 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(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 + 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(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 + 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(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 + 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(bundled_app_gemfile.read).to match(/gem "dog", ">= 1.1.pre"/) expect(the_bundle).to include_gems "dog 1.1.3.pre" end + + it "version requirement becomes >= major.minor.pre.tail when resolved version has a very long tail pre version" do + bundle "add 'lemur'" + # the trailing pre purposely matches the release version to ensure that subbing the release doesn't change the pre.version" + expect(bundled_app_gemfile.read).to match(/gem "lemur", ">= 3.1.pre.2023.1.1"/) + expect(the_bundle).to include_gems "lemur 3.1.1.pre.2023.1.1" + 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(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 requirements = ["< 3.0", "> 1.0"] bundle "add 'foo' --version='#{requirements.join(", ")}'" - expect(bundled_app("Gemfile").read).to match(/gem "foo", #{Gem::Requirement.new(requirements).as_list.map(&:dump).join(', ')}/) + expect(bundled_app_gemfile.read).to match(/gem "foo", #{Gem::Requirement.new(requirements).as_list.map(&:dump).join(", ")}/) expect(the_bundle).to include_gems "foo 2.0" end end + describe "with --require" do + it "adds the require param for the gem" do + bundle "add 'foo' --require=foo/engine" + expect(bundled_app_gemfile.read).to match(%r{gem "foo",(?: .*,) require: "foo\/engine"}) + end + + it "converts false to a boolean" do + bundle "add 'foo' --require=false" + expect(bundled_app_gemfile.read).to match(/gem "foo",(?: .*,) require: false/) + 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(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(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_uri_for(gem_repo2)}'" + bundle "add 'foo' --source='https://gem.repo2'" - expect(bundled_app("Gemfile").read).to match(/gem "foo", "~> 2.0", :source => "#{file_uri_for(gem_repo2)}"/) + expect(bundled_app_gemfile.read).to match(%r{gem "foo", ">= 2.0", source: "https://gem.repo2"}) + expect(the_bundle).to include_gems "foo 2.0" + end + end + + describe "with --path" do + it "adds dependency with specified path" do + bundle "add 'foo' --path='#{lib_path("foo-2.0")}'" + + expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2.0", path: "#{lib_path("foo-2.0")}"/) expect(the_bundle).to include_gems "foo 2.0" end end describe "with --git" do - it "adds dependency with specified github source" do + it "adds dependency with specified git source" do bundle "add foo --git=#{lib_path("foo-2.0")}" - expect(bundled_app("Gemfile").read).to match(/gem "foo", "~> 2.0", :git => "#{lib_path("foo-2.0")}"/) + expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2.0", git: "#{lib_path("foo-2.0")}"/) expect(the_bundle).to include_gems "foo 2.0" end end describe "with --git and --branch" do before do - update_git "foo", "2.0", :branch => "test" + update_git "foo", "2.0", branch: "test" end - it "adds dependency with specified github source and branch" do + it "adds dependency with specified git source and branch" do bundle "add foo --git=#{lib_path("foo-2.0")} --branch=test" - expect(bundled_app("Gemfile").read).to match(/gem "foo", "~> 2.0", :git => "#{lib_path("foo-2.0")}", :branch => "test"/) + expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2.0", git: "#{lib_path("foo-2.0")}", branch: "test"/) + expect(the_bundle).to include_gems "foo 2.0" + end + end + + describe "with --git and --ref" do + it "adds dependency with specified git source and branch" do + bundle "add foo --git=#{lib_path("foo-2.0")} --ref=#{revision_for(lib_path("foo-2.0"))}" + + expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2\.0", git: "#{lib_path("foo-2.0")}", ref: "#{revision_for(lib_path("foo-2.0"))}"/) + expect(the_bundle).to include_gems "foo 2.0" + end + end + + describe "with --github" do + before do + build_git "rake", "13.0" + git("config --global url.file://#{lib_path("rake-13.0")}.insteadOf https://github.com/ruby/rake.git") + end + + it "adds dependency with specified github source" do + bundle "add rake --github=ruby/rake" + + expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake"}) + end + + it "adds dependency with specified github source and branch" do + bundle "add rake --github=ruby/rake --branch=main" + + expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake", branch: "main"}) + end + + it "adds dependency with specified github source and ref" do + ref = revision_for(lib_path("rake-13.0")) + bundle "add rake --github=ruby/rake --ref=#{ref}" + + expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake", ref: "#{ref}"}) + end + + it "adds dependency with specified github source and glob" do + bundle "add rake --github=ruby/rake --glob='./*.gemspec'" + + expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake", glob: "\.\/\*\.gemspec"}) + end + + it "adds dependency with specified github source, branch and glob" do + bundle "add rake --github=ruby/rake --branch=main --glob='./*.gemspec'" + + expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake", branch: "main", glob: "\.\/\*\.gemspec"}) + end + + it "adds dependency with specified github source, ref and glob" do + ref = revision_for(lib_path("rake-13.0")) + bundle "add rake --github=ruby/rake --ref=#{ref} --glob='./*.gemspec'" + + expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake", ref: "#{ref}", glob: "\.\/\*\.gemspec"}) + end + end + + describe "with --git and --glob" do + it "adds dependency with specified git source" do + bundle "add foo --git=#{lib_path("foo-2.0")} --glob='./*.gemspec'" + + expect(bundled_app_gemfile.read).to match(%r{gem "foo", ">= 2.0", git: "#{lib_path("foo-2.0")}", glob: "\./\*\.gemspec"}) expect(the_bundle).to include_gems "foo 2.0" end end + describe "with --git and --branch and --glob" do + before do + update_git "foo", "2.0", branch: "test" + end + + it "adds dependency with specified git source and branch" do + bundle "add foo --git=#{lib_path("foo-2.0")} --branch=test --glob='./*.gemspec'" + + expect(bundled_app_gemfile.read).to match(%r{gem "foo", ">= 2.0", git: "#{lib_path("foo-2.0")}", branch: "test", glob: "\./\*\.gemspec"}) + expect(the_bundle).to include_gems "foo 2.0" + end + end + + describe "with --git and --ref and --glob" do + it "adds dependency with specified git source and branch" do + bundle "add foo --git=#{lib_path("foo-2.0")} --ref=#{revision_for(lib_path("foo-2.0"))} --glob='./*.gemspec'" + + expect(bundled_app_gemfile.read).to match(%r{gem "foo", ">= 2\.0", git: "#{lib_path("foo-2.0")}", ref: "#{revision_for(lib_path("foo-2.0"))}", glob: "\./\*\.gemspec"}) + expect(the_bundle).to include_gems "foo 2.0" + end + end + + describe "with mismatched pair in --git/--github, --branch/--ref" do + describe "with --git and --github" do + it "throws error" do + bundle "add 'foo' --git x --github y", raise_on_error: false + + expect(err).to include("You cannot specify `--git` and `--github` at the same time.") + end + end + + describe "with --branch and --ref with --git" do + it "throws error" do + bundle "add 'foo' --branch x --ref y --git file://git", raise_on_error: false + + expect(err).to include("You cannot specify `--branch` and `--ref` at the same time.") + end + end + + describe "with --branch but without --git or --github" do + it "throws error" do + bundle "add 'foo' --branch x", raise_on_error: false + + expect(err).to include("You cannot specify `--branch` unless `--git` or `--github` is specified.") + end + end + + describe "with --ref but without --git or --github" do + it "throws error" do + bundle "add 'foo' --ref y", raise_on_error: false + + expect(err).to include("You cannot specify `--ref` unless `--git` or `--github` is specified.") + end + end + end + describe "with --skip-install" do it "adds gem to Gemfile but is not installed" do bundle "add foo --skip-install --version=2.0" - expect(bundled_app("Gemfile").read).to match(/gem "foo", "= 2.0"/) + expect(bundled_app_gemfile.read).to match(/gem "foo", "= 2.0"/) expect(the_bundle).to_not include_gems "foo 2.0" end end it "using combination of short form options works like long form" do - bundle "add 'foo' -s='#{file_uri_for(gem_repo2)}' -g='development' -v='~>1.0'" - expect(bundled_app("Gemfile").read).to include %(gem "foo", "~> 1.0", :group => :development, :source => "#{file_uri_for(gem_repo2)}") + bundle "add 'foo' -s='https://gem.repo2' -g='development' -v='~>1.0'" + expect(bundled_app_gemfile.read).to include %(gem "foo", "~> 1.0", group: :development, source: "https://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'" + bundle "add 'foo' -v='~>1 . 0'", raise_on_error: false expect(err).to match("Invalid gem requirement pattern '~>1 . 0'") end it "shows error message when gem cannot be found" do - bundle "config set force_ruby_platform true" - bundle "add 'werk_it'" + bundle_config "force_ruby_platform true" + bundle "add 'werk_it'", raise_on_error: false expect(err).to match("Could not find gem 'werk_it' in") - bundle "add 'werk_it' -s='#{file_uri_for(gem_repo2)}'" + bundle "add 'werk_it' -s='https://gem.repo2'", raise_on_error: false expect(err).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'" + bundle "add 'baz' --source='http://badhostasdf'", raise_on_error: false, artifice: "fail" expect(err).to include("Could not reach host badhostasdf. Check your network connection and try again.") - bundle "add 'baz' --source='file://does/not/exist'" + bundle "add 'baz' --source='file://does/not/exist'", raise_on_error: false expect(err).to include("Could not fetch specs from file://does/not/exist/") end describe "with --optimistic" do - it "adds optimistic version" do - bundle! "add 'foo' --optimistic" - expect(bundled_app("Gemfile").read).to include %(gem "foo", ">= 2.0") + it "ignores option" do + bundle "add 'foo' --optimistic" + expect(bundled_app_gemfile.read).to include %(gem "foo", ">= 2.0") expect(the_bundle).to include_gems "foo 2.0" end end + describe "with --pessimistic" do + it "adds pessimistic version" do + bundle "add 'foo' --pessimistic" + expect(bundled_app_gemfile.read).to include %(gem "foo", "~> 2.0") + expect(the_bundle).to include_gems "foo 2.0" + end + end + + describe "with --quiet option" do + it "is quiet when there are no warnings" do + bundle "add 'foo' --quiet" + expect(out).to be_empty + expect(err).to be_empty + end + + it "still displays warning and errors" do + create_file("add_with_warning.rb", <<~RUBY) + require "#{lib_dir}/bundler" + require "#{lib_dir}/bundler/cli" + require "#{lib_dir}/bundler/cli/add" + + module RunWithWarning + def run + super + rescue + Bundler.ui.warn "This is a warning" + raise + end + end + + Bundler::CLI::Add.prepend(RunWithWarning) + RUBY + + bundle "add 'non-existing-gem' --quiet", raise_on_error: false, env: { "RUBYOPT" => "-r#{bundled_app("add_with_warning.rb")}" } + expect(out).to be_empty + expect(err).to include("Could not find gem 'non-existing-gem'") + expect(err).to include("This is a warning") + end + end + describe "with --strict option" do it "adds strict version" do - bundle! "add 'foo' --strict" - expect(bundled_app("Gemfile").read).to include %(gem "foo", "= 2.0") + bundle "add 'foo' --strict" + expect(bundled_app_gemfile.read).to include %(gem "foo", "= 2.0") expect(the_bundle).to include_gems "foo 2.0" end end describe "with no option" do - it "adds pessimistic version" do - bundle! "add 'foo'" - expect(bundled_app("Gemfile").read).to include %(gem "foo", "~> 2.0") + it "adds optimistic version" do + bundle "add 'foo'" + expect(bundled_app_gemfile.read).to include %(gem "foo", ">= 2.0") expect(the_bundle).to include_gems "foo 2.0" end end - describe "with --optimistic and --strict" do + describe "with --pessimistic and --strict" do it "throws error" do - bundle "add 'foo' --strict --optimistic" + bundle "add 'foo' --strict --pessimistic", raise_on_error: false - expect(err).to include("You can not specify `--strict` and `--optimistic` at the same time") + expect(err).to include("You cannot specify `--strict` and `--pessimistic` at the same time") end end context "multiple gems" do it "adds multiple gems to gemfile" do - bundle! "add bar baz" + bundle "add bar baz" - expect(bundled_app("Gemfile").read).to match(/gem "bar", "~> 0.12.3"/) - expect(bundled_app("Gemfile").read).to match(/gem "baz", "~> 1.2"/) + expect(bundled_app_gemfile.read).to match(/gem "bar", ">= 0.12.3"/) + expect(bundled_app_gemfile.read).to match(/gem "baz", ">= 1.2"/) end it "throws error if any of the specified gems are present in the gemfile with different version" do - bundle "add weakling bar" + bundle "add weakling bar", raise_on_error: false expect(err).to include("You cannot specify the same gem twice with different version requirements") expect(err).to include("You specified: weakling (~> 0.0.1) and weakling (>= 0).") @@ -201,51 +398,51 @@ RSpec.describe "bundle add" do describe "when a gem is added which is already specified in Gemfile with version" do it "shows an error when added with different version requirement" do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack", "1.0" + source "https://gem.repo2" + gem "myrack", "1.0" G - bundle "add 'rack' --version=1.1" + bundle "add 'myrack' --version=1.1", raise_on_error: false expect(err).to include("You cannot specify the same gem twice with different version requirements") - expect(err).to include("If you want to update the gem version, run `bundle update rack`. You may also need to change the version requirement specified in the Gemfile if it's too restrictive") + expect(err).to include("If you want to update the gem version, run `bundle update myrack`. You may also need to change the version requirement specified in the Gemfile if it's too restrictive") end it "shows error when added without version requirements" do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack", "1.0" + source "https://gem.repo2" + gem "myrack", "1.0" G - bundle "add 'rack'" + bundle "add 'myrack'", raise_on_error: false expect(err).to include("Gem already added.") expect(err).to include("You cannot specify the same gem twice with different version requirements") - expect(err).not_to include("If you want to update the gem version, run `bundle update rack`. You may also need to change the version requirement specified in the Gemfile if it's too restrictive") + expect(err).not_to include("If you want to update the gem version, run `bundle update myrack`. You may also need to change the version requirement specified in the Gemfile if it's too restrictive") end end describe "when a gem is added which is already specified in Gemfile without version" do it "shows an error when added with different version requirement" do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack" + source "https://gem.repo2" + gem "myrack" G - bundle "add 'rack' --version=1.1" + bundle "add 'myrack' --version=1.1", raise_on_error: false expect(err).to include("You cannot specify the same gem twice with different version requirements") - expect(err).to include("If you want to update the gem version, run `bundle update rack`.") + expect(err).to include("If you want to update the gem version, run `bundle update myrack`.") expect(err).not_to include("You may also need to change the version requirement specified in the Gemfile if it's too restrictive") end end describe "when a gem is added and cache exists" do it "caches all new dependencies added for the specified gem" do - bundle! :cache + bundle :cache - bundle "add 'rack' --version=1.0.0" - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + bundle "add 'myrack' --version=1.0.0" + expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist end end end diff --git a/spec/bundler/commands/binstubs_spec.rb b/spec/bundler/commands/binstubs_spec.rb index df10bd3498..af4d24a9e8 100644 --- a/spec/bundler/commands/binstubs_spec.rb +++ b/spec/bundler/commands/binstubs_spec.rb @@ -4,252 +4,95 @@ 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_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "binstubs rack" + bundle "binstubs myrack" - expect(bundled_app("bin/rackup")).to exist + expect(bundled_app("bin/myrackup")).to exist end it "does not install other binstubs" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" gem "rails" G bundle "binstubs rails" - expect(bundled_app("bin/rackup")).not_to exist + expect(bundled_app("bin/myrackup")).not_to exist expect(bundled_app("bin/rails")).to exist end it "does install multiple binstubs" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" gem "rails" G - bundle "binstubs rails rack" + bundle "binstubs rails myrack" - expect(bundled_app("bin/rackup")).to exist + expect(bundled_app("bin/myrackup")).to exist expect(bundled_app("bin/rails")).to exist end it "allows installing all binstubs" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G + source "https://gem.repo1" gem "rails" G - bundle! :binstubs, :all => true + bundle :binstubs, all: true expect(bundled_app("bin/rails")).to exist expect(bundled_app("bin/rake")).to exist end + it "allows installing binstubs for all platforms" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle "binstubs myrack --all-platforms" + + expect(bundled_app("bin/myrackup")).to exist + expect(bundled_app("bin/myrackup.cmd")).to exist + end + it "displays an error when used without any gem" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "binstubs" - expect(exitstatus).to eq(1) if exitstatus + bundle "binstubs", raise_on_error: false + expect(exitstatus).to eq(1) expect(err).to include("`bundle binstubs` needs at least one gem to run.") end it "displays an error when used with --all and gems" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "binstubs rack", :all => true + bundle "binstubs myrack", all: true, raise_on_error: false expect(last_command).to be_failure expect(err).to include("Cannot specify --all with specific gems") end - context "when generating bundle binstub outside bundler" do - it "should abort" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - G - - bundle "binstubs rack" - - File.open("bin/bundle", "wb") do |file| - file.print "OMG" - end - - sys_exec "bin/rackup" - - expect(err).to include("was not generated by Bundler") - end - end - - context "the bundle binstub" do - before do - if system_bundler_version == :bundler - system_gems :bundler - elsif system_bundler_version - build_repo4 do - build_gem "bundler", system_bundler_version do |s| - s.executables = "bundle" - s.bindir = "exe" - s.write "exe/bundle", "puts %(system bundler #{system_bundler_version}\\n\#{ARGV.inspect})" - end - end - system_gems "bundler-#{system_bundler_version}", :gem_repo => gem_repo4 - end - build_repo2 do - build_gem "prints_loaded_gems", "1.0" do |s| - s.executables = "print_loaded_gems" - s.bindir = "exe" - s.write "exe/print_loaded_gems", <<-R - specs = Gem.loaded_specs.values.reject {|s| Bundler.rubygems.spec_default_gem?(s) } - puts specs.map(&:full_name).sort.inspect - R - end - end - install_gemfile! <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack" - gem "prints_loaded_gems" - G - bundle! "binstubs bundler rack prints_loaded_gems" - end - - let(:system_bundler_version) { Bundler::VERSION } - - it "runs bundler" do - sys_exec! "#{bundled_app("bin/bundle")} install" - expect(out).to eq %(system bundler #{system_bundler_version}\n["install"]) - end - - context "when BUNDLER_VERSION is set" do - it "runs the correct version of bundler" do - sys_exec "#{bundled_app("bin/bundle")} install", "BUNDLER_VERSION" => "999.999.999" - expect(exitstatus).to eq(42) if exitstatus - expect(err).to include("Activating bundler (~> 999.999) failed:"). - and include("To install the version of bundler this project requires, run `gem install bundler -v '~> 999.999'`") - end - end - - context "when a lockfile exists with a locked bundler version" do - context "and the version is newer" do - before do - lockfile lockfile.gsub(system_bundler_version, "999.999") - end - - it "runs the correct version of bundler" do - sys_exec "#{bundled_app("bin/bundle")} install" - expect(exitstatus).to eq(42) if exitstatus - expect(err).to include("Activating bundler (~> 999.999) failed:"). - and include("To install the version of bundler this project requires, run `gem install bundler -v '~> 999.999'`") - end - end - - context "and the version is older and a different major" do - let(:system_bundler_version) { "55" } - - before do - lockfile lockfile.gsub(/BUNDLED WITH\n .*$/m, "BUNDLED WITH\n 44.0") - end - - it "runs the correct version of bundler" do - sys_exec "#{bundled_app("bin/bundle")} install" - expect(exitstatus).to eq(42) if exitstatus - expect(err).to include("Activating bundler (~> 44.0) failed:"). - and include("To install the version of bundler this project requires, run `gem install bundler -v '~> 44.0'`") - end - end - - context "and the version is older and the same major" do - let(:system_bundler_version) { "55.1" } - - before do - lockfile lockfile.gsub(/BUNDLED WITH\n .*$/m, "BUNDLED WITH\n 55.0") - end - - it "runs the available version of bundler when the version is older and the same major" do - sys_exec "#{bundled_app("bin/bundle")} install" - expect(exitstatus).not_to eq(42) if exitstatus - expect(err).not_to include("Activating bundler (~> 55.0) failed:") - end - end - - context "and the version is a pre-releaser" do - let(:system_bundler_version) { "55" } - - before do - lockfile lockfile.gsub(/BUNDLED WITH\n .*$/m, "BUNDLED WITH\n 2.12.0.a") - end - - it "runs the correct version of bundler when the version is a pre-release" do - sys_exec "#{bundled_app("bin/bundle")} install" - expect(exitstatus).to eq(42) if exitstatus - expect(err).to include("Activating bundler (~> 2.12.a) failed:"). - and include("To install the version of bundler this project requires, run `gem install bundler -v '~> 2.12.a'`") - end - end - end - - context "when update --bundler is called" do - before { lockfile.gsub(system_bundler_version, "1.1.1") } - - it "calls through to the latest bundler version" do - sys_exec! "#{bundled_app("bin/bundle")} update --bundler" - expect(out).to eq %(system bundler #{system_bundler_version}\n["update", "--bundler"]) - end - - it "calls through to the explicit bundler version" do - sys_exec "#{bundled_app("bin/bundle")} update --bundler=999.999.999" - expect(exitstatus).to eq(42) if exitstatus - expect(err).to include("Activating bundler (~> 999.999) failed:"). - and include("To install the version of bundler this project requires, run `gem install bundler -v '~> 999.999'`") - end - end - - context "without a lockfile" do - it "falls back to the latest installed bundler" do - FileUtils.rm bundled_app("Gemfile.lock") - sys_exec! bundled_app("bin/bundle").to_s - expect(out).to eq "system bundler #{system_bundler_version}\n[]" - end - end - - context "using another binstub" do - let(:system_bundler_version) { :bundler } - it "loads all gems" do - sys_exec! bundled_app("bin/print_loaded_gems").to_s - expect(out).to eq %(["bundler-#{Bundler::VERSION}", "prints_loaded_gems-1.0", "rack-1.2"]) - end - - context "when requesting a different bundler version" do - before { lockfile lockfile.gsub(Bundler::VERSION, "999.999.999") } - - it "attempts to load that version" do - sys_exec bundled_app("bin/rackup").to_s - expect(exitstatus).to eq(42) if exitstatus - expect(err).to include("Activating bundler (~> 999.999) failed:"). - and include("To install the version of bundler this project requires, run `gem install bundler -v '~> 999.999'`") - end - end - end - 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| + build_git "foo", "1.0", path: lib_path("foo") do |s| s.executables = %w[foo] end install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo")}" G @@ -261,10 +104,11 @@ RSpec.describe "bundle binstubs <gem>" do 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| + build_lib "foo", "1.0", path: lib_path("foo") do |s| s.executables = %w[foo] end install_gemfile <<-G + source "https://gem.repo1" gem "foo", :path => "#{lib_path("foo")}" G @@ -276,26 +120,25 @@ RSpec.describe "bundle binstubs <gem>" do it "sets correct permissions for binstubs" do with_umask(0o002) do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "binstubs rack" - binary = bundled_app("bin/rackup") - expect(File.stat(binary).mode.to_s(8)).to eq("100775") + bundle "binstubs myrack" + binary = bundled_app("bin/myrackup") + expect(File.stat(binary).mode.to_s(8)).to eq(Gem.win_platform? ? "100644" : "100775") end end context "when using --shebang" do it "sets the specified shebang for the binstub" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "binstubs rack --shebang jruby" - - expect(File.open("bin/rackup").gets).to eq("#!/usr/bin/env jruby\n") + bundle "binstubs myrack --shebang jruby" + expect(File.readlines(bundled_app("bin/myrackup")).first).to eq("#!/usr/bin/env jruby\n") end end end @@ -303,64 +146,83 @@ RSpec.describe "bundle binstubs <gem>" do context "when the gem doesn't exist" do it "displays an error with correct status" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" G - bundle "binstubs doesnt_exist" + bundle "binstubs doesnt_exist", raise_on_error: false - expect(exitstatus).to eq(7) if exitstatus + expect(exitstatus).to eq(7) expect(err).to include("Could not find gem 'doesnt_exist'.") end end - context "--path" do - it "sets the binstubs dir" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - G - - bundle "binstubs rack --path exec" - - expect(bundled_app("exec/rackup")).to exist + context "with the binstubs dir configured" do + before do + bundle_config "bin exec" end - it "setting is saved for bundle install", :bundler => "< 3" do + it "creates the binstubs in the configured dir" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - gem "rails" + source "https://gem.repo1" + gem "myrack" G - bundle! "binstubs rack", :path => "exec" - bundle! :install + bundle "binstubs myrack" - expect(bundled_app("exec/rails")).to exist + expect(bundled_app("exec/myrackup")).to exist end end context "with --standalone option" do before do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" + gem "rails" G end it "generates a standalone binstub" do - bundle! "binstubs rack --standalone" - expect(bundled_app("bin/rackup")).to exist + bundle "binstubs myrack --standalone" + expect(bundled_app("bin/myrackup")).to exist end it "generates a binstub that does not depend on rubygems or bundler" do - bundle! "binstubs rack --standalone" - expect(File.read(bundled_app("bin/rackup"))).to_not include("Gem.bin_path") + bundle "binstubs myrack --standalone" + expect(File.read(bundled_app("bin/myrackup"))).to_not include("Gem.bin_path") + end + + it "generates a standalone binstub at the given path when configured" do + bundle_config "bin foo" + bundle "binstubs myrack --standalone" + expect(bundled_app("foo/myrackup")).to exist end - context "when specified --path option" do - it "generates a standalone binstub at the given path" do - bundle! "binstubs rack --standalone --path foo" - expect(bundled_app("foo/rackup")).to exist + context "when specified --all-platforms option" do + it "generates standalone binstubs for all platforms" do + bundle "binstubs myrack --standalone --all-platforms" + expect(bundled_app("bin/myrackup")).to exist + expect(bundled_app("bin/myrackup.cmd")).to exist + end + end + + context "when the gem is bundler" do + it "warns without generating a standalone binstub" do + bundle "binstubs bundler --standalone" + expect(bundled_app("bin/bundle")).not_to exist + expect(bundled_app("bin/bundler")).not_to exist + expect(err).to include("Sorry, Bundler can only be run via RubyGems.") + end + end + + context "when specified --all option" do + it "generates standalone binstubs for all gems except bundler" do + bundle "binstubs --standalone --all" + expect(bundled_app("bin/myrackup")).to exist + expect(bundled_app("bin/rails")).to exist + expect(bundled_app("bin/bundle")).not_to exist + expect(bundled_app("bin/bundler")).not_to exist + expect(err).not_to include("Sorry, Bundler can only be run via RubyGems.") end end end @@ -368,39 +230,39 @@ RSpec.describe "bundle binstubs <gem>" do 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.open(bundled_app("bin/myrackup"), "wb") do |file| file.print "OMG" end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "binstubs rack" + bundle "binstubs myrack" - expect(bundled_app("bin/rackup")).to exist - expect(File.read(bundled_app("bin/rackup"))).to eq("OMG") - expect(err).to include("Skipped rackup") + expect(bundled_app("bin/myrackup")).to exist + expect(File.read(bundled_app("bin/myrackup"))).to eq("OMG") + expect(err).to include("Skipped myrackup") expect(err).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.open(bundled_app("bin/myrackup"), "wb") do |file| file.print "OMG" end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "binstubs rack --force" + bundle "binstubs myrack --force" - expect(bundled_app("bin/rackup")).to exist - expect(File.read(bundled_app("bin/rackup"))).not_to eq("OMG") + expect(bundled_app("bin/myrackup")).to exist + expect(File.read(bundled_app("bin/myrackup"))).not_to eq("OMG") end end end @@ -408,18 +270,18 @@ RSpec.describe "bundle binstubs <gem>" do context "when the gem has no bins" do it "suggests child gems if they have bins" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack-obama" + source "https://gem.repo1" + gem "myrack-obama" G - bundle "binstubs rack-obama" - expect(err).to include("rack-obama has no executables") - expect(err).to include("rack has: rackup") + bundle "binstubs myrack-obama" + expect(err).to include("myrack-obama has no executables") + expect(err).to include("myrack has: myrackup") end it "works if child gems don't have bins" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "actionpack" G @@ -428,8 +290,14 @@ RSpec.describe "bundle binstubs <gem>" do end it "works if the gem has development dependencies" do + build_repo2 do + build_gem "with_development_dependency" do |s| + s.add_development_dependency "activesupport", "= 2.3.5" + end + end + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo2" gem "with_development_dependency" G @@ -441,25 +309,25 @@ RSpec.describe "bundle binstubs <gem>" do context "when BUNDLE_INSTALL is specified" do it "performs an automatic bundle install" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "config set 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" + bundle_config "auto_install 1" + bundle "binstubs myrack" + expect(out).to include("Installing myrack 1.0.0") + expect(the_bundle).to include_gems "myrack 1.0.0" end it "does nothing when already up to date" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "config set auto_install 1" - bundle "binstubs rack", :env => { "BUNDLE_INSTALL" => 1 } - expect(out).not_to include("Installing rack 1.0.0") + bundle_config "auto_install 1" + bundle "binstubs myrack", env: { "BUNDLE_INSTALL" => "1" } + expect(out).not_to include("Installing myrack 1.0.0") end end end diff --git a/spec/bundler/commands/cache_spec.rb b/spec/bundler/commands/cache_spec.rb index 07ec186c2f..e223d07f7f 100644 --- a/spec/bundler/commands/cache_spec.rb +++ b/spec/bundler/commands/cache_spec.rb @@ -1,33 +1,46 @@ # frozen_string_literal: true RSpec.describe "bundle cache" do + it "doesn't update the cache multiple times, even if it already exists" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle :cache + expect(out).to include("Updating files in vendor/cache").once + + bundle :cache + expect(out).to include("Updating files in vendor/cache").once + end + context "with --gemfile" do it "finds the gemfile" do gemfile bundled_app("NotGemfile"), <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' G bundle "cache --gemfile=NotGemfile" ENV["BUNDLE_GEMFILE"] = "NotGemfile" - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end end - context "with --all" do + context "with cache_all configured" do context "without a gemspec" do it "caches all dependencies except bundler itself" do gemfile <<-D - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' gem 'bundler' D - bundle "config set cache_all true" + bundle_config "cache_all true" bundle :cache - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist end end @@ -50,15 +63,15 @@ RSpec.describe "bundle cache" do it "caches all dependencies except bundler and the gemspec specified gem" do gemfile <<-D - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' gemspec D - bundle "config set cache_all true" - bundle! :cache + bundle_config "cache_all true" + bundle :cache - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/myrack-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 @@ -82,15 +95,15 @@ RSpec.describe "bundle cache" do it "caches all dependencies except bundler and the gemspec specified gem" do gemfile <<-D - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' gemspec D - bundle "config set cache_all true" - bundle! :cache + bundle_config "cache_all true" + bundle :cache - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/myrack-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 @@ -126,16 +139,16 @@ RSpec.describe "bundle cache" do it "caches all dependencies except bundler and the gemspec specified gems" do gemfile <<-D - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' gemspec :name => 'mygem' gemspec :name => 'mygem_test' D - bundle "config set cache_all true" - bundle! :cache + bundle_config "cache_all true" + bundle :cache - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/myrack-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 @@ -145,67 +158,64 @@ RSpec.describe "bundle cache" do end end - context "with --path", :bundler => "< 3" do - it "sets root directory for gems" do - gemfile <<-D - source "#{file_uri_for(gem_repo1)}" - gem 'rack' - D - - bundle! :cache, forgotten_command_line_options(: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_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' D - bundle! "cache --no-install" + bundle "cache --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 + expect(the_bundle).not_to include_gems "myrack 1.0.0" + expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist end it "does not prevent installing gems with bundle install" do gemfile <<-D - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' D - bundle! "cache --no-install" - bundle! "install" + bundle "cache --no-install" + bundle "install" - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "does not prevent installing gems with bundle update" do gemfile <<-D - source "#{file_uri_for(gem_repo1)}" - gem "rack", "1.0.0" + source "https://gem.repo1" + gem "myrack", "1.0.0" D - bundle! "cache --no-install" - bundle! "update --all" + bundle "cache --no-install" + bundle "update --all" - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end end context "with --all-platforms" do it "puts the gems in vendor/cache even for other rubies" do gemfile <<-D - source "#{file_uri_for(gem_repo1)}" - gem 'rack', :platforms => :ruby_19 + source "https://gem.repo1" + gem 'myrack', :platforms => [:ruby_20, :windows_20] D bundle "cache --all-platforms" - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist + end + + it "prints a warn when using legacy windows rubies" do + gemfile <<-D + source "https://gem.repo1" + gem 'myrack', :platforms => [:ruby_20, :x64_mingw_20] + D + + bundle "cache --all-platforms", raise_on_error: false + expect(err).to include("will be removed in the future") + expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist end it "does not attempt to install gems in without groups" do @@ -217,52 +227,170 @@ RSpec.describe "bundle cache" do end end - install_gemfile! <<-G, forgotten_command_line_options(:without => "wo") - source "file:#{gem_repo1}" - gem "rack" + bundle_config "without wo" + install_gemfile <<-G, artifice: "compact_index_extra_api" + source "https://main.repo" + gem "myrack" group :wo do gem "weakling" - gem "uninstallable", :source => "file:#{gem_repo4}" + gem "uninstallable", :source => "https://main.repo/extra" end G - bundle! :cache, "all-platforms" => true + bundle :cache, "all-platforms" => true, artifice: "compact_index_extra_api" expect(bundled_app("vendor/cache/weakling-0.0.3.gem")).to exist expect(bundled_app("vendor/cache/uninstallable-2.0.gem")).to exist - expect(the_bundle).to include_gem "rack 1.0" + expect(the_bundle).to include_gem "myrack 1.0" expect(the_bundle).not_to include_gems "weakling", "uninstallable" - bundle! :install, forgotten_command_line_options(:without => "wo") - expect(the_bundle).to include_gem "rack 1.0" - expect(the_bundle).not_to include_gems "weakling", "uninstallable" + bundle_config "without wo" + bundle :install, artifice: "compact_index_extra_api" + expect(the_bundle).to include_gem "myrack 1.0" + expect(the_bundle).not_to include_gems "weakling" + end + + it "does not fail to cache gems in excluded groups when there's a lockfile but gems not previously installed" do + bundle_config "without wo" + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + group :wo do + gem "weakling" + end + G + + bundle :lock + bundle :cache, "all-platforms" => true + expect(bundled_app("vendor/cache/weakling-0.0.3.gem")).to exist end end - context "with --frozen" do + context "with frozen configured" do + let(:app_cache) { bundled_app("vendor/cache") } + before do + bundle_config "frozen true" + end + + it "tries to install but fails when the lockfile is out of sync" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "install" + lockfile <<-L + GEM + remote: https://gem.repo1/ + specs: + myrack (1.0.0) + myrack-obama (1.0) + myrack + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + myrack-obama + + BUNDLED WITH + #{Bundler::VERSION} + L + bundle :cache, raise_on_error: false + expect(exitstatus).to eq(16) + expect(err).to include("frozen mode") + expect(err).to include("You have deleted from the Gemfile") + expect(err).to include("* myrack-obama") + bundle "env" + expect(out).to include("frozen") end - subject { bundle :cache, forgotten_command_line_options(:frozen => true) } + it "caches gems without installing when lockfile is in sync, and --no-install is passed, even if vendor/cache directory is initially empty" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + lockfile <<-L + GEM + remote: https://gem.repo1/ + specs: + myrack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + + BUNDLED WITH + #{Bundler::VERSION} + L + FileUtils.mkdir_p app_cache + + bundle "cache --no-install" + expect(out).not_to include("Installing myrack 1.0.0") + expect(out).to include("Fetching myrack 1.0.0") + expect(app_cache.join("myrack-1.0.0.gem")).to exist + end + + it "completes a partial cache when lockfile is in sync, even if the already cached gem is no longer available remotely" do + build_repo4 do + build_gem "foo", "1.0.0" + end + + build_gem "bar", "1.0.0", path: bundled_app("vendor/cache") - it "tries to install with frozen" do - bundle! "config set deployment true" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - gem "rack-obama" + source "https://gem.repo4" + gem "foo" + gem "bar" G - subject - expect(exitstatus).to eq(16) if exitstatus - expect(err).to include("deployment mode") - expect(err).to include("You have added to the Gemfile") - expect(err).to include("* rack-obama") - bundle "env" - expect(out).to include("frozen").or include("deployment") + lockfile <<-L + GEM + remote: https://gem.repo4/ + specs: + foo (1.0.0) + bar (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo + bar + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "cache --no-install" + expect(out).to include("Fetching foo 1.0.0") + expect(out).not_to include("Fetching bar 1.0.0") + expect(app_cache.join("foo-1.0.0.gem")).to exist + expect(app_cache.join("bar-1.0.0.gem")).to exist + end + end + + context "with gems with extensions" do + before do + build_repo2 do + build_gem "racc", "2.0" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", "task(:default) { puts 'INSTALLING myrack' }" + end + end + + gemfile <<~G + source "https://gem.repo2" + + gem "racc" + G + end + + it "installs them properly from cache to a different path" do + bundle "cache" + bundle_config "path vendor/bundle" + bundle "install --local" end end end @@ -272,74 +400,215 @@ RSpec.describe "bundle install with gem sources" do it "does not hit the remote at all" do build_repo2 install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack" + source "https://gem.repo2" + gem "myrack" G bundle :cache - simulate_new_machine - FileUtils.rm_rf gem_repo2 + pristine_system_gems + FileUtils.rm_r gem_repo2 bundle "install --local" - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "does not hit the remote at all" do + it "does not hit the remote at all in frozen mode" do + build_repo2 + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + + bundle :cache + pristine_system_gems + FileUtils.rm_r gem_repo2 + + bundle_config "deployment true" + bundle_config "path vendor/bundle" + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "does not hit the remote at all in non frozen mode either" do + build_repo2 + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + + bundle :cache + pristine_system_gems + FileUtils.rm_r gem_repo2 + + bundle_config "path vendor/bundle" + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "does not hit the remote at all when cache_all_platforms configured" do build_repo2 - install_gemfile! <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack" + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + + bundle :cache + pristine_system_gems + FileUtils.rm_r gem_repo2 + + bundle_config "cache_all_platforms true" + bundle_config "path vendor/bundle" + bundle "install --local" + expect(out).not_to include("Fetching gem metadata") + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "uses cached gems for secondary sources when cache_all_platforms configured" do + build_repo4 do + build_gem "foo", "1.0.0" do |s| + s.platform = "x86_64-linux" + end + + build_gem "foo", "1.0.0" do |s| + s.platform = "arm64-darwin" + end + end + + gemfile <<~G + source "https://gem.repo2" + + source "https://gem.repo4" do + gem "foo" + end G - bundle! :cache - simulate_new_machine - FileUtils.rm_rf gem_repo2 + lockfile <<~L + GEM + remote: https://gem.repo2/ + specs: + + GEM + remote: https://gem.repo4/ + specs: + foo (1.0.0-x86_64-linux) + foo (1.0.0-arm64-darwin) + + PLATFORMS + arm64-darwin + ruby + x86_64-linux + + DEPENDENCIES + foo + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle_config "cache_all_platforms true" + bundle_config "path vendor/bundle" + bundle :cache, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + + # simulate removal of all remote gems + empty_repo4 - bundle! :install, forgotten_command_line_options(:deployment => true, :path => "vendor/bundle") - expect(the_bundle).to include_gems "rack 1.0.0" + # delete compact index cache + FileUtils.rm_r home(".bundle/cache/compact_index") + + bundle "install", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + + expect(the_bundle).to include_gems "foo 1.0.0 x86_64-linux" + end end it "does not reinstall already-installed gems" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G bundle :cache - build_gem "rack", "1.0.0", :path => bundled_app("vendor/cache") do |s| - s.write "lib/rack.rb", "raise 'omg'" + build_gem "myrack", "1.0.0", path: bundled_app("vendor/cache") do |s| + s.write "lib/myrack.rb", "raise 'omg'" end bundle :install expect(err).to be_empty - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 1.0" end it "ignores cached gems for the wrong platform" do simulate_platform "java" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "platform_specific" G bundle :cache end - simulate_new_machine + pristine_system_gems - simulate_platform "ruby" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "platform_specific" + bundle_config "force_ruby_platform true" + + install_gemfile <<-G + source "https://gem.repo1" + gem "platform_specific" + G + expect(the_bundle).to include_gems("platform_specific 1.0 ruby") + end + + it "keeps gems that are locked and cached for the current platform, even if incompatible with the current ruby" do + build_repo4 do + build_gem "bcrypt_pbkdf", "1.1.1" + build_gem "bcrypt_pbkdf", "1.1.1" do |s| + s.platform = "arm64-darwin" + s.required_ruby_version = "< #{current_ruby_minor}" + end + end + + app_cache = bundled_app("vendor/cache") + FileUtils.mkdir_p app_cache + FileUtils.cp gem_repo4("gems/bcrypt_pbkdf-1.1.1-arm64-darwin.gem"), app_cache + FileUtils.cp gem_repo4("gems/bcrypt_pbkdf-1.1.1.gem"), app_cache + + bundle_config "cache_all_platforms true" + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + bcrypt_pbkdf (1.1.1) + bcrypt_pbkdf (1.1.1-arm64-darwin) + + PLATFORMS + arm64-darwin + ruby + + DEPENDENCIES + bcrypt_pbkdf + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "arm64-darwin-23" do + install_gemfile <<~G, verbose: true + source "https://gem.repo4" + gem "bcrypt_pbkdf" G - run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" - expect(out).to eq("1.0.0 RUBY") + + expect(out).to include("Updating files in vendor/cache") + expect(err).to be_empty + expect(app_cache.join("bcrypt_pbkdf-1.1.1-arm64-darwin.gem")).to exist + expect(app_cache.join("bcrypt_pbkdf-1.1.1.gem")).to exist end end it "does not update the cache if --no-cache is passed" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G bundled_app("vendor/cache").mkpath expect(bundled_app("vendor/cache").children).to be_empty diff --git a/spec/bundler/commands/check_spec.rb b/spec/bundler/commands/check_spec.rb index c755ef2804..7fe6897ae3 100644 --- a/spec/bundler/commands/check_spec.rb +++ b/spec/bundler/commands/check_spec.rb @@ -3,163 +3,198 @@ RSpec.describe "bundle check" do it "returns success when the Gemfile is satisfied" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://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_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rails" G - Dir.chdir tmp - bundle "check --gemfile bundled_app/Gemfile" + bundle "check --gemfile bundled_app/Gemfile", dir: tmp 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_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rails" G - FileUtils.rm("Gemfile.lock") + FileUtils.rm(bundled_app_lock) bundle "check" - expect(bundled_app("Gemfile.lock")).to exist + expect(bundled_app_lock).to exist end it "does not create a Gemfile.lock if --dry-run was passed" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rails" G - FileUtils.rm("Gemfile.lock") + FileUtils.rm(bundled_app_lock) bundle "check --dry-run" - expect(bundled_app("Gemfile.lock")).not_to exist + expect(bundled_app_lock).not_to exist end - it "prints a generic error if the missing gems are unresolvable" do - system_gems ["rails-2.3.2"] + it "prints an error that shows missing gems" do + system_gems ["rails-2.3.2"], path: default_bundle_path gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rails" G - bundle :check - expect(err).to include("Bundler can't satisfy your Gemfile's dependencies.") + bundle :check, raise_on_error: false + expect(err).to include("The following gems are missing") + expect(err).to include(" * rake (#{rake_version})") + expect(err).to include(" * actionpack (2.3.2)") + expect(err).to include(" * activerecord (2.3.2)") + expect(err).to include(" * actionmailer (2.3.2)") + expect(err).to include(" * activeresource (2.3.2)") + expect(err).to include(" * activesupport (2.3.2)") + expect(err).to include("Install missing gems with `bundle install`") end - it "prints a generic error if a Gemfile.lock does not exist and a toplevel dependency does not exist" do + it "prints an error that shows missing gems if a Gemfile.lock does not exist and a toplevel dependency is missing" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rails" G - bundle :check - expect(exitstatus).to be > 0 if exitstatus + bundle :check, raise_on_error: false + expect(exitstatus).to be > 0 + expect(err).to include("The following gems are missing") + expect(err).to include(" * rails (2.3.2)") + expect(err).to include(" * rake (#{rake_version})") + expect(err).to include(" * actionpack (2.3.2)") + expect(err).to include(" * activerecord (2.3.2)") + expect(err).to include(" * actionmailer (2.3.2)") + expect(err).to include(" * activeresource (2.3.2)") + expect(err).to include(" * activesupport (2.3.2)") + expect(err).to include("Install missing gems with `bundle install`") + end + + it "prints a generic error if gem git source is not checked out" do + build_git "foo", path: lib_path("foo") + + bundle_config "path vendor/bundle" + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", git: "#{lib_path("foo")}" + G + + FileUtils.rm_r bundled_app("vendor/bundle") + bundle :check, raise_on_error: false + expect(exitstatus).to eq 1 expect(err).to include("Bundler can't satisfy your Gemfile's dependencies.") end it "prints a generic message if you changed your lockfile" do + build_repo2 do + build_gem "rails_pinned_to_old_activesupport" do |s| + s.add_dependency "activesupport", "= 1.2.3" + end + end + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo2" gem 'rails' G - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rails_fail' - G gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo2" gem "rails" - gem "rails_fail" + gem "rails_pinned_to_old_activesupport" G - bundle :check + bundle :check, raise_on_error: false expect(err).to include("Bundler can't satisfy your Gemfile's dependencies.") end - it "remembers --without option from install", :bundler => "< 3" do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + it "uses the without setting" do + bundle_config "without foo" + install_gemfile <<-G + source "https://gem.repo1" group :foo do - gem "rack" + gem "myrack" end G - bundle! "install --without foo" - bundle! "check" + bundle "check" expect(out).to include("The Gemfile's dependencies are satisfied") end - it "uses the without setting" do - bundle! "config set without foo" - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - group :foo do - gem "rack" - end + it "ensures that gems are actually installed and not just cached" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack", :group => :foo G - bundle! "check" - expect(out).to include("The Gemfile's dependencies are satisfied") - end + bundle_config "without foo" + bundle :install - it "ensures that gems are actually installed and not just cached" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :group => :foo + source "https://gem.repo1" + gem "myrack" G - bundle :install, forgotten_command_line_options(:without => "foo") + bundle "check", raise_on_error: false + expect(err).to include("* myrack (1.0.0)") + expect(exitstatus).to eq(1) + end + it "ensures that gems are actually installed and not just cached in applications' cache" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "check" - expect(err).to include("* rack (1.0.0)") - expect(exitstatus).to eq(1) if exitstatus + bundle_config "path vendor/bundle" + bundle :cache + + uninstall_gem("myrack", env: { "GEM_HOME" => vendored_gems.to_s }) + + bundle "check", raise_on_error: false + expect(err).to include("* myrack (1.0.0)") + expect(exitstatus).to eq(1) end it "ignores missing gems restricted to other platforms" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" platforms :#{not_local_tag} do gem "activesupport" end G - system_gems "rack-1.0.0", :path => :bundle_path + system_gems "myrack-1.0.0", path: default_bundle_path lockfile <<-G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: activesupport (2.3.5) - rack (1.0.0) + myrack (1.0.0) PLATFORMS - #{local} + #{generic_local_platform} #{not_local} DEPENDENCIES - rack + myrack activesupport G @@ -169,28 +204,28 @@ RSpec.describe "bundle check" do it "works with env conditionals" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" env :NOT_GOING_TO_BE_SET do gem "activesupport" end G - system_gems "rack-1.0.0", :path => :bundle_path + system_gems "myrack-1.0.0", path: default_bundle_path lockfile <<-G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: activesupport (2.3.5) - rack (1.0.0) + myrack (1.0.0) PLATFORMS - #{local} + #{generic_local_platform} #{not_local} DEPENDENCIES - rack + myrack activesupport G @@ -199,135 +234,341 @@ RSpec.describe "bundle check" do end it "outputs an error when the default Gemfile is not found" do - bundle :check - expect(exitstatus).to eq(10) if exitstatus + bundle :check, raise_on_error: false + expect(exitstatus).to eq(10) expect(err).to include("Could not locate Gemfile") end it "does not output fatal error message" do - bundle :check - expect(exitstatus).to eq(10) if exitstatus + bundle :check, raise_on_error: false + expect(exitstatus).to eq(10) expect(err).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' + it "fails when there's no lockfile and frozen is set" do + install_gemfile <<-G + source "https://gem.repo1" + gem "foo" G - simulate_new_machine - bundle "check" - last_out = out - 3.times do + bundle_config "deployment true" + bundle "install" + FileUtils.rm(bundled_app_lock) + + bundle :check, raise_on_error: false + expect(last_command).to be_failure + end + + describe "when locked" do + before :each do + system_gems "myrack-1.0.0" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "1.0" + G + end + + it "returns success when the Gemfile is satisfied" do + bundle :install bundle :check - expect(out).to eq(last_out) + 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 + FileUtils.rm_r default_bundle_path + default_system_gems + bundle :check, raise_on_error: false + expect(err).to match(/The following gems are missing/) + expect(err).to include("* myrack (1.0") end end - it "fails when there's no lock file and frozen is set" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "foo" - G + describe "when locked with multiple dependents with different requirements" do + before :each do + build_repo4 do + build_gem "depends_on_myrack" do |s| + s.add_dependency "myrack", ">= 1.0" + end + build_gem "also_depends_on_myrack" do |s| + s.add_dependency "myrack", "~> 1.0" + end + build_gem "myrack" + end - bundle! "install", forgotten_command_line_options(:deployment => true) - FileUtils.rm(bundled_app("Gemfile.lock")) + gemfile <<-G + source "https://gem.repo4" + gem "depends_on_myrack" + gem "also_depends_on_myrack" + G - bundle :check - expect(last_command).to be_failure - end + bundle "lock" + end - context "--path", :bundler => "< 3" do - context "after installing gems in the proper directory" do - before do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rails" - G - bundle "install --path vendor/bundle" + it "shows what is missing with the current Gemfile without duplications" do + bundle :check, raise_on_error: false + expect(err).to match(/The following gems are missing/) + expect(err).to include("* myrack (1.0").once + end + end - FileUtils.rm_rf(bundled_app(".bundle")) + describe "when locked under multiple platforms" do + before :each do + build_repo4 do + build_gem "myrack" end - it "returns success" do - bundle! "check --path vendor/bundle" - expect(out).to include("The Gemfile's dependencies are satisfied") + gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + lockfile <<-L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + ruby + #{local_platform} + + DEPENDENCIES + myrack + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "shows what is missing with the current Gemfile without duplications" do + bundle :check, raise_on_error: false + expect(err).to match(/The following gems are missing/) + expect(err).to include("* myrack (1.0").once + end + end + + describe "when using only scoped rubygems sources" do + before do + gemfile <<~G + source "https://gem.repo2" + source "https://gem.repo1" do + gem "myrack" + end + G + end + + it "returns success when the Gemfile is satisfied" do + system_gems "myrack-1.0.0", path: default_bundle_path + bundle :check, artifice: "compact_index" + expect(out).to include("The Gemfile's dependencies are satisfied") + end + end + + describe "when using only scoped rubygems sources with indirect dependencies" do + before do + build_repo4 do + build_gem "depends_on_myrack" do |s| + s.add_dependency "myrack" + end + + build_gem "myrack" end - it "should write to .bundle/config" do - bundle "check --path vendor/bundle" - bundle! "check" + gemfile <<~G + source "https://gem.repo1" + source "https://gem.repo4" do + gem "depends_on_myrack" + end + G + end + + it "returns success when the Gemfile is satisfied and generates a correct lockfile" do + system_gems "depends_on_myrack-1.0", "myrack-1.0", gem_repo: gem_repo4, path: default_bundle_path + bundle :check, artifice: "compact_index" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "depends_on_myrack", "1.0" + c.checksum gem_repo4, "myrack", "1.0" end + + expect(out).to include("The Gemfile's dependencies are satisfied") + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + + GEM + remote: https://gem.repo4/ + specs: + depends_on_myrack (1.0) + myrack + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + depends_on_myrack! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L end + end - context "after installing gems on a different directory" do - before do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rails" + context "with gemspec directive and scoped sources" do + before do + build_repo4 do + build_gem "awesome_print" + end + + build_repo2 do + build_gem "dex-dispatch-engine" + end + + build_lib("bundle-check-issue", path: tmp("bundle-check-issue")) do |s| + s.write "Gemfile", <<-G + source "https://localgemserver.test" + + gemspec + + source "https://localgemserver.test/extra" do + gem "dex-dispatch-engine" + end G - bundle "check --path vendor/bundle" + s.add_dependency "awesome_print" end - it "returns false" do - expect(exitstatus).to eq(1) if exitstatus - expect(err).to match(/The following gems are missing/) + bundle "install", artifice: "compact_index_extra", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }, dir: tmp("bundle-check-issue") + end + + it "does not corrupt lockfile when changing version" do + version_file = tmp("bundle-check-issue/bundle-check-issue.gemspec") + File.write(version_file, File.read(version_file).gsub(/s\.version = .+/, "s.version = '9999'")) + + bundle "check --verbose", dir: tmp("bundle-check-issue") + + lockfile = File.read(tmp("bundle-check-issue/Gemfile.lock")) + + checksums = checksums_section_when_enabled(lockfile) do |c| + c.checksum gem_repo4, "awesome_print", "1.0" + c.no_checksum "bundle-check-issue", "9999" + c.checksum gem_repo2, "dex-dispatch-engine", "1.0" end + + expect(lockfile).to eq <<~L + PATH + remote: . + specs: + bundle-check-issue (9999) + awesome_print + + GEM + remote: https://localgemserver.test/ + specs: + awesome_print (1.0) + + GEM + remote: https://localgemserver.test/extra/ + specs: + dex-dispatch-engine (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + bundle-check-issue! + dex-dispatch-engine! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L end end - describe "when locked" do - before :each do - system_gems "rack-1.0.0" - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "1.0" + context "with scoped and unscoped sources" do + it "does not corrupt lockfile" do + build_repo2 do + build_gem "foo" + build_gem "wadus" + build_gem("baz") {|s| s.add_dependency "wadus" } + end + + build_repo4 do + build_gem "bar" + end + + bundle_config "path.system true" + + # Add all gems to ensure all gems are installed so that a bundle check + # would be successful + install_gemfile(<<-G, artifice: "compact_index_extra") + source "https://gem.repo2" + + source "https://gem.repo4" do + gem "bar" + end + + gem "foo" + gem "baz" 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 + original_lockfile = lockfile - it "shows what is missing with the current Gemfile if it is not satisfied" do - simulate_new_machine - bundle :check - expect(err).to match(/The following gems are missing/) - expect(err).to include("* rack (1.0") + # Remove "baz" gem from the Gemfile, and bundle install again to generate + # a functional lockfile with no "baz" dependency or "wadus" transitive + # dependency + install_gemfile(<<-G, artifice: "compact_index_extra") + source "https://gem.repo2" + + source "https://gem.repo4" do + gem "bar" + end + + gem "foo" + G + + # Add back "baz" gem back to the Gemfile, but _crucially_ we do not perform a + # bundle install + gemfile(gemfile + 'gem "baz"') + + bundle :check, verbose: true + + # Bundle check should succeed and restore the lockfile to its original + # state + expect(lockfile).to eq(original_lockfile) end end describe "BUNDLED WITH" do def lock_with(bundler_version = nil) - lock = <<-L + lock = <<~L GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack + myrack L if bundler_version - lock += "\n BUNDLED WITH\n #{bundler_version}\n" + lock += "\nBUNDLED WITH\n #{bundler_version}\n" end lock end before do + bundle_config "path vendor/bundle" + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G end @@ -335,24 +576,25 @@ RSpec.describe "bundle check" do it "does not change the lock" do lockfile lock_with(nil) bundle :check - lockfile_should_be lock_with(nil) + expect(lockfile).to eq lock_with(nil) end end context "is newer" do - it "does not change the lock but warns" do + it "does not change the lock and does not warn" do lockfile lock_with(Bundler::VERSION.succ) - bundle! :check - expect(err).to include("the running version of Bundler (#{Bundler::VERSION}) is older than the version that created the lockfile (#{Bundler::VERSION.succ})") - lockfile_should_be lock_with(Bundler::VERSION.succ) + bundle :check + expect(err).to be_empty + expect(lockfile).to eq lock_with(Bundler::VERSION.succ) end end context "is older" do it "does not change the lock" do - lockfile lock_with("1.10.1") + system_gems "bundler-1.18.0" + lockfile lock_with("1.18.0") bundle :check - lockfile_should_be lock_with("1.10.1") + expect(lockfile).to eq lock_with("1.18.0") end end end diff --git a/spec/bundler/commands/clean_spec.rb b/spec/bundler/commands/clean_spec.rb index 7f9f84c104..c77859d378 100644 --- a/spec/bundler/commands/clean_spec.rb +++ b/spec/bundler/commands/clean_spec.rb @@ -19,116 +19,116 @@ RSpec.describe "bundle clean" do it "removes unused gems that are different" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "thin" gem "foo" G - bundle "config set path vendor/bundle" - bundle "config set clean false" - bundle! "install" + bundle_config "path vendor/bundle" + bundle_config "clean false" + bundle "install" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "thin" G - bundle! "install" + bundle "install" - bundle! :clean + bundle :clean expect(out).to include("Removing foo (1.0)") - should_have_gems "thin-1.0", "rack-1.0.0" + should_have_gems "thin-1.0", "myrack-1.0.0" should_not_have_gems "foo-1.0" - expect(vendored_gems("bin/rackup")).to exist + expect(vendored_gems("bin/myrackup")).to exist end it "removes old version of gem if unused" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack", "0.9.1" + gem "myrack", "0.9.1" gem "foo" G - bundle "config set path vendor/bundle" - bundle "config set clean false" + bundle_config "path vendor/bundle" + bundle_config "clean false" bundle "install" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" gem "foo" G bundle "install" bundle :clean - expect(out).to include("Removing rack (0.9.1)") + expect(out).to include("Removing myrack (0.9.1)") - should_have_gems "foo-1.0", "rack-1.0.0" - should_not_have_gems "rack-0.9.1" + should_have_gems "foo-1.0", "myrack-1.0.0" + should_not_have_gems "myrack-0.9.1" - expect(vendored_gems("bin/rackup")).to exist + expect(vendored_gems("bin/myrackup")).to exist end it "removes new version of gem if unused" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" gem "foo" G - bundle "config set path vendor/bundle" - bundle "config set clean false" - bundle! "install" + bundle_config "path vendor/bundle" + bundle_config "clean false" + bundle "install" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack", "0.9.1" + gem "myrack", "0.9.1" gem "foo" G - bundle! "update rack" + bundle "update myrack" - bundle! :clean + bundle :clean - expect(out).to include("Removing rack (1.0.0)") + expect(out).to include("Removing myrack (1.0.0)") - should_have_gems "foo-1.0", "rack-0.9.1" - should_not_have_gems "rack-1.0.0" + should_have_gems "foo-1.0", "myrack-0.9.1" + should_not_have_gems "myrack-1.0.0" - expect(vendored_gems("bin/rackup")).to exist + expect(vendored_gems("bin/myrackup")).to exist end it "removes gems in bundle without groups" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "foo" group :test_group do - gem "rack", "1.0.0" + gem "myrack", "1.0.0" end G - bundle "config set path vendor/bundle" + bundle_config "path vendor/bundle" bundle "install" - bundle "config set without test_group" + bundle_config "without test_group" bundle "install" bundle :clean - expect(out).to include("Removing rack (1.0.0)") + expect(out).to include("Removing myrack (1.0.0)") should_have_gems "foo-1.0" - should_not_have_gems "rack-1.0.0" + should_not_have_gems "myrack-1.0.0" - expect(vendored_gems("bin/rackup")).to_not exist + expect(vendored_gems("bin/myrackup")).to_not exist end it "does not remove cached git dir if it's being used" do @@ -137,45 +137,45 @@ RSpec.describe "bundle clean" do git_path = lib_path("foo-1.0") gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" git "#{git_path}", :ref => "#{revision}" do gem "foo" end G - bundle "config set path vendor/bundle" + bundle_config "path vendor/bundle" bundle "install" bundle :clean digest = Digest(:SHA1).hexdigest(git_path.to_s) - cache_path = Bundler::VERSION.start_with?("2.") ? vendored_gems("cache/bundler/git/foo-1.0-#{digest}") : home(".bundle/cache/git/foo-1.0-#{digest}") + cache_path = vendored_gems("cache/bundler/git/foo-1.0-#{digest}") expect(cache_path).to exist end it "removes unused git gems" do - build_git "foo", :path => lib_path("foo") + build_git "foo", path: lib_path("foo") git_path = lib_path("foo") revision = revision_for(git_path) gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" git "#{git_path}", :ref => "#{revision}" do gem "foo" end G - bundle "config set path vendor/bundle" + bundle_config "path vendor/bundle" bundle "install" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" G bundle "install" @@ -183,34 +183,34 @@ RSpec.describe "bundle clean" do expect(out).to include("Removing foo (#{revision[0..11]})") - expect(vendored_gems("gems/rack-1.0.0")).to exist + expect(vendored_gems("gems/myrack-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("specifications/myrack-1.0.0.gemspec")).to exist - expect(vendored_gems("bin/rackup")).to exist + expect(vendored_gems("bin/myrackup")).to exist end it "keeps used git gems even if installed to a symlinked location" do - build_git "foo", :path => lib_path("foo") + build_git "foo", path: lib_path("foo") git_path = lib_path("foo") revision = revision_for(git_path) gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" git "#{git_path}", :ref => "#{revision}" do gem "foo" end G FileUtils.mkdir_p(bundled_app("real-path")) - FileUtils.ln_sf(bundled_app("real-path"), bundled_app("symlink-path")) + File.symlink(bundled_app("real-path"), bundled_app("symlink-path")) - bundle "config set path #{bundled_app("symlink-path")}" + bundle_config "path #{bundled_app("symlink-path")}" bundle "install" bundle :clean @@ -220,51 +220,52 @@ RSpec.describe "bundle clean" do expect(bundled_app("symlink-path/#{Bundler.ruby_scope}/bundler/gems/foo-#{revision[0..11]}")).to exist end - it "removes old git gems" do - build_git "foo-bar", :path => lib_path("foo-bar") + it "removes old git gems on bundle update" do + build_git "foo-bar", path: lib_path("foo-bar") revision = revision_for(lib_path("foo-bar")) gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" git "#{lib_path("foo-bar")}" do gem "foo-bar" end G - bundle "config set path vendor/bundle" - bundle! "install" + bundle_config "path vendor/bundle" + bundle "install" - update_git "foo", :path => lib_path("foo-bar") + update_git "foo-bar", path: lib_path("foo-bar") revision2 = revision_for(lib_path("foo-bar")) - bundle! "update", :all => true - bundle! :clean + bundle "update", all: true + 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("gems/myrack-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("specifications/myrack-1.0.0.gemspec")).to exist - expect(vendored_gems("bin/rackup")).to exist + expect(vendored_gems("bin/myrackup")).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| + 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 + source "https://gem.repo1" gem "activesupport", :git => "#{lib_path("rails")}", :ref => '#{revision}' G - bundle "config set path vendor/bundle" + bundle_config "path vendor/bundle" bundle "install" bundle :clean expect(out).to include("") @@ -273,22 +274,22 @@ RSpec.describe "bundle clean" do end it "does not remove git sources that are in without groups" do - build_git "foo", :path => lib_path("foo") + build_git "foo", path: lib_path("foo") git_path = lib_path("foo") revision = revision_for(git_path) gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" group :test do git "#{git_path}", :ref => "#{revision}" do gem "foo" end end G - bundle "config set path vendor/bundle" - bundle "config set without test" + bundle_config "path vendor/bundle" + bundle_config "without test" bundle "install" bundle :clean @@ -301,137 +302,127 @@ RSpec.describe "bundle clean" do it "does not blow up when using without groups" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack" + gem "myrack" group :development do gem "foo" end G - bundle "config set path vendor/bundle" - bundle "config set without development" + bundle_config "path vendor/bundle" + bundle_config "without development" bundle "install" bundle :clean - expect(exitstatus).to eq(0) if exitstatus end it "displays an error when used without --path" do - bundle! "config set path.system true" + bundle_config "path.system true" install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" G - bundle :clean + bundle :clean, raise_on_error: false - expect(exitstatus).to eq(15) if exitstatus + expect(exitstatus).to eq(15) expect(err).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_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "thin" gem "foo" G - bundle "config set path vendor/bundle" + bundle_config "path vendor/bundle" bundle "install" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://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")) + FileUtils.rm(vendored_gems("bin/myrackup")) + FileUtils.rm_r(vendored_gems("gems/thin-1.0")) + FileUtils.rm_r(vendored_gems("gems/myrack-1.0.0")) bundle :clean - should_not_have_gems "thin-1.0", "rack-1.0" + should_not_have_gems "thin-1.0", "myrack-1.0" should_have_gems "foo-1.0" - expect(vendored_gems("bin/rackup")).not_to exist + expect(vendored_gems("bin/myrackup")).not_to exist end it "does not call clean automatically when using system gems" do - bundle! "config set path.system true" + bundle_config "path.system true" - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - - gem "thin" - gem "rack" - G - - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - - gem "rack" - G - - gem_command! :list - expect(out).to include("rack (1.0.0)").and include("thin (1.0)") - end - - it "--clean should override the bundle setting on install", :bundler => "< 3" do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G + source "https://gem.repo1" gem "thin" - gem "rack" + gem "myrack" G - bundle "config set path vendor/bundle" - bundle "config set clean false" - bundle "install --clean true" - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G + source "https://gem.repo1" - gem "rack" + gem "myrack" G - bundle "install" - should_have_gems "rack-1.0.0" - should_not_have_gems "thin-1.0" + installed_gems_list + expect(out).to include("myrack (1.0.0)").and include("thin (1.0)") end - it "--clean should override the bundle setting on update", :bundler => "< 3" do + it "does not clean on bundle update when path has not been set" do build_repo2 - gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + install_gemfile <<-G + source "https://gem.repo2" gem "foo" G - bundle "config set path vendor/bundle" - bundle "config set clean false" - bundle "install --clean true" update_repo2 do build_gem "foo", "1.0.1" end - bundle! "update", :all => true + bundle "update", all: true - should_have_gems "foo-1.0.1" - should_not_have_gems "foo-1.0" + files = Pathname.glob(default_bundle_path("*", "*")) + files.map! {|f| f.to_s.sub(default_bundle_path.to_s, "") } + expected_files = %W[ + /bin/bundle + /bin/bundler + /cache/bundler-#{Bundler::VERSION}.gem + /cache/foo-1.0.1.gem + /cache/foo-1.0.gem + /gems/bundler-#{Bundler::VERSION} + /gems/foo-1.0 + /gems/foo-1.0.1 + /specifications/bundler-#{Bundler::VERSION}.gemspec + /specifications/foo-1.0.1.gemspec + /specifications/foo-1.0.gemspec + ] + expected_files += ["/bin/bundle.bat", "/bin/bundler.bat"] if Gem.win_platform? + + expect(files.sort).to eq(expected_files.sort) end - it "automatically cleans when path has not been set", :bundler => "3" do + it "will automatically clean on bundle update when path has not been set", bundler: "5" do build_repo2 - install_gemfile! <<-G - source "#{file_uri_for(gem_repo2)}" + install_gemfile <<-G + source "https://gem.repo2" gem "foo" G @@ -440,10 +431,10 @@ RSpec.describe "bundle clean" do build_gem "foo", "1.0.1" end - bundle! "update", :all => true + bundle "update", all: true - files = Pathname.glob(bundled_app(".bundle", Bundler.ruby_scope, "*", "*")) - files.map! {|f| f.to_s.sub(bundled_app(".bundle", Bundler.ruby_scope).to_s, "") } + files = Pathname.glob(local_gem_path("*", "*")) + files.map! {|f| f.to_s.sub(local_gem_path.to_s, "") } expect(files.sort).to eq %w[ /cache/foo-1.0.1.gem /gems/foo-1.0.1 @@ -451,92 +442,92 @@ RSpec.describe "bundle clean" do ] end - it "does not clean automatically on --path" do + it "does not clean automatically when path configured" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "thin" - gem "rack" + gem "myrack" G - bundle "config set path vendor/bundle" + bundle_config "path vendor/bundle" bundle "install" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack" + gem "myrack" G bundle "install" - should_have_gems "rack-1.0.0", "thin-1.0" + should_have_gems "myrack-1.0.0", "thin-1.0" end - it "does not clean on bundle update with --path" do + it "does not clean on bundle update when path configured" do build_repo2 gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "foo" G - bundle "config set path vendor/bundle" - bundle! "install" + bundle_config "path vendor/bundle" + bundle "install" update_repo2 do build_gem "foo", "1.0.1" end - bundle! :update, :all => true + bundle :update, all: true should_have_gems "foo-1.0", "foo-1.0.1" end - it "does not clean on bundle update when using --system" do - bundle! "config set path.system true" + it "does not clean on bundle update when installing to system gems" do + bundle_config "path.system true" build_repo2 gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "foo" G - bundle! "install" + bundle "install" update_repo2 do build_gem "foo", "1.0.1" end - bundle! :update, :all => true + bundle :update, all: true - gem_command! :list + installed_gems_list expect(out).to include("foo (1.0.1, 1.0)") end it "cleans system gems when --force is used" do - bundle! "config set path.system true" + bundle_config "path.system true" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "foo" - gem "rack" + gem "myrack" G bundle :install gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack" + gem "myrack" G bundle :install bundle "clean --force" expect(out).to include("Removing foo (1.0)") - gem_command! :list + installed_gems_list expect(out).not_to include("foo (1.0)") - expect(out).to include("rack (1.0.0)") + expect(out).to include("myrack (1.0.0)") end - describe "when missing permissions" do + describe "when missing permissions", :permissions do before { ENV["BUNDLE_PATH__SYSTEM"] = "true" } let(:system_cache_path) { system_gem_path("cache") } after do @@ -544,30 +535,30 @@ RSpec.describe "bundle clean" do end it "returns a helpful error message" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "foo" - gem "rack" + gem "myrack" G bundle :install gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack" + gem "myrack" G bundle :install FileUtils.chmod(0o500, system_cache_path) - bundle :clean, :force => true + bundle :clean, force: true, raise_on_error: false expect(err).to include(system_gem_path.to_s) expect(err).to include("grant write permissions") - gem_command! :list + installed_gems_list expect(out).to include("foo (1.0)") - expect(out).to include("rack (1.0.0)") + expect(out).to include("myrack (1.0.0)") end end @@ -576,22 +567,22 @@ RSpec.describe "bundle clean" do revision = revision_for(lib_path("foo-1.0")) gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G - bundle "config set path vendor/bundle" + bundle_config "path vendor/bundle" bundle "install" # mimic 7 length git revisions in Gemfile.lock - gemfile_lock = File.read(bundled_app("Gemfile.lock")).split("\n") + gemfile_lock = File.read(bundled_app_lock).split("\n") gemfile_lock.each_with_index do |line, index| gemfile_lock[index] = line[0..(11 + 7)] if line.include?(" revision:") end - lockfile(bundled_app("Gemfile.lock"), gemfile_lock.join("\n")) + lockfile(bundled_app_lock, gemfile_lock.join("\n")) - bundle "config set path vendor/bundle" + bundle_config "path vendor/bundle" bundle "install" bundle :clean @@ -602,10 +593,9 @@ RSpec.describe "bundle clean" do end it "when using --force on system gems, it doesn't remove binaries" do - bundle! "config set path.system true" + bundle_config "path.system true" - build_repo2 - update_repo2 do + build_repo2 do build_gem "bindir" do |s| s.bindir = "exe" s.executables = "foo" @@ -613,7 +603,7 @@ RSpec.describe "bundle clean" do end gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "bindir" G @@ -623,10 +613,27 @@ RSpec.describe "bundle clean" do sys_exec "foo" - expect(exitstatus).to eq(0) if exitstatus expect(out).to eq("1.0") end + it "when using --force, it doesn't remove default gem binaries" do + default_irb_version = ruby "gem 'irb', '< 999999'; require 'irb'; puts IRB::VERSION", raise_on_error: false + skip "irb isn't a default gem" if default_irb_version.empty? + + # simulate executable for default gem + build_gem "irb", default_irb_version, to_system: true, default: true do |s| + s.executables = "irb" + end + + install_gemfile <<-G + source "https://gem.repo2" + G + + bundle "clean --force", env: { "BUNDLER_GEM_DEFAULT_DIR" => system_gem_path.to_s } + + expect(out).not_to include("Removing irb") + end + it "doesn't blow up on path gems without a .gemspec" do relative_path = "vendor/private_gems/bar-1.0" absolute_path = bundled_app(relative_path) @@ -636,31 +643,31 @@ RSpec.describe "bundle clean" do end gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "foo" gem "bar", "1.0", :path => "#{relative_path}" G - bundle "config set path vendor/bundle" + bundle_config "path vendor/bundle" bundle "install" - bundle! :clean + bundle :clean end it "doesn't remove gems in dry-run mode with path set" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "thin" gem "foo" G - bundle "config set path vendor/bundle" - bundle "config set clean false" + bundle_config "path vendor/bundle" + bundle_config "clean false" bundle "install" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "thin" G @@ -672,25 +679,25 @@ RSpec.describe "bundle clean" do 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" + should_have_gems "thin-1.0", "myrack-1.0.0", "foo-1.0" - expect(vendored_gems("bin/rackup")).to exist + expect(vendored_gems("bin/myrackup")).to exist end it "doesn't remove gems in dry-run mode with no path set" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "thin" gem "foo" G - bundle "config set path vendor/bundle" - bundle "config set clean false" + bundle_config "path vendor/bundle" + bundle_config "clean false" bundle "install" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "thin" G @@ -702,26 +709,26 @@ RSpec.describe "bundle clean" do 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" + should_have_gems "thin-1.0", "myrack-1.0.0", "foo-1.0" - expect(vendored_gems("bin/rackup")).to exist + expect(vendored_gems("bin/myrackup")).to exist end it "doesn't store dry run as a config setting" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "thin" gem "foo" G - bundle "config set path vendor/bundle" - bundle "config set clean false" + bundle_config "path vendor/bundle" + bundle_config "clean false" bundle "install" - bundle "config set dry_run false" + bundle_config "dry_run false" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "thin" G @@ -733,72 +740,72 @@ RSpec.describe "bundle clean" do 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_have_gems "thin-1.0", "myrack-1.0.0" should_not_have_gems "foo-1.0" - expect(vendored_gems("bin/rackup")).to exist + expect(vendored_gems("bin/myrackup")).to exist end it "performs an automatic bundle install" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "thin" gem "foo" G - bundle "config set path vendor/bundle" - bundle "config set clean false" - bundle! "install" + bundle_config "path vendor/bundle" + bundle_config "clean false" + bundle "install" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "thin" gem "weakling" G - bundle! "config set auto_install 1" - bundle! :clean + 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_have_gems "thin-1.0", "myrack-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 do + it "doesn't remove extensions artifacts from bundled git gems after clean" 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_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}" G - bundle "config set path vendor/bundle" - bundle! "install" + bundle_config "path vendor/bundle" + bundle "install" 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("") + bundle :clean + expect(out).to be_empty 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 - it "removes extension directories", :ruby_repo do + it "removes extension directories" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "thin" gem "very_simple_binary" gem "simple_binary" G - bundle "config set path vendor/bundle" - bundle! "install" + bundle_config "path vendor/bundle" + bundle "install" very_simple_binary_extensions_dir = Pathname.glob("#{vendored_gems}/extensions/*/*/very_simple_binary-1.0").first @@ -810,35 +817,35 @@ RSpec.describe "bundle clean" do expect(simple_binary_extensions_dir).to exist gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "thin" gem "simple_binary" G - bundle! "install" - bundle! :clean + bundle "install" + bundle :clean expect(out).to eq("Removing very_simple_binary (1.0)") expect(very_simple_binary_extensions_dir).not_to exist expect(simple_binary_extensions_dir).to exist end - it "removes git extension directories", :ruby_repo do + it "removes git extension directories" do build_git "very_simple_git_binary", &:add_c_extension revision = revision_for(lib_path("very_simple_git_binary-1.0")) short_revision = revision[0..11] gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "thin" gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}" G - bundle "config set path vendor/bundle" - bundle! "install" + bundle_config "path vendor/bundle" + bundle "install" very_simple_binary_extensions_dir = Pathname.glob("#{vendored_gems}/bundler/gems/extensions/*/*/very_simple_git_binary-1.0-#{short_revision}").first @@ -846,32 +853,34 @@ RSpec.describe "bundle clean" do expect(very_simple_binary_extensions_dir).to exist gemfile <<-G + source "https://gem.repo1" gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}" G - bundle! "install" - bundle! :clean + bundle "install" + bundle :clean expect(out).to include("Removing thin (1.0)") expect(very_simple_binary_extensions_dir).to exist gemfile <<-G + source "https://gem.repo1" G - bundle! "install" - bundle! :clean + bundle "install" + bundle :clean expect(out).to eq("Removing very_simple_git_binary-1.0 (#{short_revision})") expect(very_simple_binary_extensions_dir).not_to exist end - it "keeps git extension directories when excluded by group", :ruby_repo do + it "keeps git extension directories when excluded by group" do build_git "very_simple_git_binary", &:add_c_extension revision = revision_for(lib_path("very_simple_git_binary-1.0")) short_revision = revision[0..11] gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" group :development do gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}" @@ -879,14 +888,49 @@ RSpec.describe "bundle clean" do G bundle :lock - bundle "config set without development" - bundle "config set path vendor/bundle" - bundle! "install" - bundle! :clean + bundle_config "without development" + bundle_config "path vendor/bundle" + bundle "install", verbose: true + bundle :clean very_simple_binary_extensions_dir = Pathname.glob("#{vendored_gems}/bundler/gems/extensions/*/*/very_simple_git_binary-1.0-#{short_revision}").first expect(very_simple_binary_extensions_dir).to be_nil end + + it "does not remove the bundler version currently running" do + gemfile <<-G + source "https://gem.repo1" + + gem "myrack" + G + + bundle_config "path vendor/bundle" + bundle "install" + + version = Bundler.gem_version.to_s + # Simulate that the locked bundler version is installed in the bundle path + # by creating the gem directory and gemspec (as would happen after bundle install with that version) + Pathname(vendored_gems("cache/bundler-#{version}.gem")).tap do |path| + FileUtils.touch(path) + end + FileUtils.touch(vendored_gems("gems/bundler-#{version}")) + Pathname(vendored_gems("specifications/bundler-#{version}.gemspec")).tap do |path| + path.write(<<~GEMSPEC) + Gem::Specification.new do |s| + s.name = "bundler" + s.version = "#{version}" + s.authors = ["bundler team"] + s.summary = "The best way to manage your application's dependencies" + end + GEMSPEC + end + + should_have_gems "bundler-#{version}" + + bundle :clean + + should_have_gems "bundler-#{version}" + end end diff --git a/spec/bundler/commands/config_spec.rb b/spec/bundler/commands/config_spec.rb index ef580463e5..e8ab32ca5d 100644 --- a/spec/bundler/commands/config_spec.rb +++ b/spec/bundler/commands/config_spec.rb @@ -28,48 +28,103 @@ RSpec.describe ".bundle/config" do context "with env overwrite" do it "prints config with env" do - bundle "config list --parseable", :env => { "BUNDLE_FOO" => "bar3" } + bundle "config list --parseable", env: { "BUNDLE_FOO" => "bar3" } expect(out).to include("foo=bar3") end end end end - describe "location" do + describe "location with a gemfile" do before :each do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "1.0.0" + source "https://gem.repo1" + gem "myrack", "1.0.0" G end + it "is local by default" do + bundle "config set foo bar" + expect(bundled_app(".bundle/config")).to exist + expect(home(".bundle/config")).not_to exist + end + it "can be moved with an environment variable" do ENV["BUNDLE_APP_CONFIG"] = tmp("foo/bar").to_s - bundle "install", forgotten_command_line_options(:path => "vendor/bundle") + bundle "config set --local path vendor/bundle" + bundle "install" expect(bundled_app(".bundle")).not_to exist expect(tmp("foo/bar/config")).to exist - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 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", forgotten_command_line_options(:path => "vendor/bundle") + bundle "config set --local path vendor/bundle" + bundle "install", dir: bundled_app("omg") expect(bundled_app(".bundle")).not_to exist expect(bundled_app("../foo/config")).to exist - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0", dir: bundled_app("omg") + end + end + + describe "location without a gemfile" do + it "is global by default" do + bundle "config set foo bar" + expect(bundled_app(".bundle/config")).not_to exist + expect(home(".bundle/config")).to exist + end + + it "does not list global settings as local" do + bundle "config set --global foo bar" + bundle "config list", dir: home + + expect(out).to include("for the current user") + expect(out).not_to include("for your local app") + end + + it "works with an absolute path" do + ENV["BUNDLE_APP_CONFIG"] = tmp("foo/bar").to_s + bundle "config set --local path vendor/bundle" + + expect(bundled_app(".bundle")).not_to exist + expect(tmp("foo/bar/config")).to exist + end + end + + describe "config location" do + let(:bundle_user_config) { File.join(Dir.home, ".config/bundler") } + + before do + Dir.mkdir File.dirname(bundle_user_config) + end + + it "can be configured through BUNDLE_USER_CONFIG" do + bundle "config set path vendor", env: { "BUNDLE_USER_CONFIG" => bundle_user_config } + bundle "config get path", env: { "BUNDLE_USER_CONFIG" => bundle_user_config } + expect(out).to include("Set for the current user (#{bundle_user_config}): \"vendor\"") + end + + context "when not explicitly configured, but BUNDLE_USER_HOME set" do + let(:bundle_user_home) { bundled_app(".bundle").to_s } + + it "uses the right location" do + bundle "config set path vendor", env: { "BUNDLE_USER_HOME" => bundle_user_home } + bundle "config get path", env: { "BUNDLE_USER_HOME" => bundle_user_home } + expect(out).to include("Set for the current user (#{bundle_user_home}/config): \"vendor\"") + end end end describe "global" do before(:each) do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "1.0.0" + source "https://gem.repo1" + gem "myrack", "1.0.0" G end @@ -80,8 +135,8 @@ RSpec.describe ".bundle/config" do end it "can also be set explicitly" do - bundle! "config set --global foo global" - run! "puts Bundler.settings[:foo]" + bundle "config set --global foo global" + run "puts Bundler.settings[:foo]" expect(out).to eq("global") end @@ -96,17 +151,15 @@ RSpec.describe ".bundle/config" do end it "has lower precedence than env" do - begin - ENV["BUNDLE_FOO"] = "env" + ENV["BUNDLE_FOO"] = "env" - bundle "config set --global foo global" - expect(out).to match(/You have a bundler environment variable for foo set to "env"/) + bundle "config set --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 + run "puts Bundler.settings[:foo]" + expect(out).to eq("env") + ensure + ENV.delete("BUNDLE_FOO") end it "can be deleted" do @@ -138,7 +191,7 @@ RSpec.describe ".bundle/config" do it "expands the path at time of setting" do bundle "config set --global local.foo .." run "puts Bundler.settings['local.foo']" - expect(out).to eq(File.expand_path(Dir.pwd + "/..")) + expect(out).to eq(File.expand_path(bundled_app.to_s + "/..")) end it "saves with parseable option" do @@ -162,8 +215,8 @@ RSpec.describe ".bundle/config" do describe "local" do before(:each) do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "1.0.0" + source "https://gem.repo1" + gem "myrack", "1.0.0" G end @@ -174,15 +227,13 @@ RSpec.describe ".bundle/config" do end it "has higher precedence than env" do - begin - ENV["BUNDLE_FOO"] = "env" - bundle "config set --local foo local" - - run "puts Bundler.settings[:foo]" - expect(out).to eq("local") - ensure - ENV.delete("BUNDLE_FOO") - end + ENV["BUNDLE_FOO"] = "env" + bundle "config set --local foo local" + + run "puts Bundler.settings[:foo]" + expect(out).to eq("local") + ensure + ENV.delete("BUNDLE_FOO") end it "can be deleted" do @@ -205,7 +256,7 @@ RSpec.describe ".bundle/config" do it "expands the path at time of setting" do bundle "config set --local local.foo .." run "puts Bundler.settings['local.foo']" - expect(out).to eq(File.expand_path(Dir.pwd + "/..")) + expect(out).to eq(File.expand_path(bundled_app.to_s + "/..")) end it "can be deleted with parseable option" do @@ -220,8 +271,8 @@ RSpec.describe ".bundle/config" do describe "env" do before(:each) do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "1.0.0" + source "https://gem.repo1" + gem "myrack", "1.0.0" G end @@ -262,9 +313,10 @@ RSpec.describe ".bundle/config" do describe "parseable option" do it "prints an empty string" do - bundle "config get foo --parseable" + bundle "config get foo --parseable", raise_on_error: false expect(out).to eq "" + expect(last_command).to be_failure end it "only prints the value of the config" do @@ -293,8 +345,8 @@ RSpec.describe ".bundle/config" do describe "gem mirrors" do before(:each) do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "1.0.0" + source "https://gem.repo1" + gem "myrack", "1.0.0" G end @@ -307,10 +359,16 @@ end E expect(out).to eq("http://gems.example.org/ => http://gem-mirror.example.org/") end + + it "allows configuring fallback timeout for each mirror, and does not duplicate configs", rubygems: ">= 3.5.12" do + bundle "config set --global mirror.https://rubygems.org.fallback_timeout 1" + bundle "config set --global mirror.https://rubygems.org.fallback_timeout 2" + expect(File.read(home(".bundle/config"))).to include("BUNDLE_MIRROR").once + end end describe "quoting" do - before(:each) { gemfile "# no gems" } + before(:each) { gemfile "source 'https://gem.repo1'" } let(:long_string) do "--with-xml2-include=/usr/pkg/include/libxml2 --with-xml2-lib=/usr/pkg/lib " \ "--with-xslt-dir=/usr/pkg" @@ -324,7 +382,7 @@ E it "doesn't return quotes around values" do bundle "config set foo '1'" - run "puts Bundler.settings.send(:global_config_file).read" + run "puts Bundler.settings.send(:local_config_file).read" expect(out).to include('"1"') run "puts Bundler.settings[:foo]" expect(out).to eq("1") @@ -360,8 +418,8 @@ E describe "very long lines" do before(:each) do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "1.0.0" + source "https://gem.repo1" + gem "myrack", "1.0.0" G end @@ -388,87 +446,135 @@ E end end + describe "commented out settings with urls" do + before do + bundle "config set #mirror.https://rails-assets.org http://localhost:9292" + end + + it "does not make bundler crash and ignores the configuration" do + bundle "config list --parseable" + + expect(out).to be_empty + expect(err).to be_empty + + ruby(<<~RUBY) + require "bundler" + print Bundler.settings.mirror_for("https://rails-assets.org") + RUBY + expect(out).to eq("https://rails-assets.org/") + expect(err).to be_empty + + bundle "config set mirror.all http://localhost:9293" + ruby(<<~RUBY) + require "bundler" + print Bundler.settings.mirror_for("https://rails-assets.org") + RUBY + expect(out).to eq("http://localhost:9293/") + expect(err).to be_empty + end + end + describe "subcommands" do it "list" do - bundle! "config list" - expect(out).to eq "Settings are listed in order of priority. The top value will be used.\nspec_run\nSet via BUNDLE_SPEC_RUN: \"true\"" + bundle "config list", env: { "BUNDLE_FOO" => "bar" } + expect(out).to eq "Settings are listed in order of priority. The top value will be used.\nfoo\nSet via BUNDLE_FOO: \"bar\"" + + bundle "config list", env: { "BUNDLE_FOO" => "bar" }, parseable: true + expect(out).to eq "foo=bar" + end + + it "list with credentials" do + bundle "config list", env: { "BUNDLE_GEMS__MYSERVER__COM" => "user:password" } + expect(out).to eq "Settings are listed in order of priority. The top value will be used.\ngems.myserver.com\nSet via BUNDLE_GEMS__MYSERVER__COM: \"user:[REDACTED]\"" - bundle! "config list", :parseable => true - expect(out).to eq "spec_run=true" + bundle "config list", parseable: true, env: { "BUNDLE_GEMS__MYSERVER__COM" => "user:password" } + expect(out).to eq "gems.myserver.com=user:password" + end + + it "list with API token credentials" do + bundle "config list", env: { "BUNDLE_GEMS__MYSERVER__COM" => "api_token:x-oauth-basic" } + expect(out).to eq "Settings are listed in order of priority. The top value will be used.\ngems.myserver.com\nSet via BUNDLE_GEMS__MYSERVER__COM: \"[REDACTED]:x-oauth-basic\"" + + bundle "config list", parseable: true, env: { "BUNDLE_GEMS__MYSERVER__COM" => "api_token:x-oauth-basic" } + expect(out).to eq "gems.myserver.com=api_token:x-oauth-basic" end it "get" do ENV["BUNDLE_BAR"] = "bar_val" - bundle! "config get foo" + bundle "config get foo", raise_on_error: false expect(out).to eq "Settings for `foo` in order of priority. The top value will be used\nYou have not configured a value for `foo`" + expect(last_command).to be_failure ENV["BUNDLE_FOO"] = "foo_val" - bundle! "config get foo --parseable" + bundle "config get foo --parseable" expect(out).to eq "foo=foo_val" - bundle! "config get foo" + bundle "config get foo" expect(out).to eq "Settings for `foo` in order of priority. The top value will be used\nSet via BUNDLE_FOO: \"foo_val\"" end it "set" do - bundle! "config set foo 1" + bundle "config set foo 1" expect(out).to eq "" - bundle! "config set --local foo 2" + bundle "config set --local foo 2" expect(out).to eq "" - bundle! "config set --global foo 3" + bundle "config set --global foo 3" expect(out).to eq "Your application has set foo to \"2\". This will override the global value you are currently setting" - bundle! "config set --parseable --local foo 4" + bundle "config set --parseable --local foo 4" expect(out).to eq "foo=4" - bundle! "config set --local foo 4.1" + bundle "config set --local foo 4.1" expect(out).to eq "You are replacing the current local value of foo, which is currently \"4\"" - bundle "config set --global --local foo 5" + bundle "config set --global --local foo 5", raise_on_error: false expect(last_command).to be_failure expect(err).to eq "The options global and local were specified. Please only use one of the switches at a time." end it "unset" do - bundle! "config unset foo" + bundle "config unset foo" expect(out).to eq "" - bundle! "config set foo 1" - bundle! "config unset foo --parseable" + bundle "config set foo 1" + bundle "config unset foo --parseable" expect(out).to eq "" - bundle! "config set --local foo 1" - bundle! "config set --global foo 2" + bundle "config set --local foo 1" + bundle "config set --global foo 2" - bundle! "config unset foo" + bundle "config unset foo" expect(out).to eq "" - expect(bundle!("config get foo")).to eq "Settings for `foo` in order of priority. The top value will be used\nYou have not configured a value for `foo`" + expect(bundle("config get foo", raise_on_error: false)).to eq "Settings for `foo` in order of priority. The top value will be used\nYou have not configured a value for `foo`" + expect(last_command).to be_failure - bundle! "config set --local foo 1" - bundle! "config set --global foo 2" + bundle "config set --local foo 1" + bundle "config set --global foo 2" - bundle! "config unset foo --local" + bundle "config unset foo --local" expect(out).to eq "" - expect(bundle!("config get foo")).to eq "Settings for `foo` in order of priority. The top value will be used\nSet for the current user (#{home(".bundle/config")}): \"2\"" - bundle! "config unset foo --global" + expect(bundle("config get foo")).to eq "Settings for `foo` in order of priority. The top value will be used\nSet for the current user (#{home(".bundle/config")}): \"2\"" + bundle "config unset foo --global" expect(out).to eq "" - expect(bundle!("config get foo")).to eq "Settings for `foo` in order of priority. The top value will be used\nYou have not configured a value for `foo`" + expect(bundle("config get foo", raise_on_error: false)).to eq "Settings for `foo` in order of priority. The top value will be used\nYou have not configured a value for `foo`" + expect(last_command).to be_failure - bundle! "config set --local foo 1" - bundle! "config set --global foo 2" + bundle "config set --local foo 1" + bundle "config set --global foo 2" - bundle! "config unset foo --global" + bundle "config unset foo --global" expect(out).to eq "" - expect(bundle!("config get foo")).to eq "Settings for `foo` in order of priority. The top value will be used\nSet for your local app (#{bundled_app(".bundle/config")}): \"1\"" - bundle! "config unset foo --local" + expect(bundle("config get foo")).to eq "Settings for `foo` in order of priority. The top value will be used\nSet for your local app (#{bundled_app(".bundle/config")}): \"1\"" + bundle "config unset foo --local" expect(out).to eq "" - expect(bundle!("config get foo")).to eq "Settings for `foo` in order of priority. The top value will be used\nYou have not configured a value for `foo`" + expect(bundle("config get foo", raise_on_error: false)).to eq "Settings for `foo` in order of priority. The top value will be used\nYou have not configured a value for `foo`" + expect(last_command).to be_failure - bundle "config unset foo --local --global" + bundle "config unset foo --local --global", raise_on_error: false expect(last_command).to be_failure expect(err).to eq "The options global and local were specified. Please only use one of the switches at a time." end @@ -476,18 +582,65 @@ E end RSpec.describe "setting gemfile via config" do + context "when a default Gemfile exists" do + before do + gemfile <<-G + source "https://gem.repo1" + G + + gemfile bundled_app("foo/bar_gemfile"), <<-G + source "https://gem.repo1" + G + end + + it "reports the local gemfile setting without promoting it to the environment" do + bundle "config set gemfile foo/bar_gemfile" + + bundle "config list" + expect(out).to include("Set for your local app (#{bundled_app(".bundle/config")}): \"foo/bar_gemfile\"") + expect(out).not_to include("Set via BUNDLE_GEMFILE") + end + + it "unsets the local gemfile setting from the app config" do + bundle "config set gemfile foo/bar_gemfile" + + bundle "config unset gemfile" + bundle "config get gemfile", raise_on_error: false + + expect(out).to include("You have not configured a value for `gemfile`") + expect(File.read(bundled_app(".bundle/config"))).not_to include("BUNDLE_GEMFILE") + end + end + context "when only the non-default Gemfile exists" do it "persists the gemfile location to .bundle/config" do gemfile bundled_app("NotGemfile"), <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' G bundle "config set --local gemfile #{bundled_app("NotGemfile")}" - expect(File.exist?(".bundle/config")).to eq(true) + expect(File.exist?(bundled_app(".bundle/config"))).to eq(true) bundle "config list" expect(out).to include("NotGemfile") end end end + +RSpec.describe "setting lockfile via config" do + it "persists the lockfile location to .bundle/config" do + gemfile bundled_app("NotGemfile"), <<-G + source "https://gem.repo1" + gem 'myrack' + G + + bundle "config set --local gemfile #{bundled_app("NotGemfile")}" + bundle "config set --local lockfile #{bundled_app("ReallyNotGemfile.lock")}" + expect(File.exist?(bundled_app(".bundle/config"))).to eq(true) + + bundle "config list" + expect(out).to include("NotGemfile") + expect(out).to include("ReallyNotGemfile.lock") + end +end diff --git a/spec/bundler/commands/console_spec.rb b/spec/bundler/commands/console_spec.rb index a0b71ff016..a44f607546 100644 --- a/spec/bundler/commands/console_spec.rb +++ b/spec/bundler/commands/console_spec.rb @@ -1,106 +1,214 @@ # frozen_string_literal: true -RSpec.describe "bundle console", :bundler => "< 3" do +RSpec.describe "bundle console", readline: true do before :each do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - gem "activesupport", :group => :test - gem "rack_middleware", :group => :development - G - end + build_repo2 do + # 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 - it "starts IRB with the default group loaded" do - bundle "console" do |input, _, _| - input.puts("puts RACK") - input.puts("exit") + build_dummy_irb 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") + context "when the library requires a non-existent file" do + before do + build_lib "loadfuuu", "1.0.0" do |s| + s.write "lib/loadfuuu.rb", "require_relative 'loadfuuu/bar'" + s.write "lib/loadfuuu/bar.rb", "require 'not-in-bundle'" + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "irb" + path "#{lib_path}" do + gem "loadfuuu", require: true + end + G end - expect(out).to include(":irb_binding") - end - it "starts another REPL if configured as such" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "pry" - G - bundle "config set console pry" + it "does not show the bug report template" do + bundle("console", raise_on_error: false) do |input, _, _| + input.puts("exit") + end - bundle "console" do |input, _, _| - input.puts("__method__") - input.puts("exit") + expect(err).not_to include("ERROR REPORT TEMPLATE") end - expect(out).to include(":__pry__") end - it "falls back to IRB if the other REPL isn't available" do - bundle "config set console pry" - # make sure pry isn't there + context "when the library references a non-existent constant" do + before do + build_lib "loadfuuu", "1.0.0" do |s| + s.write "lib/loadfuuu.rb", "Some::NonExistent::Constant" + end - bundle "console" do |input, _, _| - input.puts("__method__") - input.puts("exit") + install_gemfile <<-G + source "https://gem.repo2" + gem "irb" + path "#{lib_path}" do + gem "loadfuuu", require: true + end + G 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") + it "does not show the bug report template" do + bundle("console", raise_on_error: false) do |input, _, _| + input.puts("exit") + end + + expect(err).not_to include("ERROR REPORT TEMPLATE") 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") + context "when the library does not have any errors" do + before do + install_gemfile <<-G + source "https://gem.repo2" + gem "irb" + gem "myrack" + gem "activesupport", :group => :test + gem "myrack_middleware", :group => :development + G + end + + it "starts IRB with the default group loaded" do + bundle "console" do |input, _, _| + input.puts("puts MYRACK") + input.puts("exit") + end + expect(out).to include("0.9.1") + end + + it "uses IRB as default console" do + skip "Does not work in a ruby-core context if irb is in the default $LOAD_PATH because it enables the real IRB, not our dummy one" if ruby_core? && Gem.ruby_version < Gem::Version.new("3.5.0.a") + + bundle "console" do |input, _, _| + input.puts("__method__") input.puts("exit") end - expect(out).to include("2.3.5") + expect(out).to include("__irb__") end - it "loads the default group" do - bundle "console test" do |input, _, _| - input.puts("puts RACK") + it "starts another REPL if configured as such" do + install_gemfile <<-G + source "https://gem.repo2" + gem "irb" + gem "pry" + G + bundle_config "console pry" + + bundle "console" do |input, _, _| + input.puts("__method__") input.puts("exit") end - expect(out).to include("0.9.1") + expect(out).to include(":__pry__") end - it "doesn't load other groups" do - bundle "console test" do |input, _, _| - input.puts("puts RACK_MIDDLEWARE") + it "falls back to IRB if the other REPL isn't available" do + skip "Does not work in a ruby-core context if irb is in the default $LOAD_PATH because it enables the real IRB, not our dummy one" if ruby_core? && Gem.ruby_version < Gem::Version.new("3.5.0.a") + + 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__") + end + + it "does not try IRB twice if no console is configured and IRB is not available" do + create_file("irb.rb", "raise LoadError, 'irb is not available'") + + bundle("console", env: { "RUBYOPT" => "-I#{bundled_app} #{ENV["RUBYOPT"]}" }, raise_on_error: false) do |input, _, _| + input.puts("puts ACTIVESUPPORT") + input.puts("exit") + end + expect(err).not_to include("falling back to irb") + expect(err).to include("irb is not available") + 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 - end - it "performs an automatic bundle install" do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - gem "activesupport", :group => :test - gem "rack_middleware", :group => :development - gem "foo" - G - - bundle "config set auto_install 1" - bundle :console do |input, _, _| - input.puts("puts 'hello'") - input.puts("exit") + 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 MYRACK") + 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 MYRACK_MIDDLEWARE") + input.puts("exit") + end + expect(out).to include("NameError") + end + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "https://gem.repo2" + gem "irb" + gem "myrack" + gem "activesupport", :group => :test + gem "myrack_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 - 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 index d829f00092..d350b4b3d1 100644 --- a/spec/bundler/commands/doctor_spec.rb +++ b/spec/bundler/commands/doctor_spec.rb @@ -4,17 +4,18 @@ require "find" require "stringio" require "bundler/cli" require "bundler/cli/doctor" +require "bundler/cli/doctor/diagnose" RSpec.describe "bundle doctor" do before(:each) do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" G @stdout = StringIO.new - [:error, :warn].each do |method| + [:error, :warn, :info].each do |method| allow(Bundler.ui).to receive(method).and_wrap_original do |m, message| m.call message @stdout.puts message @@ -24,93 +25,161 @@ RSpec.describe "bundle doctor" do it "succeeds on a sane installation" do bundle :doctor - - expect(exitstatus).to eq(0) end context "when all files in home are readable/writable" do before(:each) do stat = double("stat") unwritable_file = double("file") + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) allow(Find).to receive(:find).with(Bundler.bundle_path.to_s) { [unwritable_file] } + allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:writable?).and_call_original + allow(File).to receive(:readable?).and_call_original + allow(File).to receive(:exist?).with(unwritable_file).and_return(true) allow(File).to receive(:stat).with(unwritable_file) { stat } allow(stat).to receive(:uid) { Process.uid } allow(File).to receive(:writable?).with(unwritable_file) { true } allow(File).to receive(:readable?).with(unwritable_file) { true } + + # The following lines are for `Gem::PathSupport#initialize`. + allow(File).to receive(:exist?).with(Gem.default_dir) + allow(File).to receive(:writable?).with(Gem.default_dir) + allow(File).to receive(:writable?).with(File.expand_path("..", Gem.default_dir)) end - it "exits with no message if the installed gem has no C extensions" do - expect { Bundler::CLI::Doctor.new({}).run }.not_to raise_error - expect(@stdout.string).to be_empty + it "exits with a success message if the installed gem has no C extensions" do + doctor = Bundler::CLI::Doctor::Diagnose.new({}) + allow(doctor).to receive(:lookup_with_fiddle).and_return(false) + expect { doctor.run }.not_to raise_error + expect(@stdout.string).to include("No issues") end - it "exits with no message if the installed gem's C extension dylib breakage is fine" do - doctor = Bundler::CLI::Doctor.new({}) - expect(doctor).to receive(:bundles_for_gem).exactly(2).times.and_return ["/path/to/rack/rack.bundle"] + it "exits with a success message if the installed gem's C extension dylib breakage is fine" do + doctor = Bundler::CLI::Doctor::Diagnose.new({}) + expect(doctor).to receive(:bundles_for_gem).exactly(2).times.and_return ["/path/to/myrack/myrack.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) - expect { doctor.run }.not_to(raise_error, @stdout.string) - expect(@stdout.string).to be_empty + allow(doctor).to receive(:lookup_with_fiddle).with("/usr/lib/libSystem.dylib").and_return(false) + expect { doctor.run }.not_to raise_error + expect(@stdout.string).to include("No issues") + end + + it "parses otool output correctly" do + doctor = Bundler::CLI::Doctor::Diagnose.new({}) + expect(doctor).to receive(:`).with("/usr/bin/otool -L fake").and_return("/home/gem/ruby/3.4.3/gems/blake3-rb-1.5.4.4/lib/digest/blake3/blake3_ext.bundle:\n\t (compatibility version 0.0.0, current version 0.0.0)\n\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)") + expect(doctor.dylibs_darwin("fake")).to eq(["/usr/lib/libSystem.B.dylib"]) end it "exits with a message if one of the linked libraries is missing" do - doctor = Bundler::CLI::Doctor.new({}) - expect(doctor).to receive(:bundles_for_gem).exactly(2).times.and_return ["/path/to/rack/rack.bundle"] + doctor = Bundler::CLI::Doctor::Diagnose.new({}) + expect(doctor).to receive(:bundles_for_gem).exactly(2).times.and_return ["/path/to/myrack/myrack.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), @stdout.string + allow(doctor).to receive(:lookup_with_fiddle).with("/usr/local/opt/icu4c/lib/libicui18n.57.1.dylib").and_return(true) + expect { doctor.run }.to raise_error(Bundler::ProductionError, <<~E.strip), @stdout.string 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 + * myrack: /usr/local/opt/icu4c/lib/libicui18n.57.1.dylib E end end + context "when home contains broken symlinks" do + before(:each) do + @broken_symlink = double("file") + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + allow(Find).to receive(:find).with(Bundler.bundle_path.to_s) { [@broken_symlink] } + allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:exist?).with(@broken_symlink) { false } + end + + it "exits with an error if home contains files that are not readable/writable" do + doctor = Bundler::CLI::Doctor::Diagnose.new({}) + allow(doctor).to receive(:lookup_with_fiddle).and_return(false) + expect { doctor.run }.not_to raise_error + expect(@stdout.string).to include( + "Broken links exist in the Bundler home. Please report them to the offending gem's upstream repo. These files are:\n - #{@broken_symlink}" + ) + expect(@stdout.string).not_to include("No issues") + end + end + context "when home contains files that are not readable/writable" do before(:each) do @stat = double("stat") @unwritable_file = double("file") + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) allow(Find).to receive(:find).with(Bundler.bundle_path.to_s) { [@unwritable_file] } + allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:writable?).and_call_original + allow(File).to receive(:readable?).and_call_original + allow(File).to receive(:exist?).with(@unwritable_file) { true } allow(File).to receive(:stat).with(@unwritable_file) { @stat } end - it "exits with an error if home contains files that are not readable/writable" do + it "exits with an error if home contains files that are not readable" do + doctor = Bundler::CLI::Doctor::Diagnose.new({}) + allow(doctor).to receive(:lookup_with_fiddle).and_return(false) allow(@stat).to receive(:uid) { Process.uid } allow(File).to receive(:writable?).with(@unwritable_file) { false } allow(File).to receive(:readable?).with(@unwritable_file) { false } - expect { Bundler::CLI::Doctor.new({}).run }.not_to raise_error + expect { doctor.run }.not_to raise_error expect(@stdout.string).to include( - "Files exist in the Bundler home that are not readable/writable by the current user. These files are:\n - #{@unwritable_file}" + "Files exist in the Bundler home that are not readable by the current user. These files are:\n - #{@unwritable_file}" ) expect(@stdout.string).not_to include("No issues") end - context "when home contains files that are not owned by the current process" do + it "exits without an error if home contains files that are not writable" do + doctor = Bundler::CLI::Doctor::Diagnose.new({}) + allow(doctor).to receive(:lookup_with_fiddle).and_return(false) + allow(@stat).to receive(:uid) { Process.uid } + allow(File).to receive(:writable?).with(@unwritable_file) { false } + allow(File).to receive(:readable?).with(@unwritable_file) { true } + expect { doctor.run }.not_to raise_error + expect(@stdout.string).not_to include( + "Files exist in the Bundler home that are not readable by the current user. These files are:\n - #{@unwritable_file}" + ) + expect(@stdout.string).to include("No issues") + end + + context "when home contains files that are not owned by the current process", :permissions do before(:each) do allow(@stat).to receive(:uid) { 0o0000 } end it "exits with an error if home contains files that are not readable/writable and are not owned by the current user" do + doctor = Bundler::CLI::Doctor::Diagnose.new({}) + allow(doctor).to receive(:lookup_with_fiddle).and_return(false) allow(File).to receive(:writable?).with(@unwritable_file) { false } allow(File).to receive(:readable?).with(@unwritable_file) { false } - expect { Bundler::CLI::Doctor.new({}).run }.not_to raise_error + expect { doctor.run }.not_to raise_error expect(@stdout.string).to include( - "Files exist in the Bundler home that are owned by another user, and are not readable/writable. These files are:\n - #{@unwritable_file}" + "Files exist in the Bundler home that are owned by another user, and are not readable. These files are:\n - #{@unwritable_file}" ) expect(@stdout.string).not_to include("No issues") end it "exits with a warning if home contains files that are read/write but not owned by current user" do + doctor = Bundler::CLI::Doctor::Diagnose.new({}) + allow(doctor).to receive(:lookup_with_fiddle).and_return(false) allow(File).to receive(:writable?).with(@unwritable_file) { true } allow(File).to receive(:readable?).with(@unwritable_file) { true } - expect { Bundler::CLI::Doctor.new({}).run }.not_to raise_error + expect { doctor.run }.not_to raise_error expect(@stdout.string).to include( - "Files exist in the Bundler home that are owned by another user, but are still readable/writable. These files are:\n - #{@unwritable_file}" + "Files exist in the Bundler home that are owned by another user, but are still readable. These files are:\n - #{@unwritable_file}" ) expect(@stdout.string).not_to include("No issues") end end end + + context "when home contains filenames with special characters" do + it "escape filename before command execute" do + doctor = Bundler::CLI::Doctor::Diagnose.new({}) + expect(doctor).to receive(:`).with("/usr/bin/otool -L \\$\\(date\\)\\ \\\"\\'\\\\.bundle").and_return("dummy string") + doctor.dylibs_darwin('$(date) "\'\.bundle') + expect(doctor).to receive(:`).with("/usr/bin/ldd \\$\\(date\\)\\ \\\"\\'\\\\.bundle").and_return("dummy string") + doctor.dylibs_ldd('$(date) "\'\.bundle') + end + end end diff --git a/spec/bundler/commands/exec_spec.rb b/spec/bundler/commands/exec_spec.rb index 7ae504d360..aa35685be8 100644 --- a/spec/bundler/commands/exec_spec.rb +++ b/spec/bundler/commands/exec_spec.rb @@ -1,84 +1,127 @@ # frozen_string_literal: true 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, :path => :bundle_path) - end - it "works with --gemfile flag" do - create_file "CustomGemfile", <<-G - gem "rack", "1.0.0" + system_gems(%w[myrack-1.0.0 myrack-0.9.1], path: default_bundle_path) + + gemfile "CustomGemfile", <<-G + source "https://gem.repo1" + gem "myrack", "1.0.0" G - bundle "exec --gemfile CustomGemfile rackup" + bundle "exec --gemfile CustomGemfile myrackup" expect(out).to eq("1.0.0") end it "activates the correct gem" do + system_gems(%w[myrack-1.0.0 myrack-0.9.1], path: default_bundle_path) + + gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + G + + bundle "exec myrackup" + expect(out).to eq("0.9.1") + end + + it "works and prints no warnings when HOME is not writable" do + system_gems(%w[myrack-1.0.0 myrack-0.9.1], path: default_bundle_path) + gemfile <<-G - gem "rack", "0.9.1" + source "https://gem.repo1" + gem "myrack", "0.9.1" G - bundle "exec rackup" + bundle "exec myrackup", env: { "HOME" => "/" } expect(out).to eq("0.9.1") + expect(err).to be_empty end it "works when the bins are in ~/.bundle" do install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "exec rackup" + bundle "exec myrackup" expect(out).to eq("1.0.0") end it "works when running from a random directory" do install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "exec 'cd #{tmp("gems")} && rackup'" + bundle "exec 'cd #{tmp("gems")} && myrackup'" expect(out).to eq("1.0.0") end it "works when exec'ing something else" do - install_gemfile 'gem "rack"' + install_gemfile "source \"https://gem.repo1\"; gem \"myrack\"" bundle "exec echo exec" expect(out).to eq("exec") end it "works when exec'ing to ruby" do - install_gemfile 'gem "rack"' + install_gemfile "source \"https://gem.repo1\"; gem \"myrack\"" bundle "exec ruby -e 'puts %{hi}'" expect(out).to eq("hi") end + it "works when exec'ing to rubygems" do + install_gemfile "source \"https://gem.repo1\"; gem \"myrack\"" + bundle "exec #{gem_cmd} --version" + expect(out).to eq(Gem::VERSION) + end + + it "works when exec'ing to rubygems through sh -c" do + install_gemfile "source \"https://gem.repo1\"; gem \"myrack\"" + bundle "exec sh -c '#{gem_cmd} --version'" + expect(out).to eq(Gem::VERSION) + end + + it "works when exec'ing back to bundler to run a remote resolve" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + G + + bundle "exec bundle lock", env: { "BUNDLER_VERSION" => Bundler::VERSION } + + expect(out).to include("Writing lockfile") + end + it "respects custom process title when loading through ruby" do + skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform? + script_that_changes_its_own_title_and_checks_if_picked_up_by_ps_unix_utility = <<~'RUBY' - Process.setproctitle("1-2-3-4-5-6-7-8-9-10-11-12-13-14-15") + Process.setproctitle("1-2-3-4-5-6-7") puts `ps -ocommand= -p#{$$}` RUBY - create_file "Gemfile" + gemfile "Gemfile", "source \"https://gem.repo1\"" create_file "a.rb", script_that_changes_its_own_title_and_checks_if_picked_up_by_ps_unix_utility bundle "exec ruby a.rb" - expect(out).to eq("1-2-3-4-5-6-7-8-9-10-11-12-13-14-15") + expect(out).to eq("1-2-3-4-5-6-7") end it "accepts --verbose" do - install_gemfile 'gem "rack"' + install_gemfile "source \"https://gem.repo1\"; gem \"myrack\"" 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"' + install_gemfile "source \"https://gem.repo1\"; gem \"myrack\"" bundle "exec echo --verbose" expect(out).to eq("--verbose") end it "handles --keep-file-descriptors" do + skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform? + require "tempfile" command = Tempfile.new("io-test") @@ -95,27 +138,29 @@ RSpec.describe "bundle exec" do end G - install_gemfile "" - sys_exec "#{Gem.ruby} #{command.path}" + install_gemfile "source \"https://gem.repo1\"" + in_bundled_app "#{Gem.ruby} #{command.path}" - expect(out).to eq("") + expect(out).to be_empty expect(err).to be_empty end it "accepts --keep-file-descriptors" do - install_gemfile "" + install_gemfile "source \"https://gem.repo1\"" bundle "exec --keep-file-descriptors echo foobar" expect(err).to be_empty end it "can run a command named --verbose" do - install_gemfile 'gem "rack"' - File.open("--verbose", "w") do |f| + skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform? + + install_gemfile "source \"https://gem.repo1\"; gem \"myrack\"" + File.open(bundled_app("--verbose"), "w") do |f| f.puts "#!/bin/sh" f.puts "echo foobar" end - File.chmod(0o744, "--verbose") + File.chmod(0o744, bundled_app("--verbose")) with_path_as(".") do bundle "exec -- --verbose" end @@ -124,228 +169,225 @@ RSpec.describe "bundle exec" do it "handles different versions in different bundles" do build_repo2 do - build_gem "rack_two", "1.0.0" do |s| - s.executables = "rackup" + build_gem "myrack_two", "1.0.0" do |s| + s.executables = "myrackup" end end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "0.9.1" + source "https://gem.repo1" + gem "myrack", "0.9.1" G - Dir.chdir bundled_app2 do - install_gemfile bundled_app2("Gemfile"), <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack_two", "1.0.0" - G - end + install_gemfile bundled_app2("Gemfile"), <<-G, dir: bundled_app2 + source "https://gem.repo2" + gem "myrack_two", "1.0.0" + G - bundle! "exec rackup" + bundle "exec myrackup" expect(out).to eq("0.9.1") - Dir.chdir bundled_app2 do - bundle! "exec rackup" - expect(out).to eq("1.0.0") - end + bundle "exec myrackup", dir: bundled_app2 + expect(out).to eq("1.0.0") end context "with default gems" do - let(:system_gems_to_install) { [] } - - let(:default_irb_version) { ruby "gem 'irb', '< 999999'; require 'irb'; puts IRB::VERSION" } + # TODO: Switch to ERB::VERSION once Ruby 3.4 support is dropped, so all + # supported rubies include an `erb` gem version where `ERB::VERSION` is + # public + let(:default_erb_version) { ruby "require 'erb/version'; puts ERB.const_get(:VERSION)" } context "when not specified in Gemfile" do before do - skip "irb isn't a default gem" if default_irb_version.empty? - - install_gemfile "" + install_gemfile "source \"https://gem.repo1\"" end it "uses version provided by ruby" do - bundle! "exec irb --version" + bundle "exec erb --version" - expect(out).to include(default_irb_version) - expect(err).to be_empty + expect(stdboth).to eq(default_erb_version) end end context "when specified in Gemfile directly" do - let(:specified_irb_version) { "0.9.6" } + let(:specified_erb_version) { "2.0.0" } before do - skip "irb isn't a default gem" if default_irb_version.empty? - build_repo2 do - build_gem "irb", specified_irb_version do |s| - s.executables = "irb" + build_gem "erb", specified_erb_version do |s| + s.executables = "erb" end end install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "irb", "#{specified_irb_version}" + source "https://gem.repo2" + gem "erb", "#{specified_erb_version}" G end it "uses version specified" do - bundle! "exec irb --version" + bundle "exec erb --version" - expect(out).to eq(specified_irb_version) - expect(err).to be_empty + expect(stdboth).to eq(specified_erb_version) end end context "when specified in Gemfile indirectly" do - let(:indirect_irb_version) { "0.9.6" } + let(:indirect_erb_version) { "2.0.0" } before do - skip "irb isn't a default gem" if default_irb_version.empty? - build_repo2 do - build_gem "irb", indirect_irb_version do |s| - s.executables = "irb" + build_gem "erb", indirect_erb_version do |s| + s.executables = "erb" end - build_gem "gem_depending_on_old_irb" do |s| - s.add_dependency "irb", indirect_irb_version + build_gem "gem_depending_on_old_erb" do |s| + s.add_dependency "erb", indirect_erb_version end end install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "gem_depending_on_old_irb" + source "https://gem.repo2" + gem "gem_depending_on_old_erb" G - - bundle! "exec irb --version" end it "uses resolved version" do - expect(out).to eq(indirect_irb_version) - expect(err).to be_empty + bundle "exec erb --version" + + expect(stdboth).to eq(indirect_erb_version) end end end it "warns about executable conflicts" do build_repo2 do - build_gem "rack_two", "1.0.0" do |s| - s.executables = "rackup" + build_gem "myrack_two", "1.0.0" do |s| + s.executables = "myrackup" end end - bundle "config set path.system true" + bundle_config_global "path.system true" install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "0.9.1" + source "https://gem.repo1" + gem "myrack", "0.9.1" G - Dir.chdir bundled_app2 do - install_gemfile bundled_app2("Gemfile"), <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack_two", "1.0.0" - G - end + install_gemfile bundled_app2("Gemfile"), <<-G, dir: bundled_app2 + source "https://gem.repo2" + gem "myrack_two", "1.0.0" + G - bundle! "exec rackup" + bundle "exec myrackup" expect(last_command.stderr).to eq( - "Bundler is using a binstub that was created for a different gem (rack).\n" \ - "You should run `bundle binstub rack_two` to work around a system/bundle conflict." + "Bundler is using a binstub that was created for a different gem (myrack).\n" \ + "You should run `bundle binstub myrack_two` to work around a system/bundle conflict." ) end it "handles gems installed with --without" do - install_gemfile <<-G, forgotten_command_line_options(:without => "middleware") - source "#{file_uri_for(gem_repo1)}" - gem "rack" # rack 0.9.1 and 1.0 exist + bundle_config "without middleware" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" # myrack 0.9.1 and 1.0 exist group :middleware do - gem "rack_middleware" # rack_middleware depends on rack 0.9.1 + gem "myrack_middleware" # myrack_middleware depends on myrack 0.9.1 end G - bundle "exec rackup" + bundle "exec myrackup" expect(out).to eq("0.9.1") - expect(the_bundle).not_to include_gems "rack_middleware 1.0" + expect(the_bundle).not_to include_gems "myrack_middleware 1.0" end it "does not duplicate already exec'ed RUBYOPT" do + create_file("echoopt", "#!/usr/bin/env ruby\nprint ENV['RUBYOPT']") install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G - rubyopt = ENV["RUBYOPT"] - rubyopt = "-r#{lib_dir}/bundler/setup #{rubyopt}" + bundler_setup_opt = "-r#{lib_dir}/bundler/setup" + + rubyopt = opt_add(bundler_setup_opt, ENV["RUBYOPT"]) - bundle "exec 'echo $RUBYOPT'" - expect(out).to have_rubyopts(rubyopt) + bundle "exec echoopt" + expect(out.split(" ").count(bundler_setup_opt)).to eq(1) - bundle "exec 'echo $RUBYOPT'", :env => { "RUBYOPT" => rubyopt } - expect(out).to have_rubyopts(rubyopt) + bundle "exec echoopt", env: { "RUBYOPT" => rubyopt } + expect(out.split(" ").count(bundler_setup_opt)).to eq(1) end it "does not duplicate already exec'ed RUBYLIB" do + create_file("echolib", "#!/usr/bin/env ruby\nprint ENV['RUBYLIB']") install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G rubylib = ENV["RUBYLIB"] rubylib = rubylib.to_s.split(File::PATH_SEPARATOR).unshift lib_dir.to_s rubylib = rubylib.uniq.join(File::PATH_SEPARATOR) - bundle "exec 'echo $RUBYLIB'" + bundle "exec echolib" expect(out).to include(rubylib) - bundle "exec 'echo $RUBYLIB'", :env => { "RUBYLIB" => rubylib } + bundle "exec echolib", env: { "RUBYLIB" => rubylib } expect(out).to include(rubylib) end it "errors nicely when the argument doesn't exist" do install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "exec foobarbaz" - expect(exitstatus).to eq(127) if exitstatus + bundle "exec foobarbaz", raise_on_error: false + expect(exitstatus).to eq(127) expect(err).to include("bundler: command not found: foobarbaz") expect(err).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" + source "https://gem.repo1" + gem "myrack" G - bundle "exec touch foo" - bundle "exec ./foo" - expect(exitstatus).to eq(126) if exitstatus + bundled_app("foo").write("") + bundle "exec ./foo", raise_on_error: false + expect(exitstatus).to eq(126) expect(err).to include("bundler: not executable: ./foo") end it "errors nicely when no arguments are passed" do install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "exec" - expect(exitstatus).to eq(128) if exitstatus + bundle "exec", raise_on_error: false + expect(exitstatus).to eq(128) expect(err).to include("bundler: exec needs a command to run") end it "raises a helpful error when exec'ing to something outside of the bundle" do - bundle! "config set clean false" # want to keep the rackup binstub - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "with_license" + system_gems(%w[myrack-1.0.0 myrack-0.9.1], path: default_bundle_path) + + bundle_config "clean false" # want to keep the myrackup binstub + install_gemfile <<-G + source "https://gem.repo1" + gem "foo" G [true, false].each do |l| - bundle! "config set 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?" + bundle_config "disable_exec_load #{l}" + bundle "exec myrackup", raise_on_error: false + expect(err).to include "can't find executable myrackup for gem myrack. myrack is not currently included in the bundle, perhaps you meant to add it to your Gemfile?" end end @@ -357,14 +399,14 @@ RSpec.describe "bundle exec" do describe "when #{exec} is used" do before(:each) do install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" 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 @@ -385,6 +427,7 @@ RSpec.describe "bundle exec" do it "shows executable's man page when the executable has a -" do FileUtils.mv(bundled_app("print_args"), bundled_app("docker-template")) + FileUtils.mv(bundled_app("print_args.bat"), bundled_app("docker-template.bat")) if Gem.win_platform? bundle "#{exec} docker-template build discourse --help" expect(out).to eq('args: ["build", "discourse", "--help"]') end @@ -401,69 +444,71 @@ RSpec.describe "bundle exec" do it "shows bundle-exec's man page when --help is between exec and the executable" do with_fake_man do - bundle "#{exec} --help cat" + bundle "#{exec} --help echo" end - expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + expect(out).to include(%(["#{man_dir}/bundle-exec.1"])) end it "shows bundle-exec's man page when --help is before exec" do with_fake_man do bundle "--help #{exec}" end - expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + expect(out).to include(%(["#{man_dir}/bundle-exec.1"])) end it "shows bundle-exec's man page when -h is before exec" do with_fake_man do bundle "-h #{exec}" end - expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + expect(out).to include(%(["#{man_dir}/bundle-exec.1"])) end it "shows bundle-exec's man page when --help is after exec" do with_fake_man do bundle "#{exec} --help" end - expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + expect(out).to include(%(["#{man_dir}/bundle-exec.1"])) end it "shows bundle-exec's man page when -h is after exec" do with_fake_man do bundle "#{exec} -h" end - expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + expect(out).to include(%(["#{man_dir}/bundle-exec.1"])) end end end end describe "with gem executables" do - describe "run from a random directory", :ruby_repo do + describe "run from a random directory" do before(:each) do install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G end it "works when unlocked" do - bundle "exec 'cd #{tmp("gems")} && rackup'" + bundle "exec 'cd #{tmp("gems")} && myrackup'" expect(out).to eq("1.0.0") end it "works when locked" do expect(the_bundle).to be_locked - bundle "exec 'cd #{tmp("gems")} && rackup'" + bundle "exec 'cd #{tmp("gems")} && myrackup'" expect(out).to eq("1.0.0") end end describe "from gems bundled via :path" do before(:each) do - build_lib "fizz", :path => home("fizz") do |s| + build_lib "fizz", path: home("fizz") do |s| s.executables = "fizz" end install_gemfile <<-G + source "https://gem.repo1" gem "fizz", :path => "#{File.expand_path(home("fizz"))}" G end @@ -488,6 +533,7 @@ RSpec.describe "bundle exec" do end install_gemfile <<-G + source "https://gem.repo1" gem "fizz_git", :git => "#{lib_path("fizz_git-1.0")}" G end @@ -506,11 +552,12 @@ RSpec.describe "bundle exec" do describe "from gems bundled via :git with no gemspec" do before(:each) do - build_git "fizz_no_gemspec", :gemspec => false do |s| + build_git "fizz_no_gemspec", gemspec: false do |s| s.executables = "fizz_no_gemspec" end install_gemfile <<-G + source "https://gem.repo1" gem "fizz_no_gemspec", "1.0", :git => "#{lib_path("fizz_no_gemspec-1.0")}" G end @@ -530,16 +577,59 @@ RSpec.describe "bundle exec" do it "performs an automatic bundle install" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "0.9.1" + source "https://gem.repo1" + gem "myrack", "0.9.1" gem "foo" G - bundle "config set auto_install 1" - bundle "exec rackup" + bundle_config "auto_install 1" + bundle "exec myrackup", artifice: "compact_index" expect(out).to include("Installing foo 1.0") end + it "performs an automatic bundle install with git gems" do + build_git "foo" do |s| + s.executables = "foo" + end + gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + bundle_config "auto_install 1" + bundle "exec foo", artifice: "compact_index" + expect(out).to include("Fetching myrack 0.9.1") + expect(out).to include("Fetching #{lib_path("foo-1.0")}") + expect(out.lines).to end_with("1.0") + end + + it "loads the correct optparse when `auto_install` is set, and optparse is a dependency" do + build_repo4 do + build_gem "fastlane", "2.192.0" do |s| + s.executables = "fastlane" + s.add_dependency "optparse", "~> 999.999.999" + end + + build_gem "optparse", "999.999.998" + build_gem "optparse", "999.999.999" + end + + system_gems "optparse-999.999.998", gem_repo: gem_repo4 + + bundle_config "auto_install 1" + bundle_config "path vendor/bundle" + + gemfile <<~G + source "https://gem.repo4" + gem "fastlane" + G + + bundle "exec fastlane", artifice: "compact_index" + expect(out).to include("Installing optparse 999.999.999") + expect(out).to include("2.192.0") + end + describe "with gems bundled via :path with invalid gemspecs" do it "outputs the gemspec validation errors" do build_lib "foo" @@ -552,24 +642,28 @@ RSpec.describe "bundle exec" do s.version = '1.0' s.summary = 'TODO: Add summary' s.authors = 'Me' + s.rubygems_version = nil end G end - install_gemfile <<-G + gemfile <<-G + source "https://gem.repo1" gem "foo", :path => "#{lib_path("foo-1.0")}" G - bundle "exec irb" + bundle "exec erb", raise_on_error: false 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') + expect(err).to match(/missing value for attribute rubygems_version|rubygems_version must not be nil/) end end describe "with gems bundled for deployment" do it "works when calling bundler from another script" do gemfile <<-G + source "https://gem.repo1" + module Monkey def bin_path(a,b,c) raise Gem::GemNotFoundException.new('Fail') @@ -577,70 +671,124 @@ RSpec.describe "bundle exec" do end Bundler.rubygems.extend(Monkey) G - bundle "install --deployment" - bundle "exec ruby -e '`#{bindir.join("bundler")} -v`; puts $?.success?'" + bundle_config "path.system true" + bundle "install" + bundle "exec ruby -e '`bundle -v`; puts $?.success?'", env: { "BUNDLER_VERSION" => Bundler::VERSION } expect(out).to match("true") end end + describe "bundle exec gem uninstall" do + before do + build_repo4 do + build_gem "foo" + end + + install_gemfile <<-G + source "https://gem.repo4" + + gem "foo" + G + end + + it "works" do + bundle "exec #{gem_cmd} uninstall foo" + expect(out).to eq("Successfully uninstalled foo-1.0") + end + end + + describe "running gem commands in presence of rubygems plugins" do + before do + build_repo4 do + build_gem "foo" do |s| + s.write "lib/rubygems_plugin.rb", "puts 'FAIL'" + end + end + + system_gems "foo-1.0", path: default_bundle_path, gem_repo: gem_repo4 + + install_gemfile <<-G + source "https://gem.repo4" + G + end + + it "does not load plugins outside of the bundle" do + bundle "exec #{gem_cmd} -v" + expect(out).not_to include("FAIL") + 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 } + let(:executable) { <<~RUBY.strip } #{shebang} - require "rack" + require "myrack" 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 "MYRACK: \#{MYRACK}" + if Gem.win_platform? + process_title = "ruby" + else + process_title = `ps -o args -p \#{Process.pid}`.split("\n", 2).last.strip + end puts "PROCESS: \#{process_title}" RUBY before do - path.open("w") {|f| f << executable } - path.chmod(0o755) + create_file(bundled_app(path), executable) install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G end let(:exec) { "EXEC: load" } let(:args) { "ARGS: #{path} arg1 arg2" } - let(:rack) { "RACK: 1.0.0" } + let(:myrack) { "MYRACK: 1.0.0" } let(:process) do - title = "PROCESS: #{path}" - title += " arg1 arg2" - title + if Gem.win_platform? + "PROCESS: ruby" + else + "PROCESS: #{path} arg1 arg2" + end end let(:exit_code) { 0 } - let(:expected) { [exec, args, rack, process].join("\n") } + let(:expected) { [exec, args, myrack, process].join("\n") } let(:expected_err) { "" } - subject { bundle "exec #{path} arg1 arg2" } + subject { bundle "exec #{path} arg1 arg2", raise_on_error: false } - 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 + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) 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" + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end end context "with exit 99" do let(:exit_code) { 99 } - it_behaves_like "it runs" + + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end end end @@ -653,11 +801,17 @@ RSpec.describe "bundle exec" do end let(:expected_err) { "" } let(:exit_code) do - # signal mask 128 + plus signal 15 -> TERM - # this is specified by C99 - 128 + 15 + exit_status_for_signal(Signal.list["TERM"]) + end + + it "runs" do + skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform? + + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) end - it_behaves_like "it runs" end context "the executable is empty" do @@ -666,86 +820,146 @@ RSpec.describe "bundle exec" do let(:exit_code) { 0 } let(:expected_err) { "#{path} is empty" } let(:expected) { "" } - it_behaves_like "it runs" + + it "runs" do + # it's empty, so `create_file` won't add executable permission and bat scripts on Windows + bundled_app(path).chmod(0o755) + path.sub_ext(".bat").write <<~SCRIPT if Gem.win_platform? + @ECHO OFF + @"ruby.exe" "%~dpn0" %* + SCRIPT + + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end end context "the executable raises" do let(:executable) { super() << "\nraise 'ERROR'" } let(:exit_code) { 1 } let(:expected_err) do - "bundler: failed to load command: #{path} (#{path})" \ - "\nRuntimeError: ERROR\n #{path}:10:in `<top (required)>'" + /\Abundler: failed to load command: #{Regexp.quote(path.to_s)} \(#{Regexp.quote(path.to_s)}\)\n#{Regexp.quote(path.to_s)}:[0-9]+:in [`']<top \(required\)>': ERROR \(RuntimeError\)/ + end + + it "runs like a normally executed executable" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to match(expected_err) + expect(out).to eq(expected) end - it_behaves_like "it runs" end context "the executable raises an error without a backtrace" do let(:executable) { super() << "\nclass Err < Exception\ndef backtrace; end;\nend\nraise Err" } let(:exit_code) { 1 } - let(:expected_err) { "bundler: failed to load command: #{path} (#{path})\nErr: Err" } + let(:expected_err) { "bundler: failed to load command: #{path} (#{path})\n#{system_gem_path("bin/bundle")}: Err (Err)" } let(:expected) { super() } - it_behaves_like "it runs" + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end end context "when the file uses the current ruby shebang" do let(:shebang) { "#!#{Gem.ruby}" } - it_behaves_like "it runs" + + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end end - context "when Bundler.setup fails", :bundler => "< 3" do + context "when Bundler.setup fails" do before do + system_gems(%w[myrack-1.0.0 myrack-0.9.1], path: default_bundle_path) + gemfile <<-G - gem 'rack', '2' + source "https://gem.repo1" + gem 'myrack', '2' G ENV["BUNDLER_FORCE_TTY"] = "true" end let(:exit_code) { Bundler::GemNotFound.new.status_code } let(:expected) { "" } - let(:expected_err) { <<-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 + let(:expected_err) { <<~EOS.strip } + Could not find gem 'myrack (= 2)' in locally installed gems. + + The source contains the following gems matching 'myrack': + * myrack-0.9.1 + * myrack-1.0.0 + Run `bundle install` to install missing gems. EOS - it_behaves_like "it runs" + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end end - context "when Bundler.setup fails", :bundler => "3" do + context "when Bundler.setup fails and Gemfile is not the default" do before do - gemfile <<-G - gem 'rack', '2' + gemfile "CustomGemfile", <<-G + source "https://gem.repo1" + gem 'myrack', '2' G ENV["BUNDLER_FORCE_TTY"] = "true" + ENV["BUNDLE_GEMFILE"] = "CustomGemfile" + ENV["BUNDLER_ORIG_BUNDLE_GEMFILE"] = nil end let(:exit_code) { Bundler::GemNotFound.new.status_code } let(:expected) { "" } - let(:expected_err) { <<-EOS.strip } -\e[31mCould not find gem 'rack (= 2)' in locally installed gems. -The source contains 'rack' at: 1.0.0\e[0m -\e[33mRun `bundle install` to install missing gems.\e[0m - EOS - it_behaves_like "it runs" + it "prints proper suggestion" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to include("Run `bundle install --gemfile CustomGemfile` to install missing gems.") + expect(out).to eq(expected) + end 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" + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end end context "when disable_exec_load is set" do let(:exec) { "EXEC: exec" } - let(:process) { "PROCESS: ruby #{path} arg1 arg2" } + let(:process) do + if Gem.win_platform? + "PROCESS: ruby" + else + "PROCESS: ruby #{path} arg1 arg2" + end + end before do - bundle "config set disable_exec_load true" + bundle_config "disable_exec_load true" end - it_behaves_like "it runs" + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end end context "regarding $0 and __FILE__" do @@ -755,45 +969,72 @@ The source contains 'rack' at: 1.0.0\e[0m puts "__FILE__: #{__FILE__.inspect}" RUBY - let(:expected) { super() + <<-EOS.chomp } + context "when the path is absolute" do + let(:expected) { super() + <<~EOS.chomp } -$0: #{path.to_s.inspect} -__FILE__: #{path.to_s.inspect} - EOS + $0: #{path.to_s.inspect} + __FILE__: #{path.to_s.inspect} + EOS - it_behaves_like "it runs" + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end + end context "when the path is relative" do let(:path) { super().relative_path_from(bundled_app) } + let(:expected) { super() + <<~EOS.chomp } + + $0: #{path.to_s.inspect} + __FILE__: #{path.to_s.inspect} + EOS - it_behaves_like "it runs" + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end end context "when the path is relative with a leading ./" do - let(:path) { Pathname.new("./#{super().relative_path_from(Pathname.pwd)}") } - - pending "relative paths with ./ have absolute __FILE__" + let(:path) { Pathname.new("./#{super().relative_path_from(bundled_app)}") } + let(:expected) { super() + <<~EOS.chomp } + + $0: #{path.to_s.inspect} + __FILE__: #{File.expand_path(path, bundled_app).inspect} + EOS + + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end end end context "signal handling" do let(:test_signals) do open3_reserved_signals = %w[CHLD CLD PIPE] - reserved_signals = %w[SEGV BUS ILL FPE VTALRM KILL STOP EXIT] + reserved_signals = %w[SEGV BUS ILL FPE ABRT IOT VTALRM KILL STOP EXIT] bundler_signals = %w[INT] Signal.list.keys - (bundler_signals + reserved_signals + open3_reserved_signals) end context "signals being trapped by bundler" do - let(:executable) { strip_whitespace <<-RUBY } + let(:executable) { <<~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" + raise RuntimeError, "Didn't receive expected INT" end.join rescue Interrupt puts "foo" @@ -801,7 +1042,9 @@ __FILE__: #{path.to_s.inspect} RUBY it "receives the signal" do - bundle!("exec #{path}") do |_, o, thr| + skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform? + + bundle("exec #{path}") do |_, o, thr| o.gets # Consumes 'Started' and ensures that thread has started Process.kill("INT", thr.pid) end @@ -811,7 +1054,7 @@ __FILE__: #{path.to_s.inspect} end context "signals not being trapped by bunder" do - let(:executable) { strip_whitespace <<-RUBY } + let(:executable) { <<~RUBY } #{shebang} signals = #{test_signals.inspect} @@ -822,11 +1065,13 @@ __FILE__: #{path.to_s.inspect} RUBY it "makes sure no unexpected signals are restored to DEFAULT" do + skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform? + test_signals.each do |n| Signal.trap(n, "IGNORE") end - bundle!("exec #{path}") + bundle("exec #{path}") expect(out).to eq(test_signals.count.to_s) end @@ -837,65 +1082,191 @@ __FILE__: #{path.to_s.inspect} context "nested bundle exec" do context "when bundle in a local path" do before do + skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform? + gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "config set path vendor/bundler" - bundle! :install + bundle_config "path vendor/bundler" + bundle :install end - it "correctly shells out", :ruby_repo do + it "correctly shells out" do file = bundled_app("file_that_bundle_execs.rb") - create_file(file, <<-RB) + create_file(file, <<-RUBY) #!#{Gem.ruby} puts `bundle exec echo foo` - RB + RUBY file.chmod(0o777) - bundle! "exec #{file}" + bundle "exec #{file}", env: { "PATH" => path } expect(out).to eq("foo") end end + context "when Kernel.require uses extra monkeypatches" do + before do + skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform? + + install_gemfile "source \"https://gem.repo1\"" + end + + it "does not undo the monkeypatches" do + karafka = bundled_app("bin/karafka") + create_file(karafka, <<~RUBY) + #!#{Gem.ruby} + + module Kernel + module_function + + alias_method :require_before_extra_monkeypatches, :require + + def require(path) + puts "requiring \#{path} used the monkeypatch" + + require_before_extra_monkeypatches(path) + end + end + + Bundler.setup(:default) + + require "foo" + RUBY + karafka.chmod(0o777) + + foreman = bundled_app("bin/foreman") + create_file(foreman, <<~RUBY) + #!#{Gem.ruby} + + puts `bundle exec bin/karafka` + RUBY + foreman.chmod(0o777) + + bundle "exec #{foreman}" + expect(out).to eq("requiring foo used the monkeypatch") + end + end + + context "when gemfile and path are configured", :ruby_repo do + before do + build_repo2 do + build_gem "rails", "6.1.0" do |s| + s.executables = "rails" + end + end + + bundle_config "path vendor/bundle" + bundle_config "gemfile gemfiles/myrack_6_1.gemfile" + + gemfile(bundled_app("gemfiles/myrack_6_1.gemfile"), <<~RUBY) + source "https://gem.repo2" + + gem "rails", "6.1.0" + RUBY + + # A Gemfile needs to be in the root to trick bundler's root resolution + gemfile "source 'https://gem.repo1'" + + bundle "install", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s } + end + + it "can still find gems after a nested subprocess" do + script = bundled_app("bin/myscript") + + create_file(script, <<~RUBY) + #!#{Gem.ruby} + + puts `bundle exec rails` + RUBY + + script.chmod(0o777) + + bundle "exec #{script}" + + expect(err).to be_empty + expect(out).to eq("6.1.0") + end + + it "can still find gems after a nested subprocess when using bundler (with a final r) executable" do + script = bundled_app("bin/myscript") + + create_file(script, <<~RUBY) + #!#{Gem.ruby} + + puts `bundler exec rails` + RUBY + + script.chmod(0o777) + + bundle "exec #{script}" + + expect(err).to be_empty + expect(out).to eq("6.1.0") + 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? + default_openssl_version = ruby "require 'openssl'; puts OpenSSL::VERSION" - install_gemfile! "" # must happen before installing the broken system gem + skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform? + + install_gemfile "source \"https://gem.repo1\"" # 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 + s.write("lib/openssl.rb", <<-RUBY) + raise ArgumentError, "custom openssl should not be loaded" + RUBY end end - system_gems(:bundler, "openssl-#{openssl_version}", :gem_repo => gem_repo4) + system_gems("openssl-#{openssl_version}", gem_repo: gem_repo4) file = bundled_app("require_openssl.rb") - create_file(file, <<-RB) + create_file(file, <<-RUBY) #!/usr/bin/env ruby require "openssl" puts OpenSSL::VERSION warn Gem.loaded_specs.values.map(&:full_name) - RB + RUBY file.chmod(0o777) + env = { "PATH" => path } aggregate_failures do - expect(bundle!("exec #{file}", :artifice => nil)).to eq(expected) - expect(bundle!("exec bundle exec #{file}", :artifice => nil)).to eq(expected) - expect(bundle!("exec ruby #{file}", :artifice => nil)).to eq(expected) - expect(run!(file.read, :artifice => nil)).to eq(expected) + expect(bundle("exec #{file}", env: env)).to eq(default_openssl_version) + expect(bundle("exec bundle exec #{file}", env: env)).to eq(default_openssl_version) + expect(bundle("exec ruby #{file}", env: env)).to eq(default_openssl_version) + expect(run(file.read, artifice: nil, env: env)).to eq(default_openssl_version) end + skip "ruby_core has openssl and rubygems in the same folder, and this test needs rubygems require but default openssl not in a directly added entry in $LOAD_PATH" if ruby_core? # sanity check that we get the newer, custom version without bundler - sys_exec("#{Gem.ruby} #{file}") + sys_exec "#{Gem.ruby} #{file}", env: env, raise_on_error: false expect(err).to include("custom openssl should not be loaded") end end + + context "with a git gem that includes extensions", :ruby_repo do + before do + build_git "simple_git_binary", &:add_c_extension + bundle_config "path .bundle" + install_gemfile <<-G + source "https://gem.repo1" + gem "simple_git_binary", :git => '#{lib_path("simple_git_binary-1.0")}' + G + end + + it "allows calling bundle install" do + bundle "exec bundle install" + end + + it "allows calling bundle install after removing gem.build_complete" do + FileUtils.rm_r Dir[bundled_app(".bundle/**/gem.build_complete")] + bundle "exec #{Gem.ruby} -S bundle install" + end + end end end diff --git a/spec/bundler/commands/fund_spec.rb b/spec/bundler/commands/fund_spec.rb new file mode 100644 index 0000000000..5883b8a63a --- /dev/null +++ b/spec/bundler/commands/fund_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +RSpec.describe "bundle fund" do + before do + build_repo2 do + build_gem "has_funding_and_other_metadata" do |s| + s.metadata = { + "bug_tracker_uri" => "https://example.com/user/bestgemever/issues", + "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md", + "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1", + "homepage_uri" => "https://bestgemever.example.io", + "mailing_list_uri" => "https://groups.example.com/bestgemever", + "funding_uri" => "https://example.com/has_funding_and_other_metadata/funding", + "source_code_uri" => "https://example.com/user/bestgemever", + "wiki_uri" => "https://example.com/user/bestgemever/wiki", + } + end + + build_gem "has_funding", "1.2.3" do |s| + s.metadata = { + "funding_uri" => "https://example.com/has_funding/funding", + } + end + + build_gem "gem_with_dependent_funding", "1.0" do |s| + s.add_dependency "has_funding" + end + end + end + + it "prints fund information for all gems in the bundle" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'has_funding_and_other_metadata' + gem 'has_funding' + gem 'myrack-obama' + G + + bundle "fund" + + expect(out).to include("* has_funding_and_other_metadata (1.0)\n Funding: https://example.com/has_funding_and_other_metadata/funding") + expect(out).to include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding") + expect(out).to_not include("myrack-obama") + end + + it "does not consider fund information for gem dependencies" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'gem_with_dependent_funding' + G + + bundle "fund" + + expect(out).to_not include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding") + expect(out).to_not include("gem_with_dependent_funding") + end + + it "does not consider fund information for uninstalled optional dependencies" do + install_gemfile <<-G + source "https://gem.repo2" + group :whatever, optional: true do + gem 'has_funding_and_other_metadata' + end + gem 'has_funding' + gem 'myrack-obama' + G + + bundle "fund" + + expect(out).to include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding") + expect(out).to_not include("has_funding_and_other_metadata") + expect(out).to_not include("myrack-obama") + end + + it "considers fund information for installed optional dependencies" do + bundle_config "with whatever" + + install_gemfile <<-G + source "https://gem.repo2" + group :whatever, optional: true do + gem 'has_funding_and_other_metadata' + end + gem 'has_funding' + gem 'myrack-obama' + G + + bundle "fund" + + expect(out).to include("* has_funding_and_other_metadata (1.0)\n Funding: https://example.com/has_funding_and_other_metadata/funding") + expect(out).to include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding") + expect(out).to_not include("myrack-obama") + end + + it "prints message if none of the gems have fund information" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'myrack-obama' + G + + bundle "fund" + + expect(out).to include("None of the installed gems you directly depend on are looking for funding.") + end + + describe "with --group option" do + it "prints fund message for only specified group gems" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'has_funding_and_other_metadata', :group => :development + gem 'has_funding' + G + + bundle "fund --group development" + expect(out).to include("* has_funding_and_other_metadata (1.0)\n Funding: https://example.com/has_funding_and_other_metadata/funding") + expect(out).to_not include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding") + end + end +end diff --git a/spec/bundler/commands/help_spec.rb b/spec/bundler/commands/help_spec.rb index f4f90b9347..f9ad9fff14 100644 --- a/spec/bundler/commands/help_spec.rb +++ b/spec/bundler/commands/help_spec.rb @@ -1,33 +1,37 @@ # frozen_string_literal: true RSpec.describe "bundle help" do - it "uses mann when available" do + it "uses man when available" do with_fake_man do bundle "help gemfile" end - expect(out).to eq(%(["#{root}/man/gemfile.5"])) + expect(out).to eq(%(["#{man_dir}/gemfile.5"])) end - it "prefixes bundle commands with bundle- when finding the groff files" do + it "prefixes bundle commands with bundle- when finding the man files" do with_fake_man do bundle "help install" end - expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + expect(out).to eq(%(["#{man_dir}/bundle-install.1"])) end - it "simply outputs the txt file when there is no man on the path" do - with_path_as("") do - bundle "help install" + it "prexifes bundle commands with bundle- and resolves aliases when finding the man files" do + with_fake_man do + bundle "help package" end - expect(out).to match(/BUNDLE-INSTALL/) + expect(out).to eq(%(["#{man_dir}/bundle-cache.1"])) 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") + it "simply outputs the human readable file when there is no man on the path" do + with_path_as("") do + bundle "help install" + end + expect(out).to match(/bundle-install/) end it "looks for a binary and executes it with --help option if it's named bundler-<task>" do + skip "Could not find command testtasks, probably because not a windows friendly executable" if Gem.win_platform? + File.open(tmp("bundler-testtasks"), "w", 0o755) do |f| f.puts "#!/usr/bin/env ruby\nputs ARGV.join(' ')\n" end @@ -36,7 +40,6 @@ RSpec.describe "bundle help" do bundle "help testtasks" end - expect(exitstatus).to be_zero if exitstatus expect(out).to eq("--help") end @@ -44,33 +47,33 @@ RSpec.describe "bundle help" do with_fake_man do bundle "install --help" end - expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + expect(out).to eq(%(["#{man_dir}/bundle-install.1"])) end it "is called when the --help flag is used before the command" do with_fake_man do bundle "--help install" end - expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + expect(out).to eq(%(["#{man_dir}/bundle-install.1"])) end it "is called when the -h flag is used before the command" do with_fake_man do bundle "-h install" end - expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + expect(out).to eq(%(["#{man_dir}/bundle-install.1"])) end it "is called when the -h flag is used after the command" do with_fake_man do bundle "install -h" end - expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + expect(out).to eq(%(["#{man_dir}/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" + bundle "instill -h", raise_on_error: false end expect(err).to include('Could not find command "instill".') end @@ -79,11 +82,11 @@ RSpec.describe "bundle help" do with_fake_man do bundle "--help" end - expect(out).to eq(%(["#{root}/man/bundle.1"])) + expect(out).to eq(%(["#{man_dir}/bundle.1"])) with_fake_man do bundle "-h" end - expect(out).to eq(%(["#{root}/man/bundle.1"])) + expect(out).to eq(%(["#{man_dir}/bundle.1"])) end end diff --git a/spec/bundler/commands/info_spec.rb b/spec/bundler/commands/info_spec.rb index 4572823498..a26b1696fb 100644 --- a/spec/bundler/commands/info_spec.rb +++ b/spec/bundler/commands/info_spec.rb @@ -3,18 +3,34 @@ RSpec.describe "bundle info" do context "with a standard Gemfile" do before do + build_repo2 do + build_gem "has_metadata" do |s| + s.metadata = { + "bug_tracker_uri" => "https://example.com/user/bestgemever/issues", + "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md", + "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1", + "homepage_uri" => "https://bestgemever.example.io", + "mailing_list_uri" => "https://groups.example.com/bestgemever", + "source_code_uri" => "https://example.com/user/bestgemever", + "wiki_uri" => "https://example.com/user/bestgemever/wiki", + } + end + end + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo2" gem "rails" + gem "has_metadata" + gem "thin" G end it "creates a Gemfile.lock when invoked with a gem name" do - FileUtils.rm("Gemfile.lock") + FileUtils.rm(bundled_app_lock) bundle "info rails" - expect(bundled_app("Gemfile.lock")).to exist + expect(bundled_app_lock).to exist end it "prints information if gem exists in bundle" do @@ -35,19 +51,65 @@ RSpec.describe "bundle info" do expect(out).to eq(root.to_s) end + it "prints gem version if exists in bundle" do + bundle "info rails --version" + expect(out).to eq("2.3.2") + end + + it "doesn't claim that bundler is missing, even if using a custom path without bundler there" do + bundle_config "path vendor/bundle" + bundle "install" + bundle "info bundler" + expect(out).to include("\tPath: #{root}") + expect(err).not_to match(/The gem bundler is missing/i) + end + it "complains if gem not in bundle" do - bundle "info missing" + bundle "info missing", raise_on_error: false expect(err).to eq("Could not find gem 'missing'.") end - context "given a default gem shippped in ruby", :ruby_repo do + it "warns if path does not exist on disk, but specification is there" do + FileUtils.rm_r(default_bundle_path("gems", "rails-2.3.2")) + + bundle "info rails --path" + + expect(err).to include("The gem rails is missing.") + expect(err).to include(default_bundle_path("gems", "rails-2.3.2").to_s) + + bundle "info rail --path" + expect(err).to include("The gem rails is missing.") + expect(err).to include(default_bundle_path("gems", "rails-2.3.2").to_s) + + bundle "info rails" + expect(err).to include("The gem rails is missing.") + expect(err).to include(default_bundle_path("gems", "rails-2.3.2").to_s) + end + + context "given a default gem shipped in ruby", :ruby_repo do it "prints information about the default gem" do - bundle "info rdoc" - expect(out).to include("* rdoc") + bundle "info json" + expect(out).to include("* json") expect(out).to include("Default Gem: yes") end end + context "given a gem with metadata" do + it "prints the gem metadata" do + bundle "info has_metadata" + expect(out).to include "* has_metadata (1.0) +\tSummary: This is just a fake gem for testing +\tHomepage: http://example.com +\tDocumentation: https://www.example.info/gems/bestgemever/0.0.1 +\tSource Code: https://example.com/user/bestgemever +\tWiki: https://example.com/user/bestgemever/wiki +\tChangelog: https://example.com/user/bestgemever/CHANGELOG.md +\tBug Tracker: https://example.com/user/bestgemever/issues +\tMailing List: https://groups.example.com/bestgemever +\tPath: #{default_bundle_path("gems", "has_metadata-1.0")}" + end + end + context "when gem does not have homepage" do before do build_repo2 do @@ -62,6 +124,30 @@ RSpec.describe "bundle info" do expect(out).to_not include("Homepage:") end end + + context "when gem has a reverse dependency on any version" do + it "prints the details" do + bundle "info myrack" + + expect(out).to include("Reverse Dependencies: \n\t\tthin (1.0) depends on myrack (>= 0)") + end + end + + context "when gem has a reverse dependency on a specific version" do + it "prints the details" do + bundle "info actionpack" + + expect(out).to include("Reverse Dependencies: \n\t\trails (2.3.2) depends on actionpack (= 2.3.2)") + end + end + + context "when gem has no reverse dependencies" do + it "excludes the reverse dependencies field from the output" do + bundle "info rails" + + expect(out).not_to include("Reverse Dependencies:") + end + end end context "with a git repo in the Gemfile" do @@ -71,21 +157,23 @@ RSpec.describe "bundle info" do it "prints out git info" do install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G expect(the_bundle).to include_gems "foo 1.0" bundle "info foo" - expect(out).to include("foo (1.0 #{@git.ref_for("master", 6)}") + expect(out).to include("foo (1.0 #{@git.ref_for("main", 6)}") end - it "prints out branch names other than master" do - update_git "foo", :branch => "omg" do |s| + it "prints out branch names other than main" 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 + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "omg" G expect(the_bundle).to include_gems "foo 1.0.omg" @@ -97,6 +185,7 @@ RSpec.describe "bundle info" do it "doesn't print the branch when tied to a ref" do sha = revision_for(lib_path("foo-1.0")) install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{sha}" G @@ -105,41 +194,56 @@ RSpec.describe "bundle info" do end it "handles when a version is a '-' prerelease" do - @git = build_git("foo", "1.0.0-beta.1", :path => lib_path("foo")) + @git = build_git("foo", "1.0.0-beta.1", path: lib_path("foo")) install_gemfile <<-G + source "https://gem.repo1" 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! "info foo" + bundle "info foo" expect(out).to include("foo (1.0.0.pre.beta.1") end end context "with a valid regexp for gem name" do - it "presents alternatives" do + it "presents alternatives", :readline do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - gem "rack-obama" + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" G bundle "info rac" - expect(out).to eq "1 : rack\n2 : rack-obama\n0 : - exit -\n>" + expect(out).to match(/\A1 : myrack\n2 : myrack-obama\n0 : - exit -(\n>|\z)/) end end context "with an invalid regexp for gem name" do it "does not find the gem" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rails" G invalid_regexp = "[]" - bundle "info #{invalid_regexp}" + bundle "info #{invalid_regexp}", raise_on_error: false expect(err).to include("Could not find gem '#{invalid_regexp}'.") end end + + context "with without configured" do + it "does not find the gem, but gives a helpful error" do + bundle_config "without test" + + install_gemfile <<-G + source "https://gem.repo1" + gem "rails", group: :test + G + + bundle "info rails", raise_on_error: false + expect(err).to include("Could not find gem 'rails', because it's in the group 'test', configured to be ignored.") + end + end end diff --git a/spec/bundler/commands/init_spec.rb b/spec/bundler/commands/init_spec.rb index 7960ce85bd..989d6fa812 100644 --- a/spec/bundler/commands/init_spec.rb +++ b/spec/bundler/commands/init_spec.rb @@ -2,24 +2,47 @@ RSpec.describe "bundle init" do it "generates a Gemfile" do - bundle! :init + bundle :init expect(out).to include("Writing new Gemfile") - expect(bundled_app("Gemfile")).to be_file + expect(bundled_app_gemfile).to be_file + end + + context "with a template with permission flags not matching current process umask" do + let(:template_file) do + gemfile = Bundler.preferred_gemfile_name + templates_dir.join(gemfile) + end + + let(:target_dir) { bundled_app("init_permissions_test") } + + around do |example| + old_chmod = File.stat(template_file).mode + FileUtils.chmod(old_chmod | 0o111, template_file) # chmod +x + example.run + FileUtils.chmod(old_chmod, template_file) + end + + it "honours the current process umask when generating from a template" do + FileUtils.mkdir(target_dir) + bundle :init, dir: target_dir + generated_mode = File.stat(File.join(target_dir, "Gemfile")).mode & 0o111 + expect(generated_mode).to be_zero + end end context "when a Gemfile already exists" do before do - create_file "Gemfile", <<-G + gemfile <<-G gem "rails" G end it "does not change existing Gemfiles" do - expect { bundle :init }.not_to change { File.read(bundled_app("Gemfile")) } + expect { bundle :init, raise_on_error: false }.not_to change { File.read(bundled_app_gemfile) } end it "notifies the user that an existing Gemfile already exists" do - bundle :init + bundle :init, raise_on_error: false expect(err).to include("Gemfile already exists") end end @@ -28,13 +51,11 @@ RSpec.describe "bundle init" do let(:subdir) { "child_dir" } it "lets users generate a Gemfile in a child directory" do - bundle! :init + bundle :init FileUtils.mkdir bundled_app(subdir) - Dir.chdir bundled_app(subdir) do - bundle! :init - end + bundle :init, dir: bundled_app(subdir) expect(out).to include("Writing new Gemfile") expect(bundled_app("#{subdir}/Gemfile")).to be_file @@ -44,15 +65,13 @@ RSpec.describe "bundle init" do context "when the dir is not writable by the current user" do let(:subdir) { "child_dir" } - it "notifies the user that it can not write to it" do + it "notifies the user that it cannot write to it" do FileUtils.mkdir bundled_app(subdir) # chmod a-w it mode = File.stat(bundled_app(subdir)).mode ^ 0o222 FileUtils.chmod mode, bundled_app(subdir) - Dir.chdir bundled_app(subdir) do - bundle :init - end + bundle :init, dir: bundled_app(subdir), raise_on_error: false expect(err).to include("directory is not writable") expect(Dir[bundled_app("#{subdir}/*")]).to be_empty @@ -60,24 +79,24 @@ RSpec.describe "bundle init" do end context "given --gemspec option" do - let(:spec_file) { tmp.join("test.gemspec") } + let(:spec_file) { tmp("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_dependency 'myrack', '= 1.0.1' s.add_development_dependency 'rspec', '1.2' end S end - bundle :init, :gemspec => spec_file + bundle :init, gemspec: spec_file - gemfile = bundled_app("Gemfile").read + 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 "myrack", "= 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 @@ -93,34 +112,34 @@ RSpec.describe "bundle init" do S end - bundle :init, :gemspec => spec_file + bundle :init, gemspec: spec_file, raise_on_error: false expect(err).to include("There was an error while loading `test.gemspec`") end end end context "when init_gems_rb setting is enabled" do - before { bundle "config set init_gems_rb true" } + before { bundle_config "init_gems_rb true" } it "generates a gems.rb" do - bundle! :init + bundle :init expect(out).to include("Writing new gems.rb") expect(bundled_app("gems.rb")).to be_file end context "when gems.rb already exists" do before do - create_file("gems.rb", <<-G) + gemfile("gems.rb", <<-G) gem "rails" G end it "does not change existing Gemfiles" do - expect { bundle :init }.not_to change { File.read(bundled_app("gems.rb")) } + expect { bundle :init, raise_on_error: false }.not_to change { File.read(bundled_app("gems.rb")) } end it "notifies the user that an existing gems.rb already exists" do - bundle :init + bundle :init, raise_on_error: false expect(err).to include("gems.rb already exists") end end @@ -129,13 +148,11 @@ RSpec.describe "bundle init" do let(:subdir) { "child_dir" } it "lets users generate a Gemfile in a child directory" do - bundle! :init + bundle :init FileUtils.mkdir bundled_app(subdir) - Dir.chdir bundled_app(subdir) do - bundle! :init - end + bundle :init, dir: bundled_app(subdir) expect(out).to include("Writing new gems.rb") expect(bundled_app("#{subdir}/gems.rb")).to be_file @@ -143,14 +160,14 @@ RSpec.describe "bundle init" do end context "given --gemspec option" do - let(:spec_file) { tmp.join("test.gemspec") } + let(:spec_file) { tmp("test.gemspec") } before 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_dependency 'myrack', '= 1.0.1' s.add_development_dependency 'rspec', '1.2' end S @@ -158,20 +175,33 @@ RSpec.describe "bundle init" do end it "should generate from an existing gemspec" do - bundle :init, :gemspec => spec_file + bundle :init, gemspec: spec_file gemfile = bundled_app("gems.rb").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 "myrack", "= 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 it "prints message to user" do - bundle :init, :gemspec => spec_file + bundle :init, gemspec: spec_file expect(out).to include("Writing new gems.rb") end end end + + describe "using the --gemfile" do + it "should use the --gemfile value to name the gemfile" do + custom_gemfile_name = "NiceGemfileName" + + bundle :init, gemfile: custom_gemfile_name + + expect(out).to include("Writing new #{custom_gemfile_name}") + used_template = File.read("#{source_root}/lib/bundler/templates/Gemfile") + generated_gemfile = bundled_app(custom_gemfile_name).read + expect(generated_gemfile).to eq(used_template) + end + end end diff --git a/spec/bundler/commands/inject_spec.rb b/spec/bundler/commands/inject_spec.rb deleted file mode 100644 index 01c1f91877..0000000000 --- a/spec/bundler/commands/inject_spec.rb +++ /dev/null @@ -1,117 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe "bundle inject", :bundler => "< 3" do - before :each do - gemfile <<-G - source "#{file_uri_for(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(err).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(err).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_uri_for(gem_repo1)}" - gemfile = bundled_app("Gemfile").read - str = "gem \"foo\", \"> 0\", :source => \"#{file_uri_for(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" - if Bundler.feature_flag.bundler_3_mode? - bundle! "config set --local deployment true" - else - bundle! "config set --local frozen true" - end - 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_DEPLOYMENT"] || config["BUNDLE_FROZEN"]).to eq("true") - end - - it "doesn't allow Gemfile changes" do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack-obama" - G - bundle "inject 'rack' '> 0'" - expect(err).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 index 8e161a4aae..3b24434dc7 100644 --- a/spec/bundler/commands/install_spec.rb +++ b/spec/bundler/commands/install_spec.rb @@ -4,7 +4,7 @@ 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_uri_for(gem_repo1)}" + source "https://gem.repo1" G bundle :install @@ -12,47 +12,101 @@ RSpec.describe "bundle install with gem sources" do end it "does not make a lockfile if the install fails" do - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false raise StandardError, "FAIL" G expect(err).to include('StandardError, "FAIL"') - expect(bundled_app("Gemfile.lock")).not_to exist + expect(bundled_app_lock).not_to exist end it "creates a Gemfile.lock" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - expect(bundled_app("Gemfile.lock")).to exist + expect(bundled_app_lock).to exist end - it "does not create ./.bundle by default", :bundler => "< 3" do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + it "creates lockfile based on the lockfile method in Gemfile" do + install_gemfile <<-G + lockfile "OmgFile.lock" + source "https://gem.repo1" + gem "myrack", "1.0" + G + + bundle "install" + + expect(bundled_app("OmgFile.lock")).to exist + end + + it "creates lockfile using BUNDLE_LOCKFILE instead of lockfile method" do + ENV["BUNDLE_LOCKFILE"] = "ReallyOmgFile.lock" + install_gemfile <<-G + lockfile "OmgFile.lock" + source "https://gem.repo1" + gem "myrack", "1.0" + G + + expect(bundled_app("ReallyOmgFile.lock")).to exist + expect(bundled_app("OmgFile.lock")).not_to exist + ensure + ENV.delete("BUNDLE_LOCKFILE") + end + + it "creates lockfile based on --lockfile option is given" do + gemfile bundled_app("OmgFile"), <<-G + source "https://gem.repo1" + gem "myrack", "1.0" + G + + bundle "install --gemfile OmgFile --lockfile ReallyOmgFile.lock" + + expect(bundled_app("ReallyOmgFile.lock")).to exist + end + + it "does not make a lockfile if lockfile false is used in Gemfile" do + install_gemfile <<-G + lockfile false + source "https://gem.repo1" + gem 'myrack' + G + + expect(bundled_app_lock).not_to exist + end + + it "does not create ./.bundle by default" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" G - bundle! :install # can't use install_gemfile since it sets retry expect(bundled_app(".bundle")).not_to exist end + it "will create a ./.bundle by default", bundler: "5" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + expect(bundled_app(".bundle")).to exist + end + it "does not create ./.bundle by default when installing to system gems" do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + install_gemfile <<-G, env: { "BUNDLE_PATH__SYSTEM" => "true" } + source "https://gem.repo1" + gem "myrack" G - bundle! :install, :env => { "BUNDLE_PATH__SYSTEM" => true } # 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 + it "creates lockfiles based on the Gemfile name" do gemfile bundled_app("OmgFile"), <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "1.0" + source "https://gem.repo1" + gem "myrack", "1.0" G bundle "install --gemfile OmgFile" @@ -60,71 +114,126 @@ RSpec.describe "bundle install with gem sources" do expect(bundled_app("OmgFile.lock")).to exist end + it "doesn't create a lockfile if --no-lock option is given" do + gemfile bundled_app("OmgFile"), <<-G + source "https://gem.repo1" + gem "myrack", "1.0" + G + + bundle "install --gemfile OmgFile --no-lock" + + expect(bundled_app("OmgFile.lock")).not_to exist + end + + it "doesn't create a lockfile if --no-lock and --lockfile options are given" do + gemfile bundled_app("OmgFile"), <<-G + source "https://gem.repo1" + gem "myrack", "1.0" + G + + bundle "install --gemfile OmgFile --no-lock --lockfile ReallyOmgFile.lock" + + expect(bundled_app("OmgFile.lock")).not_to exist + expect(bundled_app("ReallyOmgFile.lock")).not_to exist + end + it "doesn't delete the lockfile if one already exists" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' G - lockfile = File.read(bundled_app("Gemfile.lock")) + lockfile = File.read(bundled_app_lock) - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false raise StandardError, "FAIL" G - expect(File.read(bundled_app("Gemfile.lock"))).to eq(lockfile) + expect(File.read(bundled_app_lock)).to eq(lockfile) end it "does not touch the lockfile if nothing changed" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - expect { run "1" }.not_to change { File.mtime(bundled_app("Gemfile.lock")) } + expect { run "1" }.not_to change { File.mtime(bundled_app_lock) } end it "fetches gems" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' G - expect(default_bundle_path("gems/rack-1.0.0")).to exist - expect(the_bundle).to include_gems("rack 1.0.0") + expect(default_bundle_path("gems/myrack-1.0.0")).to exist + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "auto-heals missing gems" do + install_gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + G + + FileUtils.rm_r(default_bundle_path("gems/myrack-1.0.0")) + + bundle "install --verbose" + + expect(out).to include("Installing myrack 1.0.0") + expect(default_bundle_path("gems/myrack-1.0.0")).to exist + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "does not state that it's constantly reinstalling empty gems" do + build_repo4 do + build_gem "empty", "1.0.0", no_default: true + end + + install_gemfile <<~G + source "https://gem.repo4" + + gem "empty" + G + gem_dir = default_bundle_path("gems/empty-1.0.0") + expect(gem_dir).to be_empty + + bundle "install --verbose" + expect(out).not_to include("Installing empty") end it "fetches gems when multiple versions are specified" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack', "> 0.9", "< 1.0" + source "https://gem.repo1" + gem 'myrack', "> 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") + expect(default_bundle_path("gems/myrack-0.9.1")).to exist + expect(the_bundle).to include_gems("myrack 0.9.1") end it "fetches gems when multiple versions are specified take 2" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack', "< 1.0", "> 0.9" + source "https://gem.repo1" + gem 'myrack', "< 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") + expect(default_bundle_path("gems/myrack-0.9.1")).to exist + expect(the_bundle).to include_gems("myrack 0.9.1") end it "raises an appropriate error when gems are specified using symbols" do - install_gemfile(<<-G) - source "#{file_uri_for(gem_repo1)}" - gem :rack + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem :myrack G - expect(exitstatus).to eq(4) if exitstatus + expect(exitstatus).to eq(4) end it "pulls in dependencies" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rails" G @@ -133,16 +242,22 @@ RSpec.describe "bundle install with gem sources" do it "does the right version" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "0.9.1" + source "https://gem.repo1" + gem "myrack", "0.9.1" G - expect(the_bundle).to include_gems "rack 0.9.1" + expect(the_bundle).to include_gems "myrack 0.9.1" end it "does not install the development dependency" do + build_repo2 do + build_gem "with_development_dependency" do |s| + s.add_development_dependency "activesupport", "= 2.3.5" + end + end + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo2" gem "with_development_dependency" G @@ -152,7 +267,7 @@ RSpec.describe "bundle install with gem sources" do it "resolves correctly" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "activemerchant" gem "rails" G @@ -162,12 +277,12 @@ RSpec.describe "bundle install with gem sources" do it "activates gem correctly according to the resolved gems" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "activesupport", "2.3.5" G install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "activemerchant" gem "rails" G @@ -176,7 +291,7 @@ RSpec.describe "bundle install with gem sources" do end it "does not reinstall any gem that is already available locally" do - system_gems "activesupport-2.3.2", :path => :bundle_path + system_gems "activesupport-2.3.2", path: default_bundle_path build_repo2 do build_gem "activesupport", "2.3.2" do |s| @@ -185,7 +300,7 @@ RSpec.describe "bundle install with gem sources" do end install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "activerecord", "2.3.2" G @@ -193,219 +308,474 @@ RSpec.describe "bundle install with gem sources" do end it "works when the gemfile specifies gems that only exist in the system" do - build_gem "foo", :to_bundle => true + build_gem "foo", to_bundle: true install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" gem "foo" G - expect(the_bundle).to include_gems "rack 1.0.0", "foo 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0", "foo 1.0.0" end it "prioritizes local gems over remote gems" do - build_gem "rack", "1.0.0", :to_bundle => true do |s| - s.add_dependency "activesupport", "2.3.5" - end + build_gem "myrack", "9.0.0", to_bundle: true install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" + G + + expect(the_bundle).to include_gems "myrack 9.0.0" + end + + it "loads env plugins" do + plugin_msg = "hello from an env plugin!" + create_file "plugins/rubygems_plugin.rb", "puts '#{plugin_msg}'" + install_gemfile <<-G, env: { "RUBYLIB" => rubylib.unshift(bundled_app("plugins").to_s).join(File::PATH_SEPARATOR) } + source "https://gem.repo1" + gem "myrack" G - expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.5" + expect(stdboth).to include(plugin_msg) 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_uri_for(gem_repo1)}" - gem "platform_specific" - G + simulate_platform "x86-darwin-100" do + install_gemfile <<-G + source "https://gem.repo1" + gem "platform_specific" + G - run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" - expect(out).to eq("1.0.0 #{Bundler.local_platform}") + expect(the_bundle).to include_gems("platform_specific 1.0 x86-darwin-100") + end end it "falls back on plain ruby" do - simulate_platform "foo-bar-baz" - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "platform_specific" - G + simulate_platform "foo-bar-baz" do + install_gemfile <<-G + source "https://gem.repo1" + gem "platform_specific" + G - run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" - expect(out).to eq("1.0.0 RUBY") + expect(the_bundle).to include_gems("platform_specific 1.0 ruby") + end end it "installs gems for java" do - simulate_platform "java" - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "platform_specific" - G + simulate_platform "java" do + install_gemfile <<-G + source "https://gem.repo1" + gem "platform_specific" + G - run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" - expect(out).to eq("1.0.0 JAVA") + expect(the_bundle).to include_gems("platform_specific 1.0 java") + end end it "installs gems for windows" do - simulate_platform mswin + simulate_platform "x86-mswin32" do + install_gemfile <<-G + source "https://gem.repo1" + gem "platform_specific" + G - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "platform_specific" - G + expect(the_bundle).to include_gems("platform_specific 1.0 x86-mswin32") + end + end + + it "installs gems for aarch64-mingw-ucrt" do + simulate_platform "aarch64-mingw-ucrt" do + install_gemfile <<-G + source "https://gem.repo1" + gem "platform_specific" + G + end - run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" - expect(out).to eq("1.0.0 MSWIN") + expect(out).to include("Installing platform_specific 1.0 (aarch64-mingw-ucrt)") end end - describe "doing bundle install foo" do - before do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - G + it "gives useful errors if no global sources are set, and gems not installed locally, with and without a lockfile" do + install_gemfile <<-G, raise_on_error: false + gem "myrack" + G + + expect(err).to eq("Could not find gem 'myrack' in locally installed gems.") + + lockfile <<~L + GEM + specs: + myrack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install", raise_on_error: false + + expect(err).to include( + "Because your Gemfile specifies no global remote source, your bundle is locked to " \ + "myrack (1.0.0) from locally installed gems. However, myrack (1.0.0) is not installed. " \ + "You'll need to either add a global remote source to your Gemfile or make sure myrack (1.0.0) " \ + "is available locally before rerunning Bundler." + ) + end + + it "creates a Gemfile.lock on a blank Gemfile" do + install_gemfile <<-G + source "https://gem.repo1" + G + + expect(File.exist?(bundled_app_lock)).to eq(true) + end + + it "throws a warning if a gem is added twice in Gemfile without version requirements" do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack" + gem "myrack" + G + + expect(err).to include("Your Gemfile lists the gem myrack (>= 0) more than once.") + expect(err).to include("Remove any duplicate entries and specify the gem only once.") + expect(err).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.") + end + + it "throws a warning if a gem is added twice in Gemfile with same versions" do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack", "1.0" + gem "myrack", "1.0" + G + + expect(err).to include("Your Gemfile lists the gem myrack (= 1.0) more than once.") + expect(err).to include("Remove any duplicate entries and specify the gem only once.") + expect(err).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.") + end + + it "throws a warning if a gem is added twice under different platforms and does not crash when using the generated lockfile" do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack", :platform => :jruby + gem "myrack" + G + + bundle "install" + + expect(err).to include("Your Gemfile lists the gem myrack (>= 0) more than once.") + expect(err).to include("Remove any duplicate entries and specify the gem only once.") + expect(err).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.") + end + + it "does not throw a warning if a gem is added once in Gemfile and also inside a gemspec as a development dependency" do + build_lib "my-gem", path: bundled_app do |s| + s.add_development_dependency "my-private-gem" end - it "works" do - bundle "install", forgotten_command_line_options(:path => "vendor") - expect(the_bundle).to include_gems "rack 1.0" + build_repo2 do + build_gem "my-private-gem" end - it "allows running bundle install --system without deleting foo", :bundler => "< 3" do - bundle "install", forgotten_command_line_options(:path => "vendor") - bundle "install", forgotten_command_line_options(:system => true) - FileUtils.rm_rf(bundled_app("vendor")) - expect(the_bundle).to include_gems "rack 1.0" + gemfile <<~G + source "https://gem.repo2" + + gemspec + + gem "my-private-gem", :group => :development + G + + bundle :install + + expect(err).to be_empty + expect(the_bundle).to include_gems("my-private-gem 1.0") + end + + it "does not warn if a gem is added once in Gemfile and also inside a gemspec as a development dependency, with compatible requirements" do + build_lib "my-gem", path: bundled_app do |s| + s.add_development_dependency "rubocop", "~> 1.36.0" end - it "allows running bundle install --system after deleting foo", :bundler => "< 3" do - bundle "install", forgotten_command_line_options(:path => "vendor") - FileUtils.rm_rf(bundled_app("vendor")) - bundle "install", forgotten_command_line_options(:system => true) - expect(the_bundle).to include_gems "rack 1.0" + build_repo4 do + build_gem "rubocop", "1.36.0" + build_gem "rubocop", "1.37.1" end + + gemfile <<~G + source "https://gem.repo4" + + gemspec + + gem "rubocop", group: :development + G + + bundle :install + + expect(err).to be_empty + + expect(the_bundle).to include_gems("rubocop 1.36.0") end - it "finds gems in multiple sources", :bundler => "< 3" do - build_repo2 - update_repo2 + it "raises an error if a gem is added once in Gemfile and also inside a gemspec as a development dependency, with incompatible requirements" do + build_lib "my-gem", path: bundled_app do |s| + s.add_development_dependency "rubocop", "~> 1.36.0" + end - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - source "#{file_uri_for(gem_repo2)}" + build_repo4 do + build_gem "rubocop", "1.36.0" + build_gem "rubocop", "1.37.1" + end + + gemfile <<~G + source "https://gem.repo4" + + gemspec - gem "activesupport", "1.2.3" - gem "rack", "1.2" + gem "rubocop", "~> 1.37.0", group: :development G - expect(the_bundle).to include_gems "rack 1.2", "activesupport 1.2.3" + bundle :install, raise_on_error: false + + expect(err).to include("The rubocop dependency has conflicting requirements in Gemfile (~> 1.37.0) and gemspec (~> 1.36.0)") end - it "gives a useful error if no sources are set" do - install_gemfile <<-G - gem "rack" + it "includes the gem without warning if two gemspecs add it with the same requirement" do + gem1 = tmp("my-gem-1") + gem2 = tmp("my-gem-2") + + build_lib "my-gem", path: gem1 do |s| + s.add_development_dependency "rubocop", "~> 1.36.0" + end + + build_lib "my-gem-2", path: gem2 do |s| + s.add_development_dependency "rubocop", "~> 1.36.0" + end + + build_repo4 do + build_gem "rubocop", "1.36.0" + end + + gemfile <<~G + source "https://gem.repo4" + + gemspec path: "#{gem1}" + gemspec path: "#{gem2}" G bundle :install - expect(err).to include("Your Gemfile has no gem server sources") + + expect(err).to be_empty + expect(the_bundle).to include_gems("rubocop 1.36.0") end - it "creates a Gemfile.lock on a blank Gemfile" do - install_gemfile <<-G + it "includes the gem without warning if two gemspecs add it with compatible requirements" do + gem1 = tmp("my-gem-1") + gem2 = tmp("my-gem-2") + + build_lib "my-gem", path: gem1 do |s| + s.add_development_dependency "rubocop", "~> 1.0" + end + + build_lib "my-gem-2", path: gem2 do |s| + s.add_development_dependency "rubocop", "~> 1.36.0" + end + + build_repo4 do + build_gem "rubocop", "1.36.0" + end + + gemfile <<~G + source "https://gem.repo4" + + gemspec path: "#{gem1}" + gemspec path: "#{gem2}" G - expect(File.exist?(bundled_app("Gemfile.lock"))).to eq(true) + bundle :install + + expect(err).to be_empty + expect(the_bundle).to include_gems("rubocop 1.36.0") end - context "throws a warning if a gem is added twice in Gemfile" do - it "without version requirements" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack" - gem "rack" - G + it "errors out if two gemspecs add it with incompatible requirements" do + gem1 = tmp("my-gem-1") + gem2 = tmp("my-gem-2") - expect(err).to include("Your Gemfile lists the gem rack (>= 0) more than once.") - expect(err).to include("Remove any duplicate entries and specify the gem only once.") - expect(err).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.") + build_lib "my-gem", path: gem1 do |s| + s.add_development_dependency "rubocop", "~> 2.0" end - it "with same versions" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack", "1.0" - gem "rack", "1.0" - G + build_lib "my-gem-2", path: gem2 do |s| + s.add_development_dependency "rubocop", "~> 1.36.0" + end + + build_repo4 do + build_gem "rubocop", "1.36.0" + end - expect(err).to include("Your Gemfile lists the gem rack (= 1.0) more than once.") - expect(err).to include("Remove any duplicate entries and specify the gem only once.") - expect(err).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.") + gemfile <<~G + source "https://gem.repo4" + + gemspec path: "#{gem1}" + gemspec path: "#{gem2}" + G + + bundle :install, raise_on_error: false + + expect(err).to include("Two gemspec development dependencies have conflicting requirements on the same gem: rubocop (~> 1.36.0) and rubocop (~> 2.0). Bundler cannot continue.") + end + + it "errors out if a gem is specified in a gemspec and in the Gemfile" do + gem = tmp("my-gem-1") + + build_lib "rubocop", path: gem do |s| + s.add_development_dependency "rubocop", "~> 1.0" + end + + build_repo4 do + build_gem "rubocop" end + + gemfile <<~G + source "https://gem.repo4" + + gem "rubocop", :path => "#{gem}" + gemspec path: "#{gem}" + G + + bundle :install, raise_on_error: false + + expect(err).to include("There was an error parsing `Gemfile`: You cannot specify the same gem twice coming from different sources.") + expect(err).to include("You specified that rubocop (>= 0) should come from source at `#{gem}` and gemspec at `#{gem}`") end - context "throws an error if a gem is added twice in Gemfile" do - it "when version of one dependency is not specified" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack" - gem "rack", "1.0" - G + it "does not warn if a gem is added once in Gemfile and also inside a gemspec as a development dependency, with same requirements, and different sources" do + build_lib "my-gem", path: bundled_app do |s| + s.add_development_dependency "activesupport" + end - expect(err).to include("You cannot specify the same gem twice with different version requirements") - expect(err).to include("You specified: rack (>= 0) and rack (= 1.0).") + build_repo4 do + build_gem "activesupport" end - it "when different versions of both dependencies are specified" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack", "1.0" - gem "rack", "1.1" - G + build_git "activesupport", "1.0", path: lib_path("activesupport") + + install_gemfile <<~G + source "https://gem.repo4" + + gemspec + + gem "activesupport", :git => "#{lib_path("activesupport")}" + G - expect(err).to include("You cannot specify the same gem twice with different version requirements") - expect(err).to include("You specified: rack (= 1.0) and rack (= 1.1).") + expect(err).to be_empty + expect(the_bundle).to include_gems "activesupport 1.0", source: "git@#{lib_path("activesupport")}" + + # if the Gemfile dependency is specified first + install_gemfile <<~G + source "https://gem.repo4" + + gem "activesupport", :git => "#{lib_path("activesupport")}" + + gemspec + G + + expect(err).to be_empty + expect(the_bundle).to include_gems "activesupport 1.0", source: "git@#{lib_path("activesupport")}" + end + + it "considers both dependencies for resolution if a gem is added once in Gemfile and also inside a local gemspec as a runtime dependency, with different requirements" do + build_lib "my-gem", path: bundled_app do |s| + s.add_dependency "rubocop", "~> 1.36.0" + end + + build_repo4 do + build_gem "rubocop", "1.36.0" + build_gem "rubocop", "1.37.1" end + + gemfile <<~G + source "https://gem.repo4" + + gemspec + + gem "rubocop" + G + + bundle :install + + expect(err).to be_empty + expect(the_bundle).to include_gems("rubocop 1.36.0") + end + + it "throws an error if a gem is added twice in Gemfile when version of one dependency is not specified" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gem "myrack" + gem "myrack", "1.0" + G + + expect(err).to include("You cannot specify the same gem twice with different version requirements") + expect(err).to include("You specified: myrack (>= 0) and myrack (= 1.0).") + end + + it "throws an error if a gem is added twice in Gemfile when different versions of both dependencies are specified" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gem "myrack", "1.0" + gem "myrack", "1.1" + G + + expect(err).to include("You cannot specify the same gem twice with different version requirements") + expect(err).to include("You specified: myrack (= 1.0) and myrack (= 1.1).") end it "gracefully handles error when rubygems server is unavailable" do - install_gemfile <<-G, :artifice => nil - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G, artifice: nil, raise_on_error: false + source "https://gem.repo1" source "http://0.0.0.0:9384" do gem 'foo' end G - bundle :install, :artifice => nil - expect(err).to include("Could not fetch specs from http://0.0.0.0:9384/") + expect(err).to eq("Could not reach host 0.0.0.0:9384. Check your network connection and try again.") expect(err).not_to include("file://") end it "fails gracefully when downloading an invalid specification from the full index" 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"]] + build_repo2(build_compact_index: false) do + build_gem "ajp-rails", "0.0.0", gemspec: false, skip_validation: true do |s| + invalid_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 + instance_variable_set(:@dependencies, invalid_deps) end + build_gem "ruby-ajp", "1.0.0" end - install_gemfile <<-G, :full_index => true - source "#{file_uri_for(gem_repo2)}" + install_gemfile <<-G, full_index: true, raise_on_error: false + source "https://gem.repo2" gem "ajp-rails", "0.0.0" G - expect(last_command.stdboth).not_to match(/Error Report/i) + expect(stdboth).not_to match(/Error Report/i) expect(err).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' --source '#{file_uri_for(gem_repo2)}/'` succeeds before bundling.") + and include("Bundler::APIResponseInvalidDependenciesError") end it "doesn't blow up when the local .bundle/config is empty" do @@ -413,11 +783,10 @@ RSpec.describe "bundle install with gem sources" do FileUtils.touch(bundled_app(".bundle/config")) install_gemfile(<<-G) - source "#{file_uri_for(gem_repo1)}" + source "https://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 @@ -425,149 +794,713 @@ RSpec.describe "bundle install with gem sources" do FileUtils.touch("#{Bundler.rubygems.user_home}/.bundle/config") install_gemfile(<<-G) - source "#{file_uri_for(gem_repo1)}" + source "https://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 = '2.0.1' - ruby '~> 2.2' + install_gemfile <<-G, raise_on_error: false + ruby '~> 1.2' + source "https://gem.repo1" G - expect(err).to include("Your Ruby version is 2.0.1, but your Gemfile specified ~> 2.2") + expect(err).to include("Your Ruby version is #{Gem.ruby_version}, but your Gemfile specified ~> 1.2") 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' + ruby '~> #{Gem.ruby_version}' + source "https://gem.repo1" G end it "writes current Ruby version to Gemfile.lock" do - lockfile_should_be <<-L + checksums = checksums_section_when_enabled + expect(lockfile).to eq <<~L GEM + remote: https://gem.repo1/ specs: PLATFORMS #{lockfile_platforms} DEPENDENCIES - + #{checksums} RUBY VERSION - ruby 2.1.3p100 + #{Bundler::RubyVersion.system} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L end - it "updates Gemfile.lock with updated incompatible ruby version" do + it "updates Gemfile.lock with updated yet still compatible ruby version" do install_gemfile <<-G - ::RUBY_VERSION = '2.2.3' - ::RUBY_PATCHLEVEL = 100 - ruby '~> 2.2.0' + ruby '~> #{current_ruby_minor}' + source "https://gem.repo1" G - lockfile_should_be <<-L + checksums = checksums_section_when_enabled + + expect(lockfile).to eq <<~L GEM + remote: https://gem.repo1/ specs: PLATFORMS #{lockfile_platforms} DEPENDENCIES - + #{checksums} RUBY VERSION - ruby 2.2.3p100 + #{Bundler::RubyVersion.system} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L end + + it "does not crash when unlocking" do + gemfile <<-G + source "https://gem.repo1" + ruby '>= 2.1.0' + G + + bundle "update" + + expect(err).not_to include("Could not find gem 'Ruby") + end end end describe "when Bundler root contains regex chars" do - before do + it "doesn't blow up when using the `gem` DSL" 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 + source "https://gem.repo1" gem 'foo', :path => "#{lib_path("foo-1.0")}" G - File.open("Gemfile", "w") do |file| + File.open("#{root_dir}/Gemfile", "w") do |file| file.puts gemfile end - bundle :install + bundle :install, dir: root_dir + end + + it "doesn't blow up when using the `gemspec` DSL" do + root_dir = tmp("foo[]bar") - expect(exitstatus).to eq(0) if exitstatus + FileUtils.mkdir_p(root_dir) + + build_lib "foo", path: root_dir + gemfile = <<-G + source "https://gem.repo1" + gemspec + G + File.open("#{root_dir}/Gemfile", "w") do |file| + file.puts gemfile + end + + bundle :install, dir: root_dir end end describe "when requesting a quiet install via --quiet" do - it "should be quiet" do - bundle "config set force_ruby_platform true" + it "should be quiet if there are no warnings" do + bundle_config "force_ruby_platform true" + + gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + G + + bundle :install, quiet: true + expect(out).to be_empty + expect(err).to be_empty + end + + it "should still display warnings and errors" do + bundle_config "force_ruby_platform true" + + create_file("install_with_warning.rb", <<~RUBY) + require "#{lib_dir}/bundler" + require "#{lib_dir}/bundler/cli" + require "#{lib_dir}/bundler/cli/install" + + module RunWithWarning + def run + super + rescue + Bundler.ui.warn "BOOOOO" + raise + end + end + + Bundler::CLI::Install.prepend(RunWithWarning) + RUBY + + gemfile <<-G + source "https://gem.repo1" + gem 'non-existing-gem' + G + + bundle :install, quiet: true, raise_on_error: false, env: { "RUBYOPT" => "-r#{bundled_app("install_with_warning.rb")}" } + expect(out).to be_empty + expect(err).to include("Could not find gem 'non-existing-gem'") + expect(err).to include("BOOOOO") + end + end + + describe "when bundle path does not have cd permission", :permissions do + let(:bundle_path) { bundled_app("vendor") } + + before do + FileUtils.mkdir_p(bundle_path) + gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + G + end + + it "should display a proper message to explain the problem" do + FileUtils.chmod(0o500, bundle_path) + + bundle_config "path vendor" + bundle :install, raise_on_error: false + expect(err).to include(bundle_path.to_s) + expect(err).to include("grant executable permissions") + end + end + + describe "when bundle gems path does not have cd permission", :permissions do + let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") } + + before do + FileUtils.mkdir_p(gems_path) + gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + G + end + + it "should display a proper message to explain the problem" do + FileUtils.chmod("-x", gems_path) + bundle_config "path vendor" + + begin + bundle :install, raise_on_error: false + ensure + FileUtils.chmod("+x", gems_path) + end + + expect(err).not_to include("ERROR REPORT TEMPLATE") + + expect(err).to include( + "There was an error while trying to create `#{gems_path.join("myrack-1.0.0")}`. " \ + "It is likely that you need to grant executable permissions for all parent directories and write permissions for `#{gems_path}`." + ) + end + end + + describe "when there's an empty install folder (like with default gems) without cd permissions", :permissions do + let(:full_gem_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems/myrack-1.0.0") } + + before do + FileUtils.mkdir_p(full_gem_path) + gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + G + end + + it "should display a proper message to explain the problem" do + FileUtils.chmod("-x", full_gem_path) + bundle_config "path vendor" + + begin + bundle :install, raise_on_error: false + ensure + FileUtils.chmod("+x", full_gem_path) + end + + expect(err).not_to include("ERROR REPORT TEMPLATE") + + expect(err).to include( + "There was an error while trying to write to `#{full_gem_path}`. " \ + "It is likely that you need to grant write permissions for that path." + ) + end + end + + describe "when bundle bin dir does not have cd permission", :permissions do + let(:bin_dir) { bundled_app("vendor/#{Bundler.ruby_scope}/bin") } + + before do + FileUtils.mkdir_p(bin_dir) + gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + G + end + + it "should display a proper message to explain the problem" do + FileUtils.chmod("-x", bin_dir) + bundle_config "path vendor" + + begin + bundle :install, raise_on_error: false + ensure + FileUtils.chmod("+x", bin_dir) + end + + expect(err).not_to include("ERROR REPORT TEMPLATE") + + expect(err).to include( + "There was an error while trying to write to `#{bin_dir}`. " \ + "It is likely that you need to grant write permissions for that path." + ) + end + end + + describe "when bundle bin dir does not have write access", :permissions do + let(:bin_dir) { bundled_app("vendor/#{Bundler.ruby_scope}/bin") } + + before do + FileUtils.mkdir_p(bin_dir) + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + end + + it "should display a proper message to explain the problem" do + FileUtils.chmod("-w", bin_dir) + bundle_config "path vendor" + + begin + bundle :install, raise_on_error: false + ensure + FileUtils.chmod("+w", bin_dir) + end + + expect(err).not_to include("ERROR REPORT TEMPLATE") + + expect(err).to include( + "There was an error while trying to write to `#{bin_dir}`. " \ + "It is likely that you need to grant write permissions for that path." + ) + end + end + + describe "when bundle extensions path does not have write access", :permissions do + let(:extensions_path) { bundled_app("vendor/#{Bundler.ruby_scope}/extensions/#{Gem::Platform.local}/#{Gem.extension_api_version}") } + + before do + FileUtils.mkdir_p(extensions_path) + gemfile <<-G + source "https://gem.repo1" + gem 'simple_binary' + G + end + + it "should display a proper message to explain the problem" do + FileUtils.chmod("-x", extensions_path) + bundle_config "path vendor" + + begin + bundle :install, raise_on_error: false + ensure + FileUtils.chmod("+x", extensions_path) + end + + expect(err).not_to include("ERROR REPORT TEMPLATE") + + expect(err).to include( + "There was an error while trying to create `#{extensions_path.join("simple_binary-1.0")}`. " \ + "It is likely that you need to grant executable permissions for all parent directories and write permissions for `#{extensions_path}`." + ) + end + end + + describe "when the path of a specific gem does not have cd permission", :permissions do + let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") } + let(:foo_path) { gems_path.join("foo-1.0.0") } + + before do + build_repo4 do + build_gem "foo", "1.0.0" do |s| + s.write "CHANGELOG.md", "foo" + end + end + + gemfile <<-G + source "https://gem.repo4" + gem 'foo' + G + end + + it "should display a proper message to explain the problem" do + bundle_config "path vendor" + bundle :install + expect(out).to include("Bundle complete!") + expect(err).to be_empty + + FileUtils.chmod("-x", foo_path) + + begin + bundle "install --force", raise_on_error: false + ensure + FileUtils.chmod("+x", foo_path) + end + + expect(err).not_to include("ERROR REPORT TEMPLATE") + expect(err).to include("Could not delete previous installation of `#{foo_path}`.") + expect(err).to include("The underlying error was Errno::EACCES") + end + end + + describe "when gem home does not have the writable bit set, yet it's still writable", :permissions do + let(:gem_home) { bundled_app("vendor/#{Bundler.ruby_scope}") } + + before do + build_repo4 do + build_gem "foo", "1.0.0" do |s| + s.write "CHANGELOG.md", "foo" + end + end gemfile <<-G - gem 'rack' + source "https://gem.repo4" + gem 'foo' G + end - bundle :install, :quiet => true - expect(err).to include("Could not find gem 'rack'") - expect(err).to_not include("Your Gemfile has no gem server sources") + it "should still work" do + bundle_config "path vendor" + bundle :install + expect(out).to include("Bundle complete!") + expect(err).to be_empty + + FileUtils.chmod("-w", gem_home) + + begin + bundle "install --force" + ensure + FileUtils.chmod("+w", gem_home) + end + + expect(out).to include("Bundle complete!") + expect(err).to be_empty end end - describe "when bundle path does not have write access" do + describe "when gems path is world writable (no sticky bit set)", :permissions do + let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") } + before do - FileUtils.mkdir_p(bundled_app("vendor")) + build_repo4 do + build_gem "foo", "1.0.0" do |s| + s.write "CHANGELOG.md", "foo" + end + end + gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo4" + gem 'foo' G end it "should display a proper message to explain the problem" do - FileUtils.chmod(0o500, bundled_app("vendor")) + bundle_config "path vendor" + bundle :install + expect(out).to include("Bundle complete!") + expect(err).to be_empty + + FileUtils.chmod(0o777, gems_path) + + bundle "install --force", raise_on_error: false + + expect(err).to include("Bundler cannot reinstall foo-1.0.0 because there's a previous installation of it at #{gems_path}/foo-1.0.0 that is unsafe to remove") + end + end + + describe "when gems path is world writable (no sticky bit set), but previous install is just an empty dir (like it happens with default gems)", :permissions do + let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") } + let(:full_path) { gems_path.join("foo-1.0.0") } + + before do + build_repo4 do + build_gem "foo", "1.0.0" do |s| + s.write "CHANGELOG.md", "foo" + end + end + + gemfile <<-G + source "https://gem.repo4" + gem 'foo' + G + end + + it "does not try to remove the directory and thus don't abort with an error about unsafe directory removal" do + bundle_config "path vendor" + + FileUtils.mkdir_p(gems_path) + FileUtils.chmod(0o777, gems_path) + Dir.mkdir(full_path) - bundle :install, forgotten_command_line_options(:path => "vendor") - expect(err).to include(bundled_app("vendor").to_s) + bundle "install" + end + end + + describe "when bundle cache path does not have write access", :permissions do + let(:cache_path) { bundled_app("vendor/#{Bundler.ruby_scope}/cache") } + + before do + FileUtils.mkdir_p(cache_path) + gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + G + end + + it "should display a proper message to explain the problem" do + FileUtils.chmod(0o500, cache_path) + + bundle_config "path vendor" + bundle :install, raise_on_error: false + expect(err).to include(cache_path.to_s) expect(err).to include("grant write permissions") end end + describe "when gemspecs are unreadable", :permissions do + let(:gemspec_path) { vendored_gems("specifications/myrack-1.0.0.gemspec") } + + before do + gemfile <<~G + source "https://gem.repo1" + gem 'myrack' + G + bundle_config "path vendor/bundle" + bundle :install + expect(out).to include("Bundle complete!") + expect(err).to be_empty + + FileUtils.chmod("-r", gemspec_path) + end + + it "shows a good error" do + bundle :install, raise_on_error: false + expect(err).to include(gemspec_path.to_s) + expect(err).to include("grant read permissions") + end + end + + describe "when using umask 002 and setgid bit", :permissions do + let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") } + let(:foo_path) { gems_path.join("foo-1.0.0") } + + before do + build_repo4 do + build_gem "foo", "1.0.0" do |s| + s.write "CHANGELOG.md", "foo" + end + end + + gemfile <<-G + source "https://gem.repo4" + gem 'foo' + G + + FileUtils.mkdir_p(gems_path) + FileUtils.chmod("g+s", gems_path) + end + + it "should create the gem directory with proper permissions" do + with_umask(0o002) do + bundle_config "path vendor" + bundle :install + expect(out).to include("Bundle complete!") + expect(err).to be_empty + # Linux's SysV-derived mkdir(2) propagates the set-group-ID bit + # from the parent directory to newly created subdirectories. BSD + # (including macOS) inherits the parent's group via mkdir(2) but + # does not copy the set-group-ID bit itself, so the expected + # mode differs by platform. + expected = RUBY_PLATFORM.include?("darwin") ? 0o0775 : 0o2775 + expect(File.stat(foo_path).mode & 0o7777).to eq(expected) + end + end + end + + describe "parallel make" do + before do + unless Gem::Installer.private_method_defined?(:build_jobs) + skip "This example is runnable when RubyGems::Installer implements `build_jobs`" + end + + @old_makeflags = ENV["MAKEFLAGS"] + @gemspec = nil + + extconf_code = <<~CODE + require "mkmf" + create_makefile("foo") + CODE + + build_repo4 do + build_gem "mypsych", "4.0.6" do |s| + @gemspec = s + extension = "ext/mypsych/extconf.rb" + s.extensions = extension + + s.write(extension, extconf_code) + end + end + end + + after do + if @old_makeflags + ENV["MAKEFLAGS"] = @old_makeflags + else + ENV.delete("MAKEFLAGS") + end + end + + it "doesn't pass down -j to make when MAKEFLAGS is set" do + ENV["MAKEFLAGS"] = "-j1" + + install_gemfile(<<~G, env: { "BUNDLE_JOBS" => "8" }) + source "https://gem.repo4" + gem "mypsych" + G + + gem_make_out = File.read(File.join(@gemspec.extension_dir, "gem_make.out")) + + expect(gem_make_out).not_to include("make -j8") + end + + it "pass down the BUNDLE_JOBS to RubyGems when running the compilation of an extension" do + ENV.delete("MAKEFLAGS") + + install_gemfile(<<~G, env: { "BUNDLE_JOBS" => "8" }) + source "https://gem.repo4" + gem "mypsych" + G + + gem_make_out = File.read(File.join(@gemspec.extension_dir, "gem_make.out")) + + expect(gem_make_out).to include("make -j8") + end + + it "uses nprocessors by default" do + ENV.delete("MAKEFLAGS") + + install_gemfile(<<~G) + source "https://gem.repo4" + gem "mypsych" + G + + gem_make_out = File.read(File.join(@gemspec.extension_dir, "gem_make.out")) + + expect(gem_make_out).to include("make -j#{Etc.nprocessors + 1}") + end + end + + describe "when a native extension requires a transitive dependency at build time" do + before do + build_repo4 do + build_gem "alpha", "1.0.0" do |s| + extension = "ext/alpha/extconf.rb" + s.extensions = extension + s.write(extension, <<~CODE) + require "mkmf" + sleep 1 + create_makefile("alpha") + CODE + s.write "lib/alpha.rb", "ALPHA = '1.0.0'" + end + + build_gem "beta", "1.0.0" do |s| + s.add_dependency "alpha" + s.write "lib/beta.rb", "require 'alpha'\nBETA = '1.0.0'" + end + + build_gem "gamma", "1.0.0" do |s| + s.add_dependency "beta" + extension = "ext/gamma/extconf.rb" + s.extensions = extension + s.write(extension, <<~EXTCONF) + require "beta" + require "mkmf" + create_makefile("gamma") + EXTCONF + end + end + end + + it "installs successfully" do + install_gemfile <<~G + source "https://gem.repo4" + gem "gamma" + G + + expect(the_bundle).to include_gems "alpha 1.0.0", "beta 1.0.0", "gamma 1.0.0" + end + end + + describe "when configured path is UTF-8 and a file inside a gem package too" do + let(:app_path) do + path = tmp("♥") + FileUtils.mkdir_p(path) + path + end + + let(:path) do + root.join("vendor/bundle") + end + + before do + build_repo4 do + build_gem "mygem" do |s| + s.write "spec/fixtures/_posts/2016-04-01-错误.html" + end + end + end + + it "works" do + bundle "config set path #{app_path}/vendor/bundle", dir: app_path + + install_gemfile app_path.join("Gemfile"),<<~G, dir: app_path + source "https://gem.repo4" + gem "mygem", "1.0" + G + end + end + context "after installing with --standalone" do before do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" G - forgotten_command_line_options(:path => "bundle") - bundle! "install", :standalone => true + bundle_config "path bundle" + bundle "install", standalone: true end it "includes the standalone path" do - bundle! "binstubs rack", :standalone => true - 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) + bundle "binstubs myrack", standalone: true + standalone_line = File.read(bundled_app("bin/myrackup")).each_line.find {|line| line.include? "$:.unshift" }.strip + expect(standalone_line).to eq %($:.unshift File.expand_path "../bundle", __dir__) end end @@ -580,10 +1513,603 @@ RSpec.describe "bundle install with gem sources" do end it "should display a helpful message explaining how to fix it" do - bundle :install, :env => { "BUNDLE_RUBYGEMS__ORG" => "user:pass{word" } - expect(exitstatus).to eq(17) if exitstatus + bundle :install, env: { "BUNDLE_RUBYGEMS__ORG" => "user:pass{word" }, raise_on_error: false + expect(exitstatus).to eq(17) expect(err).to eq("Please CGI escape your usernames and passwords before " \ "setting them for authentication.") end end + + context "when current platform not included in the lockfile" do + around do |example| + build_repo4 do + build_gem "libv8", "8.4.255.0" do |s| + s.platform = "x86_64-darwin-19" + end + + build_gem "libv8", "8.4.255.0" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "libv8" + G + + lockfile <<-L + GEM + remote: https://gem.repo4/ + specs: + libv8 (8.4.255.0-x86_64-darwin-19) + + PLATFORMS + x86_64-darwin-19 + + DEPENDENCIES + libv8 + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform("x86_64-linux", &example) + end + + it "adds the current platform to the lockfile" do + bundle "install --verbose" + + expect(out).to include("re-resolving dependencies because your lockfile is missing the current platform") + expect(out).not_to include("you are adding a new platform to your lockfile") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + libv8 (8.4.255.0-x86_64-darwin-19) + libv8 (8.4.255.0-x86_64-linux) + + PLATFORMS + x86_64-darwin-19 + x86_64-linux + + DEPENDENCIES + libv8 + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "fails loudly if frozen mode set" do + bundle_config "deployment true" + bundle "install", raise_on_error: false + + expect(err).to eq( + "Your bundle only supports platforms [\"x86_64-darwin-19\"] but your local platform is x86_64-linux. " \ + "Add the current platform to the lockfile with\n`bundle lock --add-platform x86_64-linux` and try again." + ) + end + end + + context "with missing platform specific gems in lockfile" do + before do + build_repo4 do + build_gem "racca", "1.5.2" + + build_gem "nokogiri", "1.12.4" do |s| + s.platform = "x86_64-darwin" + s.add_dependency "racca", "~> 1.4" + end + + build_gem "nokogiri", "1.12.4" do |s| + s.platform = "x86_64-linux" + s.add_dependency "racca", "~> 1.4" + end + + build_gem "crass", "1.0.6" + + build_gem "loofah", "2.12.0" do |s| + s.add_dependency "crass", "~> 1.0.2" + s.add_dependency "nokogiri", ">= 1.5.9" + end + end + + gemfile <<-G + source "https://gem.repo4" + + ruby "#{Gem.ruby_version}" + + gem "loofah", "~> 2.12.0" + G + + checksums = checksums_section do |c| + c.checksum gem_repo4, "crass", "1.0.6" + c.checksum gem_repo4, "loofah", "2.12.0" + c.checksum gem_repo4, "nokogiri", "1.12.4", "x86_64-darwin" + c.checksum gem_repo4, "racca", "1.5.2" + end + + lockfile <<-L + GEM + remote: https://gem.repo4/ + specs: + crass (1.0.6) + loofah (2.12.0) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + nokogiri (1.12.4-x86_64-darwin) + racca (~> 1.4) + racca (1.5.2) + + PLATFORMS + x86_64-darwin-20 + x86_64-linux + + DEPENDENCIES + loofah (~> 2.12.0) + #{checksums} + RUBY VERSION + #{Bundler::RubyVersion.system} + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "automatically fixes the lockfile" do + bundle_config "path vendor/bundle" + + simulate_platform "x86_64-linux" do + bundle "install", artifice: "compact_index" + end + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "crass", "1.0.6" + c.checksum gem_repo4, "loofah", "2.12.0" + c.checksum gem_repo4, "nokogiri", "1.12.4", "x86_64-darwin" + c.checksum gem_repo4, "racca", "1.5.2" + c.checksum gem_repo4, "nokogiri", "1.12.4", "x86_64-linux" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + crass (1.0.6) + loofah (2.12.0) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + nokogiri (1.12.4-x86_64-darwin) + racca (~> 1.4) + nokogiri (1.12.4-x86_64-linux) + racca (~> 1.4) + racca (1.5.2) + + PLATFORMS + x86_64-darwin-20 + x86_64-linux + + DEPENDENCIES + loofah (~> 2.12.0) + #{checksums} + RUBY VERSION + #{Bundler::RubyVersion.system} + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when lockfile has incorrect dependencies" do + before do + build_repo2 + + gemfile <<-G + source "https://gem.repo2" + gem "myrack_middleware" + G + + system_gems "myrack_middleware-1.0", path: default_bundle_path + + # we want to raise when the 1.0 line should be followed by " myrack (= 0.9.1)" but isn't + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + myrack_middleware (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack_middleware + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "raises a clear error message when frozen" do + bundle_config "frozen true" + bundle "install", raise_on_error: false + + expect(exitstatus).to eq(41) + expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack_middleware-1.0") + expect(err).to include("myrack: gemspec specifies = 0.9.1, not in lockfile") + end + + it "updates the lockfile when not frozen" do + missing_dep = "myrack (0.9.1)" + expect(lockfile).not_to include(missing_dep) + + bundle_config "frozen false" + bundle :install + + expect(lockfile).to include(missing_dep) + expect(out).to include("now installed") + end + end + + context "with --local flag" do + before do + system_gems "myrack-1.0.0", path: default_bundle_path + end + + it "respects installed gems without fetching any remote sources" do + install_gemfile <<-G, local: true + source "https://gem.repo1" + + source "https://not-existing-source" do + gem "myrack" + end + G + + expect(last_command).to be_success + end + end + + context "with only option" do + before do + bundle_config "only a:b" + end + + it "installs only gems of the specified groups" do + install_gemfile <<-G + source "https://gem.repo1" + gem "rails" + gem "myrack", group: :a + gem "rake", group: :b + gem "yard", group: :c + G + + expect(out).to include("Installing myrack") + expect(out).to include("Installing rake") + expect(out).not_to include("Installing yard") + end + end + + context "with --prefer-local flag" do + context "and gems available locally" do + before do + build_repo4 do + build_gem "foo", "1.0.1" + build_gem "foo", "1.0.0" + build_gem "bar", "1.0.0" + + build_gem "a", "1.0.0" do |s| + s.add_dependency "foo", "~> 1.0.0" + end + + build_gem "b", "1.0.0" do |s| + s.add_dependency "foo", "~> 1.0.1" + end + end + + system_gems "foo-1.0.0", path: default_bundle_path, gem_repo: gem_repo4 + end + + it "fetches remote sources when not available locally" do + install_gemfile <<-G, "prefer-local": true, verbose: true + source "https://gem.repo4" + + gem "foo" + gem "bar" + G + + expect(out).to include("Using foo 1.0.0").and include("Fetching bar 1.0.0").and include("Installing bar 1.0.0") + expect(last_command).to be_success + end + + it "fetches remote sources when local version does not match requirements" do + install_gemfile <<-G, "prefer-local": true, verbose: true + source "https://gem.repo4" + + gem "foo", "1.0.1" + gem "bar" + G + + expect(out).to include("Fetching foo 1.0.1").and include("Installing foo 1.0.1").and include("Fetching bar 1.0.0").and include("Installing bar 1.0.0") + expect(last_command).to be_success + end + + it "uses the locally available version for sub-dependencies when possible" do + install_gemfile <<-G, "prefer-local": true, verbose: true + source "https://gem.repo4" + + gem "a" + G + + expect(out).to include("Using foo 1.0.0").and include("Fetching a 1.0.0").and include("Installing a 1.0.0") + expect(last_command).to be_success + end + + it "fetches remote sources for sub-dependencies when the locally available version does not satisfy the requirement" do + install_gemfile <<-G, "prefer-local": true, verbose: true + source "https://gem.repo4" + + gem "b" + G + + expect(out).to include("Fetching foo 1.0.1").and include("Installing foo 1.0.1").and include("Fetching b 1.0.0").and include("Installing b 1.0.0") + expect(last_command).to be_success + end + end + + context "and no gems available locally" do + before do + build_repo4 do + build_gem "myreline", "0.3.8" + build_gem "debug", "0.2.1" + + build_gem "debug", "1.10.0" do |s| + s.add_dependency "myreline" + end + end + end + + it "resolves to the latest version if no gems are available locally" do + install_gemfile <<~G, "prefer-local": true, verbose: true + source "https://gem.repo4" + + gem "debug" + G + + expect(out).to include("Fetching debug 1.10.0").and include("Installing debug 1.10.0").and include("Fetching myreline 0.3.8").and include("Installing myreline 0.3.8") + expect(last_command).to be_success + end + end + end + + context "with a symlinked configured as bundle path and a gem with symlinks" do + before do + symlinked_bundled_app = tmp("bundled_app-symlink") + File.symlink(bundled_app, symlinked_bundled_app) + bundle_config "path #{File.join(symlinked_bundled_app, ".vendor")}" + + binman_path = tmp("binman") + FileUtils.mkdir_p binman_path + + readme_path = File.join(binman_path, "README.markdown") + FileUtils.touch(readme_path) + + man_path = File.join(binman_path, "man", "man0") + FileUtils.mkdir_p man_path + + File.symlink("../../README.markdown", File.join(man_path, "README.markdown")) + + build_repo4 do + build_gem "binman", path: gem_repo4("gems"), lib_path: binman_path, no_default: true do |s| + s.files = ["README.markdown", "man/man0/README.markdown"] + end + end + end + + it "installs fine" do + install_gemfile <<~G + source "https://gem.repo4" + + gem "binman" + G + end + end + + context "when a gem has equivalent versions with inconsistent dependencies" do + before do + build_repo4 do + build_gem "autobuild", "1.10.rc2" do |s| + s.add_dependency "utilrb", ">= 1.6.0" + end + + build_gem "autobuild", "1.10.0.rc2" do |s| + s.add_dependency "utilrb", ">= 2.0" + end + end + end + + it "does not crash unexpectedly" do + gemfile <<~G + source "https://gem.repo4" + + gem "autobuild", "1.10.rc2" + G + + bundle "install --jobs 1", raise_on_error: false + + expect(err).not_to include("ERROR REPORT TEMPLATE") + expect(err).to include("Could not find compatible versions") + end + end + + context "when a lockfile has unmet dependencies, and the Gemfile has no resolution" do + before do + build_repo4 do + build_gem "aaa", "0.2.0" do |s| + s.add_dependency "zzz", "< 0.2.0" + end + + build_gem "zzz", "0.2.0" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "aaa" + gem "zzz" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + aaa (0.2.0) + zzz (< 0.2.0) + zzz (0.2.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + aaa! + zzz! + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "does not install, but raises a resolution error" do + bundle "install", raise_on_error: false + expect(err).to include("Could not find compatible versions") + end + end + + context "when --jobs option given" do + before do + install_gemfile "source 'https://gem.repo1'", jobs: 1 + end + + it "does not save the flag to config" do + expect(bundled_app(".bundle/config")).not_to exist + end + end + + context "when bundler installation is corrupt" do + before do + system_gems "bundler-9.99.8" + + replace_version_file("9.99.9", dir: system_gem_path("gems/bundler-9.99.8")) + end + + it "shows a proper error" do + lockfile <<~L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + + BUNDLED WITH + 9.99.8 + L + + install_gemfile "source \"https://gem.repo1\"", env: { "BUNDLER_VERSION" => "9.99.8" }, raise_on_error: false + + expect(err).not_to include("ERROR REPORT TEMPLATE") + expect(err).to include("The running version of Bundler (9.99.9) does not match the version of the specification installed for it (9.99.8)") + end + end + + it "only installs executable files in bin" do + bundle_config "path vendor/bundle" + + install_gemfile <<~G + source "https://gem.repo1" + gem "myrack" + G + + expected_executables = [vendored_gems("bin/myrackup").to_s] + expected_executables << vendored_gems("bin/myrackup.bat").to_s if Gem.win_platform? + expect(Dir.glob(vendored_gems("bin/*"))).to eq(expected_executables) + end + + it "prevents removing binstubs when BUNDLE_CLEAN is set" do + build_repo4 do + build_gem "kamal", "4.0.6" do |s| + s.executables = ["kamal"] + end + end + + gemfile = <<~G + source "https://gem.repo4" + gem "kamal" + G + + install_gemfile(gemfile, env: { "BUNDLE_CLEAN" => "true", "BUNDLE_PATH" => "vendor/bundle" }) + + expected_executables = [vendored_gems("bin/kamal").to_s] + expected_executables << vendored_gems("bin/kamal.bat").to_s if Gem.win_platform? + expect(Dir.glob(vendored_gems("bin/*"))).to eq(expected_executables) + end + + it "preserves lockfile versions conservatively" do + build_repo4 do + build_gem "mypsych", "4.0.6" do |s| + s.add_dependency "mystringio" + end + + build_gem "mypsych", "5.1.2" do |s| + s.add_dependency "mystringio" + end + + build_gem "mystringio", "3.1.0" + build_gem "mystringio", "3.1.1" + end + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + mypsych (4.0.6) + mystringio + mystringio (3.1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + mypsych (~> 4.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + install_gemfile <<~G + source "https://gem.repo4" + gem "mypsych", "~> 5.0" + G + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + mypsych (5.1.2) + mystringio + mystringio (3.1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + mypsych (~> 5.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + end end diff --git a/spec/bundler/commands/issue_spec.rb b/spec/bundler/commands/issue_spec.rb index 143f6333ce..346cdedc42 100644 --- a/spec/bundler/commands/issue_spec.rb +++ b/spec/bundler/commands/issue_spec.rb @@ -3,7 +3,7 @@ RSpec.describe "bundle issue" do it "exits with a message" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rails" G diff --git a/spec/bundler/commands/licenses_spec.rb b/spec/bundler/commands/licenses_spec.rb index d4fa02d0a7..ebfad5ed4a 100644 --- a/spec/bundler/commands/licenses_spec.rb +++ b/spec/bundler/commands/licenses_spec.rb @@ -2,8 +2,14 @@ RSpec.describe "bundle licenses" do before :each do + build_repo2 do + build_gem "with_license" do |s| + s.license = "MIT" + end + end + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo2" gem "rails" gem "with_license" G @@ -18,13 +24,13 @@ RSpec.describe "bundle licenses" do it "performs an automatic bundle install" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo2" gem "rails" gem "with_license" gem "foo" G - bundle "config set auto_install 1" + bundle_config "auto_install 1" bundle :licenses expect(out).to include("Installing foo 1.0") end diff --git a/spec/bundler/commands/list_spec.rb b/spec/bundler/commands/list_spec.rb index 71d2136d38..c890646a81 100644 --- a/spec/bundler/commands/list_spec.rb +++ b/spec/bundler/commands/list_spec.rb @@ -1,9 +1,31 @@ # frozen_string_literal: true +require "json" + RSpec.describe "bundle list" do + def find_gem_name(json:, name:) + parse_json(json)["gems"].detect {|h| h["name"] == name } + end + + def parse_json(json) + JSON.parse(json) + end + + context "in verbose mode" do + it "logs the actual flags passed to the command" do + install_gemfile <<-G + source "https://gem.repo1" + G + + bundle "list --verbose" + + expect(out).to include("Running `bundle list --verbose`") + end + end + context "with name-only and paths option" do it "raises an error" do - bundle "list --name-only --paths" + bundle "list --name-only --paths", raise_on_error: false expect(err).to eq "The `--name-only` and `--paths` options cannot be used together" end @@ -11,74 +33,153 @@ RSpec.describe "bundle list" do context "with without-group and only-group option" do it "raises an error" do - bundle "list --without-group dev --only-group test" + bundle "list --without-group dev --only-group test", raise_on_error: false expect(err).to eq "The `--only-group` and `--without-group` options cannot be used together" end end + context "with invalid format option" do + before do + install_gemfile <<-G + source "https://gem.repo1" + G + end + + it "raises an error" do + bundle "list --format=nope", raise_on_error: false + + expect(err).to eq "Unknown option`--format=nope`. Supported formats: `json`" + end + end + describe "with without-group option" do before do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack" + gem "myrack" gem "rspec", :group => [:test] + gem "rails", :group => [:production] G end context "when group is present" do it "prints the gems not in the specified group" do - bundle! "list --without-group test" + bundle "list --without-group test" - expect(out).to include(" * rack (1.0.0)") + expect(out).to include(" * myrack (1.0.0)") + expect(out).to include(" * rails (2.3.2)") expect(out).not_to include(" * rspec (1.2.7)") end + + it "prints the gems not in the specified group with json" do + bundle "list --without-group test --format=json" + + gem = find_gem_name(json: out, name: "myrack") + expect(gem["version"]).to eq("1.0.0") + gem = find_gem_name(json: out, name: "rails") + expect(gem["version"]).to eq("2.3.2") + gem = find_gem_name(json: out, name: "rspec") + expect(gem).to be_nil + end end context "when group is not found" do it "raises an error" do - bundle "list --without-group random" + bundle "list --without-group random", raise_on_error: false expect(err).to eq "`random` group could not be found." end end + + context "when multiple groups" do + it "prints the gems not in the specified groups" do + bundle "list --without-group test production" + + expect(out).to include(" * myrack (1.0.0)") + expect(out).not_to include(" * rails (2.3.2)") + expect(out).not_to include(" * rspec (1.2.7)") + end + + it "prints the gems not in the specified groups with json" do + bundle "list --without-group test production --format=json" + + gem = find_gem_name(json: out, name: "myrack") + expect(gem["version"]).to eq("1.0.0") + gem = find_gem_name(json: out, name: "rails") + expect(gem).to be_nil + gem = find_gem_name(json: out, name: "rspec") + expect(gem).to be_nil + end + end end describe "with only-group option" do before do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack" + gem "myrack" gem "rspec", :group => [:test] + gem "rails", :group => [:production] G end context "when group is present" do it "prints the gems in the specified group" do - bundle! "list --only-group default" + bundle "list --only-group default" - expect(out).to include(" * rack (1.0.0)") + expect(out).to include(" * myrack (1.0.0)") expect(out).not_to include(" * rspec (1.2.7)") end + + it "prints the gems in the specified group with json" do + bundle "list --only-group default --format=json" + + gem = find_gem_name(json: out, name: "myrack") + expect(gem["version"]).to eq("1.0.0") + gem = find_gem_name(json: out, name: "rspec") + expect(gem).to be_nil + end end context "when group is not found" do it "raises an error" do - bundle "list --only-group random" + bundle "list --only-group random", raise_on_error: false expect(err).to eq "`random` group could not be found." end end + + context "when multiple groups" do + it "prints the gems in the specified groups" do + bundle "list --only-group default production" + + expect(out).to include(" * myrack (1.0.0)") + expect(out).to include(" * rails (2.3.2)") + expect(out).not_to include(" * rspec (1.2.7)") + end + + it "prints the gems in the specified groups with json" do + bundle "list --only-group default production --format=json" + + gem = find_gem_name(json: out, name: "myrack") + expect(gem["version"]).to eq("1.0.0") + gem = find_gem_name(json: out, name: "rails") + expect(gem["version"]).to eq("2.3.2") + gem = find_gem_name(json: out, name: "rspec") + expect(gem).to be_nil + end + end end context "with name-only option" do before do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack" + gem "myrack" gem "rspec", :group => [:test] G end @@ -86,45 +187,79 @@ RSpec.describe "bundle list" do it "prints only the name of the gems in the bundle" do bundle "list --name-only" - expect(out).to include("rack") + expect(out).to include("myrack") expect(out).to include("rspec") end + + it "prints only the name of the gems in the bundle with json" do + bundle "list --name-only --format=json" + + gem = find_gem_name(json: out, name: "myrack") + expect(gem.keys).to eq(["name"]) + gem = find_gem_name(json: out, name: "rspec") + expect(gem.keys).to eq(["name"]) + end end context "with paths option" do before do build_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + build_gem "bar" end - build_git "git_test", "1.0.0", :path => lib_path("git_test") + build_git "git_test", "1.0.0", path: lib_path("git_test") - build_lib("gemspec_test", :path => tmp.join("gemspec_test")) do |s| + build_lib("gemspec_test", path: tmp("gemspec_test")) do |s| s.add_dependency "bar", "=1.0.0" end install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack" + source "https://gem.repo2" + gem "myrack" gem "rails" gem "git_test", :git => "#{lib_path("git_test")}" - gemspec :path => "#{tmp.join("gemspec_test")}" + gemspec :path => "#{tmp("gemspec_test")}" G end it "prints the path of each gem in the bundle" do bundle "list --paths" expect(out).to match(%r{.*\/rails\-2\.3\.2}) - expect(out).to match(%r{.*\/rack\-1\.2}) + expect(out).to match(%r{.*\/myrack\-1\.2}) expect(out).to match(%r{.*\/git_test\-\w}) expect(out).to match(%r{.*\/gemspec_test}) end + + it "prints the path of each gem in the bundle with json" do + bundle "list --paths --format=json" + + gem = find_gem_name(json: out, name: "rails") + expect(gem["path"]).to match(%r{.*\/rails\-2\.3\.2}) + expect(gem["git_version"]).to be_nil + + gem = find_gem_name(json: out, name: "myrack") + expect(gem["path"]).to match(%r{.*\/myrack\-1\.2}) + expect(gem["git_version"]).to be_nil + + gem = find_gem_name(json: out, name: "git_test") + expect(gem["path"]).to match(%r{.*\/git_test\-\w}) + expect(gem["git_version"]).to be_truthy + expect(gem["git_version"].strip).to eq(gem["git_version"]) + + gem = find_gem_name(json: out, name: "gemspec_test") + expect(gem["path"]).to match(%r{.*\/gemspec_test}) + expect(gem["git_version"]).to be_nil + end end context "when no gems are in the gemfile" do before do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" G end @@ -132,30 +267,42 @@ RSpec.describe "bundle list" do bundle "list" expect(out).to include("No gems in the Gemfile") end + + it "prints empty json" do + bundle "list --format=json" + expect(parse_json(out)["gems"]).to eq([]) + end end context "without options" do before do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack" + gem "myrack" gem "rspec", :group => [:test] G end it "lists gems installed in the bundle" do bundle "list" - expect(out).to include(" * rack (1.0.0)") + expect(out).to include(" * myrack (1.0.0)") + end + + it "lists gems installed in the bundle with json" do + bundle "list --format=json" + + gem = find_gem_name(json: out, name: "myrack") + expect(gem["version"]).to eq("1.0.0") end end context "when using the ls alias" do before do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack" + gem "myrack" gem "rspec", :group => [:test] G end diff --git a/spec/bundler/commands/lock_spec.rb b/spec/bundler/commands/lock_spec.rb index 1d9813a835..8ab3cc7e8d 100644 --- a/spec/bundler/commands/lock_spec.rb +++ b/spec/bundler/commands/lock_spec.rb @@ -1,27 +1,22 @@ # frozen_string_literal: true 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_uri_for(repo)}" - gem "rails" - gem "with_license" - gem "foo" - G + let(:expected_lockfile) do + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "actionmailer", "2.3.2" + c.checksum gem_repo4, "actionpack", "2.3.2" + c.checksum gem_repo4, "activerecord", "2.3.2" + c.checksum gem_repo4, "activeresource", "2.3.2" + c.checksum gem_repo4, "activesupport", "2.3.2" + c.checksum gem_repo4, "foo", "1.0" + c.checksum gem_repo4, "rails", "2.3.2" + c.checksum gem_repo4, "rake", rake_version + c.checksum gem_repo4, "weakling", "0.0.3" + end - @lockfile = strip_lockfile(<<-L) + <<~L GEM - remote: #{file_uri_for(repo)}/ + remote: https://gem.repo4/ specs: actionmailer (2.3.2) activesupport (= 2.3.2) @@ -38,9 +33,9 @@ RSpec.describe "bundle lock" do actionpack (= 2.3.2) activerecord (= 2.3.2) activeresource (= 2.3.2) - rake (= 12.3.2) - rake (12.3.2) - with_license (1.0) + rake (= #{rake_version}) + rake (#{rake_version}) + weakling (0.0.3) PLATFORMS #{lockfile_platforms} @@ -48,55 +43,247 @@ RSpec.describe "bundle lock" do DEPENDENCIES foo rails - with_license + weakling + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + let(:outdated_lockfile) do + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "actionmailer", "2.3.1" + c.checksum gem_repo4, "actionpack", "2.3.1" + c.checksum gem_repo4, "activerecord", "2.3.1" + c.checksum gem_repo4, "activeresource", "2.3.1" + c.checksum gem_repo4, "activesupport", "2.3.1" + c.checksum gem_repo4, "foo", "1.0" + c.checksum gem_repo4, "rails", "2.3.1" + c.checksum gem_repo4, "rake", rake_version + c.checksum gem_repo4, "weakling", "0.0.3" + end + + <<~L + GEM + remote: https://gem.repo4/ + specs: + actionmailer (2.3.1) + activesupport (= 2.3.1) + actionpack (2.3.1) + activesupport (= 2.3.1) + activerecord (2.3.1) + activesupport (= 2.3.1) + activeresource (2.3.1) + activesupport (= 2.3.1) + activesupport (2.3.1) + foo (1.0) + rails (2.3.1) + actionmailer (= 2.3.1) + actionpack (= 2.3.1) + activerecord (= 2.3.1) + activeresource (= 2.3.1) + rake (= #{rake_version}) + rake (#{rake_version}) + weakling (0.0.3) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo + rails + weakling + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L end + let(:gemfile_with_rails_weakling_and_foo_from_repo4) do + build_repo4 do + build_gem "rake", "10.0.1" + build_gem "rake", rake_version + + %w[2.3.1 2.3.2].each do |version| + build_gem "rails", version do |s| + s.executables = "rails" + s.add_dependency "rake", version == "2.3.1" ? "10.0.1" : rake_version + s.add_dependency "actionpack", version + s.add_dependency "activerecord", version + s.add_dependency "actionmailer", version + s.add_dependency "activeresource", version + end + build_gem "actionpack", version do |s| + s.add_dependency "activesupport", version + end + build_gem "activerecord", version do |s| + s.add_dependency "activesupport", version + end + build_gem "actionmailer", version do |s| + s.add_dependency "activesupport", version + end + build_gem "activeresource", version do |s| + s.add_dependency "activesupport", version + end + build_gem "activesupport", version + end + + build_gem "weakling", "0.0.3" + + build_gem "foo" + end + + gemfile <<-G + source "https://gem.repo4" + gem "rails" + gem "weakling" + gem "foo" + G + end + it "prints a lockfile when there is no existing lockfile with --print" do + gemfile_with_rails_weakling_and_foo_from_repo4 + bundle "lock --print" - expect(out).to eq(@lockfile) + expect(out).to eq(expected_lockfile.chomp) end it "prints a lockfile when there is an existing lockfile with --print" do - lockfile @lockfile + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile expected_lockfile bundle "lock --print" - expect(out).to eq(@lockfile) + expect(out).to eq(expected_lockfile.chomp) + end + + it "prints a lockfile when there is an existing checksums lockfile with --print" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile expected_lockfile + + bundle "lock --print" + + expect(out).to eq(expected_lockfile.chomp) end it "writes a lockfile when there is no existing lockfile" do + gemfile_with_rails_weakling_and_foo_from_repo4 + bundle "lock" - expect(read_lockfile).to eq(@lockfile) + expect(read_lockfile).to eq(expected_lockfile) + end + + it "prints a lockfile without fetching new checksums if the existing lockfile had no checksums" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile expected_lockfile + + bundle "lock --print" + + expect(out).to eq(expected_lockfile.chomp) + end + + it "touches the lockfile when there is an existing lockfile that does not need changes" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile expected_lockfile + + expect do + bundle "lock" + end.to change { bundled_app_lock.mtime } + end + + it "does not touch lockfile with --print" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile expected_lockfile + + expect do + bundle "lock --print" + end.not_to change { bundled_app_lock.mtime } end it "writes a lockfile when there is an outdated lockfile using --update" do - lockfile @lockfile.gsub("2.3.2", "2.3.1") + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile outdated_lockfile + + bundle "lock --update" + + expect(read_lockfile).to eq(expected_lockfile) + end - bundle! "lock --update" + it "prints an updated lockfile when there is an outdated lockfile using --print --update" do + gemfile_with_rails_weakling_and_foo_from_repo4 - expect(read_lockfile).to eq(@lockfile) + lockfile outdated_lockfile + + bundle "lock --print --update" + + expect(out).to eq(expected_lockfile.rstrip) + end + + it "emits info messages to stderr when updating an outdated lockfile using --print --update" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile outdated_lockfile + + bundle "lock --print --update" + + expect(err).to eq(<<~STDERR.rstrip) + Fetching gem metadata from https://gem.repo4/... + Resolving dependencies... + STDERR + end + + it "writes a lockfile when there is an outdated lockfile and bundle is frozen" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile outdated_lockfile + + bundle "lock --update", env: { "BUNDLE_FROZEN" => "true" } + + expect(read_lockfile).to eq(expected_lockfile) end it "does not fetch remote specs when using the --local option" do - bundle "lock --update --local" + gemfile_with_rails_weakling_and_foo_from_repo4 + + bundle "lock --update --local", raise_on_error: false + + expect(err).to match(/locally installed gems/) + end + + it "does not fetch remote checksums with --local" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile expected_lockfile + + bundle "lock --print --local" - expect(err).to match(/sources listed in your Gemfile|installed locally/) + expect(out).to eq(expected_lockfile.chomp) end it "works with --gemfile flag" do - create_file "CustomGemfile", <<-G - source "#{file_uri_for(repo)}" + gemfile_with_rails_weakling_and_foo_from_repo4 + + gemfile "CustomGemfile", <<-G + source "https://gem.repo4" gem "foo" G - lockfile = strip_lockfile(<<-L) + bundle "lock --gemfile CustomGemfile" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "foo", "1.0" + end + + lockfile = <<~L GEM - remote: #{file_uri_for(repo)}/ + remote: https://gem.repo4/ specs: foo (1.0) @@ -105,61 +292,395 @@ RSpec.describe "bundle lock" do DEPENDENCIES foo - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L - bundle "lock --gemfile CustomGemfile" - expect(out).to match(/Writing lockfile to.+CustomGemfile\.lock/) expect(read_lockfile("CustomGemfile.lock")).to eq(lockfile) expect { read_lockfile }.to raise_error(Errno::ENOENT) end it "writes to a custom location using --lockfile" do + gemfile_with_rails_weakling_and_foo_from_repo4 + bundle "lock --lockfile=lock" expect(out).to match(/Writing lockfile to.+lock/) - expect(read_lockfile("lock")).to eq(@lockfile) + expect(read_lockfile("lock")).to eq(expected_lockfile) expect { read_lockfile }.to raise_error(Errno::ENOENT) end + it "updates a specific gem and write to a custom location" do + build_repo4 do + build_gem "foo", %w[1.0.2 1.0.3] + build_gem "warning", %w[1.4.0 1.5.0] + end + + gemfile <<~G + source "https://gem.repo4" + + gem "foo" + gem "warning" + G + + lockfile <<~L + GEM + remote: https://gem.repo4 + specs: + foo (1.0.2) + warning (1.4.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + uri + warning + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --update foo --lockfile=lock" + + lockfile_content = read_lockfile("lock") + expect(lockfile_content).to include("foo (1.0.3)") + expect(lockfile_content).to include("warning (1.4.0)") + end + it "writes to custom location using --lockfile when a default lockfile is present" do + gemfile_with_rails_weakling_and_foo_from_repo4 + bundle "install" bundle "lock --lockfile=lock" + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "actionmailer", "2.3.2" + c.checksum gem_repo4, "actionpack", "2.3.2" + c.checksum gem_repo4, "activerecord", "2.3.2" + c.checksum gem_repo4, "activeresource", "2.3.2" + c.checksum gem_repo4, "activesupport", "2.3.2" + c.checksum gem_repo4, "foo", "1.0" + c.checksum gem_repo4, "rails", "2.3.2" + c.checksum gem_repo4, "rake", rake_version + c.checksum gem_repo4, "weakling", "0.0.3" + end + + lockfile = <<~L + GEM + remote: https://gem.repo4/ + 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 (= #{rake_version}) + rake (#{rake_version}) + weakling (0.0.3) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo + rails + weakling + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + expect(out).to match(/Writing lockfile to.+lock/) - expect(read_lockfile("lock")).to eq(@lockfile) + expect(read_lockfile("lock")).to eq(lockfile) end it "update specific gems using --update" do - lockfile @lockfile.gsub("2.3.2", "2.3.1").gsub("12.3.2", "10.0.1") + gemfile_with_rails_weakling_and_foo_from_repo4 + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "actionmailer", "2.3.1" + c.checksum gem_repo4, "actionpack", "2.3.1" + c.checksum gem_repo4, "activerecord", "2.3.1" + c.checksum gem_repo4, "activeresource", "2.3.1" + c.checksum gem_repo4, "activesupport", "2.3.1" + c.checksum gem_repo4, "foo", "1.0" + c.checksum gem_repo4, "rails", "2.3.1" + c.checksum gem_repo4, "rake", "10.0.1" + c.checksum gem_repo4, "weakling", "0.0.3" + end + + lockfile_with_outdated_rails_and_rake = <<~L + GEM + remote: https://gem.repo4/ + specs: + actionmailer (2.3.1) + activesupport (= 2.3.1) + actionpack (2.3.1) + activesupport (= 2.3.1) + activerecord (2.3.1) + activesupport (= 2.3.1) + activeresource (2.3.1) + activesupport (= 2.3.1) + activesupport (2.3.1) + foo (1.0) + rails (2.3.1) + actionmailer (= 2.3.1) + actionpack (= 2.3.1) + activerecord (= 2.3.1) + activeresource (= 2.3.1) + rake (= 10.0.1) + rake (10.0.1) + weakling (0.0.3) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo + rails + weakling + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + lockfile lockfile_with_outdated_rails_and_rake bundle "lock --update rails rake" - expect(read_lockfile).to eq(@lockfile) + expect(read_lockfile).to eq(expected_lockfile) + end + + it "updates specific gems using --update, even if that requires unlocking other top level gems" do + build_repo4 do + build_gem "prism", "0.15.1" + build_gem "prism", "0.24.0" + + build_gem "ruby-lsp", "0.12.0" do |s| + s.add_dependency "prism", "< 0.24.0" + end + + build_gem "ruby-lsp", "0.16.1" do |s| + s.add_dependency "prism", ">= 0.24.0" + end + + build_gem "tapioca", "0.11.10" do |s| + s.add_dependency "prism", "< 0.24.0" + end + + build_gem "tapioca", "0.13.1" do |s| + s.add_dependency "prism", ">= 0.24.0" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "tapioca" + gem "ruby-lsp" + G + + lockfile <<~L + GEM + remote: https://gem.repo4 + specs: + prism (0.15.1) + ruby-lsp (0.12.0) + prism (< 0.24.0) + tapioca (0.11.10) + prism (< 0.24.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ruby-lsp + tapioca + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --update tapioca --verbose" + + expect(lockfile).to include("tapioca (0.13.1)") + end + + it "updates specific gems using --update, even if that requires unlocking other top level gems, but only as few as possible" do + build_repo4 do + build_gem "prism", "0.15.1" + build_gem "prism", "0.24.0" + + build_gem "ruby-lsp", "0.12.0" do |s| + s.add_dependency "prism", "< 0.24.0" + end + + build_gem "ruby-lsp", "0.16.1" do |s| + s.add_dependency "prism", ">= 0.24.0" + end + + build_gem "tapioca", "0.11.10" do |s| + s.add_dependency "prism", "< 0.24.0" + end + + build_gem "tapioca", "0.13.1" do |s| + s.add_dependency "prism", ">= 0.24.0" + end + + build_gem "other-prism-dependent", "1.0.0" do |s| + s.add_dependency "prism", ">= 0.15.1" + end + + build_gem "other-prism-dependent", "1.1.0" do |s| + s.add_dependency "prism", ">= 0.15.1" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "tapioca" + gem "ruby-lsp" + gem "other-prism-dependent" + G + + lockfile <<~L + GEM + remote: https://gem.repo4 + specs: + other-prism-dependent (1.0.0) + prism (>= 0.15.1) + prism (0.15.1) + ruby-lsp (0.12.0) + prism (< 0.24.0) + tapioca (0.11.10) + prism (< 0.24.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ruby-lsp + tapioca + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --update tapioca" + + expect(lockfile).to include("tapioca (0.13.1)") + expect(lockfile).to include("other-prism-dependent (1.0.0)") + end + + it "preserves unknown checksum algorithms" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile expected_lockfile.gsub(/(sha256=[a-f0-9]+)$/, "constant=true,\\1,xyz=123") + + previous_lockfile = read_lockfile + + bundle "lock" + + expect(read_lockfile).to eq(previous_lockfile) + end + + it "does not unlock git sources when only uri shape changes" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + build_git("foo") + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + # Change uri format to end with "/" and reinstall + install_gemfile <<-G, verbose: true + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}/" + G + + expect(out).to include("using resolution from the lockfile") + expect(out).not_to include("re-resolving dependencies because the list of sources changed") + end + + it "updates specific gems using --update using the locked revision of unrelated git gems for resolving" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + ref = build_git("foo").ref_for("HEAD") + + gemfile <<-G + source "https://gem.repo1" + gem "rake" + gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "deadbeef" + G + + lockfile <<~L + GIT + remote: #{lib_path("foo-1.0")} + revision: #{ref} + branch: deadbeef + specs: + foo (1.0) + + GEM + remote: https://gem.repo1/ + specs: + rake (10.0.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + rake + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --update rake --verbose" + expect(out).to match(/Writing lockfile to.+lock/) + expect(lockfile).to include("rake (#{rake_version})") end it "errors when updating a missing specific gems using --update" do - lockfile @lockfile + gemfile_with_rails_weakling_and_foo_from_repo4 - bundle "lock --update blahblah" + lockfile expected_lockfile + + bundle "lock --update blahblah", raise_on_error: false expect(err).to eq("Could not find gem 'blahblah'.") - expect(read_lockfile).to eq(@lockfile) + expect(read_lockfile).to eq(expected_lockfile) end it "can lock without downloading gems" do + gemfile_with_rails_weakling_and_foo_from_repo4 + gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "thin" - gem "rack_middleware", :group => "test" + gem "myrack_middleware", :group => "test" G - bundle! "config set without test" - bundle! "config set path .bundle" - bundle! "lock" - expect(bundled_app(".bundle")).not_to exist + bundle_config "without test" + bundle_config "path vendor/bundle" + bundle "lock", verbose: true + expect(bundled_app("vendor/bundle")).not_to exist end # see update_spec for more coverage on same options. logic is shared so it's not necessary @@ -176,13 +697,16 @@ RSpec.describe "bundle lock" do 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 "foo", %w[2.0.0.pre] do |s| + s.add_dependency "bar" + end + build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 2.1.2.pre 3.0.0 3.1.0.pre 4.0.0.pre] 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_uri_for(gem_repo4)}" + source "https://gem.repo4" gem 'foo', '1.4.3' gem 'bar', '2.0.3' gem 'qux', '1.0.0' @@ -191,66 +715,340 @@ RSpec.describe "bundle lock" do # remove 1.4.3 requirement and bar altogether # to setup update specs below gemfile <<-G - source "#{file_uri_for(gem_repo4)}" + source "https://gem.repo4" gem 'foo' gem 'qux' G + + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) 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) + expect(the_bundle.locked_specs).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) + expect(the_bundle.locked_specs).to eq(%w[foo-1.5.0 bar-2.1.1 qux-1.1.0].sort) + end + + it "shows proper error when Gemfile changes forbid patch upgrades, and --patch --strict is given" do + # force next minor via Gemfile + gemfile <<-G + source "https://gem.repo4" + gem 'foo', '1.5.0' + gem 'qux' + G + + bundle "lock --update foo --patch --strict", raise_on_error: false + + expect(err).to include( + "foo is locked to 1.4.3, while Gemfile is requesting foo (= 1.5.0). " \ + "--strict --patch was specified, but there are no patch level upgrades from 1.4.3 satisfying foo (= 1.5.0), so version solving has failed" + ) + end + + context "pre" do + it "defaults to major" do + bundle "lock --update --pre" + + expect(the_bundle.locked_specs).to eq(%w[foo-2.0.0.pre bar-4.0.0.pre qux-2.0.0].sort) + end + + it "patch preferred" do + bundle "lock --update --patch --pre" + + expect(the_bundle.locked_specs).to eq(%w[foo-1.4.5 bar-2.1.2.pre qux-1.0.1].sort) + end + + it "minor preferred" do + bundle "lock --update --minor --pre" + + expect(the_bundle.locked_specs).to eq(%w[foo-1.5.1 bar-3.1.0.pre qux-1.1.0].sort) + end + + it "major preferred" do + bundle "lock --update --major --pre" + + expect(the_bundle.locked_specs).to eq(%w[foo-2.0.0.pre bar-4.0.0.pre qux-2.0.0].sort) + end end end - it "supports adding new platforms" do - bundle! "lock --add-platform java x86-mingw32" + context "conservative updates when minor update adds a new dependency" do + before do + build_repo4 do + build_gem "sequel", "5.71.0" + build_gem "sequel", "5.72.0" do |s| + s.add_dependency "bigdecimal", ">= 0" + end + build_gem "bigdecimal", %w[1.4.4 99.1.4] + end + + gemfile <<~G + source "https://gem.repo4" + gem 'sequel' + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + sequel (5.71.0) + + PLATFORMS + ruby + + DEPENDENCIES + sequel + + BUNDLED WITH + #{Bundler::VERSION} + L + + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + end + + it "adds the latest version of the new dependency" do + bundle "lock --minor --update sequel" + + expect(the_bundle.locked_specs).to eq(%w[sequel-5.72.0 bigdecimal-99.1.4].sort) + end + end + + it "updates the bundler version in the lockfile to the latest bundler version" do + build_repo4 do + build_gem "bundler", "55" + end + + system_gems "bundler-55", gem_repo: gem_repo4 + + install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + source "https://gem.repo4" + G + lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, '\11.0.0\2') + + bundle "lock --update --bundler --verbose", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + expect(lockfile).to end_with("BUNDLED WITH\n 55\n") + + build_repo4 do + build_gem "bundler", "99" + end + + bundle "lock --update --bundler --verbose", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + expect(lockfile).to end_with("BUNDLED WITH\n 99\n") + end + + it "supports adding new platforms when there's no previous lockfile" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + bundle "lock --add-platform java x86-mingw32 --verbose" + expect(out).to include("Resolving dependencies because there's no lockfile") + + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + expect(the_bundle.locked_platforms).to match_array(default_platform_list("java", "x86-mingw32")) + end + + it "supports adding new platforms when a previous lockfile exists" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + bundle "lock" + bundle "lock --add-platform java x86-mingw32 --verbose" + expect(out).to include("Found changes from the lockfile, re-resolving dependencies because you are adding a new platform to your lockfile") + + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + expect(the_bundle.locked_platforms).to match_array(default_platform_list("java", "x86-mingw32")) + end + + it "supports adding new platforms, when most specific locked platform is not the current platform, and current resolve is not compatible with the target platform" do + simulate_platform "arm64-darwin-23" do + build_repo4 do + build_gem "foo" do |s| + s.platform = "arm64-darwin" + end + + build_gem "foo" do |s| + s.platform = "java" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "foo" + G + + lockfile <<-L + GEM + remote: https://gem.repo4/ + specs: + foo (1.0-arm64-darwin) + + PLATFORMS + arm64-darwin + + DEPENDENCIES + foo + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --add-platform java" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + foo (1.0-arm64-darwin) + foo (1.0-java) + + PLATFORMS + arm64-darwin + java + + DEPENDENCIES + foo + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "supports adding new platforms with force_ruby_platform = true" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile <<-L + GEM + remote: https://gem.repo1/ + specs: + platform_specific (1.0) + platform_specific (1.0-x86-64_linux) + + PLATFORMS + ruby + x86_64-linux - lockfile = Bundler::LockfileParser.new(read_lockfile) - expect(lockfile.platforms).to match_array(local_platforms.unshift(java, mingw).uniq) + DEPENDENCIES + platform_specific + L + + bundle_config "force_ruby_platform true" + bundle "lock --add-platform java x86-mingw32" + + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + expect(the_bundle.locked_platforms).to contain_exactly(Gem::Platform::RUBY, "x86_64-linux", "java", "x86-mingw32") end it "supports adding the `ruby` platform" do - bundle! "lock --add-platform ruby" - lockfile = Bundler::LockfileParser.new(read_lockfile) - expect(lockfile.platforms).to match_array(local_platforms.unshift("ruby").uniq) + gemfile_with_rails_weakling_and_foo_from_repo4 + + bundle "lock --add-platform ruby" + + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + expect(the_bundle.locked_platforms).to match_array(default_platform_list("ruby")) end - it "warns when adding an unknown platform" do - bundle "lock --add-platform foobarbaz" - expect(err).to include("The platform `foobarbaz` is unknown to RubyGems and adding it will likely lead to resolution errors") + it "fails when adding an unknown platform" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + bundle "lock --add-platform foobarbaz", raise_on_error: false + expect(err).to include("The platform `foobarbaz` is unknown to RubyGems and can't be added to the lockfile") + expect(last_command).to be_failure end it "allows removing platforms" do - bundle! "lock --add-platform java x86-mingw32" + gemfile_with_rails_weakling_and_foo_from_repo4 + + bundle "lock --add-platform java x86-mingw32" - lockfile = Bundler::LockfileParser.new(read_lockfile) - expect(lockfile.platforms).to match_array(local_platforms.unshift(java, mingw).uniq) + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + expect(the_bundle.locked_platforms).to match_array(default_platform_list("java", "x86-mingw32")) - bundle! "lock --remove-platform java" + bundle "lock --remove-platform java" - lockfile = Bundler::LockfileParser.new(read_lockfile) - expect(lockfile.platforms).to match_array(local_platforms.unshift(mingw).uniq) + expect(the_bundle.locked_platforms).to match_array(default_platform_list("x86-mingw32")) + end + + it "also cleans up redundant platform gems when removing platforms" do + build_repo4 do + build_gem "nokogiri", "1.12.0" + build_gem "nokogiri", "1.12.0" do |s| + s.platform = "x86_64-darwin" + end + end + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.12.0" + c.checksum gem_repo4, "nokogiri", "1.12.0", "x86_64-darwin" + end + + simulate_platform "x86_64-darwin-22" do + install_gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + G + end + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.12.0) + nokogiri (1.12.0-x86_64-darwin) + + PLATFORMS + ruby + x86_64-darwin + + DEPENDENCIES + nokogiri + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + checksums.delete("nokogiri", Gem::Platform::RUBY) + + simulate_platform "x86_64-darwin-22" do + bundle "lock --remove-platform ruby" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.12.0-x86_64-darwin) + + PLATFORMS + x86_64-darwin + + DEPENDENCIES + nokogiri + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L end it "errors when removing all platforms" do - bundle "lock --remove-platform #{local_platforms.join(" ")}" + gemfile_with_rails_weakling_and_foo_from_repo4 + + bundle "lock --remove-platform #{local_platform}", raise_on_error: false expect(err).to include("Removing all platforms from the bundle is not allowed") end - # from https://github.com/bundler/bundler/issues/4896 + # from https://github.com/rubygems/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 + s.platform = "x86-mingw32" end build_gem "gssapi", "0.1" @@ -276,17 +1074,24 @@ RSpec.describe "bundle lock" do end gemfile <<-G - source "#{file_uri_for(gem_repo4)}" + source "https://gem.repo4" gem "mixlib-shellout" gem "gssapi" G - simulate_platform(mingw) { bundle! :lock } + simulate_platform("x86-mingw32") { bundle :lock } + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "ffi", "1.9.14", "x86-mingw32" + c.checksum gem_repo4, "gssapi", "1.2.0" + c.checksum gem_repo4, "mixlib-shellout", "2.2.6", "universal-mingw32" + c.checksum gem_repo4, "win32-process", "0.8.3" + end - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo4)}/ + remote: https://gem.repo4/ specs: ffi (1.9.14-x86-mingw32) gssapi (1.2.0) @@ -302,16 +1107,20 @@ RSpec.describe "bundle lock" do DEPENDENCIES gssapi mixlib-shellout - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G - simulate_platform(rb) { bundle! :lock } + bundle_config "force_ruby_platform true" + bundle :lock - lockfile_should_be <<-G + checksums.checksum gem_repo4, "ffi", "1.9.14" + checksums.checksum gem_repo4, "mixlib-shellout", "2.2.6" + + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo4)}/ + remote: https://gem.repo4/ specs: ffi (1.9.14) ffi (1.9.14-x86-mingw32) @@ -330,33 +1139,1739 @@ RSpec.describe "bundle lock" do DEPENDENCIES gssapi mixlib-shellout + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "doesn't crash when an update candidate doesn't have any matching platform" do + build_repo4 do + build_gem "libv8", "8.4.255.0" + build_gem "libv8", "8.4.255.0" do |s| + s.platform = "x86_64-darwin-19" + end + + build_gem "libv8", "15.0.71.48.1beta2" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "libv8" + G + + lockfile <<-G + GEM + remote: https://gem.repo4/ + specs: + libv8 (8.4.255.0) + libv8 (8.4.255.0-x86_64-darwin-19) + + PLATFORMS + ruby + x86_64-darwin-19 + + DEPENDENCIES + libv8 BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G + + simulate_platform("x86_64-darwin-19") { bundle "lock --update" } + + expect(out).to match(/Writing lockfile to.+Gemfile\.lock/) end - context "when an update is available" do - let(:repo) { gem_repo2 } + it "adds all more specific candidates when they all have the same dependencies" do + build_repo4 do + build_gem "libv8", "8.4.255.0" do |s| + s.platform = "x86_64-darwin-19" + end + + build_gem "libv8", "8.4.255.0" do |s| + s.platform = "x86_64-darwin-20" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "libv8" + G + + simulate_platform("x86_64-darwin-19") { bundle "lock" } + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-19" + c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-20" + end + + expect(lockfile).to eq <<~G + GEM + remote: https://gem.repo4/ + specs: + libv8 (8.4.255.0-x86_64-darwin-19) + libv8 (8.4.255.0-x86_64-darwin-20) + + PLATFORMS + x86_64-darwin-19 + x86_64-darwin-20 + + DEPENDENCIES + libv8 + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "respects the previous lockfile if it had a matching less specific platform already locked, and installs the best variant for each platform" do + build_repo4 do + build_gem "libv8", "8.4.255.0" do |s| + s.platform = "x86_64-darwin-19" + end + + build_gem "libv8", "8.4.255.0" do |s| + s.platform = "x86_64-darwin-20" + end + end + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-19" + c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-20" + end + + gemfile <<-G + source "https://gem.repo4" + + gem "libv8" + G + + lockfile <<-G + GEM + remote: https://gem.repo4/ + specs: + libv8 (8.4.255.0-x86_64-darwin-19) + libv8 (8.4.255.0-x86_64-darwin-20) + + PLATFORMS + x86_64-darwin + + DEPENDENCIES + libv8 + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G + + previous_lockfile = lockfile + + %w[x86_64-darwin-19 x86_64-darwin-20].each do |platform| + simulate_platform(platform) do + bundle "lock" + expect(lockfile).to eq(previous_lockfile) + + bundle "install" + expect(the_bundle).to include_gem("libv8 8.4.255.0 #{platform}") + end + end + end + + it "does not conflict on ruby requirements when adding new platforms" do + build_repo4 do + build_gem "raygun-apm", "1.0.78" do |s| + s.platform = "x86_64-linux" + s.required_ruby_version = "< #{next_ruby_minor}.dev" + end + + build_gem "raygun-apm", "1.0.78" do |s| + s.platform = "universal-darwin" + s.required_ruby_version = "< #{next_ruby_minor}.dev" + end + + build_gem "raygun-apm", "1.0.78" do |s| + s.platform = "x64-mingw-ucrt" + s.required_ruby_version = "< #{next_ruby_minor}.dev" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "raygun-apm" + G + + lockfile <<-L + GEM + remote: https://gem.repo4/ + specs: + raygun-apm (1.0.78-universal-darwin) + + PLATFORMS + x86_64-darwin-19 + + DEPENDENCIES + raygun-apm + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --add-platform x86_64-linux" + end + + it "adds platform specific gems as necessary, even when adding the current platform" do + build_repo4 do + build_gem "nokogiri", "1.16.0" + build_gem "nokogiri", "1.16.0" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "nokogiri" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.16.0) + + PLATFORMS + ruby + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle "lock --add-platform x86_64-linux" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.16.0) + nokogiri (1.16.0-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "refuses to add platforms incompatible with the lockfile" do + build_repo4 do + build_gem "sorbet-static", "0.5.11989" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet-static" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet-static (0.5.11989-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + sorbet-static + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle "lock --add-platform ruby", raise_on_error: false + end + + nice_error = <<~E.strip + Could not find gems matching 'sorbet-static' valid for all resolution platforms (x86_64-linux, ruby) in rubygems repository https://gem.repo4/ or installed locally. + + The source contains the following gems matching 'sorbet-static': + * sorbet-static-0.5.11989-x86_64-linux + E + expect(err).to include(nice_error) + end + + it "respects lower bound ruby requirements" do + build_repo4 do + build_gem "our_private_gem", "0.1.0" do |s| + s.required_ruby_version = ">= #{Gem.ruby_version}" + end + end + + gemfile <<-G + source "https://localgemserver.test" + + gem "our_private_gem" + G + + lockfile <<-L + GEM + remote: https://localgemserver.test/ + specs: + our_private_gem (0.1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + our_private_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + end + + context "when an update is available" do before do - lockfile(@lockfile) - build_repo2 do + gemfile_with_rails_weakling_and_foo_from_repo4 + + build_repo4 do build_gem "foo", "2.0" end + + lockfile(expected_lockfile) end it "does not implicitly update" do - bundle! "lock" + bundle "lock" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "actionmailer", "2.3.2" + c.checksum gem_repo4, "actionpack", "2.3.2" + c.checksum gem_repo4, "activerecord", "2.3.2" + c.checksum gem_repo4, "activeresource", "2.3.2" + c.checksum gem_repo4, "activesupport", "2.3.2" + c.checksum gem_repo4, "foo", "1.0" + c.checksum gem_repo4, "rails", "2.3.2" + c.checksum gem_repo4, "rake", rake_version + c.checksum gem_repo4, "weakling", "0.0.3" + end - expect(read_lockfile).to eq(@lockfile) + expected_lockfile = <<~L + GEM + remote: https://gem.repo4/ + 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 (= #{rake_version}) + rake (#{rake_version}) + weakling (0.0.3) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo + rails + weakling + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + expect(read_lockfile).to eq(expected_lockfile) end it "accounts for changes in the gemfile" do gemfile gemfile.gsub('"foo"', '"foo", "2.0"') - bundle! "lock" + bundle "lock" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "actionmailer", "2.3.2" + c.checksum gem_repo4, "actionpack", "2.3.2" + c.checksum gem_repo4, "activerecord", "2.3.2" + c.checksum gem_repo4, "activeresource", "2.3.2" + c.checksum gem_repo4, "activesupport", "2.3.2" + c.checksum gem_repo4, "foo", "2.0" + c.checksum gem_repo4, "rails", "2.3.2" + c.checksum gem_repo4, "rake", rake_version + c.checksum gem_repo4, "weakling", "0.0.3" + end + + expected_lockfile = <<~L + GEM + remote: https://gem.repo4/ + 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 (2.0) + rails (2.3.2) + actionmailer (= 2.3.2) + actionpack (= 2.3.2) + activerecord (= 2.3.2) + activeresource (= 2.3.2) + rake (= #{rake_version}) + rake (#{rake_version}) + weakling (0.0.3) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo (= 2.0) + rails + weakling + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + expect(read_lockfile).to eq(expected_lockfile) + end + end + + context "when a system gem has incorrect dependencies, different from the lockfile" do + before do + build_repo4 do + build_gem "debug", "1.6.3" do |s| + s.add_dependency "irb", ">= 1.3.6" + end + + build_gem "irb", "1.5.0" + end + + system_gems "irb-1.5.0", gem_repo: gem_repo4 + system_gems "debug-1.6.3", gem_repo: gem_repo4 + + # simulate gemspec with wrong empty dependencies + debug_gemspec_path = system_gem_path("specifications/debug-1.6.3.gemspec") + debug_gemspec = Gem::Specification.load(debug_gemspec_path.to_s) + debug_gemspec.dependencies.clear + File.write(debug_gemspec_path, debug_gemspec.to_ruby) + end + + it "respects the existing lockfile, even when reresolving" do + gemfile <<~G + source "https://gem.repo4" + + gem "debug" + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "debug", "1.6.3" + c.checksum gem_repo4, "irb", "1.5.0" + end + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + debug (1.6.3) + irb (>= 1.3.6) + irb (1.5.0) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + debug + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "arm64-darwin-22" do + bundle "lock" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + debug (1.6.3) + irb (>= 1.3.6) + irb (1.5.0) + + PLATFORMS + arm64-darwin-22 + x86_64-linux + + DEPENDENCIES + debug + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when a system gem has incorrect dependencies, different from remote gems" do + before do + build_repo4 do + build_gem "foo", "1.0.0" do |s| + s.add_dependency "bar" + end + + build_gem "bar", "1.0.0" + end + + system_gems "foo-1.0.0", gem_repo: gem_repo4, path: default_bundle_path + + # simulate gemspec with wrong empty dependencies + foo_gemspec_path = default_bundle_path("specifications/foo-1.0.0.gemspec") + foo_gemspec = Gem::Specification.load(foo_gemspec_path.to_s) + foo_gemspec.dependencies.clear + File.write(foo_gemspec_path, foo_gemspec.to_ruby) + end + + it "generates a lockfile using remote dependencies, and prints a warning" do + gemfile <<~G + source "https://gem.repo4" + + gem "foo" + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "foo", "1.0.0" + c.checksum gem_repo4, "bar", "1.0.0" + end + + simulate_platform "x86_64-linux" do + bundle "lock --verbose" + end + + expect(err).to eq("Local specification for foo-1.0.0 has different dependencies than the remote gem, ignoring it") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + bar (1.0.0) + foo (1.0.0) + bar + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + foo + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "properly shows resolution errors including OR requirements" do + build_repo4 do + build_gem "activeadmin", "2.13.1" do |s| + s.add_dependency "railties", ">= 6.1", "< 7.1" + end + build_gem "actionpack", "6.1.4" + build_gem "actionpack", "7.0.3.1" + build_gem "actionpack", "7.0.4" + build_gem "railties", "6.1.4" do |s| + s.add_dependency "actionpack", "6.1.4" + end + build_gem "rails", "7.0.3.1" do |s| + s.add_dependency "railties", "7.0.3.1" + end + build_gem "rails", "7.0.4" do |s| + s.add_dependency "railties", "7.0.4" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "rails", ">= 7.0.3.1" + gem "activeadmin", "2.13.1" + G + + bundle "lock", raise_on_error: false + + expect(err).to eq <<~ERR.strip + Could not find compatible versions + + Because rails >= 7.0.4 depends on railties = 7.0.4 + and rails < 7.0.4 depends on railties = 7.0.3.1, + railties = 7.0.3.1 OR = 7.0.4 is required. + So, because railties = 7.0.3.1 OR = 7.0.4 could not be found in rubygems repository https://gem.repo4/ or installed locally, + version solving has failed. + ERR + end + + it "is able to display some explanation on crazy irresolvable cases" do + build_repo4 do + build_gem "activeadmin", "2.13.1" do |s| + s.add_dependency "ransack", "= 3.1.0" + end + + # Activemodel is missing as a dependency in lockfile + build_gem "ransack", "3.1.0" do |s| + s.add_dependency "activemodel", ">= 6.0.4" + s.add_dependency "activesupport", ">= 6.0.4" + end + + %w[6.0.4 7.0.2.3 7.0.3.1 7.0.4].each do |version| + build_gem "activesupport", version + + # Activemodel is only available on 6.0.4 + if version == "6.0.4" + build_gem "activemodel", version do |s| + s.add_dependency "activesupport", version + end + end + + build_gem "rails", version do |s| + # Depednencies of Rails 7.0.2.3 are in reverse order + if version == "7.0.2.3" + s.add_dependency "activesupport", version + s.add_dependency "activemodel", version + else + s.add_dependency "activemodel", version + s.add_dependency "activesupport", version + end + end + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "rails", ">= 7.0.2.3" + gem "activeadmin", "= 2.13.1" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + activeadmin (2.13.1) + ransack (= 3.1.0) + ransack (3.1.0) + activemodel (>= 6.0.4) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + activeadmin (= 2.13.1) + ransack (= 3.1.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + expected_error = <<~ERR.strip + Could not find compatible versions + + Because rails >= 7.0.4 depends on activemodel = 7.0.4 + and rails >= 7.0.3.1, < 7.0.4 depends on activemodel = 7.0.3.1, + rails >= 7.0.3.1 requires activemodel = 7.0.3.1 OR = 7.0.4. + (1) So, because rails >= 7.0.2.3, < 7.0.3.1 depends on activemodel = 7.0.2.3 + and every version of activemodel depends on activesupport = 6.0.4, + rails >= 7.0.2.3 requires activesupport = 6.0.4. + + Because rails >= 7.0.2.3, < 7.0.3.1 depends on activesupport = 7.0.2.3 + and rails >= 7.0.3.1, < 7.0.4 depends on activesupport = 7.0.3.1, + rails >= 7.0.2.3, < 7.0.4 requires activesupport = 7.0.2.3 OR = 7.0.3.1. + And because rails >= 7.0.4 depends on activesupport = 7.0.4, + rails >= 7.0.2.3 requires activesupport = 7.0.2.3 OR = 7.0.3.1 OR = 7.0.4. + And because rails >= 7.0.2.3 requires activesupport = 6.0.4 (1), + rails >= 7.0.2.3 cannot be used. + So, because Gemfile depends on rails >= 7.0.2.3, + version solving has failed. + ERR + + bundle "lock", raise_on_error: false + expect(err).to eq(expected_error) + + lockfile lockfile.gsub(/PLATFORMS\n #{local_platform}/m, "PLATFORMS\n #{lockfile_platforms("ruby")}") + + bundle "lock", raise_on_error: false + expect(err).to eq(expected_error) + end + + it "does not accidentally resolves to prereleases" do + build_repo4 do + build_gem "autoproj", "2.0.3" do |s| + s.add_dependency "autobuild", ">= 1.10.0.a" + s.add_dependency "tty-prompt" + end + + build_gem "tty-prompt", "0.6.0" + build_gem "tty-prompt", "0.7.0" + + build_gem "autobuild", "1.10.0.b3" + build_gem "autobuild", "1.10.1" do |s| + s.add_dependency "tty-prompt", "~> 0.6.0" + end + end + + gemfile <<~G + source "https://gem.repo4" + gem "autoproj", ">= 2.0.0" + G + + bundle "lock" + expect(lockfile).to_not include("autobuild (1.10.0.b3)") + expect(lockfile).to include("autobuild (1.10.1)") + end + + # Newer rails depends on Bundler, while ancient Rails does not. Bundler tries + # a first resolution pass that does not consider pre-releases. However, when + # using a pre-release Bundler (like the .dev version), that results in that + # pre-release being ignored and resolving to a version that does not depend on + # Bundler at all. We should avoid that and still consider .dev Bundler. + # + it "does not ignore prereleases with there's only one candidate" do + build_repo4 do + build_gem "rails", "7.4.0.2" do |s| + s.add_dependency "bundler", ">= 1.15.0" + end + + build_gem "rails", "2.3.18" + end + + gemfile <<~G + source "https://gem.repo4" + gem "rails" + G + + bundle "lock" + expect(lockfile).to_not include("rails (2.3.18)") + expect(lockfile).to include("rails (7.4.0.2)") + end + + it "deals with platform specific incompatibilities" do + build_repo4 do + build_gem "activerecord", "6.0.6" + build_gem "activerecord-jdbc-adapter", "60.4" do |s| + s.platform = "java" + s.add_dependency "activerecord", "~> 6.0.0" + end + build_gem "activerecord-jdbc-adapter", "61.0" do |s| + s.platform = "java" + s.add_dependency "activerecord", "~> 6.1.0" + end + end + + gemfile <<~G + source "https://gem.repo4" + gem "activerecord", "6.0.6" + gem "activerecord-jdbc-adapter", "61.0" + G + + simulate_platform "universal-java-19" do + bundle "lock", raise_on_error: false + end + + expect(err).to include("Could not find compatible versions") + expect(err).not_to include("ERROR REPORT TEMPLATE") + end + + it "adds checksums to an existing lockfile, when re-resolving is necessary" do + build_repo4 do + build_gem "nokogiri", "1.14.2" + build_gem "nokogiri", "1.14.2" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "nokogiri" + G + + # lockfile has a typo (nogokiri) in the dependencies section, so Bundler + # sees dependencies have changed, and re-resolves + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nogokiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle "lock --add-checksums" + end + + checksums = checksums_section do |c| + c.checksum gem_repo4, "nokogiri", "1.14.2" + c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "adds checksums to an existing lockfile, when no re-resolve is necessary" do + build_repo4 do + build_gem "nokogiri", "1.14.2" + build_gem "nokogiri", "1.14.2" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "nokogiri" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle "lock --add-checksums" + end + + checksums = checksums_section do |c| + c.checksum gem_repo4, "nokogiri", "1.14.2" + c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "adds checksums when source is not specified" do + system_gems(%w[myrack-1.0.0], path: default_bundle_path) + + gemfile <<-G + gem "myrack" + G + + lockfile <<~L + GEM + specs: + myrack (1.0.0) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + myrack + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle "lock --add-checksums" + end + + # myrack is coming from gem_repo1 + # but it's simulated to install in the system gems path + checksums = checksums_section do |c| + c.checksum gem_repo1, "myrack", "1.0.0" + end + + expect(lockfile).to eq <<~L + GEM + specs: + myrack (1.0.0) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "adds checksums to an existing lockfile, when gems are already installed" do + build_repo4 do + build_gem "nokogiri", "1.14.2" + build_gem "nokogiri", "1.14.2" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "nokogiri" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle "install" + + bundle "lock --add-checksums" + end + + checksums = checksums_section do |c| + c.checksum gem_repo4, "nokogiri", "1.14.2" + c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "generates checksums by default" do + build_repo4 do + build_gem "nokogiri", "1.14.2" + build_gem "nokogiri", "1.14.2" do |s| + s.platform = "x86_64-linux" + end + end + + simulate_platform "x86_64-linux" do + install_gemfile <<-G + source "https://gem.repo4" + + gem "nokogiri" + G + end + + checksums = checksums_section do |c| + c.checksum gem_repo4, "nokogiri", "1.14.2" + c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "disables checksums if configured to do so" do + build_repo4 do + build_gem "nokogiri", "1.14.2" + build_gem "nokogiri", "1.14.2" do |s| + s.platform = "x86_64-linux" + end + end + + bundle_config "lockfile_checksums false" + + simulate_platform "x86_64-linux" do + install_gemfile <<-G + source "https://gem.repo4" + + gem "nokogiri" + G + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "add checksums for gems installed on disk" do + build_repo4 do + build_gem "warning", "18.0.0" + end + + bundle_config "lockfile_checksums false" + + simulate_platform "x86_64-linux" do + install_gemfile(<<-G, artifice: "endpoint") + source "https://gem.repo4" + + gem "warning" + G + + bundle "config --delete lockfile_checksums" + bundle("lock --add-checksums", artifice: "endpoint") + end + + checksums = checksums_section do |c| + c.checksum gem_repo4, "warning", "18.0.0" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + warning (18.0.0) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + warning + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "doesn't add checksum for gems not installed on disk" do + lockfile(<<~L) + GEM + remote: https://gem.repo4/ + specs: + warning (18.0.0) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + warning + + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile(<<~G) + source "https://gem.repo4" + + gem "warning" + G + + build_repo4 do + build_gem "warning", "18.0.0" + end + + FileUtils.rm_rf("#{gem_repo4}/gems") + + bundle("lock --add-checksums", artifice: "endpoint") + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "warning", "18.0.0" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + warning (18.0.0) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + warning + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + context "when re-resolving to include prereleases" do + before do + build_repo4 do + build_gem "tzinfo-data", "1.2022.7" + build_gem "rails", "7.1.0.alpha" do |s| + s.add_dependency "activesupport" + end + build_gem "activesupport", "7.1.0.alpha" + end + end + + it "does not end up including gems scoped to other platforms in the lockfile" do + gemfile <<-G + source "https://gem.repo4" + gem "rails" + gem "tzinfo-data", platform: :windows + G + + simulate_platform "x86_64-darwin-22" do + bundle "lock" + end + + expect(lockfile).not_to include("tzinfo-data (1.2022.7)") + end + end + + context "when resolving platform specific gems as indirect dependencies on truffleruby", :truffleruby_only do + before do + build_lib "foo", path: bundled_app do |s| + s.add_dependency "nokogiri" + end + + build_repo4 do + build_gem "nokogiri", "1.14.2" + build_gem "nokogiri", "1.14.2" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<-G + source "https://gem.repo4" + gemspec + G + end + + it "locks both ruby and platform specific specs" do + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo4, "nokogiri", "1.14.2" + c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux" + end + + simulate_platform "x86_64-linux" do + bundle "lock" + end + + expect(lockfile).to eq <<~L + PATH + remote: . + specs: + foo (1.0) + nokogiri + + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + context "and a lockfile with platform specific gems only already exists" do + before do + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux" + end + + lockfile <<~L + PATH + remote: . + specs: + foo (1.0) + nokogiri + + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "keeps platform specific gems" do + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo4, "nokogiri", "1.14.2" + c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux" + end + + simulate_platform "x86_64-linux" do + bundle "install" + end + + expect(lockfile).to eq <<~L + PATH + remote: . + specs: + foo (1.0) + nokogiri + + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + end + + context "when adding a new gem that requires unlocking other transitive deps" do + before do + build_repo4 do + build_gem "govuk_app_config", "0.1.0" + + build_gem "govuk_app_config", "4.13.0" do |s| + s.add_dependency "railties", ">= 5.0" + end + + %w[7.0.4.1 7.0.4.3].each do |v| + build_gem "railties", v do |s| + s.add_dependency "actionpack", v + s.add_dependency "activesupport", v + end + + build_gem "activesupport", v + build_gem "actionpack", v + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "govuk_app_config" + gem "activesupport", "7.0.4.3" + G + + # Simulate out of sync lockfile because top level dependency on + # activesuport has just been added to the Gemfile, and locked to a higher + # version + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + actionpack (7.0.4.1) + activesupport (7.0.4.1) + govuk_app_config (4.13.0) + railties (>= 5.0) + railties (7.0.4.1) + actionpack (= 7.0.4.1) + activesupport (= 7.0.4.1) + + PLATFORMS + arm64-darwin-22 + + DEPENDENCIES + govuk_app_config + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "does not downgrade top level dependencies" do + checksums = checksums_section_when_enabled do |c| + c.no_checksum "actionpack", "7.0.4.3" + c.no_checksum "activesupport", "7.0.4.3" + c.no_checksum "govuk_app_config", "4.13.0" + c.no_checksum "railties", "7.0.4.3" + end + + simulate_platform "arm64-darwin-22" do + bundle "lock" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + actionpack (7.0.4.3) + activesupport (7.0.4.3) + govuk_app_config (4.13.0) + railties (>= 5.0) + railties (7.0.4.3) + actionpack (= 7.0.4.3) + activesupport (= 7.0.4.3) + + PLATFORMS + arm64-darwin-22 + + DEPENDENCIES + activesupport (= 7.0.4.3) + govuk_app_config + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when lockfile has incorrectly indented platforms" do + before do + build_repo4 do + build_gem "ffi", "1.1.0" do |s| + s.platform = "x86_64-linux" + end + + build_gem "ffi", "1.1.0" do |s| + s.platform = "arm64-darwin" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "ffi" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + ffi (1.1.0-arm64-darwin) + + PLATFORMS + arm64-darwin + + DEPENDENCIES + ffi + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "does not remove any gems" do + simulate_platform "x86_64-linux" do + bundle "lock --update" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + ffi (1.1.0-arm64-darwin) + ffi (1.1.0-x86_64-linux) + + PLATFORMS + arm64-darwin + x86_64-linux + + DEPENDENCIES + ffi + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + describe "--normalize-platforms on linux" do + let(:normalized_lockfile) do + <<~L + GEM + remote: https://gem.repo4/ + specs: + irb (1.0.0) + irb (1.0.0-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + irb + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + before do + build_repo4 do + build_gem "irb", "1.0.0" + + build_gem "irb", "1.0.0" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "irb" + G + end + + context "when already normalized" do + before do + lockfile normalized_lockfile + end + + it "is a noop" do + simulate_platform "x86_64-linux" do + bundle "lock --normalize-platforms" + end + + expect(lockfile).to eq(normalized_lockfile) + end + end + + context "when not already normalized" do + before do + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + irb (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + irb + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "normalizes the list of platforms and native gems in the lockfile" do + simulate_platform "x86_64-linux" do + bundle "lock --normalize-platforms" + end + + expect(lockfile).to eq(normalized_lockfile) + end + end + end + + describe "--normalize-platforms on darwin" do + let(:normalized_lockfile) do + <<~L + GEM + remote: https://gem.repo4/ + specs: + irb (1.0.0) + irb (1.0.0-arm64-darwin) + + PLATFORMS + arm64-darwin + ruby + + DEPENDENCIES + irb + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + before do + build_repo4 do + build_gem "irb", "1.0.0" + + build_gem "irb", "1.0.0" do |s| + s.platform = "arm64-darwin" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "irb" + G + end + + context "when already normalized" do + before do + lockfile normalized_lockfile + end + + it "is a noop" do + simulate_platform "arm64-darwin-23" do + bundle "lock --normalize-platforms" + end + + expect(lockfile).to eq(normalized_lockfile) + end + end + + context "when having only ruby" do + before do + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + irb (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + irb + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "normalizes the list of platforms and native gems in the lockfile" do + simulate_platform "arm64-darwin-23" do + bundle "lock --normalize-platforms" + end + + expect(lockfile).to eq(normalized_lockfile) + end + end + + context "when having only the current platform with version" do + before do + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + irb (1.0.0-arm64-darwin) + + PLATFORMS + arm64-darwin-23 + + DEPENDENCIES + irb + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "normalizes the list of platforms by removing version" do + simulate_platform "arm64-darwin-23" do + bundle "lock --normalize-platforms" + end + + expect(lockfile).to eq(normalized_lockfile) + end + end + + context "when having other platforms with version" do + before do + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + irb (1.0.0-arm64-darwin) + + PLATFORMS + arm64-darwin-22 + + DEPENDENCIES + irb + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "normalizes the list of platforms by removing version" do + simulate_platform "arm64-darwin-23" do + bundle "lock --normalize-platforms" + end + + expect(lockfile).to eq(normalized_lockfile) + end + end + end + + describe "--normalize-platforms with gems without generic variant" do + let(:original_lockfile) do + <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet-static (1.0-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + sorbet-static + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + before do + build_repo4 do + build_gem "sorbet-static" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet-static" + G + + lockfile original_lockfile + end + + it "removes invalid platforms" do + simulate_platform "x86_64-linux" do + bundle "lock --normalize-platforms" + end - expect(read_lockfile).to eq(@lockfile.sub("foo (1.0)", "foo (2.0)").sub(/foo$/, "foo (= 2.0)")) + expect(lockfile).to eq(original_lockfile.gsub(/^ ruby\n/m, "")) end end end diff --git a/spec/bundler/commands/newgem_spec.rb b/spec/bundler/commands/newgem_spec.rb index 708b41f623..65fbad05aa 100644 --- a/spec/bundler/commands/newgem_spec.rb +++ b/spec/bundler/commands/newgem_spec.rb @@ -6,96 +6,92 @@ RSpec.describe "bundle gem" do 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/#{require_path}.rb")).to exist - expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb")).to exist - end - - let(:generated_gemspec) { Bundler.load_gemspec_uncached(bundled_app(gem_name).join("#{gem_name}.gemspec")) } + expect(bundled_app("#{gem_name}/lib/#{gem_name}.rb")).to exist + expect(bundled_app("#{gem_name}/lib/#{gem_name}/version.rb")).to exist - let(:gem_name) { "mygem" } - - let(:require_path) { "mygem" } + expect(ignore_paths).to include("bin/") + expect(ignore_paths).to include("Gemfile") + end - before do - global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false" - git_config_content = <<-EOF - [user] - name = "Bundler User" - email = user@example.com - [github] - user = bundleuser - EOF - @git_config_location = ENV["GIT_CONFIG"] - path = "#{tmp}/test_git_config.txt" - File.open(path, "w") {|f| f.write(git_config_content) } - ENV["GIT_CONFIG"] = path + def bundle_exec_rubocop + prepare_gemspec(bundled_app(gem_name, "#{gem_name}.gemspec")) + bundle "config set path #{rubocop_gem_path}", dir: bundled_app(gem_name) + bundle "exec rubocop --debug --config .rubocop.yml", dir: bundled_app(gem_name) end - after do - FileUtils.rm(ENV["GIT_CONFIG"]) if File.exist?(ENV["GIT_CONFIG"]) - ENV["GIT_CONFIG"] = @git_config_location + def bundle_exec_standardrb + prepare_gemspec(bundled_app(gem_name, "#{gem_name}.gemspec")) + bundle "config set path #{standard_gem_path}", dir: bundled_app(gem_name) + bundle "exec standardrb --debug", dir: bundled_app(gem_name) 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_gemspec.authors.first).to eq("Bundler User") - end + def ignore_paths + generated = bundled_app("#{gem_name}/#{gem_name}.gemspec").read + matched = generated.match(/^\s+f\.start_with\?\(\*%w\[(?<ignored>.*)\]\)$/) + matched[:ignored]&.split(" ") + end - it "sets gemspec email to git user.email if available" do - expect(generated_gemspec.email.first).to eq("user@example.com") - end - end + def installed_go? + sys_exec("go version", raise_on_error: true) + true + rescue StandardError + false 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_gemspec.authors.first).to eq("TODO: Write your name") - end + let(:generated_gemspec) { Bundler.load_gemspec_uncached(bundled_app(gem_name).join("#{gem_name}.gemspec")) } - it "sets gemspec email to default message if git user.email is not set or empty" do - expect(generated_gemspec.email.first).to eq("TODO: Write your email address") - end + let(:gem_name) { "mygem" } + + before do + git("config --global user.name 'Bundler User'") + git("config --global user.email user@example.com") + git("config --global github.user bundleuser") + + bundle_config_global "gem.mit false" + bundle_config_global "gem.test false" + bundle_config_global "gem.coc false" + bundle_config_global "gem.linter false" + bundle_config_global "gem.ci false" + bundle_config_global "gem.changelog false" + bundle_config_global "gem.bundle false" end describe "git repo initialization" do - shared_examples_for "a gem with an initial git repo" do - before do - bundle! "gem #{gem_name} #{flags}" - end - - it "generates a gem skeleton with a .git folder" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/.git")).to exist - end + it "generates a gem skeleton with a .git folder" do + bundle "gem #{gem_name}" + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/.git")).to exist end - context "when using the default" do - it_behaves_like "a gem with an initial git repo" do - let(:flags) { "" } - end + it "generates a gem skeleton with a .git folder when passing --git" do + bundle "gem #{gem_name} --git" + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/.git")).to exist end - context "when explicitly passing --git" do - it_behaves_like "a gem with an initial git repo" do - let(:flags) { "--git" } - end + it "generates a gem skeleton without a .git folder when passing --no-git" do + bundle "gem #{gem_name} --no-git" + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/.git")).not_to exist end - context "when passing --no-git" do + context "on a path with spaces" do before do - bundle! "gem #{gem_name} --no-git" + Dir.mkdir(bundled_app("path with spaces")) end - it "generates a gem skeleton without a .git folder" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/.git")).not_to exist + + it "properly initializes git repo" do + skip "path with spaces needs special handling on Windows" if Gem.win_platform? + + bundle "gem #{gem_name}", dir: bundled_app("path with spaces") + expect(bundled_app("path with spaces/#{gem_name}/.git")).to exist end end end shared_examples_for "--mit flag" do before do - bundle! "gem #{gem_name} --mit" + bundle "gem #{gem_name} --mit" end it "generates a gem skeleton with MIT license" do gem_skeleton_assertions @@ -106,7 +102,7 @@ RSpec.describe "bundle gem" do shared_examples_for "--no-mit flag" do before do - bundle! "gem #{gem_name} --no-mit" + bundle "gem #{gem_name} --no-mit" end it "generates a gem skeleton without MIT license" do gem_skeleton_assertions @@ -115,43 +111,298 @@ RSpec.describe "bundle gem" do end shared_examples_for "--coc flag" do - before do - bundle! "gem #{gem_name} --coc" - end it "generates a gem skeleton with MIT license" do + bundle "gem #{gem_name} --coc" gem_skeleton_assertions expect(bundled_app("#{gem_name}/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("#{gem_name}/README.md").read).to include("## Code of Conduct") - expect(bundled_app("#{gem_name}/README.md").read).to include("https://github.com/bundleuser/#{gem_name}/blob/master/CODE_OF_CONDUCT.md") - end + it "generates the README with a section for the Code of Conduct" do + bundle "gem #{gem_name} --coc" + expect(bundled_app("#{gem_name}/README.md").read).to include("## Code of Conduct") + expect(bundled_app("#{gem_name}/README.md").read).to match(%r{https://github\.com/bundleuser/#{gem_name}/blob/.*/CODE_OF_CONDUCT.md}) + end + + it "generates the README with a section for the Code of Conduct, respecting the configured git default branch", git: ">= 2.28.0" do + git("config --global init.defaultBranch main") + bundle "gem #{gem_name} --coc" + + expect(bundled_app("#{gem_name}/README.md").read).to include("## Code of Conduct") + expect(bundled_app("#{gem_name}/README.md").read).to include("https://github.com/bundleuser/#{gem_name}/blob/main/CODE_OF_CONDUCT.md") end end shared_examples_for "--no-coc flag" do before do - bundle! "gem #{gem_name} --no-coc" + bundle "gem #{gem_name} --no-coc" end it "generates a gem skeleton without Code of Conduct" do gem_skeleton_assertions expect(bundled_app("#{gem_name}/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("#{gem_name}/README.md").read).not_to include("## Code of Conduct") - expect(bundled_app("#{gem_name}/README.md").read).not_to include("https://github.com/bundleuser/#{gem_name}/blob/master/CODE_OF_CONDUCT.md") - end + it "generates the README without a section for the Code of Conduct" do + expect(bundled_app("#{gem_name}/README.md").read).not_to include("## Code of Conduct") + expect(bundled_app("#{gem_name}/README.md").read).not_to match(%r{https://github\.com/bundleuser/#{gem_name}/blob/.*/CODE_OF_CONDUCT.md}) + end + end + + shared_examples_for "--changelog flag" do + before do + bundle "gem #{gem_name} --changelog" + end + it "generates a gem skeleton with a CHANGELOG" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/CHANGELOG.md")).to exist + end + end + + shared_examples_for "--no-changelog flag" do + before do + bundle "gem #{gem_name} --no-changelog" + end + it "generates a gem skeleton without a CHANGELOG" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/CHANGELOG.md")).to_not exist + end + end + + shared_examples_for "--bundle flag" do + before do + bundle "gem #{gem_name} --bundle" + end + it "generates a gem skeleton with bundle install" do + gem_skeleton_assertions + expect(out).to include("Running bundle install in the new gem directory.") + end + end + + shared_examples_for "--no-bundle flag" do + before do + bundle "gem #{gem_name} --no-bundle" + end + it "generates a gem skeleton without bundle install" do + gem_skeleton_assertions + expect(out).to_not include("Running bundle install in the new gem directory.") + end + end + + shared_examples_for "--linter=rubocop flag" do + before do + bundle "gem #{gem_name} --linter=rubocop" + end + + it "generates a gem skeleton with rubocop" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/Rakefile")).to read_as( + include("# frozen_string_literal: true"). + and(include('require "rubocop/rake_task"'). + and(include("RuboCop::RakeTask.new"). + and(match(/default:.+:rubocop/)))) + ) + end + + it "includes rubocop in generated Gemfile" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + builder = Bundler::Dsl.new + builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) + builder.dependencies + rubocop_dep = builder.dependencies.find {|d| d.name == "rubocop" } + expect(rubocop_dep).not_to be_specific + expect(rubocop_dep.requirement).to eq(Gem::Requirement.new([">= 0"])) + end + + it "generates a default .rubocop.yml" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist + end + + it "includes .rubocop.yml into ignore list" do + expect(ignore_paths).to include(".rubocop.yml") + end + end + + shared_examples_for "--linter=standard flag" do + before do + bundle "gem #{gem_name} --linter=standard" + end + + it "generates a gem skeleton with standard" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/Rakefile")).to read_as( + include('require "standard/rake"'). + and(match(/default:.+:standard/)) + ) + end + + it "includes standard in generated Gemfile" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + builder = Bundler::Dsl.new + builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) + builder.dependencies + standard_dep = builder.dependencies.find {|d| d.name == "standard" } + expect(standard_dep).not_to be_specific + expect(standard_dep.requirement).to eq(Gem::Requirement.new([">= 0"])) + end + + it "generates a default .standard.yml" do + expect(bundled_app("#{gem_name}/.standard.yml")).to exist + end + + it "includes .standard.yml into ignore list" do + expect(ignore_paths).to include(".standard.yml") + end + end + + shared_examples_for "--no-linter flag" do + define_negated_matcher :exclude, :include + + before do + bundle "gem #{gem_name} --no-linter" + end + + it "generates a gem skeleton without rubocop" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/Rakefile")).to read_as(exclude("rubocop")) + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to read_as(exclude("rubocop")) + end + + it "does not include rubocop in generated Gemfile" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + builder = Bundler::Dsl.new + builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) + builder.dependencies + rubocop_dep = builder.dependencies.find {|d| d.name == "rubocop" } + expect(rubocop_dep).to be_nil + end + + it "does not include standard in generated Gemfile" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + builder = Bundler::Dsl.new + builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) + builder.dependencies + standard_dep = builder.dependencies.find {|d| d.name == "standard" } + expect(standard_dep).to be_nil + end + + it "doesn't generate a default .rubocop.yml" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist + end + + it "does not add .rubocop.yml into ignore list" do + expect(ignore_paths).not_to include(".rubocop.yml") + end + + it "doesn't generate a default .standard.yml" do + expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist + end + + it "does not add .standard.yml into ignore list" do + expect(ignore_paths).not_to include(".standard.yml") + end + end + + it "has no rubocop offenses when using --linter=rubocop flag" do + skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core? + bundle "gem #{gem_name} --linter=rubocop" + bundle_exec_rubocop + expect(last_command).to be_success + end + + it "has no rubocop offenses when using --ext=c and --linter=rubocop flag" do + skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core? + bundle "gem #{gem_name} --ext=c --linter=rubocop" + bundle_exec_rubocop + expect(last_command).to be_success + end + + it "has no rubocop offenses when using --ext=c, --test=minitest, and --linter=rubocop flag" do + skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core? + bundle "gem #{gem_name} --ext=c --test=minitest --linter=rubocop" + bundle_exec_rubocop + expect(last_command).to be_success + end + + it "has no rubocop offenses when using --ext=c, --test=rspec, and --linter=rubocop flag" do + skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core? + bundle "gem #{gem_name} --ext=c --test=rspec --linter=rubocop" + bundle_exec_rubocop + expect(last_command).to be_success + end + + it "has no rubocop offenses when using --ext=c, --test=test-unit, and --linter=rubocop flag" do + skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core? + bundle "gem #{gem_name} --ext=c --test=test-unit --linter=rubocop" + bundle_exec_rubocop + expect(last_command).to be_success + end + + it "has no standard offenses when using --linter=standard flag" do + skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core? + bundle "gem #{gem_name} --linter=standard" + bundle_exec_standardrb + expect(last_command).to be_success + end + + it "has no rubocop offenses when using --ext=rust and --linter=rubocop flag" do + skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core? + + bundle "gem #{gem_name} --ext=rust --linter=rubocop" + bundle_exec_rubocop + expect(last_command).to be_success + end + + it "has no rubocop offenses when using --ext=rust, --test=minitest, and --linter=rubocop flag" do + skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core? + + bundle "gem #{gem_name} --ext=rust --test=minitest --linter=rubocop" + bundle_exec_rubocop + expect(last_command).to be_success + end + + it "has no rubocop offenses when using --ext=rust, --test=rspec, and --linter=rubocop flag" do + skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core? + + bundle "gem #{gem_name} --ext=rust --test=rspec --linter=rubocop" + bundle_exec_rubocop + expect(last_command).to be_success + end + + it "has no rubocop offenses when using --ext=rust, --test=test-unit, and --linter=rubocop flag" do + skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core? + + bundle "gem #{gem_name} --ext=rust --test=test-unit --linter=rubocop" + bundle_exec_rubocop + expect(last_command).to be_success + end + + shared_examples_for "CI config is absent" do + it "does not create any CI files" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist + end + end + + shared_examples_for "test framework is absent" do + it "does not create any test framework files" do + expect(bundled_app("#{gem_name}/.rspec")).to_not exist + expect(bundled_app("#{gem_name}/spec/#{gem_name}_spec.rb")).to_not exist + expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to_not exist + expect(bundled_app("#{gem_name}/test/#{gem_name}.rb")).to_not exist + expect(bundled_app("#{gem_name}/test/test_helper.rb")).to_not exist + end + + it "does not add any test framework files into ignore list" do + expect(ignore_paths).not_to include("test/") + expect(ignore_paths).not_to include(".rspec") + expect(ignore_paths).not_to include("spec/") end end context "README.md" do context "git config github.user present" do before do - bundle! "gem #{gem_name}" + bundle "gem #{gem_name}" end it "contribute URL set to git username" do @@ -162,7 +413,7 @@ RSpec.describe "bundle gem" do context "git config github.user is absent" do before do - sys_exec("git config --unset github.user") + git("config --global --unset github.user") bundle "gem #{gem_name}" end @@ -171,6 +422,22 @@ RSpec.describe "bundle gem" do expect(bundled_app("#{gem_name}/README.md").read).not_to include("github.com/bundleuser") end end + + describe "test task name on readme" do + shared_examples_for "test task name on readme" do |framework, task_name| + before do + bundle "gem #{gem_name} --test=#{framework}" + end + + it "renders with correct name" do + expect(bundled_app("#{gem_name}/README.md").read).to include("Then, run `rake #{task_name}` to run the tests.") + end + end + + it_behaves_like "test task name on readme", "test-unit", "test" + it_behaves_like "test task name on readme", "minitest", "test" + it_behaves_like "test task name on readme", "rspec", "spec" + end end it "creates a new git repository" do @@ -181,10 +448,7 @@ RSpec.describe "bundle gem" do context "when git is not available" do # This spec cannot have `git` available in the test env before do - load_paths = [lib_dir, spec_dir] - load_path_str = "-I#{load_paths.join(File::PATH_SEPARATOR)}" - - sys_exec "#{Gem.ruby} #{load_path_str} #{bindir.join("bundle")} gem #{gem_name}", "PATH" => "" + bundle "gem #{gem_name}", env: { "PATH" => "" } end it "creates the gem without the need for git" do @@ -198,27 +462,32 @@ RSpec.describe "bundle gem" do it "doesn't create a .gitignore file" do expect(bundled_app("#{gem_name}/.gitignore")).to_not exist end + + it "does not add .gitignore into ignore list" do + expect(ignore_paths).not_to include(".gitignore") + end end it "generates a valid gemspec" do - bundle! "gem newgem --bin" + bundle "gem newgem --bin" prepare_gemspec(bundled_app("newgem", "newgem.gemspec")) - Dir.chdir(bundled_app("newgem")) do - gems = ["rake-12.3.2"] - system_gems gems, :path => :bundle_path - bundle! "exec rake build" + build_repo2 do + build_dummy_irb "9.9.9" end + gems = ["rake-#{rake_version}", "irb-9.9.9"] + system_gems gems, path: system_gem_path, gem_repo: gem_repo2 + bundle "exec rake build", dir: bundled_app("newgem") - expect(last_command.stdboth).not_to include("ERROR") + expect(stdboth).not_to include("ERROR") end context "gem naming with relative paths" do it "resolves ." do create_temporary_dir("tmp") - bundle "gem ." + bundle "gem .", dir: bundled_app("tmp") expect(bundled_app("tmp/lib/tmp.rb")).to exist end @@ -226,7 +495,7 @@ RSpec.describe "bundle gem" do it "resolves .." do create_temporary_dir("temp/empty_dir") - bundle "gem .." + bundle "gem ..", dir: bundled_app("temp/empty_dir") expect(bundled_app("temp/lib/temp.rb")).to exist end @@ -234,144 +503,906 @@ RSpec.describe "bundle gem" do it "resolves relative directory" do create_temporary_dir("tmp/empty/tmp") - bundle "gem ../../empty" + bundle "gem ../../empty", dir: bundled_app("tmp/empty/tmp") expect(bundled_app("tmp/empty/lib/empty.rb")).to exist end def create_temporary_dir(dir) - FileUtils.mkdir_p(dir) - Dir.chdir(dir) + FileUtils.mkdir_p(bundled_app(dir)) end end - shared_examples_for "generating a gem" do + shared_examples_for "--github-username option" do |github_username| + before do + bundle "gem #{gem_name} --github-username=#{github_username}" + end + it "generates a gem skeleton" do - bundle! "gem #{gem_name}" + gem_skeleton_assertions + end - expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to exist - expect(bundled_app("#{gem_name}/Gemfile")).to exist - expect(bundled_app("#{gem_name}/Rakefile")).to exist - expect(bundled_app("#{gem_name}/lib/#{require_path}.rb")).to exist - expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb")).to exist - expect(bundled_app("#{gem_name}/.gitignore")).to exist + it "contribute URL set to given github username" do + expect(bundled_app("#{gem_name}/README.md").read).not_to include("[USERNAME]") + expect(bundled_app("#{gem_name}/README.md").read).to include("github.com/#{github_username}") + end + end + + shared_examples_for "github_username configuration" do + context "with github_username setting set to some value" do + before do + bundle_config_global "gem.github_username different_username" + bundle "gem #{gem_name}" + end + + it "generates a gem skeleton" do + gem_skeleton_assertions + end + + it "contribute URL set to bundle config setting" do + expect(bundled_app("#{gem_name}/README.md").read).not_to include("[USERNAME]") + expect(bundled_app("#{gem_name}/README.md").read).to include("github.com/different_username") + end + end + + context "with github_username setting set to false" do + before do + bundle_config_global "gem.github_username false" + bundle "gem #{gem_name}" + end + + it "generates a gem skeleton" do + gem_skeleton_assertions + end - expect(bundled_app("#{gem_name}/bin/setup")).to exist - expect(bundled_app("#{gem_name}/bin/console")).to exist + it "contribute URL set to [USERNAME]" do + expect(bundled_app("#{gem_name}/README.md").read).to include("[USERNAME]") + expect(bundled_app("#{gem_name}/README.md").read).not_to include("github.com/bundleuser") + end + end + end + + it "generates a gem skeleton" do + bundle "gem #{gem_name}" + + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to exist + expect(bundled_app("#{gem_name}/Gemfile")).to exist + expect(bundled_app("#{gem_name}/Rakefile")).to exist + expect(bundled_app("#{gem_name}/lib/#{gem_name}.rb")).to exist + expect(bundled_app("#{gem_name}/lib/#{gem_name}/version.rb")).to exist + expect(bundled_app("#{gem_name}/sig/#{gem_name}.rbs")).to exist + expect(bundled_app("#{gem_name}/.gitignore")).to exist + + expect(bundled_app("#{gem_name}/bin/setup")).to exist + expect(bundled_app("#{gem_name}/bin/console")).to exist + + unless Gem.win_platform? expect(bundled_app("#{gem_name}/bin/setup")).to be_executable expect(bundled_app("#{gem_name}/bin/console")).to be_executable end - it "starts with version 0.1.0" do - bundle! "gem #{gem_name}" + expect(bundled_app("#{gem_name}/bin/setup").read).to start_with("#!") + expect(bundled_app("#{gem_name}/bin/console").read).to start_with("#!") + end + + it "includes bin/ into ignore list" do + bundle "gem #{gem_name}" + + expect(ignore_paths).to include("bin/") + end + + it "includes Gemfile into ignore list" do + bundle "gem #{gem_name}" + + expect(ignore_paths).to include("Gemfile") + end + + it "includes .gitignore into ignore list" do + bundle "gem #{gem_name}" + + expect(ignore_paths).to include(".gitignore") + end + + it "starts with version 0.1.0" do + bundle "gem #{gem_name}" + + expect(bundled_app("#{gem_name}/lib/#{gem_name}/version.rb").read).to match(/VERSION = "0.1.0"/) + end + + it "declare String type for VERSION constant" do + bundle "gem #{gem_name}" + + expect(bundled_app("#{gem_name}/sig/#{gem_name}.rbs").read).to match(/VERSION: String/) + end + + context "git config user.{name,email} is set" do + before do + bundle "gem #{gem_name}" + end + + it "sets gemspec author to git user.name if available" do + expect(generated_gemspec.authors.first).to eq("Bundler User") + end - expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb").read).to match(/VERSION = "0.1.0"/) + it "sets gemspec email to git user.email if available" do + expect(generated_gemspec.email.first).to eq("user@example.com") end + end - context "git config user.{name,email} is set" do - before do - bundle! "gem #{gem_name}" + context "git config user.{name,email} is not set" do + before do + git("config --global --unset user.name") + git("config --global --unset user.email") + bundle "gem #{gem_name}" + end + + it "sets gemspec author to default message if git user.name is not set or empty" do + expect(generated_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_gemspec.email.first).to eq("TODO: Write your email address") + end + end + + it "sets gemspec metadata['allowed_push_host']" do + bundle "gem #{gem_name}" + + expect(generated_gemspec.metadata["allowed_push_host"]). + to match(/example\.com/) + end + + it "includes a commented-out rubygems_mfa_required metadata hint" do + bundle "gem #{gem_name}" + + gemspec_contents = bundled_app("#{gem_name}/#{gem_name}.gemspec").read + + expect(gemspec_contents).to include('# spec.metadata["rubygems_mfa_required"] = "true"') + expect(gemspec_contents).to include("https://guides.rubygems.org/mfa-requirement-opt-in/") + end + + it "sets a minimum ruby version" do + bundle "gem #{gem_name}" + + expect(generated_gemspec.required_ruby_version.to_s).to start_with(">=") + end + + it "does not include the gemspec file in files" do + bundle "gem #{gem_name}" + + bundler_gemspec = Bundler::GemHelper.new(bundled_app(gem_name), gem_name).gemspec + + expect(bundler_gemspec.files).not_to include("#{gem_name}.gemspec") + end + + it "does not include the Gemfile file in files" do + bundle "gem #{gem_name}" + + bundler_gemspec = Bundler::GemHelper.new(bundled_app(gem_name), gem_name).gemspec + + expect(bundler_gemspec.files).not_to include("Gemfile") + end + + it "runs rake without problems" do + bundle "gem #{gem_name}" + + system_gems ["rake-#{rake_version}"] + + rakefile = <<~RAKEFILE + task :default do + puts 'SUCCESS' end + RAKEFILE + File.open(bundled_app("#{gem_name}/Rakefile"), "w") do |file| + file.puts rakefile + end - it_should_behave_like "git config is present" + sys_exec("rake", dir: bundled_app(gem_name)) + expect(out).to include("SUCCESS") + end + + context "--exe parameter set" do + before do + bundle "gem #{gem_name} --exe" end - context "git config user.{name,email} is not set" do - before do - `git config --unset user.name` - `git config --unset user.email` - bundle "gem #{gem_name}" + it "builds exe skeleton" do + expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist + unless Gem.win_platform? + expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to be_executable end + end + end - it_should_behave_like "git config is absent" + context "--bin parameter set" do + before do + bundle "gem #{gem_name} --bin" end - it "sets gemspec metadata['allowed_push_host']" do - bundle! "gem #{gem_name}" + it "builds exe skeleton" do + expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist + end + end - expect(generated_gemspec.metadata["allowed_push_host"]). - to match(/mygemserver\.com/) + context "no --test parameter" do + before do + bundle "gem #{gem_name}" end - it "sets a minimum ruby version" do - bundle! "gem #{gem_name}" + it_behaves_like "test framework is absent" + end - bundler_gemspec = Bundler::GemHelper.new(gemspec_dir).gemspec + context "--test parameter set to rspec" do + before do + bundle "gem #{gem_name} --test=rspec" + end - expect(bundler_gemspec.required_ruby_version).to eq(generated_gemspec.required_ruby_version) + it "builds spec skeleton" do + expect(bundled_app("#{gem_name}/.rspec")).to exist + expect(bundled_app("#{gem_name}/spec/#{gem_name}_spec.rb")).to exist + expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist end - it "requires the version file" do - bundle! "gem #{gem_name}" + it "includes .rspec and spec/ into ignore list" do + expect(ignore_paths).to include(".rspec") + expect(ignore_paths).to include("spec/") + end - expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(%r{require "#{require_path}/version"}) + it "depends on a non-specific version of rspec in generated Gemfile" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + builder = Bundler::Dsl.new + builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) + builder.dependencies + rspec_dep = builder.dependencies.find {|d| d.name == "rspec" } + expect(rspec_dep).not_to be_specific + expect(rspec_dep.requirement).to eq(Gem::Requirement.new([">= 0"])) end + end - it "creates a base error class" do - bundle! "gem #{gem_name}" + context "init_gems_rb setting to true" do + before do + bundle_config "init_gems_rb true" + bundle "gem #{gem_name}" + end - expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/class Error < StandardError; end$/) + it "generates gems.rb instead of Gemfile" do + expect(bundled_app("#{gem_name}/gems.rb")).to exist + expect(bundled_app("#{gem_name}/Gemfile")).to_not exist + end + + it "includes gems.rb and gems.locked into ignore list" do + expect(ignore_paths).to include("gems.rb") + expect(ignore_paths).to include("gems.locked") + expect(ignore_paths).not_to include("Gemfile") + end + end + + context "init_gems_rb setting to false" do + before do + bundle_config "init_gems_rb false" + bundle "gem #{gem_name}" + end + + it "generates Gemfile instead of gems.rb" do + expect(bundled_app("#{gem_name}/gems.rb")).to_not exist + expect(bundled_app("#{gem_name}/Gemfile")).to exist + end + + it "includes Gemfile into ignore list" do + expect(ignore_paths).to include("Gemfile") + expect(ignore_paths).not_to include("gems.rb") + expect(ignore_paths).not_to include("gems.locked") + end + end + + context "gem.test setting set to rspec" do + before do + bundle_config "gem.test rspec" + bundle "gem #{gem_name}" + end + + it "builds spec skeleton" do + expect(bundled_app("#{gem_name}/.rspec")).to exist + expect(bundled_app("#{gem_name}/spec/#{gem_name}_spec.rb")).to exist + expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist end - it "runs rake without problems" do - bundle! "gem #{gem_name}" + it "includes .rspec and spec/ into ignore list" do + expect(ignore_paths).to include(".rspec") + expect(ignore_paths).to include("spec/") + end + end - system_gems ["rake-12.3.2"] + context "gem.test setting set to rspec and --test is set to minitest" do + before do + bundle_config "gem.test rspec" + bundle "gem #{gem_name} --test=minitest" + end + + it "builds spec skeleton" do + expect(bundled_app("#{gem_name}/test/test_#{gem_name}.rb")).to exist + expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist + end + + it "includes test/ into ignore list" do + expect(ignore_paths).to include("test/") + end + end + + context "--test parameter set to minitest" do + before do + bundle "gem #{gem_name} --test=minitest" + end - rakefile = strip_whitespace <<-RAKEFILE - task :default do - puts 'SUCCESS' + it "depends on a non-specific version of minitest" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + builder = Bundler::Dsl.new + builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) + builder.dependencies + minitest_dep = builder.dependencies.find {|d| d.name == "minitest" } + expect(minitest_dep).not_to be_specific + expect(minitest_dep.requirement).to eq(Gem::Requirement.new([">= 0"])) + end + + it "builds spec skeleton" do + expect(bundled_app("#{gem_name}/test/test_#{gem_name}.rb")).to exist + expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist + end + + it "includes test/ into ignore list" do + expect(ignore_paths).to include("test/") + end + + it "creates a default rake task to run the test suite" do + rakefile = <<~RAKEFILE + # frozen_string_literal: true + + require "bundler/gem_tasks" + require "minitest/test_task" + + Minitest::TestTask.create + + task default: :test + RAKEFILE + + expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) + end + end + + context "gem.test setting set to minitest" do + before do + bundle_config "gem.test minitest" + bundle "gem #{gem_name}" + end + + it "creates a default rake task to run the test suite" do + rakefile = <<~RAKEFILE + # frozen_string_literal: true + + require "bundler/gem_tasks" + require "minitest/test_task" + + Minitest::TestTask.create + + task default: :test + RAKEFILE + + expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) + end + end + + context "--test parameter set to test-unit" do + before do + bundle "gem #{gem_name} --test=test-unit" + end + + it "depends on a non-specific version of test-unit" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + builder = Bundler::Dsl.new + builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) + builder.dependencies + test_unit_dep = builder.dependencies.find {|d| d.name == "test-unit" } + expect(test_unit_dep).not_to be_specific + expect(test_unit_dep.requirement).to eq(Gem::Requirement.new([">= 0"])) + end + + it "builds spec skeleton" do + expect(bundled_app("#{gem_name}/test/#{gem_name}_test.rb")).to exist + expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist + end + + it "includes test/ into ignore list" do + expect(ignore_paths).to include("test/") + end + + it "creates a default rake task to run the test suite" do + rakefile = <<~RAKEFILE + # frozen_string_literal: true + + 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 - File.open(bundled_app("#{gem_name}/Rakefile"), "w") do |file| - file.puts rakefile + + expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) + end + end + + context "--test parameter set to an invalid value" do + before do + bundle "gem #{gem_name} --test=foo", raise_on_error: false + end + + it "fails loudly" do + expect(last_command).to be_failure + expect(err).to match(/Expected '--test' to be one of .*; got foo/) + end + end + + context "gem.test set to rspec and --test with no arguments" do + before do + bundle_config "gem.test rspec" + bundle "gem #{gem_name} --test" + end + + it "builds spec skeleton" do + expect(bundled_app("#{gem_name}/.rspec")).to exist + expect(bundled_app("#{gem_name}/spec/#{gem_name}_spec.rb")).to exist + expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist + end + + it "includes .rspec and spec/ into ignore list" do + expect(ignore_paths).to include(".rspec") + expect(ignore_paths).to include("spec/") + end + + it "hints that --test is already configured" do + expect(out).to match("rspec is already configured, ignoring --test flag.") + end + end + + context "gem.test setting set to false and --test with no arguments", :readline do + before do + bundle_config "gem.test false" + bundle "gem #{gem_name} --test" do |input, _, _| + input.puts end + end + + it "asks to generate test files" do + expect(out).to match("Do you want to generate tests with your gem?") + end + + it "hints that the choice will only be applied to the current gem" do + expect(out).to match("Your choice will only be applied to this gem.") + end + + it_behaves_like "test framework is absent" + end - Dir.chdir(bundled_app(gem_name)) do - sys_exec(rake) - expect(out).to include("SUCCESS") + context "gem.test setting not set and --test with no arguments", :readline do + before do + bundle_config_global "BUNDLE_GEM__TEST" => nil + bundle "gem #{gem_name} --test" do |input, _, _| + input.puts end end - context "--exe parameter set" do - before do - bundle "gem #{gem_name} --exe" + it "asks to generate test files" do + expect(out).to match("Do you want to generate tests with your gem?") + end + + it "hints that the choice will be applied to future bundle gem calls" do + hint = "Future `bundle gem` calls will use your choice. " \ + "This setting can be changed anytime with `bundle config gem.test`." + expect(out).to match(hint) + end + + it_behaves_like "test framework is absent" + end + + context "gem.test setting set to a test framework and --no-test" do + before do + bundle_config "gem.test rspec" + bundle "gem #{gem_name} --no-test" + end + + it_behaves_like "test framework is absent" + end + + context "--ci with no argument" do + before do + bundle "gem #{gem_name}" + end + + it "does not generate any CI config" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist + end + + it "does not add any CI config files into ignore list" do + expect(ignore_paths).not_to include(".github/") + expect(ignore_paths).not_to include(".gitlab-ci.yml") + expect(ignore_paths).not_to include(".circleci/") + end + end + + context "--ci set to github" do + before do + bundle "gem #{gem_name} --ci=github" + end + + it "generates a GitHub Actions config file" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist + end + + it "includes .github/ into ignore list" do + expect(ignore_paths).to include(".github/") + end + end + + context "--ci set to gitlab" do + before do + bundle "gem #{gem_name} --ci=gitlab" + end + + it "generates a GitLab CI config file" do + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist + end + + it "includes .gitlab-ci.yml into ignore list" do + expect(ignore_paths).to include(".gitlab-ci.yml") + end + end + + context "--ci set to circle" do + before do + bundle "gem #{gem_name} --ci=circle" + end + + it "generates a CircleCI config file" do + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist + end + + it "includes .circleci/ into ignore list" do + expect(ignore_paths).to include(".circleci/") + end + end + + context "--ci set to an invalid value" do + before do + bundle "gem #{gem_name} --ci=foo", raise_on_error: false + end + + it "fails loudly" do + expect(last_command).to be_failure + expect(err).to match(/Expected '--ci' to be one of .*; got foo/) + end + end + + context "gem.ci setting set to none" do + it "doesn't generate any CI config" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist + end + end + + context "gem.ci setting set to github" do + it "generates a GitHub Actions config file" do + bundle_config "gem.ci github" + bundle "gem #{gem_name}" + + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist + end + end + + context "gem.ci setting set to gitlab" do + it "generates a GitLab CI config file" do + bundle_config "gem.ci gitlab" + bundle "gem #{gem_name}" + + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist + end + end + + context "gem.ci setting set to circle" do + it "generates a CircleCI config file" do + bundle_config "gem.ci circle" + bundle "gem #{gem_name}" + + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist + end + end + + context "gem.ci set to github and --ci with no arguments" do + before do + bundle_config "gem.ci github" + bundle "gem #{gem_name} --ci" + end + + it "generates a GitHub Actions config file" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist + end + + it "hints that --ci is already configured" do + expect(out).to match("github is already configured, ignoring --ci flag.") + end + end + + context "gem.ci setting set to false and --ci with no arguments", :readline do + before do + bundle_config "gem.ci false" + bundle "gem #{gem_name} --ci" do |input, _, _| + input.puts "github" end + end - it "builds exe skeleton" do - expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist + it "asks to setup CI" do + expect(out).to match("Do you want to set up continuous integration for your gem?") + end + + it "hints that the choice will only be applied to the current gem" do + expect(out).to match("Your choice will only be applied to this gem.") + end + end + + context "gem.ci setting not set and --ci with no arguments", :readline do + before do + bundle_config_global "BUNDLE_GEM__CI" => nil + bundle "gem #{gem_name} --ci" do |input, _, _| + input.puts "github" end + end - it "requires the main file" do - expect(bundled_app("#{gem_name}/exe/#{gem_name}").read).to match(/require "#{require_path}"/) + it "asks to setup CI" do + expect(out).to match("Do you want to set up continuous integration for your gem?") + end + + it "hints that the choice will be applied to future bundle gem calls" do + hint = "Future `bundle gem` calls will use your choice. " \ + "This setting can be changed anytime with `bundle config gem.ci`." + expect(out).to match(hint) + end + end + + context "gem.ci setting set to a CI service and --no-ci" do + before do + bundle_config "gem.ci github" + bundle "gem #{gem_name} --no-ci" + end + + it "does not generate any CI config" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist + end + end + + context "--linter with no argument" do + before do + bundle "gem #{gem_name}" + end + + it "does not generate any linter config" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist + expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist + end + + it "does not add any linter config files into ignore list" do + expect(ignore_paths).not_to include(".rubocop.yml") + expect(ignore_paths).not_to include(".standard.yml") + end + end + + context "--linter set to rubocop" do + before do + bundle "gem #{gem_name} --linter=rubocop" + end + + it "generates a RuboCop config" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist + expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist + end + + it "includes .rubocop.yml into ignore list" do + expect(ignore_paths).to include(".rubocop.yml") + expect(ignore_paths).not_to include(".standard.yml") + end + end + + context "--linter set to standard" do + before do + bundle "gem #{gem_name} --linter=standard" + end + + it "generates a Standard config" do + expect(bundled_app("#{gem_name}/.standard.yml")).to exist + expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist + end + + it "includes .standard.yml into ignore list" do + expect(ignore_paths).to include(".standard.yml") + expect(ignore_paths).not_to include(".rubocop.yml") + end + end + + context "--linter set to an invalid value" do + before do + bundle "gem #{gem_name} --linter=foo", raise_on_error: false + end + + it "fails loudly" do + expect(last_command).to be_failure + expect(err).to match(/Expected '--linter' to be one of .*; got foo/) + end + end + + context "gem.linter setting set to none" do + before do + bundle "gem #{gem_name}" + end + + it "doesn't generate any linter config" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist + expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist + end + + it "does not add any linter config files into ignore list" do + expect(ignore_paths).not_to include(".rubocop.yml") + expect(ignore_paths).not_to include(".standard.yml") + end + end + + context "gem.linter setting set to rubocop" do + before do + bundle_config "gem.linter rubocop" + bundle "gem #{gem_name}" + end + + it "generates a RuboCop config file" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist + end + + it "includes .rubocop.yml into ignore list" do + expect(ignore_paths).to include(".rubocop.yml") + end + end + + context "gem.linter setting set to standard" do + before do + bundle_config "gem.linter standard" + bundle "gem #{gem_name}" + end + + it "generates a Standard config file" do + expect(bundled_app("#{gem_name}/.standard.yml")).to exist + end + + it "includes .standard.yml into ignore list" do + expect(ignore_paths).to include(".standard.yml") + end + end + + context "gem.linter set to rubocop and --linter with no arguments" do + before do + bundle_config "gem.linter rubocop" + bundle "gem #{gem_name} --linter" + end + + it "generates a RuboCop config file" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist + end + + it "includes .rubocop.yml into ignore list" do + expect(ignore_paths).to include(".rubocop.yml") + end + + it "hints that --linter is already configured" do + expect(out).to match("rubocop is already configured, ignoring --linter flag.") + end + end + + context "gem.linter setting set to false and --linter with no arguments", :readline do + before do + bundle_config "gem.linter false" + bundle "gem #{gem_name} --linter" do |input, _, _| + input.puts "rubocop" end end - context "--bin parameter set" do - before do - bundle "gem #{gem_name} --bin" + it "asks to setup a linter" do + expect(out).to match("Do you want to add a code linter and formatter to your gem?") + end + + it "hints that the choice will only be applied to the current gem" do + expect(out).to match("Your choice will only be applied to this gem.") + end + end + + context "gem.linter setting not set and --linter with no arguments", :readline do + before do + bundle_config_global "BUNDLE_GEM__LINTER" => nil + bundle "gem #{gem_name} --linter" do |input, _, _| + input.puts "rubocop" end + end - it "builds exe skeleton" do - expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist + it "asks to setup a linter" do + expect(out).to match("Do you want to add a code linter and formatter to your gem?") + end + + it "hints that the choice will be applied to future bundle gem calls" do + hint = "Future `bundle gem` calls will use your choice. " \ + "This setting can be changed anytime with `bundle config gem.linter`." + expect(out).to match(hint) + end + end + + context "gem.linter setting set to a linter and --no-linter" do + before do + bundle_config "gem.linter rubocop" + bundle "gem #{gem_name} --no-linter" + end + + it "does not generate any linter config" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist + expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist + end + + it "does not add any linter config files into ignore list" do + expect(ignore_paths).not_to include(".rubocop.yml") + expect(ignore_paths).not_to include(".standard.yml") + end + end + + context "--edit option" do + it "opens the generated gemspec in the user's text editor" do + output = bundle "gem #{gem_name} --edit=echo" + gemspec_path = File.join(bundled_app, gem_name, "#{gem_name}.gemspec") + expect(output).to include("echo \"#{gemspec_path}\"") + end + end + + shared_examples_for "paths that depend on gem name" do + it "generates entrypoint, version file and signatures file at the proper path, with the proper content" do + bundle "gem #{gem_name}" + + expect(bundled_app("#{gem_name}/lib/#{require_path}.rb")).to exist + expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(%r{require_relative "#{require_relative_path}/version"}) + expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/class Error < StandardError; end$/) + + expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb")).to exist + expect(bundled_app("#{gem_name}/sig/#{require_path}.rbs")).to exist + end + + context "--exe parameter set" do + before do + bundle "gem #{gem_name} --exe" end - it "requires the main file" do + it "builds an exe file that requires the proper entrypoint" do + expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist expect(bundled_app("#{gem_name}/exe/#{gem_name}").read).to match(/require "#{require_path}"/) end end - context "no --test parameter" do + context "--bin parameter set" do before do - bundle "gem #{gem_name}" + bundle "gem #{gem_name} --bin" end - it "doesn't create any spec/test file" do - expect(bundled_app("#{gem_name}/.rspec")).to_not exist - expect(bundled_app("#{gem_name}/spec/#{require_path}_spec.rb")).to_not exist - expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to_not exist - expect(bundled_app("#{gem_name}/test/#{require_path}.rb")).to_not exist - expect(bundled_app("#{gem_name}/test/minitest_helper.rb")).to_not exist + it "builds an exe file that requires the proper entrypoint" do + expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist + expect(bundled_app("#{gem_name}/exe/#{gem_name}").read).to match(/require "#{require_path}"/) end end @@ -380,189 +1411,288 @@ RSpec.describe "bundle gem" do bundle "gem #{gem_name} --test=rspec" end - it "builds spec skeleton" do - expect(bundled_app("#{gem_name}/.rspec")).to exist - expect(bundled_app("#{gem_name}/spec/#{require_path}_spec.rb")).to exist + it "builds a spec helper that requires the proper entrypoint, and a default test in the proper path which fails" do expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist - end - - it "depends on a specific version of rspec in generated Gemfile" do - Dir.chdir(bundled_app(gem_name)) do - builder = Bundler::Dsl.new - builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) - builder.dependencies - rspec_dep = builder.dependencies.find {|d| d.name == "rspec" } - expect(rspec_dep).to be_specific - end - end - - it "requires the main file" do expect(bundled_app("#{gem_name}/spec/spec_helper.rb").read).to include(%(require "#{require_path}")) - end - - it "creates a default test which fails" do + expect(bundled_app("#{gem_name}/spec/#{require_path}_spec.rb")).to exist expect(bundled_app("#{gem_name}/spec/#{require_path}_spec.rb").read).to include("expect(false).to eq(true)") end end - context "gem.test setting set to rspec" do + context "--test parameter set to minitest" do before do - bundle "config set gem.test rspec" - bundle "gem #{gem_name}" + bundle "gem #{gem_name} --test=minitest" end - it "builds spec skeleton" do - expect(bundled_app("#{gem_name}/.rspec")).to exist - expect(bundled_app("#{gem_name}/spec/#{require_path}_spec.rb")).to exist - expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist + it "builds a test helper that requires the proper entrypoint, and default test file in the proper path that defines the proper test class name, requires helper, and fails" do + expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist + expect(bundled_app("#{gem_name}/test/test_helper.rb").read).to include(%(require "#{require_path}")) + + expect(bundled_app("#{gem_name}/#{minitest_test_file_path}")).to exist + expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include(minitest_test_class_name) + expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include(%(require "test_helper")) + expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include("assert false") end end - context "gem.test setting set to rspec and --test is set to minitest" do + context "--test parameter set to test-unit" do before do - bundle "config set gem.test rspec" - bundle "gem #{gem_name} --test=minitest" + bundle "gem #{gem_name} --test=test-unit" end - it "builds spec skeleton" do - expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb")).to exist + it "builds a test helper that requires the proper entrypoint, and default test file in the proper path which requires helper and fails" do expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist + expect(bundled_app("#{gem_name}/test/test_helper.rb").read).to include(%(require "#{require_path}")) + expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb")).to exist + expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb").read).to include(%(require "test_helper")) + expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb").read).to include("assert_equal(\"expected\", \"actual\")") end end + end - context "--test parameter set to minitest" do - before do - bundle "gem #{gem_name} --test=minitest" - end + context "with mit option in bundle config settings set to true" do + before do + bundle_config_global "gem.mit true" + end + it_behaves_like "--mit flag" + it_behaves_like "--no-mit flag" + end - it "depends on a specific version of minitest" do - Dir.chdir(bundled_app(gem_name)) do - builder = Bundler::Dsl.new - builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) - builder.dependencies - minitest_dep = builder.dependencies.find {|d| d.name == "minitest" } - expect(minitest_dep).to be_specific - end - end + context "with mit option in bundle config settings set to false" do + before do + bundle_config_global "gem.mit false" + end + it_behaves_like "--mit flag" + it_behaves_like "--no-mit flag" + end - it "builds spec skeleton" do - expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb")).to exist - expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist - end + context "with coc option in bundle config settings set to true" do + before do + bundle_config_global "gem.coc true" + end + it_behaves_like "--coc flag" + it_behaves_like "--no-coc flag" + end - it "requires the main file" do - expect(bundled_app("#{gem_name}/test/test_helper.rb").read).to include(%(require "#{require_path}")) - end + context "with coc option in bundle config settings set to false" do + before do + bundle_config_global "gem.coc false" + end + it_behaves_like "--coc flag" + it_behaves_like "--no-coc flag" + end - it "requires 'minitest_helper'" do - expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb").read).to include(%(require "test_helper")) - end + context "with rubocop option in bundle config settings set to true" do + before do + bundle_config_global "gem.rubocop true" + end + it_behaves_like "--linter=rubocop flag" + it_behaves_like "--linter=standard flag" + it_behaves_like "--no-linter flag" + end - it "creates a default test which fails" do - expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb").read).to include("assert false") - end + context "with rubocop option in bundle config settings set to false" do + before do + bundle_config_global "gem.rubocop false" end + it_behaves_like "--linter=rubocop flag" + it_behaves_like "--linter=standard flag" + it_behaves_like "--no-linter flag" + end - context "gem.test setting set to minitest" do - before do - bundle "config set gem.test minitest" - bundle "gem #{gem_name}" - end + context "with linter option in bundle config settings set to rubocop" do + before do + bundle_config_global "gem.linter rubocop" + end + it_behaves_like "--linter=rubocop flag" + it_behaves_like "--linter=standard flag" + it_behaves_like "--no-linter flag" + end - it "creates a default rake task to run the test suite" do - rakefile = strip_whitespace <<-RAKEFILE - require "bundler/gem_tasks" - require "rake/testtask" + context "with linter option in bundle config settings set to standard" do + before do + bundle_config_global "gem.linter standard" + end + it_behaves_like "--linter=rubocop flag" + it_behaves_like "--linter=standard flag" + it_behaves_like "--no-linter flag" + end - Rake::TestTask.new(:test) do |t| - t.libs << "test" - t.libs << "lib" - t.test_files = FileList["test/**/*_test.rb"] - end + context "with linter option in bundle config settings set to false" do + before do + bundle_config_global "gem.linter false" + end + it_behaves_like "--linter=rubocop flag" + it_behaves_like "--linter=standard flag" + it_behaves_like "--no-linter flag" + end - task :default => :test - RAKEFILE + context "with changelog option in bundle config settings set to true" do + before do + bundle_config_global "gem.changelog true" + end + it_behaves_like "--changelog flag" + it_behaves_like "--no-changelog flag" + end - expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) - end + context "with changelog option in bundle config settings set to false" do + before do + bundle_config_global "gem.changelog false" end + it_behaves_like "--changelog flag" + it_behaves_like "--no-changelog flag" + end + + context "with bundle option in bundle config settings set to true" do + before do + bundle_config_global "gem.bundle true" + end + it_behaves_like "--bundle flag" + it_behaves_like "--no-bundle flag" + + it "runs bundle install" do + bundle "gem #{gem_name}" + expect(out).to include("Running bundle install in the new gem directory.") + end + end + + context "with bundle option in bundle config settings set to false" do + before do + bundle_config_global "gem.bundle false" + end + it_behaves_like "--bundle flag" + it_behaves_like "--no-bundle flag" + + it "does not run bundle install" do + bundle "gem #{gem_name}" + expect(out).to_not include("Running bundle install in the new gem directory.") + end + end - context "--test with no arguments" do + context "without git config github.user set" do + before do + git("config --global --unset github.user") + end + context "with github-username option in bundle config settings set to some value" do before do - bundle "gem #{gem_name} --test" + bundle_config_global "gem.github_username different_username" end + it_behaves_like "--github-username option", "gh_user" + end - it "defaults to rspec" do - expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist - expect(bundled_app("#{gem_name}/test/minitest_helper.rb")).to_not exist + it_behaves_like "github_username configuration" + + context "with github-username option in bundle config settings set to false" do + before do + bundle_config_global "gem.github_username false" end + it_behaves_like "--github-username option", "gh_user" + end - it "creates a .travis.yml file to test the library against the current Ruby version on Travis CI" do - expect(bundled_app("#{gem_name}/.travis.yml").read).to match(/- #{RUBY_VERSION}/) + context "when changelog is enabled" do + it "sets gemspec changelog_uri, homepage, homepage_uri, source_code_uri to TODOs" do + bundle "gem #{gem_name} --changelog" + + expect(generated_gemspec.metadata["changelog_uri"]). + to eq("TODO: Put your gem's CHANGELOG.md URL here.") + expect(generated_gemspec.homepage).to eq("TODO: Put your gem's website or public repo URL here.") + expect(generated_gemspec.metadata["homepage_uri"]).to eq("TODO: Put your gem's website or public repo URL here.") + expect(generated_gemspec.metadata["source_code_uri"]).to eq("TODO: Put your gem's public repo URL here.") end end - context "--edit option" do - it "opens the generated gemspec in the user's text editor" do - 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}\"") + context "when changelog is not enabled" do + it "sets gemspec homepage, homepage_uri, source_code_uri to TODOs and changelog_uri to nil" do + bundle "gem #{gem_name}" + + expect(generated_gemspec.metadata["changelog_uri"]).to be_nil + expect(generated_gemspec.homepage).to eq("TODO: Put your gem's website or public repo URL here.") + expect(generated_gemspec.metadata["homepage_uri"]).to eq("TODO: Put your gem's website or public repo URL here.") + expect(generated_gemspec.metadata["source_code_uri"]).to eq("TODO: Put your gem's public repo URL here.") end end end - context "testing --mit and --coc options against bundle config settings" do - let(:gem_name) { "test-gem" } + context "with git config github.user set" do + context "with github-username option in bundle config settings set to some value" do + before do + bundle_config_global "gem.github_username different_username" + end + it_behaves_like "--github-username option", "gh_user" + end - let(:require_path) { "test/gem" } + it_behaves_like "github_username configuration" - context "with mit option in bundle config settings set to true" do + context "with github-username option in bundle config settings set to false" do before do - global_config "BUNDLE_GEM__MIT" => "true", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false" + bundle_config_global "gem.github_username false" end - it_behaves_like "--mit flag" - it_behaves_like "--no-mit flag" + it_behaves_like "--github-username option", "gh_user" 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 "when changelog is enabled" do + it "sets gemspec changelog_uri, homepage, homepage_uri, source_code_uri based on git username" do + bundle "gem #{gem_name} --changelog" - 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" + expect(generated_gemspec.metadata["changelog_uri"]). + to eq("https://github.com/bundleuser/#{gem_name}/blob/main/CHANGELOG.md") + expect(generated_gemspec.homepage).to eq("https://github.com/bundleuser/#{gem_name}") + expect(generated_gemspec.metadata["homepage_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") + expect(generated_gemspec.metadata["source_code_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") end - 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" + context "when changelog is not enabled" do + it "sets gemspec source_code_uri, homepage, homepage_uri but not changelog_uri" do + bundle "gem #{gem_name}" + + expect(generated_gemspec.metadata["changelog_uri"]).to be_nil + expect(generated_gemspec.homepage).to eq("https://github.com/bundleuser/#{gem_name}") + expect(generated_gemspec.metadata["homepage_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") + expect(generated_gemspec.metadata["source_code_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") + end end end + context "standard gem naming" do + let(:require_path) { gem_name } + + let(:require_relative_path) { gem_name } + + let(:minitest_test_file_path) { "test/test_#{gem_name}.rb" } + + let(:minitest_test_class_name) { "class TestMygem < Minitest::Test" } + + include_examples "paths that depend on gem name" + end + context "gem naming with underscore" do let(:gem_name) { "test_gem" } let(:require_path) { "test_gem" } - let(:flags) { nil } + let(:require_relative_path) { "test_gem" } - before do - bundle! ["gem", gem_name, flags].compact.join(" ") - end + let(:minitest_test_file_path) { "test/test_test_gem.rb" } + + let(:minitest_test_class_name) { "class TestTestGem < Minitest::Test" } + + let(:flags) { nil } it "does not nest constants" do + bundle ["gem", gem_name, flags].compact.join(" ") expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb").read).to match(/module TestGem/) expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/module TestGem/) end - include_examples "generating a gem" + include_examples "paths that depend on gem name" - context "--ext parameter set" do - let(:flags) { "--ext" } + context "--ext parameter set with C" do + let(:flags) { "--ext=c" } + + before do + bundle ["gem", gem_name, flags].compact.join(" ") + end it "builds ext skeleton" do expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist @@ -570,26 +1700,262 @@ RSpec.describe "bundle gem" do expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c")).to exist end - it "includes rake-compiler" do + it "generates native extension loading code" do + expect(bundled_app("#{gem_name}/lib/#{gem_name}.rb").read).to include(<<~RUBY) + require_relative "test_gem/version" + require "#{gem_name}/#{gem_name}" + RUBY + end + + it "includes rake-compiler, but no Rust related changes" do expect(bundled_app("#{gem_name}/Gemfile").read).to include('gem "rake-compiler"') + + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to_not include('spec.add_dependency "rb_sys"') + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to_not include('spec.required_rubygems_version = ">= ') end it "depends on compile task for build" do - rakefile = strip_whitespace <<-RAKEFILE + rakefile = <<~RAKEFILE + # frozen_string_literal: true + require "bundler/gem_tasks" require "rake/extensiontask" - task :build => :compile + task build: :compile + + GEMSPEC = Gem::Specification.load("#{gem_name}.gemspec") + + Rake::ExtensionTask.new("#{gem_name}", GEMSPEC) do |ext| + ext.lib_dir = "lib/#{gem_name}" + end + + task default: %i[clobber compile] + RAKEFILE + + expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) + end + end + + context "--ext parameter set with rust" do + let(:flags) { "--ext=rust" } + + before do + bundle ["gem", gem_name, flags].compact.join(" ") + end + + it "is not deprecated" do + expect(err).not_to include "[DEPRECATED] Option `--ext` without explicit value is deprecated." + end + + it "builds ext skeleton" do + expect(bundled_app("#{gem_name}/Cargo.toml")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/Cargo.toml")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/src/lib.rs")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/build.rs")).to exist + end + + it "includes rake-compiler and rb_sys gems constraint" do + expect(bundled_app("#{gem_name}/Gemfile").read).to include('gem "rake-compiler"') + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "rb_sys"') + end + + it "depends on compile task for build" do + rakefile = <<~RAKEFILE + # frozen_string_literal: true - Rake::ExtensionTask.new("#{gem_name}") do |ext| + require "bundler/gem_tasks" + require "rb_sys/extensiontask" + + task build: :compile + + GEMSPEC = Gem::Specification.load("#{gem_name}.gemspec") + + RbSys::ExtensionTask.new("#{gem_name}", GEMSPEC) do |ext| ext.lib_dir = "lib/#{gem_name}" end - task :default => [:clobber, :compile, :spec] + task default: :compile RAKEFILE expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) end + + it "configures the crate such that `cargo test` works", :ruby_repo, :mri_only do + env = setup_rust_env + gem_path = bundled_app(gem_name) + result = sys_exec("cargo test", env: env, dir: gem_path, timeout: 300) + + expect(result).to include("1 passed") + end + + def setup_rust_env + skip "rust toolchain of mingw is broken" if RUBY_PLATFORM.match?("mingw") + + env = { + "CARGO_HOME" => ENV.fetch("CARGO_HOME", File.join(ENV["HOME"], ".cargo")), + "RUSTUP_HOME" => ENV.fetch("RUSTUP_HOME", File.join(ENV["HOME"], ".rustup")), + "RUSTUP_TOOLCHAIN" => ENV.fetch("RUSTUP_TOOLCHAIN", "stable"), + } + + system(env, "cargo", "-V", out: IO::NULL, err: [:child, :out]) + skip "cargo not present" unless $?.success? + # Hermetic Cargo setup + RbConfig::CONFIG.each {|k, v| env["RBCONFIG_#{k}"] = v } + env + end + end + + context "--ext parameter set with go" do + let(:flags) { "--ext=go" } + + before do + bundle ["gem", gem_name, flags].compact.join(" ") + end + + after do + sys_exec("go clean -modcache", raise_on_error: true) if installed_go? + end + + it "is not deprecated" do + expect(err).not_to include "[DEPRECATED] Option `--ext` without explicit value is deprecated." + end + + it "builds ext skeleton" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.h")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod")).to exist + end + + it "includes extconf.rb in gem_name.gemspec" do + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include(%(spec.extensions = ["ext/#{gem_name}/extconf.rb"])) + end + + it "includes go_gem in gem_name.gemspec" do + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "go_gem", ">= 0.2"') + end + + it "includes go_gem extension in extconf.rb" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(<<~RUBY) + require "mkmf" + require "go_gem/mkmf" + RUBY + + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(%(create_go_makefile("#{gem_name}/#{gem_name}"))) + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).not_to include("create_makefile") + end + + it "includes go_gem extension in gem_name.c" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c").read).to eq(<<~C) + #include "#{gem_name}.h" + #include "_cgo_export.h" + C + end + + it "includes skeleton code in gem_name.go" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) + /* + #include "#{gem_name}.h" + + VALUE rb_#{gem_name}_sum(VALUE self, VALUE a, VALUE b); + */ + import "C" + GO + + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) + //export rb_#{gem_name}_sum + func rb_#{gem_name}_sum(_ C.VALUE, a C.VALUE, b C.VALUE) C.VALUE { + GO + + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) + //export Init_#{gem_name} + func Init_#{gem_name}() { + GO + end + + it "includes valid module name in go.mod" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/bundleuser/#{gem_name}") + end + + it "includes go_gem extension in Rakefile" do + expect(bundled_app("#{gem_name}/Rakefile").read).to include(<<~RUBY) + require "go_gem/rake_task" + + GoGem::RakeTask.new("#{gem_name}") + RUBY + end + + context "with --no-ci" do + let(:flags) { "--ext=go --no-ci" } + + it_behaves_like "CI config is absent" + end + + context "--ci set to github" do + let(:flags) { "--ext=go --ci=github" } + + it "generates .github/workflows/main.yml" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist + expect(bundled_app("#{gem_name}/.github/workflows/main.yml").read).to include("go-version-file: ext/#{gem_name}/go.mod") + end + end + + context "--ci set to circle" do + let(:flags) { "--ext=go --ci=circle" } + + it "generates a .circleci/config.yml" do + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist + + expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML.strip) + environment: + GO_VERSION: + YAML + + expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML) + - run: + name: Install Go + command: | + wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz + tar -C /usr/local -xzf /tmp/go.tar.gz + echo 'export PATH=/usr/local/go/bin:"$PATH"' >> "$BASH_ENV" + YAML + end + end + + context "--ci set to gitlab" do + let(:flags) { "--ext=go --ci=gitlab" } + + it "generates a .gitlab-ci.yml" do + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist + + expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML) + - wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz + - tar -C /usr/local -xzf /tmp/go.tar.gz + - export PATH=/usr/local/go/bin:$PATH + YAML + + expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML.strip) + variables: + GO_VERSION: + YAML + end + end + + context "without github.user" do + before do + # FIXME: GitHub Actions Windows Runner hang up here for some reason... + skip "Workaround for hung up" if Gem.win_platform? + + git("config --global --unset github.user") + bundle ["gem", gem_name, flags].compact.join(" ") + end + + it "includes valid module name in go.mod" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/username/#{gem_name}") + end + end end end @@ -598,42 +1964,45 @@ RSpec.describe "bundle gem" do let(:require_path) { "test/gem" } - before do - bundle! "gem #{gem_name}" - end + let(:require_relative_path) { "gem" } + + let(:minitest_test_file_path) { "test/test/test_gem.rb" } + + let(:minitest_test_class_name) { "class Test::TestGem < Minitest::Test" } it "nests constants so they work" do + bundle "gem #{gem_name}" expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb").read).to match(/module Test\n module Gem/) expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/module Test\n module Gem/) end - include_examples "generating a gem" + include_examples "paths that depend on gem name" end describe "uncommon gem names" do it "can deal with two dashes" do - bundle! "gem a--a" + bundle "gem a--a" expect(bundled_app("a--a/a--a.gemspec")).to exist end it "fails gracefully with a ." do - bundle "gem foo.gemspec" + bundle "gem foo.gemspec", raise_on_error: false expect(err).to end_with("Invalid gem name foo.gemspec -- `Foo.gemspec` is an invalid constant name") end it "fails gracefully with a ^" do - bundle "gem ^" + bundle "gem ^", raise_on_error: false expect(err).to end_with("Invalid gem name ^ -- `^` is an invalid constant name") end it "fails gracefully with a space" do - bundle "gem 'foo bar'" + bundle "gem 'foo bar'", raise_on_error: false expect(err).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" + bundle "gem foo bar baz", raise_on_error: false expect(err).to eq(<<-E.strip) ERROR: "bundle gem" was called with arguments ["foo", "bar", "baz"] Usage: "bundle gem NAME [OPTIONS]" @@ -643,7 +2012,7 @@ Usage: "bundle gem NAME [OPTIONS]" describe "#ensure_safe_gem_name" do before do - bundle "gem #{subject}" + bundle "gem #{subject}", raise_on_error: false end context "with an existing const name" do @@ -656,6 +2025,19 @@ Usage: "bundle gem NAME [OPTIONS]" it { expect(err).to include("Invalid gem name #{subject}") } end + context "starting with a number" do + subject { "1gem" } + it { expect(err).to include("Invalid gem name #{subject}") } + end + + context "including capital letter" do + subject { "CAPITAL" } + it "should warn but not error" do + expect(err).to include("Gem names with capital letters are not recommended") + expect(bundled_app("#{subject}/#{subject}.gemspec")).to exist + end + end + context "starting with an existing const name" do subject { "gem-somenewconstantname" } it { expect(err).not_to include("Invalid gem name #{subject}") } @@ -667,30 +2049,42 @@ Usage: "bundle gem NAME [OPTIONS]" end end - context "on first run" do + context "on first run", :readline do it "asks about test framework" do - global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__COC" => "false" + bundle_config_global "BUNDLE_GEM__TEST" => nil bundle "gem foobar" do |input, _, _| input.puts "rspec" end expect(bundled_app("foobar/spec/spec_helper.rb")).to exist - rakefile = strip_whitespace <<-RAKEFILE + rakefile = <<~RAKEFILE + # frozen_string_literal: true + require "bundler/gem_tasks" require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) - task :default => :spec + task default: :spec RAKEFILE expect(bundled_app("foobar/Rakefile").read).to eq(rakefile) expect(bundled_app("foobar/Gemfile").read).to include('gem "rspec"') end - it "asks about MIT license" do - global_config "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false" + it "asks about CI service" do + bundle_config_global "BUNDLE_GEM__CI" => nil + + bundle "gem foobar" do |input, _, _| + input.puts "github" + end + + expect(bundled_app("foobar/.github/workflows/main.yml")).to exist + end + + it "asks about MIT license just once" do + bundle_config_global "BUNDLE_GEM__MIT" => nil bundle "config list" @@ -699,32 +2093,45 @@ Usage: "bundle gem NAME [OPTIONS]" end expect(bundled_app("foobar/LICENSE.txt")).to exist + expect(out).to include("Using a MIT license means").once end - it "asks about CoC" do - global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false" + it "asks about CoC just once" do + bundle_config_global "BUNDLE_GEM__COC" => nil bundle "gem foobar" do |input, _, _| input.puts "yes" end expect(bundled_app("foobar/CODE_OF_CONDUCT.md")).to exist + expect(out).to include("Codes of conduct can increase contributions to your project").once + end + + it "asks about CHANGELOG just once" do + bundle_config_global "BUNDLE_GEM__CHANGELOG" => nil + + bundle "gem foobar" do |input, _, _| + input.puts "yes" + end + + expect(bundled_app("foobar/CHANGELOG.md")).to exist + expect(out).to include("A changelog is a file which contains").once end end context "on conflicts with a previously created file" do it "should fail gracefully" do - FileUtils.touch("conflict-foobar") - bundle "gem conflict-foobar" - expect(err).to include("Errno::ENOTDIR") - expect(exitstatus).to eql(32) if exitstatus + FileUtils.touch(bundled_app("conflict-foobar")) + bundle "gem conflict-foobar", raise_on_error: false + expect(err).to eq("Couldn't create a new gem named `conflict-foobar` because there's an existing file named `conflict-foobar`.") + expect(exitstatus).to eql(32) end end context "on conflicts with a previously created directory" do it "should succeed" do - FileUtils.mkdir_p("conflict-foobar/Gemfile") - bundle! "gem conflict-foobar" + FileUtils.mkdir_p(bundled_app("conflict-foobar/Gemfile")) + bundle "gem conflict-foobar" expect(out).to include("file_clash conflict-foobar/Gemfile"). and include "Initializing git repo in #{bundled_app("conflict-foobar")}" end diff --git a/spec/bundler/commands/open_spec.rb b/spec/bundler/commands/open_spec.rb index 8fae4af5b4..664dc58919 100644 --- a/spec/bundler/commands/open_spec.rb +++ b/spec/bundler/commands/open_spec.rb @@ -4,97 +4,155 @@ RSpec.describe "bundle open" do context "when opening a regular gem" do before do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://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" } + 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" => "" } + 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" => "" } + 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" => "" } + 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" => "" } + bundle "open missing", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false expect(err).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) + ref = git.ref_for("main", 11) install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://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}")}") + bundle "open foo", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("editor #{default_bundle_path("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(err).to match(/did you mean rails\?/i) + bundle "open Rails", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false + expect(err).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" } + 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 + it "opens subpath of the gem" do + bundle "open activerecord --path lib/activerecord", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("editor #{default_bundle_path("gems", "activerecord-2.3.2")}/lib/activerecord") + end + + it "opens subpath file of the gem" do + bundle "open activerecord --path lib/version.rb", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("editor #{default_bundle_path("gems", "activerecord-2.3.2")}/lib/version.rb") + end + + it "opens deep subpath of the gem" do + bundle "open activerecord --path lib/active_record", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("editor #{default_bundle_path("gems", "activerecord-2.3.2")}/lib/active_record") + end + + it "requires value for --path arg" do + bundle "open activerecord --path", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false + expect(err).to eq "Cannot specify `--path` option without a value" + end + + it "suggests alternatives for similar-sounding gems when using subpath" do + bundle "open Rails --path README.md", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false + expect(err).to match(/did you mean 'rails'\?/i) + end + + it "suggests alternatives for similar-sounding gems when using deep subpath" do + bundle "open Rails --path some/path/here", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false + expect(err).to match(/did you mean 'rails'\?/i) + end + + it "opens subpath of the short worded gem" do + bundle "open rec --path CHANGELOG.md", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("editor #{default_bundle_path("gems", "activerecord-2.3.2")}/CHANGELOG.md") + end + + it "opens deep subpath of the short worded gem" do + bundle "open rec --path lib/activerecord", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("editor #{default_bundle_path("gems", "activerecord-2.3.2")}/lib/activerecord") + end + + it "opens subpath of the selected matching gem", :readline do + env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" } + bundle "open active --path CHANGELOG.md", env: env do |input, _, _| + input.puts "2" + end + + expect(out).to include("bundler_editor #{default_bundle_path("gems", "activerecord-2.3.2").join("CHANGELOG.md")}") + end + + it "opens deep subpath of the selected matching gem", :readline do env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" } - bundle "open active", :env => env do |input, _, _| + bundle "open active --path lib/activerecord/version.rb", env: env do |input, _, _| input.puts "2" end - expect(out).to match(/bundler_editor #{default_bundle_path('gems', 'activerecord-2.3.2')}\z/) + expect(out).to include("bundler_editor #{default_bundle_path("gems", "activerecord-2.3.2").join("lib", "activerecord", "version.rb")}") + end + + it "select the gem from many match gems", :readline 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 include("bundler_editor #{default_bundle_path("gems", "activerecord-2.3.2")}") end - it "allows selecting exit from many match gems" do + it "allows selecting exit from many match gems", :readline do env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" } - bundle! "open active", :env => env do |input, _, _| + bundle "open active", env: env do |input, _, _| input.puts "0" end end it "performs an automatic bundle install" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rails" gem "foo" G - bundle "config set auto_install 1" - bundle "open rails", :env => { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + 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" => "" } + bundle "open", env: { "EDITOR" => "sh -c 'env'", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false expect(out).not_to include("BUNDLE_GEMFILE=") end end context "when opening a default gem" do let(:default_gems) do - ruby!(<<-RUBY).split("\n") + ruby(<<-RUBY).split("\n") if Gem::Specification.is_a?(Enumerable) puts Gem::Specification.select(&:default_gem?).map(&:name) end @@ -105,12 +163,12 @@ RSpec.describe "bundle open" do skip "No default gems available on this test run" if default_gems.empty? install_gemfile <<-G - gem "json" + source "https://gem.repo1" G end it "throws proper error when trying to open default gem" do - bundle "open json", :env => { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" } + bundle "open json", env: { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" } expect(out).to include("Unable to open json because it's a default gem, so the directory it would normally be installed to does not exist.") end end diff --git a/spec/bundler/commands/outdated_spec.rb b/spec/bundler/commands/outdated_spec.rb index ab54925756..28ed51d61e 100644 --- a/spec/bundler/commands/outdated_spec.rb +++ b/spec/bundler/commands/outdated_spec.rb @@ -1,63 +1,83 @@ # frozen_string_literal: true 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_uri_for(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 + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://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 + 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") + update_git "foo", path: lib_path("foo") + update_git "zebra", path: lib_path("zebra") end - bundle "outdated" + bundle "outdated", raise_on_error: false + + expected_output = <<~TABLE.gsub("x", "\\\h").tr(".", "\.").strip + Gem Current Latest Requested Groups Release Date + activesupport 2.3.5 3.0 = 2.3.5 default + foo 1.0 xxxxxxx 1.0 xxxxxxx >= 0 default + weakling 0.0.3 0.2 ~> 0.0.1 default + zebra 1.0 xxxxxxx 1.0 xxxxxxx >= 0 default + TABLE + + expect(out).to match(Regexp.new(expected_output)) + end + + it "excludes header row from the sorting" do + update_repo2 do + build_gem "AAA", %w[1.0.0 2.0.0] + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "AAA", "1.0.0" + G - 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") + bundle "outdated", raise_on_error: false - # 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) + expected_output = <<~TABLE + Gem Current Latest Requested Groups Release Date + AAA 1.0.0 2.0.0 = 1.0.0 default + TABLE + + expect(out).to include(expected_output.strip) 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") + update_git "foo", path: lib_path("foo") end - bundle "outdated" + bundle "outdated", raise_on_error: false - expect(exitstatus).to_not be_zero if exitstatus + expect(exitstatus).to_not be_zero 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_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "terranova", '8' @@ -69,16 +89,77 @@ RSpec.describe "bundle outdated" do update_repo2 { build_gem "activesupport", "3.0" } update_repo2 { build_gem "terranova", "9" } - bundle "outdated --verbose" - expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5) in groups \"development, test\"") - expect(out).to include("terranova (newest 9, installed 8, requested = 8) in group \"default\"") + bundle "outdated", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + activesupport 2.3.5 3.0 = 2.3.5 development, test + terranova 8 9 = 8 default + TABLE + + expect(out).to end_with(expected_output) + end + end + + describe "with --verbose option" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://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 + + it "shows the location of the latest version's gemspec if installed" do + bundle_config "clean false" + + update_repo2 { build_gem "activesupport", "3.0" } + update_repo2 { build_gem "terranova", "9" } + + install_gemfile <<-G + source "https://gem.repo2" + + gem "terranova", '9' + gem 'activesupport', '2.3.5' + G + + gemfile <<-G + source "https://gem.repo2" + + gem "terranova", '8' + gem 'activesupport', '2.3.5' + G + + bundle "outdated --verbose", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date Path + activesupport 2.3.5 3.0 = 2.3.5 default + terranova 8 9 = 8 default #{default_bundle_path("specifications/terranova-9.gemspec")} + TABLE + + expect(out).to end_with(expected_output) end end describe "with --group option" do before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "weakling", "~> 0.0.1" gem "terranova", '8' @@ -89,64 +170,110 @@ RSpec.describe "bundle outdated" do G end - def test_group_option(group = nil, gems_list_size = 1) + def test_group_option(group) 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 + bundle "outdated --group #{group}", raise_on_error: false end - it "not outdated gems" do + it "works when the bundle is up to date" do bundle "outdated --group" - expect(out).to include("Bundle up to date!") + expect(out).to end_with("Bundle up to date!") + end + + it "works when only out of date gems are not in given group" do + update_repo2 do + build_gem "terranova", "9" + end + bundle "outdated --group development" + expect(out).to end_with("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 (") + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + terranova 8 9 = 8 default + TABLE - expect(out).not_to include("===== Groups \"development, test\" =====") - expect(out).not_to include("activesupport") - expect(out).not_to include("duradura") + expect(out).to end_with(expected_output) end it "returns a sorted list of outdated gems from one group => 'development'" do - test_group_option("development", 2) + test_group_option("development") - expect(out).not_to include("===== Group \"default\" =====") - expect(out).not_to include("terranova (") + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + activesupport 2.3.5 3.0 = 2.3.5 development, test + duradura 7.0 8.0 = 7.0 development, test + TABLE - expect(out).to include("===== Groups \"development, test\" =====") - expect(out).to include("activesupport") - expect(out).to include("duradura") + expect(out).to end_with(expected_output) end it "returns a sorted list of outdated gems from one group => 'test'" do - test_group_option("test", 2) + test_group_option("test") - expect(out).not_to include("===== Group \"default\" =====") - expect(out).not_to include("terranova (") + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + activesupport 2.3.5 3.0 = 2.3.5 development, test + duradura 7.0 8.0 = 7.0 development, test + TABLE - expect(out).to include("===== Groups \"development, test\" =====") - expect(out).to include("activesupport") - expect(out).to include("duradura") + expect(out).to end_with(expected_output) + end + end + + describe "with --groups option and outdated transitive dependencies" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + + build_gem "bar", %w[2.0.0] + + build_gem "bar_dependant", "7.0" do |s| + s.add_dependency "bar", "~> 2.0" + end + end + + install_gemfile <<-G + source "https://gem.repo2" + + gem "bar_dependant", '7.0' + G + + update_repo2 do + build_gem "bar", %w[3.0.0] + end + end + + it "returns a sorted list of outdated gems" do + bundle "outdated --groups", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + bar 2.0.0 3.0.0 + TABLE + + expect(out).to end_with(expected_output) end end describe "with --groups option" do before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "weakling", "~> 0.0.1" gem "terranova", '8' @@ -159,7 +286,7 @@ RSpec.describe "bundle outdated" do it "not outdated gems" do bundle "outdated --groups" - expect(out).to include("Bundle up to date!") + expect(out).to end_with("Bundle up to date!") end it "returns a sorted list of outdated gems by groups" do @@ -169,39 +296,62 @@ RSpec.describe "bundle outdated" do 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("===== Groups \"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)") + bundle "outdated --groups", raise_on_error: false - expect(out).not_to include("weakling (") + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + activesupport 2.3.5 3.0 = 2.3.5 development, test + duradura 7.0 8.0 = 7.0 development, test + terranova 8 9 = 8 default + TABLE - # TODO: check gems order inside the group + expect(out).to end_with(expected_output) end end describe "with --local option" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://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 + end + it "uses local cache to return a list of outdated gems" do update_repo2 do build_gem "activesupport", "2.3.4" end - bundle! "config set clean false" + bundle_config "clean false" install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "activesupport", "2.3.4" G - bundle "outdated --local" + bundle "outdated --local", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + activesupport 2.3.4 2.3.5 = 2.3.4 default + TABLE - expect(out).to include("activesupport (newest 2.3.5, installed 2.3.4, requested = 2.3.4)") + expect(out).to end_with(expected_output) end it "doesn't hit repo2" do - FileUtils.rm_rf(gem_repo2) + FileUtils.rm_r(gem_repo2) bundle "outdated --local" expect(out).not_to match(/Fetching (gem|version|dependency) metadata from/) @@ -211,54 +361,163 @@ RSpec.describe "bundle outdated" do shared_examples_for "a minimal output is desired" do context "and gems are outdated" do before do - update_repo2 do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + build_gem "activesupport", "3.0" build_gem "weakling", "0.2" end + + install_gemfile <<-G + source "https://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 - it "outputs a sorted list of outdated gems with a more minimal format" do + it "outputs a sorted list of outdated gems with a more minimal format to stdout" 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 + + it "outputs progress to stderr" do + subject + expect(err).to include("Fetching gem metadata") + end end context "and no gems are outdated" do - it "has empty output" do + before do + build_repo2 do + build_gem "activesupport", "3.0" + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport", "3.0" + G + end + + it "does not output to stdout" do + subject + expect(out).to be_empty + end + + it "outputs progress to stderr" do subject - expect(out).to eq("") + expect(err).to include("Fetching gem metadata") end end end describe "with --parseable option" do - subject { bundle "outdated --parseable" } + subject { bundle "outdated --parseable", raise_on_error: false } it_behaves_like "a minimal output is desired" end describe "with aliased --porcelain option" do - subject { bundle "outdated --porcelain" } + subject { bundle "outdated --porcelain", raise_on_error: false } it_behaves_like "a minimal output is desired" end describe "with specified gems" do it "returns list of outdated gems" do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://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 + update_repo2 do build_gem "activesupport", "3.0" - update_git "foo", :path => lib_path("foo") + 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") + bundle "outdated foo", raise_on_error: false + + expected_output = <<~TABLE.gsub("x", "\\\h").tr(".", "\.").strip + Gem Current Latest Requested Groups Release Date + foo 1.0 xxxxxxx 1.0 xxxxxxx >= 0 default + TABLE + + expect(out).to match(Regexp.new(expected_output)) + end + + it "does not require gems to be installed" do + build_repo4 do + build_gem "zeitwerk", "1.0.0" + build_gem "zeitwerk", "2.0.0" + end + + gemfile <<-G + source "https://gem.repo4" + gem "zeitwerk" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + zeitwerk (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + zeitwerk + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "outdated zeitwerk", raise_on_error: false + + expected_output = <<~TABLE.tr(".", "\.").strip + Gem Current Latest Requested Groups Release Date + zeitwerk 1.0.0 2.0.0 >= 0 default + TABLE + + expect(out).to match(Regexp.new(expected_output)) + expect(err).to be_empty end end describe "pre-release gems" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://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 + context "without the --pre option" do it "ignores pre-release versions" do update_repo2 do @@ -266,7 +525,8 @@ RSpec.describe "bundle outdated" do end bundle "outdated" - expect(out).not_to include("activesupport (3.0.0.beta > 2.3.5)") + + expect(out).to end_with("Bundle up to date!") end end @@ -276,8 +536,14 @@ RSpec.describe "bundle outdated" 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)") + bundle "outdated --pre", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + activesupport 2.3.5 3.0.0.beta = 2.3.5 default + TABLE + + expect(out).to end_with(expected_output) end end @@ -289,45 +555,98 @@ RSpec.describe "bundle outdated" do end install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://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)") + bundle "outdated", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + activesupport 3.0.0.beta.1 3.0.0.beta.2 = 3.0.0.beta.1 default + TABLE + + expect(out).to end_with(expected_output) end end end - filter_strict_option = Bundler.feature_flag.bundler_2_mode? ? :"filter-strict" : :strict - describe "with --#{filter_strict_option} option" do + describe "with --filter-strict option" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://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 + 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, filter_strict_option => true + bundle :outdated, "filter-strict": true, raise_on_error: false - expect(out).to_not include("activesupport (newest") - expect(out).to include("(newest 0.0.5, installed 0.0.3, requested ~> 0.0.1)") + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + weakling 0.0.3 0.0.5 ~> 0.0.1 default + TABLE + + expect(out).to end_with(expected_output) + end + + it "only reports gems that have a newer version that matches the specified dependency version requirements, using --strict alias" do + update_repo2 do + build_gem "activesupport", "3.0" + build_gem "weakling", "0.0.5" + end + + bundle :outdated, strict: true, raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + weakling 0.0.3 0.0.5 ~> 0.0.1 default + TABLE + + expect(out).to end_with(expected_output) + end + + it "doesn't crash when some deps unused on the current platform" do + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport", platforms: [:ruby_22] + G + + bundle :outdated, "filter-strict": true + + expect(out).to end_with("Bundle up to date!") end it "only reports gem dependencies when they can actually be updated" do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack_middleware", "1.0" + source "https://gem.repo2" + gem "myrack_middleware", "1.0" G - bundle :outdated, filter_strict_option => true + bundle :outdated, "filter-strict": true - expect(out).to_not include("rack (1.2") + expect(out).to end_with("Bundle up to date!") end describe "and filter options" do it "only reports gems that match requirement and patch filter level" do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "activesupport", "~> 2.3" gem "weakling", ">= 0.0.1" G @@ -337,15 +656,19 @@ RSpec.describe "bundle outdated" do build_gem "weakling", "0.0.5" end - bundle :outdated, filter_strict_option => true, "filter-patch" => true + bundle :outdated, :"filter-strict" => true, "filter-patch" => true, :raise_on_error => false - expect(out).to_not include("activesupport (newest") - expect(out).to include("(newest 0.0.5, installed 0.0.3") + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + weakling 0.0.3 0.0.5 >= 0.0.1 default + TABLE + + expect(out).to end_with(expected_output) end it "only reports gems that match requirement and minor filter level" do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "activesupport", "~> 2.3" gem "weakling", ">= 0.0.1" G @@ -355,15 +678,19 @@ RSpec.describe "bundle outdated" do build_gem "weakling", "0.1.5" end - bundle :outdated, filter_strict_option => true, "filter-minor" => true + bundle :outdated, :"filter-strict" => true, "filter-minor" => true, :raise_on_error => false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + weakling 0.0.3 0.1.5 >= 0.0.1 default + TABLE - expect(out).to_not include("activesupport (newest") - expect(out).to include("(newest 0.1.5, installed 0.0.3") + expect(out).to end_with(expected_output) end it "only reports gems that match requirement and major filter level" do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "activesupport", "~> 2.3" gem "weakling", ">= 0.0.1" G @@ -373,75 +700,105 @@ RSpec.describe "bundle outdated" do build_gem "weakling", "1.1.5" end - bundle :outdated, filter_strict_option => true, "filter-major" => true + bundle :outdated, :"filter-strict" => true, "filter-major" => true, :raise_on_error => false - expect(out).to_not include("activesupport (newest") - expect(out).to include("(newest 1.1.5, installed 0.0.3") + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + weakling 0.0.3 1.1.5 >= 0.0.1 default + TABLE + + expect(out).to end_with(expected_output) end end end describe "with invalid gem name" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://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 + it "returns could not find gem name" do - bundle "outdated invalid_gem_name" + bundle "outdated invalid_gem_name", raise_on_error: false expect(err).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 + bundle "outdated invalid_gem_name", raise_on_error: false + expect(exitstatus).to_not be_zero end end it "performs an automatic bundle install" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "0.9.1" + source "https://gem.repo1" + gem "myrack", "0.9.1" gem "foo" G - bundle "config set auto_install 1" - bundle :outdated + bundle_config "auto_install 1" + bundle :outdated, raise_on_error: false expect(out).to include("Installing foo 1.0") end - context "after bundle install --deployment", :bundler => "< 3" do + context "in deployment mode" do before do - install_gemfile <<-G, forgotten_command_line_options(:deployment => true) - source "#{file_uri_for(gem_repo2)}" + build_repo2 + + gemfile <<-G + source "https://gem.repo2" - gem "rack" + gem "myrack" gem "foo" G + bundle :lock + bundle_config "deployment true" end it "outputs a helpful message about being in deployment mode" do update_repo2 { build_gem "activesupport", "3.0" } - bundle "outdated" + bundle "outdated", raise_on_error: false expect(last_command).to be_failure expect(err).to include("You are trying to check outdated gems in deployment mode.") expect(err).to include("Run `bundle outdated` elsewhere.") expect(err).to include("If this is a development machine, remove the ") - expect(err).to include("Gemfile freeze\nby running `bundle install --no-deployment`.") + expect(err).to include("Gemfile freeze\nby running `bundle config unset deployment`.") end end - context "after bundle config set deployment true" do + context "after bundle config set --local deployment true" do before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" - gem "rack" + gem "myrack" gem "foo" G - bundle! "config set deployment true" + bundle_config "deployment true" end it "outputs a helpful message about being in deployment mode" do update_repo2 { build_gem "activesupport", "3.0" } - bundle "outdated" + bundle "outdated", raise_on_error: false expect(last_command).to be_failure expect(err).to include("You are trying to check outdated gems in deployment mode.") expect(err).to include("Run `bundle outdated` elsewhere.") @@ -452,56 +809,79 @@ RSpec.describe "bundle outdated" do context "update available for a gem on a different platform" do before do + build_repo2 + install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://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!") + expect(out).to end_with("Bundle up to date!") end end context "update available for a gem on the same platform while multiple platforms used for gem" do + before do + build_repo2 + end + it "reports that updates are available if the Ruby platform is used" do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] G bundle "outdated" - expect(out).to include("Bundle up to date!") + expect(out).to end_with("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_uri_for(gem_repo2)}" - gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] - G + it "reports that updates are available if the JRuby platform is used", :jruby_only do + install_gemfile <<-G + source "https://gem.repo2" + gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] + G + + bundle "outdated", raise_on_error: false - 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 + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + laduradura 5.15.2 5.15.3 = 5.15.2 default + TABLE + + expect(out).to end_with(expected_output) 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") + + outdated_gems = out.split("\n").drop_while {|l| !l.start_with?("Gem") }[1..-1] + + expect(outdated_gems.size).to be > 0 end end shared_examples_for "major version updates are detected" do before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://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 + update_repo2 do build_gem "activesupport", "3.3.5" build_gem "weakling", "0.8.0" @@ -513,21 +893,51 @@ RSpec.describe "bundle outdated" do context "when on a new machine" do before do - simulate_new_machine + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end - update_git "foo", :path => lib_path("foo") + install_gemfile <<-G + source "https://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 + + pristine_system_gems + + 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" } + subject { bundle "outdated", raise_on_error: false } it_behaves_like "version update is detected" end shared_examples_for "minor version updates are detected" do before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://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 + update_repo2 do build_gem "activesupport", "2.7.5" build_gem "weakling", "2.0.1" @@ -539,6 +949,21 @@ RSpec.describe "bundle outdated" do shared_examples_for "patch version updates are detected" do before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://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 + update_repo2 do build_gem "activesupport", "2.3.7" build_gem "weakling", "0.3.1" @@ -551,15 +976,27 @@ RSpec.describe "bundle outdated" do 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") + expect(out).to end_with("updates to display.") end end shared_examples_for "major version is ignored" do before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://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 + update_repo2 do build_gem "activesupport", "3.3.5" build_gem "weakling", "1.0.1" @@ -571,6 +1008,21 @@ RSpec.describe "bundle outdated" do shared_examples_for "minor version is ignored" do before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://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 + update_repo2 do build_gem "activesupport", "2.4.5" build_gem "weakling", "0.3.1" @@ -582,6 +1034,21 @@ RSpec.describe "bundle outdated" do shared_examples_for "patch version is ignored" do before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://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 + update_repo2 do build_gem "activesupport", "2.3.6" build_gem "weakling", "0.0.4" @@ -592,7 +1059,7 @@ RSpec.describe "bundle outdated" do end describe "with --filter-major option" do - subject { bundle "outdated --filter-major" } + subject { bundle "outdated --filter-major", raise_on_error: false } it_behaves_like "major version updates are detected" it_behaves_like "minor version is ignored" @@ -600,7 +1067,7 @@ RSpec.describe "bundle outdated" do end describe "with --filter-minor option" do - subject { bundle "outdated --filter-minor" } + subject { bundle "outdated --filter-minor", raise_on_error: false } it_behaves_like "minor version updates are detected" it_behaves_like "major version is ignored" @@ -608,7 +1075,7 @@ RSpec.describe "bundle outdated" do end describe "with --filter-patch option" do - subject { bundle "outdated --filter-patch" } + subject { bundle "outdated --filter-patch", raise_on_error: false } it_behaves_like "patch version updates are detected" it_behaves_like "major version is ignored" @@ -616,7 +1083,7 @@ RSpec.describe "bundle outdated" do end describe "with --filter-minor --filter-patch options" do - subject { bundle "outdated --filter-minor --filter-patch" } + subject { bundle "outdated --filter-minor --filter-patch", raise_on_error: false } it_behaves_like "minor version updates are detected" it_behaves_like "patch version updates are detected" @@ -624,7 +1091,7 @@ RSpec.describe "bundle outdated" do end describe "with --filter-major --filter-minor options" do - subject { bundle "outdated --filter-major --filter-minor" } + subject { bundle "outdated --filter-major --filter-minor", raise_on_error: false } it_behaves_like "major version updates are detected" it_behaves_like "minor version updates are detected" @@ -632,7 +1099,7 @@ RSpec.describe "bundle outdated" do end describe "with --filter-major --filter-patch options" do - subject { bundle "outdated --filter-major --filter-patch" } + subject { bundle "outdated --filter-major --filter-patch", raise_on_error: false } it_behaves_like "major version updates are detected" it_behaves_like "patch version updates are detected" @@ -640,7 +1107,7 @@ RSpec.describe "bundle outdated" do end describe "with --filter-major --filter-minor --filter-patch options" do - subject { bundle "outdated --filter-major --filter-minor --filter-patch" } + subject { bundle "outdated --filter-major --filter-minor --filter-patch", raise_on_error: false } it_behaves_like "major version updates are detected" it_behaves_like "minor version updates are detected" @@ -648,106 +1115,125 @@ RSpec.describe "bundle outdated" do 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 + 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_uri_for(gem_repo4)}" + # establish a lockfile set to 1.0.0 + install_gemfile <<-G + source "https://gem.repo4" gem 'patch', '1.0.0' gem 'minor', '1.0.0' gem 'major', '1.0.0' - G + G - # remove 1.4.3 requirement and bar altogether - # to setup update specs below - gemfile <<-G - source "#{file_uri_for(gem_repo4)}" + # remove all version requirements + gemfile <<-G + source "https://gem.repo4" gem 'patch' gem 'minor' gem 'major' - G - end + G + end - it "shows nothing when patching and filtering to minor" do - bundle "outdated --patch --filter-minor" + 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 + expect(out).to end_with("No minor updates to display.") + end - it "shows all gems when patching and filtering to patch" do - bundle "outdated --patch --filter-patch" + it "shows all gems when patching and filtering to patch" do + bundle "outdated --patch --filter-patch", raise_on_error: false - 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 + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + major 1.0.0 1.0.1 >= 0 default + minor 1.0.0 1.0.1 >= 0 default + patch 1.0.0 1.0.1 >= 0 default + TABLE - it "shows minor and major when updating to minor and filtering to patch and minor" do - bundle "outdated --minor --filter-minor" + expect(out).to end_with(expected_output) + end - 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 and major when updating to minor and filtering to patch and minor" do + bundle "outdated --minor --filter-minor", raise_on_error: false - it "shows minor when updating to major and filtering to minor with parseable" do - bundle "outdated --major --filter-minor --parseable" + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + major 1.0.0 1.1.0 >= 0 default + minor 1.0.0 1.1.0 >= 0 default + TABLE - expect(out).not_to include("patch (newest") - expect(out).to include("minor (newest") - expect(out).not_to include("major (newest") - end + expect(out).to end_with(expected_output) 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 + it "shows minor when updating to major and filtering to minor with parseable" do + bundle "outdated --major --filter-minor --parseable", raise_on_error: false - # establish a lockfile set to 1.4.3 - install_gemfile <<-G - source "#{file_uri_for(gem_repo4)}" - gem 'foo', '1.4.3' - gem 'bar', '2.0.3' - gem 'qux', '1.0.0' - G + expect(out).not_to include("patch (newest") + expect(out).to include("minor (newest") + expect(out).not_to include("major (newest") + end + end - # remove 1.4.3 requirement and bar altogether - # to setup update specs below - gemfile <<-G - source "#{file_uri_for(gem_repo4)}" - gem 'foo' - gem 'qux' - G + context "tricky 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.1.0 2.0.0] end - it "shows gems with update-strict updating to patch and filtering to patch" do - bundle "outdated --patch --update-strict --filter-patch" + # establish a lockfile set to 1.4.3 + install_gemfile <<-G + source "https://gem.repo4" + gem 'foo', '1.4.3' + gem 'bar', '2.0.3' + gem 'qux', '1.0.0' + G - 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 + # remove 1.4.3 requirement and bar altogether + # to setup update specs below + gemfile <<-G + source "https://gem.repo4" + gem 'foo' + gem 'qux' + G + end + + it "shows gems updating to patch and filtering to patch" do + bundle "outdated --patch --filter-patch", raise_on_error: false, env: { "DEBUG_RESOLVER" => "1" } + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + bar 2.0.3 2.0.5 + foo 1.4.3 1.4.4 >= 0 default + TABLE + + expect(out).to end_with(expected_output) + end + + it "shows gems updating to patch and filtering to patch, in debug mode" do + bundle "outdated --patch --filter-patch", raise_on_error: false, env: { "DEBUG" => "1" } + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date Path + bar 2.0.3 2.0.5 + foo 1.4.3 1.4.4 >= 0 default + TABLE + + expect(out).to end_with(expected_output) end end @@ -761,20 +1247,123 @@ RSpec.describe "bundle outdated" do end install_gemfile <<-G - source "#{file_uri_for(gem_repo4)}" + source "https://gem.repo4" gem 'weakling', '0.2' gem 'bar', '2.1' G gemfile <<-G - source "#{file_uri_for(gem_repo4)}" + source "https://gem.repo4" gem 'weakling' G - bundle "outdated --only-explicit" + bundle "outdated --only-explicit", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + weakling 0.2 0.3 >= 0 default + TABLE + + expect(out).to end_with(expected_output) + end + end + + describe "with a multiplatform lockfile" do + before do + build_repo4 do + build_gem "nokogiri", "1.11.1" + build_gem "nokogiri", "1.11.1" do |s| + s.platform = Bundler.local_platform + end + + build_gem "nokogiri", "1.11.2" + build_gem "nokogiri", "1.11.2" do |s| + s.platform = Bundler.local_platform + end + end + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.11.1) + nokogiri (1.11.1-#{Bundler.local_platform}) + + PLATFORMS + ruby + #{Bundler.local_platform} + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile <<-G + source "https://gem.repo4" + gem "nokogiri" + G + end + + it "reports a single entry per gem" do + bundle "outdated", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + nokogiri 1.11.1 1.11.2 >= 0 default + TABLE + + expect(out).to end_with(expected_output) + end + end + + context "when a gem is no longer a dependency after a full update" do + before do + build_repo4 do + build_gem "mini_portile2", "2.5.2" do |s| + s.add_dependency "net-ftp", "~> 0.1" + end + + build_gem "mini_portile2", "2.5.3" + + build_gem "net-ftp", "0.1.2" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "mini_portile2" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + mini_portile2 (2.5.2) + net-ftp (~> 0.1) + net-ftp (0.1.2) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + mini_portile2 + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "works" do + bundle "outdated", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + mini_portile2 2.5.2 2.5.3 >= 0 default + TABLE - expect(out).to include("weakling (newest 0.3") - expect(out).not_to include("bar (newest 2.2") + expect(out).to end_with(expected_output) end end end diff --git a/spec/bundler/commands/platform_spec.rb b/spec/bundler/commands/platform_spec.rb new file mode 100644 index 0000000000..9d7354c54f --- /dev/null +++ b/spec/bundler/commands/platform_spec.rb @@ -0,0 +1,1260 @@ +# frozen_string_literal: true + +RSpec.describe "bundle platform" do + context "without flags" do + it "returns all the output" do + gemfile <<-G + source "https://gem.repo1" + + #{ruby_version_correct} + + gem "foo" + G + + bundle "platform" + expect(out).to eq(<<-G.chomp) +Your platform is: #{Gem::Platform.local} + +Your app has gems that work on these platforms: +* #{local_platform} + +Your Gemfile specifies a Ruby version requirement: +* ruby #{Gem.ruby_version} + +Your current platform satisfies the Ruby version requirement. +G + end + + it "returns all the output including the patchlevel" do + gemfile <<-G + source "https://gem.repo1" + + #{ruby_version_correct_patchlevel} + + gem "foo" + G + + bundle "platform" + expect(out).to eq(<<-G.chomp) +Your platform is: #{Gem::Platform.local} + +Your app has gems that work on these platforms: +* #{local_platform} + +Your Gemfile specifies a Ruby version requirement: +* #{Bundler::RubyVersion.system.single_version_string} + +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 "https://gem.repo1" + + gem "foo" + G + + bundle "platform" + expect(out).to eq(<<-G.chomp) +Your platform is: #{Gem::Platform.local} + +Your app has gems that work on these platforms: +* #{local_platform} + +Your Gemfile does not specify a Ruby version requirement. +G + end + + it "doesn't match the ruby version requirement" do + gemfile <<-G + source "https://gem.repo1" + + #{ruby_version_incorrect} + + gem "foo" + G + + bundle "platform" + expect(out).to eq(<<-G.chomp) +Your platform is: #{Gem::Platform.local} + +Your app has gems that work on these platforms: +* #{local_platform} + +Your Gemfile specifies a Ruby version requirement: +* ruby #{not_local_ruby_version} + +Your Ruby version is #{Gem.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 "https://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 "https://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 "https://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 "https://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 "handles truffleruby" do + gemfile <<-G + source "https://gem.repo1" + ruby "2.5.1", :engine => 'truffleruby', :engine_version => '1.0.0-rc6' + + gem "foo" + G + + bundle "platform --ruby" + + expect(out).to eq("ruby 2.5.1 (truffleruby 1.0.0-rc6)") + end + + it "raises an error if engine is used but engine version is not" do + gemfile <<-G + source "https://gem.repo1" + ruby "1.8.7", :engine => 'rbx' + + gem "foo" + G + + bundle "platform", raise_on_error: false + + expect(exitstatus).not_to eq(0) + end + + it "raises an error if engine_version is used but engine is not" do + gemfile <<-G + source "https://gem.repo1" + ruby "1.8.7", :engine_version => '1.2.4' + + gem "foo" + G + + bundle "platform", raise_on_error: false + + expect(exitstatus).not_to eq(0) + end + + it "raises an error if engine version doesn't match ruby version for MRI" do + gemfile <<-G + source "https://gem.repo1" + ruby "1.8.7", :engine => 'ruby', :engine_version => '1.2.4' + + gem "foo" + G + + bundle "platform", raise_on_error: false + + expect(exitstatus).not_to eq(0) + end + + it "should print if no ruby version is specified" do + gemfile <<-G + source "https://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 + source "https://gem.repo1" + ruby "< 1.8.7" + G + + lockfile <<-L + GEM + remote: https://gem.repo1/ + 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.0") + end + + it "handles when there is a lockfile with no requirement" do + gemfile <<-G + source "https://gem.repo1" + G + + lockfile <<-L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + ruby + + DEPENDENCIES + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "platform --ruby" + expect(out).to eq("No ruby version specified") + end + + it "handles when there is a requirement in the gemfile" do + gemfile <<-G + source "https://gem.repo1" + 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 + source "https://gem.repo1" + 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 \"#{Gem.ruby_version}\", :engine => \"#{local_ruby_engine}\", :engine_version => \"#{local_engine_version}\"" } + let(:ruby_version_correct_engineless) { "ruby \"#{Gem.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 \"#{Gem.ruby_version}\", :engine => \"#{not_local_tag}\", :engine_version => \"#{Gem.ruby_version}\"" } + let(:engine_version_incorrect) { "ruby \"#{Gem.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) + expect(err).to be_include("Your Ruby version is #{Gem.ruby_version}, but your Gemfile specified #{not_local_ruby_version}") + end + + def should_be_engine_incorrect + expect(exitstatus).to eq(18) + expect(err).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) + expect(err).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_ignore_patchlevel + expect(exitstatus).to eq(0) + expect(err).to eq("") + end + + def should_be_patchlevel_fixnum + expect(exitstatus).to eq(18) + expect(err).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 "https://gem.repo1" + gem "myrack" + + #{ruby_version_correct} + G + + expect(bundled_app_lock).to exist + end + + it "installs fine with any engine", :jruby_only do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + + #{ruby_version_correct_engineless} + G + + expect(bundled_app_lock).to exist + end + + it "installs fine when the patchlevel matches" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + + #{ruby_version_correct_patchlevel} + G + + expect(bundled_app_lock).to exist + end + + it "doesn't install when the ruby version doesn't match" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "myrack" + + #{ruby_version_incorrect} + G + + expect(bundled_app_lock).not_to exist + should_be_ruby_version_incorrect + end + + it "doesn't install when engine doesn't match" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "myrack" + + #{engine_incorrect} + G + + expect(bundled_app_lock).not_to exist + should_be_engine_incorrect + end + + it "doesn't install when engine version doesn't match", :jruby_only do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "myrack" + + #{engine_version_incorrect} + G + + expect(bundled_app_lock).not_to exist + should_be_engine_version_incorrect + end + + it "does install even when patchlevel doesn't match" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "myrack" + + #{patchlevel_incorrect} + G + + expect(bundled_app_lock).to exist + should_ignore_patchlevel + end + end + + context "bundle check" do + it "checks fine when the ruby version matches" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + + #{ruby_version_correct} + G + + bundle :check + expect(out).to match(/\AResolving dependencies\.\.\.\.*\nThe Gemfile's dependencies are satisfied\z/) + end + + it "checks fine with any engine", :jruby_only do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + + #{ruby_version_correct_engineless} + G + + bundle :check + expect(out).to match(/\AResolving dependencies\.\.\.\.*\nThe Gemfile's dependencies are satisfied\z/) + end + + it "fails when ruby version doesn't match" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + + #{ruby_version_incorrect} + G + + bundle :check, raise_on_error: false + should_be_ruby_version_incorrect + end + + it "fails when engine doesn't match" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + + #{engine_incorrect} + G + + bundle :check, raise_on_error: false + should_be_engine_incorrect + end + + it "fails when engine version doesn't match", :jruby_only do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + + #{engine_version_incorrect} + G + + bundle :check, raise_on_error: false + should_be_engine_version_incorrect + end + + it "checks fine even when patchlevel doesn't match" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + + #{patchlevel_incorrect} + G + + bundle :check + should_ignore_patchlevel + end + end + + context "bundle update" do + before do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + G + end + + it "updates successfully when the ruby version matches" do + gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + + #{ruby_version_correct} + G + update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + + build_gem "activesupport", "3.0" + end + + bundle "update", all: true + expect(the_bundle).to include_gems "myrack 1.2", "myrack-obama 1.0", "activesupport 3.0" + end + + it "updates fine with any engine", :jruby_only do + gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + + #{ruby_version_correct_engineless} + G + update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + + build_gem "activesupport", "3.0" + end + + bundle "update", all: true + expect(the_bundle).to include_gems "myrack 1.2", "myrack-obama 1.0", "activesupport 3.0" + end + + it "fails when ruby version doesn't match" do + gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + + #{ruby_version_incorrect} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle :update, all: true, raise_on_error: false + should_be_ruby_version_incorrect + end + + it "fails when ruby engine doesn't match", :jruby_only do + gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + + #{engine_incorrect} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle :update, all: true, raise_on_error: false + should_be_engine_incorrect + end + + it "fails when ruby engine version doesn't match", :jruby_only do + gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + + #{engine_version_incorrect} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle :update, all: true, raise_on_error: false + should_be_engine_version_incorrect + end + + it "updates fine even when patchlevel doesn't match" do + gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + + #{patchlevel_incorrect} + G + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle :update, all: true + should_ignore_patchlevel + expect(the_bundle).to include_gems "activesupport 3.0" + end + end + + context "bundle info" do + before do + install_gemfile <<-G + source "https://gem.repo1" + gem "rails" + G + end + + it "prints path if ruby version is correct" do + install_gemfile <<-G + source "https://gem.repo1" + gem "rails" + + #{ruby_version_correct} + G + + bundle "info rails --path" + 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", :jruby_only do + install_gemfile <<-G + source "https://gem.repo1" + gem "rails" + + #{ruby_version_correct_engineless} + G + + bundle "info rails --path" + expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s) + end + + it "fails if ruby version doesn't match" do + gemfile <<-G + source "https://gem.repo1" + gem "rails" + + #{ruby_version_incorrect} + G + + bundle "show rails", raise_on_error: false + should_be_ruby_version_incorrect + end + + it "fails if engine doesn't match" do + gemfile <<-G + source "https://gem.repo1" + gem "rails" + + #{engine_incorrect} + G + + bundle "show rails", raise_on_error: false + should_be_engine_incorrect + end + + it "fails if engine version doesn't match", jruby_only: true do + gemfile <<-G + source "https://gem.repo1" + gem "rails" + + #{engine_version_incorrect} + G + + bundle "show rails", raise_on_error: false + should_be_engine_version_incorrect + end + + it "prints path even when patchlevel doesn't match" do + gemfile <<-G + source "https://gem.repo1" + gem "rails" + + #{patchlevel_incorrect} + G + + bundle "show rails" + should_ignore_patchlevel + expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s) + end + end + + context "bundle cache" do + before do + install_gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + G + end + + it "copies the .gem file to vendor/cache when ruby version matches" do + gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + + #{ruby_version_correct} + G + + bundle :cache + expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist + end + + it "copies the .gem file to vendor/cache when ruby version matches for any engine", :jruby_only do + install_gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + + #{ruby_version_correct_engineless} + G + + bundle :cache + expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist + end + + it "fails if the ruby version doesn't match" do + gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + + #{ruby_version_incorrect} + G + + bundle :cache, raise_on_error: false + should_be_ruby_version_incorrect + end + + it "fails if the engine doesn't match" do + gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + + #{engine_incorrect} + G + + bundle :cache, raise_on_error: false + should_be_engine_incorrect + end + + it "fails if the engine version doesn't match", :jruby_only do + gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + + #{engine_version_incorrect} + G + + bundle :cache, raise_on_error: false + should_be_engine_version_incorrect + end + + it "copies the .gem file to vendor/cache even when patchlevel doesn't match" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + + #{patchlevel_incorrect} + G + + bundle :cache + should_ignore_patchlevel + expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist + end + end + + context "bundle pack" do + before do + install_gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + G + end + + it "copies the .gem file to vendor/cache when ruby version matches" do + gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + + #{ruby_version_correct} + G + + bundle :cache + expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist + end + + it "copies the .gem file to vendor/cache when ruby version matches any engine", :jruby_only do + install_gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + + #{ruby_version_correct_engineless} + G + + bundle :cache + expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist + end + + it "fails if the ruby version doesn't match" do + gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + + #{ruby_version_incorrect} + G + + bundle :cache, raise_on_error: false + should_be_ruby_version_incorrect + end + + it "fails if the engine doesn't match" do + gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + + #{engine_incorrect} + G + + bundle :cache, raise_on_error: false + should_be_engine_incorrect + end + + it "fails if the engine version doesn't match", :jruby_only do + gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + + #{engine_version_incorrect} + G + + bundle :cache, raise_on_error: false + should_be_engine_version_incorrect + end + + it "copies the .gem file to vendor/cache even when patchlevel doesn't match" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + + #{patchlevel_incorrect} + G + + bundle :cache + should_ignore_patchlevel + expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist + end + end + + context "bundle exec" do + before do + ENV["BUNDLER_FORCE_TTY"] = "true" + system_gems "myrack-1.0.0", "myrack-0.9.1", path: default_bundle_path + end + + it "activates the correct gem when ruby version matches" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + + #{ruby_version_correct} + G + + bundle "exec myrackup" + expect(out).to include("0.9.1") + end + + it "activates the correct gem when ruby version matches any engine", :jruby_only do + system_gems "myrack-1.0.0", "myrack-0.9.1", path: default_bundle_path + gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + + #{ruby_version_correct_engineless} + G + + bundle "exec myrackup" + expect(out).to include("0.9.1") + end + + it "fails when the ruby version doesn't match" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + + #{ruby_version_incorrect} + G + + bundle "exec myrackup", raise_on_error: false + should_be_ruby_version_incorrect + end + + it "fails when the engine doesn't match" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + + #{engine_incorrect} + G + + bundle "exec myrackup", raise_on_error: false + should_be_engine_incorrect + end + + it "fails when the engine version doesn't match", :jruby_only do + gemfile <<-G + gem "myrack", "0.9.1" + + #{engine_version_incorrect} + G + + bundle "exec myrackup", raise_on_error: false + should_be_engine_version_incorrect + end + + it "activates the correct gem even when patchlevel doesn't match" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + + #{patchlevel_incorrect} + G + + bundle "exec myrackup" + should_ignore_patchlevel + expect(out).to include("1.0.0") + end + end + + context "bundle console" do + before do + build_repo2 do + build_dummy_irb + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "irb" + gem "myrack" + gem "activesupport", :group => :test + gem "myrack_middleware", :group => :development + G + end + + it "starts IRB with the default group loaded when ruby version matches", :readline do + gemfile gemfile + "\n\n#{ruby_version_correct}\n" + + bundle "console" do |input, _, _| + input.puts("puts MYRACK") + input.puts("exit") + end + expect(out).to include("0.9.1") + end + + it "starts IRB with the default group loaded when ruby version matches", :readline, :jruby_only do + gemfile gemfile + "\n\n#{ruby_version_correct_engineless}\n" + + bundle "console" do |input, _, _| + input.puts("puts MYRACK") + input.puts("exit") + end + expect(out).to include("0.9.1") + end + + it "fails when ruby version doesn't match" do + gemfile gemfile + "\n\n#{ruby_version_incorrect}\n" + + bundle "console", raise_on_error: false + should_be_ruby_version_incorrect + end + + it "fails when engine doesn't match" do + gemfile gemfile + "\n\n#{engine_incorrect}\n" + + bundle "console", raise_on_error: false + should_be_engine_incorrect + end + + it "fails when engine version doesn't match", :jruby_only do + gemfile gemfile + "\n\n#{engine_version_incorrect}\n" + + bundle "console", raise_on_error: false + should_be_engine_version_incorrect + end + + it "starts IRB with the default group loaded even when patchlevel doesn't match", :readline do + gemfile gemfile + "\n\n#{patchlevel_incorrect}\n" + + bundle "console" do |input, _, _| + input.puts("puts MYRACK") + input.puts("exit") + end + should_ignore_patchlevel + expect(out).to include("0.9.1") + end + end + + context "Bundler.setup" do + before do + install_gemfile <<-G + source "https://gem.repo1" + gem "yard" + gem "myrack", :group => :test + G + + ENV["BUNDLER_FORCE_TTY"] = "true" + end + + it "makes a Gemfile.lock if setup succeeds" do + install_gemfile <<-G + source "https://gem.repo1" + gem "yard" + gem "myrack" + + #{ruby_version_correct} + G + + FileUtils.rm(bundled_app_lock) + + run "1" + expect(bundled_app_lock).to exist + end + + it "makes a Gemfile.lock if setup succeeds for any engine", :jruby_only do + install_gemfile <<-G + source "https://gem.repo1" + gem "yard" + gem "myrack" + + #{ruby_version_correct_engineless} + G + + FileUtils.rm(bundled_app_lock) + + run "1" + expect(bundled_app_lock).to exist + end + + it "fails when ruby version doesn't match" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "yard" + gem "myrack" + + #{ruby_version_incorrect} + G + + FileUtils.rm(bundled_app_lock) + + ruby "require 'bundler/setup'", env: { "BUNDLER_VERSION" => Bundler::VERSION }, raise_on_error: false + + expect(bundled_app_lock).not_to exist + should_be_ruby_version_incorrect + end + + it "fails when engine doesn't match" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "yard" + gem "myrack" + + #{engine_incorrect} + G + + FileUtils.rm(bundled_app_lock) + + ruby "require 'bundler/setup'", env: { "BUNDLER_VERSION" => Bundler::VERSION }, raise_on_error: false + + expect(bundled_app_lock).not_to exist + should_be_engine_incorrect + end + + it "fails when engine version doesn't match", :jruby_only do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "yard" + gem "myrack" + + #{engine_version_incorrect} + G + + FileUtils.rm(bundled_app_lock) + + ruby "require 'bundler/setup'", env: { "BUNDLER_VERSION" => Bundler::VERSION }, raise_on_error: false + + expect(bundled_app_lock).not_to exist + should_be_engine_version_incorrect + end + + it "makes a Gemfile.lock even when patchlevel doesn't match" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "yard" + gem "myrack" + + #{patchlevel_incorrect} + G + + FileUtils.rm(bundled_app_lock) + + ruby "require 'bundler/setup'", env: { "BUNDLER_VERSION" => Bundler::VERSION } + + should_ignore_patchlevel + expect(bundled_app_lock).to exist + end + end + + context "bundle outdated" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + end + + install_gemfile <<-G + source "https://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 "https://gem.repo2" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{ruby_version_correct} + G + + bundle "outdated", raise_on_error: false + + expected_output = <<~TABLE.gsub("x", "\\\h").tr(".", "\.").strip + Gem Current Latest Requested Groups Release Date + activesupport 2.3.5 3.0 = 2.3.5 default + foo 1.0 xxxxxxx 1.0 xxxxxxx >= 0 default + TABLE + + expect(out).to match(Regexp.new(expected_output)) + end + + it "returns list of outdated gems when the ruby version matches for any engine", :jruby_only do + bundle :install + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", path: lib_path("foo") + end + + gemfile <<-G + source "https://gem.repo2" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{ruby_version_correct_engineless} + G + + bundle "outdated", raise_on_error: false + + expected_output = <<~TABLE.gsub("x", "\\\h").tr(".", "\.").strip + Gem Current Latest Requested Groups Release Date + activesupport 2.3.5 3.0 = 2.3.5 default + foo 1.0 xxxxxxx 1.0 xxxxxxx >= 0 default + TABLE + + expect(out).to match(Regexp.new(expected_output)) + 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 "https://gem.repo2" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{ruby_version_incorrect} + G + + bundle "outdated", raise_on_error: false + 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 "https://gem.repo2" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{engine_incorrect} + G + + bundle "outdated", raise_on_error: false + should_be_engine_incorrect + end + + it "fails when the engine version doesn't match", :jruby_only do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", path: lib_path("foo") + end + + gemfile <<-G + source "https://gem.repo2" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{engine_version_incorrect} + G + + bundle "outdated", raise_on_error: false + should_be_engine_version_incorrect + end + + it "reports outdated gems even when patchlevel doesn't match" do + update_repo2 do + build_gem "activesupport", "3.0" + update_git "foo", path: lib_path("foo") + end + + gemfile <<-G + source "https://gem.repo2" + gem "activesupport", "2.3.5" + gem "foo", :git => "#{lib_path("foo")}" + + #{patchlevel_incorrect} + G + + bundle "outdated", raise_on_error: false + expect(err).not_to include("patchlevel") + expect(out).to include("activesupport") + expect(out).to include("foo") + end + end +end diff --git a/spec/bundler/commands/post_bundle_message_spec.rb b/spec/bundler/commands/post_bundle_message_spec.rb index 6fd4fb7089..088fc29fe1 100644 --- a/spec/bundler/commands/post_bundle_message_spec.rb +++ b/spec/bundler/commands/post_bundle_message_spec.rb @@ -3,13 +3,13 @@ RSpec.describe "post bundle message" do before :each do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" gem "activesupport", "2.3.5", :group => [:emo, :test] group :test do gem "rspec" end - gem "rack-obama", :group => :obama + gem "myrack-obama", :group => :obama G end @@ -18,192 +18,165 @@ RSpec.describe "post bundle message" do let(:bundle_show_path_message) { "Bundled gems are installed into `#{bundle_path}`" } let(:bundle_complete_message) { "Bundle complete!" } let(:bundle_updated_message) { "Bundle updated!" } - let(:installed_gems_stats) { "4 Gemfile dependencies, 5 gems now installed." } - let(:bundle_show_message) { Bundler::VERSION.split(".").first.to_i < 3 ? bundle_show_system_message : bundle_show_path_message } + let(:installed_gems_stats) { "4 Gemfile dependencies, 4 gems now installed." } - describe "for fresh bundle install" do - it "without any options" do + describe "when installing to system gems" do + before do + bundle_config "path.system true" + end + + it "shows proper messages according to the configured groups" do bundle :install - expect(out).to include(bundle_show_message) + expect(out).to include(bundle_show_system_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, forgotten_command_line_options(:without => "emo") - expect(out).to include(bundle_show_message) - expect(out).to include("Gems in the group emo were not installed") + bundle_config "without emo" + bundle :install + expect(out).to include(bundle_show_system_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, forgotten_command_line_options(:without => "emo test") - expect(out).to include(bundle_show_message) - expect(out).to include("Gems in the groups emo and test were not installed") + bundle_config "without emo test" + bundle :install + expect(out).to include(bundle_show_system_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 + expect(out).to include("4 Gemfile dependencies, 2 gems now installed.") - it "with --without more groups" do - bundle! :install, forgotten_command_line_options(: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") + bundle_config "without emo obama test" + bundle :install + expect(out).to include(bundle_show_system_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.") + expect(out).to include("4 Gemfile dependencies, 1 gem now installed.") end - describe "with --path and" do - let(:bundle_path) { "./vendor" } - + describe "for second bundle install run" do it "without any options" do - bundle! :install, forgotten_command_line_options(:path => "vendor") - expect(out).to include(bundle_show_path_message) - expect(out).to_not include("Gems in the group") + 2.times { bundle :install } + expect(out).to include(bundle_show_system_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 + end + end - it "with --without one group" do - bundle! :install, forgotten_command_line_options(:without => "emo", :path => "vendor") - expect(out).to include(bundle_show_path_message) - expect(out).to include("Gems in the group emo were not installed") - expect(out).to include(bundle_complete_message) - end + describe "with `path` configured" do + let(:bundle_path) { "./vendor" } - it "with --without two groups" do - bundle! :install, forgotten_command_line_options(:without => "emo test", :path => "vendor") - expect(out).to include(bundle_show_path_message) - expect(out).to include("Gems in the groups emo and test were not installed") - expect(out).to include(bundle_complete_message) - end + it "shows proper messages according to the configured groups" do + bundle_config "path vendor" + bundle :install + expect(out).to include(bundle_show_path_message) + expect(out).to_not include("Gems in the group") + expect(out).to include(bundle_complete_message) - it "with --without more groups" do - bundle! :install, forgotten_command_line_options(:without => "emo obama test", :path => "vendor") - expect(out).to include(bundle_show_path_message) - expect(out).to include("Gems in the groups emo, obama and test were not installed") - expect(out).to include(bundle_complete_message) - end + bundle_config "path vendor" + bundle_config "without emo" + bundle :install + expect(out).to include(bundle_show_path_message) + expect(out).to include("Gems in the group 'emo' were not installed") + expect(out).to include(bundle_complete_message) - it "with an absolute --path inside the cwd" do - bundle! :install, forgotten_command_line_options(: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 + bundle_config "path vendor" + bundle_config "without emo test" + bundle :install + expect(out).to include(bundle_show_path_message) + expect(out).to include("Gems in the groups 'emo' and 'test' were not installed") + expect(out).to include(bundle_complete_message) - it "with an absolute --path outside the cwd" do - bundle! :install, forgotten_command_line_options(:path => tmp("not_bundled_app")) - expect(out).to include("Bundled gems are installed into `#{tmp("not_bundled_app")}`") - expect(out).to_not include("Gems in the group") - expect(out).to include(bundle_complete_message) - end + bundle_config "path vendor" + bundle_config "without emo obama test" + bundle :install + expect(out).to include(bundle_show_path_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 "with misspelled or non-existent gem name" do - before do - bundle "config set force_ruby_platform true" - end - - it "should report a helpful error message", :bundler => "< 3" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - gem "not-a-gem", :group => :development - G - expect(err).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", :bundler => "3" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - gem "not-a-gem", :group => :development - G - expect(err).to include <<-EOS.strip -Could not find gem 'not-a-gem' in rubygems repository #{file_uri_for(gem_repo1)}/ or installed locally. -The source does not contain any versions of 'not-a-gem' - EOS - end + describe "with an absolute `path` inside the cwd configured" do + let(:bundle_path) { bundled_app("cache") } - it "should report a helpful error message with reference to cache if available" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - G - bundle :cache - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - gem "not-a-gem", :group => :development - G - expect(err).to include("Could not find gem 'not-a-gem' in"). - and include("or in gems cached in vendor/cache.") - end + it "shows proper messages according to the configured groups" do + bundle_config "path #{bundle_path}" + bundle :install + 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 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") + describe "with `path` configured to an absolute path outside the cwd" do + let(:bundle_path) { tmp("not_bundled_app") } + + it "shows proper messages according to the configured groups" do + bundle_config "path #{bundle_path}" + bundle :install + expect(out).to include("Bundled gems are installed into `#{tmp("not_bundled_app")}`") + expect(out).to_not include("Gems in the group") expect(out).to include(bundle_complete_message) - expect(out).to include(installed_gems_stats) end + end - it "with --without one group" do - bundle! :install, forgotten_command_line_options(: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) + describe "with misspelled or non-existent gem name" do + before do + bundle_config "force_ruby_platform true" end - it "with --without two groups" do - bundle! :install, forgotten_command_line_options(: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) + it "should report a helpful error message" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "myrack" + gem "not-a-gem", :group => :development + G + expect(err).to include <<~EOS.strip + Could not find gem 'not-a-gem' in rubygems repository https://gem.repo1/ or installed locally. + EOS end - it "with --without more groups" do - bundle! :install, forgotten_command_line_options(: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) + it "should report a helpful error message with reference to cache if available" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + bundle :cache + expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "myrack" + gem "not-a-gem", :group => :development + G + expect(err).to include("Could not find gem 'not-a-gem' in"). + and include("or in gems cached in vendor/cache.") end end describe "for bundle update" do - it "without any options" do - bundle! :update, :all => true + it "shows proper messages according to the configured groups" do + bundle :update, all: true 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, forgotten_command_line_options(:without => "emo") - bundle! :update, :all => true - expect(out).to include("Gems in the group emo were not updated") + bundle_config "without emo" + bundle :install + bundle :update, all: true + expect(out).to include("Gems in the group 'emo' were not updated") expect(out).to include(bundle_updated_message) - end - it "with --without two groups" do - bundle! :install, forgotten_command_line_options(:without => "emo test") - bundle! :update, :all => true - expect(out).to include("Gems in the groups emo and test were not updated") + bundle_config "without emo test" + bundle :install + bundle :update, all: true + expect(out).to include("Gems in the groups 'emo' and 'test' were not updated") expect(out).to include(bundle_updated_message) - end - it "with --without more groups" do - bundle! :install, forgotten_command_line_options(:without => "emo obama test") - bundle! :update, :all => true - expect(out).to include("Gems in the groups emo, obama and test were not updated") + bundle_config "without emo obama test" + bundle :install + bundle :update, all: true + expect(out).to include("Gems in the groups 'emo', 'obama' and 'test' were not updated") expect(out).to include(bundle_updated_message) end end diff --git a/spec/bundler/commands/pristine_spec.rb b/spec/bundler/commands/pristine_spec.rb index aa5c2213d1..5f80b9e534 100644 --- a/spec/bundler/commands/pristine_spec.rb +++ b/spec/bundler/commands/pristine_spec.rb @@ -2,9 +2,9 @@ require "bundler/vendored_fileutils" -RSpec.describe "bundle pristine", :ruby_repo do +RSpec.describe "bundle pristine" do before :each do - build_lib "baz", :path => bundled_app do |s| + build_lib "baz", path: bundled_app do |s| s.version = "1.0.0" s.add_development_dependency "baz-dev", "=1.0.0" end @@ -13,26 +13,28 @@ RSpec.describe "bundle pristine", :ruby_repo 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_git "git_with_ext", :path => lib_path("git_with_ext"), &:add_c_extension - build_lib "bar", :path => lib_path("bar") + build_git "foo", path: lib_path("foo") + build_git "git_with_ext", path: lib_path("git_with_ext"), &:add_c_extension + build_lib "bar", path: lib_path("bar") end - install_gemfile! <<-G - source "#{file_uri_for(gem_repo2)}" + install_gemfile <<-G + source "https://gem.repo2" gem "weakling" gem "very_simple_binary" - gem "foo", :git => "#{lib_path("foo")}" + gem "foo", :git => "#{lib_path("foo")}", :branch => "main" gem "git_with_ext", :git => "#{lib_path("git_with_ext")}" gem "bar", :path => "#{lib_path("bar")}" gemspec G + + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) end context "when sourced from RubyGems" do it "reverts using cached .gem file" do - spec = Bundler.definition.specs["weakling"].first + spec = find_spec("weakling") changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt") FileUtils.touch(changes_txt) @@ -43,49 +45,115 @@ RSpec.describe "bundle pristine", :ruby_repo do end it "does not delete the bundler gem" do - system_gems :bundler - bundle! "install" - bundle! "pristine", :system_bundler => true - bundle! "-v", :system_bundler => true - - expected = if Bundler::VERSION < "3.0" - "Bundler version" - else - Bundler::VERSION - end + bundle "install" + bundle "pristine" + bundle "-v" - expect(out).to start_with(expected) + 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 + spec = find_spec("foo") changed_file = Pathname.new(spec.full_gem_path).join("lib/foo.rb") diff = "#Pristine spec changes" File.open(changed_file, "a") {|f| f.puts diff } expect(File.read(changed_file)).to include(diff) - bundle! "pristine" + bundle "pristine" expect(File.read(changed_file)).to_not include(diff) end it "removes added files" do - spec = Bundler.definition.specs["foo"].first + spec = find_spec("foo") changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt") FileUtils.touch(changes_txt) expect(changes_txt).to be_file - bundle! "pristine" + bundle "pristine" expect(changes_txt).not_to be_file end + + it "displays warning and ignores changes when a local config exists" do + spec = find_spec("foo") + bundle_config "local.#{spec.name} #{lib_path(spec.name)}" + + 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 be_file + expect(err).to include("Cannot pristine #{spec.name} (#{spec.version}#{spec.git_version}). Gem is locally overridden.") + end + + it "doesn't run multiple git processes for the same repository" do + nested_gems = [ + "actioncable", + "actionmailer", + "actionpack", + "actionview", + "activejob", + "activemodel", + "activerecord", + "activestorage", + "activesupport", + "railties", + ] + + build_repo2 do + nested_gems.each do |gem| + build_lib gem, path: lib_path("rails/#{gem}") + end + + build_git "rails", path: lib_path("rails") do |s| + nested_gems.each do |gem| + s.add_dependency gem + end + end + end + + install_gemfile <<-G + source 'https://rubygems.org' + + git "#{lib_path("rails")}" do + gem "rails" + gem "actioncable" + gem "actionmailer" + gem "actionpack" + gem "actionview" + gem "activejob" + gem "activemodel" + gem "activerecord" + gem "activestorage" + gem "activesupport" + gem "railties" + end + G + + changed_files = [] + diff = "#Pristine spec changes" + + nested_gems.each do |gem| + spec = find_spec(gem) + changed_files << Pathname.new(spec.full_gem_path).join("lib/#{gem}.rb") + File.open(changed_files.last, "a") {|f| f.puts diff } + end + + bundle "pristine" + + changed_files.each do |changed_file| + expect(File.read(changed_file)).to_not include(diff) + end + 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 + spec = find_spec("baz") changed_file = Pathname.new(spec.full_gem_path).join("lib/baz.rb") diff = "#Pristine spec changes" @@ -98,8 +166,8 @@ RSpec.describe "bundle pristine", :ruby_repo do 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") + spec = find_spec("baz-dev") + 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" } @@ -112,7 +180,7 @@ RSpec.describe "bundle pristine", :ruby_repo do context "when sourced from path" do it "displays warning and ignores changes when sourced from local path" do - spec = Bundler.definition.specs["bar"].first + spec = find_spec("bar") changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt") FileUtils.touch(changes_txt) expect(changes_txt).to be_file @@ -124,22 +192,22 @@ RSpec.describe "bundle pristine", :ruby_repo do context "when passing a list of gems to pristine" do it "resets them" do - foo = Bundler.definition.specs["foo"].first + foo = find_spec("foo") foo_changes_txt = Pathname.new(foo.full_gem_path).join("lib/changes.txt") FileUtils.touch(foo_changes_txt) expect(foo_changes_txt).to be_file - bar = Bundler.definition.specs["bar"].first + bar = find_spec("bar") bar_changes_txt = Pathname.new(bar.full_gem_path).join("lib/changes.txt") FileUtils.touch(bar_changes_txt) expect(bar_changes_txt).to be_file - weakling = Bundler.definition.specs["weakling"].first + weakling = find_spec("weakling") weakling_changes_txt = Pathname.new(weakling.full_gem_path).join("lib/changes.txt") FileUtils.touch(weakling_changes_txt) expect(weakling_changes_txt).to be_file - bundle! "pristine foo bar weakling" + bundle "pristine foo bar weakling" expect(err).to include("Cannot pristine bar (1.0). Gem is sourced from local path.") expect(out).to include("Installing weakling 1.0") @@ -150,42 +218,58 @@ RSpec.describe "bundle pristine", :ruby_repo do end it "raises when one of them is not in the lockfile" do - bundle "pristine abcabcabc" + bundle "pristine abcabcabc", raise_on_error: false expect(err).to include("Could not find gem 'abcabcabc'.") 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(:very_simple_binary) { find_spec("very_simple_binary") } 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 set build.very_simple_binary -- #{build_opt}" } + 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" + 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}/) + expect(makefile_contents).to match(/libpath =.*#{Regexp.escape(c_ext_dir.to_s)}/) + expect(makefile_contents).to match(/LIBPATH =.*-L#{Regexp.escape(c_ext_dir.to_s)}/) end end context "when a build config exists for a git sourced gem" do - let(:git_with_ext) { Bundler.definition.specs["git_with_ext"].first } + let(:git_with_ext) { find_spec("git_with_ext") } let(:c_ext_dir) { Pathname.new(git_with_ext.full_gem_path).join("ext") } let(:build_opt) { "--with-ext-lib=#{c_ext_dir}" } - before { bundle "config set build.git_with_ext -- #{build_opt}" } + before { bundle_config "build.git_with_ext -- #{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" + 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}/) + expect(makefile_contents).to match(/libpath =.*#{Regexp.escape(c_ext_dir.to_s)}/) + expect(makefile_contents).to match(/LIBPATH =.*-L#{Regexp.escape(c_ext_dir.to_s)}/) + end + end + + context "when BUNDLE_GEMFILE doesn't exist" do + before do + bundle "pristine", env: { "BUNDLE_GEMFILE" => "does/not/exist" }, raise_on_error: false + end + + it "shows a meaningful error" do + expect(err).to eq("#{bundled_app("does/not/exist")} not found") + end + end + + def find_spec(name) + without_env_side_effects do + Bundler.definition.specs[name].first end end end diff --git a/spec/bundler/commands/remove_spec.rb b/spec/bundler/commands/remove_spec.rb index 402faaf1f3..8a2e6778ea 100644 --- a/spec/bundler/commands/remove_spec.rb +++ b/spec/bundler/commands/remove_spec.rb @@ -4,27 +4,42 @@ RSpec.describe "bundle remove" do context "when no gems are specified" do it "throws error" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" G - bundle "remove" + bundle "remove", raise_on_error: false expect(err).to include("Please specify gems to remove.") end end - context "when --install flag is specified" do - it "removes gems from .bundle" do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + context "after 'bundle install' is run" do + describe "running 'bundle remove GEM_NAME'" do + it "removes it from the lockfile" do + myrack_dep = <<~L - gem "rack" - G + DEPENDENCIES + myrack + + L + + gemfile <<-G + source "https://gem.repo1" - bundle! "remove rack --install" + gem "myrack" + G + + bundle "install" + + expect(lockfile).to include(myrack_dep) - expect(out).to include("rack was removed.") - expect(the_bundle).to_not include_gems "rack" + bundle "remove myrack" + + expect(gemfile).to eq <<~G + source "https://gem.repo1" + G + expect(lockfile).to_not include(myrack_dep) + end end end @@ -32,49 +47,76 @@ RSpec.describe "bundle remove" do context "when gem is present in gemfile" do it "shows success for removed gem" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack" + gem "myrack" G - bundle! "remove rack" + bundle "remove myrack" - expect(out).to include("rack was removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(out).to include("myrack was removed.") + expect(the_bundle).to_not include_gems "myrack" + expect(gemfile).to eq <<~G + source "https://gem.repo1" G end + + context "when gem is specified in multiple lines" do + it "shows success for removed gem" do + build_git "myrack" + + gemfile <<-G + source 'https://gem.repo1' + + gem 'git' + gem 'myrack', + git: "#{lib_path("myrack-1.0")}", + branch: 'main' + gem 'nokogiri' + G + + bundle "remove myrack" + + expect(out).to include("myrack was removed.") + expect(gemfile).to eq <<~G + source 'https://gem.repo1' + + gem 'git' + gem 'nokogiri' + G + end + end end context "when gem is not present in gemfile" do it "shows warning for gem that could not be removed" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" G - bundle "remove rack" + bundle "remove myrack", raise_on_error: false - expect(err).to include("`rack` is not specified in #{bundled_app("Gemfile")} so it could not be removed.") + expect(err).to include("`myrack` is not specified in #{bundled_app_gemfile} so it could not be removed.") end end end - describe "remove mutiple gems from gemfile" do + describe "remove multiple gems from gemfile" do context "when all gems are present in gemfile" do it "shows success fir all removed gems" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack" + gem "myrack" gem "rails" G - bundle! "remove rack rails" + bundle "remove myrack rails" - expect(out).to include("rack was removed.") + expect(out).to include("myrack was removed.") expect(out).to include("rails was removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(gemfile).to eq <<~G + source "https://gem.repo1" G end end @@ -82,18 +124,18 @@ RSpec.describe "bundle remove" do context "when some gems are not present in the gemfile" do it "shows warning for those not present and success for those that can be removed" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rails" gem "minitest" gem "rspec" G - bundle "remove rails rack minitest" + bundle "remove rails myrack minitest", raise_on_error: false - expect(err).to include("`rack` is not specified in #{bundled_app("Gemfile")} so it could not be removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(err).to include("`myrack` is not specified in #{bundled_app_gemfile} so it could not be removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" gem "rails" gem "minitest" @@ -106,16 +148,16 @@ RSpec.describe "bundle remove" do context "with inline groups" do it "removes the specified gem" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack", :group => [:dev] + gem "myrack", :group => [:dev] G - bundle! "remove rack" + bundle "remove myrack" - expect(out).to include("rack was removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(out).to include("myrack was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" G end end @@ -124,18 +166,18 @@ RSpec.describe "bundle remove" do context "when single group block with gem to be removed is present" do it "removes the group block" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" group :test do gem "rspec" end G - bundle! "remove rspec" + bundle "remove rspec" expect(out).to include("rspec was removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(gemfile).to eq <<~G + source "https://gem.repo1" G end end @@ -143,19 +185,19 @@ RSpec.describe "bundle remove" do context "when gem to be removed is outside block" do it "does not modify group" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack" + gem "myrack" group :test do gem "coffee-script-source" end G - bundle! "remove rack" + bundle "remove myrack" - expect(out).to include("rack was removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(out).to include("myrack was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" group :test do gem "coffee-script-source" @@ -167,7 +209,7 @@ RSpec.describe "bundle remove" do context "when an empty block is also present" do it "removes all empty blocks" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" group :test do gem "rspec" @@ -177,38 +219,38 @@ RSpec.describe "bundle remove" do end G - bundle! "remove rspec" + bundle "remove rspec" expect(out).to include("rspec was removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(gemfile).to eq <<~G + source "https://gem.repo1" G end end - context "when the gem belongs to mutiple groups" do + context "when the gem belongs to multiple groups" do it "removes the groups" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" group :test, :serioustest do gem "rspec" end G - bundle! "remove rspec" + bundle "remove rspec" expect(out).to include("rspec was removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(gemfile).to eq <<~G + source "https://gem.repo1" G end end - context "when the gem is present in mutiple groups" do + context "when the gem is present in multiple groups" do it "removes all empty blocks" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" group :one do gem "rspec" @@ -219,11 +261,11 @@ RSpec.describe "bundle remove" do end G - bundle! "remove rspec" + bundle "remove rspec" expect(out).to include("rspec was removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(gemfile).to eq <<~G + source "https://gem.repo1" G end end @@ -233,7 +275,7 @@ RSpec.describe "bundle remove" do context "when all the groups will be empty after removal" do it "removes the empty nested blocks" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" group :test do group :serioustest do @@ -242,11 +284,11 @@ RSpec.describe "bundle remove" do end G - bundle! "remove rspec" + bundle "remove rspec" expect(out).to include("rspec was removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(gemfile).to eq <<~G + source "https://gem.repo1" G end end @@ -254,10 +296,10 @@ RSpec.describe "bundle remove" do context "when outer group will not be empty after removal" do it "removes only empty blocks" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" group :test do - gem "rack-test" + gem "myrack-test" group :serioustest do gem "rspec" @@ -265,14 +307,14 @@ RSpec.describe "bundle remove" do end G - bundle! "remove rspec" + bundle "remove rspec" expect(out).to include("rspec was removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(gemfile).to eq <<~G + source "https://gem.repo1" group :test do - gem "rack-test" + gem "myrack-test" end G @@ -282,25 +324,25 @@ RSpec.describe "bundle remove" do context "when inner group will not be empty after removal" do it "removes only empty blocks" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" group :test do group :serioustest do gem "rspec" - gem "rack-test" + gem "myrack-test" end end G - bundle! "remove rspec" + bundle "remove rspec" expect(out).to include("rspec was removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(gemfile).to eq <<~G + source "https://gem.repo1" group :test do group :serioustest do - gem "rack-test" + gem "myrack-test" end end G @@ -309,41 +351,41 @@ RSpec.describe "bundle remove" do end describe "arbitrary gemfile" do - context "when mutiple gems are present in same line" do + context "when multiple gems are present in same line" do it "shows warning for gems not removed" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack"; gem "rails" + source "https://gem.repo1" + gem "myrack"; gem "rails" G - bundle "remove rails" + bundle "remove rails", raise_on_error: false - expect(err).to include("Gems could not be removed. rack (>= 0) would also have been removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack"; gem "rails" + expect(err).to include("Gems could not be removed. myrack (>= 0) would also have been removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + gem "myrack"; gem "rails" G end end context "when some gems could not be removed" do it "shows warning for gems not removed and success for those removed" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem"rack" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem"myrack" gem"rspec" gem "rails" gem "minitest" G - bundle! "remove rails rack rspec minitest" + bundle "remove rails myrack rspec minitest" expect(out).to include("rails was removed.") expect(out).to include("minitest was removed.") - expect(out).to include("rack, rspec could not be removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" - gem"rack" + expect(out).to include("myrack, rspec could not be removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + gem"myrack" gem"rspec" G end @@ -352,31 +394,31 @@ RSpec.describe "bundle remove" do context "with sources" do before do - build_repo gem_repo3 do + build_repo3 do build_gem "rspec" end end it "removes gems and empty source blocks" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack" + gem "myrack" - source "#{file_uri_for(gem_repo3)}" do + source "https://gem.repo3" do gem "rspec" end G - bundle! "install" + bundle "install" - bundle! "remove rspec" + bundle "remove rspec" expect(out).to include("rspec was removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(gemfile).to eq <<~G + source "https://gem.repo1" - gem "rack" + gem "myrack" G end end @@ -384,40 +426,40 @@ RSpec.describe "bundle remove" do describe "with eval_gemfile" do context "when gems are present in both gemfiles" do it "removes the gems" do - create_file "Gemfile-other", <<-G - gem "rack" + gemfile "Gemfile-other", <<-G + gem "myrack" G install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" eval_gemfile "Gemfile-other" - gem "rack" + gem "myrack" G - bundle! "remove rack" + bundle "remove myrack" - expect(out).to include("rack was removed.") + expect(out).to include("myrack was removed.") end end context "when gems are present in other gemfile" do it "removes the gems" do - create_file "Gemfile-other", <<-G - gem "rack" + gemfile "Gemfile-other", <<-G + gem "myrack" G install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" eval_gemfile "Gemfile-other" G - bundle! "remove rack" + bundle "remove myrack" - expect(bundled_app("Gemfile-other").read).to_not include("gem \"rack\"") - expect(out).to include("rack was removed.") + expect(bundled_app("Gemfile-other").read).to_not include("gem \"myrack\"") + expect(out).to include("myrack was removed.") end end @@ -429,61 +471,61 @@ RSpec.describe "bundle remove" do G install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" eval_gemfile "Gemfile-other" G - bundle "remove rack" + bundle "remove myrack", raise_on_error: false - expect(err).to include("`rack` is not specified in #{bundled_app("Gemfile")} so it could not be removed.") + expect(err).to include("`myrack` is not specified in #{bundled_app_gemfile} so it could not be removed.") end end context "when the gem is present in parent file but not in gemfile specified by eval_gemfile" do it "removes the gem" do - create_file "Gemfile-other", <<-G + gemfile "Gemfile-other", <<-G gem "rails" G install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" eval_gemfile "Gemfile-other" - gem "rack" + gem "myrack" G - bundle "remove rack" + bundle "remove myrack", raise_on_error: false - expect(out).to include("rack was removed.") - expect(err).to include("`rack` is not specified in #{bundled_app("Gemfile-other")} so it could not be removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(out).to include("myrack was removed.") + expect(err).to include("`myrack` is not specified in #{bundled_app("Gemfile-other")} so it could not be removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" eval_gemfile "Gemfile-other" G end end - context "when gems can not be removed from other gemfile" do + context "when gems cannot be removed from other gemfile" do it "shows error" do - create_file "Gemfile-other", <<-G - gem "rails"; gem "rack" + gemfile "Gemfile-other", <<-G + gem "rails"; gem "myrack" G install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" eval_gemfile "Gemfile-other" - gem "rack" + gem "myrack" G - bundle "remove rack" + bundle "remove myrack", raise_on_error: false - expect(out).to include("rack was removed.") + expect(out).to include("myrack was removed.") expect(err).to include("Gems could not be removed. rails (>= 0) would also have been removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(gemfile).to eq <<~G + source "https://gem.repo1" eval_gemfile "Gemfile-other" G @@ -492,47 +534,47 @@ RSpec.describe "bundle remove" do context "when gems could not be removed from parent gemfile" do it "shows error" do - create_file "Gemfile-other", <<-G - gem "rack" + gemfile "Gemfile-other", <<-G + gem "myrack" G install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" eval_gemfile "Gemfile-other" - gem "rails"; gem "rack" + gem "rails"; gem "myrack" G - bundle "remove rack" + bundle "remove myrack", raise_on_error: false expect(err).to include("Gems could not be removed. rails (>= 0) would also have been removed.") - expect(bundled_app("Gemfile-other").read).to include("gem \"rack\"") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(bundled_app("Gemfile-other").read).to include("gem \"myrack\"") + expect(gemfile).to eq <<~G + source "https://gem.repo1" eval_gemfile "Gemfile-other" - gem "rails"; gem "rack" + gem "rails"; gem "myrack" G end end context "when gem present in gemfiles but could not be removed from one from one of them" do - it "removes gem which can be removed and shows warning for file from which it can not be removed" do - create_file "Gemfile-other", <<-G - gem "rack" + it "removes gem which can be removed and shows warning for file from which it cannot be removed" do + gemfile "Gemfile-other", <<-G + gem "myrack" G install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" eval_gemfile "Gemfile-other" - gem"rack" + gem"myrack" G - bundle! "remove rack" + bundle "remove myrack" - expect(out).to include("rack was removed.") - expect(bundled_app("Gemfile-other").read).to_not include("gem \"rack\"") + expect(out).to include("myrack was removed.") + expect(bundled_app("Gemfile-other").read).to_not include("gem \"myrack\"") end end end @@ -540,18 +582,18 @@ RSpec.describe "bundle remove" do context "with install_if" do it "removes gems inside blocks and empty blocks" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" install_if(lambda { false }) do - gem "rack" + gem "myrack" end G - bundle! "remove rack" + bundle "remove myrack" - expect(out).to include("rack was removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(out).to include("myrack was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" G end end @@ -559,37 +601,136 @@ RSpec.describe "bundle remove" do context "with env" do it "removes gems inside blocks and empty blocks" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" env "BUNDLER_TEST" do - gem "rack" + gem "myrack" end G - bundle! "remove rack" + bundle "remove myrack" - expect(out).to include("rack was removed.") - gemfile_should_be <<-G - source "#{file_uri_for(gem_repo1)}" + expect(out).to include("myrack was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" G end end context "with gemspec" do it "should not remove the gem" do - build_lib("foo", :path => tmp.join("foo")) do |s| + build_lib("foo", path: tmp("foo")) do |s| s.write("foo.gemspec", "") - s.add_dependency "rack" + s.add_dependency "myrack" end install_gemfile(<<-G) - source "#{file_uri_for(gem_repo1)}" - gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + source "https://gem.repo1" + gemspec :path => '#{tmp("foo")}', :name => 'foo' G - bundle! "remove foo" + bundle "remove foo" expect(out).to include("foo could not be removed.") end end + + describe "with comments that mention gems" do + context "when comment is a separate line comment" do + it "does not remove the line comment" do + gemfile <<-G + source "https://gem.repo1" + + # gem "myrack" might be used in the future + gem "myrack" + G + + bundle "remove myrack" + + expect(out).to include("myrack was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + + # gem "myrack" might be used in the future + G + end + end + + context "when gem specified for removal has an inline comment" do + it "removes the inline comment" do + gemfile <<-G + source "https://gem.repo1" + + gem "myrack" # this can be removed + G + + bundle "remove myrack" + + expect(out).to include("myrack was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + G + end + end + + context "when gem specified for removal is mentioned in other gem's comment" do + it "does not remove other gem" do + gemfile <<-G + source "https://gem.repo1" + gem "puma" # implements interface provided by gem "myrack" + + gem "myrack" + G + + bundle "remove myrack" + + expect(out).to_not include("puma was removed.") + expect(out).to include("myrack was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + gem "puma" # implements interface provided by gem "myrack" + G + end + end + + context "when gem specified for removal has a comment that mentions other gem" do + it "does not remove other gem" do + gemfile <<-G + source "https://gem.repo1" + gem "puma" # implements interface provided by gem "myrack" + + gem "myrack" + G + + bundle "remove puma" + + expect(out).to include("puma was removed.") + expect(out).to_not include("myrack was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + + gem "myrack" + G + end + end + end + + context "when gem definition has parentheses" do + it "removes the gem" do + gemfile <<-G + source "https://gem.repo1" + + gem("myrack") + gem("myrack", ">= 0") + gem("myrack", require: false) + G + + bundle "remove myrack" + + expect(out).to include("myrack was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + G + end + end end diff --git a/spec/bundler/commands/show_spec.rb b/spec/bundler/commands/show_spec.rb index 61b8f73e7f..d0d55ffbb9 100644 --- a/spec/bundler/commands/show_spec.rb +++ b/spec/bundler/commands/show_spec.rb @@ -1,28 +1,30 @@ # frozen_string_literal: true -RSpec.describe "bundle show", :bundler => "< 3" do +RSpec.describe "bundle show" do context "with a standard Gemfile" do before :each do + build_repo2 + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo2" gem "rails" G end it "creates a Gemfile.lock if one did not exist" do - FileUtils.rm("Gemfile.lock") + FileUtils.rm(bundled_app_lock) bundle "show" - expect(bundled_app("Gemfile.lock")).to exist + expect(bundled_app_lock).to exist end it "creates a Gemfile.lock when invoked with a gem name" do - FileUtils.rm("Gemfile.lock") + FileUtils.rm(bundled_app_lock) bundle "show rails" - expect(bundled_app("Gemfile.lock")).to exist + expect(bundled_app_lock).to exist end it "prints path if gem exists in bundle" do @@ -35,12 +37,12 @@ RSpec.describe "bundle show", :bundler => "< 3" do 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(default_bundle_path("gems", "rails-2.3.2")) + it "warns if specification is installed, but path does not exist on disk" do + FileUtils.rm_r(default_bundle_path("gems", "rails-2.3.2")) bundle "show rails" - expect(err).to match(/has been deleted/i) + expect(err).to match(/is missing/i) expect(err).to match(default_bundle_path("gems", "rails-2.3.2").to_s) end @@ -50,14 +52,14 @@ RSpec.describe "bundle show", :bundler => "< 3" do end it "complains if gem not in bundle" do - bundle "show missing" + bundle "show missing", raise_on_error: false expect(err).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-12.3.2").to_s) + expect(out).to include(default_bundle_path("gems", "rake-#{rake_version}").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. @@ -86,6 +88,24 @@ RSpec.describe "bundle show", :bundler => "< 3" do \tStatus: Up to date MSG end + + it "includes up to date status in summary of gems" do + update_repo2 do + build_gem "rails", "3.0.0" + end + + bundle "show --verbose" + + expect(out).to include <<~MSG + * rails (2.3.2) + \tSummary: This is just a fake gem for testing + \tHomepage: http://example.com + \tStatus: Outdated - 2.3.2 < 3.0.0 + MSG + + # check lockfile is not accidentally updated + expect(lockfile).to include("actionmailer (2.3.2)") + end end context "with a git repo in the Gemfile" do @@ -100,11 +120,11 @@ RSpec.describe "bundle show", :bundler => "< 3" do expect(the_bundle).to include_gems "foo 1.0" bundle :show - expect(out).to include("foo (1.0 #{@git.ref_for("master", 6)}") + expect(out).to include("foo (1.0 #{@git.ref_for("main", 6)}") end - it "prints out branch names other than master" do - update_git "foo", :branch => "omg" do |s| + it "prints out branch names other than main" 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] @@ -129,97 +149,69 @@ RSpec.describe "bundle show", :bundler => "< 3" do end it "handles when a version is a '-' prerelease" do - @git = build_git("foo", "1.0.0-beta.1", :path => lib_path("foo")) + @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 + 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" + build_git "foo", path: lib_path("foo") + File.open(lib_path("foo/Gemfile"), "w") {|f| f.puts "gemspec" } + sys_exec "rm -rf .git && git init", dir: lib_path("foo") end it "does not output git errors" do - bundle :show + bundle :show, dir: lib_path("foo") expect(err_without_deprecations).to be_empty end end it "performs an automatic bundle install" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "foo" G - bundle "config set auto_install 1" + bundle_config "auto_install 1" bundle :show expect(out).to include("Installing foo 1.0") end context "with a valid regexp for gem name" do - it "presents alternatives" do + it "presents alternatives", :readline do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - gem "rack-obama" + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" G bundle "show rac" - expect(out).to eq "1 : rack\n2 : rack-obama\n0 : - exit -\n>" + expect(out).to match(/\A1 : myrack\n2 : myrack-obama\n0 : - exit -(\n>|\z)/) end end context "with an invalid regexp for gem name" do it "does not find the gem" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rails" G invalid_regexp = "[]" - bundle "show #{invalid_regexp}" + bundle "show #{invalid_regexp}", raise_on_error: false expect(err).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_uri_for(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 -RSpec.describe "bundle show", :bundler => "3" do +RSpec.describe "bundle show", bundler: "5" do pending "shows a friendly error about the command removal" end diff --git a/spec/bundler/commands/ssl_spec.rb b/spec/bundler/commands/ssl_spec.rb new file mode 100644 index 0000000000..4220731b69 --- /dev/null +++ b/spec/bundler/commands/ssl_spec.rb @@ -0,0 +1,373 @@ +# frozen_string_literal: true + +require "bundler/cli" +require "bundler/cli/doctor" +require "bundler/cli/doctor/ssl" +require_relative "../support/artifice/helpers/artifice" +require "bundler/vendored_persistent.rb" + +RSpec.describe "bundle doctor ssl" do + before(:each) do + require_rack_test + require_relative "../support/artifice/helpers/endpoint" + + @dummy_endpoint = Class.new(Endpoint) do + get "/" do + end + end + + @previous_ui = Bundler.ui + Bundler.ui = Bundler::UI::Shell.new + Bundler.ui.level = "info" + + @previous_client = Gem::Request::ConnectionPools.client + Artifice.activate_with(@dummy_endpoint) + Gem::Request::ConnectionPools.client = Gem::Net::HTTP + end + + after(:each) do + Bundler.ui = @previous_ui + Artifice.deactivate + Gem::Request::ConnectionPools.client = @previous_client + end + + context "when a diagnostic fails" do + it "prints the diagnostic when openssl can't be loaded" do + subject = Bundler::CLI::Doctor::SSL.new({}) + allow(subject).to receive(:require).with("openssl").and_raise(LoadError) + + expected_err = <<~MSG + Oh no! Your Ruby doesn't have OpenSSL, so it can't connect to rubygems.org. + You'll need to recompile or reinstall Ruby with OpenSSL support and try again. + MSG + + expect { subject.run }.to output("").to_stdout.and output(expected_err).to_stderr + end + + it "fails due to certificate verification", :ruby_repo do + net_http = Class.new(Artifice::Net::HTTP) do + def connect + raise OpenSSL::SSL::SSLError, "certificate verify failed" + end + end + + Artifice.replace_net_http(net_http) + Gem::Request::ConnectionPools.client = net_http + Gem::RemoteFetcher.fetcher.close_all + + expected_out = <<~MSG + Here's your OpenSSL environment: + + OpenSSL: #{OpenSSL::VERSION} + Compiled with: #{OpenSSL::OPENSSL_VERSION} + Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION} + + Trying connections to https://rubygems.org: + MSG + + expected_err = <<~MSG + Bundler: failed (certificate verification) + RubyGems: failed (certificate verification) + Ruby net/http: failed + + Unfortunately, this Ruby can't connect to rubygems.org. + + Below affect only Ruby net/http connections: + SSL_CERT_FILE: exists #{OpenSSL::X509::DEFAULT_CERT_FILE} + SSL_CERT_DIR: exists #{OpenSSL::X509::DEFAULT_CERT_DIR} + + Your Ruby can't connect to rubygems.org because you are missing the certificate files OpenSSL needs to verify you are connecting to the genuine rubygems.org servers. + + MSG + + subject = Bundler::CLI::Doctor::SSL.new({}) + expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr + end + + it "fails due to a too old tls version" do + subject = Bundler::CLI::Doctor::SSL.new({}) + + net_http = Class.new(Artifice::Net::HTTP) do + def connect + raise OpenSSL::SSL::SSLError, "read server hello A" + end + end + + Artifice.replace_net_http(net_http) + Gem::Request::ConnectionPools.client = Gem::Net::HTTP + Gem::RemoteFetcher.fetcher.close_all + + expected_out = <<~MSG + Here's your OpenSSL environment: + + OpenSSL: #{OpenSSL::VERSION} + Compiled with: #{OpenSSL::OPENSSL_VERSION} + Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION} + + Trying connections to https://rubygems.org: + MSG + + expected_err = <<~MSG + Bundler: failed (SSL/TLS protocol version mismatch) + RubyGems: failed (SSL/TLS protocol version mismatch) + Ruby net/http: failed + + Unfortunately, this Ruby can't connect to rubygems.org. + + Your Ruby can't connect to rubygems.org because your version of OpenSSL is too old. + You'll need to upgrade your OpenSSL install and/or recompile Ruby to use a newer OpenSSL. + + MSG + + expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr + end + + it "fails due to unsupported tls 1.3 version" do + net_http = Class.new(Artifice::Net::HTTP) do + def connect + raise OpenSSL::SSL::SSLError, "read server hello A" + end + end + + Artifice.replace_net_http(net_http) + Gem::Request::ConnectionPools.client = net_http + Gem::RemoteFetcher.fetcher.close_all + + expected_out = <<~MSG + Here's your OpenSSL environment: + + OpenSSL: #{OpenSSL::VERSION} + Compiled with: #{OpenSSL::OPENSSL_VERSION} + Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION} + + Trying connections to https://rubygems.org: + MSG + + expected_err = <<~MSG + Bundler: failed (SSL/TLS protocol version mismatch) + RubyGems: failed (SSL/TLS protocol version mismatch) + Ruby net/http: failed + + Unfortunately, this Ruby can't connect to rubygems.org. + + Your Ruby can't connect to rubygems.org because TLS1_3 isn't supported yet. + + MSG + + subject = Bundler::CLI::Doctor::SSL.new("tls-version": "1.3") + expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr + end + + it "fails due to a bundler and rubygems connection error" do + endpoint = Class.new(Endpoint) do + get "/" do + raise OpenSSL::SSL::SSLError, "read server hello A" + end + end + + Artifice.activate_with(endpoint) + Gem::Request::ConnectionPools.client = Gem::Net::HTTP + + expected_out = <<~MSG + Here's your OpenSSL environment: + + OpenSSL: #{OpenSSL::VERSION} + Compiled with: #{OpenSSL::OPENSSL_VERSION} + Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION} + + Trying connections to https://rubygems.org: + Ruby net/http: success + + For some reason, your Ruby installation can connect to rubygems.org, but neither RubyGems nor Bundler can. + The most likely fix is to manually upgrade RubyGems by following the instructions at http://ruby.to/ssl-check-failed. + After you've done that, run `gem install bundler` to upgrade Bundler, and then run this script again to make sure everything worked. ❣ + + MSG + + expected_err = <<~MSG + Bundler: failed (SSL/TLS protocol version mismatch) + RubyGems: failed (SSL/TLS protocol version mismatch) + MSG + + subject = Bundler::CLI::Doctor::SSL.new({}) + expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr + end + + it "fails due to a bundler connection error" do + endpoint = Class.new(Endpoint) do + get "/" do + if request.user_agent.include?("bundler") + raise OpenSSL::SSL::SSLError, "read server hello A" + end + end + end + + Artifice.activate_with(endpoint) + Gem::Request::ConnectionPools.client = Gem::Net::HTTP + + expected_out = <<~MSG + Here's your OpenSSL environment: + + OpenSSL: #{OpenSSL::VERSION} + Compiled with: #{OpenSSL::OPENSSL_VERSION} + Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION} + + Trying connections to https://rubygems.org: + RubyGems: success + Ruby net/http: success + + Although your Ruby installation and RubyGems can both connect to rubygems.org, Bundler is having trouble. + The most likely way to fix this is to upgrade Bundler by running `gem install bundler`. + Run this script again after doing that to make sure everything is all set. + If you're still having trouble, check out the troubleshooting guide at http://ruby.to/ssl-check-failed. + + MSG + + expected_err = <<~MSG + Bundler: failed (SSL/TLS protocol version mismatch) + MSG + + subject = Bundler::CLI::Doctor::SSL.new({}) + expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr + end + + it "fails due to a RubyGems connection error" do + endpoint = Class.new(Endpoint) do + get "/" do + if request.user_agent.include?("Ruby, RubyGems") + raise OpenSSL::SSL::SSLError, "read server hello A" + end + end + end + + Artifice.activate_with(endpoint) + Gem::Request::ConnectionPools.client = Gem::Net::HTTP + + expected_out = <<~MSG + Here's your OpenSSL environment: + + OpenSSL: #{OpenSSL::VERSION} + Compiled with: #{OpenSSL::OPENSSL_VERSION} + Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION} + + Trying connections to https://rubygems.org: + Bundler: success + Ruby net/http: success + + It looks like Ruby and Bundler can connect to rubygems.org, but RubyGems itself cannot. + You can likely solve this by manually downloading and installing a RubyGems update. + Visit http://ruby.to/ssl-check-failed for instructions on how to manually upgrade RubyGems. + + MSG + + expected_err = <<~MSG + RubyGems: failed (SSL/TLS protocol version mismatch) + MSG + + subject = Bundler::CLI::Doctor::SSL.new({}) + expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr + end + end + + context "when no diagnostic fails" do + it "prints the SSL environment" do + expected_out = <<~MSG + Here's your OpenSSL environment: + + OpenSSL: #{OpenSSL::VERSION} + Compiled with: #{OpenSSL::OPENSSL_VERSION} + Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION} + + Trying connections to https://rubygems.org: + Bundler: success + RubyGems: success + Ruby net/http: success + + Hooray! This Ruby can connect to rubygems.org. + You are all set to use Bundler and RubyGems. + + MSG + + subject = Bundler::CLI::Doctor::SSL.new({}) + expect { subject.run }.to output(expected_out).to_stdout.and output("").to_stderr + end + + it "uses the tls_version verify mode and host when given as option" do + net_http = Class.new(Artifice::Net::HTTP) do + class << self + attr_accessor :verify_mode, :min_version, :max_version + end + + def connect + self.class.verify_mode = verify_mode + self.class.min_version = min_version + self.class.max_version = max_version + + super + end + end + + net_http.endpoint = @dummy_endpoint + Artifice.replace_net_http(net_http) + Gem::Request::ConnectionPools.client = net_http + Gem::RemoteFetcher.fetcher.close_all + + expected_out = <<~MSG + Here's your OpenSSL environment: + + OpenSSL: #{OpenSSL::VERSION} + Compiled with: #{OpenSSL::OPENSSL_VERSION} + Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION} + + Trying connections to https://example.org: + Bundler: success + RubyGems: success + Ruby net/http: success + + Hooray! This Ruby can connect to example.org. + You are all set to use Bundler and RubyGems. + + MSG + + subject = Bundler::CLI::Doctor::SSL.new("tls-version": "1.3", "verify-mode": :none, host: "example.org") + expect { subject.run }.to output(expected_out).to_stdout.and output("").to_stderr + expect(net_http.verify_mode).to eq(0) + expect(net_http.min_version.to_s).to eq("TLS1_3") + expect(net_http.max_version.to_s).to eq("TLS1_3") + end + + it "warns when TLS1.2 is not supported" do + expected_out = <<~MSG + Here's your OpenSSL environment: + + OpenSSL: #{OpenSSL::VERSION} + Compiled with: #{OpenSSL::OPENSSL_VERSION} + Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION} + + Trying connections to https://rubygems.org: + Bundler: success + RubyGems: success + Ruby net/http: success + + Hooray! This Ruby can connect to rubygems.org. + You are all set to use Bundler and RubyGems. + + MSG + + expected_err = <<~MSG + + WARNING: Although your Ruby can connect to rubygems.org today, your OpenSSL is very old! + WARNING: You will need to upgrade OpenSSL to use rubygems.org. + + MSG + + previous_version = OpenSSL::SSL::TLS1_2_VERSION + OpenSSL::SSL.send(:remove_const, :TLS1_2_VERSION) + + subject = Bundler::CLI::Doctor::SSL.new({}) + expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr + ensure + OpenSSL::SSL.const_set(:TLS1_2_VERSION, previous_version) + end + end +end diff --git a/spec/bundler/commands/update_spec.rb b/spec/bundler/commands/update_spec.rb index a0c7e33299..03a3786d80 100644 --- a/spec/bundler/commands/update_spec.rb +++ b/spec/bundler/commands/update_spec.rb @@ -1,99 +1,148 @@ # frozen_string_literal: true RSpec.describe "bundle update" do - before :each do - build_repo2 + describe "with no arguments" do + before do + build_repo2 - install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "activesupport" - gem "rack-obama" - gem "platform_specific" - G - end + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + gem "platform_specific" + G + end - describe "with no arguments", :bundler => "< 3" do it "updates the entire bundle" do update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + 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" + expect(the_bundle).to include_gems "myrack 1.2", "myrack-obama 1.0", "activesupport 3.0" end it "doesn't delete the Gemfile.lock file if something goes wrong" do gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "activesupport" - gem "rack-obama" + gem "myrack-obama" exit! G - bundle "update" - expect(bundled_app("Gemfile.lock")).to exist + bundle "update", raise_on_error: false + expect(bundled_app_lock).to exist + end + end + + describe "with --verbose" do + before do + build_repo2 + + install_gemfile <<~G + source "https://gem.repo2" + gem "myrack" + G + end + + it "logs the reason for re-resolving" do + bundle "update --verbose" + expect(out).not_to include("Found changes from the lockfile") + expect(out).to include("Re-resolving dependencies because bundler is unlocking") end end - describe "with --all", :bundler => "3" do + describe "with --all" do + before do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + gem "platform_specific" + G + end + it "updates the entire bundle" do update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + build_gem "activesupport", "3.0" end - bundle! "update", :all => true + bundle "update", all: true expect(out).to include("Bundle updated!") - expect(the_bundle).to include_gems "rack 1.2", "rack-obama 1.0", "activesupport 3.0" + expect(the_bundle).to include_gems "myrack 1.2", "myrack-obama 1.0", "activesupport 3.0" end it "doesn't delete the Gemfile.lock file if something goes wrong" do + install_gemfile "source 'https://gem.repo1'" + gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "activesupport" - gem "rack-obama" + gem "myrack-obama" exit! G - bundle "update", :all => true - expect(bundled_app("Gemfile.lock")).to exist + bundle "update", all: true, raise_on_error: false + expect(bundled_app_lock).to exist end end describe "with --gemfile" do - it "creates lock files based on the Gemfile name" do + it "creates lockfiles based on the Gemfile name" do gemfile bundled_app("OmgFile"), <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "1.0" + source "https://gem.repo1" + gem "myrack", "1.0" G - bundle! "update --gemfile OmgFile", :all => true + bundle "update --gemfile OmgFile", all: true expect(bundled_app("OmgFile.lock")).to exist end end context "when update_requires_all_flag is set" do - before { bundle! "config set update_requires_all_flag true" } + before { bundle_config "update_requires_all_flag true" } it "errors when passed nothing" do - install_gemfile! "" - bundle :update + install_gemfile "source 'https://gem.repo1'" + bundle :update, raise_on_error: false expect(err).to eq("To update everything, pass the `--all` flag.") end it "errors when passed --all and another option" do - install_gemfile! "" - bundle "update --all foo" + install_gemfile "source 'https://gem.repo1'" + bundle "update --all foo", raise_on_error: false expect(err).to eq("Cannot specify --all along with specific options.") end it "updates everything when passed --all" do - install_gemfile! "" + install_gemfile "source 'https://gem.repo1'" bundle "update --all" expect(out).to include("Bundle updated!") end end describe "--quiet argument" do + before do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + gem "platform_specific" + G + end + it "hides UI messages" do bundle "update --quiet" expect(out).not_to include("Bundle updated!") @@ -101,133 +150,604 @@ RSpec.describe "bundle update" do end describe "with a top level dependency" do + before do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + gem "platform_specific" + G + end + it "unlocks all child dependencies that are unrelated to other locked dependencies" do update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + 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" + bundle "update myrack-obama" + expect(the_bundle).to include_gems "myrack 1.2", "myrack-obama 1.0", "activesupport 2.3.5" end end describe "with an unknown dependency" do + before do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + gem "platform_specific" + G + end + it "should inform the user" do - bundle "update halting-problem-solver" + bundle "update halting-problem-solver", raise_on_error: false expect(err).to include "Could not find gem 'halting-problem-solver'" end it "should suggest alternatives" do - bundle "update platformspecific" - expect(err).to include "Did you mean platform_specific?" + bundle "update platformspecific", raise_on_error: false + expect(err).to include "Did you mean 'platform_specific'?" end end describe "with a child dependency" do + before do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + gem "platform_specific" + G + end + it "should update the child dependency" do - update_repo2 - bundle "update rack" - expect(the_bundle).to include_gems "rack 1.2" + update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + end + + bundle "update myrack" + expect(the_bundle).to include_gems "myrack 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 set only_update_to_newer_versions true" + it "does not go to an older version" do + build_repo4 do + build_gem "tilt", "2.0.8" + build_gem "slim", "3.0.9" do |s| + s.add_dependency "tilt", [">= 1.3.3", "< 2.1"] + end + build_gem "slim_lint", "0.16.1" do |s| + s.add_dependency "slim", [">= 3.0", "< 5.0"] + end + build_gem "slim-rails", "0.2.1" do |s| + s.add_dependency "slim", ">= 0.9.2" + end + build_gem "slim-rails", "3.1.3" do |s| + s.add_dependency "slim", "~> 3.0" + end end - it "does not go to an older version" do - build_repo4 do - build_gem "tilt", "2.0.8" - build_gem "slim", "3.0.9" do |s| - s.add_dependency "tilt", [">= 1.3.3", "< 2.1"] - end - build_gem "slim_lint", "0.16.1" do |s| - s.add_dependency "slim", [">= 3.0", "< 5.0"] - end - build_gem "slim-rails", "0.2.1" do |s| - s.add_dependency "slim", ">= 0.9.2" - end - build_gem "slim-rails", "3.1.3" do |s| - s.add_dependency "slim", "~> 3.0" - end + install_gemfile <<-G + source "https://gem.repo4" + gem "slim-rails" + gem "slim_lint" + G + + expect(the_bundle).to include_gems("slim 3.0.9", "slim-rails 3.1.3", "slim_lint 0.16.1") + + build_repo4 do + build_gem "slim", "4.0.0" do |s| + s.add_dependency "tilt", [">= 2.0.6", "< 2.1"] end + end - install_gemfile! <<-G - source "#{file_uri_for(gem_repo4)}" - gem "slim-rails" - gem "slim_lint" - G + bundle "update", all: true - expect(the_bundle).to include_gems("slim 3.0.9", "slim-rails 3.1.3", "slim_lint 0.16.1") + expect(the_bundle).to include_gems("slim 3.0.9", "slim-rails 3.1.3", "slim_lint 0.16.1") + end - update_repo4 do - build_gem "slim", "4.0.0" do |s| - s.add_dependency "tilt", [">= 2.0.6", "< 2.1"] - end + it "does not go to an older version, even if the version upgrade that could cause another gem to downgrade is activated first" do + build_repo4 do + # countries is processed before country_select by the resolver due to having less spec groups (groups of versions with the same dependencies) (2 vs 3) + + build_gem "countries", "2.1.4" + build_gem "countries", "3.1.0" + + build_gem "countries", "4.0.0" do |s| + s.add_dependency "sixarm_ruby_unaccent", "~> 1.1" + end + + build_gem "country_select", "1.2.0" + + build_gem "country_select", "2.1.4" do |s| + s.add_dependency "countries", "~> 2.0" + end + build_gem "country_select", "3.1.1" do |s| + s.add_dependency "countries", "~> 2.0" + end + + build_gem "country_select", "5.1.0" do |s| + s.add_dependency "countries", "~> 3.0" end - bundle! "update", :all => true + build_gem "sixarm_ruby_unaccent", "1.1.0" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "country_select" + gem "countries" + G - expect(the_bundle).to include_gems("slim 3.0.9", "slim-rails 3.1.3", "slim_lint 0.16.1") + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo4, "countries", "3.1.0") + c.checksum(gem_repo4, "country_select", "5.1.0") end - it "should still downgrade if forced by the Gemfile" do - build_repo4 do - build_gem "a" - build_gem "b", "1.0" - build_gem "b", "2.0" + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + countries (3.1.0) + country_select (5.1.0) + countries (~> 3.0) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + countries + country_select + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + previous_lockfile = lockfile + + bundle "lock --update", env: { "DEBUG" => "1" }, verbose: true + + expect(lockfile).to eq(previous_lockfile) + end + + it "does not downgrade direct dependencies when run with --conservative" do + build_repo4 do + build_gem "oauth2", "2.0.6" do |s| + s.add_dependency "faraday", ">= 0.17.3", "< 3.0" end - install_gemfile! <<-G - source "#{file_uri_for(gem_repo4)}" - gem "a" - gem "b" - G + build_gem "oauth2", "1.4.10" do |s| + s.add_dependency "faraday", ">= 0.17.3", "< 3.0" + s.add_dependency "multi_json", "~> 1.3" + end - expect(the_bundle).to include_gems("a 1.0", "b 2.0") + build_gem "faraday", "2.5.2" - gemfile <<-G - source "#{file_uri_for(gem_repo4)}" - gem "a" - gem "b", "1.0" - G + build_gem "multi_json", "1.15.0" + + build_gem "quickbooks-ruby", "1.0.19" do |s| + s.add_dependency "oauth2", "~> 1.4" + end + + build_gem "quickbooks-ruby", "0.1.9" do |s| + s.add_dependency "oauth2" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "oauth2" + gem "quickbooks-ruby" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + faraday (2.5.2) + multi_json (1.15.0) + oauth2 (1.4.10) + faraday (>= 0.17.3, < 3.0) + multi_json (~> 1.3) + quickbooks-ruby (1.0.19) + oauth2 (~> 1.4) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + oauth2 + quickbooks-ruby + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update --conservative --verbose" + + expect(out).not_to include("Installing quickbooks-ruby 0.1.9") + expect(out).to include("Installing quickbooks-ruby 1.0.19").and include("Installing oauth2 1.4.10") + end + + it "does not downgrade direct dependencies when using gemspec sources" do + create_file("rails.gemspec", <<-G) + Gem::Specification.new do |gem| + gem.name = "rails" + gem.version = "7.1.0.alpha" + gem.author = "DHH" + gem.summary = "Full-stack web application framework." + end + G + + build_repo4 do + build_gem "rake", "12.3.3" + build_gem "rake", "13.0.6" + + build_gem "sneakers", "2.11.0" do |s| + s.add_dependency "rake" + end + + build_gem "sneakers", "2.12.0" do |s| + s.add_dependency "rake", "~> 12.3" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gemspec + + gem "rake" + gem "sneakers" + G + + lockfile <<~L + PATH + remote: . + specs: + + GEM + remote: https://gem.repo4/ + specs: + rake (13.0.6) + sneakers (2.11.0) + rake + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rake + sneakers + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update --verbose" + + expect(out).not_to include("Installing sneakers 2.12.0") + expect(out).not_to include("Installing rake 12.3.3") + expect(out).to include("Installing sneakers 2.11.0").and include("Installing rake 13.0.6") + end + + it "downgrades indirect dependencies if required to fulfill an explicit upgrade request" do + build_repo4 do + build_gem "rbs", "3.6.1" + build_gem "rbs", "3.9.4" + + build_gem "solargraph", "0.56.0" do |s| + s.add_dependency "rbs", "~> 3.3" + end + + build_gem "solargraph", "0.56.2" do |s| + s.add_dependency "rbs", "~> 3.6.1" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem 'solargraph', '~> 0.56.0' + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + rbs (3.9.4) + solargraph (0.56.0) + rbs (~> 3.3) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + solargraph (~> 0.56.0) - bundle! "update b" + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --update solargraph" + + expect(lockfile).to include("solargraph (0.56.2)") + end + + it "does not downgrade direct dependencies unnecessarily" do + build_repo4 do + build_gem "redis", "4.8.1" + build_gem "redis", "5.3.0" + + build_gem "sidekiq", "6.5.5" do |s| + s.add_dependency "redis", ">= 4.5.0" + end + + build_gem "sidekiq", "6.5.12" do |s| + s.add_dependency "redis", ">= 4.5.0", "< 5" + end - expect(the_bundle).to include_gems("a 1.0", "b 1.0") + # one version of sidekiq above Gemfile's range is needed to make the + # resolver choose `redis` first and trying to upgrade it, reproducing + # the accidental sidekiq downgrade as a result + build_gem "sidekiq", "7.0.0 " do |s| + s.add_dependency "redis", ">= 4.2.0" + end + + build_gem "sentry-sidekiq", "5.22.0" do |s| + s.add_dependency "sidekiq", ">= 3.0" + end + + build_gem "sentry-sidekiq", "5.22.4" do |s| + s.add_dependency "sidekiq", ">= 3.0" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "redis" + gem "sidekiq", "~> 6.5" + gem "sentry-sidekiq" + G + + original_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + redis (4.8.1) + sentry-sidekiq (5.22.0) + sidekiq (>= 3.0) + sidekiq (6.5.12) + redis (>= 4.5.0, < 5) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + redis + sentry-sidekiq + sidekiq (~> 6.5) + + BUNDLED WITH + #{Bundler::VERSION} + L + + lockfile original_lockfile + + bundle "lock --update sentry-sidekiq" + + expect(lockfile).to eq(original_lockfile.sub("sentry-sidekiq (5.22.0)", "sentry-sidekiq (5.22.4)")) + end + + it "does not downgrade indirect dependencies unnecessarily" 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, verbose: true + source "https://gem.repo4" + gem "a" + G + + expect(the_bundle).to include_gems("a 1.0", "b 1.0", "c 2.0") + + build_repo4 do + build_gem "b", "2.0" do |s| + s.add_dependency "c", "< 2" + end end + + bundle "update", all: true, verbose: true + expect(the_bundle).to include_gems("a 1.0", "b 1.0", "c 2.0") + end + + it "should still downgrade if forced by the Gemfile" do + build_repo4 do + build_gem "a" + build_gem "b", "1.0" + build_gem "b", "2.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "a" + gem "b" + G + + expect(the_bundle).to include_gems("a 1.0", "b 2.0") + + gemfile <<-G + source "https://gem.repo4" + gem "a" + gem "b", "1.0" + G + + bundle "update b" + + expect(the_bundle).to include_gems("a 1.0", "b 1.0") + end + + it "should still downgrade if forced by the Gemfile, when transitive dependencies also need downgrade" do + build_repo4 do + build_gem "activesupport", "6.1.4.1" do |s| + s.add_dependency "tzinfo", "~> 2.0" + end + + build_gem "activesupport", "6.0.4.1" do |s| + s.add_dependency "tzinfo", "~> 1.1" + end + + build_gem "tzinfo", "2.0.4" + build_gem "tzinfo", "1.2.9" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "activesupport", "~> 6.1.0" + G + + expect(the_bundle).to include_gems("activesupport 6.1.4.1", "tzinfo 2.0.4") + + gemfile <<-G + source "https://gem.repo4" + gem "activesupport", "~> 6.0.0" + G + + original_lockfile = lockfile + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "activesupport", "6.0.4.1" + c.checksum gem_repo4, "tzinfo", "1.2.9" + end + + expected_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + activesupport (6.0.4.1) + tzinfo (~> 1.1) + tzinfo (1.2.9) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + activesupport (~> 6.0.0) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update activesupport" + expect(the_bundle).to include_gems("activesupport 6.0.4.1", "tzinfo 1.2.9") + expect(lockfile).to eq(expected_lockfile) + + lockfile original_lockfile + bundle "update" + expect(the_bundle).to include_gems("activesupport 6.0.4.1", "tzinfo 1.2.9") + expect(lockfile).to eq(expected_lockfile) + + lockfile original_lockfile + bundle "lock --update" + expect(the_bundle).to include_gems("activesupport 6.0.4.1", "tzinfo 1.2.9") + expect(lockfile).to eq(expected_lockfile) end end describe "with --local option" do - it "doesn't hit repo2" do - FileUtils.rm_rf(gem_repo2) + before do + build_repo2 + + gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + gem "platform_specific" + G + end - bundle "update --local --all" - expect(out).not_to include("Fetching source index") + it "doesn't hit repo2" do + simulate_platform "x86-darwin-100" do + lockfile <<~L + GEM + remote: https://gem.repo2/ + specs: + activesupport (2.3.5) + platform_specific (1.0-x86-darwin-100) + myrack (1.0.0) + myrack-obama (1.0) + myrack + + PLATFORMS + x86-darwin-100 + + DEPENDENCIES + activesupport + platform_specific + myrack-obama + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install" + + FileUtils.rm_r(gem_repo2) + + bundle "update --local --all" + expect(out).not_to include("Fetching source index") + end end end describe "with --group option" do + before do + build_repo2 + end + it "should update only specified group gems" do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "activesupport", :group => :development - gem "rack" + gem "myrack" G update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + 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" + expect(the_bundle).not_to include_gems "myrack 1.2" end context "when conservatively updating a group with non-group sub-deps" do it "should update only specified group gems" do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "activemerchant", :group => :development gem "activesupport" G @@ -242,10 +762,10 @@ RSpec.describe "bundle update" do 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") + before do + build_git "foo", path: lib_path("activesupport") install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "activesupport", :group => :development gem "foo", :git => "#{lib_path("activesupport")}" G @@ -253,7 +773,7 @@ RSpec.describe "bundle update" do 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") + update_git "foo", "2.0", path: lib_path("activesupport") bundle "update --group development" expect(the_bundle).to include_gems "activesupport 3.0" @@ -264,82 +784,100 @@ RSpec.describe "bundle update" do context "when bundler itself is a transitive dependency" do it "executes without error" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "activesupport", :group => :development - gem "rack" + gem "myrack" G update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + build_gem "activesupport", "3.0" end bundle "update --group development" expect(the_bundle).to include_gems "activesupport 2.3.5" expect(the_bundle).to include_gems "bundler #{Bundler::VERSION}" - expect(the_bundle).not_to include_gems "rack 1.2" + expect(the_bundle).not_to include_gems "myrack 1.2" end end end describe "in a frozen bundle" do - it "should fail loudly", :bundler => "< 3" do - bundle! "install --deployment" - bundle "update", :all => true + before do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + gem "platform_specific" + G + end + + it "should fail loudly" do + bundle_config "deployment true" + bundle "update", all: true, raise_on_error: false expect(last_command).to be_failure - expect(err).to match(/You are trying to install in deployment mode after changing.your Gemfile/m) - expect(err).to match(/freeze \nby running `bundle install --no-deployment`./m) + expect(err).to eq <<~ERROR.strip + Bundler is unlocking, but the lockfile can't be updated because frozen mode is set + + If this is a development machine, remove the Gemfile.lock freeze by running `bundle config set frozen false`. + ERROR + end + + it "should fail loudly when frozen is set globally" do + bundle_config_global "frozen 1" + bundle "update", all: true, raise_on_error: false + expect(err).to eq <<~ERROR.strip + Bundler is unlocking, but the lockfile can't be updated because frozen mode is set + + If this is a development machine, remove the Gemfile.lock freeze by running `bundle config set frozen false`. + ERROR end - it "should suggest different command when frozen is set globally", :bundler => "< 3" do - bundle! "config set --global frozen 1" - bundle "update", :all => true - expect(err).to match(/You are trying to install in deployment mode after changing.your Gemfile/m). - and match(/freeze \nby running `bundle config unset frozen`./m) + it "should fail loudly when deployment is set globally" do + bundle_config_global "deployment true" + bundle "update", all: true, raise_on_error: false + expect(err).to eq <<~ERROR.strip + Bundler is unlocking, but the lockfile can't be updated because frozen mode is set + + If this is a development machine, remove the Gemfile.lock freeze by running `bundle config set frozen false`. + ERROR end - it "should suggest different command when frozen is set globally", :bundler => "3" do - bundle! "config set --global deployment true" - bundle "update", :all => true - expect(err).to match(/You are trying to install in deployment mode after changing.your Gemfile/m). - and match(/freeze \nby running `bundle config unset deployment`./m) + it "should not suggest any command to unfreeze bundler if frozen is set through ENV" do + bundle "update", all: true, raise_on_error: false, env: { "BUNDLE_FROZEN" => "true" } + expect(err).to eq("Bundler is unlocking, but the lockfile can't be updated because frozen mode is set") end end describe "with --source option" do - it "should not update gems not included in the source that happen to have the same name", :bundler => "< 3" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo2)}" + before do + build_repo2 + end + + it "should not update gems not included in the source that happen to have the same name" do + install_gemfile <<-G + source "https://gem.repo2" gem "activesupport" G update_repo2 { build_gem "activesupport", "3.0" } - bundle! "update --source activesupport" - expect(the_bundle).to include_gem "activesupport 3.0" + bundle "update --source activesupport" + expect(the_bundle).not_to include_gem "activesupport 3.0" end - it "should not update gems not included in the source that happen to have the same name", :bundler => "3" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo2)}" + it "should not update gems not included in the source that happen to have the same name" do + install_gemfile <<-G + source "https://gem.repo2" gem "activesupport" G update_repo2 { build_gem "activesupport", "3.0" } - bundle! "update --source activesupport" - expect(the_bundle).not_to include_gem "activesupport 3.0" - end - - context "with unlock_source_unlocks_spec set to false" do - before { bundle! "config set unlock_source_unlocks_spec false" } - - it "should not update gems not included in the source that happen to have the same name" do - install_gemfile <<-G - source "#{file_uri_for(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 + bundle "update --source activesupport" + expect(the_bundle).not_to include_gems "activesupport 3.0" end end @@ -353,26 +891,13 @@ RSpec.describe "bundle update" do end install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://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", :bundler => "< 3" 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 - - it "should not update the child dependencies of a gem that has the same name as the source", :bundler => "3" do + 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| @@ -398,13 +923,13 @@ RSpec.describe "bundle update" do end install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://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", :bundler => "< 3" do + 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| @@ -413,70 +938,198 @@ RSpec.describe "bundle update" do 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" + expect(the_bundle).to include_gems "harry 1.0", "fred 1.0", "george 1.0" + end + end + + it "shows the previous version of the gem when updated from rubygems source" do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + G + + bundle "update", all: true, verbose: true + expect(out).to include("Using activesupport 2.3.5") + + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle "update", all: true + expect(out).to include("Installing activesupport 3.0 (was 2.3.5)") + end + + it "only prints `Using` for versions that have changed" do + build_repo4 do + build_gem "bar" + build_gem "foo" end - it "should not update the child dependencies of a gem that has the same name as the source", :bundler => "3" do + install_gemfile <<-G + source "https://gem.repo4" + gem "bar" + gem "foo" + G + + bundle "update", all: true + expect(out).to match(/Resolving dependencies\.\.\.\.*\nBundle updated!/) + + build_repo4 do + build_gem "foo", "2.0" + end + + bundle "update", all: true + expect(out.sub("Removing foo (1.0)\n", "")).to match(/Resolving dependencies\.\.\.\.*\nFetching foo 2\.0 \(was 1\.0\)\nInstalling foo 2\.0 \(was 1\.0\)\nBundle updated/) + end + + it "shows error message when Gemfile.lock is not preset and gem is specified" do + gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + G + + bundle "update nonexisting", raise_on_error: false + expect(err).to include("This Bundle hasn't been installed yet. Run `bundle install` to update and install the bundled gems.") + expect(exitstatus).to eq(22) + end + + context "with multiple sources and caching enabled" do + before do + build_repo2 do + build_gem "myrack", "1.0.0" + + build_gem "request_store", "1.0.0" do |s| + s.add_dependency "myrack", "1.0.0" + end + end + + build_repo4 do + # set up repo with no gems + end + + gemfile <<~G + source "https://gem.repo2" + + gem "request_store" + + source "https://gem.repo4" do + end + G + + lockfile <<~L + GEM + remote: https://gem.repo2/ + specs: + myrack (1.0.0) + request_store (1.0.0) + myrack (= 1.0.0) + + GEM + remote: https://gem.repo4/ + specs: + + PLATFORMS + #{local_platform} + + DEPENDENCIES + request_store + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "works" do + bundle :install + bundle :cache + update_repo2 do - build_gem "george", "2.0" - build_gem "harry", "2.0" do |s| - s.add_dependency "george" + build_gem "request_store", "1.1.0" do |s| + s.add_dependency "myrack", "1.0.0" end end - bundle "update --source harry" - expect(the_bundle).to include_gems "harry 1.0", "fred 1.0", "george 1.0" + bundle "update request_store" + + expect(out).to include("Bundle updated!") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo2/ + specs: + myrack (1.0.0) + request_store (1.1.0) + myrack (= 1.0.0) + + GEM + remote: https://gem.repo4/ + specs: + + PLATFORMS + #{local_platform} + + DEPENDENCIES + request_store + + BUNDLED WITH + #{Bundler::VERSION} + L end end end RSpec.describe "bundle update in more complicated situations" do - before :each do + before do build_repo2 end it "will eagerly unlock dependencies of a specified gem" do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "thin" - gem "rack-obama" + gem "myrack-obama" G update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + build_gem "thin", "2.0" do |s| - s.add_dependency "rack" + s.add_dependency "myrack" end end bundle "update thin" - expect(the_bundle).to include_gems "thin 2.0", "rack 1.2", "rack-obama 1.0" + expect(the_bundle).to include_gems "thin 2.0", "myrack 1.2", "myrack-obama 1.0" end it "will warn when some explicitly updated gems are not updated" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo2)}" + install_gemfile <<-G + source "https://gem.repo2" gem "thin" - gem "rack-obama" + gem "myrack-obama" G update_repo2 do - build_gem("thin", "2.0") {|s| s.add_dependency "rack" } - build_gem "rack", "10.0" + build_gem("thin", "2.0") {|s| s.add_dependency "myrack" } + build_gem "myrack", "10.0" end - bundle! "update thin rack-obama" - expect(last_command.stdboth).to include "Bundler attempted to update rack-obama but its version stayed the same" - expect(the_bundle).to include_gems "thin 2.0", "rack 10.0", "rack-obama 1.0" + bundle "update thin myrack-obama" + expect(stdboth).to include "Bundler attempted to update myrack-obama but its version stayed the same" + expect(the_bundle).to include_gems "thin 2.0", "myrack 10.0", "myrack-obama 1.0" end it "will not warn when an explicitly updated git gem changes sha but not version" do build_git "foo" - install_gemfile! <<-G + install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => '#{lib_path("foo-1.0")}' G @@ -484,33 +1137,34 @@ RSpec.describe "bundle update in more complicated situations" do s.write "lib/foo2.rb", "puts :foo2" end - bundle! "update foo" + bundle "update foo" - expect(last_command.stdboth).not_to include "attempted to update" + expect(stdboth).not_to include "attempted to update" end it "will not warn when changing gem sources but not versions" do - build_git "rack" + build_git "myrack" - install_gemfile! <<-G - gem "rack", :git => '#{lib_path("rack-1.0")}' + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack", :git => '#{lib_path("myrack-1.0")}' G gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle! "update rack" + bundle "update myrack" - expect(last_command.stdboth).not_to include "attempted to update" + expect(stdboth).not_to include "attempted to update" end it "will update only from pinned source" do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" - source "#{file_uri_for(gem_repo1)}" do + source "https://gem.repo1" do gem "thin" end G @@ -519,12 +1173,12 @@ RSpec.describe "bundle update in more complicated situations" do build_gem "thin", "2.0" end - bundle "update" + bundle "update", artifice: "compact_index" expect(the_bundle).to include_gems "thin 1.0" end context "when the lockfile is for a different platform" do - before do + around do |example| build_repo4 do build_gem("a", "0.9") build_gem("a", "0.9") {|s| s.platform = "java" } @@ -533,13 +1187,13 @@ RSpec.describe "bundle update in more complicated situations" do end gemfile <<-G - source "#{file_uri_for(gem_repo4)}" + source "https://gem.repo4" gem "a" G lockfile <<-L GEM - remote: #{file_uri_for(gem_repo4)} + remote: https://gem.repo4 specs: a (0.9-java) @@ -550,16 +1204,16 @@ RSpec.describe "bundle update in more complicated situations" do a L - simulate_platform linux + simulate_platform "x86_64-linux", &example end it "allows updating" do - bundle! :update, :all => true + bundle :update, all: true expect(the_bundle).to include_gem "a 1.1" end it "allows updating a specific gem" do - bundle! "update a" + bundle "update a" expect(the_bundle).to include_gem "a 1.1" end end @@ -572,13 +1226,13 @@ RSpec.describe "bundle update in more complicated situations" do end gemfile <<-G - source "#{file_uri_for(gem_repo4)}" + source "https://gem.repo4" gem "a", platform: :jruby G lockfile <<-L GEM - remote: #{file_uri_for(gem_repo4)} + remote: https://gem.repo4 specs: a (0.9-java) @@ -588,14 +1242,14 @@ RSpec.describe "bundle update in more complicated situations" do DEPENDENCIES a L - - simulate_platform linux end it "is not updated because it is not actually included in the bundle" do - bundle! "update a" - expect(last_command.stdboth).to include "Bundler attempted to update a but it was not considered because it is for a different platform from the current one" - expect(the_bundle).to_not include_gem "a" + simulate_platform "x86_64-linux" do + bundle "update a" + expect(stdboth).to include "Bundler attempted to update a but it was not considered because it is for a different platform from the current one" + expect(the_bundle).to_not include_gem "a" + end end end end @@ -605,144 +1259,69 @@ RSpec.describe "bundle update without a Gemfile.lock" do build_repo2 gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" - gem "rack", "1.0" + gem "myrack", "1.0" G - bundle "update", :all => true + bundle "update", all: true - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end end RSpec.describe "bundle update when a gem depends on a newer version of bundler" do - before(:each) do + before do build_repo2 do build_gem "rails", "3.0.1" do |s| - s.add_dependency "bundler", Bundler::VERSION.succ + s.add_dependency "bundler", "9.9.9" end + + build_gem "bundler", "9.9.9" end gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "rails", "3.0.1" G end - it "should explain that bundler conflicted", :bundler => "< 3" do - bundle "update", :all => true - expect(last_command.stdboth).not_to match(/in snapshot/i) + it "should explain that bundler conflicted and how to resolve the conflict" do + bundle "update", all: true, raise_on_error: false + expect(stdboth).not_to match(/in snapshot/i) expect(err).to match(/current Bundler version/i). - and match(/perhaps you need to update bundler/i) - end - - it "should warn that the newer version of Bundler would conflict", :bundler => "3" do - bundle! "update", :all => true - expect(err).to include("rails (3.0.1) has dependency bundler"). - and include("so the dependency is being ignored") - expect(the_bundle).to include_gem "rails 3.0.1" - end -end - -RSpec.describe "bundle update" do - it "shows the previous version of the gem when updated from rubygems source", :bundler => "< 3" do - build_repo2 - - install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "activesupport" - G - - bundle "update", :all => true - expect(out).to include("Using activesupport 2.3.5") - - update_repo2 do - build_gem "activesupport", "3.0" - end - - bundle "update", :all => true - expect(out).to include("Installing activesupport 3.0 (was 2.3.5)") - end - - context "with suppress_install_using_messages set" do - before { bundle! "config set suppress_install_using_messages true" } - - it "only prints `Using` for versions that have changed" do - build_repo4 do - build_gem "bar" - build_gem "foo" - end - - install_gemfile! <<-G - source "#{file_uri_for(gem_repo4)}" - gem "bar" - gem "foo" - G - - bundle! "update", :all => true - out.gsub!(/RubyGems [\d\.]+ is not threadsafe.*\n?/, "") - expect(out).to include "Resolving dependencies...\nBundle updated!" - - update_repo4 do - build_gem "foo", "2.0" - end - - bundle! "update", :all => true - out.sub!("Removing foo (1.0)\n", "") - out.gsub!(/RubyGems [\d\.]+ is not threadsafe.*\n?/, "") - expect(out).to include strip_whitespace(<<-EOS).strip - Resolving dependencies... - Fetching foo 2.0 (was 1.0) - Installing foo 2.0 (was 1.0) - Bundle updated - EOS - end - end - - it "shows error message when Gemfile.lock is not preset and gem is specified" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "activesupport" - G - - bundle "update nonexisting" - expect(err).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 + and match(/Install the necessary version with `gem install bundler:9\.9\.9`/i) 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 + ruby '~> #{Gem.ruby_version}' + source "https://gem.repo1" + G + + gemfile <<-G + source "https://gem.repo1" G end + it "removes the Ruby from the Gemfile.lock" do bundle "update --ruby" - lockfile_should_be <<-L + expect(lockfile).to eq <<~L GEM + remote: https://gem.repo1/ specs: PLATFORMS #{lockfile_platforms} DEPENDENCIES - + #{checksums_section_when_enabled} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L end end @@ -750,27 +1329,33 @@ RSpec.describe "bundle update --ruby" do 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' + ruby '~> #{Gem.ruby_version}' + source "https://gem.repo1" + G + + gemfile <<-G + ruby '~> #{current_ruby_minor}' + source "https://gem.repo1" G end + it "updates the Gemfile.lock with the latest version" do bundle "update --ruby" - lockfile_should_be <<-L - GEM - specs: - - PLATFORMS - #{lockfile_platforms} + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: - DEPENDENCIES + PLATFORMS + #{lockfile_platforms} - RUBY VERSION - ruby 2.1.4p222 + DEPENDENCIES + #{checksums_section_when_enabled} + RUBY VERSION + #{Bundler::RubyVersion.system} - BUNDLED WITH + BUNDLED WITH #{Bundler::VERSION} L end @@ -779,66 +1364,435 @@ RSpec.describe "bundle update --ruby" do 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 '~> #{Gem.ruby_version}' + source "https://gem.repo1" + G + + gemfile <<-G ruby '~> 2.1.0' + source "https://gem.repo1" G end it "shows a helpful error message" do - bundle "update --ruby" + bundle "update --ruby", raise_on_error: false - expect(err).to include("Your Ruby version is 2.2.2, but your Gemfile specified ~> 2.1.0") + expect(err).to include("Your Ruby version is #{Bundler::RubyVersion.system.gem_version}, 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' + lockfile <<~L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + + CHECKSUMS + + RUBY VERSION + ruby 2.1.4p222 + + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile <<-G + ruby '~> #{Gem.ruby_version}' + source "https://gem.repo1" G end + it "updates the Gemfile.lock with the latest version" do bundle "update --ruby" - lockfile_should_be <<-L + expect(lockfile).to eq <<~L GEM + remote: https://gem.repo1/ specs: PLATFORMS #{lockfile_platforms} DEPENDENCIES - + #{checksums_section_when_enabled} RUBY VERSION - ruby 1.8.3p55 + #{Bundler::RubyVersion.system} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L end end end RSpec.describe "bundle update --bundler" do - it "updates the bundler version in the lockfile without re-resolving" do + it "updates the bundler version in the lockfile" do build_repo4 do - build_gem "rack", "1.0" + build_gem "bundler", "2.5.9" + build_gem "myrack", "1.0" end - install_gemfile! <<-G - source "#{file_uri_for(gem_repo4)}" - gem "rack" + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo4, "myrack", "1.0") + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" G + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, '\11.0.0\2') - FileUtils.rm_r gem_repo4 + bundle :update, bundler: true, verbose: true + expect(out).to include("Using bundler #{Bundler::VERSION}") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L - bundle! :update, :bundler => true, :verbose => true - expect(the_bundle).to include_gem "rack 1.0" + expect(the_bundle).to include_gem "myrack 1.0" + end + + it "updates the bundler version in the lockfile without re-resolving if the highest version is already installed" do + build_repo4 do + build_gem "bundler", "2.3.9" + build_gem "myrack", "1.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, "2.3.9") + + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo4, "myrack", "1.0") + end + + bundle :update, bundler: true, verbose: true + expect(out).to include("Using bundler #{Bundler::VERSION}") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} - expect(the_bundle.locked_gems.bundler_version).to eq v(Bundler::VERSION) + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + expect(the_bundle).to include_gem "myrack 1.0" + end + + it "updates the bundler version in the lockfile even if the latest version is not installed", :ruby_repo do + bundle_config "path.system true" + + pristine_system_gems "bundler-9.0.0" + + build_repo4 do + build_gem "myrack", "1.0" + + build_bundler "999.0.0" + end + + checksums = checksums_section do |c| + c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "999.0.0") + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle :update, bundler: true, verbose: true + + expect(out).to include("Updating bundler to 999.0.0") + expect(out).to include("Running `bundle update --bundler \"> 0.a\" --verbose` with bundler 999.0.0") + expect(out).not_to include("Installing Bundler 2.99.9 and restarting using that version.") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 999.0.0 + L + + expect(the_bundle).to include_gems "bundler 999.0.0" + expect(the_bundle).to include_gems "myrack 1.0" + end + + it "does not claim to update to Bundler version to a wrong version when cached gems are present" do + pristine_system_gems "bundler-4.99.0" + + build_repo4 do + build_gem "myrack", "3.0.9.1" + + build_bundler "4.99.0" + end + + gemfile <<~G + source "https://gem.repo4" + gem "myrack" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (3.0.9.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + + BUNDLED WITH + 2.99.0 + L + + bundle :cache, verbose: true + + bundle :update, bundler: true, verbose: true + + expect(out).not_to include("Updating bundler to") + end + + it "does not update the bundler version in the lockfile if the latest version is not compatible with current ruby", :ruby_repo do + pristine_system_gems "bundler-9.9.9" + + build_repo4 do + build_gem "myrack", "1.0" + + build_bundler "9.9.9" + build_bundler "999.0.0" do |s| + s.required_ruby_version = "> #{Gem.ruby_version}" + end + end + + checksums = checksums_section do |c| + c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "9.9.9") + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle :update, bundler: true, verbose: true + + expect(out).to include("Using bundler 9.9.9") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 9.9.9 + L + + expect(the_bundle).to include_gems "bundler 9.9.9" + expect(the_bundle).to include_gems "myrack 1.0" + end + + it "errors if the explicit target version does not exist" do + pristine_system_gems "bundler-9.9.9" + + build_repo4 do + build_gem "myrack", "1.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle :update, bundler: "999.999.999", raise_on_error: false + + expect(last_command).to be_failure + expect(err).to eq("The `bundle update --bundler` target version (999.999.999) does not exist") + end + + it "errors if the explicit target version does not exist, even if auto switching is disabled" do + pristine_system_gems "bundler-9.9.9" + + build_repo4 do + build_gem "myrack", "1.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle :update, bundler: "999.999.999", raise_on_error: false, env: { "BUNDLER_VERSION" => "9.9.9" } + + expect(last_command).to be_failure + expect(err).to eq("The `bundle update --bundler` target version (999.999.999) does not exist") + end + + it "allows updating to development versions if already installed locally" do + system_gems "bundler-9.9.9" + + build_repo4 do + build_gem "myrack", "1.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + system_gems "bundler-9.0.0.dev", path: local_gem_path + bundle :update, bundler: "9.0.0.dev", verbose: "true" + + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo4, "myrack", "1.0") + end + checksums.delete("bundler") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 9.0.0.dev + L + + expect(out).to include("Using bundler 9.0.0.dev") + end + + it "does not touch the network if not necessary" do + system_gems "bundler-9.9.9" + + build_repo4 do + build_gem "myrack", "1.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + system_gems "bundler-9.0.0", path: local_gem_path + bundle :update, bundler: "9.0.0", verbose: true + + expect(out).not_to include("Fetching gem metadata from https://rubygems.org/") + + # Only updates properly on modern RubyGems. + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(local_gem_path, "bundler", "9.0.0", Gem::Platform::RUBY, "cache") + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 9.0.0 + L + + expect(out).to include("Using bundler 9.0.0") + end + + it "prints an error when trying to update bundler in frozen mode" do + system_gems "bundler-9.0.0" + + gemfile <<~G + source "https://gem.repo2" + G + + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + + PLATFORMS + ruby + + DEPENDENCIES + + BUNDLED WITH + 9.0.0 + L + + system_gems "bundler-9.9.9", path: local_gem_path + + bundle "update --bundler=9.9.9", env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false + expect(err).to include("An update to the version of Bundler itself was requested, but the lockfile can't be updated because frozen mode is set") end end @@ -856,13 +1810,16 @@ RSpec.describe "bundle update conservative" do 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 "foo", %w[2.0.0.pre] do |s| + s.add_dependency "bar" + end + build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 2.1.2.pre 3.0.0 3.1.0.pre 4.0.0.pre] 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_uri_for(gem_repo4)}" + source "https://gem.repo4" gem 'foo', '1.4.3' gem 'bar', '2.0.3' gem 'qux', '1.0.0' @@ -871,7 +1828,7 @@ RSpec.describe "bundle update conservative" do # remove 1.4.3 requirement and bar altogether # to setup update specs below gemfile <<-G - source "#{file_uri_for(gem_repo4)}" + source "https://gem.repo4" gem 'foo' gem 'qux' G @@ -879,8 +1836,8 @@ RSpec.describe "bundle update conservative" do context "with patch set as default update level in config" do it "should do a patch level update" do - bundle! "config set --local prefer_patch true" - bundle! "update foo" + bundle_config "prefer_patch true" + bundle "update foo" expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.0" end @@ -888,13 +1845,13 @@ RSpec.describe "bundle update conservative" do context "patch preferred" do it "single gem updates dependent gem to minor" do - bundle! "update --patch foo" + 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", :all => true + bundle "update --patch", all: true expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.1" end @@ -902,7 +1859,7 @@ RSpec.describe "bundle update conservative" do context "minor preferred" do it "single gem updates dependent gem to major" do - bundle! "update --minor foo" + bundle "update --minor foo" expect(the_bundle).to include_gems "foo 1.5.1", "bar 3.0.0", "qux 1.0.0" end @@ -910,17 +1867,43 @@ RSpec.describe "bundle update conservative" do context "strict" do it "patch preferred" do - bundle! "update --patch foo bar --strict" + 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", :all => true + bundle "update --minor --strict", all: true expect(the_bundle).to include_gems "foo 1.5.0", "bar 2.1.1", "qux 1.1.0" end end + + context "pre" do + it "defaults to major" do + bundle "update --pre foo bar" + + expect(the_bundle).to include_gems "foo 2.0.0.pre", "bar 4.0.0.pre", "qux 1.0.0" + end + + it "patch preferred" do + bundle "update --patch --pre foo bar" + + expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.2.pre", "qux 1.0.0" + end + + it "minor preferred" do + bundle "update --minor --pre foo bar" + + expect(the_bundle).to include_gems "foo 1.5.1", "bar 3.1.0.pre", "qux 1.0.0" + end + + it "major preferred" do + bundle "update --major --pre foo bar" + + expect(the_bundle).to include_gems "foo 2.0.0.pre", "bar 4.0.0.pre", "qux 1.0.0" + end + end end context "eager unlocking" do @@ -941,16 +1924,16 @@ RSpec.describe "bundle update conservative" do end gemfile <<-G - source "#{file_uri_for(gem_repo4)}" + source "https://gem.repo4" gem 'isolated_owner' gem 'shared_owner_a' gem 'shared_owner_b' G - lockfile <<-L + lockfile <<~L GEM - remote: #{file_uri_for(gem_repo4)} + remote: https://gem.repo4/ specs: isolated_dep (2.0.1) isolated_owner (1.0.1) @@ -962,15 +1945,17 @@ RSpec.describe "bundle update conservative" do shared_dep (~> 5.0) PLATFORMS - ruby + #{local_platform} DEPENDENCIES + isolated_owner shared_owner_a shared_owner_b - isolated_owner + + CHECKSUMS BUNDLED WITH - 1.13.0 + #{Bundler::VERSION} L end @@ -989,12 +1974,55 @@ RSpec.describe "bundle update conservative" do 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" + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + end + + it "should only update direct dependencies when fully updating with --conservative" do + bundle "update --conservative" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.2" + end + + it "should only change direct dependencies when updating the lockfile with --conservative" do + bundle "lock --update --conservative" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "isolated_dep", "2.0.1" + c.checksum gem_repo4, "isolated_owner", "1.0.2" + c.checksum gem_repo4, "shared_dep", "5.0.1" + c.checksum gem_repo4, "shared_owner_a", "3.0.2" + c.checksum gem_repo4, "shared_owner_b", "4.0.2" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + isolated_dep (2.0.1) + isolated_owner (1.0.2) + isolated_dep (~> 2.0) + shared_dep (5.0.1) + shared_owner_a (3.0.2) + shared_dep (~> 5.0) + shared_owner_b (4.0.2) + shared_dep (~> 5.0) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + isolated_owner + shared_owner_a + shared_owner_b + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L end it "should match bundle install conservative update behavior when not eagerly unlocking" do gemfile <<-G - source "#{file_uri_for(gem_repo4)}" + source "https://gem.repo4" gem 'isolated_owner', '1.0.2' gem 'shared_owner_a', '3.0.2' @@ -1003,17 +2031,63 @@ RSpec.describe "bundle update conservative" do 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" + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + end + end + + context "when Gemfile dependencies have changed" do + before do + build_repo4 do + build_gem "nokogiri", "1.16.4" do |s| + s.platform = "arm64-darwin" + end + + build_gem "nokogiri", "1.16.4" do |s| + s.platform = "x86_64-linux" + end + + build_gem "prism", "0.25.0" + end + + gemfile <<~G + source "https://gem.repo4" + gem "nokogiri", ">=1.16.4" + gem "prism", ">=0.25.0" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.16.4-arm64-darwin) + nokogiri (1.16.4-x86_64-linux) + + PLATFORMS + arm64-darwin + x86_64-linux + + DEPENDENCIES + nokogiri (>= 1.16.4) + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "still works" do + simulate_platform "arm64-darwin-23" do + bundle "update" + end end end context "error handling" do before do - gemfile "" + gemfile "source 'https://gem.repo1'" end it "raises if too many flags are provided" do - bundle "update --patch --minor", :all => true + bundle "update --patch --minor", all: true, raise_on_error: false expect(err).to eq "Provide only one of the following options: minor, patch" end diff --git a/spec/bundler/commands/version_spec.rb b/spec/bundler/commands/version_spec.rb index f85ac82a40..4320ad0611 100644 --- a/spec/bundler/commands/version_spec.rb +++ b/spec/bundler/commands/version_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative '../support/path' +require_relative "../support/path" RSpec.describe "bundle version" do if Spec::Path.ruby_core? @@ -10,38 +10,56 @@ RSpec.describe "bundle version" do end context "with -v" do - it "outputs the version", :bundler => "< 3" do - bundle! "-v" - expect(out).to eq("Bundler version #{Bundler::VERSION}") - end + it "outputs the version and virtual version if set" do + bundle "-v" + expect(out).to eq(Bundler::VERSION.to_s) - it "outputs the version", :bundler => "3" do - bundle! "-v" - expect(out).to eq(Bundler::VERSION) + bundle_config "simulate_version 5" + bundle "-v" + expect(out).to eq("#{Bundler::VERSION} (simulating Bundler 5)") end end context "with --version" do - it "outputs the version", :bundler => "< 3" do - bundle! "--version" - expect(out).to eq("Bundler version #{Bundler::VERSION}") - end + it "outputs the version and virtual version if set" do + bundle "--version" + expect(out).to eq(Bundler::VERSION.to_s) - it "outputs the version", :bundler => "3" do - bundle! "--version" - expect(out).to eq(Bundler::VERSION) + bundle_config "simulate_version 5" + bundle "--version" + expect(out).to eq("#{Bundler::VERSION} (simulating Bundler 5)") end end context "with version" do - it "outputs the version with build metadata", :bundler => "< 3" do - bundle! "version" - expect(out).to match(/\ABundler version #{Regexp.escape(Bundler::VERSION)} \(\d{4}-\d{2}-\d{2} commit #{COMMIT_HASH}\)\z/) + context "when released", :ruby_repo do + before do + system_gems "bundler-4.9.9", released: true + end + + it "outputs the version, virtual version if set, and build metadata" do + bundle "version" + expect(out).to match(/\A4\.9\.9 \(2100-01-01 commit #{COMMIT_HASH}\)\z/) + + bundle_config "simulate_version 5" + bundle "version" + expect(out).to match(/\A4\.9\.9 \(simulating Bundler 5\) \(2100-01-01 commit #{COMMIT_HASH}\)\z/) + end end - it "outputs the version with build metadata", :bundler => "3" do - bundle! "version" - expect(out).to match(/\A#{Regexp.escape(Bundler::VERSION)} \(\d{4}-\d{2}-\d{2} commit #{COMMIT_HASH}\)\z/) + context "when not released" do + before do + system_gems "bundler-4.9.9", released: false + end + + it "outputs the version, virtual version if set, and build metadata" do + bundle "version" + expect(out).to match(/\A4\.9\.9 \(20\d{2}-\d{2}-\d{2} commit #{COMMIT_HASH}\)\z/) + + bundle_config "simulate_version 5" + bundle "version" + expect(out).to match(/\A4\.9\.9 \(simulating Bundler 5\) \(20\d{2}-\d{2}-\d{2} commit #{COMMIT_HASH}\)\z/) + end end end end diff --git a/spec/bundler/commands/viz_spec.rb b/spec/bundler/commands/viz_spec.rb deleted file mode 100644 index 029c3aca24..0000000000 --- a/spec/bundler/commands/viz_spec.rb +++ /dev/null @@ -1,149 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe "bundle viz", :bundler => "< 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_uri_for(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_uri_for(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_uri_for(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_uri_for(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_uri_for(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 index 8af88b7efe..c7ab7c3d7e 100644 --- a/spec/bundler/install/allow_offline_install_spec.rb +++ b/spec/bundler/install/allow_offline_install_spec.rb @@ -1,23 +1,19 @@ # frozen_string_literal: true -RSpec.describe "bundle install with :allow_offline_install" do - before do - bundle "config set allow_offline_install true" - end - +RSpec.describe "bundle install allows offline install" do context "with no cached data locally" do it "still installs" do - install_gemfile! <<-G, :artifice => "compact_index" + install_gemfile <<-G, artifice: "compact_index" source "http://testgemserver.local" - gem "rack-obama" + gem "myrack-obama" G - expect(the_bundle).to include_gem("rack 1.0") + expect(the_bundle).to include_gem("myrack 1.0") end it "still fails when the network is down" do - install_gemfile <<-G, :artifice => "fail" + install_gemfile <<-G, artifice: "fail", raise_on_error: false source "http://testgemserver.local" - gem "rack-obama" + gem "myrack-obama" G expect(err).to include("Could not reach host testgemserver.local.") expect(the_bundle).to_not be_locked @@ -26,34 +22,37 @@ RSpec.describe "bundle install with :allow_offline_install" do context "with cached data locally" do it "will install from the compact index" do - system_gems ["rack-1.0.0"], :path => :bundle_path + system_gems ["myrack-1.0.0"], path: default_bundle_path - bundle! "config set clean false" - install_gemfile! <<-G, :artifice => "compact_index" + bundle_config "clean false" + install_gemfile <<-G, artifice: "compact_index" source "http://testgemserver.local" - gem "rack-obama" - gem "rack", "< 1.0" + gem "myrack-obama" + gem "myrack", "< 1.0" G - expect(the_bundle).to include_gems("rack-obama 1.0", "rack 0.9.1") + expect(the_bundle).to include_gems("myrack-obama 1.0", "myrack 0.9.1") gemfile <<-G source "http://testgemserver.local" - gem "rack-obama" + gem "myrack-obama" G - bundle! :update, :artifice => "fail", :all => true - expect(last_command.stdboth).to include "Using the cached data for the new index because of a network error" + bundle :update, artifice: "fail", all: true + expect(stdboth).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") + expect(the_bundle).to include_gems("myrack-obama 1.0", "myrack 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) + f.puts <<~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? + fetch_args = %w(fetch --force --quiet --no-tags) + clone_args = %w(clone --bare --no-hardlinks --quiet) + + if (fetch_args.-(ARGV).empty? || clone_args.-(ARGV).empty?) && File.exist?(ARGV[ARGV.index("--") + 1]) warn "git remote ops have been disabled" exit 1 end @@ -70,18 +69,22 @@ RSpec.describe "bundle install with :allow_offline_install" do 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 + skip "doesn't print errors" if Gem.win_platform? + + git = build_git "a", "1.0.0", path: lib_path("a") + update_git("a", path: git.path, branch: "new_branch") + install_gemfile <<-G + source "https://gem.repo1" gem "a", :git => #{git.path.to_s.dump} G - break_git_remote_ops! { bundle! :update, :all => true } + break_git_remote_ops! { bundle :update, all: true } expect(err).to include("Using cached git data because of network errors") expect(the_bundle).to be_locked break_git_remote_ops! do - install_gemfile! <<-G + install_gemfile <<-G + source "https://gem.repo1" gem "a", :git => #{git.path.to_s.dump}, :branch => "new_branch" G end diff --git a/spec/bundler/install/binstubs_spec.rb b/spec/bundler/install/binstubs_spec.rb index 78ee893b81..c2eccb3ef2 100644 --- a/spec/bundler/install/binstubs_spec.rb +++ b/spec/bundler/install/binstubs_spec.rb @@ -2,20 +2,18 @@ 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 + expect(Pathname.new("/usr/bin")).not_to be_writable gemfile <<-G def Gem.bindir; "/usr/bin"; end - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - config "BUNDLE_SYSTEM_BINDIR" => system_gem_path("altbin").to_s + bundle_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 + expect(the_bundle).to include_gems "myrack 1.0.0" + expect(system_gem_path("altbin/myrackup")).to exist end end @@ -23,26 +21,26 @@ RSpec.describe "bundle install" do before do build_repo2 do build_gem "fake", "14" do |s| - s.executables = "rackup" + s.executables = "myrackup" end end install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "fake" - gem "rack" + gem "myrack" G end it "warns about the situation" do - bundle! "exec rackup" + bundle "exec myrackup" expect(last_command.stderr).to include( - "The `rackup` executable in the `fake` gem is being loaded, but it's also present in other gems (rack).\n" \ + "The `myrackup` executable in the `fake` gem is being loaded, but it's also present in other gems (myrack).\n" \ "If you meant to run the executable for another gem, make sure you use a project specific binstub (`bundle binstub <gem_name>`).\n" \ "If you plan to use multiple conflicting executables, generate binstubs for them and disambiguate their names." ).or include( - "The `rackup` executable in the `rack` gem is being loaded, but it's also present in other gems (fake).\n" \ + "The `myrackup` executable in the `myrack` gem is being loaded, but it's also present in other gems (fake).\n" \ "If you meant to run the executable for another gem, make sure you use a project specific binstub (`bundle binstub <gem_name>`).\n" \ "If you plan to use multiple conflicting executables, generate binstubs for them and disambiguate their names." ) diff --git a/spec/bundler/install/bundler_spec.rb b/spec/bundler/install/bundler_spec.rb index 6ea15d13b5..86c22dad55 100644 --- a/spec/bundler/install/bundler_spec.rb +++ b/spec/bundler/install/bundler_spec.rb @@ -5,7 +5,7 @@ RSpec.describe "bundle install" do before(:each) do build_repo2 do build_gem "rails", "3.0" do |s| - s.add_dependency "bundler", ">= 0.9.0.pre" + s.add_dependency "bundler", ">= 0.9.0" end build_gem "bundler", "0.9.1" build_gem "bundler", Bundler::VERSION @@ -14,57 +14,101 @@ RSpec.describe "bundle install" do it "are forced to the current bundler version" do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://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 + it "are forced to the current bundler version even if not already present" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - expect(the_bundle).not_to include_gems "bundler #{Bundler::VERSION}" + expect(the_bundle).to include_gems "bundler #{Bundler::VERSION}" end - it "causes a conflict if explicitly requesting a different version" do - bundle "config set force_ruby_platform true" + it "causes a conflict if explicitly requesting a different version of bundler" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gem "rails", "3.0" + gem "bundler", "0.9.1" + G - install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + nice_error = <<~E.strip + Could not find compatible versions + + Because the current Bundler version (#{Bundler::VERSION}) does not satisfy bundler = 0.9.1 + and Gemfile depends on bundler = 0.9.1, + version solving has failed. + + Your bundle requires a different version of Bundler than the one you're running. + Install the necessary version with `gem install bundler:0.9.1` and rerun bundler using `bundle _0.9.1_ install` + E + expect(err).to include(nice_error) + end + + it "causes a conflict if explicitly requesting a non matching requirement on bundler" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gem "rails", "3.0" + gem "bundler", "~> 0.8" + G + + nice_error = <<~E.strip + Could not find compatible versions + + Because rails >= 3.0 depends on bundler >= 0.9.0 + and the current Bundler version (#{Bundler::VERSION}) does not satisfy bundler >= 0.9.0, < 1.A, + rails >= 3.0 requires bundler >= 1.A. + So, because Gemfile depends on rails = 3.0 + and Gemfile depends on bundler ~> 0.8, + version solving has failed. + + Your bundle requires a different version of Bundler than the one you're running. + Install the necessary version with `gem install bundler:0.9.1` and rerun bundler using `bundle _0.9.1_ install` + E + expect(err).to include(nice_error) + end + + it "causes a conflict if explicitly requesting a version of bundler that doesn't exist" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" gem "rails", "3.0" gem "bundler", "0.9.2" G - nice_error = <<-E.strip.gsub(/^ {8}/, "") - Bundler could not find compatible versions for gem "bundler": - In Gemfile: - bundler (= 0.9.2) + nice_error = <<~E.strip + Could not find compatible versions - 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`? + Because the current Bundler version (#{Bundler::VERSION}) does not satisfy bundler = 0.9.2 + and Gemfile depends on bundler = 0.9.2, + version solving has failed. - Could not find gem 'bundler (= 0.9.2)' in any + Your bundle requires a different version of Bundler than the one you're running, and that version could not be found. E expect(err).to include(nice_error) end it "works for gems with multiple versions in its dependencies" do + build_repo2 do + build_gem "multiple_versioned_deps" do |s| + s.add_dependency "weakling", ">= 0.0.1", "< 0.1" + end + end + install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "multiple_versioned_deps" G install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "multiple_versioned_deps" - gem "rack" + gem "myrack" G expect(the_bundle).to include_gems "multiple_versioned_deps 1.0.0" @@ -72,7 +116,7 @@ RSpec.describe "bundle install" do it "includes bundler in the bundle when it's a child dependency" do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "rails", "3.0" G @@ -82,8 +126,8 @@ RSpec.describe "bundle install" do it "allows gem 'bundler' when Bundler is not in the Gemfile or its dependencies" do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack" + source "https://gem.repo2" + gem "myrack" G run "begin; gem 'bundler'; puts 'WIN'; rescue Gem::LoadError => e; puts e.backtrace; end" @@ -91,92 +135,134 @@ RSpec.describe "bundle install" do end it "causes a conflict if child dependencies conflict" do - bundle "config set force_ruby_platform true" + bundle_config "force_ruby_platform true" - install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + update_repo2 do + build_gem "rails_pinned_to_old_activesupport" do |s| + s.add_dependency "activesupport", "= 1.2.3" + end + end + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" gem "activemerchant" - gem "rails_fail" + gem "rails_pinned_to_old_activesupport" G - nice_error = <<-E.strip.gsub(/^ {8}/, "") - Bundler could not find compatible versions for gem "activesupport": - In Gemfile: - activemerchant was resolved to 1.0, which depends on - activesupport (>= 2.0.0) + nice_error = <<~E.strip + Could not find compatible versions - rails_fail was resolved to 1.0, which depends on - activesupport (= 1.2.3) + Because every version of rails_pinned_to_old_activesupport depends on activesupport = 1.2.3 + and every version of activemerchant depends on activesupport >= 2.0.0, + every version of rails_pinned_to_old_activesupport is incompatible with activemerchant >= 0. + So, because Gemfile depends on activemerchant >= 0 + and Gemfile depends on rails_pinned_to_old_activesupport >= 0, + version solving has failed. E expect(err).to include(nice_error) end it "causes a conflict if a child dependency conflicts with the Gemfile" do - bundle "config set force_ruby_platform true" + bundle_config "force_ruby_platform true" - install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rails_fail" + update_repo2 do + build_gem "rails_pinned_to_old_activesupport" do |s| + s.add_dependency "activesupport", "= 1.2.3" + end + end + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gem "rails_pinned_to_old_activesupport" gem "activesupport", "2.3.5" G - nice_error = <<-E.strip.gsub(/^ {8}/, "") - Bundler could not find compatible versions for gem "activesupport": - In Gemfile: - activesupport (= 2.3.5) + nice_error = <<~E.strip + Could not find compatible versions - rails_fail was resolved to 1.0, which depends on - activesupport (= 1.2.3) + Because every version of rails_pinned_to_old_activesupport depends on activesupport = 1.2.3 + and Gemfile depends on rails_pinned_to_old_activesupport >= 0, + activesupport = 1.2.3 is required. + So, because Gemfile depends on activesupport = 2.3.5, + version solving has failed. E expect(err).to include(nice_error) end - it "can install dependencies with newer bundler version with system gems" do - bundle! "config set path.system true" - install_gemfile! <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rails", "3.0" + it "does not cause a conflict if new dependencies in the Gemfile require older dependencies than the lockfile" do + update_repo2 do + build_gem "rails_pinned_to_old_activesupport" do |s| + s.add_dependency "activesupport", "= 1.2.3" + end + end + + install_gemfile <<-G + source "https://gem.repo2" + gem 'rails', "2.3.2" G - simulate_bundler_version "99999999.99.1" + install_gemfile <<-G + source "https://gem.repo2" + gem "rails_pinned_to_old_activesupport" + G - bundle! "check" - expect(out).to include("The Gemfile's dependencies are satisfied") + expect(out).to include("Installing activesupport 1.2.3 (was 2.3.2)") + expect(err).to be_empty end - it "can install dependencies with newer bundler version with a local path" do - bundle! "config set path .bundle" - install_gemfile! <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rails", "3.0" + it "prints the previous version when switching to a previously downloaded gem" do + build_repo4 do + build_gem "rails", "7.0.3" + build_gem "rails", "7.0.4" + end + + bundle_config "path.system true" + + install_gemfile <<-G + source "https://gem.repo4" + gem 'rails', "7.0.4" G - simulate_bundler_version "99999999.99.1" + install_gemfile <<-G + source "https://gem.repo4" + gem 'rails', "7.0.3" + G - bundle! "check" - expect(out).to include("The Gemfile's dependencies are satisfied") + install_gemfile <<-G + source "https://gem.repo4" + gem 'rails', "7.0.4" + G + + expect(out).to include("Using rails 7.0.4 (was 7.0.3)") + expect(err).to be_empty end - context "with allow_bundler_dependency_conflicts set" do - before { bundle! "config set allow_bundler_dependency_conflicts true" } + it "can install dependencies with newer bundler version with system gems" do + bundle_config "path.system true" - it "are forced to the current bundler version with warnings when no compatible version is found" do - build_repo4 do - build_gem "requires_nonexistant_bundler" do |s| - s.add_runtime_dependency "bundler", "99.99.99.99" - end - end + system_gems "bundler-99999999.99.1" - install_gemfile! <<-G - source "#{file_uri_for(gem_repo4)}" - gem "requires_nonexistant_bundler" - G + install_gemfile <<-G + source "https://gem.repo2" + gem "rails", "3.0" + G - expect(err).to include "requires_nonexistant_bundler (1.0) has dependency bundler (= 99.99.99.99), " \ - "which is unsatisfied by the current bundler version #{Bundler::VERSION}, so the dependency is being ignored" + bundle "check" + expect(out).to include("The Gemfile's dependencies are satisfied") + end - expect(the_bundle).to include_gems "bundler #{Bundler::VERSION}", "requires_nonexistant_bundler 1.0" - end + it "can install dependencies with newer bundler version with a local path" do + bundle_config "path .bundle" + + system_gems "bundler-99999999.99.1" + + install_gemfile <<-G + source "https://gem.repo2" + gem "rails", "3.0" + G + + bundle "check" + expect(out).to include("The Gemfile's dependencies are satisfied") end end end diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb new file mode 100644 index 0000000000..bad7b7cf34 --- /dev/null +++ b/spec/bundler/install/cooldown_spec.rb @@ -0,0 +1,433 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with the cooldown setting" do + before do + build_repo2 + end + + context "Gemfile DSL" do + it "accepts `source ..., cooldown: N` without error" do + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo2", cooldown: 5 + gem "myrack" + G + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "accepts `cooldown: 0` to disable cooldown for a source" do + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo2", cooldown: 0 + gem "myrack" + G + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + end + + context "CLI flag" do + before do + gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + end + + it "accepts --cooldown N on install" do + bundle "install --cooldown 7", artifice: "compact_index" + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "accepts --cooldown 0 as an escape hatch" do + bundle "install --cooldown 0", artifice: "compact_index" + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "rejects a negative --cooldown value" do + bundle "install --cooldown=-7", artifice: "compact_index", raise_on_error: false + + expect(err).to match(/non-negative integer/) + end + end + + context "configuration" do + it "reads BUNDLE_COOLDOWN as an integer" do + gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + + bundle "install", env: { "BUNDLE_COOLDOWN" => "7" }, artifice: "compact_index" + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "reads `bundle config set cooldown N`" do + gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + + bundle "config set cooldown 7" + bundle "install", artifice: "compact_index" + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + end + + context "end-to-end with v2 compact index" do + before do + now = Time.now.utc + build_repo3 do + build_gem "ripe_gem", "1.0.0" do |s| + s.date = now - (30 * 86_400) + end + build_gem "ripe_gem", "2.0.0" do |s| + s.date = now - (1 * 86_400) + end + + # parent only resolves with the in-cooldown child 2.0.0 + build_gem "child", "1.0.0" do |s| + s.date = now - (30 * 86_400) + end + build_gem "child", "2.0.0" do |s| + s.date = now - (1 * 86_400) + end + build_gem "parent", "1.0.0" do |s| + s.add_dependency "child", ">= 2.0.0" + s.date = now - (30 * 86_400) + end + + # a cooldown-eligible version exists above the in-cooldown locked one + build_gem "upgradable", "2.0.0" do |s| + s.date = now - (1 * 86_400) + end + build_gem "upgradable", "3.0.0" do |s| + s.date = now - (30 * 86_400) + end + end + end + + it "excludes versions within the cooldown window" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "selects the latest version when --cooldown 0 is passed" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "install --cooldown 0", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end + + it "applies cooldown declared per-source in the Gemfile" do + gemfile <<-G + source "https://gem.repo3", cooldown: 7 + gem "ripe_gem" + G + + bundle "install", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "is overridden by CLI --cooldown when Gemfile sets a different per-source value" do + gemfile <<-G + source "https://gem.repo3", cooldown: 0 + gem "ripe_gem" + G + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "bypasses cooldown when bundle install uses an existing lockfile" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end + + it "annotates in-cooldown versions in bundle outdated table output" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem", "1.0.0" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem (= 1.0.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "outdated --cooldown 7", artifice: "compact_index_cooldown", raise_on_error: false + + expect(out).to match(/ripe_gem.*\(cooldown \d+d\)/) + end + + it "annotates in-cooldown versions in bundle outdated --parseable output" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem", "1.0.0" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem (= 1.0.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "outdated --cooldown 7 --parseable", artifice: "compact_index_cooldown", raise_on_error: false + + expect(out).to match(/ripe_gem.*in cooldown for \d+ more day/) + end + + it "excludes a locally-installed version that is still within the cooldown window" do + system_gems "ripe_gem-2.0.0", gem_repo: gem_repo3 + + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "selects a locally-installed in-cooldown version when --cooldown 0 bypasses the filter" do + system_gems "ripe_gem-2.0.0", gem_repo: gem_repo3 + + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "install --cooldown 0", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end + + it "surfaces a cooldown hint when bundle update filters every candidate" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update ripe_gem --cooldown 99999", artifice: "compact_index_cooldown", raise_on_error: false + + expect(err).to match(/excluded by the cooldown setting/) + expect(err).to match(/--cooldown 0/) + end + + it "keeps an in-cooldown locked version on bundle update --all instead of failing" do + # Lockfile written before cooldown was enabled pins the now-in-cooldown + # latest version. A full update must not downgrade below it, and cooldown + # must not filter it out, otherwise resolution becomes impossible (#9598). + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update --all --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end + + it "does not fail bundle outdated when the locked version is in cooldown" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "outdated --cooldown 7", artifice: "compact_index_cooldown", raise_on_error: false + + # exit 0 means no outdated gems and, crucially, no resolution failure (exit 7) + expect(exitstatus).to eq(0) + end + + it "still applies cooldown and downgrades a gem that is updated explicitly" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update ripe_gem --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "keeps an in-cooldown transitive dependency on bundle update --all" do + gemfile <<-G + source "https://gem.repo3" + gem "parent" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + child (2.0.0) + parent (1.0.0) + child (>= 2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + parent + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update --all --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("parent 1.0.0", "child 2.0.0") + end + + it "still upgrades to a cooldown-eligible version above the locked one" do + gemfile <<-G + source "https://gem.repo3" + gem "upgradable" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + upgradable (2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + upgradable + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update --all --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("upgradable 3.0.0") + end + end +end diff --git a/spec/bundler/install/deploy_spec.rb b/spec/bundler/install/deploy_spec.rb index d607f8bb46..a3b4a87ecf 100644 --- a/spec/bundler/install/deploy_spec.rb +++ b/spec/bundler/install/deploy_spec.rb @@ -1,107 +1,139 @@ # frozen_string_literal: true -RSpec.describe "install with --deployment or --frozen" do +RSpec.describe "install in deployment or frozen mode" do before do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G end - context "with CLI flags", :bundler => "< 3" do - it "fails without a lockfile and says that --deployment requires a lock" do - bundle "install --deployment" - expect(err).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(err).to include("The --frozen flag requires a Gemfile.lock") - end - - it "disallows --deployment --system" do - bundle "install --deployment --system" - expect(err).to include("You have specified both --deployment") - expect(err).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(err).to include("You have specified both --path") - expect(err).to include("as well as --system") - expect(err).to include("Please choose only one option") - expect(exitstatus).to eq(15) if exitstatus - end + it "fails without a lockfile and says that deployment requires a lock" do + bundle_config "deployment true" + bundle "install", raise_on_error: false + expect(err).to include("The deployment setting requires a lockfile") + end - it "works after you try to deploy without a lock" do - bundle "install --deployment" - bundle! :install - expect(the_bundle).to include_gems "rack 1.0" - end + it "fails without a lockfile and says that frozen requires a lock" do + bundle_config "frozen true" + bundle "install", raise_on_error: false + expect(err).to include("The frozen setting requires a lockfile") end it "still works if you are not in the app directory and specify --gemfile" do bundle "install" - Dir.chdir tmp do - simulate_new_machine - bundle! :install, - forgotten_command_line_options(:gemfile => "#{tmp}/bundled_app/Gemfile", - :deployment => true, - :path => "vendor/bundle") - end - expect(the_bundle).to include_gems "rack 1.0" + pristine_system_gems + bundle_config "deployment true" + bundle_config "path vendor/bundle" + bundle "install --gemfile #{tmp}/bundled_app/Gemfile", dir: tmp + expect(the_bundle).to include_gems "myrack 1.0" end it "works if you exclude a group with a git gem" do build_git "foo" gemfile <<-G + source "https://gem.repo1" group :test do gem "foo", :git => "#{lib_path("foo-1.0")}" end G bundle :install - bundle! :install, forgotten_command_line_options(:deployment => true, :without => "test") + bundle_config "deployment true" + bundle_config "without test" + bundle :install end it "works when you bundle exec bundle" do + skip "doesn't find bundle" if Gem.win_platform? + + bundle :install + bundle_config "deployment true" bundle :install - bundle "install --deployment" - bundle! "exec bundle check" + bundle "exec bundle check", env: { "PATH" => path } 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") + build_lib "foo", path: lib_path("nested/foo") + build_lib "bar", path: lib_path("nested/bar") gemfile <<-G + source "https://gem.repo1" gem "foo", "1.0", :path => "#{lib_path("nested")}" gem "bar", :path => "#{lib_path("nested")}" G - bundle! :install - bundle! :install, forgotten_command_line_options(:deployment => true) + bundle :install + bundle_config "deployment true" + bundle :install + end + + it "works when path gems are specified twice" do + build_lib "foo", path: lib_path("nested/foo") + gemfile <<-G + source "https://gem.repo1" + gem "foo", :path => "#{lib_path("nested/foo")}" + gem "foo", :path => "#{lib_path("nested/foo")}" + G + + bundle :install + bundle_config "deployment true" + bundle :install end it "works when there are credentials in the source URL" do - install_gemfile(<<-G, :artifice => "endpoint_strict_basic_authentication", :quiet => true) + install_gemfile(<<-G, artifice: "endpoint_strict_basic_authentication", quiet: true) source "http://user:pass@localgemserver.test/" - gem "rack-obama", ">= 1.0" + gem "myrack-obama", ">= 1.0" G - bundle! :install, forgotten_command_line_options(:deployment => true).merge(:artifice => "endpoint_strict_basic_authentication") + bundle_config "deployment true" + bundle :install, artifice: "endpoint_strict_basic_authentication" end it "works with sources given by a block" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" do - gem "rack" + install_gemfile <<-G + source "https://gem.repo1" + source "https://gem.repo1" do + gem "myrack" end G - bundle! :install, forgotten_command_line_options(:deployment => true) + bundle_config "deployment true" + bundle :install + + expect(the_bundle).to include_gems "myrack 1.0" + end + + context "when replacing a host with the same host with credentials" do + before do + bundle_config "path vendor/bundle" + bundle "install" + gemfile <<-G + source "http://user_name:password@localgemserver.test/" + gem "myrack" + G + + lockfile <<-G + GEM + remote: http://localgemserver.test/ + specs: + myrack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + myrack + G + + bundle_config "deployment true" + end + + it "allows the replace" do + bundle :install - expect(the_bundle).to include_gems "rack 1.0" + expect(out).to match(/Bundle complete!/) + end end describe "with an existing lockfile" do @@ -109,297 +141,349 @@ RSpec.describe "install with --deployment or --frozen" do bundle "install" end - it "works with the --deployment flag if you didn't change anything", :bundler => "< 3" do - bundle! "install --deployment" + it "installs gems by default to vendor/bundle" do + bundle_config "deployment true" + expect do + bundle "install" + end.not_to change { bundled_app_lock.mtime } + expect(out).to include("vendor/bundle") + end + + it "installs gems to custom path if specified" do + bundle_config "path vendor/bundle2" + bundle_config "deployment true" + bundle "install" + expect(out).to include("vendor/bundle2") + end + + it "installs gems to custom path if specified, even when configured through ENV" do + bundle_config "deployment true" + bundle "install", env: { "BUNDLE_PATH" => "vendor/bundle2" } + expect(out).to include("vendor/bundle2") end - it "works with the --frozen flag if you didn't change anything", :bundler => "< 3" do - bundle! "install --frozen" + it "works with the `frozen` setting" do + bundle_config "frozen true" + expect do + bundle "install" + end.not_to change { bundled_app_lock.mtime } end it "works with BUNDLE_FROZEN if you didn't change anything" do - bundle! :install, :env => { "BUNDLE_FROZEN" => "true" } + expect do + bundle :install, env: { "BUNDLE_FROZEN" => "true" } + end.not_to change { bundled_app_lock.mtime } end - it "explodes with the --deployment flag if you make a change and don't check in the lockfile" do + it "explodes with the `deployment` setting if you make a change and don't check in the lockfile" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - gem "rack-obama" + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" G - bundle :install, forgotten_command_line_options(:deployment => true) - expect(err).to include("deployment mode") + bundle_config "deployment true" + bundle :install, raise_on_error: false + expect(err).to include("frozen mode") expect(err).to include("You have added to the Gemfile") - expect(err).to include("* rack-obama") + expect(err).to include("* myrack-obama") expect(err).not_to include("You have deleted from the Gemfile") expect(err).not_to include("You have changed in the Gemfile") end it "works if a path gem is missing but is in a without group" do build_lib "path_gem" - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G + source "https://gem.repo1" gem "rake" gem "path_gem", :path => "#{lib_path("path_gem-1.0")}", :group => :development G expect(the_bundle).to include_gems "path_gem 1.0" FileUtils.rm_r lib_path("path_gem-1.0") - bundle! :install, forgotten_command_line_options(:path => ".bundle", :without => "development", :deployment => true).merge(:env => { :DEBUG => "1" }) - run! "puts :WIN" + bundle_config "path .bundle" + bundle_config "without development" + bundle_config "deployment true" + bundle :install, env: { "DEBUG" => "1" } + run "puts :WIN" expect(out).to eq("WIN") end + it "works if a gem is missing, but it's on a different platform" do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + + source "https://gem.repo1" do + gem "rake", platform: :#{not_local_tag} + end + G + + bundle :install, env: { "BUNDLE_FROZEN" => "true" } + expect(last_command).to be_success + end + + it "shows a good error if a gem is missing from the lockfile" do + build_repo4 do + build_gem "foo" + build_gem "bar" + end + + gemfile <<-G + source "https://gem.repo4" + + gem "foo" + gem "bar" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo + bar + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle :install, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false, artifice: "compact_index" + expect(err).to include("Your lockfile is missing \"bar\", but can't be updated because frozen mode is set") + end + it "explodes if a path gem is missing" do build_lib "path_gem" - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G + source "https://gem.repo1" gem "rake" gem "path_gem", :path => "#{lib_path("path_gem-1.0")}", :group => :development G expect(the_bundle).to include_gems "path_gem 1.0" FileUtils.rm_r lib_path("path_gem-1.0") - bundle :install, forgotten_command_line_options(:path => ".bundle", :deployment => true) + bundle_config "path .bundle" + bundle_config "deployment true" + bundle :install, raise_on_error: false expect(err).to include("The path `#{lib_path("path_gem-1.0")}` does not exist.") end - it "can have --frozen set via an environment variable", :bundler => "< 3" do + it "can have --frozen set via an environment variable" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - gem "rack-obama" + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" G ENV["BUNDLE_FROZEN"] = "1" - bundle "install" - expect(err).to include("deployment mode") + bundle "install", raise_on_error: false + expect(err).to include("frozen mode") expect(err).to include("You have added to the Gemfile") - expect(err).to include("* rack-obama") + expect(err).to include("* myrack-obama") expect(err).not_to include("You have deleted from the Gemfile") expect(err).not_to include("You have changed in the Gemfile") end it "can have --deployment set via an environment variable" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - gem "rack-obama" + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" G ENV["BUNDLE_DEPLOYMENT"] = "true" - bundle "install" - expect(err).to include("deployment mode") + bundle "install", raise_on_error: false + expect(err).to include("frozen mode") expect(err).to include("You have added to the Gemfile") - expect(err).to include("* rack-obama") + expect(err).to include("* myrack-obama") expect(err).not_to include("You have deleted from the Gemfile") expect(err).not_to include("You have changed in the Gemfile") end + it "installs gems by default to vendor/bundle when deployment mode is set via an environment variable" do + ENV["BUNDLE_DEPLOYMENT"] = "true" + bundle "install" + expect(out).to include("vendor/bundle") + end + + it "installs gems to custom path when deployment mode is set via an environment variable " do + ENV["BUNDLE_DEPLOYMENT"] = "true" + ENV["BUNDLE_PATH"] = "vendor/bundle2" + bundle "install" + expect(out).to include("vendor/bundle2") + end + it "can have --frozen set to false via an environment variable" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - gem "rack-obama" + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" G ENV["BUNDLE_FROZEN"] = "false" ENV["BUNDLE_DEPLOYMENT"] = "false" bundle "install" - expect(out).not_to include("deployment mode") + expect(out).not_to include("frozen mode") expect(out).not_to include("You have added to the Gemfile") - expect(out).not_to include("* rack-obama") + expect(out).not_to include("* myrack-obama") end - it "explodes if you remove a gem and don't check in the lockfile" do + it "explodes if you replace a gem and don't check in the lockfile" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "activesupport" G - bundle :install, forgotten_command_line_options(:deployment => true) - expect(err).to include("deployment mode") + bundle_config "deployment true" + bundle :install, raise_on_error: false + expect(err).to include("frozen mode") expect(err).to include("You have added to the Gemfile:\n* activesupport\n\n") - expect(err).to include("You have deleted from the Gemfile:\n* rack") + expect(err).to include("You have deleted from the Gemfile:\n* myrack") + expect(err).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 'source "https://gem.repo1"' + + bundle_config "deployment true" + bundle :install, raise_on_error: false + expect(err).to include("Some dependencies were deleted") + expect(err).to include("frozen mode") + expect(err).to include("You have deleted from the Gemfile:\n* myrack") expect(err).not_to include("You have changed in the Gemfile") end it "explodes if you add a source" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "git://hubz.com" + source "https://gem.repo1" + gem "myrack", :git => "git://hubz.com" G - bundle :install, forgotten_command_line_options(:deployment => true) - expect(err).to include("deployment mode") - expect(err).to include("You have added to the Gemfile:\n* source: git://hubz.com (at master)") - expect(err).not_to include("You have changed in the Gemfile") + bundle_config "deployment true" + bundle :install, raise_on_error: false + expect(err).to include("frozen mode") + expect(err).not_to include("You have added to the Gemfile") + expect(err).to include("You have changed in the Gemfile:\n* myrack from `no specified source` to `git://hubz.com`") end - it "explodes if you unpin a source" do - build_git "rack" + it "explodes if you change a source from git to the default" do + build_git "myrack" install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-1.0")}" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-1.0")}" G gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle :install, forgotten_command_line_options(:deployment => true) - expect(err).to include("deployment mode") - expect(err).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]}") + bundle_config "deployment true" + bundle :install, raise_on_error: false + expect(err).to include("frozen mode") + expect(err).not_to include("You have deleted from the Gemfile") expect(err).not_to include("You have added to the Gemfile") - expect(err).not_to include("You have changed in the Gemfile") + expect(err).to include("You have changed in the Gemfile:\n* myrack from `#{lib_path("myrack-1.0")}` to `no specified source`") 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") + it "explodes if you change a source from git to the default, in presence of other git sources" do + build_lib "foo", path: lib_path("myrack/foo") + build_git "myrack", path: lib_path("myrack") install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack")}" - gem "foo", :git => "#{lib_path("rack")}" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack")}" + gem "foo", :git => "#{lib_path("myrack")}" G gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - gem "foo", :git => "#{lib_path("rack")}" + source "https://gem.repo1" + gem "myrack" + gem "foo", :git => "#{lib_path("myrack")}" G - bundle :install, forgotten_command_line_options(:deployment => true) - expect(err).to include("deployment mode") - expect(err).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]})`") + bundle_config "deployment true" + bundle :install, raise_on_error: false + expect(err).to include("frozen mode") + expect(err).to include("You have changed in the Gemfile:\n* myrack from `#{lib_path("myrack")}` to `no specified source`") expect(err).not_to include("You have added to the Gemfile") expect(err).not_to include("You have deleted from the Gemfile") end - context "when replacing a host with the same host with credentials" do - let(:success_message) do - "Bundle complete!" - end - - before do - install_gemfile <<-G - source "http://user_name:password@localgemserver.test/" - gem "rack" - G - - lockfile <<-G - GEM - remote: http://localgemserver.test/ - specs: - rack (1.0.0) + it "explodes if you change a source from path to git" do + build_git "myrack", path: lib_path("myrack") - PLATFORMS - #{local} - - DEPENDENCIES - rack - G - - bundle! "config set --local deployment true" - end - - it "prevents the replace by default" do - bundle :install - - expect(err).to match(/The list of sources changed/) - end - - context "when allow_deployment_source_credential_changes is true" do - before { bundle! "config set allow_deployment_source_credential_changes true" } - - it "allows the replace" do - bundle :install - - expect(out).to match(/#{success_message}/) - end - end - - context "when allow_deployment_source_credential_changes is false" do - before { bundle! "config set allow_deployment_source_credential_changes false" } - - it "prevents the replace" do - bundle :install - - expect(err).to match(/The list of sources changed/) - end - end - - context "when BUNDLE_ALLOW_DEPLOYMENT_SOURCE_CREDENTIAL_CHANGES env var is true" do - before { ENV["BUNDLE_ALLOW_DEPLOYMENT_SOURCE_CREDENTIAL_CHANGES"] = "true" } - - it "allows the replace" do - bundle :install - - expect(out).to match(/#{success_message}/) - end - end - - context "when BUNDLE_ALLOW_DEPLOYMENT_SOURCE_CREDENTIAL_CHANGES env var is false" do - before { ENV["BUNDLE_ALLOW_DEPLOYMENT_SOURCE_CREDENTIAL_CHANGES"] = "false" } + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :path => "#{lib_path("myrack")}" + G - it "prevents the replace" do - bundle :install + gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "https:/my-git-repo-for-myrack" + G - expect(err).to match(/The list of sources changed/) - end - end + bundle_config "frozen true" + bundle :install, raise_on_error: false + expect(err).to include("frozen mode") + expect(err).to include("You have changed in the Gemfile:\n* myrack from `#{lib_path("myrack")}` to `https:/my-git-repo-for-myrack`") + expect(err).not_to include("You have added to the Gemfile") + expect(err).not_to include("You have deleted from the Gemfile") end it "remembers that the bundle is frozen at runtime" do - bundle! :lock + bundle :lock - bundle! "config set --local deployment true" + bundle_config "deployment true" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "1.0.0" - gem "rack-obama" + source "https://gem.repo1" + gem "myrack", "1.0.0" + gem "myrack-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 + run "require 'myrack'", raise_on_error: false + expect(err).to include <<~E.strip + The dependencies in your gemfile changed, but the lockfile can't be updated because frozen mode is set (Bundler::ProductionError) -You have added to the Gemfile: -* rack (= 1.0.0) -* rack-obama + You have added to the Gemfile: + * myrack (= 1.0.0) + * myrack-obama -You have deleted from the Gemfile: -* rack + You have deleted from the Gemfile: + * myrack 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 + build_lib "foo", path: lib_path("foo") + install_gemfile <<-G + source "https://gem.repo1" gem "foo", :path => "#{lib_path("foo")}" G - bundle! :install + bundle :install expect(the_bundle).to include_gems "foo 1.0" - bundle "config set cache_all true" - bundle! :cache + bundle :cache expect(bundled_app("vendor/cache/foo")).to be_directory - bundle! "install --local" + bundle "install --local" expect(out).to include("Updating files in vendor/cache") - simulate_new_machine - bundle! "config set --local deployment true" - bundle! "install --verbose" - expect(out).not_to include("You are trying to install in deployment mode after changing your Gemfile") + pristine_system_gems + bundle_config "deployment true" + bundle "install --verbose" + expect(out).not_to include("can't be updated because frozen mode is set") 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("vendor/cache/foo") diff --git a/spec/bundler/install/failure_spec.rb b/spec/bundler/install/failure_spec.rb index 57ffafd588..32ca455439 100644 --- a/spec/bundler/install/failure_spec.rb +++ b/spec/bundler/install/failure_spec.rb @@ -2,7 +2,7 @@ RSpec.describe "bundle install" do context "installing a gem fails" do - it "prints out why that gem was being installed" do + it "prints out why that gem was being installed and the underlying error" do build_repo2 do build_gem "activesupport", "2.3.2" do |s| s.extensions << "Rakefile" @@ -14,13 +14,13 @@ RSpec.describe "bundle install" do end end - install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" gem "rails" G + expect(err).to start_with("Gem::Ext::BuildError: ERROR: Failed to build gem native extension.") expect(err).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' --source '#{file_uri_for(gem_repo2)}/'` succeeds before bundling. In Gemfile: rails was resolved to 2.3.2, which depends on @@ -29,116 +29,57 @@ In Gemfile: M end - context "when installing a git gem" do - it "does not tell the user to run 'gem install'" do - build_git "activesupport", "2.3.2", :path => lib_path("activesupport") do |s| - s.extensions << "Rakefile" - s.write "Rakefile", <<-RUBY - task :default do - abort "make installing activesupport-2.3.2 fail" - end - RUBY + context "because the downloaded .gem was invalid" do + before do + build_repo4 do + build_gem "a" end - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rails" - gem "activesupport", :git => "#{lib_path("activesupport")}" - G - - expect(err).to end_with(<<-M.strip) -An error occurred while installing activesupport (2.3.2), and Bundler cannot continue. - -In Gemfile: - rails was resolved to 2.3.2, which depends on - actionmailer was resolved to 2.3.2, which depends on - activesupport - M + gem_repo4("gems", "a-1.0.gem").open("w") {|f| f << "<html></html>" } end - end - - context "when installing a gem using a git block" do - it "does not tell the user to run 'gem install'" do - build_git "activesupport", "2.3.2", :path => lib_path("activesupport") do |s| - s.extensions << "Rakefile" - s.write "Rakefile", <<-RUBY - task :default do - abort "make installing activesupport-2.3.2 fail" - end - RUBY - end - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rails" - - git "#{lib_path("activesupport")}" do - gem "activesupport" - end + it "removes the downloaded .gem" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo4" + gem "a" G - expect(err).to end_with(<<-M.strip) -An error occurred while installing activesupport (2.3.2), and Bundler cannot continue. - - -In Gemfile: - rails was resolved to 2.3.2, which depends on - actionmailer was resolved to 2.3.2, which depends on - activesupport - M + expect(default_bundle_path("cache", "a-1.0.gem")).not_to exist end end + end - it "prints out the hint for the remote source when available" 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 + context "when lockfile dependencies don't match the gemspec" do + before do + build_repo4 do + build_gem "myrack", "1.0.0" do |s| + s.add_dependency "myrack-test", "~> 1.0" end - end - build_repo4 do - build_gem "a" + build_gem "myrack-test", "1.0.0" end - install_gemfile <<-G - source "#{file_uri_for(gem_repo4)}" - source "#{file_uri_for(gem_repo2)}" do - gem "rails" - end + gemfile <<-G + source "https://gem.repo4" + gem "myrack" G - expect(err).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' --source '#{file_uri_for(gem_repo2)}/'` 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 - context "because the downloaded .gem was invalid" do - before do - build_repo4 do - build_gem "a" - end + # First install to generate lockfile + bundle :install - gem_repo4("gems", "a-1.0.gem").open("w") {|f| f << "<html></html>" } - end + # Manually edit lockfile to have incorrect dependencies + lockfile_content = File.read(bundled_app_lock) + # Remove the myrack-test dependency from myrack + lockfile_content.gsub!(/^ myrack \(1\.0\.0\)\n myrack-test \(~> 1\.0\)\n/, " myrack (1.0.0)\n") + File.write(bundled_app_lock, lockfile_content) + end - it "removes the downloaded .gem" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo4)}" - gem "a" - G + it "reports the mismatch with detailed information" do + bundle :install, raise_on_error: false, env: { "BUNDLE_FROZEN" => "true" } - expect(default_bundle_path("cache", "a-1.0.gem")).not_to exist - end + expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack-1.0.0") + expect(err).to include("myrack-test: gemspec specifies ~> 1.0, not in lockfile") + expect(err).to include("Please run `bundle install` to regenerate the lockfile.") 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..e0f6fb6364 --- /dev/null +++ b/spec/bundler/install/force_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install" do + before :each do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + end + + shared_examples_for "an option to force reinstalling gems" do + it "re-installs installed gems" do + myrack_lib = default_bundle_path("gems/myrack-1.0.0/lib/myrack.rb") + + bundle :install + myrack_lib.open("w") {|f| f.write("blah blah blah") } + bundle :install, flag => true + + expect(out).to include "Installing myrack 1.0.0" + expect(myrack_lib.open(&:read)).to eq("MYRACK = '1.0.0'\n") + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "works on first bundle install" do + bundle :install, flag => true + + expect(out).to include "Installing myrack 1.0.0" + expect(the_bundle).to include_gems "myrack 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 + source "https://gem.repo1" + 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, flag => true + + 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, flag => true + + expect(the_bundle).to include_gems "foo 1.0" + end + end + end + + describe "with --force" do + it_behaves_like "an option to force reinstalling gems" do + let(:flag) { "force" } + end + end + + describe "with --redownload" do + it_behaves_like "an option to force reinstalling gems" do + let(:flag) { "redownload" } + end + end +end diff --git a/spec/bundler/install/gemfile/eval_gemfile_spec.rb b/spec/bundler/install/gemfile/eval_gemfile_spec.rb index 7df94aaff5..3afa4f5daa 100644 --- a/spec/bundler/install/gemfile/eval_gemfile_spec.rb +++ b/spec/bundler/install/gemfile/eval_gemfile_spec.rb @@ -2,7 +2,7 @@ RSpec.describe "bundle install with gemfile that uses eval_gemfile" do before do - build_lib("gunks", :path => bundled_app.join("gems/gunks")) do |s| + build_lib("gunks", path: bundled_app("gems/gunks")) do |s| s.name = "gunks" s.version = "0.0.1" end @@ -10,44 +10,83 @@ RSpec.describe "bundle install with gemfile that uses eval_gemfile" do context "eval-ed Gemfile points to an internal gemspec" do before do - create_file "Gemfile-other", <<-G + gemfile "Gemfile-other", <<-G + source "https://gem.repo1" gemspec :path => 'gems/gunks' G end it "installs the gemspec specified gem" do install_gemfile <<-G + source "https://gem.repo1" eval_gemfile 'Gemfile-other' G expect(out).to include("Resolving dependencies") expect(out).to include("Bundle complete") - expect(the_bundle).to include_gem "gunks 0.0.1", :source => "path@#{bundled_app("gems", "gunks")}" + expect(the_bundle).to include_gem "gunks 0.0.1", source: "path@#{bundled_app("gems", "gunks")}" + end + end + + context "eval-ed Gemfile points to an internal gemspec and uses a scoped source that duplicates the main Gemfile global source" do + before do + build_repo2 do + build_gem "rails", "6.1.3.2" + + build_gem "zip-zip", "0.3" + end + + gemfile bundled_app("gems/Gemfile"), <<-G + source "https://gem.repo2" + + gemspec :path => "\#{__dir__}/gunks" + + source "https://gem.repo2" do + gem "zip-zip" + end + G + end + + it "installs and finds gems correctly" do + install_gemfile <<-G + source "https://gem.repo2" + + gem "rails" + + eval_gemfile File.join(__dir__, "gems/Gemfile") + G + expect(out).to include("Resolving dependencies") + expect(out).to include("Bundle complete") + + expect(the_bundle).to include_gem "rails 6.1.3.2" 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 + build_lib("a", path: bundled_app("gems/a")) + gemfile bundled_app("nested/Gemfile-nested"), <<-G + source "https://gem.repo1" gem "a", :path => "../gems/a" G gemfile <<-G + source "https://gem.repo1" eval_gemfile "nested/Gemfile-nested" G end it "installs the path gem" do - bundle! :install + 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, forgotten_command_line_options(:deployment => true) + it "bundles with deployment mode configured" do + bundle :install + bundle_config "deployment true" + bundle :install end end @@ -56,27 +95,28 @@ RSpec.describe "bundle install with gemfile that uses eval_gemfile" do it "installs the gemspec specified gem" do install_gemfile <<-G + source "https://gem.repo1" eval_gemfile 'other/Gemfile-other' gemspec :path => 'gems/gunks' G expect(out).to include("Resolving dependencies") expect(out).to include("Bundle complete") - expect(the_bundle).to include_gem "gunks 0.0.1", :source => "path@#{bundled_app("gems", "gunks")}" + expect(the_bundle).to include_gem "gunks 0.0.1", source: "path@#{bundled_app("gems", "gunks")}" end end context "eval-ed Gemfile references other gemfiles" do it "works with relative paths" do - create_file "other/Gemfile-other", "gem 'rack'" - create_file "other/Gemfile", "eval_gemfile 'Gemfile-other'" - create_file "Gemfile-alt", <<-G - source "#{file_uri_for(gem_repo1)}" + gemfile "other/Gemfile-other", "gem 'myrack'" + gemfile "other/Gemfile", "eval_gemfile 'Gemfile-other'" + gemfile "Gemfile-alt", <<-G + source "https://gem.repo1" eval_gemfile "other/Gemfile" G - install_gemfile! "eval_gemfile File.expand_path('Gemfile-alt')" + install_gemfile "eval_gemfile File.expand_path('Gemfile-alt')" - expect(the_bundle).to include_gem "rack 1.0.0" + expect(the_bundle).to include_gem "myrack 1.0.0" end end end diff --git a/spec/bundler/install/gemfile/force_ruby_platform_spec.rb b/spec/bundler/install/gemfile/force_ruby_platform_spec.rb new file mode 100644 index 0000000000..bcc1f36823 --- /dev/null +++ b/spec/bundler/install/gemfile/force_ruby_platform_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with force_ruby_platform DSL option", :jruby do + context "when no transitive deps" do + before do + build_repo4 do + # Build a gem with platform specific versions + build_gem("platform_specific") + + build_gem("platform_specific") do |s| + s.platform = Bundler.local_platform + end + + # Build the exact same gem with a different name to compare using vs not using the option + build_gem("platform_specific_forced") + + build_gem("platform_specific_forced") do |s| + s.platform = Bundler.local_platform + end + end + end + + it "pulls the pure ruby variant of the given gem" do + install_gemfile <<-G + source "https://gem.repo4" + + gem "platform_specific_forced", :force_ruby_platform => true + gem "platform_specific" + G + + expect(the_bundle).to include_gems "platform_specific_forced 1.0 ruby" + expect(the_bundle).to include_gems "platform_specific 1.0 #{Bundler.local_platform}" + end + + it "still respects a global `force_ruby_platform` config" do + install_gemfile <<-G, env: { "BUNDLE_FORCE_RUBY_PLATFORM" => "true" } + source "https://gem.repo4" + + gem "platform_specific_forced", :force_ruby_platform => true + gem "platform_specific" + G + + expect(the_bundle).to include_gems "platform_specific_forced 1.0 ruby" + expect(the_bundle).to include_gems "platform_specific 1.0 ruby" + end + end + + context "when also a transitive dependency" do + before do + build_repo4 do + build_gem("depends_on_platform_specific") {|s| s.add_dependency "platform_specific" } + + build_gem("platform_specific") + + build_gem("platform_specific") do |s| + s.platform = Bundler.local_platform + end + end + end + + it "still pulls the ruby variant" do + install_gemfile <<-G + source "https://gem.repo4" + + gem "depends_on_platform_specific" + gem "platform_specific", :force_ruby_platform => true + G + + expect(the_bundle).to include_gems "platform_specific 1.0 ruby" + end + end + + context "with transitive dependencies with platform specific versions" do + before do + build_repo4 do + build_gem("depends_on_platform_specific") do |s| + s.add_dependency "platform_specific" + end + + build_gem("depends_on_platform_specific") do |s| + s.add_dependency "platform_specific" + s.platform = Bundler.local_platform + end + + build_gem("platform_specific") + + build_gem("platform_specific") do |s| + s.platform = Bundler.local_platform + end + end + end + + it "ignores ruby variants for the transitive dependencies" do + install_gemfile <<-G, env: { "DEBUG_RESOLVER" => "true" } + source "https://gem.repo4" + + gem "depends_on_platform_specific", :force_ruby_platform => true + G + + expect(the_bundle).to include_gems "depends_on_platform_specific 1.0 ruby" + expect(the_bundle).to include_gems "platform_specific 1.0 #{Bundler.local_platform}" + end + + it "reinstalls the ruby variant when a platform specific variant is already installed, the lockile has only ruby platform, and :force_ruby_platform is used in the Gemfile" do + skip "Can't simulate platform reliably on JRuby, installing a platform specific gem fails to activate io-wait because only the -java version is present, and we're simulating a different platform" if RUBY_ENGINE == "jruby" + + lockfile <<-L + GEM + remote: https://gem.repo4 + specs: + platform_specific (1.0) + + PLATFORMS + ruby + + DEPENDENCIES + platform_specific + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86-darwin-100" do + system_gems "platform_specific-1.0-x86-darwin-100", path: default_bundle_path + + install_gemfile <<-G, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }, artifice: "compact_index" + source "https://gem.repo4" + + gem "platform_specific", :force_ruby_platform => true + G + + expect(the_bundle).to include_gems "platform_specific 1.0 ruby" + end + end + end +end diff --git a/spec/bundler/install/gemfile/gemspec_spec.rb b/spec/bundler/install/gemfile/gemspec_spec.rb index c50f8c9668..e51fc9247d 100644 --- a/spec/bundler/install/gemfile/gemspec_spec.rb +++ b/spec/bundler/install/gemfile/gemspec_spec.rb @@ -9,35 +9,35 @@ RSpec.describe "bundle install from an existing gemspec" do end it "should install runtime and development dependencies" do - build_lib("foo", :path => tmp.join("foo")) do |s| + build_lib("foo", path: tmp("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_uri_for(gem_repo2)}" - gemspec :path => '#{tmp.join("foo")}' + source "https://gem.repo2" + gemspec :path => '#{tmp("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 + 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| + build_lib("foo", path: tmp("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") + FileUtils.mv tmp("foo", "foo.gemspec"), tmp("foo", ".gemspec") install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gemspec :path => '#{tmp.join("foo")}' + source "https://gem.repo2" + gemspec :path => '#{tmp("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 + expect(the_bundle).to include_gems "bar-dev 1.0.0", groups: :development end it "should handle a list of requirements" do @@ -46,174 +46,171 @@ RSpec.describe "bundle install from an existing gemspec" do build_gem "baz", "1.1" end - build_lib("foo", :path => tmp.join("foo")) do |s| + build_lib("foo", path: tmp("foo")) do |s| s.write("Gemfile", "source :rubygems\ngemspec") s.add_dependency "baz", ">= 1.0", "< 1.1" end install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gemspec :path => '#{tmp.join("foo")}' + source "https://gem.repo2" + gemspec :path => '#{tmp("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) + build_lib("foo", path: tmp("foo"), gemspec: false) - install_gemfile(<<-G) - source "#{file_uri_for(gem_repo2)}" - gemspec :path => '#{tmp.join("foo")}' + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gemspec :path => '#{tmp("foo")}' G - expect(err).to match(/There are no gemspecs at #{tmp.join('foo')}/) + expect(err).to match(/There are no gemspecs at #{tmp("foo")}/) end it "should raise if there are too many gemspecs available" do - build_lib("foo", :path => tmp.join("foo")) do |s| + build_lib("foo", path: tmp("foo")) do |s| s.write("foo2.gemspec", build_spec("foo", "4.0").first.to_ruby) end - install_gemfile(<<-G) - source "#{file_uri_for(gem_repo2)}" - gemspec :path => '#{tmp.join("foo")}' + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gemspec :path => '#{tmp("foo")}' G - expect(err).to match(/There are multiple gemspecs at #{tmp.join('foo')}/) + expect(err).to match(/There are multiple gemspecs at #{tmp("foo")}/) end it "should pick a specific gemspec" do - build_lib("foo", :path => tmp.join("foo")) do |s| + build_lib("foo", path: tmp("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_uri_for(gem_repo2)}" - gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + source "https://gem.repo2" + gemspec :path => '#{tmp("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 + 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| + build_lib("foo", path: tmp("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_uri_for(gem_repo2)}" - gemspec :path => '#{tmp.join("foo")}', :name => 'foo', :development_group => :dev + source "https://gem.repo2" + gemspec :path => '#{tmp("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 + 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_uri_for(gem_repo1)}'\ngemspec") + build_lib("foo", path: tmp("foo")) do |s| + s.write("Gemfile", "source 'https://gem.repo1'\ngemspec") s.add_dependency "actionpack", "=2.3.2" - s.add_development_dependency "rake", "=12.3.2" + s.add_development_dependency "rake", rake_version 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 + bundle "install", dir: tmp("foo"), artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s } + # 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). + bundle_config "deployment true" + output = bundle("install", dir: tmp("foo"), artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }) + 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(/the lockfile can't be updated because frozen mode is set/) 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" + build_lib("foo", path: tmp("foo")) do |s| + s.add_dependency "myrack" end - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gemspec :path => '#{tmp.join("foo")}' + install_gemfile <<-G + source "https://gem.repo1" + gemspec :path => '#{tmp("foo")}' G - bundle! "install", :verbose => true + bundle "install", verbose: true message = "Found no changes, using resolution from the lockfile" expect(out.scan(message).size).to eq(1) 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 + simulate_platform "java" do + build_lib("foo", path: tmp("foo")) do |s| + s.add_dependency "myrack" + s.add_development_dependency "thin" + end - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gemspec :path => '#{tmp.join("foo")}' - G + install_gemfile <<-G + source "https://gem.repo1" + gemspec :path => '#{tmp("foo")}' + G - bundle! "install", :verbose => true + bundle "install", verbose: true - message = "Found no changes, using resolution from the lockfile" - expect(out.scan(message).size).to eq(1) + message = "Found no changes, using resolution from the lockfile" + expect(out.scan(message).size).to eq(1) + end 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| + it "should match a lockfile on non-ruby platforms with a transitive platform dependency", :jruby_only do + build_lib("foo", path: tmp("foo")) do |s| s.add_dependency "platform_specific" end - system_gems "platform_specific-1.0-java", :path => :bundle_path, :keep_path => true + system_gems "platform_specific-1.0-java", path: default_bundle_path - install_gemfile! <<-G - gemspec :path => '#{tmp.join("foo")}' + install_gemfile <<-G + gemspec :path => '#{tmp("foo")}' G - bundle! "update --bundler", :verbose => true - expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 JAVA" + bundle "update --bundler", artifice: "compact_index", 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")}'" + build_lib("foo", path: tmp("foo")) + File.open(tmp("foo/foo.gemspec"), "w") do |s| + s.write "raise 'ahh' unless Dir.pwd == '#{tmp("foo")}'" end - install_gemfile <<-G - gemspec :path => '#{tmp.join("foo")}' + install_gemfile <<-G, raise_on_error: false + gemspec :path => '#{tmp("foo")}' G - expect(last_command.stdboth).not_to include("ahh") + expect(stdboth).not_to include("ahh") end it "allows the gemspec to activate other gems" do ENV["BUNDLE_PATH__SYSTEM"] = "true" - # see https://github.com/bundler/bundler/issues/5409 + # see https://github.com/rubygems/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] + system_gems %w[myrack-1.0.0 myrack-0.9.1 myrack-obama-1.0] - build_lib("foo", :path => bundled_app) + 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' }" + f.write "#{gemspec.strip}.tap { gem 'myrack-obama'; require 'myrack/obama' }" end - install_gemfile! <<-G + install_gemfile <<-G + source "https://gem.repo1" gemspec G @@ -221,26 +218,26 @@ RSpec.describe "bundle install from an existing gemspec" do end it "allows conflicts" do - build_lib("foo", :path => tmp.join("foo")) do |s| + build_lib("foo", path: tmp("foo")) do |s| s.version = "1.0.0" s.add_dependency "bar", "= 1.0.0" end - build_gem "deps", :to_bundle => true do |s| + build_gem "deps", to_bundle: true do |s| s.add_dependency "foo", "= 0.0.1" end - build_gem "foo", "0.0.1", :to_bundle => true + build_gem "foo", "0.0.1", to_bundle: true install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "deps" - gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + gemspec :path => '#{tmp("foo")}', :name => 'foo' G expect(the_bundle).to include_gems "foo 1.0.0" end it "does not break Gem.finish_resolve with conflicts" do - build_lib("foo", :path => tmp.join("foo")) do |s| + build_lib("foo", path: tmp("foo")) do |s| s.version = "1.0.0" s.add_dependency "bar", "= 1.0.0" end @@ -251,28 +248,48 @@ RSpec.describe "bundle install from an existing gemspec" do build_gem "foo", "0.0.1" end - install_gemfile! <<-G - source "#{file_uri_for(gem_repo2)}" + install_gemfile <<-G + source "https://gem.repo2" gem "deps" - gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + gemspec :path => '#{tmp("foo")}', :name => 'foo' G expect(the_bundle).to include_gems "foo 1.0.0" - run! "Gem.finish_resolve; puts 'WIN'" + run "Gem.finish_resolve; puts 'WIN'" expect(out).to eq("WIN") end - it "works with only_update_to_newer_versions" do - build_lib "omg", "2.0", :path => lib_path("omg") + it "does not make Gem.try_activate warn when local gem has extensions" do + build_lib("foo", path: tmp("foo")) do |s| + s.version = "1.0.0" + s.add_c_extension + end + build_repo2 install_gemfile <<-G + source "https://gem.repo2" + gemspec :path => '#{tmp("foo")}' + G + + expect(the_bundle).to include_gems "foo 1.0.0" + + run "Gem.try_activate('irb/lc/es/error.rb'); puts 'WIN'" + expect(out).to eq("WIN") + expect(err).to be_empty + end + + it "handles downgrades" do + build_lib "omg", "2.0", path: lib_path("omg") + + install_gemfile <<-G + source "https://gem.repo1" gemspec :path => "#{lib_path("omg")}" G - build_lib "omg", "1.0", :path => lib_path("omg") + build_lib "omg", "1.0", path: lib_path("omg") - bundle! :install, :env => { "BUNDLE_BUNDLE_ONLY_UPDATE_TO_NEWER_VERSIONS" => "true" } + bundle :install expect(the_bundle).to include_gems "omg 1.0" end @@ -280,22 +297,23 @@ RSpec.describe "bundle install from an existing gemspec" do 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| + build_lib "cocoapods", path: bundled_app do |s| s.add_dependency "activesupport", ">= 1" end - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G + source "https://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| + build_lib "cocoapods", path: bundled_app do |s| s.add_dependency "activesupport", ">= 1.0.1" end - bundle :install, forgotten_command_line_options(:deployment => true) + bundle_config "deployment true" + bundle :install, raise_on_error: false expect(err).to include("changed") end @@ -305,117 +323,93 @@ RSpec.describe "bundle install from an existing gemspec" do 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" + build_lib "source_conflict", path: bundled_app do |s| + s.add_dependency "myrack_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 + build_lib "myrack_middleware", "1.0", path: bundled_app("myrack_middleware") do |s| + s.add_dependency "myrack", "1.0" # anything other than 0.9.1 end end it "should install the child gemspec's deps" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gemspec G - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 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" } - - before do - build_lib("foo", :path => tmp.join("foo")) do |s| - s.add_dependency "rack", "=1.0.0" - 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 + before do + build_lib("foo", path: tmp("foo")) do |s| + s.add_dependency "myrack", "=1.0.0" end - context "using JRuby with explicit platform" do - let(:platform) { "java" } - - before do - 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 - 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 + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" end - context "using JRuby" do - let(:platform) { "java" } + lockfile <<-L + PATH + remote: ../foo + specs: + foo (1.0) + myrack (= 1.0.0) + + GEM + remote: #{source_uri} + specs: + myrack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end - 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 + context "using JRuby with explicit platform", :jruby_only do + before do + create_file( + tmp("foo", "foo-java.gemspec"), + build_spec("foo", "1.0", "java") do + dep "myrack", "=1.0.0" + @spec.authors = "authors" + @spec.summary = "summary" + end.first.to_ruby + ) 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 + it "should install" do + results = bundle "install", artifice: "endpoint" + expect(results).to include("Installing myrack 1.0.0") + expect(the_bundle).to include_gems "myrack 1.0.0" end end - context "bundled for ruby and jruby" do + it "should install", :jruby do + results = bundle "install", artifice: "endpoint" + expect(results).to include("Installing myrack 1.0.0") + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + context "bundled for multiple platforms" do let(:platform_specific_type) { :runtime } let(:dependency) { "platform_specific" } before do @@ -425,36 +419,49 @@ RSpec.describe "bundle install from an existing gemspec" do end end - build_lib "foo", :path => "." do |s| - if platform_specific_type == :runtime + build_lib "foo", path: bundled_app do |s| + case platform_specific_type + when :runtime s.add_runtime_dependency dependency - elsif platform_specific_type == :development + when :development s.add_development_dependency dependency else - raise "wrong dependency type #{platform_specific_type}, can only be :development or :runtime" + raise ArgumentError, "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_uri_for(gem_repo2)}" - gemspec - G - end - end + gemfile <<-G + source "https://gem.repo2" + gemspec + G + + bundle_config "force_ruby_platform true" + bundle "install" + + simulate_new_machine + simulate_platform("jruby") { bundle "install" } + expect(lockfile).to include("platform_specific (1.0-java)") + simulate_platform("x64-mingw-ucrt") { bundle "install" } end context "on ruby" do before do - simulate_platform("ruby") + bundle_config "force_ruby_platform true" 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) + it "keeps all platform dependencies in the lockfile" do + expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 ruby" + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo2, "platform_specific", "1.0" + c.checksum gem_repo2, "platform_specific", "1.0", "java" + c.checksum gem_repo2, "platform_specific", "1.0", "x64-mingw-ucrt" + end + + expect(lockfile).to eq <<~L PATH remote: . specs: @@ -462,20 +469,22 @@ RSpec.describe "bundle install from an existing gemspec" do platform_specific GEM - remote: #{file_uri_for(gem_repo2)}/ + remote: https://gem.repo2/ specs: platform_specific (1.0) platform_specific (1.0-java) + platform_specific (1.0-x64-mingw-ucrt) PLATFORMS java ruby + x64-mingw-ucrt DEPENDENCIES foo! - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L end end @@ -483,30 +492,40 @@ RSpec.describe "bundle install from an existing gemspec" do 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) + it "keeps all platform dependencies in the lockfile" do + expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 ruby" + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo2, "platform_specific", "1.0" + c.checksum gem_repo2, "platform_specific", "1.0", "java" + c.checksum gem_repo2, "platform_specific", "1.0", "x64-mingw-ucrt" + end + + expect(lockfile).to eq <<~L PATH remote: . specs: foo (1.0) GEM - remote: #{file_uri_for(gem_repo2)}/ + remote: https://gem.repo2/ specs: platform_specific (1.0) platform_specific (1.0-java) + platform_specific (1.0-x64-mingw-ucrt) PLATFORMS java ruby + x64-mingw-ucrt DEPENDENCIES foo! platform_specific - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L end end @@ -515,32 +534,43 @@ RSpec.describe "bundle install from an existing gemspec" 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) + it "keeps all platform dependencies in the lockfile" do + expect(the_bundle).to include_gems "foo 1.0", "indirect_platform_specific 1.0", "platform_specific 1.0 ruby" + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo2, "indirect_platform_specific", "1.0" + c.checksum gem_repo2, "platform_specific", "1.0" + c.checksum gem_repo2, "platform_specific", "1.0", "java" + c.checksum gem_repo2, "platform_specific", "1.0", "x64-mingw-ucrt" + end + + expect(lockfile).to eq <<~L PATH remote: . specs: foo (1.0) GEM - remote: #{file_uri_for(gem_repo2)}/ + remote: https://gem.repo2/ specs: indirect_platform_specific (1.0) platform_specific platform_specific (1.0) platform_specific (1.0-java) + platform_specific (1.0-x64-mingw-ucrt) PLATFORMS java ruby + x64-mingw-ucrt DEPENDENCIES foo! indirect_platform_specific - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L end end @@ -550,34 +580,158 @@ RSpec.describe "bundle install from an existing gemspec" do context "with multiple platforms" do before do - build_lib("foo", :path => tmp.join("foo")) do |s| + build_lib("foo", path: tmp("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 + s.add_development_dependency "myrack" + s.write "foo-universal-java.gemspec", build_spec("foo", "1.0.0", "universal-java") {|sj| sj.runtime "myrack", "1.0.0" }.first.to_ruby end end it "installs the ruby platform gemspec" do - simulate_platform "ruby" + bundle_config "force_ruby_platform true" - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + install_gemfile <<-G + source "https://gem.repo1" + gemspec :path => '#{tmp("foo")}', :name => 'foo' G - expect(the_bundle).to include_gems "foo 1.0.0", "rack 1.0.0" + expect(the_bundle).to include_gems "foo 1.0.0", "myrack 1.0.0" end - it "installs the ruby platform gemspec and skips dev deps with --without development" do - simulate_platform "ruby" + it "installs the ruby platform gemspec and skips dev deps with `without development` configured" do + bundle_config "force_ruby_platform true" - install_gemfile! <<-G, forgotten_command_line_options(:without => "development") - source "#{file_uri_for(gem_repo1)}" - gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + bundle_config "without development" + install_gemfile <<-G + source "https://gem.repo1" + gemspec :path => '#{tmp("foo")}', :name => 'foo' G expect(the_bundle).to include_gem "foo 1.0.0" - expect(the_bundle).not_to include_gem "rack" + expect(the_bundle).not_to include_gem "myrack" + end + end + + context "with multiple platforms and resolving for more specific platforms" do + before do + build_lib("chef", path: tmp("chef")) do |s| + s.version = "17.1.17" + s.write "chef-universal-mingw-ucrt.gemspec", build_spec("chef", "17.1.17", "universal-mingw-ucrt") {|sw| sw.runtime "win32-api", "~> 1.5.3" }.first.to_ruby + end + end + + it "does not remove the platform specific specs from the lockfile when updating" do + build_repo4 do + build_gem "win32-api", "1.5.3" do |s| + s.platform = "universal-mingw-ucrt" + end + end + + gemfile <<-G + source "https://gem.repo4" + gemspec :path => "../chef" + G + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "chef", "17.1.17" + c.no_checksum "chef", "17.1.17", "universal-mingw-ucrt" + c.checksum gem_repo4, "win32-api", "1.5.3", "universal-mingw-ucrt" + end + + initial_lockfile = <<~L + PATH + remote: ../chef + specs: + chef (17.1.17) + chef (17.1.17-universal-mingw-ucrt) + win32-api (~> 1.5.3) + + GEM + remote: https://gem.repo4/ + specs: + win32-api (1.5.3-universal-mingw-ucrt) + + PLATFORMS + #{lockfile_platforms("ruby", "x64-mingw-ucrt", "x86-mingw32")} + + DEPENDENCIES + chef! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + lockfile initial_lockfile + + bundle "update" + + expect(lockfile).to eq initial_lockfile + end + end + + context "with multiple locked platforms" do + before do + build_lib("activeadmin", path: tmp("activeadmin")) do |s| + s.version = "2.9.0" + s.add_dependency "railties", ">= 5.2", "< 6.2" + end + + build_repo4 do + build_gem "railties", "6.1.4" + + build_gem "jruby-openssl", "0.10.7" do |s| + s.platform = "java" + end + end + + install_gemfile <<-G + source "https://gem.repo4" + gemspec :path => "../activeadmin" + gem "jruby-openssl", :platform => :jruby + G + + bundle "lock --add-platform java" + end + + it "does not remove the platform specific specs from the lockfile when re-resolving due to gemspec changes" do + checksums = checksums_section_when_enabled do |c| + c.no_checksum "activeadmin", "2.9.0" + c.checksum gem_repo4, "jruby-openssl", "0.10.7", "java" + c.checksum gem_repo4, "railties", "6.1.4" + end + + expect(lockfile).to eq <<~L + PATH + remote: ../activeadmin + specs: + activeadmin (2.9.0) + railties (>= 5.2, < 6.2) + + GEM + remote: https://gem.repo4/ + specs: + jruby-openssl (0.10.7-java) + railties (6.1.4) + + PLATFORMS + #{lockfile_platforms("java")} + + DEPENDENCIES + activeadmin! + jruby-openssl + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + gemspec = tmp("activeadmin/activeadmin.gemspec") + File.write(gemspec, File.read(gemspec).sub(">= 5.2", ">= 6.0")) + + previous_lockfile = lockfile + + bundle "install --local" + + expect(lockfile).to eq(previous_lockfile.sub(">= 5.2", ">= 6.0")) end end end diff --git a/spec/bundler/install/gemfile/git_spec.rb b/spec/bundler/install/gemfile/git_spec.rb index 00f8e96625..b2a82caf01 100644 --- a/spec/bundler/install/gemfile/git_spec.rb +++ b/spec/bundler/install/gemfile/git_spec.rb @@ -1,21 +1,26 @@ # frozen_string_literal: true 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_uri_for(gem_repo1)}" + describe "when floating on main" do + let(:base_gemfile) do + <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}" do gem 'foo' end G end + let(:install_base_gemfile) do + build_git "foo" do |s| + s.executables = "foobar" + end + + install_gemfile base_gemfile + end + it "fetches gems" do + install_base_gemfile expect(the_bundle).to include_gems("foo 1.0") run <<-RUBY @@ -26,18 +31,69 @@ RSpec.describe "bundle install with git sources" do expect(out).to eq("WIN") end - it "caches the git repo", :bundler => "< 3" do - expect(Dir["#{default_bundle_path}/cache/bundler/git/foo-1.0-*"]).to have_attributes :size => 1 + it "does not (yet?) enforce CHECKSUMS" do + build_git "foo" + revision = revision_for(lib_path("foo-1.0")) + + bundle_config "lockfile_checksums true" + gemfile base_gemfile + + lockfile <<~L + GIT + remote: #{lib_path("foo-1.0")} + revision: #{revision} + specs: + foo (1.0) + + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + CHECKSUMS + foo (1.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle_config "frozen true" + + bundle "install" + expect(the_bundle).to include_gems("foo 1.0") + end + + it "caches the git repo" do + install_base_gemfile + expect(Dir["#{default_cache_path}/git/foo-1.0-*"]).to have_attributes size: 1 + end + + it "does not write to cache on bundler/setup" do + install_base_gemfile + FileUtils.rm_r(default_cache_path) + ruby "require 'bundler/setup'" + expect(default_cache_path).not_to exist end - it "caches the git repo globally" do - simulate_new_machine - bundle! "config set global_gem_cache true" - bundle! :install - expect(Dir["#{home}/.bundle/cache/git/foo-1.0-*"]).to have_attributes :size => 1 + it "caches the git repo globally and properly uses the cached repo on the next invocation" do + install_base_gemfile + pristine_system_gems + bundle_config "global_gem_cache true" + bundle :install + expect(Dir["#{home}/.bundle/cache/git/foo-1.0-*"]).to have_attributes size: 1 + + bundle "install --verbose" + expect(err).to be_empty + expect(out).to include("Using foo 1.0 from #{lib_path("foo")}") end it "caches the evaluated gemspec" do + install_base_gemfile 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 @@ -47,35 +103,35 @@ RSpec.describe "bundle install with git sources" do 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 + sha = git.ref_for("main", 11) + spec_file = default_bundle_path("bundler/gems/foo-1.0-#{sha}/foo.gemspec") + expect(spec_file).to exist + ruby_code = Gem::Specification.load(spec_file.to_s).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 + install_base_gemfile 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 + install_gemfile bundled_app2("Gemfile"), <<-G, dir: bundled_app2 + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G - in_app_root do - run <<-RUBY - require 'foo' - puts "fail" if defined?(FOO_PREV_REF) - RUBY + run <<-RUBY + require 'foo' + puts "fail" if defined?(FOO_PREV_REF) + RUBY - expect(out).to be_empty - end + expect(out).to be_empty end it "sets up git gem executables on the path" do + install_base_gemfile bundle "exec foobar" expect(out).to eq("1.0") end @@ -83,32 +139,30 @@ RSpec.describe "bundle install with git sources" do it "complains if pinned specs don't exist in the git repo" do build_git "foo" - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" gem "foo", "1.1", :git => "#{lib_path("foo-1.0")}" G - expect(err).to include("The source contains 'foo' at: 1.0") + expect(err).to include("The source contains the following gems matching 'foo':\n * foo-1.0") end - it "complains with version and platform if pinned specs don't exist in the git repo" do - simulate_platform "java" - + it "complains with version and platform if pinned specs don't exist in the git repo", :jruby_only do build_git "only_java" do |s| s.platform = "java" end - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" platforms :jruby do gem "only_java", "1.2", :git => "#{lib_path("only_java-1.0-java")}" end G - expect(err).to include("The source contains 'only_java' at: 1.0 java") + expect(err).to include("The source contains the following gems matching 'only_java':\n * only_java-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" - + it "complains with multiple versions and platforms if pinned specs don't exist in the git repo", :jruby_only do build_git "only_java", "1.0" do |s| s.platform = "java" end @@ -118,42 +172,45 @@ RSpec.describe "bundle install with git sources" do s.write "only_java1-0.gemspec", File.read("#{lib_path("only_java-1.0-java")}/only_java.gemspec") end - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" platforms :jruby do gem "only_java", "1.2", :git => "#{lib_path("only_java-1.1-java")}" end G - expect(err).to include("The source contains 'only_java' at: 1.0 java, 1.1 java") + expect(err).to include("The source contains the following gems matching 'only_java':\n * only_java-1.0-java\n * only_java-1.1-java") end it "still works after moving the application directory" do - bundle "install --path vendor/bundle" + bundle_config "path vendor/bundle" + install_base_gemfile + FileUtils.mv bundled_app, tmp("bundled_app.bck") - Dir.chdir tmp("bundled_app.bck") - expect(the_bundle).to include_gems "foo 1.0" + expect(the_bundle).to include_gems "foo 1.0", dir: tmp("bundled_app.bck") end it "can still install after moving the application directory" do - bundle "install --path vendor/bundle" + bundle_config "path vendor/bundle" + install_base_gemfile + FileUtils.mv bundled_app, tmp("bundled_app.bck") - update_git "foo", "1.1", :path => lib_path("foo-1.0") + 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_uri_for(gem_repo1)}" + source "https://gem.repo1" git "#{lib_path("foo-1.0")}" do gem 'foo' end - gem "rack", "1.0" + gem "myrack", "1.0" G - bundle "update foo" + bundle "update foo", dir: tmp("bundled_app.bck") - expect(the_bundle).to include_gems "foo 1.1", "rack 1.0" + expect(the_bundle).to include_gems "foo 1.1", "myrack 1.0", dir: tmp("bundled_app.bck") end end @@ -161,8 +218,8 @@ RSpec.describe "bundle install with git sources" do before do build_git "foo" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" git "#{lib_path("foo-1.0")}" do # this page left intentionally blank @@ -172,7 +229,7 @@ RSpec.describe "bundle install with git sources" do it "does not explode" do bundle "install" - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 1.0" end end @@ -185,10 +242,12 @@ RSpec.describe "bundle install with git sources" do it "works" do install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}", :ref => "#{@revision}" do gem "foo" end G + expect(err).to be_empty run <<-RUBY require 'foo' @@ -200,6 +259,7 @@ RSpec.describe "bundle install with git sources" do it "works when the revision is a symbol" do install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}", :ref => #{@revision.to_sym.inspect} do gem "foo" end @@ -214,29 +274,67 @@ RSpec.describe "bundle install with git sources" do expect(out).to eq("WIN") end + it "works when an abbreviated revision is added after an initial, potentially shallow clone" do + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}" do + gem "foo" + end + G + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}", :ref => #{@revision[0..7].inspect} do + gem "foo" + end + G + end + + it "works when a tag that does not look like a commit hash is used as the value of :ref" do + build_git "foo" + @remote = build_git("bar", bare: true) + update_git "foo", remote: @remote.path + update_git "foo", push: "main" + + install_gemfile <<-G + source "https://gem.repo1" + gem 'foo', :git => "#{@remote.path}" + G + + # Create a new tag on the remote that needs fetching + update_git "foo", tag: "v1.0.0" + update_git "foo", push: "v1.0.0" + + install_gemfile <<-G + source "https://gem.repo1" + gem 'foo', :git => "#{@remote.path}", :ref => "v1.0.0" + G + + expect(err).to be_empty + end + it "works when the revision is a non-head ref" do - # want to ensure we don't fallback to master - update_git "foo", :path => lib_path("foo-1.0") do |s| + # want to ensure we don't fallback to main + update_git "foo", path: lib_path("foo-1.0") do |s| s.write("lib/foo.rb", "raise 'FAIL'") end - Dir.chdir(lib_path("foo-1.0")) do - `git update-ref -m "Bundler Spec!" refs/bundler/1 master~1` - end + git("update-ref -m \"Bundler Spec!\" refs/bundler/1 main~1", lib_path("foo-1.0")) # want to ensure we don't fallback to HEAD - update_git "foo", :path => lib_path("foo-1.0"), :branch => "rando" do |s| - s.write("lib/foo.rb", "raise 'FAIL'") + update_git "foo", path: lib_path("foo-1.0"), branch: "rando" do |s| + s.write("lib/foo.rb", "raise 'FAIL_FROM_RANDO'") end - install_gemfile! <<-G + install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}", :ref => "refs/bundler/1" do gem "foo" end G expect(err).to be_empty - run! <<-RUBY + run <<-RUBY require 'foo' puts "WIN" if defined?(FOO) RUBY @@ -245,34 +343,34 @@ RSpec.describe "bundle install with git sources" do end it "works when the revision is a non-head ref and it was previously downloaded" do - install_gemfile! <<-G + install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}" do gem "foo" end G - # want to ensure we don't fallback to master - update_git "foo", :path => lib_path("foo-1.0") do |s| + # want to ensure we don't fallback to main + update_git "foo", path: lib_path("foo-1.0") do |s| s.write("lib/foo.rb", "raise 'FAIL'") end - Dir.chdir(lib_path("foo-1.0")) do - `git update-ref -m "Bundler Spec!" refs/bundler/1 master~1` - end + git("update-ref -m \"Bundler Spec!\" refs/bundler/1 main~1", lib_path("foo-1.0")) # want to ensure we don't fallback to HEAD - update_git "foo", :path => lib_path("foo-1.0"), :branch => "rando" do |s| - s.write("lib/foo.rb", "raise 'FAIL'") + update_git "foo", path: lib_path("foo-1.0"), branch: "rando" do |s| + s.write("lib/foo.rb", "raise 'FAIL_FROM_RANDO'") end - install_gemfile! <<-G + install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}", :ref => "refs/bundler/1" do gem "foo" end G expect(err).to be_empty - run! <<-RUBY + run <<-RUBY require 'foo' puts "WIN" if defined?(FOO) RUBY @@ -281,24 +379,21 @@ RSpec.describe "bundle install with git sources" do end it "does not download random non-head refs" do - Dir.chdir(lib_path("foo-1.0")) do - sys_exec!('git update-ref -m "Bundler Spec!" refs/bundler/1 master~1') - end + git("update-ref -m \"Bundler Spec!\" refs/bundler/1 main~1", lib_path("foo-1.0")) - bundle! "config set global_gem_cache true" + bundle_config "global_gem_cache true" - install_gemfile! <<-G + install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}" do gem "foo" end G # ensure we also git fetch after cloning - bundle! :update, :all => true + bundle :update, all: true - Dir.chdir(Dir[home(".bundle/cache/git/foo-*")].first) do - sys_exec("git ls-remote .") - end + git("ls-remote .", Dir[home(".bundle/cache/git/foo-*")].first) expect(out).not_to include("refs/bundler/1") end @@ -307,12 +402,12 @@ RSpec.describe "bundle install with git sources" do 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 + update_git("foo", path: repo, branch: branch) + install_gemfile <<-G + source "https://gem.repo1" git "#{repo}", :branch => #{branch.dump} do gem "foo" end @@ -324,7 +419,12 @@ RSpec.describe "bundle install with git sources" do context "when the branch starts with a `#`" do let(:branch) { "#149/redirect-url-fragment" } it "works" do + skip "git does not accept this" if Gem.win_platform? + + update_git("foo", path: repo, branch: branch) + install_gemfile <<-G + source "https://gem.repo1" git "#{repo}", :branch => #{branch.dump} do gem "foo" end @@ -337,7 +437,12 @@ RSpec.describe "bundle install with git sources" do context "when the branch includes quotes" do let(:branch) { %('") } it "works" do + skip "git does not accept this" if Gem.win_platform? + + update_git("foo", path: repo, branch: branch) + install_gemfile <<-G + source "https://gem.repo1" git "#{repo}", :branch => #{branch.dump} do gem "foo" end @@ -351,12 +456,12 @@ RSpec.describe "bundle install with git sources" do 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 + update_git("foo", path: repo, tag: tag) + install_gemfile <<-G + source "https://gem.repo1" git "#{repo}", :tag => #{tag.dump} do gem "foo" end @@ -368,7 +473,12 @@ RSpec.describe "bundle install with git sources" do context "when the tag starts with a `#`" do let(:tag) { "#149/redirect-url-fragment" } it "works" do + skip "git does not accept this" if Gem.win_platform? + + update_git("foo", path: repo, tag: tag) + install_gemfile <<-G + source "https://gem.repo1" git "#{repo}", :tag => #{tag.dump} do gem "foo" end @@ -381,7 +491,12 @@ RSpec.describe "bundle install with git sources" do context "when the tag includes quotes" do let(:tag) { %('") } it "works" do + skip "git does not accept this" if Gem.win_platform? + + update_git("foo", path: repo, tag: tag) + install_gemfile <<-G + source "https://gem.repo1" git "#{repo}", :tag => #{tag.dump} do gem "foo" end @@ -394,122 +509,119 @@ RSpec.describe "bundle install with git sources" do 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" + build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" end gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - bundle! %(config set local.rack #{lib_path("local-rack")}) - bundle! :install + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install - run "require 'rack'" + run "require 'myrack'" expect(out).to eq("LOCAL") end it "chooses the local repository on runtime" do - build_git "rack", "0.8" + build_git "myrack", "0.8" - FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - update_git "rack", "0.8", :path => lib_path("local-rack") do |s| - s.write "lib/rack.rb", "puts :LOCAL" + update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - bundle %(config set local.rack #{lib_path("local-rack")}) - run "require 'rack'" + bundle %(config set local.myrack #{lib_path("local-myrack")}) + run "require 'myrack'" 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" + build_git "myrack", "0.8" - FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - 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" + update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "myrack.gemspec", build_spec("myrack", "0.8") { runtime "rspec", "> 0" }.first.to_ruby + s.write "lib/myrack.rb", "puts :LOCAL" end - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - bundle! %(config set local.rack #{lib_path("local-rack")}) - bundle! :install - run! "require 'rack'" + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install + run "require 'myrack'" expect(out).to eq("LOCAL") end it "updates specs on runtime" do system_gems "nokogiri-1.4.2" - build_git "rack", "0.8" + build_git "myrack", "0.8" install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - lockfile0 = File.read(bundled_app("Gemfile.lock")) + lockfile0 = File.read(bundled_app_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| + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| s.add_dependency "nokogiri", "1.4.2" end - bundle %(config set local.rack #{lib_path("local-rack")}) - run "require 'rack'" + bundle %(config set local.myrack #{lib_path("local-myrack")}) + run "require 'myrack'" - lockfile1 = File.read(bundled_app("Gemfile.lock")) + lockfile1 = File.read(bundled_app_lock) expect(lockfile1).not_to eq(lockfile0) end it "updates ref on install" do - build_git "rack", "0.8" + build_git "myrack", "0.8" install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - lockfile0 = File.read(bundled_app("Gemfile.lock")) + lockfile0 = File.read(bundled_app_lock) - FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) - update_git "rack", "0.8", :path => lib_path("local-rack") + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + update_git "myrack", "0.8", path: lib_path("local-myrack") - bundle %(config set local.rack #{lib_path("local-rack")}) + bundle %(config set local.myrack #{lib_path("local-myrack")}) bundle :install - lockfile1 = File.read(bundled_app("Gemfile.lock")) + lockfile1 = File.read(bundled_app_lock) expect(lockfile1).not_to eq(lockfile0) end it "explodes and gives correct solution if given path does not exist on install" do - build_git "rack", "0.8" + build_git "myrack", "0.8" install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - bundle %(config set local.rack #{lib_path("local-rack")}) - bundle :install - expect(err).to match(/Cannot use local override for rack-0.8 because #{Regexp.escape(lib_path('local-rack').to_s)} does not exist/) + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install, raise_on_error: false + expect(err).to match(/Cannot use local override for myrack-0.8 because #{Regexp.escape(lib_path("local-myrack").to_s)} does not exist/) - solution = "config unset local.rack" + solution = "config unset local.myrack" expect(err).to match(/Run `bundle #{solution}` to remove the local override/) bundle solution @@ -519,19 +631,19 @@ RSpec.describe "bundle install with git sources" do end it "explodes and gives correct solution 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")) + build_git "myrack", "0.8" + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}" G - bundle %(config set local.rack #{lib_path("local-rack")}) - bundle :install - expect(err).to match(/Cannot use local override for rack-0.8 at #{Regexp.escape(lib_path('local-rack').to_s)} because :branch is not specified in Gemfile/) + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install, raise_on_error: false + expect(err).to match(/Cannot use local override for myrack-0.8 at #{Regexp.escape(lib_path("local-myrack").to_s)} because :branch is not specified in Gemfile/) - solution = "config unset local.rack" + solution = "config unset local.myrack" expect(err).to match(/Specify a branch or run `bundle #{solution}` to remove the local override/) bundle solution @@ -541,55 +653,73 @@ RSpec.describe "bundle install with git sources" do 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")) + build_git "myrack", "0.8" + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}" G - bundle %(config set local.rack #{lib_path("local-rack")}) + bundle %(config set local.myrack #{lib_path("local-myrack")}) bundle %(config set 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" + build_git "myrack", "0.8" - FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - update_git "rack", "0.8", :path => lib_path("local-rack"), :branch => "another" do |s| - s.write "lib/rack.rb", "puts :LOCAL" + update_git "myrack", "0.8", path: lib_path("local-myrack"), branch: "another" do |s| + s.write "lib/myrack.rb", "puts :LOCAL" end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - bundle %(config set local.rack #{lib_path("local-rack")}) - bundle :install - expect(err).to match(/is using branch another but Gemfile specifies master/) + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install, raise_on_error: false + expect(err).to match(/is using branch another but Gemfile specifies main/) end it "explodes on invalid revision on install" do - build_git "rack", "0.8" + build_git "myrack", "0.8" - build_git "rack", "0.8", :path => lib_path("local-rack") do |s| - s.write "lib/rack.rb", "puts :LOCAL" + build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - bundle %(config set local.rack #{lib_path("local-rack")}) - bundle :install + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install, raise_on_error: false expect(err).to match(/The Gemfile lock is pointing to revision \w+/) end + + it "does not explode on invalid revision on install" do + build_git "myrack", "0.8" + + build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle %(config set disable_local_revision_check true) + bundle :install + expect(out).to match(/Bundle complete!/) + end end describe "specified inline" do @@ -610,75 +740,76 @@ RSpec.describe "bundle install with git sources" do # end it "installs from git even if a newer gem is available elsewhere" do - build_git "rack", "0.8" + build_git "myrack", "0.8" install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}" G - expect(the_bundle).to include_gems "rack 0.8" + expect(the_bundle).to include_gems "myrack 0.8" end it "installs dependencies from git even if a newer gem is available elsewhere" do - system_gems "rack-1.0.0" + system_gems "myrack-1.0.0" - build_lib "rack", "1.0", :path => lib_path("nested/bar") do |s| - s.write "lib/rack.rb", "puts 'WIN OVERRIDE'" + build_lib "myrack", "1.0", path: lib_path("nested/bar") do |s| + s.write "lib/myrack.rb", "puts 'WIN OVERRIDE'" end - build_git "foo", :path => lib_path("nested") do |s| - s.add_dependency "rack", "= 1.0" + build_git "foo", path: lib_path("nested") do |s| + s.add_dependency "myrack", "= 1.0" end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "foo", :git => "#{lib_path("nested")}" G - run "require 'rack'" + run "require 'myrack'" expect(out).to eq("WIN OVERRIDE") end it "correctly unlocks when changing to a git source" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "0.9.1" + source "https://gem.repo1" + gem "myrack", "0.9.1" G - build_git "rack", :path => lib_path("rack") + build_git "myrack", path: lib_path("myrack") install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "1.0.0", :git => "#{lib_path("rack")}" + source "https://gem.repo1" + gem "myrack", "1.0.0", :git => "#{lib_path("myrack")}" G - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "correctly unlocks when changing to a git source without versions" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - build_git "rack", "1.2", :path => lib_path("rack") + build_git "myrack", "1.2", path: lib_path("myrack") install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack")}" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack")}" G - expect(the_bundle).to include_gems "rack 1.2" + expect(the_bundle).to include_gems "myrack 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") + build_lib "omg", path: lib_path("hi2u/omg") + build_lib "hi2u", path: lib_path("hi2u") install_gemfile <<-G + source "https://gem.repo1" path "#{lib_path("hi2u")}" do gem "omg" gem "hi2u" @@ -695,6 +826,7 @@ RSpec.describe "bundle install with git sources" do update_git "foo" install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{@revision}" G @@ -712,7 +844,7 @@ RSpec.describe "bundle install with git sources" do end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" gem "rails", "2.3.2" G @@ -722,7 +854,7 @@ RSpec.describe "bundle install with git sources" do 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| + 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 @@ -737,12 +869,12 @@ RSpec.describe "bundle install with git sources" do G end - build_git "foo", :path => lib_path("foo") do |s| + build_git "foo", path: lib_path("foo") do |s| s.write "bin/foo", "" end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "bar", :git => "#{lib_path("foo")}" gem "rails", "2.3.2" G @@ -751,14 +883,41 @@ RSpec.describe "bundle install with git sources" do expect(the_bundle).to include_gems "rails 2.3.2" end + it "runs the gemspec in the context of its parent directory, when using local overrides" do + build_git "foo", path: lib_path("foo"), gemspec: false do |s| + s.write lib_path("foo/lib/foo/version.rb"), %(FOO_VERSION = '1.0') + s.write "foo.gemspec", <<-G + $:.unshift Dir.pwd + require 'lib/foo/version' + Gem::Specification.new do |s| + s.name = 'foo' + s.author = 'no one' + s.version = FOO_VERSION + s.summary = 'Foo' + s.files = Dir["lib/**/*.rb"] + end + G + end + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "https://github.com/gems/foo", branch: "main" + G + + bundle %(config set local.foo #{lib_path("foo")}) + + expect(the_bundle).to include_gems "foo 1.0" + 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| + 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 + source "https://gem.repo1" gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}" G @@ -766,10 +925,10 @@ RSpec.describe "bundle install with git sources" do end it "fakes the gem out if there is no gemspec" do - build_git "foo", :gemspec => false + build_git "foo", gemspec: false install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}" gem "rails", "2.3.2" G @@ -780,10 +939,11 @@ RSpec.describe "bundle install with git sources" do it "catches git errors and spits out useful output" do gemfile <<-G + source "https://gem.repo1" gem "foo", "1.0", :git => "omgomg" G - bundle :install + bundle :install, raise_on_error: false expect(err).to include("Git error:") expect(err).to include("fatal") @@ -791,9 +951,10 @@ RSpec.describe "bundle install with git sources" do end it "works when the gem path has spaces in it" do - build_git "foo", :path => lib_path("foo space-1.0") + build_git "foo", path: lib_path("foo space-1.0") install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo space-1.0")}" G @@ -804,6 +965,7 @@ RSpec.describe "bundle install with git sources" do build_git "forced", "1.0" install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("forced-1.0")}" do gem 'forced' end @@ -814,48 +976,50 @@ RSpec.describe "bundle install with git sources" do s.write "lib/forced.rb", "FORCED = '1.1'" end - bundle "update", :all => true + bundle "update", all: true expect(the_bundle).to include_gems "forced 1.1" - Dir.chdir(lib_path("forced-1.0")) do - `git reset --hard HEAD^` - end + git("reset --hard HEAD^", lib_path("forced-1.0")) - bundle "update", :all => true + bundle "update", all: true expect(the_bundle).to include_gems "forced 1.0" end it "ignores submodules if :submodule is not passed" do + # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ + system(*%W[git config --global protocol.file.allow always]) + 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 + git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") + git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" git "#{lib_path("has_submodule-1.0")}" do gem "has_submodule" end G - expect(err).to match(/could not find gem 'submodule/i) + expect(err).to match(%r{submodule >= 0 could not be found in rubygems repository https://gem.repo1/ or installed locally}) expect(the_bundle).not_to include_gems "has_submodule 1.0" end it "handles repos with submodules" do + # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ + system(*%W[git config --global protocol.file.allow always]) + 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 + git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") + git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("has_submodule-1.0")}", :submodules => true do gem "has_submodule" end @@ -864,10 +1028,33 @@ RSpec.describe "bundle install with git sources" do expect(the_bundle).to include_gems "has_submodule 1.0" end + it "does not warn when deiniting submodules" do + # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ + system(*%W[git config --global protocol.file.allow always]) + + build_git "submodule", "1.0" + build_git "has_submodule", "1.0" + + git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") + git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("has_submodule-1.0")}" do + gem "has_submodule" + end + G + expect(err).to be_empty + + expect(the_bundle).to include_gems "has_submodule 1.0" + expect(the_bundle).to_not include_gems "submodule 1.0" + end + it "handles implicit updates when modifying the source info" do git = build_git "foo" install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}" do gem "foo" end @@ -877,6 +1064,7 @@ RSpec.describe "bundle install with git sources" do update_git "foo" install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}", :ref => "#{git.ref_for("HEAD^")}" do gem "foo" end @@ -890,14 +1078,15 @@ RSpec.describe "bundle install with git sources" do expect(out).to eq("WIN") end - it "does not to a remote fetch if the revision is cached locally" do + it "does not do a remote fetch if the revision is cached locally" do build_git "foo" install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G - FileUtils.rm_rf(lib_path("foo-1.0")) + FileUtils.rm_r(lib_path("foo-1.0")) bundle "install" expect(out).not_to match(/updating/i) @@ -907,12 +1096,12 @@ RSpec.describe "bundle install with git sources" do build_git "foo" gemfile <<-G + source "https://gem.repo1" 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 @@ -921,48 +1110,50 @@ RSpec.describe "bundle install with git sources" do FileUtils.mkdir_p(default_bundle_path) FileUtils.touch(default_bundle_path("bundler")) - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G - expect(exitstatus).to_not eq(0) if exitstatus + expect(last_command).to be_failure expect(err).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_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") + build_git "foo", path: lib_path("nested") + build_git "bar", path: lib_path("nested") install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("nested")}" gem "bar", :git => "#{lib_path("nested")}" G - expect(File.read(bundled_app("Gemfile.lock")).scan("GIT").size).to eq(1) + expect(File.read(bundled_app_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| + 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| + 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_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "bar", :path => "#{lib_path("bar")}" G install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "bar", :git => "#{lib_path("bar")}" G @@ -971,24 +1162,67 @@ RSpec.describe "bundle install with git sources" do it "doesn't explode when switching Gem to Git source" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack-obama" - gem "rack", "1.0.0" + source "https://gem.repo1" + gem "myrack-obama" + gem "myrack", "1.0.0" G - build_git "rack", "1.0" do |s| + build_git "myrack", "1.0" do |s| s.write "lib/new_file.rb", "puts 'USING GIT'" end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack-obama" - gem "rack", "1.0.0", :git => "#{lib_path("rack-1.0")}" + source "https://gem.repo1" + gem "myrack-obama" + gem "myrack", "1.0.0", :git => "#{lib_path("myrack-1.0")}" G run "require 'new_file'" expect(out).to eq("USING GIT") end + + it "doesn't explode when removing an explicit exact version from a git gem with dependencies" do + build_lib "activesupport", "7.1.4", path: lib_path("rails/activesupport") + build_git "rails", "7.1.4", path: lib_path("rails") do |s| + s.add_dependency "activesupport", "= 7.1.4" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "rails", "7.1.4", :git => "#{lib_path("rails")}" + G + + install_gemfile <<-G + source "https://gem.repo1" + gem "rails", :git => "#{lib_path("rails")}" + G + + expect(the_bundle).to include_gem "rails 7.1.4", "activesupport 7.1.4" + end + + it "doesn't explode when adding an explicit ref to a git gem with dependencies" do + lib_root = lib_path("rails") + + build_lib "activesupport", "7.1.4", path: lib_root.join("activesupport") + build_git "rails", "7.1.4", path: lib_root do |s| + s.add_dependency "activesupport", "= 7.1.4" + end + + old_revision = revision_for(lib_root) + update_git "rails", "7.1.4", path: lib_root + + install_gemfile <<-G + source "https://gem.repo1" + gem "rails", "7.1.4", :git => "#{lib_root}" + G + + install_gemfile <<-G + source "https://gem.repo1" + gem "rails", :git => "#{lib_root}", :ref => "#{old_revision}" + G + + expect(the_bundle).to include_gem "rails 7.1.4", "activesupport 7.1.4" + end end describe "bundle install after the remote has been updated" do @@ -996,15 +1230,16 @@ RSpec.describe "bundle install with git sources" do build_git "valim" install_gemfile <<-G - gem "valim", :git => "#{file_uri_for(lib_path("valim-1.0"))}" + source "https://gem.repo1" + gem "valim", :git => "#{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")) - old_lockfile = File.read(bundled_app("Gemfile.lock")) - lockfile(bundled_app("Gemfile.lock"), old_lockfile.gsub(/revision: #{old_revision}/, "revision: #{new_revision}")) + old_lockfile = File.read(bundled_app_lock) + lockfile(bundled_app_lock, old_lockfile.gsub(/revision: #{old_revision}/, "revision: #{new_revision}")) bundle "install" @@ -1021,29 +1256,43 @@ RSpec.describe "bundle install with git sources" do revision = revision_for(lib_path("foo-1.0")) install_gemfile <<-G - gem "foo", :git => "#{file_uri_for(lib_path("foo-1.0"))}", :ref => "#{revision}" + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{revision}" G expect(out).to_not match(/Revision.*does not exist/) - install_gemfile <<-G - gem "foo", :git => "#{file_uri_for(lib_path("foo-1.0"))}", :ref => "deadbeef" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "deadbeef" + G + expect(err).to include("Revision deadbeef does not exist in the repository") + end + + it "gives a helpful error message when the remote branch no longer exists" do + build_git "foo" + + install_gemfile <<-G, env: { "LANG" => "en" }, raise_on_error: false + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "deadbeef" G + expect(err).to include("Revision deadbeef does not exist in the repository") end end - describe "bundle install --deployment with git sources" do + describe "bundle install with deployment mode configured and git sources" do it "works" do - build_git "valim", :path => lib_path("valim") + build_git "valim", path: lib_path("valim") install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "valim", "= 1.0", :git => "#{lib_path("valim")}" G - simulate_new_machine + pristine_system_gems - bundle! :install, forgotten_command_line_options(:deployment => true) + bundle_config "deployment true" + bundle :install end end @@ -1051,6 +1300,7 @@ RSpec.describe "bundle install with git sources" do it "runs pre-install hooks" do build_git "foo" gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G @@ -1063,13 +1313,14 @@ RSpec.describe "bundle install with git sources" do end bundle :install, - :requires => [lib_path("install_hooks.rb")] + requires: [lib_path("install_hooks.rb")] expect(err_without_deprecations).to eq("Ran pre-install hook: foo-1.0") end it "runs post-install hooks" do build_git "foo" gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G @@ -1082,13 +1333,14 @@ RSpec.describe "bundle install with git sources" do end bundle :install, - :requires => [lib_path("install_hooks.rb")] + requires: [lib_path("install_hooks.rb")] expect(err_without_deprecations).to eq("Ran post-install hook: foo-1.0") end it "complains if the install hook fails" do build_git "foo" gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G @@ -1100,8 +1352,7 @@ RSpec.describe "bundle install with git sources" do H end - bundle :install, - :requires => [lib_path("install_hooks.rb")] + bundle :install, requires: [lib_path("install_hooks.rb")], raise_on_error: false expect(err).to include("failed for foo-1.0") end end @@ -1113,7 +1364,7 @@ RSpec.describe "bundle install with git sources" do s.extensions << "Rakefile" s.write "Rakefile", <<-RUBY task :default do - path = File.expand_path("../lib", __FILE__) + path = File.expand_path("lib", __dir__) FileUtils.mkdir_p(path) File.open("\#{path}/foo.rb", "w") do |f| f.puts "FOO = 'YES'" @@ -1123,7 +1374,7 @@ RSpec.describe "bundle install with git sources" do end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G @@ -1133,14 +1384,14 @@ RSpec.describe "bundle install with git sources" do R expect(out).to eq("YES") - run! <<-R + run <<-R puts $:.grep(/ext/) R expect(out).to include(Pathname.glob(default_bundle_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| + it "does not use old extension after ref changes" do + git_reader = build_git "foo", no_default: true do |s| s.extensions = ["ext/extconf.rb"] s.write "ext/extconf.rb", <<-RUBY require "mkmf" @@ -1150,20 +1401,19 @@ RSpec.describe "bundle install with git sources" do 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` + File.open(git_reader.path.join("ext/foo.c"), "w") do |file| + file.write <<-C + #include "ruby.h" + VALUE foo(VALUE self) { 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", git_reader.path) + git_commit_sha = git_reader.ref_for("HEAD") install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{git_commit_sha}" G @@ -1187,8 +1437,8 @@ RSpec.describe "bundle install with git sources" do RUBY end - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G @@ -1207,7 +1457,7 @@ In Gemfile: s.extensions << "Rakefile" s.write "Rakefile", <<-RUBY task :default do - path = File.expand_path("../lib", __FILE__) + path = File.expand_path("lib", __dir__) FileUtils.mkdir_p(path) cur_time = Time.now.to_f.to_s File.open("\#{path}/foo.rb", "w") do |f| @@ -1218,11 +1468,11 @@ In Gemfile: end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G - run! <<-R + run <<-R require 'foo' puts FOO R @@ -1231,11 +1481,11 @@ In Gemfile: expect(installed_time).to match(/\A\d+\.\d+\z/) install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G - run! <<-R + run <<-R require 'foo' puts FOO R @@ -1248,7 +1498,7 @@ In Gemfile: s.extensions << "Rakefile" s.write "Rakefile", <<-RUBY task :default do - path = File.expand_path("../lib", __FILE__) + path = File.expand_path("lib", __dir__) FileUtils.mkdir_p(path) cur_time = Time.now.to_f.to_s File.open("\#{path}/foo.rb", "w") do |f| @@ -1259,12 +1509,12 @@ In Gemfile: end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "0.9.1" + source "https://gem.repo1" + gem "myrack", "0.9.1" gem "foo", :git => "#{lib_path("foo-1.0")}" G - run! <<-R + run <<-R require 'foo' puts FOO R @@ -1273,12 +1523,12 @@ In Gemfile: expect(installed_time).to match(/\A\d+\.\d+\z/) install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "1.0.0" + source "https://gem.repo1" + gem "myrack", "1.0.0" gem "foo", :git => "#{lib_path("foo-1.0")}" G - run! <<-R + run <<-R require 'foo' puts FOO R @@ -1291,7 +1541,7 @@ In Gemfile: s.extensions << "Rakefile" s.write "Rakefile", <<-RUBY task :default do - path = File.expand_path("../lib", __FILE__) + path = File.expand_path("lib", __dir__) FileUtils.mkdir_p(path) cur_time = Time.now.to_f.to_s File.open("\#{path}/foo.rb", "w") do |f| @@ -1302,26 +1552,27 @@ In Gemfile: end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G - run! <<-R + run <<-R require 'foo' puts FOO R - update_git("foo", :branch => "branch2") - installed_time = out + + update_git("foo", branch: "branch2") + expect(installed_time).to match(/\A\d+\.\d+\z/) install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "branch2" G - run! <<-R + run <<-R require 'foo' puts FOO R @@ -1330,9 +1581,9 @@ In Gemfile: installed_time = out update_git("foo") - bundle! "update foo" + bundle "update foo" - run! <<-R + run <<-R require 'foo' puts FOO R @@ -1350,55 +1601,91 @@ In Gemfile: ENV["GIT_WORK_TREE"] = "bar" install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://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 + it "prints a better error message when installing" do + gemfile <<-G + source "https://gem.repo1" + + gem "rake", git: "https://github.com/ruby/rake" + G + + lockfile <<-L + GIT + remote: https://github.com/ruby/rake + revision: 5c60da8644a9e4f655e819252e3b6ca77f42b7af + specs: + rake (13.0.6) + + GEM + remote: https://rubygems.org/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rake! + + BUNDLED WITH + #{Bundler::VERSION} + L + + with_path_as("") do + bundle "install", raise_on_error: false + end + expect(err). + 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 "prints a better error message when updating" do build_git "foo" install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}" do gem 'foo' end G with_path_as("") do - bundle "update", :all => true + bundle "update", all: true, raise_on_error: false end expect(err). 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 + it "doesn't need git in the new machine if an installed git gem is copied to another machine" do build_git "foo" install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}" do gem 'foo' end G - bundle "config set cache_all true" - bundle :cache - simulate_new_machine + bundle_config_global "path vendor/bundle" + bundle :install + pristine_system_gems - bundle! "install", :env => { "PATH" => "" } + bundle "install", env: { "PATH" => "" } expect(out).to_not include("You need to install git to be able to use gems from git repositories.") end end describe "when the git source is overridden with a local git repo" do before do - bundle! "config set --global local.foo #{lib_path("foo")}" + bundle_config_global "local.foo #{lib_path("foo")}" end describe "and git output is colorized" do @@ -1409,10 +1696,11 @@ In Gemfile: end it "installs successfully" do - build_git "foo", "1.0", :path => lib_path("foo") + build_git "foo", "1.0", path: lib_path("foo") gemfile <<-G - gem "foo", :git => "#{lib_path("foo")}", :branch => "master" + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo")}", :branch => "main" G bundle :install @@ -1426,13 +1714,14 @@ In Gemfile: let(:credentials) { "user1:password1" } it "does not display the password" do - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" git "https://#{credentials}@github.com/company/private-repo" do gem "foo" end G - expect(last_command.stdboth).to_not include("password1") + expect(stdboth).to_not include("password1") expect(out).to include("Fetching https://user1@github.com/company/private-repo") end end @@ -1441,13 +1730,14 @@ In Gemfile: let(:credentials) { "oauth_token" } it "displays the oauth scheme but not the oauth token" do - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" git "https://#{credentials}:x-oauth-basic@github.com/company/private-repo" do gem "foo" end G - expect(last_command.stdboth).to_not include("oauth_token") + expect(stdboth).to_not include("oauth_token") expect(out).to include("Fetching https://x-oauth-basic@github.com/company/private-repo") end end diff --git a/spec/bundler/install/gemfile/groups_spec.rb b/spec/bundler/install/gemfile/groups_spec.rb index 63be1a4e43..4013b112ec 100644 --- a/spec/bundler/install/gemfile/groups_spec.rb +++ b/spec/bundler/install/gemfile/groups_spec.rb @@ -4,8 +4,8 @@ RSpec.describe "bundle install with groups" do describe "installing with no options" do before :each do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" group :emo do gem "activesupport", "2.3.5" end @@ -14,7 +14,7 @@ RSpec.describe "bundle install with groups" do end it "installs gems in the default group" do - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "installs gems in a group block into that group" do @@ -25,7 +25,7 @@ RSpec.describe "bundle install with groups" do puts ACTIVESUPPORT R - expect(err_without_deprecations).to eq("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- activesupport/) end it "installs gems with inline :groups into those groups" do @@ -36,11 +36,11 @@ RSpec.describe "bundle install with groups" do puts THIN R - expect(err_without_deprecations).to eq("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- thin/) end it "sets up everything if Bundler.setup is used with no groups" do - output = run("require 'rack'; puts RACK") + output = run("require 'myrack'; puts MYRACK") expect(output).to eq("1.0.0") output = run("require 'activesupport'; puts ACTIVESUPPORT") @@ -57,7 +57,7 @@ RSpec.describe "bundle install with groups" do puts THIN RUBY - expect(err_without_deprecations).to eq("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- thin/) end it "sets up old groups when they have previously been removed" do @@ -70,12 +70,12 @@ RSpec.describe "bundle install with groups" do end end - describe "installing --without" do + describe "without option" do describe "with gems assigned to a single group" do before :each do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" group :emo do gem "activesupport", "2.3.5" end @@ -86,54 +86,58 @@ RSpec.describe "bundle install with groups" do end it "installs gems in the default group" do - bundle! :install, forgotten_command_line_options(:without => "emo") - expect(the_bundle).to include_gems "rack 1.0.0", :groups => [:default] + bundle_config "without emo" + bundle :install + expect(the_bundle).to include_gems "myrack 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] + it "respects global `without` configuration, but does not save it locally" do + bundle_config_global "without emo" + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0", groups: [:default] + bundle "config list" + expect(out).not_to include("Set for your local app (#{bundled_app(".bundle/config")}): [:emo]") + expect(out).to include("Set for the current user (#{home(".bundle/config")}): [:emo]") + end + + it "allows running application where groups where configured by a different user" do + bundle_config "without emo" + bundle :install + bundle "exec ruby -e 'puts 42'", env: { "BUNDLE_USER_HOME" => tmp("new_home").to_s } + expect(out).to include("42") end - it "does not install gems from the previously excluded group" do - bundle :install, forgotten_command_line_options(:without => "emo") - expect(the_bundle).not_to include_gems "activesupport 2.3.5" + it "does not install gems from the excluded group" do + bundle_config "without emo" bundle :install - expect(the_bundle).not_to include_gems "activesupport 2.3.5" + expect(the_bundle).not_to include_gems "activesupport 2.3.5", groups: [:default] end it "does not say it installed gems from the excluded group" do - bundle! :install, forgotten_command_line_options(:without => "emo") + bundle_config "without emo" + bundle :install expect(out).not_to include("activesupport") end it "allows Bundler.setup for specific groups" do - bundle :install, forgotten_command_line_options(:without => "emo") - run!("require 'rack'; puts RACK", :default) + bundle_config "without emo" + bundle :install + run("require 'myrack'; puts MYRACK", :default) expect(out).to eq("1.0.0") end it "does not effect the resolve" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "activesupport" group :emo do gem "rails", "2.3.2" end G - bundle :install, forgotten_command_line_options(: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, forgotten_command_line_options(:without => "emo") - - simulate_new_machine - bundle :install, forgotten_command_line_options(: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] + bundle_config "without emo" + bundle :install + expect(the_bundle).to include_gems "activesupport 2.3.2", groups: [:default] end it "still works when BUNDLE_WITHOUT is set" do @@ -142,100 +146,52 @@ RSpec.describe "bundle install with groups" do 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] + expect(the_bundle).to include_gems "myrack 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, forgotten_command_line_options(:without => "emo") - - bundle :install, forgotten_command_line_options(:without => "") - expect(the_bundle).to include_gems "activesupport 2.3.5" - end - - it "doesn't clear without when nothing is passed" do - bundle :install, forgotten_command_line_options(: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, forgotten_command_line_options(:with => "debugging") - expect(the_bundle).to include_gems "thin 1.0" - end - - it "does install gems from the previously requested group" do - bundle :install, forgotten_command_line_options(:with => "debugging") - expect(the_bundle).to include_gems "thin 1.0" + it "installs gems from the optional group when requested" do + bundle_config "with debugging" 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 + it "installs 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, forgotten_command_line_options(:with => "debugging") - bundle :install, forgotten_command_line_options(:with => "") - expect(the_bundle).not_to include_gems "thin 1.0" - end - - it "does remove groups from without when passed at --with", :bundler => "< 3" do - bundle :install, forgotten_command_line_options(:without => "emo") - bundle :install, forgotten_command_line_options(:with => "emo") - expect(the_bundle).to include_gems "activesupport 2.3.5" - end - - it "does remove groups from with when passed at --without", :bundler => "< 3" do - bundle :install, forgotten_command_line_options(:with => "debugging") - bundle :install, forgotten_command_line_options(:without => "debugging") - expect(the_bundle).not_to include_gem "thin 1.0" - end - - it "errors out when passing a group to with and without via CLI flags", :bundler => "< 3" do - bundle :install, forgotten_command_line_options(:with => "emo debugging", :without => "emo") - expect(last_command).to be_failure - expect(err).to include("The offending groups are: emo") - end - it "allows the BUNDLE_WITH setting to override BUNDLE_WITHOUT" do ENV["BUNDLE_WITH"] = "debugging" - bundle! :install + bundle :install expect(the_bundle).to include_gem "thin 1.0" ENV["BUNDLE_WITHOUT"] = "debugging" expect(the_bundle).to include_gem "thin 1.0" - bundle! :install + bundle :install expect(the_bundle).to include_gem "thin 1.0" end - it "can add and remove a group at the same time" do - bundle :install, forgotten_command_line_options(: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, forgotten_command_line_options(:with => "emo") + it "has no effect when listing a not optional group in with" do + bundle_config "with emo" + bundle :install 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, forgotten_command_line_options(:without => "debugging") + it "has no effect when listing an optional group in without" do + bundle_config "without debugging" + bundle :install expect(the_bundle).not_to include_gems "thin 1.0" end end @@ -243,8 +199,8 @@ RSpec.describe "bundle install with groups" do describe "with gems assigned to multiple groups" do before :each do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" group :emo, :lolercoaster do gem "activesupport", "2.3.5" end @@ -252,20 +208,22 @@ RSpec.describe "bundle install with groups" do end it "installs gems in the default group" do - bundle! :install, forgotten_command_line_options(:without => "emo lolercoaster") - expect(the_bundle).to include_gems "rack 1.0.0" + bundle_config "without emo lolercoaster" + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0" end it "installs the gem if any of its groups are installed" do - bundle! :install, forgotten_command_line_options(:without => "emo") - expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.5" + bundle_config "without emo" + bundle :install + expect(the_bundle).to include_gems "myrack 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_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" group :emo do gem "activesupport", "2.3.5" @@ -277,23 +235,21 @@ RSpec.describe "bundle install with groups" do G end - it "installs the gem w/ option --without emo" do - bundle :install, forgotten_command_line_options(:without => "emo") + it "installs the gem unless all groups are excluded" do + bundle_config "without emo" + bundle :install expect(the_bundle).to include_gems "activesupport 2.3.5" - end - it "installs the gem w/ option --without lolercoaster" do - bundle :install, forgotten_command_line_options(:without => "lolercoaster") + bundle_config "without lolercoaster" + bundle :install 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, forgotten_command_line_options(:without => "emo lolercoaster") + bundle_config "without emo lolercoaster" + bundle :install 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, forgotten_command_line_options(:without => "'emo lolercoaster'") + bundle "config set --local without 'emo lolercoaster'" + bundle :install expect(the_bundle).not_to include_gems "activesupport 2.3.5" end end @@ -302,8 +258,8 @@ RSpec.describe "bundle install with groups" do describe "nesting groups" do before :each do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" group :emo do group :lolercoaster do gem "activesupport", "2.3.5" @@ -313,13 +269,15 @@ RSpec.describe "bundle install with groups" do end it "installs gems in the default group" do - bundle! :install, forgotten_command_line_options(:without => "emo lolercoaster") - expect(the_bundle).to include_gems "rack 1.0.0" + bundle_config "without emo lolercoaster" + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0" end it "installs the gem if any of its groups are installed" do - bundle! :install, forgotten_command_line_options(:without => "emo") - expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.5" + bundle_config "without emo" + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.5" end end end @@ -327,16 +285,16 @@ RSpec.describe "bundle install with groups" do describe "when loading only the default group" do it "should not load all groups" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" gem "activesupport", :groups => :development G ruby <<-R - require "#{lib_dir}/bundler" + require "bundler" Bundler.setup :default Bundler.require :default - puts RACK + puts MYRACK begin require "activesupport" rescue LoadError @@ -349,36 +307,39 @@ RSpec.describe "bundle install with groups" do end end - describe "when locked and installed with --without" do + describe "when locked and installed with `without` setting" do before(:each) do build_repo2 - system_gems "rack-0.9.1" do - install_gemfile <<-G, forgotten_command_line_options(:without => "rack") - source "#{file_uri_for(gem_repo2)}" - gem "rack" - group :rack do - gem "rack_middleware" - end - G - end + system_gems "myrack-0.9.1" + + bundle_config "without myrack" + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack" + + group :myrack do + gem "myrack_middleware" + end + G 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" + it "uses versions from excluded gems in a machine without the without configuration" do + expect(the_bundle).to include_gems "myrack 0.9.1" + expect(the_bundle).not_to include_gems "myrack_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" + expect(the_bundle).to include_gems "myrack 0.9.1" + expect(the_bundle).to include_gems "myrack_middleware 1.0" end it "does not hit the remote a second time" do - FileUtils.rm_rf gem_repo2 - bundle! :install, forgotten_command_line_options(:without => "rack").merge(:verbose => true) - expect(last_command.stdboth).not_to match(/fetching/i) + FileUtils.rm_r gem_repo2 + bundle_config "without myrack" + bundle :install, verbose: true + expect(stdboth).not_to match(/fetching/i) end end end diff --git a/spec/bundler/install/gemfile/install_if.rb b/spec/bundler/install/gemfile/install_if.rb deleted file mode 100644 index bfdd8fbae8..0000000000 --- a/spec/bundler/install/gemfile/install_if.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -describe "bundle install with install_if conditionals" do - it "follows the install_if DSL" do - install_gemfile <<-G - source "#{file_uri_for(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_uri_for(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/install_if_spec.rb b/spec/bundler/install/gemfile/install_if_spec.rb new file mode 100644 index 0000000000..05a6d15129 --- /dev/null +++ b/spec/bundler/install/gemfile/install_if_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with install_if conditionals" do + it "follows the install_if DSL" do + install_gemfile <<-G + source "https://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 "myrack" + G + + expect(the_bundle).to include_gems("myrack 1.0", "activesupport 2.3.5") + expect(the_bundle).not_to include_gems("thin") + expect(the_bundle).not_to include_gems("foo") + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo1, "activesupport", "2.3.5" + c.checksum gem_repo1, "foo", "1.0" + c.checksum gem_repo1, "myrack", "1.0.0" + c.checksum gem_repo1, "thin", "1.0" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + activesupport (2.3.5) + foo (1.0) + myrack (1.0.0) + thin (1.0) + myrack + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + activesupport (= 2.3.5) + foo + myrack + thin + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end +end diff --git a/spec/bundler/install/gemfile/lockfile_spec.rb b/spec/bundler/install/gemfile/lockfile_spec.rb index b9545b91c2..19bd7074b2 100644 --- a/spec/bundler/install/gemfile/lockfile_spec.rb +++ b/spec/bundler/install/gemfile/lockfile_spec.rb @@ -2,13 +2,14 @@ RSpec.describe "bundle install with a lockfile present" do let(:gf) { <<-G } - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" G - subject do + it "touches the lockfile on install even when nothing has changed" do install_gemfile(gf) + expect { bundle :install }.to change { bundled_app_lock.mtime } end context "gemfile evaluation" do @@ -16,32 +17,25 @@ RSpec.describe "bundle install with a lockfile present" do context "with plugins disabled" do before do - bundle! "config set plugins false" - subject + bundle_config "plugins false" end - it "does not evaluate the gemfile twice" do - bundle! :install + it "does not evaluate the gemfile twice when the gem is already installed" do + install_gemfile(gf) + bundle :install - with_env_vars("BUNDLER_SPEC_NO_APPEND" => "1") { expect(the_bundle).to include_gem "rack 1.0.0" } + with_env_vars("BUNDLER_SPEC_NO_APPEND" => "1") { expect(the_bundle).to include_gem "myrack 1.0.0" } - # The first eval is from the initial install, we're testing that the - # second install doesn't double-eval expect(bundled_app("evals").read.lines.to_a.size).to eq(2) end - context "when the gem is not installed" do - before { FileUtils.rm_rf ".bundle" } + it "does not evaluate the gemfile twice when the gem is not installed" do + gemfile(gf) + bundle :install - it "does not evaluate the gemfile twice" do - bundle! :install + with_env_vars("BUNDLER_SPEC_NO_APPEND" => "1") { expect(the_bundle).to include_gem "myrack 1.0.0" } - with_env_vars("BUNDLER_SPEC_NO_APPEND" => "1") { expect(the_bundle).to include_gem "rack 1.0.0" } - - # The first eval is from the initial install, we're testing that the - # second install doesn't double-eval - expect(bundled_app("evals").read.lines.to_a.size).to eq(2) - end + expect(bundled_app("evals").read.lines.to_a.size).to eq(1) end end end diff --git a/spec/bundler/install/gemfile/override_spec.rb b/spec/bundler/install/gemfile/override_spec.rb new file mode 100644 index 0000000000..02b0e7d772 --- /dev/null +++ b/spec/bundler/install/gemfile/override_spec.rb @@ -0,0 +1,401 @@ +# frozen_string_literal: true + +RSpec.describe "override DSL" do + context "with a version: string operation" do + it "replaces a direct dependency requirement with the override version spec" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 0.9.1" + gem "myrack" + G + + expect(the_bundle).to include_gems "myrack 0.9.1" + end + + it "replaces a transitive dependency requirement" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 1.0.0" + gem "myrack_middleware" + G + + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0" + end + + it "replaces the requirement even when the Gemfile pins a different version" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 0.9.1" + gem "myrack", "= 1.0.0" + G + + expect(the_bundle).to include_gems "myrack 0.9.1" + end + + it "applies the override against an existing lockfile" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + + gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 0.9.1" + gem "myrack" + G + + bundle :install + + expect(the_bundle).to include_gems "myrack 0.9.1" + end + + it "pins a prerelease version that the Gemfile dependency would otherwise filter out" do + build_repo2 do + build_gem "has_prerelease", "1.0" + build_gem "has_prerelease", "1.1.pre" + end + + install_gemfile <<-G + source "https://gem.repo2" + override "has_prerelease", version: "= 1.1.pre" + gem "has_prerelease" + G + + expect(the_bundle).to include_gems "has_prerelease 1.1.pre" + end + end + + context "with a version: :ignore_upper operation" do + it "strips a < upper bound on a direct dependency" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: :ignore_upper + gem "myrack", "< 1.0" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "folds ~> into >= so newer versions become reachable" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: :ignore_upper + gem "myrack", "~> 0.9.1" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + end + + context "with a version: nil operation" do + it "drops a direct dependency's pin entirely" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: nil + gem "myrack", "= 0.9.1" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "drops a transitive dependency's pin entirely" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: nil + gem "myrack_middleware" + G + + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0" + end + + it "applies a transitive-only override against an existing lockfile" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack_middleware" + G + + expect(the_bundle).to include_gems "myrack 0.9.1", "myrack_middleware 1.0" + + gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 1.0.0" + gem "myrack_middleware" + G + + bundle :install + + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0" + end + end + + context "lockfile contents" do + it "does not record the override directive in Gemfile.lock" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 0.9.1" + gem "myrack" + G + + expect(lockfile).not_to match(/override/i) + end + end + + context "with a required_ruby_version: operation" do + it "lets the resolver pick a gem whose required_ruby_version excludes the current Ruby with :ignore_upper" do + build_repo2 do + build_gem "needs_old_ruby", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + gemfile <<-G + source "https://gem.repo2" + override "needs_old_ruby", required_ruby_version: :ignore_upper + gem "needs_old_ruby" + G + + bundle :lock + expect(lockfile).to include("needs_old_ruby (1.0)") + end + + it "lets the resolver pick the gem with required_ruby_version: nil" do + build_repo2 do + build_gem "needs_old_ruby", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + gemfile <<-G + source "https://gem.repo2" + override "needs_old_ruby", required_ruby_version: nil + gem "needs_old_ruby" + G + + bundle :lock + expect(lockfile).to include("needs_old_ruby (1.0)") + end + + it "applies to a transitive dependency's required_ruby_version" do + build_repo2 do + build_gem "needs_old_ruby", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + build_gem "wraps_old", "1.0" do |s| + s.add_dependency "needs_old_ruby" + end + end + + gemfile <<-G + source "https://gem.repo2" + override "needs_old_ruby", required_ruby_version: :ignore_upper + gem "wraps_old" + G + + bundle :lock + expect(lockfile).to include("needs_old_ruby (1.0)") + expect(lockfile).to include("wraps_old (1.0)") + end + + it "re-resolves a direct dep when a metadata override is added against an existing lockfile" do + build_repo2 do + build_gem "selectable", "1.0" + build_gem "selectable", "2.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + gemfile <<-G + source "https://gem.repo2" + gem "selectable" + G + + bundle :lock + expect(lockfile).to include("selectable (1.0)") + + gemfile <<-G + source "https://gem.repo2" + override "selectable", required_ruby_version: :ignore_upper + gem "selectable" + G + + bundle :lock + expect(lockfile).to include("selectable (2.0)") + end + end + + context "with a required_rubygems_version: operation" do + it "lets the resolver pick a gem whose required_rubygems_version excludes the current RubyGems with :ignore_upper" do + build_repo2 do + build_gem "needs_old_rubygems", "1.0" do |s| + s.required_rubygems_version = "< #{Gem.rubygems_version}" + end + end + + gemfile <<-G + source "https://gem.repo2" + override "needs_old_rubygems", required_rubygems_version: :ignore_upper + gem "needs_old_rubygems" + G + + bundle :lock + expect(lockfile).to include("needs_old_rubygems (1.0)") + end + end + + context "with an :all target" do + it "applies required_ruby_version: :ignore_upper to every gem" do + build_repo2 do + build_gem "needs_old_ruby_a", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + build_gem "needs_old_ruby_b", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + gemfile <<-G + source "https://gem.repo2" + override :all, required_ruby_version: :ignore_upper + gem "needs_old_ruby_a" + gem "needs_old_ruby_b" + G + + bundle :lock + expect(lockfile).to include("needs_old_ruby_a (1.0)") + expect(lockfile).to include("needs_old_ruby_b (1.0)") + end + + it "is overridden by a per-gem override on the same field" do + build_repo2 do + build_gem "permissive", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + build_gem "still_blocked", "1.0" do |s| + s.required_ruby_version = "= #{Gem.ruby_version}.999" + end + end + + # :all says ignore_upper (would unblock both), but per-gem on + # still_blocked nails it to a hard requirement that still fails. + gemfile <<-G + source "https://gem.repo2" + override :all, required_ruby_version: :ignore_upper + override "still_blocked", required_ruby_version: "= #{Gem.ruby_version}.999" + gem "permissive" + gem "still_blocked" + G + + bundle :lock, raise_on_error: false + expect(err).to include("still_blocked") + end + + it "preserves locked versions when an :all metadata override is added without bundle update" do + build_repo2 do + build_gem "selectable", "1.0" + build_gem "selectable", "2.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + gemfile <<-G + source "https://gem.repo2" + gem "selectable" + G + + bundle :lock + expect(lockfile).to include("selectable (1.0)") + + gemfile <<-G + source "https://gem.repo2" + override :all, required_ruby_version: :ignore_upper + gem "selectable" + G + + # :all override alone does not pre-unlock locked specs; narrow change + # should not trigger unrelated lockfile churn. + bundle :lock + expect(lockfile).to include("selectable (1.0)") + + # bundle update opts the user into re-resolution under the override. + bundle "update selectable" + expect(lockfile).to include("selectable (2.0)") + end + end + + context "diagnostic on resolve failure" do + it "lists active overrides with their Gemfile location" do + build_repo2 do + build_gem "needs_old_ruby", "1.0" do |s| + s.required_ruby_version = "= #{Gem.ruby_version}.999" + end + end + + gemfile <<-G + source "https://gem.repo2" + override "needs_old_ruby", required_ruby_version: "= #{Gem.ruby_version}.999" + gem "needs_old_ruby" + G + + bundle :lock, raise_on_error: false + expect(err).to include("Bundler applied the following overrides") + expect(err).to include("override \"needs_old_ruby\", required_ruby_version:") + expect(err).to match(/declared at Gemfile:\d+/) + end + end + + context "install-time compatibility" do + it "installs a gem whose required_ruby_version excludes the current Ruby when an override removes the constraint" do + build_repo2 do + build_gem "needs_old_ruby", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + install_gemfile <<-G + source "https://gem.repo2" + override "needs_old_ruby", required_ruby_version: nil + gem "needs_old_ruby" + G + + expect(the_bundle).to include_gems "needs_old_ruby 1.0" + end + + it "installs a gem whose required_rubygems_version excludes the current RubyGems when an override removes it" do + build_repo2 do + build_gem "needs_old_rubygems", "1.0" do |s| + s.required_rubygems_version = "< #{Gem.rubygems_version}" + end + end + + install_gemfile <<-G + source "https://gem.repo2" + override "needs_old_rubygems", required_rubygems_version: nil + gem "needs_old_rubygems" + G + + expect(the_bundle).to include_gems "needs_old_rubygems 1.0" + end + + it "installs every gem when :all required_ruby_version override is in effect" do + build_repo2 do + build_gem "needs_old_ruby_a", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + build_gem "needs_old_ruby_b", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + install_gemfile <<-G + source "https://gem.repo2" + override :all, required_ruby_version: :ignore_upper + gem "needs_old_ruby_a" + gem "needs_old_ruby_b" + G + + expect(the_bundle).to include_gems "needs_old_ruby_a 1.0", "needs_old_ruby_b 1.0" + end + end +end diff --git a/spec/bundler/install/gemfile/path_spec.rb b/spec/bundler/install/gemfile/path_spec.rb index 786b767354..b069488531 100644 --- a/spec/bundler/install/gemfile/path_spec.rb +++ b/spec/bundler/install/gemfile/path_spec.rb @@ -1,17 +1,6 @@ # frozen_string_literal: true RSpec.describe "bundle install with explicit source paths" do - it "fetches gems with a global path source", :bundler => "< 3" 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 "fetches gems" do build_lib "foo" @@ -37,7 +26,7 @@ RSpec.describe "bundle install with explicit source paths" do it "supports relative paths" do build_lib "foo" - relative_path = lib_path("foo-1.0").relative_path_from(Pathname.new(Dir.pwd)) + relative_path = lib_path("foo-1.0").relative_path_from(bundled_app) install_gemfile <<-G gem 'foo', :path => "#{relative_path}" @@ -59,11 +48,13 @@ RSpec.describe "bundle install with explicit source paths" do end it "expands paths raise error with not existing user's home dir" do + skip "problems with ~ expansion" if Gem.win_platform? + 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 + install_gemfile <<-G, raise_on_error: false gem 'foo', :path => "~#{username}/#{relative_path}" G expect(err).to match("There was an error while trying to use the path `~#{username}/#{relative_path}`.") @@ -71,28 +62,29 @@ RSpec.describe "bundle install with explicit source paths" do end it "expands paths relative to Bundler.root" do - build_lib "foo", :path => bundled_app("foo-1.0") + 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 + expect(the_bundle).to include_gems("foo 1.0", dir: bundled_app("subdir").mkpath) end it "sorts paths consistently on install and update when they start with ./" do - build_lib "demo", :path => lib_path("demo") - build_lib "aaa", :path => lib_path("demo/aaa") + build_lib "demo", path: lib_path("demo") + build_lib "aaa", path: lib_path("demo/aaa") - gemfile = <<-G + gemfile lib_path("demo/Gemfile"), <<-G + source "https://gem.repo1" gemspec gem "aaa", :path => "./aaa" G - File.open(lib_path("demo/Gemfile"), "w") {|f| f.puts gemfile } + checksums = checksums_section_when_enabled do |c| + c.no_checksum "aaa", "1.0" + c.no_checksum "demo", "1.0" + end lockfile = <<~L PATH @@ -106,6 +98,7 @@ RSpec.describe "bundle install with explicit source paths" do aaa (1.0) GEM + remote: https://gem.repo1/ specs: PLATFORMS @@ -114,60 +107,57 @@ RSpec.describe "bundle install with explicit source paths" do DEPENDENCIES aaa! demo! - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L - Dir.chdir(lib_path("demo")) do - bundle :install - expect(lib_path("demo/Gemfile.lock")).to have_lockfile(lockfile) - bundle :update, :all => true - expect(lib_path("demo/Gemfile.lock")).to have_lockfile(lockfile) - end + bundle :install, dir: lib_path("demo") + expect(lib_path("demo/Gemfile.lock")).to read_as(lockfile) + bundle :update, all: true, dir: lib_path("demo") + expect(lib_path("demo/Gemfile.lock")).to read_as(lockfile) end it "expands paths when comparing locked paths to Gemfile paths" do - build_lib "foo", :path => bundled_app("foo-1.0") + build_lib "foo", path: bundled_app("foo-1.0") install_gemfile <<-G - gem 'foo', :path => File.expand_path("../foo-1.0", __FILE__) + gem 'foo', :path => File.expand_path("foo-1.0", __dir__) G - bundle! :install, forgotten_command_line_options(:frozen => true) - expect(exitstatus).to eq(0) if exitstatus + bundle_config "frozen true" + bundle :install end it "installs dependencies from the path even if a newer gem is available elsewhere" do - system_gems "rack-1.0.0" + system_gems "myrack-1.0.0" - build_lib "rack", "1.0", :path => lib_path("nested/bar") do |s| - s.write "lib/rack.rb", "puts 'WIN OVERRIDE'" + build_lib "myrack", "1.0", path: lib_path("nested/bar") do |s| + s.write "lib/myrack.rb", "puts 'WIN OVERRIDE'" end - build_lib "foo", :path => lib_path("nested") do |s| - s.add_dependency "rack", "= 1.0" + build_lib "foo", path: lib_path("nested") do |s| + s.add_dependency "myrack", "= 1.0" end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" gem "foo", :path => "#{lib_path("nested")}" G - run "require 'rack'" + run "require 'myrack'" expect(out).to eq("WIN OVERRIDE") end it "works" do - build_gem "foo", "1.0.0", :to_system => true do |s| + 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| + 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") + build_lib "foo", "1.0.0", path: lib_path("omg/foo") install_gemfile <<-G gem "omg", :path => "#{lib_path("omg")}" @@ -176,22 +166,88 @@ RSpec.describe "bundle install with explicit source paths" do expect(the_bundle).to include_gems "foo 1.0" end - it "works with only_update_to_newer_versions" do - build_lib "omg", "2.0", :path => lib_path("omg") + it "works when using prereleases of 0.0.0" do + build_lib "foo", "0.0.0.dev", path: lib_path("foo") + + gemfile <<~G + source "https://gem.repo1" + gem "foo", :path => "#{lib_path("foo")}" + G + + lockfile <<~L + PATH + remote: #{lib_path("foo")} + specs: + foo (0.0.0.dev) + + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle :install + + expect(the_bundle).to include_gems "foo 0.0.0.dev" + end + + it "works when using uppercase prereleases of 0.0.0" do + build_lib "foo", "0.0.0.SNAPSHOT", path: lib_path("foo") + + gemfile <<~G + source "https://gem.repo1" + gem "foo", :path => "#{lib_path("foo")}" + G + + lockfile <<~L + PATH + remote: #{lib_path("foo")} + specs: + foo (0.0.0.SNAPSHOT) + + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle :install + + expect(the_bundle).to include_gems "foo 0.0.0.SNAPSHOT" + end + + it "handles downgrades" do + build_lib "omg", "2.0", path: lib_path("omg") install_gemfile <<-G gem "omg", :path => "#{lib_path("omg")}" G - build_lib "omg", "1.0", :path => lib_path("omg") + build_lib "omg", "1.0", path: lib_path("omg") - bundle! :install, :env => { "BUNDLE_BUNDLE_ONLY_UPDATE_TO_NEWER_VERSIONS" => "true" } + bundle :install expect(the_bundle).to include_gems "omg 1.0" end it "prefers gemspecs closer to the path root" do - build_lib "premailer", "1.0.0", :path => lib_path("premailer") do |s| + 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' @@ -223,106 +279,152 @@ RSpec.describe "bundle install with explicit source paths" do G end - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false gem "foo", :path => "#{lib_path("foo-1.0")}" G - expect(err).to_not include("ERROR REPORT") - expect(err).to_not include("Your Gemfile has no gem server sources.") expect(err).to match(/is not valid. Please fix this gemspec./) expect(err).to match(/The validation error was 'missing value for attribute version'/) expect(err).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" + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack", "1.0" end - gemfile = <<-G - source "#{file_uri_for(gem_repo1)}" + gemfile lib_path("foo/Gemfile"), <<-G + source "https://gem.repo1" gemspec G - File.open(lib_path("foo/Gemfile"), "w") {|f| f.puts gemfile } + bundle "install", dir: lib_path("foo") + expect(the_bundle).to include_gems "foo 1.0", dir: lib_path("foo") + expect(the_bundle).to include_gems "myrack 1.0", dir: lib_path("foo") + end - 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" + it "does not unlock dependencies of path sources" do + build_repo4 do + build_gem "graphql", "2.0.15" + build_gem "graphql", "2.0.16" + end + + build_lib "foo", "0.1.0", path: lib_path("foo") do |s| + s.add_dependency "graphql", "~> 2.0" + end + + gemfile_path = lib_path("foo/Gemfile") + + gemfile gemfile_path, <<-G + source "https://gem.repo4" + gemspec + G + + lockfile_path = lib_path("foo/Gemfile.lock") + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "0.1.0" + c.checksum gem_repo4, "graphql", "2.0.15" end + + original_lockfile = <<~L + PATH + remote: . + specs: + foo (0.1.0) + graphql (~> 2.0) + + GEM + remote: https://gem.repo4/ + specs: + graphql (2.0.15) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + lockfile lockfile_path, original_lockfile + + build_lib "foo", "0.1.1", path: lib_path("foo") do |s| + s.add_dependency "graphql", "~> 2.0" + end + + bundle "install", dir: lib_path("foo") + expect(lockfile_path).to read_as(original_lockfile.gsub("foo (0.1.0)", "foo (0.1.1)")) 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" + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack", "1.0" end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://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" + expect(the_bundle).to include_gems "myrack 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" + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack", ">= 1.0" end - Dir.chdir lib_path("foo") - - install_gemfile lib_path("foo/Gemfile"), <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile lib_path("foo/Gemfile"), <<-G, dir: lib_path("foo") + source "https://gem.repo1" gemspec G - build_gem "rack", "1.0.1", :to_system => true + build_gem "myrack", "1.0.1", to_system: true - bundle "install" + bundle "install", dir: lib_path("foo") - expect(the_bundle).to include_gems "foo 1.0" - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "foo 1.0", dir: lib_path("foo") + expect(the_bundle).to include_gems "myrack 1.0", dir: lib_path("foo") 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" + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack", ">= 1.0" s.add_development_dependency "activesupport" end - Dir.chdir lib_path("foo") - - install_gemfile lib_path("foo/Gemfile"), <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile lib_path("foo/Gemfile"), <<-G, dir: lib_path("foo") + source "https://gem.repo1" gemspec G - build_gem "rack", "1.0.1", :to_system => true + build_gem "myrack", "1.0.1", to_system: true - bundle "install" + bundle "install", dir: lib_path("foo") - expect(the_bundle).to include_gems "foo 1.0" - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "foo 1.0", dir: lib_path("foo") + expect(the_bundle).to include_gems "myrack 1.0", dir: lib_path("foo") end it "raises if there are multiple gemspecs" do - build_lib "foo", "1.0", :path => lib_path("foo") do |s| + 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 + install_gemfile <<-G, raise_on_error: false gemspec :path => "#{lib_path("foo")}" G - expect(exitstatus).to eq(15) if exitstatus + expect(exitstatus).to eq(15) expect(err).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| + build_lib "foo", "1.0", path: lib_path("foo") do |s| s.write "bar.gemspec" end @@ -338,11 +440,12 @@ RSpec.describe "bundle install with explicit source paths" do s.executables = "foobar" end - install_gemfile <<-G + install_gemfile <<-G, verbose: true path "#{lib_path("foo-1.0")}" do gem 'foo' end G + expect(out).to include("Using foo 1.0 from source at `#{lib_path("foo-1.0")}` and installing its executables") expect(the_bundle).to include_gems "foo 1.0" bundle "exec foobar" @@ -351,7 +454,7 @@ RSpec.describe "bundle install with explicit source paths" do it "handles directories in bin/" do build_lib "foo" - lib_path("foo-1.0").join("foo.gemspec").rmtree + FileUtils.rm_rf lib_path("foo-1.0").join("foo.gemspec") lib_path("foo-1.0").join("bin/performance").mkpath install_gemfile <<-G @@ -387,9 +490,9 @@ RSpec.describe "bundle install with explicit source paths" do 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| + 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 @@ -402,7 +505,7 @@ RSpec.describe "bundle install with explicit source paths" do end it "works when the path does not have a gemspec" do - build_lib "foo", :gemspec => false + build_lib "foo", gemspec: false gemfile <<-G gem "foo", "1.0", :path => "#{lib_path("foo-1.0")}" @@ -418,50 +521,46 @@ RSpec.describe "bundle install with explicit source paths" do PATH remote: vendor/bar specs: - - GEM - remote: http://rubygems.org L - in_app_root { FileUtils.mkdir_p("vendor/bar") } + FileUtils.mkdir_p(bundled_app("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_uri_for(gem_repo1)}" - gem 'rack-obama', '1.0' + source "https://gem.repo1" + gem 'myrack-obama', '1.0' gem 'net-ssh', '1.0' G - bundle :check, :env => { "DEBUG" => 1 } + 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" + expect(the_bundle).to include_gems "myrack-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| + build_lib "myrack-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| + build_lib "net-ssh", "1.0", path: lib_path("omg") do |s| s.add_dependency "yard" end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack-obama', :path => "#{lib_path("omg")}" + source "https://gem.repo1" + gem 'myrack-obama', :path => "#{lib_path("omg")}" gem 'net-ssh', :path => "#{lib_path("omg")}" G - bundle :check, :env => { "DEBUG" => 1 } + 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" + expect(the_bundle).to include_gems "myrack-obama 1.0", "net-ssh 1.0" end end @@ -480,10 +579,10 @@ RSpec.describe "bundle install with explicit source paths" do 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| + 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") + build_lib "bar", "1.0", path: lib_path("foo/bar") install_gemfile <<-G gem "foo", :path => "#{lib_path("foo")}" @@ -491,7 +590,7 @@ RSpec.describe "bundle install with explicit source paths" do end it "unlocks all gems when the top level gem is updated" do - build_lib "foo", "2.0", :path => lib_path("foo") do |s| + build_lib "foo", "2.0", path: lib_path("foo") do |s| s.add_dependency "bar" end @@ -501,7 +600,7 @@ RSpec.describe "bundle install with explicit source paths" do end it "unlocks all gems when a child dependency gem is updated" do - build_lib "bar", "2.0", :path => lib_path("foo/bar") + build_lib "bar", "2.0", path: lib_path("foo/bar") bundle "install" @@ -511,104 +610,280 @@ RSpec.describe "bundle install with explicit source paths" do describe "when dependencies in the path are updated" do before :each do - build_lib "foo", "1.0", :path => lib_path("foo") + build_lib "foo", "1.0", path: lib_path("foo") install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://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" + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack" end bundle "install" - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "keeps using the same version if it's compatible" do - build_lib "foo", "1.0", :path => lib_path("foo") do |s| - s.add_dependency "rack", "0.9.1" + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack", "0.9.1" end bundle "install" - expect(the_bundle).to include_gems "rack 0.9.1" + expect(the_bundle).to include_gems "myrack 0.9.1" - lockfile_should_be <<-G + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo1, "myrack", "0.9.1" + end + + expect(lockfile).to eq <<~G PATH remote: #{lib_path("foo")} specs: foo (1.0) - rack (= 0.9.1) + myrack (= 0.9.1) GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: - rack (0.9.1) + myrack (0.9.1) PLATFORMS #{lockfile_platforms} DEPENDENCIES foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G + + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack" + end + bundle "install" + + expect(lockfile).to eq <<~G + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + myrack + + GEM + remote: https://gem.repo1/ + specs: + myrack (0.9.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G - build_lib "foo", "1.0", :path => lib_path("foo") do |s| - s.add_dependency "rack" + expect(the_bundle).to include_gems "myrack 0.9.1" + end + + it "keeps using the same version even when another dependency is added" do + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack", "0.9.1" end bundle "install" - lockfile_should_be <<-G + expect(the_bundle).to include_gems "myrack 0.9.1" + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo1, "myrack", "0.9.1" + end + + expect(lockfile).to eq <<~G PATH remote: #{lib_path("foo")} specs: foo (1.0) - rack + myrack (= 0.9.1) GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: - rack (0.9.1) + myrack (0.9.1) PLATFORMS #{lockfile_platforms} DEPENDENCIES foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G + + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack" + s.add_dependency "rake", rake_version + end + + bundle "install" + checksums.checksum gem_repo1, "rake", rake_version + + expect(lockfile).to eq <<~G + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + myrack + rake (= #{rake_version}) + + GEM + remote: https://gem.repo1/ + specs: + myrack (0.9.1) + rake (#{rake_version}) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G - expect(the_bundle).to include_gems "rack 0.9.1" + expect(the_bundle).to include_gems "myrack 0.9.1" + end + + it "does not remove existing ruby platform" do + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack", "0.9.1" + end + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + end + + lockfile <<~L + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms("ruby")} + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock" + + checksums.checksum gem_repo1, "myrack", "0.9.1" + + expect(lockfile).to eq <<~G + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + myrack (= 0.9.1) + + GEM + remote: https://gem.repo1/ + specs: + myrack (0.9.1) + + PLATFORMS + #{lockfile_platforms("ruby")} + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G + end + end + + context "when platform specific version locked, and having less dependencies that the generic version that's actually installed" do + before do + build_repo4 do + build_gem "racc", "1.8.1" + build_gem "mini_portile2", "2.8.2" + end + + build_lib "nokogiri", "1.18.9", path: lib_path("nokogiri") do |s| + s.add_dependency "mini_portile2", "~> 2.8.2" + s.add_dependency "racc", "~> 1.4" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri", path: "#{lib_path("nokogiri")}" + G + + lockfile <<~L + PATH + remote: #{lib_path("nokogiri")} + specs: + nokogiri (1.18.9) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.18.9-arm64-darwin) + racc (~> 1.4) + + GEM + remote: https://rubygems.org/ + specs: + racc (1.8.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + nokogiri! + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "works" do + bundle "install" 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| + 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| + 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_uri_for(gem_repo1)}" gem "bar", :git => "#{lib_path("bar")}" G install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" gem "bar", :path => "#{lib_path("bar")}" G @@ -616,23 +891,23 @@ RSpec.describe "bundle install with explicit source paths" do 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| + build_lib "foo", "1.0", path: lib_path("foo") + build_gem "bar", "1.0", to_bundle: true do |s| s.write "lib/bar.rb", "raise 'fail'" end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "bar" path "#{lib_path("foo")}" do gem "foo" end G - build_lib "bar", "1.0", :path => lib_path("foo/bar") + build_lib "bar", "1.0", path: lib_path("foo/bar") install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" path "#{lib_path("foo")}" do gem "foo" gem "bar" @@ -645,21 +920,17 @@ RSpec.describe "bundle install with explicit source paths" do 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 + build_lib "private_lib", "2.2", path: lib_path("private_lib") + gemfile lib_path("private_lib/Gemfile"), <<-G source "http://localgemserver.test" gemspec - gem 'rack' + gem 'myrack' 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 + bundle :install, env: { "DEBUG" => "1" }, artifice: "endpoint", dir: lib_path("private_lib") + expect(out).to match(%r{^HTTP GET http://localgemserver\.test/api/v1/dependencies\?gems=myrack$}) + expect(out).not_to match(/^HTTP GET.*private_lib/) + expect(the_bundle).to include_gems "private_lib 2.2", dir: lib_path("private_lib") + expect(the_bundle).to include_gems "myrack 1.0", dir: lib_path("private_lib") end end @@ -679,7 +950,7 @@ RSpec.describe "bundle install with explicit source paths" do end bundle :install, - :requires => [lib_path("install_hooks.rb")] + requires: [lib_path("install_hooks.rb")] expect(err_without_deprecations).to eq("Ran pre-install hook: foo-1.0") end @@ -698,7 +969,7 @@ RSpec.describe "bundle install with explicit source paths" do end bundle :install, - :requires => [lib_path("install_hooks.rb")] + requires: [lib_path("install_hooks.rb")] expect(err_without_deprecations).to eq("Ran post-install hook: foo-1.0") end @@ -716,8 +987,7 @@ RSpec.describe "bundle install with explicit source paths" do H end - bundle :install, - :requires => [lib_path("install_hooks.rb")] + bundle :install, requires: [lib_path("install_hooks.rb")], raise_on_error: false expect(err).to include("failed for foo-1.0") end @@ -728,14 +998,14 @@ RSpec.describe "bundle install with explicit source paths" do expect(bar_file).not_to be_file build_lib "foo" do |s| - s.write("lib/rubygems_plugin.rb", "FileUtils.touch('#{foo_file}')") + s.write("lib/rubygems_plugin.rb", "require 'fileutils'; FileUtils.touch('#{foo_file}')") end build_git "bar" do |s| - s.write("lib/rubygems_plugin.rb", "FileUtils.touch('#{bar_file}')") + s.write("lib/rubygems_plugin.rb", "require 'fileutils'; FileUtils.touch('#{bar_file}')") end - install_gemfile! <<-G + install_gemfile <<-G gem "foo", :path => "#{lib_path("foo-1.0")}" gem "bar", :path => "#{lib_path("bar-1.0")}" G diff --git a/spec/bundler/install/gemfile/platform_spec.rb b/spec/bundler/install/gemfile/platform_spec.rb index c096531398..e12933ebcf 100644 --- a/spec/bundler/install/gemfile/platform_spec.rb +++ b/spec/bundler/install/gemfile/platform_spec.rb @@ -4,30 +4,30 @@ 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_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: - rack (0.9.1) + myrack (0.9.1) PLATFORMS #{not_local} DEPENDENCIES - rack + myrack G install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack" + gem "myrack" G - expect(the_bundle).to include_gems "rack 0.9.1" + expect(the_bundle).to include_gems "myrack 0.9.1" end it "pulls in the correct platform specific gem" do lockfile <<-G GEM - remote: #{file_uri_for(gem_repo1)} + remote: https://gem.repo1 specs: platform_specific (1.0) platform_specific (1.0-java) @@ -40,37 +40,139 @@ RSpec.describe "bundle install across platforms" do platform_specific G - simulate_platform "java" + simulate_platform "java" do + install_gemfile <<-G + source "https://gem.repo1" + + gem "platform_specific" + G + + expect(the_bundle).to include_gems "platform_specific 1.0 java" + end + end + + it "pulls the pure ruby version on jruby if the java platform is not present in the lockfile and bundler is run in frozen mode", :jruby_only do + lockfile <<-G + GEM + remote: https://gem.repo1 + specs: + platform_specific (1.0) + + PLATFORMS + ruby + + DEPENDENCIES + platform_specific + G + + bundle_config "frozen true" + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "platform_specific" G - expect(the_bundle).to include_gems "platform_specific 1.0 JAVA" + expect(the_bundle).to include_gems "platform_specific 1.0 ruby" + expect(err).to be_empty + end + + context "on universal Rubies" do + before do + build_repo4 do + build_gem "darwin_single_arch" do |s| + s.platform = "ruby" + end + build_gem "darwin_single_arch" do |s| + s.platform = "arm64-darwin" + end + build_gem "darwin_single_arch" do |s| + s.platform = "x86_64-darwin" + end + end + end + + it "pulls in the correct architecture gem" do + lockfile <<-G + GEM + remote: https://gem.repo4 + specs: + darwin_single_arch (1.0) + darwin_single_arch (1.0-arm64-darwin) + darwin_single_arch (1.0-x86_64-darwin) + + PLATFORMS + ruby + + DEPENDENCIES + darwin_single_arch + G + + simulate_platform "universal-darwin-21" do + simulate_ruby_platform "universal.x86_64-darwin21" do + install_gemfile <<-G + source "https://gem.repo4" + + gem "darwin_single_arch" + G + + expect(the_bundle).to include_gems "darwin_single_arch 1.0 x86_64-darwin" + end + end + end + + it "pulls in the correct architecture gem on arm64e macOS Ruby" do + lockfile <<-G + GEM + remote: https://gem.repo4 + specs: + darwin_single_arch (1.0) + darwin_single_arch (1.0-arm64-darwin) + darwin_single_arch (1.0-x86_64-darwin) + + PLATFORMS + ruby + + DEPENDENCIES + darwin_single_arch + G + + simulate_platform "universal-darwin-21" do + simulate_ruby_platform "universal.arm64e-darwin21" do + install_gemfile <<-G + source "https://gem.repo4" + + gem "darwin_single_arch" + G + + expect(the_bundle).to include_gems "darwin_single_arch 1.0 arm64-darwin" + end + end + end end it "works with gems that have different dependencies" do - simulate_platform "java" - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + simulate_platform "java" do + install_gemfile <<-G + source "https://gem.repo1" - gem "nokogiri" - G + gem "nokogiri" + G - expect(the_bundle).to include_gems "nokogiri 1.4.2 JAVA", "weakling 0.0.3" + expect(the_bundle).to include_gems "nokogiri 1.4.2 java", "weakling 0.0.3" - simulate_new_machine + pristine_system_gems + bundle_config "force_ruby_platform true" + bundle "install" - simulate_platform "ruby" - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + expect(the_bundle).to include_gems "nokogiri 1.4.2" + expect(the_bundle).not_to include_gems "weakling" - gem "nokogiri" - G + simulate_new_machine + bundle "install" - expect(the_bundle).to include_gems "nokogiri 1.4.2" - expect(the_bundle).not_to include_gems "weakling" + expect(the_bundle).to include_gems "nokogiri 1.4.2 java", "weakling 0.0.3" + end end it "does not keep unneeded platforms for gems that are used" do @@ -78,204 +180,255 @@ RSpec.describe "bundle install across platforms" do build_gem "empyrean", "0.1.0" build_gem "coderay", "1.1.2" build_gem "method_source", "0.9.0" - build_gem("spoon", "0.0.6") {|s| s.add_runtime_dependency "ffi" } + build_gem("spoon", "0.0.6") {|s| s.add_dependency "ffi" } build_gem "pry", "0.11.3" do |s| s.platform = "java" - s.add_runtime_dependency "coderay", "~> 1.1.0" - s.add_runtime_dependency "method_source", "~> 0.9.0" - s.add_runtime_dependency "spoon", "~> 0.0" + s.add_dependency "coderay", "~> 1.1.0" + s.add_dependency "method_source", "~> 0.9.0" + s.add_dependency "spoon", "~> 0.0" end build_gem "pry", "0.11.3" do |s| - s.add_runtime_dependency "coderay", "~> 1.1.0" - s.add_runtime_dependency "method_source", "~> 0.9.0" + s.add_dependency "coderay", "~> 1.1.0" + s.add_dependency "method_source", "~> 0.9.0" end build_gem("ffi", "1.9.23") {|s| s.platform = "java" } build_gem("ffi", "1.9.23") end - simulate_platform java + simulate_platform "java" do + install_gemfile <<-G + source "https://gem.repo4" - install_gemfile! <<-G - source "#{file_uri_for(gem_repo4)}" + gem "empyrean", "0.1.0" + gem "pry" + G - gem "empyrean", "0.1.0" - gem "pry" - G + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "coderay", "1.1.2" + c.checksum gem_repo4, "empyrean", "0.1.0" + c.checksum gem_repo4, "ffi", "1.9.23", "java" + c.checksum gem_repo4, "method_source", "0.9.0" + c.checksum gem_repo4, "pry", "0.11.3", "java" + c.checksum gem_repo4, "spoon", "0.0.6" + end - lockfile_should_be <<-L - GEM - remote: #{file_uri_for(gem_repo4)}/ - specs: - coderay (1.1.2) - empyrean (0.1.0) - ffi (1.9.23-java) - method_source (0.9.0) - pry (0.11.3-java) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - spoon (~> 0.0) - spoon (0.0.6) - ffi + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + coderay (1.1.2) + empyrean (0.1.0) + ffi (1.9.23-java) + method_source (0.9.0) + pry (0.11.3-java) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + spoon (~> 0.0) + spoon (0.0.6) + ffi + + PLATFORMS + java + + DEPENDENCIES + empyrean (= 0.1.0) + pry + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --add-platform ruby" + + checksums.checksum gem_repo4, "pry", "0.11.3" + + good_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + coderay (1.1.2) + empyrean (0.1.0) + ffi (1.9.23-java) + method_source (0.9.0) + pry (0.11.3) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + pry (0.11.3-java) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + spoon (~> 0.0) + spoon (0.0.6) + ffi + + PLATFORMS + java + ruby + + DEPENDENCIES + empyrean (= 0.1.0) + pry + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + expect(lockfile).to eq good_lockfile + + bad_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + coderay (1.1.2) + empyrean (0.1.0) + ffi (1.9.23) + ffi (1.9.23-java) + method_source (0.9.0) + pry (0.11.3) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + pry (0.11.3-java) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + spoon (~> 0.0) + spoon (0.0.6) + ffi + + PLATFORMS + java + ruby + + DEPENDENCIES + empyrean (= 0.1.0) + pry + #{checksums} + BUNDLED WITH + 1.16.1 + L + + aggregate_failures do + lockfile bad_lockfile + bundle :install, env: { "BUNDLER_VERSION" => Bundler::VERSION } + expect(lockfile).to eq good_lockfile + + lockfile bad_lockfile + bundle :update, all: true, env: { "BUNDLER_VERSION" => Bundler::VERSION } + expect(lockfile).to eq good_lockfile + + lockfile bad_lockfile + bundle "update ffi", env: { "BUNDLER_VERSION" => Bundler::VERSION } + expect(lockfile).to eq good_lockfile + + lockfile bad_lockfile + bundle "update empyrean", env: { "BUNDLER_VERSION" => Bundler::VERSION } + expect(lockfile).to eq good_lockfile + + lockfile bad_lockfile + bundle :lock, env: { "BUNDLER_VERSION" => Bundler::VERSION } + expect(lockfile).to eq good_lockfile + end + end + end - PLATFORMS - java + it "works with gems with platform-specific dependency having different requirements order" do + simulate_platform "x86_64-darwin-15" do + update_repo2 do + build_gem "fspath", "3" + build_gem "image_optim_pack", "1.2.3" do |s| + s.add_dependency "fspath", ">= 2.1", "< 4" + end + build_gem "image_optim_pack", "1.2.3" do |s| + s.platform = "universal-darwin" + s.add_dependency "fspath", "< 4", ">= 2.1" + end + end - DEPENDENCIES - empyrean (= 0.1.0) - pry + install_gemfile <<-G + source "https://gem.repo2" + G - BUNDLED WITH - #{Bundler::VERSION} - L + install_gemfile <<-G + source "https://gem.repo2" - bundle! "lock --add-platform ruby" + gem "image_optim_pack" + G - good_lockfile = strip_whitespace(<<-L) - GEM - remote: #{file_uri_for(gem_repo4)}/ - specs: - coderay (1.1.2) - empyrean (0.1.0) - ffi (1.9.23-java) - method_source (0.9.0) - pry (0.11.3) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - pry (0.11.3-java) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - spoon (~> 0.0) - spoon (0.0.6) - ffi + expect(err).not_to include "Unable to use the platform-specific" - PLATFORMS - java - ruby + expect(the_bundle).to include_gem "image_optim_pack 1.2.3 universal-darwin" + end + end - DEPENDENCIES - empyrean (= 0.1.0) - pry + it "fetches gems again after changing the version of Ruby" do + gemfile <<-G + source "https://gem.repo1" - BUNDLED WITH - #{Bundler::VERSION} - L + gem "myrack", "1.0.0" + G + + bundle_config "path vendor/bundle" + bundle :install - lockfile_should_be good_lockfile + FileUtils.mv(vendored_gems, bundled_app("vendor/bundle", Gem.ruby_engine, "1.8")) + + bundle :install + expect(vendored_gems("gems/myrack-1.0.0")).to exist + end - bad_lockfile = strip_whitespace <<-L + it "keeps existing platforms when installing with force_ruby_platform" do + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo1, "platform_specific", "1.0" + c.checksum gem_repo1, "platform_specific", "1.0", "java" + end + + lockfile <<-G GEM - remote: #{file_uri_for(gem_repo4)}/ + remote: https://gem.repo1/ specs: - coderay (1.1.2) - empyrean (0.1.0) - ffi (1.9.23) - ffi (1.9.23-java) - method_source (0.9.0) - pry (0.11.3) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - pry (0.11.3-java) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - spoon (~> 0.0) - spoon (0.0.6) - ffi + platform_specific (1.0-java) PLATFORMS java - ruby DEPENDENCIES - empyrean (= 0.1.0) - pry - - BUNDLED WITH - #{Bundler::VERSION} - L - - aggregate_failures do - lockfile bad_lockfile - bundle! :install - lockfile_should_be good_lockfile - - lockfile bad_lockfile - bundle! :update, :all => true - lockfile_should_be good_lockfile - - lockfile bad_lockfile - bundle! "update ffi" - lockfile_should_be good_lockfile - - lockfile bad_lockfile - bundle! "update empyrean" - lockfile_should_be good_lockfile + platform_specific + #{checksums} + G - lockfile bad_lockfile - bundle! :lock - lockfile_should_be good_lockfile - end - end + bundle_config "force_ruby_platform true" - it "works the other way with gems that have different dependencies" do - simulate_platform "ruby" install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - - gem "nokogiri" + source "https://gem.repo1" + gem "platform_specific" G - simulate_platform "java" - bundle "install" + checksums.checksum gem_repo1, "platform_specific", "1.0" - 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", :bundler => "< 3" 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_uri_for(gem_repo2)}" + expect(the_bundle).to include_gem "platform_specific 1.0 ruby" - gem "facter" - G - - expect(err).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 set 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 + expect(lockfile).to eq <<~G + GEM + remote: https://gem.repo1/ + specs: + platform_specific (1.0) + platform_specific (1.0-java) - it "fetches gems again after changing the version of Ruby" do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + PLATFORMS + java + ruby - gem "rack", "1.0.0" + DEPENDENCIES + platform_specific + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} G - - bundle! :install, forgotten_command_line_options(:path => "vendor/bundle") - - FileUtils.mv(vendored_gems, bundled_app("vendor/bundle", Gem.ruby_engine, "1.8")) - - bundle! :install - 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_uri_for(gem_repo1)}" + source "https://gem.repo1" platforms :#{local_tag} do gem "nokogiri" @@ -287,20 +440,63 @@ RSpec.describe "bundle install with platform conditionals" do it "does not install gems tagged w/ another platforms" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" platforms :#{not_local_tag} do gem "nokogiri" end G - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 1.0" expect(the_bundle).not_to include_gems "nokogiri 1.4.2" end + it "installs gems tagged w/ another platform but also dependent on the current one transitively" do + build_repo4 do + build_gem "activesupport", "6.1.4.1" do |s| + s.add_dependency "tzinfo", "~> 2.0" + end + + build_gem "tzinfo", "2.0.4" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "activesupport" + + platforms :#{not_local_tag} do + gem "tzinfo", "~> 1.2" + end + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + activesupport (6.1.4.1) + tzinfo (~> 2.0) + tzinfo (2.0.4) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + activesupport + tzinfo (~> 1.2) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install --verbose" + + expect(the_bundle).to include_gems "tzinfo 2.0.4" + end + it "installs gems tagged w/ the current platforms inline" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "nokogiri", :platforms => :#{local_tag} G expect(the_bundle).to include_gems "nokogiri 1.4.2" @@ -308,17 +504,17 @@ RSpec.describe "bundle install with platform conditionals" do it "does not install gems tagged w/ another platforms inline" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" gem "nokogiri", :platforms => :#{not_local_tag} G - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 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_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "nokogiri", :platform => :#{local_tag} G expect(the_bundle).to include_gems "nokogiri 1.4.2" @@ -326,7 +522,7 @@ RSpec.describe "bundle install with platform conditionals" do it "doesn't install gems tagged w/ another platform inline" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "nokogiri", :platform => :#{not_local_tag} G expect(the_bundle).not_to include_gems "nokogiri 1.4.2" @@ -336,21 +532,20 @@ RSpec.describe "bundle install with platform conditionals" do build_git "foo" install_gemfile <<-G + source "https://gem.repo1" platform :#{not_local_tag} do gem "foo", :git => "#{lib_path("foo-1.0")}" end G bundle :list - 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" + bundle_config "force_ruby_platform true" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "some_gem", :platform => :rbx G @@ -359,67 +554,85 @@ RSpec.describe "bundle install with platform conditionals" do 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 - + bundle_config "force_ruby_platform true" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "some_gem", platform: :#{other_ruby_version_tag} + source "https://gem.repo1" + gem "some_gem", platform: :ruby_22 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" + it "does not print a warning when a dependency is unused on a platform different from the current one" do + bundle_config "force_ruby_platform true" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack", :platform => [:mingw, :mswin, :x64_mingw, :jruby] + gem "myrack", :platform => [:windows, :jruby] G - bundle! "install" + bundle "install" - expect(err).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 + expect(err).to be_empty - context "when disable_platform_warnings is true" do - before { bundle! "config set disable_platform_warnings true" } + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: - it "does not print the warning when a dependency is unused on any platform" do - simulate_platform "ruby" - simulate_ruby_engine "ruby" + PLATFORMS + ruby - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + DEPENDENCIES + myrack + #{checksums_section_when_enabled} + BUNDLED WITH + #{Bundler::VERSION} + L + end - gem "rack", :platform => [:mingw, :mswin, :x64_mingw, :jruby] - G + it "resolves fine when a dependency is unused on a platform different from the current one, but reintroduced transitively" do + bundle_config "force_ruby_platform true" - bundle! "install" + build_repo4 do + build_gem "listen", "3.7.1" do |s| + s.add_dependency "ffi" + end - expect(out).not_to match(/The dependency (.*) will be unused/) + build_gem "ffi", "1.15.5" end + + install_gemfile <<~G + source "https://gem.repo4" + + gem "listen" + gem "ffi", :platform => :windows + G + expect(err).to be_empty end end RSpec.describe "when a gem has no architecture" do it "still installs correctly" do - simulate_platform mswin + simulate_platform "x86-mswin32" do + build_repo2 do + # 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 + end - gemfile <<-G - # Try to install gem with nil arch - source "http://localgemserver.test/" - gem "rcov" - G + 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" + bundle :install, artifice: "windows", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s } + expect(the_bundle).to include_gems "rcov 1.0.0" + end end end diff --git a/spec/bundler/install/gemfile/ruby_spec.rb b/spec/bundler/install/gemfile/ruby_spec.rb index d1e9fc7e05..d937abd714 100644 --- a/spec/bundler/install/gemfile/ruby_spec.rb +++ b/spec/bundler/install/gemfile/ruby_spec.rb @@ -2,107 +2,156 @@ RSpec.describe "ruby requirement" do def locked_ruby_version - Bundler::RubyVersion.from_string(Bundler::LockfileParser.new(lockfile).ruby_version) + Bundler::RubyVersion.from_string(Bundler::LockfileParser.new(File.read(bundled_app_lock)).ruby_version) end - # As discovered by https://github.com/bundler/bundler/issues/4147, there is + # As discovered by https://github.com/rubygems/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_uri_for(gem_repo1)}" - ruby "#{RUBY_VERSION}" - gem "rack" + source "https://gem.repo1" + ruby "#{Gem.ruby_version}" + gem "myrack" G install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - ruby "#{RUBY_VERSION}" - gem "rack" - gem "rack-obama" + source "https://gem.repo1" + ruby "#{Gem.ruby_version}" + gem "myrack" + gem "myrack-obama" G - expect(exitstatus).to eq(0) if exitstatus - expect(the_bundle).to include_gems "rack-obama 1.0" + expect(the_bundle).to include_gems "myrack-obama 1.0" end it "allows removing the ruby version requirement" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - ruby "~> #{RUBY_VERSION}" - gem "rack" + source "https://gem.repo1" + ruby "~> #{Gem.ruby_version}" + gem "myrack" G expect(lockfile).to include("RUBY VERSION") install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 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_uri_for(gem_repo1)}" - ruby ">= 1.0.0" - gem "rack" + source "https://gem.repo1" + ruby ">= #{current_ruby_minor}" + gem "myrack" G + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) expect(locked_ruby_version).to eq(Bundler::RubyVersion.system) - simulate_ruby_version "5100" - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - ruby ">= 1.0.1" - gem "rack" + source "https://gem.repo1" + ruby ">= #{Gem.ruby_version}" + gem "myrack" G - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 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_uri_for(gem_repo1)}" + source "https://gem.repo1" ruby ">= 1.0.0" - gem "rack" + gem "myrack" G - expect(locked_ruby_version).to eq(Bundler::RubyVersion.system) + lockfile <<~L + GEM + remote: https://gem.repo1/ + specs: + myrack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + + RUBY VERSION + ruby 2.1.4 + + BUNDLED WITH + #{Bundler::VERSION} + L - simulate_ruby_version "5100" + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - ruby ">= 5000.0" - gem "rack" + source "https://gem.repo1" + ruby ">= #{current_ruby_minor}" + gem "myrack" G - expect(the_bundle).to include_gems "rack 1.0.0" - expect(locked_ruby_version.versions).to eq(["5100"]) + expect(the_bundle).to include_gems "myrack 1.0.0" + expect(locked_ruby_version).to eq(Bundler::RubyVersion.system) end it "allows requirements with trailing whitespace" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - ruby "#{RUBY_VERSION}\\n \t\\n" - gem "rack" + install_gemfile <<-G + source "https://gem.repo1" + ruby "#{Gem.ruby_version}\\n \t\\n" + gem "myrack" G - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "fails gracefully with malformed requirements" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" ruby ">= 0", "-.\\0" - gem "rack" + gem "myrack" G expect(err).to include("There was an error parsing") # i.e. DSL error, not error template end + + it "allows picking up ruby version from a file" do + create_file ".ruby-version", Gem.ruby_version.to_s + + install_gemfile <<-G + source "https://gem.repo1" + ruby file: ".ruby-version" + gem "myrack" + G + + expect(lockfile).to include("RUBY VERSION") + end + + it "reads the ruby version file from the right folder when nested Gemfiles are involved" do + create_file ".ruby-version", Gem.ruby_version.to_s + + gemfile <<-G + source "https://gem.repo1" + ruby file: ".ruby-version" + gem "myrack" + G + + nested_dir = bundled_app(".ruby-lsp") + + FileUtils.mkdir nested_dir + + gemfile ".ruby-lsp/Gemfile", <<-G + eval_gemfile(File.expand_path("../Gemfile", __dir__)) + G + + bundle "install", dir: nested_dir + + expect(bundled_app(".ruby-lsp/Gemfile.lock").read).to include("RUBY VERSION") + end end diff --git a/spec/bundler/install/gemfile/sources_spec.rb b/spec/bundler/install/gemfile/sources_spec.rb index 61943ef2e5..654d638e1f 100644 --- a/spec/bundler/install/gemfile/sources_spec.rb +++ b/spec/bundler/install/gemfile/sources_spec.rb @@ -2,433 +2,514 @@ 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", :bundler => "< 3" do - let(:repo3_rack_version) { "1.0.0" } - - before do - gemfile <<-G - source "#{file_uri_for(gem_repo3)}" - source "#{file_uri_for(gem_repo1)}" - gem "rack-obama" - gem "rack" - G - end - - it "warns about ambiguous gems, but installs anyway, prioritizing sources last to first", :bundler => "2" do - bundle :install - - expect(err).to include("Warning: the gem 'rack' was found in multiple sources.") - expect(err).to include("Installed from: #{file_uri_for(gem_repo1)}") - expect(the_bundle).to include_gems("rack-obama 1.0.0", "rack 1.0.0", :source => "remote1") - end - - it "fails", :bundler => "3" do - bundle :install - expect(err).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", :bundler => "< 3" do - let(:repo3_rack_version) { "1.2" } - - before do - gemfile <<-G - source "#{file_uri_for(gem_repo3)}" - source "#{file_uri_for(gem_repo1)}" - gem "rack-obama" - gem "rack", "1.0.0" # force it to install the working version in repo1 - G - - bundle :install - end - - it "warns about ambiguous gems, but installs anyway", :bundler => "2" do - expect(err).to include("Warning: the gem 'rack' was found in multiple sources.") - expect(err).to include("Installed from: #{file_uri_for(gem_repo1)}") - expect(the_bundle).to include_gems("rack-obama 1.0.0", "rack 1.0.0", :source => "remote1") - end - - it "fails", :bundler => "3" do - expect(err).to include("Each source after the first must include a block") - expect(exitstatus).to eq(4) if exitstatus - end - end - end + # it contains myrack-obama 1.0.0 and myrack 0.9.1 & 1.0.0 amongst other gems context "with source affinity" do context "with sources given by a block" do before do - # Oh no! Someone evil is trying to hijack rack :( + # Oh no! Someone evil is trying to hijack myrack :( # 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'" + build_repo3 do + build_gem "myrack", "1.0.0" do |s| + s.write "lib/myrack.rb", "MYRACK = 'FAIL'" end - build_gem "rack-obama" do |s| - s.add_dependency "rack" + build_gem "myrack-obama" do |s| + s.add_dependency "myrack" end end gemfile <<-G - source "#{file_uri_for(gem_repo3)}" - source "#{file_uri_for(gem_repo1)}" do + source "https://gem.repo3" + source "https://gem.repo1" do gem "thin" # comes first to test name sorting - gem "rack" + gem "myrack" end - gem "rack-obama" # shoud come from repo3! + gem "myrack-obama" # should 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") + bundle :install, artifice: "compact_index" + expect(err).not_to include("Warning") + expect(the_bundle).to include_gems("myrack-obama 1.0.0") + expect(the_bundle).to include_gems("myrack 1.0.0", source: "remote1") end it "can cache and deploy" do - bundle! :cache + bundle :cache, artifice: "compact_index" - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist - expect(bundled_app("vendor/cache/rack-obama-1.0.gem")).to exist + expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/myrack-obama-1.0.gem")).to exist - bundle! :install, forgotten_command_line_options(:deployment => true) + bundle_config "deployment true" + bundle :install, artifice: "compact_index" - expect(the_bundle).to include_gems("rack-obama 1.0.0", "rack 1.0.0") + expect(the_bundle).to include_gems("myrack-obama 1.0.0", "myrack 1.0.0") end end context "with sources set by an option" do before do - # Oh no! Someone evil is trying to hijack rack :( + # Oh no! Someone evil is trying to hijack myrack :( # 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'" + build_repo3 do + build_gem "myrack", "1.0.0" do |s| + s.write "lib/myrack.rb", "MYRACK = 'FAIL'" end - build_gem "rack-obama" do |s| - s.add_dependency "rack" + build_gem "myrack-obama" do |s| + s.add_dependency "myrack" end end - gemfile <<-G - source "#{file_uri_for(gem_repo3)}" - gem "rack-obama" # should come from repo3! - gem "rack", :source => "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo3" + gem "myrack-obama" # should come from repo3! + gem "myrack", :source => "https://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") + expect(err).not_to include("Warning") + expect(the_bundle).to include_gems("myrack-obama 1.0.0", "myrack 1.0.0") end end - context "when a pinned gem has an indirect dependency" do + context "when a pinned gem has an indirect dependency in the pinned source" do before do - build_repo gem_repo3 do - build_gem "depends_on_rack", "1.0.1" do |s| - s.add_dependency "rack" + build_repo3 do + build_gem "depends_on_myrack", "1.0.1" do |s| + s.add_dependency "myrack" 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_uri_for(gem_repo2)}" - source "#{file_uri_for(gem_repo3)}" do - gem "depends_on_rack" - end - G + # we need a working myrack gem in repo3 + update_repo gem_repo3 do + build_gem "myrack", "1.0.0" end - context "and not in any other sources" do - before do - build_repo(gem_repo2) {} + gemfile <<-G + source "https://gem.repo2" + source "https://gem.repo3" do + gem "depends_on_myrack" end + G + 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 + context "and not in any other sources" do + before do + build_repo(gem_repo2) {} 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 + it "installs from the same source without any warning" do + bundle :install, artifice: "compact_index" + expect(err).not_to include("Warning") + expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3") + 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 "myrack", "1.0.0" do |s| + s.write "lib/myrack.rb", "MYRACK = 'FAIL'" end end + end - context "when disable_multisource is set" do - before do - bundle! "config set disable_multisource true" - end + it "installs from the same source without any warning" do + bundle :install, artifice: "compact_index" - it "installs from the same source without any warning" do - bundle! :install + expect(err).not_to include("Warning: the gem 'myrack' was found in multiple sources.") + expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3") - expect(out).not_to include("Warning: the gem 'rack' was found in multiple sources.") - expect(err).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 lockfile, and the gems are missing, so try again + system_gems [] + bundle :install, artifice: "compact_index" - # when there is already a lock file, and the gems are missing, so try again - system_gems [] - bundle! :install + expect(err).not_to include("Warning: the gem 'myrack' was found in multiple sources.") + expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3") + end + end + end - expect(out).not_to include("Warning: the gem 'rack' was found in multiple sources.") - expect(err).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 + context "when a pinned gem has an indirect dependency in a different source" do + before do + # In these tests, we need a working myrack gem in repo2 and not repo3 + + build_repo3 do + build_gem "depends_on_myrack", "1.0.1" do |s| + s.add_dependency "myrack" end end + + build_repo gem_repo2 do + build_gem "myrack", "1.0.0" + end end - context "when the indirect dependency is in a different source" do + context "and not in any other sources" 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 + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo2" + source "https://gem.repo3" do + gem "depends_on_myrack" + end + G + end + + it "installs from the other source without any warning" do + expect(err).not_to include("Warning") + expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0") end + end + end + + context "when a top-level gem can only be found in an scoped source" do + before do + build_repo2 - context "and not in any other sources" do - before do - gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - source "#{file_uri_for(gem_repo3)}" do - gem "depends_on_rack" - end - G + build_repo3 do + build_gem "private_gem_1", "1.0.0" + build_gem "private_gem_2", "1.0.0" + end + + gemfile <<-G + source "https://gem.repo2" + + gem "private_gem_1" + + source "https://gem.repo3" do + gem "private_gem_2" 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") + it "fails" do + bundle :install, artifice: "compact_index", raise_on_error: false + expect(err).to include("Could not find gem 'private_gem_1' in rubygems repository https://gem.repo2/ or installed locally.") + end + end + + context "when a top-level gem has an indirect dependency" do + before do + build_repo gem_repo2 do + build_gem "depends_on_myrack", "1.0.1" do |s| + s.add_dependency "myrack" end end - context "and in yet another source", :bundler => "< 3" do - before do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - source "#{file_uri_for(gem_repo2)}" - source "#{file_uri_for(gem_repo3)}" do - gem "depends_on_rack" - end - G + build_repo3 do + build_gem "unrelated_gem", "1.0.0" + end - bundle :install - end + gemfile <<-G + source "https://gem.repo2" - it "installs from the other source and warns about ambiguous gems", :bundler => "2" do - expect(err).to include("Warning: the gem 'rack' was found in multiple sources.") - expect(err).to include("Installed from: #{file_uri_for(gem_repo2)}") - expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + gem "depends_on_myrack" + + source "https://gem.repo3" do + gem "unrelated_gem" end + G + end - it "fails", :bundler => "3" do - expect(err).to include("Each source after the first must include a block") - expect(exitstatus).to eq(4) if exitstatus + context "and the dependency is only in the top-level source" do + before do + update_repo gem_repo2 do + build_gem "myrack", "1.0.0" end end - context "and only the dependency is pinned", :bundler => "< 3" 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 + it "installs the dependency from the top-level source without warning" do + bundle :install, artifice: "compact_index" + expect(err).not_to include("Warning") + expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", "unrelated_gem 1.0.0") + expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote2") + expect(the_bundle).to include_gems("unrelated_gem 1.0.0", source: "remote3") + end + end + + context "and the dependency is only in a pinned source" do + before do + update_repo gem_repo3 do + build_gem "myrack", "1.0.0" do |s| + s.write "lib/myrack.rb", "MYRACK = 'FAIL'" end + end + end - gemfile <<-G - source "#{file_uri_for(gem_repo3)}" # contains depends_on_rack - source "#{file_uri_for(gem_repo2)}" # contains broken rack + it "does not find the dependency" do + bundle :install, artifice: "compact_index", raise_on_error: false + expect(err).to end_with <<~E.strip + Could not find compatible versions + + Because every version of depends_on_myrack depends on myrack >= 0 + and myrack >= 0 could not be found in rubygems repository https://gem.repo2/ or installed locally, + depends_on_myrack cannot be used. + So, because Gemfile depends on depends_on_myrack >= 0, + version solving has failed. + E + end + end - gem "depends_on_rack" # installed from gem_repo3 - gem "rack", :source => "#{file_uri_for(gem_repo1)}" - G + context "and the dependency is in both the top-level and a pinned source" do + before do + update_repo gem_repo2 do + build_gem "myrack", "1.0.0" end - it "installs the dependency from the pinned source without warning", :bundler => "2" do - bundle :install + update_repo gem_repo3 do + build_gem "myrack", "1.0.0" do |s| + s.write "lib/myrack.rb", "MYRACK = 'FAIL'" + end + end + end + + it "installs the dependency from the top-level source without warning" do + bundle :install, artifice: "compact_index" + expect(err).not_to include("Warning") + expect(run("require 'myrack'; puts MYRACK")).to eq("1.0.0") + expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", "unrelated_gem 1.0.0") + expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote2") + expect(the_bundle).to include_gems("unrelated_gem 1.0.0", source: "remote3") + end + end + end + + context "when a scoped gem has a deeply nested indirect dependency" do + before do + build_repo3 do + build_gem "depends_on_depends_on_myrack", "1.0.1" do |s| + s.add_dependency "depends_on_myrack" + end - expect(err).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") + build_gem "depends_on_myrack", "1.0.1" do |s| + s.add_dependency "myrack" + end + end - # 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 + gemfile <<-G + source "https://gem.repo2" - expect(err).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") + source "https://gem.repo3" do + gem "depends_on_depends_on_myrack" end + G + end - it "fails", :bundler => "3" do - bundle :install - expect(err).to include("Each source after the first must include a block") - expect(exitstatus).to eq(4) if exitstatus + context "and the dependency is only in the top-level source" do + before do + update_repo gem_repo2 do + build_gem "myrack", "1.0.0" end end + + it "installs the dependency from the top-level source" do + bundle :install, artifice: "compact_index" + expect(the_bundle).to include_gems("depends_on_depends_on_myrack 1.0.1", "depends_on_myrack 1.0.1", "myrack 1.0.0") + expect(the_bundle).to include_gems("myrack 1.0.0", source: "remote2") + expect(the_bundle).to include_gems("depends_on_depends_on_myrack 1.0.1", "depends_on_myrack 1.0.1", source: "remote3") + end end - end - context "when a top-level gem has an indirect dependency" do - context "when disable_multisource is set" do + context "and the dependency is only in a pinned source" do before do - bundle! "config set disable_multisource true" + build_repo2 + + update_repo gem_repo3 do + build_gem "myrack", "1.0.0" + end end + it "installs the dependency from the pinned source" do + bundle :install, artifice: "compact_index" + expect(the_bundle).to include_gems("depends_on_depends_on_myrack 1.0.1", "depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3") + end + end + + context "and the dependency is in both the top-level and a pinned source" do before do - build_repo gem_repo2 do - build_gem "depends_on_rack", "1.0.1" do |s| - s.add_dependency "rack" + update_repo gem_repo2 do + build_gem "myrack", "1.0.0" do |s| + s.write "lib/myrack.rb", "MYRACK = 'FAIL'" end end - build_repo gem_repo3 do - build_gem "unrelated_gem", "1.0.0" + update_repo gem_repo3 do + build_gem "myrack", "1.0.0" end + end - gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - - gem "depends_on_rack" + it "installs the dependency from the pinned source without warning" do + bundle :install, artifice: "compact_index" + expect(the_bundle).to include_gems("depends_on_depends_on_myrack 1.0.1", "depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3") + end + end + end - source "#{file_uri_for(gem_repo3)}" do - gem "unrelated_gem" - end - G + context "when a top-level gem has an indirect dependency present in the default source, but with a different version from the one resolved" do + before do + build_lib "activesupport", "7.0.0.alpha", path: lib_path("rails/activesupport") + build_lib "rails", "7.0.0.alpha", path: lib_path("rails") do |s| + s.add_dependency "activesupport", "= 7.0.0.alpha" end - context "and the dependency is only in the top-level source" do - before do - update_repo gem_repo2 do - build_gem "rack", "1.0.0" - end - end + build_repo gem_repo2 do + build_gem "activesupport", "6.1.2" - it "installs all gems without warning" do - bundle :install - expect(err).not_to include("Warning") - expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0", "unrelated_gem 1.0.0") + build_gem "webpacker", "5.2.1" do |s| + s.add_dependency "activesupport", ">= 5.2" end end - context "and the dependency is only in a pinned source" do - before do - update_repo gem_repo3 do - build_gem "rack", "1.0.0" do |s| - s.write "lib/rack.rb", "RACK = 'FAIL'" - end - end + gemfile <<-G + source "https://gem.repo2" + + gemspec :path => "#{lib_path("rails")}" + + gem "webpacker", "~> 5.0" + G + end + + it "installs all gems without warning" do + bundle :install, artifice: "compact_index" + expect(err).not_to include("Warning") + expect(the_bundle).to include_gems("activesupport 7.0.0.alpha", "rails 7.0.0.alpha") + expect(the_bundle).to include_gems("activesupport 7.0.0.alpha", source: "path@#{lib_path("rails/activesupport")}") + expect(the_bundle).to include_gems("rails 7.0.0.alpha", source: "path@#{lib_path("rails")}") + end + end + + context "when a pinned gem has an indirect dependency with more than one level of indirection in the default source " do + before do + build_repo3 do + build_gem "handsoap", "0.2.5.5" do |s| + s.add_dependency "nokogiri", ">= 1.2.3" end + end - it "does not find the dependency" do - bundle :install - expect(err).to include("Could not find gem 'rack', which is required by gem 'depends_on_rack', in any of the relevant sources") + update_repo gem_repo2 do + build_gem "nokogiri", "1.11.1" do |s| + s.add_dependency "racca", "~> 1.4" end + + build_gem "racca", "1.5.2" end - context "and the dependency is in both the top-level and a pinned source" do - before do - update_repo gem_repo2 do - build_gem "rack", "1.0.0" - end + gemfile <<-G + source "https://gem.repo2" - update_repo gem_repo3 do - build_gem "rack", "1.0.0" do |s| - s.write "lib/rack.rb", "RACK = 'FAIL'" - end - end + source "https://gem.repo3" do + gem "handsoap" end - it "installs the dependency from the top-level source without warning" do - bundle :install - expect(err).not_to include("Warning") - expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0", "unrelated_gem 1.0.0") - end + gem "nokogiri" + G + end + + it "installs from the default source without any warnings or errors and generates a proper lockfile" do + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo3, "handsoap", "0.2.5.5" + c.checksum gem_repo2, "nokogiri", "1.11.1" + c.checksum gem_repo2, "racca", "1.5.2" end + + expected_lockfile = <<~L + GEM + remote: https://gem.repo2/ + specs: + nokogiri (1.11.1) + racca (~> 1.4) + racca (1.5.2) + + GEM + remote: https://gem.repo3/ + specs: + handsoap (0.2.5.5) + nokogiri (>= 1.2.3) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + handsoap! + nokogiri + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install --verbose", artifice: "compact_index" + expect(err).not_to include("Warning") + expect(the_bundle).to include_gems("handsoap 0.2.5.5", "nokogiri 1.11.1", "racca 1.5.2") + expect(the_bundle).to include_gems("handsoap 0.2.5.5", source: "remote3") + expect(the_bundle).to include_gems("nokogiri 1.11.1", "racca 1.5.2", source: "remote2") + expect(lockfile).to eq(expected_lockfile) + + # Even if the gems are already installed + FileUtils.rm bundled_app_lock + bundle "install --verbose", artifice: "compact_index" + expect(err).not_to include("Warning") + expect(the_bundle).to include_gems("handsoap 0.2.5.5", "nokogiri 1.11.1", "racca 1.5.2") + expect(the_bundle).to include_gems("handsoap 0.2.5.5", source: "remote3") + expect(the_bundle).to include_gems("nokogiri 1.11.1", "racca 1.5.2", source: "remote2") + expect(lockfile).to eq(expected_lockfile) end end context "with a gem that is only found in the wrong source" do before do - build_repo gem_repo3 do + build_repo3 do build_gem "not_in_repo1", "1.0.0" end - gemfile <<-G - source "#{file_uri_for(gem_repo3)}" - gem "not_in_repo1", :source => "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G, artifice: "compact_index", raise_on_error: false + source "https://gem.repo3" + gem "not_in_repo1", :source => "https://gem.repo1" G end it "does not install the gem" do - bundle :install expect(err).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", :path => :bundle_path + system_gems "myrack-0.9.1", "myrack-1.0.0", path: default_bundle_path lockfile <<-L GEM - remote: #{file_uri_for(gem_repo1)} - remote: #{file_uri_for(gem_repo3)} + remote: https://gem.repo1 + specs: + + GEM + remote: https://gem.repo3 specs: - rack (0.9.1) + myrack (0.9.1) PLATFORMS - ruby + #{lockfile_platforms} DEPENDENCIES - rack! + myrack! L gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - source "#{file_uri_for(gem_repo3)}" do - gem 'rack' + source "https://gem.repo1" + source "https://gem.repo3" do + gem 'myrack' end G end - # Reproduction of https://github.com/bundler/bundler/issues/3298 + # Reproduction of https://github.com/rubygems/bundler/issues/3298 it "does not unlock the installed gem on exec" do - expect(the_bundle).to include_gems("rack 0.9.1") + expect(the_bundle).to include_gems("myrack 0.9.1") end end @@ -437,15 +518,16 @@ RSpec.describe "bundle install with gems on multiple sources" do build_lib "foo" gemfile <<-G - gem "rack", :source => "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" + gem "myrack", :source => "https://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 :install, artifice: "compact_index" - bundle! %(exec ruby -e 'puts "OK"') + bundle %(exec ruby -e 'puts "OK"') expect(out).to include("OK") end @@ -454,18 +536,17 @@ RSpec.describe "bundle install with gems on multiple sources" do context "when an older version of the same gem also ships with Ruby" do before do - system_gems "rack-0.9.1" + system_gems "myrack-0.9.1" - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" # shoud come from repo1! + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo1" + gem "myrack" # should come from repo1! G end it "installs the gems without any warning" do - bundle :install expect(err).not_to include("Warning") - expect(the_bundle).to include_gems("rack 1.0.0") + expect(the_bundle).to include_gems("myrack 1.0.0") end end @@ -479,16 +560,17 @@ RSpec.describe "bundle install with gems on multiple sources" do # Installing this gemfile... gemfile <<-G - source '#{file_uri_for(gem_repo1)}' - gem 'rack' - gem 'foo', '~> 0.1', :source => '#{file_uri_for(gem_repo4)}' - gem 'bar', '~> 0.1', :source => '#{file_uri_for(gem_repo4)}' + source 'https://gem.repo1' + gem 'myrack' + gem 'foo', '~> 0.1', :source => 'https://gem.repo4' + gem 'bar', '~> 0.1', :source => 'https://gem.repo4' G - bundle! :install, forgotten_command_line_options(:path => "../gems/system") + bundle_config "path ../gems/system" + bundle :install, artifice: "compact_index" # And then we add some new versions... - update_repo4 do + build_repo4 do build_gem "foo", "0.2" build_gem "bar", "0.3" end @@ -496,11 +578,11 @@ RSpec.describe "bundle install with gems on multiple sources" do it "allows them to be unlocked separately" do # And install this gemfile, updating only foo. - install_gemfile <<-G - source '#{file_uri_for(gem_repo1)}' - gem 'rack' - gem 'foo', '~> 0.2', :source => '#{file_uri_for(gem_repo4)}' - gem 'bar', '~> 0.1', :source => '#{file_uri_for(gem_repo4)}' + install_gemfile <<-G, artifice: "compact_index" + source 'https://gem.repo1' + gem 'myrack' + gem 'foo', '~> 0.2', :source => 'https://gem.repo4' + gem 'bar', '~> 0.1', :source => 'https://gem.repo4' G # It should update foo to 0.2, but not the (locked) bar 0.1 @@ -511,18 +593,21 @@ RSpec.describe "bundle install with gems on multiple sources" do context "re-resolving" do context "when there is a mix of sources in the gemfile" do before do - build_repo3 + build_repo3 do + build_gem "myrack" + end + build_lib "path1" build_lib "path2" build_git "git1" build_git "git2" - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo1" gem "rails" - source "#{file_uri_for(gem_repo3)}" do - gem "rack" + source "https://gem.repo3" do + gem "myrack" end gem "path1", :path => "#{lib_path("path1-1.0")}" @@ -533,7 +618,7 @@ RSpec.describe "bundle install with gems on multiple sources" do end it "does not re-resolve" do - bundle! :install, :verbose => true + bundle :install, artifice: "compact_index", verbose: true expect(out).to include("using resolution from the lockfile") expect(out).not_to include("re-resolving dependencies") end @@ -542,106 +627,687 @@ RSpec.describe "bundle install with gems on multiple sources" do context "when a gem is installed to system gems" do before do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo1" + gem "myrack" G end context "and the gemfile changes" do it "is still able to find that gem from remote sources" do - source_uri = file_uri_for(gem_repo1) - second_uri = file_uri_for(gem_repo4) - build_repo4 do - build_gem "rack", "2.0.1.1.forked" + build_gem "myrack", "2.0.1.1.forked" build_gem "thor", "0.19.1.1.forked" end # When this gemfile is installed... - install_gemfile <<-G - source "#{source_uri}" + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo1" - source "#{second_uri}" do - gem "rack", "2.0.1.1.forked" + source "https://gem.repo4" do + gem "myrack", "2.0.1.1.forked" gem "thor" end - gem "rack-obama" + gem "myrack-obama" G # Then we change the Gemfile by adding a version to thor gemfile <<-G - source "#{source_uri}" + source "https://gem.repo1" - source "#{second_uri}" do - gem "rack", "2.0.1.1.forked" + source "https://gem.repo4" do + gem "myrack", "2.0.1.1.forked" gem "thor", "0.19.1.1.forked" end - gem "rack-obama" + gem "myrack-obama" G - # But we should still be able to find rack 2.0.1.1.forked and install it - bundle! :install + # But we should still be able to find myrack 2.0.1.1.forked and install it + bundle :install, artifice: "compact_index" end end end describe "source changed to one containing a higher version of a dependency" do before do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo1" - gem "rack" + gem "myrack" G build_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + build_gem "bar" end - build_lib("gemspec_test", :path => tmp.join("gemspec_test")) do |s| + build_lib("gemspec_test", path: tmp("gemspec_test")) do |s| s.add_dependency "bar", "=1.0.0" end - install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack" - gemspec :path => "#{tmp.join("gemspec_test")}" + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo2" + gem "myrack" + gemspec :path => "#{tmp("gemspec_test")}" G end - it "keeps the old version", :bundler => "2" do - expect(the_bundle).to include_gems("rack 1.0.0") + it "conservatively installs the existing locked version" do + expect(the_bundle).to include_gems("myrack 1.0.0") end + end - it "installs the higher version in the new repo", :bundler => "3" do - expect(the_bundle).to include_gems("rack 1.2") + context "when Gemfile overrides a gemspec development dependency to change the default source" do + before do + build_repo4 do + build_gem "bar" + end + + build_lib("gemspec_test", path: tmp("gemspec_test")) do |s| + s.add_development_dependency "bar" + end + + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo1" + + source "https://gem.repo4" do + gem "bar" + end + + gemspec :path => "#{tmp("gemspec_test")}" + G + end + + it "does not print warnings" do + expect(err).to be_empty end end - context "when a gem is available from multiple ambiguous sources", :bundler => "3" do + it "doesn't update version when a gem uses a source block but a higher version from another source is already installed locally" do + build_repo2 do + build_gem "example", "0.1.0" + end + + build_repo4 do + build_gem "example", "1.0.2" + end + + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo4" + + gem "example", :source => "https://gem.repo2" + G + + bundle "info example" + expect(out).to include("example (0.1.0)") + + system_gems "example-1.0.2", path: default_bundle_path, gem_repo: gem_repo4 + + bundle "update example --verbose", artifice: "compact_index" + expect(out).not_to include("Using example 1.0.2") + expect(out).to include("Using example 0.1.0") + end + + it "fails immediately with a helpful error when a rubygems source does not exist and bundler/setup is required" do + gemfile <<-G + source "https://gem.repo1" + + source "https://gem.repo4" do + gem "example" + end + G + + ruby <<~R, raise_on_error: false + require 'bundler/setup' + R + + expect(last_command).to be_failure + expect(err).to include("Could not find gem 'example' in locally installed gems.") + end + + it "fails immediately with a helpful error when a non retriable network error happens while resolving sources" do + gemfile <<-G + source "https://gem.repo1" + + source "https://gem.repo4" do + gem "example" + end + G + + bundle "install", artifice: nil, raise_on_error: false + + expect(last_command).to be_failure + expect(err).to include("Could not reach host gem.repo4. Check your network connection and try again.") + end + + context "when an indirect dependency is available from multiple ambiguous sources" do it "raises, suggesting a source block" do build_repo4 do - build_gem "depends_on_rack" do |s| - s.add_dependency "rack" + build_gem "depends_on_myrack" do |s| + s.add_dependency "myrack" end - build_gem "rack" + build_gem "myrack" end - install_gemfile <<-G - source "#{file_uri_for(gem_repo4)}" - source "#{file_uri_for(gem_repo1)}" do + install_gemfile <<-G, artifice: "compact_index_extra_api", raise_on_error: false + source "https://global.source" + + source "https://scoped.source/extra" do + gem "depends_on_myrack" + end + + source "https://scoped.source" do gem "thin" end - gem "depends_on_rack" G expect(last_command).to be_failure - expect(err).to eq strip_whitespace(<<-EOS).strip - The gem 'rack' was found in multiple relevant sources. - * rubygems repository #{file_uri_for(gem_repo1)}/ or installed locally - * rubygems repository #{file_uri_for(gem_repo4)}/ or installed locally + expect(err).to eq <<~EOS.strip + The gem 'myrack' was found in multiple relevant sources. + * rubygems repository https://scoped.source/ + * rubygems repository https://scoped.source/extra/ You must add this gem to the source block for the source you wish it to be installed from. EOS expect(the_bundle).not_to be_locked end end + + context "when default source includes old gems with nil required_ruby_version" do + before do + build_repo2 do + build_gem "ruport", "1.7.0.3" do |s| + s.add_dependency "pdf-writer", "1.1.8" + end + end + + build_repo gem_repo4 do + build_gem "pdf-writer", "1.1.8" + end + + path = "#{gem_repo4}/#{Gem::MARSHAL_SPEC_DIR}/pdf-writer-1.1.8.gemspec.rz" + spec = Marshal.load(Bundler.rubygems.inflate(File.binread(path))) + spec.instance_variable_set(:@required_ruby_version, nil) + File.open(path, "wb") do |f| + f.write Gem.deflate(Marshal.dump(spec)) + end + + gemfile <<~G + source "https://gem.repo4" + + gem "ruport", "= 1.7.0.3", :source => "https://gem.repo4/extra" + G + end + + it "handles that fine" do + bundle "install", artifice: "compact_index_extra" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "pdf-writer", "1.1.8" + c.checksum gem_repo2, "ruport", "1.7.0.3" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + pdf-writer (1.1.8) + + GEM + remote: https://gem.repo4/extra/ + specs: + ruport (1.7.0.3) + pdf-writer (= 1.1.8) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ruport (= 1.7.0.3)! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when default source includes old gems with nil required_rubygems_version" do + before do + build_repo2 do + build_gem "ruport", "1.7.0.3" do |s| + s.add_dependency "pdf-writer", "1.1.8" + end + end + + build_repo gem_repo4 do + build_gem "pdf-writer", "1.1.8" + end + + path = "#{gem_repo4}/#{Gem::MARSHAL_SPEC_DIR}/pdf-writer-1.1.8.gemspec.rz" + spec = Marshal.load(Bundler.rubygems.inflate(File.binread(path))) + spec.instance_variable_set(:@required_rubygems_version, nil) + File.open(path, "wb") do |f| + f.write Gem.deflate(Marshal.dump(spec)) + end + + gemfile <<~G + source "https://gem.repo4" + + gem "ruport", "= 1.7.0.3", :source => "https://gem.repo4/extra" + G + end + + it "handles that fine" do + bundle "install", artifice: "compact_index_extra" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "pdf-writer", "1.1.8" + c.checksum gem_repo2, "ruport", "1.7.0.3" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + pdf-writer (1.1.8) + + GEM + remote: https://gem.repo4/extra/ + specs: + ruport (1.7.0.3) + pdf-writer (= 1.1.8) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ruport (= 1.7.0.3)! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when default source uses the old API and includes old gems with nil required_rubygems_version" do + before do + build_repo4 do + build_gem "pdf-writer", "1.1.8" + end + + path = "#{gem_repo4}/#{Gem::MARSHAL_SPEC_DIR}/pdf-writer-1.1.8.gemspec.rz" + spec = Marshal.load(Bundler.rubygems.inflate(File.binread(path))) + spec.instance_variable_set(:@required_rubygems_version, nil) + File.open(path, "wb") do |f| + f.write Gem.deflate(Marshal.dump(spec)) + end + + gemfile <<~G + source "https://gem.repo4" + + gem "pdf-writer", "= 1.1.8" + G + end + + it "handles that fine" do + bundle "install --verbose", artifice: "endpoint" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "pdf-writer", "1.1.8" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + pdf-writer (1.1.8) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + pdf-writer (= 1.1.8) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when mistakenly adding a top level gem already depended on and cached under the wrong source" do + before do + build_repo4 do + build_gem "some_private_gem", "0.1.0" do |s| + s.add_dependency "example", "~> 1.0" + end + end + + build_repo2 do + build_gem "example", "1.0.0" + end + + install_gemfile <<~G, artifice: "compact_index" + source "https://gem.repo2" + + source "https://gem.repo4" do + gem "some_private_gem" + end + G + + gemfile <<~G + source "https://gem.repo2" + + source "https://gem.repo4" do + gem "some_private_gem" + gem "example" # MISTAKE, example is not available at gem.repo4 + end + G + end + + it "shows a proper error message and does not generate a corrupted lockfile" do + expect do + bundle :install, artifice: "compact_index", raise_on_error: false, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + end.not_to change { lockfile } + + expect(err).to include("Could not find gem 'example' in rubygems repository https://gem.repo4/") + end + end + + context "when a gem has versions in two sources, but only the locked one has updates" do + let(:original_lockfile) do + <<~L + GEM + remote: https://main.source/ + specs: + activesupport (1.0) + bigdecimal + bigdecimal (1.0.0) + + GEM + remote: https://main.source/extra/ + specs: + foo (1.0) + bigdecimal + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + activesupport + foo! + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + before do + build_repo3 do + build_gem "activesupport" do |s| + s.add_dependency "bigdecimal" + end + + build_gem "bigdecimal", "1.0.0" + build_gem "bigdecimal", "3.3.1" + end + + build_repo4 do + build_gem "foo" do |s| + s.add_dependency "bigdecimal" + end + + build_gem "bigdecimal", "1.0.0" + end + + gemfile <<~G + source "https://main.source" + + gem "activesupport" + + source "https://main.source/extra" do + gem "foo" + end + G + + lockfile original_lockfile + end + + it "properly upgrades the lockfile when updating that specific gem" do + bundle "update bigdecimal --conservative", artifice: "compact_index_extra_api", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo3.to_s } + + expect(lockfile).to eq original_lockfile.gsub("bigdecimal (1.0.0)", "bigdecimal (3.3.1)") + end + end + + context "when switching a gem with components from rubygems to git source" do + before do + build_repo2 do + build_gem "rails", "7.0.0" do |s| + s.add_dependency "actionpack", "7.0.0" + s.add_dependency "activerecord", "7.0.0" + end + build_gem "actionpack", "7.0.0" + build_gem "activerecord", "7.0.0" + # propshaft also depends on actionpack, creating the conflict + build_gem "propshaft", "1.0.0" do |s| + s.add_dependency "actionpack", ">= 7.0.0" + end + end + + build_git "rails", "7.0.0", path: lib_path("rails") do |s| + s.add_dependency "actionpack", "7.0.0" + s.add_dependency "activerecord", "7.0.0" + end + + build_git "actionpack", "7.0.0", path: lib_path("rails") + build_git "activerecord", "7.0.0", path: lib_path("rails") + + install_gemfile <<-G + source "https://gem.repo2" + gem "rails", "7.0.0" + gem "propshaft" + G + end + + it "moves component gems to the git source in the lockfile" do + expect(lockfile).to include("remote: https://gem.repo2") + expect(lockfile).to include("rails (7.0.0)") + expect(lockfile).to include("actionpack (7.0.0)") + expect(lockfile).to include("activerecord (7.0.0)") + expect(lockfile).to include("propshaft (1.0.0)") + + gemfile <<-G + source "https://gem.repo2" + gem "rails", git: "#{lib_path("rails")}" + gem "propshaft" + G + + bundle "install" + + expect(lockfile).to include("remote: #{lib_path("rails")}") + expect(lockfile).to include("rails (7.0.0)") + expect(lockfile).to include("actionpack (7.0.0)") + expect(lockfile).to include("activerecord (7.0.0)") + + # Component gems should NOT remain in the GEM section + # Extract just the GEM section by splitting on GIT first, then GEM + gem_section = lockfile.split("GEM\n").last.split(/\n(PLATFORMS|DEPENDENCIES)/)[0] + expect(gem_section).not_to include("actionpack (7.0.0)") + expect(gem_section).not_to include("activerecord (7.0.0)") + end + end + + context "when switching a gem with components from rubygems to path source" do + before do + build_repo2 do + build_gem "rails", "7.0.0" do |s| + s.add_dependency "actionpack", "7.0.0" + s.add_dependency "activerecord", "7.0.0" + end + build_gem "actionpack", "7.0.0" + build_gem "activerecord", "7.0.0" + # propshaft also depends on actionpack, creating the conflict + build_gem "propshaft", "1.0.0" do |s| + s.add_dependency "actionpack", ">= 7.0.0" + end + end + + build_lib "rails", "7.0.0", path: lib_path("rails") do |s| + s.add_dependency "actionpack", "7.0.0" + s.add_dependency "activerecord", "7.0.0" + end + + build_lib "actionpack", "7.0.0", path: lib_path("rails") + build_lib "activerecord", "7.0.0", path: lib_path("rails") + + install_gemfile <<-G + source "https://gem.repo2" + gem "rails", "7.0.0" + gem "propshaft" + G + end + + it "moves component gems to the path source in the lockfile" do + expect(lockfile).to include("remote: https://gem.repo2") + expect(lockfile).to include("rails (7.0.0)") + expect(lockfile).to include("actionpack (7.0.0)") + expect(lockfile).to include("activerecord (7.0.0)") + expect(lockfile).to include("propshaft (1.0.0)") + + gemfile <<-G + source "https://gem.repo2" + gem "rails", path: "#{lib_path("rails")}" + gem "propshaft" + G + + bundle "install" + + expect(lockfile).to include("remote: #{lib_path("rails")}") + expect(lockfile).to include("rails (7.0.0)") + expect(lockfile).to include("actionpack (7.0.0)") + expect(lockfile).to include("activerecord (7.0.0)") + + # Component gems should NOT remain in the GEM section + # Extract just the GEM section by splitting appropriately + gem_section = lockfile.split("GEM\n").last.split(/\n(PLATFORMS|DEPENDENCIES)/)[0] + expect(gem_section).not_to include("actionpack (7.0.0)") + expect(gem_section).not_to include("activerecord (7.0.0)") + end + end + + context "when a scoped rubygems source is missing a transitive dependency" do + before do + build_repo2 do + build_gem "fallback_dep", "1.0.0" + build_gem "foo", "1.0.0" + end + + build_repo3 do + build_gem "private_parent", "1.0.0" do |s| + s.add_dependency "fallback_dep" + end + end + + gemfile <<-G + source "https://gem.repo2" + + gem "foo" + + source "https://gem.repo3" do + gem "private_parent", "1.0.0" + end + G + + bundle :install, artifice: "compact_index" + end + + it "falls back to the default rubygems source for that dependency" do + build_repo2 do + build_gem "foo", "2.0.0" + end + + system_gems [] + + bundle "update foo", artifice: "compact_index" + + expect(the_bundle).to include_gems("private_parent 1.0.0", "fallback_dep 1.0.0", "foo 2.0.0") + expect(the_bundle).to include_gems("private_parent 1.0.0", source: "remote3") + expect(the_bundle).to include_gems("fallback_dep 1.0.0", source: "remote2") + end + end + + context "when a path gem has a transitive dependency that does not exist in the path source" do + before do + build_repo2 do + build_gem "missing_dep", "1.0.0" + build_gem "foo", "1.0.0" + end + + build_lib "parent_gem", "1.0.0", path: lib_path("parent_gem") do |s| + s.add_dependency "missing_dep" + end + + gemfile <<-G + source "https://gem.repo2" + + gem "foo" + + gem "parent_gem", path: "#{lib_path("parent_gem")}" + G + + bundle :install, artifice: "compact_index" + end + + it "falls back to the default rubygems source for that dependency when updating" do + build_repo2 do + build_gem "foo", "2.0.0" + end + + system_gems [] + + bundle "update foo", artifice: "compact_index" + + expect(the_bundle).to include_gems("parent_gem 1.0.0", "missing_dep 1.0.0", "foo 2.0.0") + expect(the_bundle).to include_gems("parent_gem 1.0.0", source: "path@#{lib_path("parent_gem")}") + expect(the_bundle).to include_gems("missing_dep 1.0.0", source: "remote2") + end + end + + context "when a git gem has a transitive dependency that does not exist in the git source" do + before do + build_repo2 do + build_gem "missing_dep", "1.0.0" + build_gem "foo", "1.0.0" + end + + build_git "parent_gem", "1.0.0", path: lib_path("parent_gem") do |s| + s.add_dependency "missing_dep" + end + + gemfile <<-G + source "https://gem.repo2" + + gem "foo" + + gem "parent_gem", git: "#{lib_path("parent_gem")}" + G + + bundle :install, artifice: "compact_index" + end + + it "falls back to the default rubygems source for that dependency when updating" do + build_repo2 do + build_gem "foo", "2.0.0" + end + + system_gems [] + + bundle "update foo", artifice: "compact_index" + + expect(the_bundle).to include_gems("parent_gem 1.0.0", "missing_dep 1.0.0", "foo 2.0.0") + expect(the_bundle).to include_gems("parent_gem 1.0.0", source: "git@#{lib_path("parent_gem")}") + expect(the_bundle).to include_gems("missing_dep 1.0.0", source: "remote2") + end + end end diff --git a/spec/bundler/install/gemfile/specific_platform_spec.rb b/spec/bundler/install/gemfile/specific_platform_spec.rb index 24b602589f..97b1d233bf 100644 --- a/spec/bundler/install/gemfile/specific_platform_spec.rb +++ b/spec/bundler/install/gemfile/specific_platform_spec.rb @@ -1,113 +1,1972 @@ # frozen_string_literal: true -RSpec.describe "bundle install with specific_platform enabled" do - before do - bundle "config set 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" } +RSpec.describe "bundle install with specific platforms" do + let(:google_protobuf) { <<-G } + source "https://gem.repo2" + gem "google-protobuf" + G - 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") + it "locks to the specific darwin platform" do + simulate_platform "x86_64-darwin-15" do + setup_multiplatform_gem + install_gemfile(google_protobuf) + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + expect(the_bundle.locked_platforms).to include("universal-darwin") + 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 include( + "google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin" + ) + end + end - 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" } + it "still installs the platform specific variant when locked only to ruby, and the platform specific variant has different dependencies" do + simulate_platform "x86_64-darwin-15" do + build_repo4 do + build_gem("sass-embedded", "1.72.0") do |s| + s.add_dependency "rake" + end - 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("sass-embedded", "1.72.0") do |s| + s.platform = "x86_64-darwin-15" + end - build_gem("facter", "2.4.6") - build_gem("facter", "2.4.6") do |s| - s.platform = "universal-darwin" - s.add_runtime_dependency "CFPropertyList" + build_gem "rake" end - build_gem("CFPropertyList") + + gemfile <<~G + source "https://gem.repo4" + + gem "sass-embedded" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + rake (1.0) + sass-embedded (1.72.0) + rake + + PLATFORMS + ruby + + DEPENDENCIES + sass-embedded + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install --verbose" + expect(err).to include("The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version") + expect(out).to include("Installing sass-embedded 1.72.0 (x86_64-darwin-15)") + + expect(the_bundle).to include_gem("sass-embedded 1.72.0 x86_64-darwin-15") end end - let(:google_protobuf) { <<-G } - source "#{file_uri_for(gem_repo2)}" - gem "google-protobuf" - G + it "understands that a non-platform specific gem in a old lockfile doesn't necessarily mean installing the non-specific variant" do + simulate_platform "x86_64-darwin-15" do + setup_multiplatform_gem + + # Consistent location to install and look for gems + bundle_config "path vendor/bundle" + + install_gemfile(google_protobuf) + + # simulate lockfile created with old bundler, which only locks for ruby platform + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + google-protobuf (3.0.0.alpha.5.0.5.1) - context "when on a darwin machine" do - before { simulate_platform "x86_64-darwin-15" } + PLATFORMS + ruby - 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")]) + DEPENDENCIES + google-protobuf + + BUNDLED WITH + #{Bundler::VERSION} + L + + # force strict usage of the lockfile by setting frozen mode + bundle_config "frozen true" + + # make sure the platform that got actually installed with the old bundler is used 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 + end + + it "understands that a non-platform specific gem in a new lockfile locked only to ruby doesn't necessarily mean installing the non-specific variant" do + simulate_platform "x86_64-darwin-15" do + setup_multiplatform_gem + + # Consistent location to install and look for gems + bundle_config "path vendor/bundle" + + gemfile google_protobuf + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "google-protobuf", "3.0.0.alpha.4.0" + end + + # simulate lockfile created with old bundler, which only locks for ruby platform + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + google-protobuf (3.0.0.alpha.4.0) + + PLATFORMS + ruby + + DEPENDENCIES + google-protobuf + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update" + expect(err).to include("The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version") + + checksums.checksum gem_repo2, "google-protobuf", "3.0.0.alpha.5.0.5.1" + + # make sure the platform that the platform specific dependency is used, since we're only locked to ruby + expect(the_bundle).to include_gem("google-protobuf 3.0.0.alpha.5.0.5.1 universal-darwin") + + # make sure we're still only locked to ruby + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo2/ + specs: + google-protobuf (3.0.0.alpha.5.0.5.1) + + PLATFORMS + ruby + + DEPENDENCIES + google-protobuf + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when running on a legacy lockfile locked only to ruby" do + # Exercises the legacy lockfile path (use_exact_resolved_specifications? = false) + # because most_specific_locked_platform is ruby, matching the generic platform. + # Key insight: when target (arm64-darwin-22) != platform (ruby), the code tries + # both platforms before falling back, preserving lockfile integrity. + + around do |example| + build_repo4 do + build_gem "nokogiri", "1.3.10" + build_gem "nokogiri", "1.3.10" do |s| + s.platform = "arm64-darwin" + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + G + + lockfile <<-L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.3.10) + + PLATFORMS + ruby + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "arm64-darwin-22", &example + end + + it "still installs the generic ruby variant if necessary" do + bundle "install" + expect(the_bundle).to include_gem("nokogiri 1.3.10") + expect(the_bundle).not_to include_gem("nokogiri 1.3.10 arm64-darwin") + end + + it "still installs the generic ruby variant if necessary, even in frozen mode" do + bundle "install", env: { "BUNDLE_FROZEN" => "true" } + expect(the_bundle).to include_gem("nokogiri 1.3.10") + expect(the_bundle).not_to include_gem("nokogiri 1.3.10 arm64-darwin") + end + end + + context "when platform-specific gem has incompatible required_ruby_version" do + # Key insight: candidate_platforms tries [target, platform, ruby] in order. + # Ruby platform is last since it requires compilation, but works when + # precompiled gems are incompatible with the current Ruby version. + # + # Note: This fix requires the lockfile to include both ruby and platform- + # specific variants (typical after `bundle lock --add-platform`). If the + # lockfile only has platform-specific gems, frozen mode cannot help because + # Bundler.setup would still expect the locked (incompatible) gem. + + # Exercises the exact spec path (use_exact_resolved_specifications? = true) + # because lockfile has platform-specific entry as most_specific_locked_platform + it "falls back to ruby platform in frozen mode when lockfile includes both variants" do + build_repo4 do + build_gem "nokogiri", "1.18.10" + build_gem "nokogiri", "1.18.10" do |s| + s.platform = "x86_64-linux" + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + G + + # Lockfile has both ruby and platform-specific gem (typical after `bundle lock --add-platform`) + lockfile <<-L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.18.10) + nokogiri (1.18.10-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle "install", env: { "BUNDLE_FROZEN" => "true" } + expect(the_bundle).to include_gem("nokogiri 1.18.10") + expect(the_bundle).not_to include_gem("nokogiri 1.18.10 x86_64-linux") + end + end + end + + it "doesn't discard previously installed platform specific gem and fall back to ruby on subsequent bundles" do + simulate_platform "x86_64-darwin-15" do + build_repo2 do + build_gem("libv8", "8.4.255.0") + build_gem("libv8", "8.4.255.0") {|s| s.platform = "universal-darwin" } + + build_gem("mini_racer", "1.0.0") do |s| + s.add_dependency "libv8" + end + end + + # Consistent location to install and look for gems + bundle_config "path vendor/bundle" + + gemfile <<-G + source "https://gem.repo2" + gem "libv8" + G + + # simulate lockfile created with old bundler, which only locks for ruby platform + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + libv8 (8.4.255.0) + + PLATFORMS + ruby + + DEPENDENCIES + libv8 + + BUNDLED WITH + #{Bundler::VERSION} + L - it "caches both the universal-darwin and ruby gems when --all-platforms is passed" do + bundle "install --verbose" + expect(err).to include("The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version") + expect(out).to include("Installing libv8 8.4.255.0 (universal-darwin)") + + bundle "add mini_racer --verbose" + expect(out).to include("Using libv8 8.4.255.0 (universal-darwin)") + end + end + + it "chooses platform specific gems even when resolving upon materialization and the API returns more specific platforms first" do + simulate_platform "x86_64-darwin-15" do + build_repo4 do + build_gem("grpc", "1.50.0") + build_gem("grpc", "1.50.0") {|s| s.platform = "universal-darwin" } + end + + gemfile <<-G + source "https://gem.repo4" + gem "grpc" + G + + # simulate lockfile created with old bundler, which only locks for ruby platform + lockfile <<-L + GEM + remote: https://gem.repo4/ + specs: + grpc (1.50.0) + + PLATFORMS + ruby + + DEPENDENCIES + grpc + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install --verbose", artifice: "compact_index_precompiled_before" + expect(err).to include("The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version") + expect(out).to include("Installing grpc 1.50.0 (universal-darwin)") + end + end + + it "caches the universal-darwin gem when --all-platforms is passed and properly picks it up on further bundler invocations" do + simulate_platform "x86_64-darwin-15" do + setup_multiplatform_gem 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) + bundle "cache --all-platforms" + expect(cached_gem("google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin")).to exist + + bundle "install --verbose" + expect(err).to be_empty end + end + + it "caches the universal-darwin gem when cache_all_platforms is configured and properly picks it up on further bundler invocations" do + simulate_platform "x86_64-darwin-15" do + setup_multiplatform_gem + gemfile(google_protobuf) + bundle_config "cache_all_platforms true" + bundle "cache" + expect(cached_gem("google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin")).to exist - it "uses the platform-specific gem with extra dependencies" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo2)}" + bundle "install --verbose" + expect(err).to be_empty + end + end + + it "caches multiplatform git gems with a single gemspec when --all-platforms is passed" do + git = build_git "pg_array_parser", "1.0" + + gemfile <<-G + source "https://gem.repo1" + gem "pg_array_parser", :git => "#{lib_path("pg_array_parser-1.0")}" + G + + lockfile <<-L + GIT + remote: #{lib_path("pg_array_parser-1.0")} + revision: #{git.ref_for("main")} + specs: + pg_array_parser (1.0-java) + pg_array_parser (1.0) + + GEM + specs: + + PLATFORMS + #{lockfile_platforms("java")} + + DEPENDENCIES + pg_array_parser! + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "cache --all-platforms" + + expect(err).to be_empty + end + + it "uses the platform-specific gem with extra dependencies" do + simulate_platform "x86_64-darwin-15" do + setup_multiplatform_gem_with_different_dependencies_per_platform + install_gemfile <<-G + source "https://gem.repo2" gem "facter" G + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) - expect(the_bundle.locked_gems.platforms).to eq([pl("ruby"), pl("x86_64-darwin-15")]) + expect(the_bundle.locked_platforms).to include("universal-darwin") 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"]) + expect(the_bundle.locked_gems.specs.map(&:full_name)).to include("CFPropertyList-1.0", + "facter-2.4.6-universal-darwin") end + 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}" + context "when adding a platform via lock --add_platform" do + before do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + end - 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 + it "adds the foreign platform" do + simulate_platform "x86_64-darwin-15" do + setup_multiplatform_gem + install_gemfile(google_protobuf) + bundle "lock --add-platform=x64-mingw-ucrt" + + expect(the_bundle.locked_platforms).to include("x64-mingw-ucrt", "universal-darwin") + expect(the_bundle.locked_gems.specs.map(&:full_name)).to include(*%w[ google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin - google-protobuf-3.0.0.alpha.5.0.5.1-x64-mingw32 + google-protobuf-3.0.0.alpha.5.0.5.1-x64-mingw-ucrt ]) end + 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}" + it "falls back on plain ruby when that version doesn't have a platform-specific gem" do + simulate_platform "x86_64-darwin-15" do + setup_multiplatform_gem + 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 - ]) + expect(the_bundle.locked_platforms).to include("java", "universal-darwin") + expect(the_bundle.locked_gems.specs.map(&:full_name)).to include( + "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 + + it "installs sorbet-static, which does not provide a pure ruby variant, in absence of a lockfile, just fine", :truffleruby do + skip "does not apply to Windows" if Gem.win_platform? + + build_repo2 do + build_gem("sorbet-static", "0.5.6403") {|s| s.platform = Bundler.local_platform } + end + + gemfile <<~G + source "https://gem.repo2" + + gem "sorbet-static", "0.5.6403" + G + + bundle "install --verbose" + end + + it "installs sorbet-static, which does not provide a pure ruby variant, in presence of a lockfile, just fine", :truffleruby do + skip "does not apply to Windows" if Gem.win_platform? + + build_repo2 do + build_gem("sorbet-static", "0.5.6403") {|s| s.platform = Bundler.local_platform } + end + + gemfile <<~G + source "https://gem.repo2" + + gem "sorbet-static", "0.5.6403" + G + + lockfile <<~L + GEM + remote: https://gem.repo2/ + specs: + sorbet-static (0.5.6403-#{Bundler.local_platform}) + + PLATFORMS + ruby + + DEPENDENCIES + sorbet-static (= 0.5.6403) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install --verbose" + end + + it "does not resolve if the current platform does not match any of available platform specific variants for a top level dependency" do + build_repo4 do + build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "x86_64-linux" } + build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "universal-darwin-20" } + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet-static", "0.5.6433" + G + + error_message = <<~ERROR.strip + Could not find gem 'sorbet-static (= 0.5.6433)' with platform 'arm64-darwin-21' in rubygems repository https://gem.repo4/ or installed locally. + + The source contains the following gems matching 'sorbet-static (= 0.5.6433)': + * sorbet-static-0.5.6433-universal-darwin-20 + * sorbet-static-0.5.6433-x86_64-linux + ERROR + + simulate_platform "arm64-darwin-21" do + bundle "lock", raise_on_error: false + end + + expect(err).to include(error_message).once + + # Make sure it doesn't print error twice in verbose mode + + simulate_platform "arm64-darwin-21" do + bundle "lock --verbose", raise_on_error: false + end + + expect(err).to include(error_message).once + end + + it "shows a platform mismatch hint when the current platform is not in the lockfile's platforms" do + build_repo4 do + build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "x86_64-linux-musl" } + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet-static", "0.5.6433" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet-static (0.5.6433-x86_64-linux-musl) + + PLATFORMS + x86_64-linux-musl + + DEPENDENCIES + sorbet-static (= 0.5.6433) + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle "install", raise_on_error: false + end + + expect(err).to include("Your current platform (x86_64-linux) is not included in the lockfile's platforms (x86_64-linux-musl)") + expect(err).to include("bundle lock --add-platform x86_64-linux") + end + + it "does not resolve if the current platform does not match any of available platform specific variants for a transitive dependency" do + build_repo4 do + build_gem("sorbet", "0.5.6433") {|s| s.add_dependency "sorbet-static", "= 0.5.6433" } + build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "x86_64-linux" } + build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "universal-darwin-20" } + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet", "0.5.6433" + G + + error_message = <<~ERROR.strip + Could not find compatible versions + + Because every version of sorbet depends on sorbet-static = 0.5.6433 + and sorbet-static = 0.5.6433 could not be found in rubygems repository https://gem.repo4/ or installed locally for any resolution platforms (arm64-darwin-21), + sorbet cannot be used. + So, because Gemfile depends on sorbet = 0.5.6433, + version solving has failed. + + The source contains the following gems matching 'sorbet-static (= 0.5.6433)': + * sorbet-static-0.5.6433-universal-darwin-20 + * sorbet-static-0.5.6433-x86_64-linux + ERROR + + simulate_platform "arm64-darwin-21" do + bundle "lock", raise_on_error: false + end + + expect(err).to include(error_message).once + + # Make sure it doesn't print error twice in verbose mode + + simulate_platform "arm64-darwin-21" do + bundle "lock --verbose", raise_on_error: false + end + + expect(err).to include(error_message).once + end + + it "does not generate a lockfile if ruby platform is forced and some gem has no ruby variant available" do + build_repo4 do + build_gem("sorbet-static", "0.5.9889") {|s| s.platform = Gem::Platform.local } + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet-static", "0.5.9889" + G + + bundle "lock", raise_on_error: false, env: { "BUNDLE_FORCE_RUBY_PLATFORM" => "true" } + + expect(err).to include <<~ERROR.rstrip + Could not find gem 'sorbet-static (= 0.5.9889)' with platform 'ruby' in rubygems repository https://gem.repo4/ or installed locally. + + The source contains the following gems matching 'sorbet-static (= 0.5.9889)': + * sorbet-static-0.5.9889-#{Gem::Platform.local} + ERROR + end + + it "automatically fixes the lockfile if ruby platform is locked and some gem has no ruby variant available" do + build_repo4 do + build_gem("sorbet-static-and-runtime", "0.5.10160") do |s| + s.add_dependency "sorbet", "= 0.5.10160" + s.add_dependency "sorbet-runtime", "= 0.5.10160" + end + + build_gem("sorbet", "0.5.10160") do |s| + s.add_dependency "sorbet-static", "= 0.5.10160" + end + + build_gem("sorbet-runtime", "0.5.10160") + + build_gem("sorbet-static", "0.5.10160") do |s| + s.platform = Gem::Platform.local + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet-static-and-runtime" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet (0.5.10160) + sorbet-static (= 0.5.10160) + sorbet-runtime (0.5.10160) + sorbet-static (0.5.10160-#{Gem::Platform.local}) + sorbet-static-and-runtime (0.5.10160) + sorbet (= 0.5.10160) + sorbet-runtime (= 0.5.10160) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + sorbet-static-and-runtime + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "sorbet", "0.5.10160" + c.checksum gem_repo4, "sorbet-runtime", "0.5.10160" + c.checksum gem_repo4, "sorbet-static", "0.5.10160", Gem::Platform.local + c.checksum gem_repo4, "sorbet-static-and-runtime", "0.5.10160" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet (0.5.10160) + sorbet-static (= 0.5.10160) + sorbet-runtime (0.5.10160) + sorbet-static (0.5.10160-#{Gem::Platform.local}) + sorbet-static-and-runtime (0.5.10160) + sorbet (= 0.5.10160) + sorbet-runtime (= 0.5.10160) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + sorbet-static-and-runtime + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "automatically fixes the lockfile if both ruby platform and a more specific platform are locked, and some gem has no ruby variant available" do + build_repo4 do + build_gem "nokogiri", "1.12.0" + build_gem "nokogiri", "1.12.0" do |s| + s.platform = "x86_64-darwin" + end + + build_gem "nokogiri", "1.13.0" + build_gem "nokogiri", "1.13.0" do |s| + s.platform = "x86_64-darwin" + end + + build_gem("sorbet-static", "0.5.10601") do |s| + s.platform = "x86_64-darwin" + end + end + + simulate_platform "x86_64-darwin-22" do + install_gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + gem "sorbet-static" + G + end + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.13.0", "x86_64-darwin" + c.checksum gem_repo4, "sorbet-static", "0.5.10601", "x86_64-darwin" + end + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.12.0) + nokogiri (1.12.0-x86_64-darwin) + sorbet-static (0.5.10601-x86_64-darwin) + + PLATFORMS + ruby + x86_64-darwin + + DEPENDENCIES + nokogiri + sorbet-static + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-darwin-22" do + bundle "update --conservative nokogiri" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.13.0-x86_64-darwin) + sorbet-static (0.5.10601-x86_64-darwin) + + PLATFORMS + x86_64-darwin + + DEPENDENCIES + nokogiri + sorbet-static + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "automatically fixes the lockfile if only ruby platform is locked and some gem has no ruby variant available" do + build_repo4 do + build_gem("sorbet-static-and-runtime", "0.5.10160") do |s| + s.add_dependency "sorbet", "= 0.5.10160" + s.add_dependency "sorbet-runtime", "= 0.5.10160" + end + + build_gem("sorbet", "0.5.10160") do |s| + s.add_dependency "sorbet-static", "= 0.5.10160" + end + + build_gem("sorbet-runtime", "0.5.10160") + + build_gem("sorbet-static", "0.5.10160") do |s| + s.platform = Gem::Platform.local + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet-static-and-runtime" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet (0.5.10160) + sorbet-static (= 0.5.10160) + sorbet-runtime (0.5.10160) + sorbet-static (0.5.10160-#{Gem::Platform.local}) + sorbet-static-and-runtime (0.5.10160) + sorbet (= 0.5.10160) + sorbet-runtime (= 0.5.10160) + + PLATFORMS + ruby + + DEPENDENCIES + sorbet-static-and-runtime + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "sorbet", "0.5.10160" + c.checksum gem_repo4, "sorbet-runtime", "0.5.10160" + c.checksum gem_repo4, "sorbet-static", "0.5.10160", Gem::Platform.local + c.checksum gem_repo4, "sorbet-static-and-runtime", "0.5.10160" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet (0.5.10160) + sorbet-static (= 0.5.10160) + sorbet-runtime (0.5.10160) + sorbet-static (0.5.10160-#{Gem::Platform.local}) + sorbet-static-and-runtime (0.5.10160) + sorbet (= 0.5.10160) + sorbet-runtime (= 0.5.10160) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + sorbet-static-and-runtime + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "automatically fixes the lockfile when adding a gem that introduces dependencies with no ruby platform variants transitively" do + simulate_platform "x86_64-linux" do + build_repo4 do + build_gem "nokogiri", "1.18.2" + + build_gem "nokogiri", "1.18.2" do |s| + s.platform = "x86_64-linux" + end + + build_gem("sorbet", "0.5.11835") do |s| + s.add_dependency "sorbet-static", "= 0.5.11835" + end + + build_gem "sorbet-static", "0.5.11835" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + gem "sorbet" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.18.2) + nokogiri (1.18.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.18.2", "x86_64-linux" + c.checksum gem_repo4, "sorbet", "0.5.11835" + c.checksum gem_repo4, "sorbet-static", "0.5.11835", "x86_64-linux" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.18.2) + nokogiri (1.18.2-x86_64-linux) + sorbet (0.5.11835) + sorbet-static (= 0.5.11835) + sorbet-static (0.5.11835-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + nokogiri + sorbet + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "automatically fixes the lockfile if multiple platforms locked, but no valid versions of direct dependencies for all of them" do + simulate_platform "x86_64-linux" do + build_repo4 do + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "x86_64-linux" + end + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "arm-linux" + end + + build_gem "sorbet-static", "0.5.10696" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + gem "sorbet-static" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.0-arm-linux) + nokogiri (1.14.0-x86_64-linux) + sorbet-static (0.5.10696-x86_64-linux) + + PLATFORMS + aarch64-linux + arm-linux + x86_64-linux + + DEPENDENCIES + nokogiri + sorbet-static + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.14.0", "x86_64-linux" + c.checksum gem_repo4, "sorbet-static", "0.5.10696", "x86_64-linux" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.0-x86_64-linux) + sorbet-static (0.5.10696-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + nokogiri + sorbet-static + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "automatically fixes the lockfile without removing other variants if it's missing platform gems, but they are installed locally" do + simulate_platform "x86_64-darwin-21" do + build_repo4 do + build_gem("sorbet-static", "0.5.10549") do |s| + s.platform = "universal-darwin-20" + end + + build_gem("sorbet-static", "0.5.10549") do |s| + s.platform = "universal-darwin-21" + end + end + + # Make sure sorbet-static-0.5.10549-universal-darwin-21 is installed + install_gemfile <<~G + source "https://gem.repo4" + + gem "sorbet-static", "= 0.5.10549" + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "sorbet-static", "0.5.10549", "universal-darwin-20" + c.checksum gem_repo4, "sorbet-static", "0.5.10549", "universal-darwin-21" + end + + # Make sure the lockfile is missing sorbet-static-0.5.10549-universal-darwin-21 + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet-static (0.5.10549-universal-darwin-20) + + PLATFORMS + x86_64-darwin + + DEPENDENCIES + sorbet-static (= 0.5.10549) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet-static (0.5.10549-universal-darwin-20) + sorbet-static (0.5.10549-universal-darwin-21) + + PLATFORMS + x86_64-darwin + + DEPENDENCIES + sorbet-static (= 0.5.10549) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "automatically fixes the lockfile if locked only to ruby, and some locked specs don't meet locked dependencies" do + simulate_platform "x86_64-linux" do + build_repo4 do + build_gem("ibandit", "0.7.0") do |s| + s.add_dependency "i18n", "~> 0.7.0" + end + + build_gem("i18n", "0.7.0.beta1") + build_gem("i18n", "0.7.0") + end + + gemfile <<~G + source "https://gem.repo4" + + gem "ibandit", "~> 0.7.0" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + i18n (0.7.0.beta1) + ibandit (0.7.0) + i18n (~> 0.7.0) + + PLATFORMS + ruby + + DEPENDENCIES + ibandit (~> 0.7.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --update i18n" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + i18n (0.7.0) + ibandit (0.7.0) + i18n (~> 0.7.0) + + PLATFORMS + ruby + + DEPENDENCIES + ibandit (~> 0.7.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "does not remove ruby if gems for other platforms, and not present in the lockfile, exist in the Gemfile" do + build_repo4 do + build_gem "nokogiri", "1.13.8" + build_gem "nokogiri", "1.13.8" do |s| + s.platform = Gem::Platform.local + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + + gem "tzinfo", "~> 1.2", platform: :#{not_local_tag} + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.13.8" + c.checksum gem_repo4, "nokogiri", "1.13.8", Gem::Platform.local + end + + original_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.13.8) + nokogiri (1.13.8-#{Gem::Platform.local}) + + PLATFORMS + #{lockfile_platforms("ruby")} + + DEPENDENCIES + nokogiri + tzinfo (~> 1.2) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + lockfile original_lockfile + + bundle "lock --update" + + expect(lockfile).to eq(original_lockfile) + end + + it "does not remove ruby if gems for other platforms, and not present in the lockfile, exist in the Gemfile, and the lockfile only has ruby" do + build_repo4 do + build_gem "nokogiri", "1.13.8" + build_gem "nokogiri", "1.13.8" do |s| + s.platform = "arm64-darwin" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + + gem "tzinfo", "~> 1.2", platforms: %i[windows jruby] + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.13.8" + end + + original_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.13.8) + + PLATFORMS + ruby + + DEPENDENCIES + nokogiri + tzinfo (~> 1.2) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + lockfile original_lockfile + + simulate_platform "arm64-darwin-23" do + bundle "lock --update" + end + + expect(lockfile).to eq(original_lockfile) + end + + it "does not remove ruby when adding a new gem to the Gemfile" do + build_repo4 do + build_gem "concurrent-ruby", "1.2.2" + build_gem "myrack", "3.0.7" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "concurrent-ruby" + gem "myrack" + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "concurrent-ruby", "1.2.2" + c.checksum gem_repo4, "myrack", "3.0.7" + end + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + concurrent-ruby (1.2.2) + + PLATFORMS + ruby + + DEPENDENCIES + concurrent-ruby + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + concurrent-ruby (1.2.2) + myrack (3.0.7) + + PLATFORMS + #{lockfile_platforms(generic_default_locked_platform || local_platform, defaults: ["ruby"])} + + DEPENDENCIES + concurrent-ruby + myrack + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "can fallback to a source gem when platform gems are incompatible with current ruby version" do + setup_multiplatform_gem_with_source_gem + + gemfile <<~G + source "https://gem.repo2" + + gem "my-precompiled-gem" + G + + # simulate lockfile which includes both a precompiled gem with: + # - Gem the current platform (with incompatible ruby version) + # - A source gem with compatible ruby version + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + my-precompiled-gem (3.0.0) + my-precompiled-gem (3.0.0-#{Bundler.local_platform}) + + PLATFORMS + ruby + #{Bundler.local_platform} + + DEPENDENCIES + my-precompiled-gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle :install + end + + it "automatically adds the ruby variant to the lockfile if the specific platform is locked and we move to a newer ruby version for which a native package is not available" do + # + # Given an existing application using native gems (e.g., nokogiri) + # And a lockfile generated with a stable ruby version + # When want test the application against ruby-head and `bundle install` + # Then bundler should fall back to the generic ruby platform gem + # + simulate_platform "x86_64-linux" do + build_repo4 do + build_gem "nokogiri", "1.14.0" + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "x86_64-linux" + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri", "1.14.0" + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.14.0", "x86_64-linux" + end + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.0-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + nokogiri (= 1.14.0) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle :install + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.14.0" + c.checksum gem_repo4, "nokogiri", "1.14.0", "x86_64-linux" + end + + expect(lockfile).to eq(<<~L) + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.0) + nokogiri (1.14.0-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + nokogiri (= 1.14.0) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "automatically fixes the lockfile when only ruby platform locked, and adding a dependency with subdependencies not valid for ruby" do + simulate_platform "x86_64-linux" do + build_repo4 do + build_gem("sorbet", "0.5.10160") do |s| + s.add_dependency "sorbet-static", "= 0.5.10160" + end + + build_gem("sorbet-static", "0.5.10160") do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + + PLATFORMS + ruby + + DEPENDENCIES + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet (0.5.10160) + sorbet-static (= 0.5.10160) + sorbet-static (0.5.10160-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + sorbet + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "locks specific platforms automatically" do + simulate_platform "x86_64-linux" do + build_repo4 do + build_gem "nokogiri", "1.14.0" + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "x86_64-linux" + end + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "arm-linux" + end + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "x64-mingw-ucrt" + end + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "java" + end + + build_gem "sorbet-static", "0.5.10696" do |s| + s.platform = "x86_64-linux" + end + build_gem "sorbet-static", "0.5.10696" do |s| + s.platform = "universal-darwin-22" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + G + + bundle "lock" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.14.0" + c.checksum gem_repo4, "nokogiri", "1.14.0", "arm-linux" + c.checksum gem_repo4, "nokogiri", "1.14.0", "x86_64-linux" + end + + # locks all compatible platforms, excluding Java and Windows + expect(lockfile).to eq(<<~L) + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.0) + nokogiri (1.14.0-arm-linux) + nokogiri (1.14.0-x86_64-linux) + + PLATFORMS + arm-linux + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + gem "sorbet-static" + G + + FileUtils.rm bundled_app_lock + + bundle "lock" + + checksums.delete "nokogiri", "arm-linux" + checksums.checksum gem_repo4, "sorbet-static", "0.5.10696", "universal-darwin-22" + checksums.checksum gem_repo4, "sorbet-static", "0.5.10696", "x86_64-linux" + + # locks only platforms compatible with all gems in the bundle + expect(lockfile).to eq(<<~L) + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.0) + nokogiri (1.14.0-x86_64-linux) + sorbet-static (0.5.10696-universal-darwin-22) + sorbet-static (0.5.10696-x86_64-linux) + + PLATFORMS + universal-darwin-22 + x86_64-linux + + DEPENDENCIES + nokogiri + sorbet-static + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "does not fail when a platform variant is incompatible with the current ruby and another equivalent platform specific variant is part of the resolution" do + build_repo4 do + build_gem "nokogiri", "1.15.5" + + build_gem "nokogiri", "1.15.5" do |s| + s.platform = "x86_64-linux" + s.required_ruby_version = "< #{current_ruby_minor}.dev" + end + + build_gem "sass-embedded", "1.69.5" + + build_gem "sass-embedded", "1.69.5" do |s| + s.platform = "x86_64-linux-gnu" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + gem "sass-embedded" + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.15.5" + c.checksum gem_repo4, "sass-embedded", "1.69.5" + c.checksum gem_repo4, "sass-embedded", "1.69.5", "x86_64-linux-gnu" + end + + simulate_platform "x86_64-linux" do + bundle "install --verbose" + + # locks all compatible platforms, excluding Java and Windows + expect(lockfile).to eq(<<~L) + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.15.5) + sass-embedded (1.69.5) + sass-embedded (1.69.5-x86_64-linux-gnu) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + sass-embedded + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "does not add ruby platform gem if it brings extra dependencies not resolved originally" do + build_repo4 do + build_gem "nokogiri", "1.15.5" do |s| + s.add_dependency "mini_portile2", "~> 2.8.2" + end + + build_gem "nokogiri", "1.15.5" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.15.5", "x86_64-linux" + end + + simulate_platform "x86_64-linux" do + bundle "install --verbose" + + expect(lockfile).to eq(<<~L) + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.15.5-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + nokogiri + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + ["x86_64-linux", "x86_64-linux-musl"].each do |host_platform| + describe "on host platform #{host_platform}" do + it "adds current musl platform" do + build_repo4 do + build_gem "rcee_precompiled", "0.5.0" do |s| + s.platform = "x86_64-linux" + end + + build_gem "rcee_precompiled", "0.5.0" do |s| + s.platform = "x86_64-linux-musl" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "rcee_precompiled", "0.5.0" + G + + simulate_platform host_platform do + bundle "lock" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "x86_64-linux" + c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "x86_64-linux-musl" + end + + expect(lockfile).to eq(<<~L) + GEM + remote: https://gem.repo4/ + specs: + rcee_precompiled (0.5.0-x86_64-linux) + rcee_precompiled (0.5.0-x86_64-linux-musl) + + PLATFORMS + x86_64-linux + x86_64-linux-musl + + DEPENDENCIES + rcee_precompiled (= 0.5.0) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + end + end + + it "adds current musl platform, when there are also gnu variants" do + build_repo4 do + build_gem "rcee_precompiled", "0.5.0" do |s| + s.platform = "x86_64-linux-gnu" + end + + build_gem "rcee_precompiled", "0.5.0" do |s| + s.platform = "x86_64-linux-musl" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "rcee_precompiled", "0.5.0" + G + + simulate_platform "x86_64-linux-musl" do + bundle "lock" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "x86_64-linux-gnu" + c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "x86_64-linux-musl" + end + + expect(lockfile).to eq(<<~L) + GEM + remote: https://gem.repo4/ + specs: + rcee_precompiled (0.5.0-x86_64-linux-gnu) + rcee_precompiled (0.5.0-x86_64-linux-musl) + + PLATFORMS + x86_64-linux-gnu + x86_64-linux-musl + + DEPENDENCIES + rcee_precompiled (= 0.5.0) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "does not add current platform if there's an equivalent less specific platform among the ones resolved" do + build_repo4 do + build_gem "rcee_precompiled", "0.5.0" do |s| + s.platform = "universal-darwin" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "rcee_precompiled", "0.5.0" + G + + simulate_platform "x86_64-darwin-15" do + bundle "lock" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "universal-darwin" + end + + expect(lockfile).to eq(<<~L) + GEM + remote: https://gem.repo4/ + specs: + rcee_precompiled (0.5.0-universal-darwin) + + PLATFORMS + universal-darwin + + DEPENDENCIES + rcee_precompiled (= 0.5.0) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "does not re-resolve when a specific platform, but less specific than the current platform, is locked" do + build_repo4 do + build_gem "nokogiri" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.0) + + PLATFORMS + arm64-darwin + + DEPENDENCIES + nokogiri! + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "arm64-darwin-23" do + bundle "install --verbose" + + expect(out).to include("Found no changes, using resolution from the lockfile") + end + end + + it "does not remove generic platform gems locked for a specific platform from lockfile when unlocking an unrelated gem" do + build_repo4 do + build_gem "ffi" + + build_gem "ffi" do |s| + s.platform = "x86_64-linux" + end + + build_gem "nokogiri" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "ffi" + gem "nokogiri" + G + + original_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + ffi (1.0) + nokogiri (1.0) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + ffi + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + lockfile original_lockfile + + simulate_platform "x86_64-linux" do + bundle "lock --update nokogiri" + + expect(lockfile).to eq(original_lockfile) + end + end + + it "does not remove generic platform gems locked for a specific platform from lockfile when unlocking an unrelated gem, and variants for other platform also locked" do + build_repo4 do + build_gem "ffi" + + build_gem "ffi" do |s| + s.platform = "x86_64-linux" + end + + build_gem "ffi" do |s| + s.platform = "java" + end + + build_gem "nokogiri" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "ffi" + gem "nokogiri" + G + + original_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + ffi (1.0) + ffi (1.0-java) + nokogiri (1.0) + + PLATFORMS + java + x86_64-linux + + DEPENDENCIES + ffi + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + lockfile original_lockfile + + simulate_platform "x86_64-linux" do + bundle "lock --update nokogiri" + + expect(lockfile).to eq(original_lockfile) + end + end + + it "does not remove platform specific gems from lockfile when using a ruby version that does not match their ruby requirements, since they may be useful in other rubies" do + build_repo4 do + build_gem("google-protobuf", "3.25.5") + build_gem("google-protobuf", "3.25.5") do |s| + s.required_ruby_version = "< #{current_ruby_minor}.dev" + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "google-protobuf", "~> 3.0" + G + + original_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + google-protobuf (3.25.5) + google-protobuf (3.25.5-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + google-protobuf (~> 3.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + lockfile original_lockfile + + simulate_platform "x86_64-linux" do + bundle "lock --update" + end + + expect(lockfile).to eq(original_lockfile) + end + + private + + def setup_multiplatform_gem + 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 = "x64-mingw-ucrt" } + 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 = "x64-mingw-ucrt" } + 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.4.0") + build_gem("google-protobuf", "3.0.0.alpha.3.1.pre") + end + end + + def setup_multiplatform_gem_with_different_dependencies_per_platform + build_repo2 do + build_gem("facter", "2.4.6") + build_gem("facter", "2.4.6") do |s| + s.platform = "universal-darwin" + s.add_dependency "CFPropertyList" + end + build_gem("CFPropertyList") + end + end + + def setup_multiplatform_gem_with_source_gem + build_repo2 do + build_gem("my-precompiled-gem", "3.0.0") + build_gem("my-precompiled-gem", "3.0.0") do |s| + s.platform = Bundler.local_platform + + # purposely unresolvable + s.required_ruby_version = ">= 1000.0.0" end end end diff --git a/spec/bundler/install/gemfile_spec.rb b/spec/bundler/install/gemfile_spec.rb index dd08939cb0..83875a3d0e 100644 --- a/spec/bundler/install/gemfile_spec.rb +++ b/spec/bundler/install/gemfile_spec.rb @@ -3,7 +3,9 @@ RSpec.describe "bundle install" do context "with duplicated gems" do it "will display a warning" do - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem 'rails', '~> 4.0.0' gem 'rails', '~> 4.0.0' G @@ -14,92 +16,156 @@ RSpec.describe "bundle install" do context "with --gemfile" do it "finds the gemfile" do gemfile bundled_app("NotGemfile"), <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' G - bundle :install, :gemfile => bundled_app("NotGemfile") + bundle :install, gemfile: bundled_app("NotGemfile") # Specify BUNDLE_GEMFILE for `the_bundle` # to retrieve the proper Gemfile ENV["BUNDLE_GEMFILE"] = "NotGemfile" - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "respects lockfile and BUNDLE_LOCKFILE" do + gemfile bundled_app("NotGemfile"), <<-G + lockfile "ReallyNotGemfile.lock" + source "https://gem.repo1" + gem 'myrack' + G + + bundle :install, gemfile: bundled_app("NotGemfile") + + ENV["BUNDLE_GEMFILE"] = "NotGemfile" + ENV["BUNDLE_LOCKFILE"] = "ReallyNotGemfile.lock" + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "respects BUNDLE_LOCKFILE during bundle install" do + ENV["BUNDLE_LOCKFILE"] = "ReallyNotGemfile.lock" + + gemfile bundled_app("NotGemfile"), <<-G + source "https://gem.repo1" + gem 'myrack' + G + + bundle :install, gemfile: bundled_app("NotGemfile") + expect(bundled_app("ReallyNotGemfile.lock")).to exist + + ENV["BUNDLE_GEMFILE"] = "NotGemfile" + expect(the_bundle).to include_gems "myrack 1.0.0" end end context "with gemfile set via config" do before do gemfile bundled_app("NotGemfile"), <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' G - bundle "config set --local gemfile #{bundled_app("NotGemfile")}" + bundle_config "gemfile #{bundled_app("NotGemfile")}" end it "uses the gemfile to install" do bundle "install" bundle "list" - expect(out).to include("rack (1.0.0)") + expect(out).to include("myrack (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 "list" + bundle "install", dir: bundled_app("subdir") + bundle "list", dir: bundled_app("subdir") - expect(out).to include("rack (1.0.0)") - end + expect(out).to include("myrack (1.0.0)") end end - context "with deprecated features" do - before :each do - in_app_root + it "reports that lib is an invalid option" do + gemfile <<-G + source "https://gem.repo1" + + gem "myrack", :lib => "myrack" + G + + bundle :install, raise_on_error: false + expect(err).to match(/You passed :lib as an option for gem 'myrack', but it is invalid/) + end + + it "reports that type is an invalid option" do + gemfile <<-G + source "https://gem.repo1" + + gem "myrack", :type => "development" + G + + bundle :install, raise_on_error: false + expect(err).to match(/You passed :type as an option for gem 'myrack', but it is invalid/) + end + + it "reports that gemfile is an invalid option" do + gemfile <<-G + source "https://gem.repo1" + + gem "myrack", :gemfile => "foo" + G + + bundle :install, raise_on_error: false + expect(err).to match(/You passed :gemfile as an option for gem 'myrack', but it is invalid/) + end + + context "when an internal error happens" do + let(:bundler_bug) do + create_file("bundler_bug.rb", <<~RUBY) + require "bundler" + + module Bundler + class Dsl + def source(source, *args, &blk) + nil.name + end + end + end + RUBY + + bundled_app("bundler_bug.rb").to_s end - it "reports that lib is an invalid option" do - gemfile <<-G - gem "rack", :lib => "rack" - G + it "shows culprit file and line" do + skip "ruby-core test setup has always \"lib\" in $LOAD_PATH so `require \"bundler\"` always activates the local version rather than using RubyGems gem activation stuff, causing conflicts" if ruby_core? - bundle :install - expect(err).to match(/You passed :lib as an option for gem 'rack', but it is invalid/) + install_gemfile "source 'https://gem.repo1'", requires: [bundler_bug], artifice: nil, raise_on_error: false + expect(err).to include("bundler_bug.rb:6") end end - context "with engine specified in symbol" do + context "with engine specified in symbol", :jruby_only 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_uri_for(gem_repo1)}" - ruby "2.3.0", :engine => :jruby, :engine_version => "9.1.2.0" - G - - expect(out).to match(/Bundle complete!/) - end - end + install_gemfile <<-G + source "https://gem.repo1" + ruby "#{RUBY_VERSION}", :engine => :jruby, :engine_version => "#{RUBY_ENGINE_VERSION}" + G + + expect(out).to match(/Bundle complete!/) 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_uri_for(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 + install_gemfile <<-G + source "https://gem.repo1" + ruby "#{RUBY_VERSION}", :engine => :jruby, :engine_version => "#{RUBY_ENGINE_VERSION}" + gem "myrack" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" end end context "with a Gemfile containing non-US-ASCII characters" do it "reads the Gemfile with the UTF-8 encoding by default" do install_gemfile <<-G + source "https://gem.repo1" + str = "Il était une fois ..." puts "The source encoding is: " + str.encoding.name G @@ -113,6 +179,8 @@ RSpec.describe "bundle install" do # NOTE: This works thanks to #eval interpreting the magic encoding comment install_gemfile <<-G # encoding: iso-8859-1 + source "https://gem.repo1" + str = "Il #{"\xE9".dup.force_encoding("binary")}tait une fois ..." puts "The source encoding is: " + str.encoding.name G diff --git a/spec/bundler/install/gems/compact_index_spec.rb b/spec/bundler/install/gems/compact_index_spec.rb index 2c145ce643..9db73b84b5 100644 --- a/spec/bundler/install/gems/compact_index_spec.rb +++ b/spec/bundler/install/gems/compact_index_spec.rb @@ -7,12 +7,27 @@ RSpec.describe "compact index api" do it "should use the API" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :artifice => "compact_index" + 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" + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "has a debug mode" do + gemfile <<-G + source "#{source_uri}" + gem "myrack" + G + + bundle :install, artifice: "compact_index", env: { "DEBUG_COMPACT_INDEX" => "true" } + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(err).to include("[Bundler::CompactIndexClient] available?") + expect(err).to include("[Bundler::CompactIndexClient] fetching versions") + expect(err).to include("[Bundler::CompactIndexClient] info(myrack)") + expect(err).to include("[Bundler::CompactIndexClient] fetching info/myrack") + expect(the_bundle).to include_gems "myrack 1.0.0" end it "should URI encode gem names" do @@ -21,7 +36,7 @@ RSpec.describe "compact index api" do gem " sinatra" G - bundle :install, :artifice => "compact_index" + bundle :install, artifice: "compact_index", raise_on_error: false expect(err).to include("' sinatra' is not a valid gem name because it contains whitespace.") end @@ -31,7 +46,7 @@ RSpec.describe "compact index api" do gem "rails" G - bundle! :install, :artifice => "compact_index" + 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", @@ -44,23 +59,23 @@ RSpec.describe "compact index api" do 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") + build_repo4(build_compact_index: false) do + build_gem "myrack", "1.0" do |s| + s.add_dependency("Myrack", "0.1") end - build_gem "Rack", "0.1" + build_gem "Myrack", "0.1" end - install_gemfile! <<-G, :artifice => "compact_index", :env => { "BUNDLER_SPEC_GEM_REPO" => gem_repo4 } + install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } source "#{source_uri}" - gem "rack", "1.0" - gem "Rack", "0.1" + gem "myrack", "1.0" + gem "Myrack", "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") + run "Bundler.require; puts Gem.loaded_specs.values_at('myrack', 'Myrack').map(&:full_name)" + expect(out).to eq("myrack-1.0\nMyrack-0.1") end it "should handle multiple gem dependencies on the same gem" do @@ -69,20 +84,21 @@ RSpec.describe "compact index api" do gem "net-sftp" G - bundle! :install, :artifice => "compact_index" + 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 + it "should use the endpoint when using deployment mode" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :artifice => "compact_index" + bundle :install, artifice: "compact_index" - bundle! :install, forgotten_command_line_options(:deployment => true, :path => "vendor/bundle").merge(:artifice => "compact_index") + bundle_config "deployment true" + 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" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "handles git dependencies that are in rubygems" do @@ -93,17 +109,17 @@ RSpec.describe "compact index api" do gemfile <<-G source "#{source_uri}" - git "#{file_uri_for(lib_path("foo-1.0"))}" do + git "#{lib_path("foo-1.0")}" do gem 'foo' end G - bundle! :install, :artifice => "compact_index" + 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 + it "handles git dependencies that are in rubygems using deployment mode" do build_git "foo" do |s| s.executables = "foobar" s.add_dependency "rails", "2.3.2" @@ -111,65 +127,73 @@ RSpec.describe "compact index api" do gemfile <<-G source "#{source_uri}" - gem 'foo', :git => "#{file_uri_for(lib_path("foo-1.0"))}" + gem 'foo', :git => "#{lib_path("foo-1.0")}" G - bundle! :install, :artifice => "compact_index" + bundle :install, artifice: "compact_index" - bundle "install --deployment", :artifice => "compact_index" + bundle_config "deployment true" + bundle :install, 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 + it "doesn't fail if you only have a git gem with no deps when using deployment mode" do build_git "foo" gemfile <<-G source "#{source_uri}" - gem 'foo', :git => "#{file_uri_for(lib_path("foo-1.0"))}" + gem 'foo', :git => "#{lib_path("foo-1.0")}" G - bundle "install", :artifice => "compact_index" - bundle! :install, forgotten_command_line_options(:deployment => true).merge(:artifice => "compact_index") + bundle "install", artifice: "compact_index" + bundle_config "deployment true" + bundle :install, artifice: "compact_index" expect(the_bundle).to include_gems("foo 1.0") end - it "falls back when the API errors out" do - simulate_platform mswin - + it "falls back when the API URL returns 403 Forbidden" do gemfile <<-G source "#{source_uri}" - gem "rcov" + gem "myrack" 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" + bundle :install, verbose: true, artifice: "compact_index_forbidden" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "falls back when the API URL returns 403 Forbidden" do + it "falls back when the versions endpoint has a checksum mismatch" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :verbose => true, :artifice => "compact_index_forbidden" + bundle :install, verbose: true, artifice: "compact_index_checksum_mismatch" expect(out).to include("Fetching gem metadata from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(out).to include("The checksum of /versions does not match the checksum provided by the server!") + expect(out).to include("Calculated checksums #{{ "sha-256" => "8KfZiM/fszVkqhP/m5s9lvE6M9xKu4I1bU4Izddp5Ms=" }.inspect} did not match expected #{{ "sha-256" => "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=" }.inspect}") + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "falls back when the versions endpoint has a checksum mismatch" do + it "shows proper path when permission errors happen", :permissions do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" 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" + versions = compact_index_cache_path.join( + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions" + ) + versions.dirname.mkpath + versions.write("created_at") + FileUtils.chmod("-r", versions) + + bundle :install, artifice: "compact_index", raise_on_error: false + + expect(err).to include( + "There was an error while trying to read from `#{versions}`. It is likely that you need to grant read permissions for that path." + ) end it "falls back when the user's home directory does not exist or is not writable" do @@ -177,28 +201,28 @@ The checksum of /versions does not match the checksum provided by the server! So gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :artifice => "compact_index" + 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" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "handles host redirects" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :artifice => "compact_index_host_redirect" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "compact_index_host_redirect" + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "handles host redirects without Net::HTTP::Persistent" do + it "handles host redirects without Gem::Net::HTTP::Persistent" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G FileUtils.mkdir_p lib_path @@ -214,18 +238,18 @@ The checksum of /versions does not match the checksum provided by the server! So H end - bundle! :install, :artifice => "compact_index_host_redirect", :requires => [lib_path("disable_net_http_persistent.rb")] + 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" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "times out when Bundler::Fetcher redirects too much" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "compact_index_redirects" + bundle :install, artifice: "compact_index_redirects", raise_on_error: false expect(err).to match(/Too many redirects/) end @@ -233,69 +257,74 @@ The checksum of /versions does not match the checksum provided by the server! So it "should use the modern index for install" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle "install --full-index", :artifice => "compact_index" + 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" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "should use the modern index for update" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle! "update --full-index", :artifice => "compact_index", :all => true + bundle "update --full-index", artifice: "compact_index", all: true expect(out).to include("Fetching source index from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end end it "does not double check for gems that are only installed locally" do - system_gems %w[rack-1.0.0 thin-1.0 net_a-1.0] - bundle! "config set --local path.system true" - ENV["BUNDLER_SPEC_ALL_REQUESTS"] = strip_whitespace(<<-EOS).strip - #{source_uri}/versions - #{source_uri}/info/rack - EOS - - install_gemfile! <<-G, :artifice => "compact_index", :verbose => true - source "#{source_uri}" - gem "rack" - G - - expect(last_command.stdboth).not_to include "Double checking" - end - - it "fetches again when more dependencies are found in subsequent sources", :bundler => "< 3" do build_repo2 do - build_gem "back_deps" do |s| - s.add_dependency "foo" + 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", __dir__) + FileUtils.mkdir_p(path) + File.open("\#{path}/net_build_extensions.rb", "w") do |f| + f.puts "NET_BUILD_EXTENSIONS = 'YES'" + end + end + RUBY end - FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] end - gemfile <<-G + system_gems %w[myrack-1.0.0 thin-1.0 net_a-1.0], gem_repo: gem_repo2 + bundle_config "path.system true" + ENV["BUNDLER_SPEC_ALL_REQUESTS"] = <<~EOS.strip + #{source_uri}/versions + #{source_uri}/info/myrack + EOS + + install_gemfile <<-G, artifice: "compact_index", verbose: true, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s } source "#{source_uri}" - source "#{source_uri}/extra" - gem "back_deps" + gem "myrack" G - bundle! :install, :artifice => "compact_index_extra" - expect(the_bundle).to include_gems "back_deps 1.0", "foo 1.0" + expect(stdboth).not_to include "Double checking" end - it "fetches again when more dependencies are found in subsequent sources with source blocks" do + 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")] + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] end - install_gemfile! <<-G, :artifice => "compact_index_extra", :verbose => true + install_gemfile <<-G, artifice: "compact_index_extra", verbose: true source "#{source_uri}" source "#{source_uri}/extra" do gem "back_deps" @@ -308,55 +337,33 @@ The checksum of /versions does not match the checksum provided by the server! So it "fetches gem versions even when those gems are already installed" do gemfile <<-G source "#{source_uri}" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" G - bundle! :install, :artifice => "compact_index_extra_api" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "compact_index_extra_api" + expect(the_bundle).to include_gems "myrack 1.0.0" build_repo4 do - build_gem "rack", "1.2" do |s| - s.executables = "rackup" + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" end end gemfile <<-G source "#{source_uri}" do; end source "#{source_uri}/extra" - gem "rack", "1.2" + gem "myrack", "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", :bundler => "< 3" 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" + bundle :install, artifice: "compact_index_extra_api" + expect(the_bundle).to include_gems "myrack 1.2" end - it "considers all possible versions of dependencies from all api gem sources when using blocks", :bundler => "< 3" 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. + it "resolves indirect dependencies to the most scoped source that includes them" 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 tries to + # use the version in the most scoped source, even if not compatible, and + # gives a resolution error build_repo4 do build_gem "activesupport", "1.2.0" build_gem "somegem", "1.0.0" do |s| @@ -371,10 +378,9 @@ The checksum of /versions does not match the checksum provided by the server! So end G - bundle! :install, :artifice => "compact_index_extra_api" + bundle :install, artifice: "compact_index_extra_api", raise_on_error: false - expect(the_bundle).to include_gems "somegem 1.0.0" - expect(the_bundle).to include_gems "activesupport 1.2.3" + expect(err).to include("Could not find compatible versions") end it "prints API output properly with back deps" do @@ -382,7 +388,7 @@ The checksum of /versions does not match the checksum provided by the server! So build_gem "back_deps" do |s| s.add_dependency "foo" end - FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] end gemfile <<-G @@ -392,52 +398,43 @@ The checksum of /versions does not match the checksum provided by the server! So end G - bundle! :install, :artifice => "compact_index_extra" + 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 + it "does not fetch every spec 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")] + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] end - gemfile <<-G + install_gemfile <<-G, artifice: "compact_index_extra_missing" source "#{source_uri}" source "#{source_uri}/extra" do gem "back_deps" end G - bundle! :install, :artifice => "compact_index_extra_missing" expect(the_bundle).to include_gems "back_deps 1.0" end - it "does not fetch every spec if the index of gems is large when doing back deps & everything is the compact index" do + it "does not fetch every spec when doing back deps & everything is the compact index" do build_repo4 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_repo4("gems/foo-*.gem")] + FileUtils.rm_r Dir[gem_repo4("gems/foo-*.gem")] end - install_gemfile! <<-G, :artifice => "compact_index_extra_api_missing" + install_gemfile <<-G, artifice: "compact_index_extra_api_missing" source "#{source_uri}" source "#{source_uri}/extra" do gem "back_deps" @@ -454,36 +451,16 @@ The checksum of /versions does not match the checksum provided by the server! So gem 'foo' G - bundle! :install, :artifice => "compact_index_api_missing" + 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", :bundler => "< 3" do + it "fetches again when more dependencies are found in subsequent sources using deployment mode" 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 "fetches again when more dependencies are found in subsequent sources using --deployment with blocks" do - build_repo2 do - build_gem "back_deps" do |s| - s.add_dependency "foo" - end - FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] end gemfile <<-G @@ -493,95 +470,55 @@ The checksum of /versions does not match the checksum provided by the server! So end G - bundle! :install, :artifice => "compact_index_extra" - - bundle "install --deployment", :artifice => "compact_index_extra" + bundle :install, artifice: "compact_index_extra" + bundle_config "deployment true" + bundle :install, 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 + build_repo2 do + build_gem "bundler_dep" do |s| + s.add_dependency "bundler" + end + end + gemfile <<-G source "#{source_uri}" gem "bundler_dep" G - bundle! :install, :artifice => "compact_index" + bundle :install, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s } 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", :bundler => "< 3" 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", :bundler => "< 3" 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", :bundler => "< 3" 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' + gem 'myrack-obama' G - bundle! :install, :artifice => "compact_index" - expect(out).to include("Post-install message from rack:") + bundle :install, artifice: "compact_index" + expect(out).to include("Post-install message from myrack:") end it "should display the post install message for a dependency" do gemfile <<-G source "#{source_uri}" - gem 'rack_middleware' + gem 'myrack_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") + bundle :install, artifice: "compact_index" + expect(out).to include("Post-install message from myrack:") + expect(out).to include("Myrack'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 = Gem::URI.parse(source_uri) uri.user = user uri.password = password @@ -591,115 +528,101 @@ The checksum of /versions does not match the checksum provided by the server! So it "passes basic authentication details and strips out creds" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :artifice => "compact_index_basic_authentication" + bundle :install, artifice: "compact_index_basic_authentication" expect(out).not_to include("#{user}:#{password}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "strips http basic authentication creds for modern index" do + it "passes basic authentication details and strips out creds also in verbose mode" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :artifice => "endopint_marshal_fail_basic_authentication" + bundle :install, verbose: true, 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 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", :bundler => "< 3" do - gemfile <<-G - source "#{basic_auth_source_uri}" - source "#{file_uri_for(gem_repo1)}" - gem "rack" - G - - bundle! :install, :artifice => "compact_index_basic_authentication" - expect(err).to include("Warning: the gem 'rack' was found in multiple sources.") - expect(err).not_to include("#{user}:#{password}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 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" + gem "myrack" G - bundle! :install, :artifice => "compact_index_creds_diff_host" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "compact_index_creds_diff_host" + expect(the_bundle).to include_gems "myrack 1.0.0" end describe "with authentication details in bundle config" do before do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G end it "reads authentication details by host name from bundle config" do bundle "config set #{source_hostname} #{user}:#{password}" - bundle! :install, :artifice => "compact_index_strict_basic_authentication" + 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" + expect(the_bundle).to include_gems "myrack 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 set #{source_uri}/ #{user}:#{password}" - bundle! :install, :artifice => "compact_index_strict_basic_authentication" + 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" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "should use the API" do bundle "config set #{source_hostname} #{user}:#{password}" - bundle! :install, :artifice => "compact_index_strict_basic_authentication" + 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" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "prefers auth supplied in the source uri" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G bundle "config set #{source_hostname} otheruser:wrong" - bundle! :install, :artifice => "compact_index_strict_basic_authentication" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "compact_index_strict_basic_authentication" + expect(the_bundle).to include_gems "myrack 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(err).to include("bundle config set #{source_hostname} username:password") + bundle :install, artifice: "compact_index_strict_basic_authentication", raise_on_error: false + expect(err).to include("bundle config set --global #{source_hostname} username:password") end it "fails if authentication has already been provided, but failed" do bundle "config set #{source_hostname} #{user}:wrong" - bundle :install, :artifice => "compact_index_strict_basic_authentication" + bundle :install, artifice: "compact_index_strict_basic_authentication", raise_on_error: false expect(err).to include("Bad username or password") end + + it "does not fallback to old dependency API if bad authentication is provided" do + bundle "config set #{source_hostname} #{user}:wrong" + + bundle :install, artifice: "compact_index_strict_basic_authentication", raise_on_error: false, verbose: true + expect(err).to include("Bad username or password") + expect(out).to include("HTTP 401 Unauthorized http://user@localgemserver.test/versions") + expect(out).not_to include("HTTP 401 Unauthorized http://user@localgemserver.test/api/v1/dependencies") + end end describe "with no password" do @@ -708,11 +631,11 @@ The checksum of /versions does not match the checksum provided by the server! So it "passes basic authentication details" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :artifice => "compact_index_basic_authentication" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "compact_index_basic_authentication" + expect(the_bundle).to include_gems "myrack 1.0.0" end end end @@ -730,14 +653,14 @@ The checksum of /versions does not match the checksum provided by the server! So end end - it "explains what to do to get it" do + it "explains what to do to get it, and includes original error" do gemfile <<-G source "#{source_uri.gsub(/http/, "https")}" - gem "rack" + gem "myrack" G - bundle :install, :env => { "RUBYOPT" => "-I#{bundled_app("broken_ssl")}" } - expect(err).to include("OpenSSL") + bundle :install, env: { "RUBYOPT" => "-I#{bundled_app("broken_ssl")}" }, raise_on_error: false, artifice: nil + expect(err).to include("recompile Ruby").and include("cannot load such file") end end @@ -746,195 +669,344 @@ The checksum of /versions does not match the checksum provided by the server! So # Install a monkeypatch that reproduces the effects of openssl raising # a certificate validation error when RubyGems tries to connect. gemfile <<-G - class Net::HTTP + class Gem::Net::HTTP def start raise OpenSSL::SSL::SSLError, "certificate verify failed" end end source "#{source_uri.gsub(/http/, "https")}" - gem "rack" + gem "myrack" G - bundle :install + bundle :install, raise_on_error: false expect(err).to match(/could not verify the SSL certificate/i) end end context ".gemrc with sources is present" do - before do + it "uses other sources declared in the Gemfile" do File.open(home(".gemrc"), "w") do |file| - file.puts({ :sources => ["https://rubygems.org"] }.to_yaml) + 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 + begin + gemfile <<-G + source "#{source_uri}" + gem 'myrack' + G - bundle! :install, :artifice => "compact_index_forbidden" + bundle :install, artifice: "compact_index_forbidden" + ensure + FileUtils.rm_rf home(".gemrc") + end end end - it "performs partial update with a non-empty range" do + it "performs update with etag not-modified" do + versions_etag = compact_index_cache_path.join( + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions.etag" + ) + expect(versions_etag.file?).to eq(false) + gemfile <<-G source "#{source_uri}" - gem 'rack', '0.9.1' + gem 'myrack', '0.9.1' G - # Initial install creates the cached versions file - bundle! :install, :artifice => "compact_index" + # Initial install creates the cached versions file and etag file + bundle :install, artifice: "compact_index" + + expect(versions_etag.file?).to eq(true) + previous_content = versions_etag.binread # Update the Gemfile so we can check subsequent install was successful gemfile <<-G source "#{source_uri}" - gem 'rack', '1.0.0' + gem 'myrack', '1.0.0' + G + + # Second install should match etag + bundle :install, artifice: "compact_index_etag_match" + + expect(versions_etag.binread).to eq(previous_content) + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "performs full update when range is ignored" do + gemfile <<-G + source "#{source_uri}" + gem 'myrack', '0.9.1' + G + + # Initial install creates the cached versions file and etag file + bundle :install, artifice: "compact_index" + + gemfile <<-G + source "#{source_uri}" + gem 'myrack', '1.0.0' + G + + versions = compact_index_cache_path.join( + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions" + ) + # Modify the cached file. The ranged request will be based on this but, + # in this test, the range is ignored so this gets overwritten, allowing install. + versions.write "ruining this file" + + bundle :install, artifice: "compact_index_range_ignored" + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "performs partial update with a non-empty range" do + build_repo4 do + build_gem "myrack", "0.9.1" + end + + # Initial install creates the cached versions file + install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + source "#{source_uri}" + gem 'myrack', '0.9.1' G - # Second install should make only a partial request to /versions - bundle! :install, :artifice => "compact_index_partial_update" + build_repo4 do + build_gem "myrack", "1.0.0" + end + + install_gemfile <<-G, artifice: "compact_index_partial_update", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + source "#{source_uri}" + gem 'myrack', '1.0.0' + G - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "performs partial update while local cache is updated by another process" do gemfile <<-G source "#{source_uri}" - gem 'rack' + gem 'myrack' 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) + # Create a partial cache versions file + versions = compact_index_cache_path.join( + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions" + ) + versions.dirname.mkpath + versions.write("created_at") - bundle! :install, :artifice => "compact_index_concurrent_download" + 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" + expect(versions.read).to start_with("created_at") + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "performs a partial update that fails digest check, then a full update" do + build_repo4 do + build_gem "myrack", "0.9.1" + end + + install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + source "#{source_uri}" + gem 'myrack', '0.9.1' + G + + build_repo4 do + build_gem "myrack", "1.0.0" + end + + install_gemfile <<-G, artifice: "compact_index_partial_update_bad_digest", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + source "#{source_uri}" + gem 'myrack', '1.0.0' + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "performs full update if server endpoints serve partial content responses but don't have incremental content and provide no digest" do + build_repo4 do + build_gem "myrack", "0.9.1" + end + + install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + source "#{source_uri}" + gem 'myrack', '0.9.1' + G + + build_repo4 do + build_gem "myrack", "1.0.0" + end + + install_gemfile <<-G, artifice: "compact_index_partial_update_no_digest_not_incremental", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + source "#{source_uri}" + gem 'myrack', '1.0.0' + G + + expect(the_bundle).to include_gems "myrack 1.0.0" end it "performs full update of compact index info cache if range is not satisfiable" do gemfile <<-G source "#{source_uri}" - gem 'rack', '0.9.1' + gem 'myrack', '0.9.1' G - rake_info_path = File.join(Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", - "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "info", "rack") + bundle :install, artifice: "compact_index" - bundle! :install, :artifice => "compact_index" + cache_path = compact_index_cache_path.join("localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5") - expected_rack_info_content = File.read(rake_info_path) + # We must remove the etag so that we don't ignore the range and get a 304 Not Modified. + myrack_info_etag_path = File.join(cache_path, "info-etags", "myrack-92f3313ce5721296f14445c3a6b9c073") + File.unlink(myrack_info_etag_path) if File.exist?(myrack_info_etag_path) - # Modify the cache files. We expect them to be reset to the normal ones when we re-run :install - File.open(rake_info_path, "w") {|f| f << (expected_rack_info_content + "this is different") } + myrack_info_path = File.join(cache_path, "info", "myrack") + expected_myrack_info_content = File.read(myrack_info_path) + + # Modify the cache files to make the range not satisfiable + File.open(myrack_info_path, "a") {|f| f << "0.9.2 |checksum:c55b525b421fd833a93171ad3d7f04528ca8e87d99ac273f8933038942a5888c" } # Update the Gemfile so the next install does its normal things gemfile <<-G source "#{source_uri}" - gem 'rack', '1.0.0' + gem 'myrack', '1.0.0' G # The cache files now being longer means the requested range is going to be not satisfiable # Bundler must end up requesting the whole file to fix things up. - bundle! :install, :artifice => "compact_index_range_not_satisfiable" + bundle :install, artifice: "compact_index_range_not_satisfiable" - resulting_rack_info_content = File.read(rake_info_path) + resulting_myrack_info_content = File.read(myrack_info_path) - expect(resulting_rack_info_content).to eq(expected_rack_info_content) + expect(resulting_myrack_info_content).to eq(expected_myrack_info_content) end it "fails gracefully when the source URI has an invalid scheme" do - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false source "htps://rubygems.org" - gem "rack" + gem "myrack" G - expect(exitstatus).to eq(15) if exitstatus + expect(exitstatus).to eq(15) expect(err).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" do + before do + lockfile <<-L + GEM + remote: #{source_uri} + specs: + myrack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + #{checksums_section} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "handles checksums from the server in base64" do + api_checksum = checksum_digest(gem_repo1, "myrack", "1.0.0") + myrack_checksum = [[api_checksum].pack("H*")].pack("m0") + install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_MYRACK_CHECKSUM" => myrack_checksum } + source "#{source_uri}" + gem "myrack" + G + + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems("myrack 1.0.0") + end + it "raises when the checksum does not match" do - install_gemfile <<-G, :artifice => "compact_index_wrong_gem_checksum" + install_gemfile <<-G, artifice: "compact_index_wrong_gem_checksum", raise_on_error: false source "#{source_uri}" - gem "rack" + gem "myrack" G - expect(exitstatus).to eq(19) if exitstatus - expect(err). - 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: `#{default_bundle_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 set 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 ".+?"\.\)/) + gem_path = default_cache_path.dirname.join("myrack-1.0.0.gem") + + expect(exitstatus).to eq(37) + expect(err).to eq <<~E.strip + Bundler found mismatched checksums. This is a potential security risk. + myrack (1.0.0) sha256=2222222222222222222222222222222222222222222222222222222222222222 + from the API at http://localgemserver.test/ + #{checksum_to_lock(gem_repo1, "myrack", "1.0.0")} + from the gem at #{gem_path} + + If you trust the API at http://localgemserver.test/, to resolve this issue you can: + 1. remove the gem at #{gem_path} + 2. run `bundle install` + + To ignore checksum security warnings, disable checksum validation with + `bundle config set --local disable_checksum_validation true` + E 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!" } + install_gemfile <<-G, artifice: "compact_index_wrong_gem_checksum", env: { "BUNDLER_SPEC_MYRACK_CHECKSUM" => "checksum!", "DEBUG" => "1" }, verbose: true, raise_on_error: false source "#{source_uri}" - gem "rack" + gem "myrack" G - expect(exitstatus).to eq(5) if exitstatus - expect(err).to include("The given checksum for rack-1.0.0 (\"checksum!\") is not a valid SHA256 hexdigest nor base64digest") + expect(exitstatus).to eq(14) + expect(err).to include('Invalid checksum for myrack-0.9.1: "checksum!" is not a valid SHA256 hex or base64 digest') end it "does not raise when disable_checksum_validation is set" do - bundle! "config set disable_checksum_validation true" - install_gemfile! <<-G, :artifice => "compact_index_wrong_gem_checksum" + bundle_config "disable_checksum_validation true" + install_gemfile <<-G, artifice: "compact_index_wrong_gem_checksum" source "#{source_uri}" - gem "rack" + gem "myrack" G end end it "works when cache dir is world-writable" do - install_gemfile! <<-G, :artifice => "compact_index" + install_gemfile <<-G, artifice: "compact_index" File.umask(0000) source "#{source_uri}" - gem "rack" + gem "myrack" G end it "doesn't explode when the API dependencies are wrong" do - install_gemfile <<-G, :artifice => "compact_index_wrong_dependencies", :env => { "DEBUG" => "true" } + install_gemfile <<-G, artifice: "compact_index_wrong_dependencies", env: { "DEBUG" => "true" }, raise_on_error: false source "#{source_uri}" gem "rails" G - deps = [Gem::Dependency.new("rake", "= 12.3.2"), + deps = [Gem::Dependency.new("rake", "= #{rake_version}"), 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. + expect(out).to include("rails-2.3.2 from rubygems remote at #{source_uri}/ has corrupted API dependencies") + expect(err).to include(<<-E.strip) +Bundler::APIResponseMismatchError: Downloading rails-2.3.2 revealed dependencies not in the API (#{deps.map(&:to_s).join(", ")}). +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" + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo1" 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) + uninstall_gem("activemerchant") + bundle "update rails", artifice: "compact_index" + count = lockfile.match?("CHECKSUMS") ? 2 : 1 # Once in the specs, and once in CHECKSUMS + expect(lockfile.scan(/activemerchant \(/).size).to eq(count) + end + + it "handles an API that does not provide checksums info (undocumented, support may get removed)" do + install_gemfile <<-G, artifice: "compact_index_no_checksums" + source "https://gem.repo1" + gem "rake" + G end end diff --git a/spec/bundler/install/gems/dependency_api_fallback_spec.rb b/spec/bundler/install/gems/dependency_api_fallback_spec.rb new file mode 100644 index 0000000000..c7b0c537e4 --- /dev/null +++ b/spec/bundler/install/gems/dependency_api_fallback_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +RSpec.describe "gemcutter's dependency API" do + context "when Gemcutter API takes too long to respond" do + before do + bundle_config "timeout 1" + end + + it "times out and falls back on the modern index" do + install_gemfile <<-G, artifice: "endpoint_timeout" + source "https://gem.repo1" + gem "myrack" + G + + expect(out).to include("Fetching source index from https://gem.repo1/") + expect(the_bundle).to include_gems "myrack 1.0.0" + end + end +end diff --git a/spec/bundler/install/gems/dependency_api_spec.rb b/spec/bundler/install/gems/dependency_api_spec.rb index a8713eb445..32a1b98b6d 100644 --- a/spec/bundler/install/gems/dependency_api_spec.rb +++ b/spec/bundler/install/gems/dependency_api_spec.rb @@ -7,12 +7,12 @@ RSpec.describe "gemcutter's dependency API" do it "should use the API" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint" + bundle :install, artifice: "endpoint" expect(out).to include("Fetching gem metadata from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "should URI encode gem names" do @@ -21,7 +21,7 @@ RSpec.describe "gemcutter's dependency API" do gem " sinatra" G - bundle :install, :artifice => "endpoint" + bundle :install, artifice: "endpoint", raise_on_error: false expect(err).to include("' sinatra' is not a valid gem name because it contains whitespace.") end @@ -31,7 +31,7 @@ RSpec.describe "gemcutter's dependency API" do gem "rails" G - bundle :install, :artifice => "endpoint" + bundle :install, artifice: "endpoint" expect(out).to include("Fetching gem metadata from #{source_uri}/...") expect(the_bundle).to include_gems( "rails 2.3.2", @@ -49,20 +49,21 @@ RSpec.describe "gemcutter's dependency API" do gem "net-sftp" G - bundle :install, :artifice => "endpoint" + 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 + it "should use the endpoint when using deployment mode" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint" + bundle :install, artifice: "endpoint" - bundle! :install, forgotten_command_line_options(:deployment => true, :path => "vendor/bundle").merge(:artifice => "endpoint") + bundle_config "deployment true" + bundle :install, artifice: "endpoint" expect(out).to include("Fetching gem metadata from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "handles git dependencies that are in rubygems" do @@ -73,17 +74,17 @@ RSpec.describe "gemcutter's dependency API" do gemfile <<-G source "#{source_uri}" - git "#{file_uri_for(lib_path("foo-1.0"))}" do + git "#{lib_path("foo-1.0")}" do gem 'foo' end G - bundle :install, :artifice => "endpoint" + 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 + it "handles git dependencies that are in rubygems using deployment mode" do build_git "foo" do |s| s.executables = "foobar" s.add_dependency "rails", "2.3.2" @@ -91,40 +92,50 @@ RSpec.describe "gemcutter's dependency API" do gemfile <<-G source "#{source_uri}" - gem 'foo', :git => "#{file_uri_for(lib_path("foo-1.0"))}" + gem 'foo', :git => "#{lib_path("foo-1.0")}" G - bundle :install, :artifice => "endpoint" + bundle :install, artifice: "endpoint" - bundle "install --deployment", :artifice => "endpoint" + bundle_config "deployment true" + bundle :install, 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 + it "doesn't fail if you only have a git gem with no deps when using deployment mode" do build_git "foo" gemfile <<-G source "#{source_uri}" - gem 'foo', :git => "#{file_uri_for(lib_path("foo-1.0"))}" + gem 'foo', :git => "#{lib_path("foo-1.0")}" G - bundle "install", :artifice => "endpoint" - bundle! :install, forgotten_command_line_options(:deployment => true).merge(:artifice => "endpoint") + bundle "install", artifice: "endpoint" + bundle_config "deployment true" + bundle :install, artifice: "endpoint" expect(the_bundle).to include_gems("foo 1.0") end it "falls back when the API errors out" do - simulate_platform mswin + simulate_platform "x86-mswin32" do + build_repo2 do + # 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 + end - gemfile <<-G - source "#{source_uri}" - gem "rcov" - G + 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" + bundle :install, artifice: "windows", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s } + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "rcov 1.0.0" + end end it "falls back when hitting the Gemcutter Dependency Limit" do @@ -135,10 +146,10 @@ RSpec.describe "gemcutter's dependency API" do gem "actionmailer" gem "activeresource" gem "thin" - gem "rack" + gem "myrack" gem "rails" G - bundle :install, :artifice => "endpoint_fallback" + bundle :install, artifice: "endpoint_fallback" expect(out).to include("Fetching source index from #{source_uri}") expect(the_bundle).to include_gems( @@ -148,7 +159,7 @@ RSpec.describe "gemcutter's dependency API" do "activeresource 2.3.2", "activesupport 2.3.2", "thin 1.0.0", - "rack 1.0.0", + "myrack 1.0.0", "rails 2.3.2" ) end @@ -156,39 +167,39 @@ RSpec.describe "gemcutter's dependency API" do it "falls back when Gemcutter API doesn't return proper Marshal format" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :verbose => true, :artifice => "endpoint_marshal_fail" + 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" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "falls back when the API URL returns 403 Forbidden" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :verbose => true, :artifice => "endpoint_api_forbidden" + 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" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "handles host redirects" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint_host_redirect" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "endpoint_host_redirect" + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "handles host redirects without Net::HTTP::Persistent" do + it "handles host redirects without Gem::Net::HTTP::Persistent" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G FileUtils.mkdir_p lib_path @@ -204,18 +215,18 @@ RSpec.describe "gemcutter's dependency API" do H end - bundle :install, :artifice => "endpoint_host_redirect", :requires => [lib_path("disable_net_http_persistent.rb")] + 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" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "timeouts when Bundler::Fetcher redirects too much" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint_redirect" + bundle :install, artifice: "endpoint_redirect", raise_on_error: false expect(err).to match(/Too many redirects/) end @@ -223,50 +234,32 @@ RSpec.describe "gemcutter's dependency API" do it "should use the modern index for install" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle "install --full-index", :artifice => "endpoint" + 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" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "should use the modern index for update" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle! "update --full-index", :artifice => "endpoint", :all => true + bundle "update --full-index", artifice: "endpoint", all: true 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", :bundler => "< 3" do - build_repo2 do - build_gem "back_deps" do |s| - s.add_dependency "foo" - end - FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + expect(the_bundle).to include_gems "myrack 1.0.0" 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", "foo 1.0" end - it "fetches again when more dependencies are found in subsequent sources using blocks" do + 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")] + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] end gemfile <<-G @@ -276,61 +269,39 @@ RSpec.describe "gemcutter's dependency API" do end G - bundle :install, :artifice => "endpoint_extra" + bundle :install, artifice: "endpoint_extra" expect(the_bundle).to include_gems "back_deps 1.0", "foo 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" + gem "myrack", "1.0.0" G - bundle :install, :artifice => "endpoint_extra_api" + bundle :install, artifice: "endpoint_extra_api" build_repo4 do - build_gem "rack", "1.2" do |s| - s.executables = "rackup" + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" 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", :bundler => "< 3" 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' + gem "myrack", "1.2" 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" + bundle :install, artifice: "endpoint_extra_api" + expect(the_bundle).to include_gems "myrack 1.2" end - it "considers all possible versions of dependencies from all api gem sources using blocks" 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. + it "resolves indirect dependencies to the most scoped source that includes them" 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 tries to + # use the version in the most scoped source, even if not compatible, and + # gives a resolution error build_repo4 do build_gem "activesupport", "1.2.0" build_gem "somegem", "1.0.0" do |s| @@ -345,10 +316,9 @@ RSpec.describe "gemcutter's dependency API" do end G - bundle :install, :artifice => "endpoint_extra_api" + bundle :install, artifice: "compact_index_extra_api", raise_on_error: false - expect(the_bundle).to include_gems "somegem 1.0.0" - expect(the_bundle).to include_gems "activesupport 1.2.3" + expect(err).to include("Could not find compatible versions") end it "prints API output properly with back deps" do @@ -356,7 +326,7 @@ RSpec.describe "gemcutter's dependency API" do build_gem "back_deps" do |s| s.add_dependency "foo" end - FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] end gemfile <<-G @@ -366,48 +336,38 @@ RSpec.describe "gemcutter's dependency API" do end G - bundle :install, :artifice => "endpoint_extra" + 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", :bundler => "< 3" do + it "does not fetch every spec 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")] + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] end - gemfile <<-G + install_gemfile <<-G, artifice: "endpoint_extra_missing" source "#{source_uri}" - source "#{source_uri}/extra" - gem "back_deps" + source "#{source_uri}/extra" do + gem "back_deps" + end G - bundle :install, :artifice => "endpoint_extra_missing" expect(the_bundle).to include_gems "back_deps 1.0" end - it "does not fetch every spec if the index of gems is large when doing back deps using blocks" do + it "fetches again when more dependencies are found in subsequent sources using deployment mode" 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")] + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] end gemfile <<-G @@ -417,145 +377,71 @@ RSpec.describe "gemcutter's dependency API" do end G - bundle :install, :artifice => "endpoint_extra_missing" + bundle :install, artifice: "endpoint_extra" + bundle_config "deployment true" + bundle "install", artifice: "endpoint_extra" 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", :bundler => "< 3" do + it "does not fetch all marshaled specs" do build_repo2 do - build_gem "back_deps" do |s| - s.add_dependency "foo" - end - FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + build_gem "foo", "1.0" + build_gem "foo", "2.0" end - gemfile <<-G + install_gemfile <<-G, artifice: "endpoint", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }, verbose: true source "#{source_uri}" - source "#{source_uri}/extra" - gem "back_deps" - G - bundle :install, :artifice => "endpoint_extra" + gem "foo" + G - bundle "install --deployment", :artifice => "endpoint_extra" - expect(the_bundle).to include_gems "back_deps 1.0" + expect(out).to include("foo-2.0.gemspec.rz") + expect(out).not_to include("foo-1.0.gemspec.rz") end - it "fetches again when more dependencies are found in subsequent sources using --deployment with blocks" do + it "does not refetch if the only unmet dependency is bundler" do build_repo2 do - build_gem "back_deps" do |s| - s.add_dependency "foo" + build_gem "bundler_dep" do |s| + s.add_dependency "bundler" end - FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] end gemfile <<-G source "#{source_uri}" - source "#{source_uri}/extra" do - gem "back_deps" - end - 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" + bundle :install, artifice: "endpoint", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s } 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", :bundler => "< 3" 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", :bundler => "< 3" 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", :bundler => "< 3" 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' + gem 'myrack-obama' G - bundle :install, :artifice => "endpoint" - expect(out).to include("Post-install message from rack:") + bundle :install, artifice: "endpoint" + expect(out).to include("Post-install message from myrack:") end it "should display the post install message for a dependency" do gemfile <<-G source "#{source_uri}" - gem 'rack_middleware' + gem 'myrack_middleware' G - bundle :install, :artifice => "endpoint" - expect(out).to include("Post-install message from rack:") - expect(out).to include("Rack's post install message") + bundle :install, artifice: "endpoint" + expect(out).to include("Post-install message from myrack:") + expect(out).to include("Myrack'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 = Gem::URI.parse(source_uri) uri.user = user uri.password = password @@ -565,113 +451,127 @@ RSpec.describe "gemcutter's dependency API" do it "passes basic authentication details and strips out creds" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint_basic_authentication" + bundle :install, artifice: "endpoint_basic_authentication" expect(out).not_to include("#{user}:#{password}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "strips http basic authentication creds for modern index" do + it "passes basic authentication details and strips out creds also in verbose mode" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endopint_marshal_fail_basic_authentication" + bundle :install, verbose: true, artifice: "endpoint_basic_authentication" expect(out).not_to include("#{user}:#{password}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "strips http basic auth creds when it can't reach the server" do + it "strips http basic authentication creds for modern index" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint_500" + bundle :install, artifice: "endpoint_marshal_fail_basic_authentication" expect(out).not_to include("#{user}:#{password}") + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "strips http basic auth creds when warning about ambiguous sources", :bundler => "< 3" do + it "strips http basic auth creds when it can't reach the server" do gemfile <<-G source "#{basic_auth_source_uri}" - source "#{file_uri_for(gem_repo1)}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint_basic_authentication" - expect(err).to include("Warning: the gem 'rack' was found in multiple sources.") - expect(err).not_to include("#{user}:#{password}") - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "endpoint_500", raise_on_error: false + expect(out).not_to include("#{user}:#{password}") end it "does not pass the user / password to different hosts on redirect" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint_creds_diff_host" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "endpoint_creds_diff_host" + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + describe "with host including dashes" do + before do + gemfile <<-G + source "http://local-gemserver.test" + gem "myrack" + G + end + + it "reads authentication details from a valid ENV variable" do + bundle :install, artifice: "endpoint_strict_basic_authentication", env: { "BUNDLE_LOCAL___GEMSERVER__TEST" => "#{user}:#{password}" } + + expect(out).to include("Fetching gem metadata from http://local-gemserver.test") + expect(the_bundle).to include_gems "myrack 1.0.0" + end end describe "with authentication details in bundle config" do before do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G end it "reads authentication details by host name from bundle config" do bundle "config set #{source_hostname} #{user}:#{password}" - bundle :install, :artifice => "endpoint_strict_basic_authentication" + 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" + expect(the_bundle).to include_gems "myrack 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 set #{source_uri}/ #{user}:#{password}" - bundle :install, :artifice => "endpoint_strict_basic_authentication" + 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" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "should use the API" do bundle "config set #{source_hostname} #{user}:#{password}" - bundle :install, :artifice => "endpoint_strict_basic_authentication" + 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" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "prefers auth supplied in the source uri" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G bundle "config set #{source_hostname} otheruser:wrong" - bundle :install, :artifice => "endpoint_strict_basic_authentication" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "endpoint_strict_basic_authentication" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "shows instructions if auth is not provided for the source" do - bundle :install, :artifice => "endpoint_strict_basic_authentication" - expect(err).to include("bundle config set #{source_hostname} username:password") + bundle :install, artifice: "endpoint_strict_basic_authentication", raise_on_error: false + expect(err).to include("bundle config set --global #{source_hostname} username:password") end it "fails if authentication has already been provided, but failed" do bundle "config set #{source_hostname} #{user}:wrong" - bundle :install, :artifice => "endpoint_strict_basic_authentication" + bundle :install, artifice: "endpoint_strict_basic_authentication", raise_on_error: false expect(err).to include("Bad username or password") end end @@ -682,11 +582,11 @@ RSpec.describe "gemcutter's dependency API" do it "passes basic authentication details" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint_basic_authentication" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "endpoint_basic_authentication" + expect(the_bundle).to include_gems "myrack 1.0.0" end end end @@ -704,14 +604,14 @@ RSpec.describe "gemcutter's dependency API" do end end - it "explains what to do to get it" do + it "explains what to do to get it, and includes original error" do gemfile <<-G source "#{source_uri.gsub(/http/, "https")}" - gem "rack" + gem "myrack" G - bundle :install, :env => { "RUBYOPT" => "-I#{bundled_app("broken_ssl")}" } - expect(err).to include("OpenSSL") + bundle :install, artifice: "fail", env: { "RUBYOPT" => "-I#{bundled_app("broken_ssl")}" }, raise_on_error: false + expect(err).to include("recompile Ruby").and include("cannot load such file") end end @@ -720,41 +620,37 @@ RSpec.describe "gemcutter's dependency API" 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 + class Gem::Net::HTTP def start raise OpenSSL::SSL::SSLError, "certificate verify failed" end end source "#{source_uri.gsub(/http/, "https")}" - gem "rack" + gem "myrack" G - bundle :install + bundle :install, raise_on_error: false expect(err).to match(/could not verify the SSL certificate/i) end end context ".gemrc with sources is present" do - before do + it "uses other sources declared in the Gemfile" do File.open(home(".gemrc"), "w") do |file| - file.puts({ :sources => ["https://rubygems.org"] }.to_yaml) + 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" + begin + gemfile <<-G + source "#{source_uri}" + gem 'myrack' + G - expect(exitstatus).to eq(0) if exitstatus + bundle "install", artifice: "endpoint_marshal_fail" + ensure + FileUtils.rm_rf home(".gemrc") + end end end end diff --git a/spec/bundler/install/gems/env_spec.rb b/spec/bundler/install/gems/env_spec.rb index a6dfadcfc8..6d5aa456fe 100644 --- a/spec/bundler/install/gems/env_spec.rb +++ b/spec/bundler/install/gems/env_spec.rb @@ -4,104 +4,104 @@ 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_uri_for(gem_repo1)}" + source "https://gem.repo1" env "BUNDLER_TEST" do - gem "rack" + gem "myrack" 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" + expect(the_bundle).not_to include_gems "myrack" 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" + expect(the_bundle).to include_gems "myrack 1.0" end end describe "when just setting an ENV key as a symbol" do before :each do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" env :BUNDLER_TEST do - gem "rack" + gem "myrack" 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" + expect(the_bundle).not_to include_gems "myrack" 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" + expect(the_bundle).to include_gems "myrack 1.0" end end describe "when setting a string to match the env" do before :each do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" env "BUNDLER_TEST" => "foo" do - gem "rack" + gem "myrack" 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" + expect(the_bundle).not_to include_gems "myrack" 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" + expect(the_bundle).not_to include_gems "myrack" 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" + expect(the_bundle).to include_gems "myrack 1.0" end end describe "when setting a regex to match the env" do before :each do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" env "BUNDLER_TEST" => /foo/ do - gem "rack" + gem "myrack" 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" + expect(the_bundle).not_to include_gems "myrack" 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" + expect(the_bundle).not_to include_gems "myrack" 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" + expect(the_bundle).to include_gems "myrack 1.0" end end end diff --git a/spec/bundler/install/gems/flex_spec.rb b/spec/bundler/install/gems/flex_spec.rb index 865bc7b72a..a30b53d6ad 100644 --- a/spec/bundler/install/gems/flex_spec.rb +++ b/spec/bundler/install/gems/flex_spec.rb @@ -3,30 +3,30 @@ RSpec.describe "bundle flex_install" do it "installs the gems as expected" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' G - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" expect(the_bundle).to be_locked end it "installs even when the lockfile is invalid" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' G - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" expect(the_bundle).to be_locked gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack', '1.0' + source "https://gem.repo1" + gem 'myrack', '1.0' G bundle :install - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" expect(the_bundle).to be_locked end @@ -34,19 +34,19 @@ RSpec.describe "bundle flex_install" do build_repo2 install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack-obama" + source "https://gem.repo2" + gem "myrack-obama" G - expect(the_bundle).to include_gems "rack 1.0.0", "rack-obama 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0.0" update_repo2 install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack-obama", "1.0" + source "https://gem.repo2" + gem "myrack-obama", "1.0" G - expect(the_bundle).to include_gems "rack 1.0.0", "rack-obama 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0.0" end describe "adding new gems" do @@ -54,38 +54,38 @@ RSpec.describe "bundle flex_install" do build_repo2 install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem 'rack' + source "https://gem.repo2" + gem 'myrack' G update_repo2 install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem 'rack' + source "https://gem.repo2" + gem 'myrack' gem 'activesupport', '2.3.5' G - expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.5" + expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.5" end it "keeps child dependencies pinned" do build_repo2 install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack-obama" + source "https://gem.repo2" + gem "myrack-obama" G update_repo2 install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack-obama" + source "https://gem.repo2" + gem "myrack-obama" gem "thin" G - expect(the_bundle).to include_gems "rack 1.0.0", "rack-obama 1.0", "thin 1.0" + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0", "thin 1.0" end end @@ -93,43 +93,43 @@ RSpec.describe "bundle flex_install" do it "removes gems without changing the versions of remaining gems" do build_repo2 install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem 'rack' + source "https://gem.repo2" + gem 'myrack' gem 'activesupport', '2.3.5' G update_repo2 install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem 'rack' + source "https://gem.repo2" + gem 'myrack' G - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" expect(the_bundle).not_to include_gems "activesupport 2.3.5" install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem 'rack' + source "https://gem.repo2" + gem 'myrack' gem 'activesupport', '2.3.2' G - expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.2" + expect(the_bundle).to include_gems "myrack 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_uri_for(gem_repo2)}" - gem 'rack' + source "https://gem.repo2" + gem 'myrack' gem 'activesupport', '2.3.5' G update_repo2 install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem 'rack' + source "https://gem.repo2" + gem 'myrack' G expect(the_bundle).not_to include_gems "activesupport 2.3.5" @@ -138,176 +138,227 @@ RSpec.describe "bundle flex_install" do it "removes child dependencies" do build_repo2 install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem 'rack-obama' + source "https://gem.repo2" + gem 'myrack-obama' gem 'activesupport' G - expect(the_bundle).to include_gems "rack 1.0.0", "rack-obama 1.0.0", "activesupport 2.3.5" + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0.0", "activesupport 2.3.5" update_repo2 install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://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" + expect(the_bundle).not_to include_gems "myrack-obama", "myrack" end end - describe "when Gemfile conflicts with lockfile" do + describe "when running bundle install and Gemfile conflicts with lockfile" do before(:each) do build_repo2 install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack_middleware" + source "https://gem.repo2" + gem "myrack_middleware" G - expect(the_bundle).to include_gems "rack_middleware 1.0", "rack 0.9.1" + expect(the_bundle).to include_gems "myrack_middleware 1.0", "myrack 0.9.1" - build_repo2 - update_repo2 do - build_gem "rack-obama", "2.0" do |s| - s.add_dependency "rack", "=1.2" + build_repo2 do + build_gem "myrack-obama", "2.0" do |s| + s.add_dependency "myrack", "=1.2" end - build_gem "rack_middleware", "2.0" do |s| - s.add_dependency "rack", ">=1.0" + build_gem "myrack_middleware", "2.0" do |s| + s.add_dependency "myrack", ">=1.0" end end gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack-obama", "2.0" - gem "rack_middleware" + source "https://gem.repo2" + gem "myrack-obama", "2.0" + gem "myrack_middleware" G end it "does not install gems whose dependencies are not met" do - bundle :install - ruby <<-RUBY + bundle :install, raise_on_error: false + ruby <<-RUBY, raise_on_error: false require 'bundler/setup' RUBY - expect(err).to match(/could not find gem 'rack-obama/i) + expect(err).to match(/could not find gem 'myrack-obama/i) end - it "suggests bundle update when the Gemfile requires different versions than the lock" do - bundle "config set force_ruby_platform true" + it "discards the locked gems when the Gemfile requires different versions than the lock" do + bundle_config "force_ruby_platform true" - nice_error = <<-E.strip.gsub(/^ {8}/, "") - Bundler could not find compatible versions for gem "rack": - In snapshot (Gemfile.lock): - rack (= 0.9.1) + nice_error = <<~E.strip + Could not find compatible versions - In Gemfile: - rack-obama (= 2.0) was resolved to 2.0, which depends on - rack (= 1.2) + Because myrack-obama >= 2.0 depends on myrack = 1.2 + and myrack = 1.2 could not be found in rubygems repository https://gem.repo2/ or installed locally, + myrack-obama >= 2.0 cannot be used. + So, because Gemfile depends on myrack-obama = 2.0, + version solving has failed. + E - rack_middleware was resolved to 1.0, which depends on - rack (= 0.9.1) + bundle :install, retry: 0, raise_on_error: false + expect(err).to end_with(nice_error) + end - Running `bundle update` will rebuild your snapshot from scratch, using only - the gems in your Gemfile, which may resolve the conflict. + it "does not include conflicts with a single requirement tree, because that can't possibly be a conflict" do + bundle_config "force_ruby_platform true" + + bad_error = <<~E.strip + Bundler could not find compatible versions for gem "myrack-obama": + In Gemfile: + myrack-obama (= 2.0) E - bundle :install, :retry => 0 - expect(err).to end_with(nice_error) + bundle "update myrack_middleware", retry: 0, raise_on_error: false + expect(err).not_to end_with(bad_error) end end - describe "subtler cases" do - before :each do + describe "when running bundle update and Gemfile conflicts with lockfile" do + before(:each) do + build_repo4 do + build_gem "jekyll-feed", "0.16.0" + build_gem "jekyll-feed", "0.15.1" + + build_gem "github-pages", "226" do |s| + s.add_dependency "jekyll-feed", "0.15.1" + end + end + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - gem "rack-obama" + source "https://gem.repo4" + gem "jekyll-feed", "~> 0.12" G gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "0.9.1" - gem "rack-obama" + source "https://gem.repo4" + gem "github-pages", "~> 226" + gem "jekyll-feed", "~> 0.12" G end - it "does something" do - expect do - bundle "install" - end.not_to change { File.read(bundled_app("Gemfile.lock")) } - - expect(err).to include("rack = 0.9.1") - expect(err).to include("locked at 1.0.0") - expect(err).to include("bundle update rack") - end - - it "should work when you update" do - bundle "update rack" + it "discards the conflicting lockfile information and resolves properly" do + bundle :update, raise_on_error: false, all: true + expect(err).to be_empty end end - describe "when adding a new source" do - it "updates the lockfile", :bundler => "< 3" do - build_repo2 - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + describe "subtler cases" do + before :each do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" G - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - source "#{file_uri_for(gem_repo2)}" - gem "rack" + + gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + gem "myrack-obama" G + end + + it "should work when you install" do + bundle "install" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo1, "myrack", "0.9.1" + c.checksum gem_repo1, "myrack-obama", "1.0" + end - lockfile_should_be <<-L - GEM - remote: #{file_uri_for(gem_repo1)}/ - remote: #{file_uri_for(gem_repo2)}/ - specs: - rack (1.0.0) + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + myrack (0.9.1) + myrack-obama (1.0) + myrack + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack (= 0.9.1) + myrack-obama + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end - PLATFORMS - #{lockfile_platforms} + it "should work when you update" do + bundle "update myrack" - DEPENDENCIES - rack + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo1, "myrack", "0.9.1" + c.checksum gem_repo1, "myrack-obama", "1.0" + end - BUNDLED WITH - #{Bundler::VERSION} + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + myrack (0.9.1) + myrack-obama (1.0) + myrack + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack (= 0.9.1) + myrack-obama + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} L end + end - it "updates the lockfile", :bundler => "3" do + describe "when adding a new source" do + it "updates the lockfile" do build_repo2 - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" G - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - source "#{file_uri_for(gem_repo2)}" do + install_gemfile <<-G + source "https://gem.repo1" + source "https://gem.repo2" do end - gem "rack" + gem "myrack" G - lockfile_should_be <<-L - GEM - remote: #{file_uri_for(gem_repo1)}/ - specs: - rack (1.0.0) + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo1, "myrack", "1.0.0" + end - GEM - remote: #{file_uri_for(gem_repo2)}/ - specs: + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + myrack (1.0.0) - PLATFORMS - #{lockfile_platforms} + GEM + remote: https://gem.repo2/ + specs: - DEPENDENCIES - rack + PLATFORMS + #{lockfile_platforms} - BUNDLED WITH - #{Bundler::VERSION} + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} L end end @@ -317,37 +368,36 @@ RSpec.describe "bundle flex_install" do before(:each) do build_repo2 do build_gem "capybara", "0.3.9" do |s| - s.add_dependency "rack", ">= 1.0.0" + s.add_dependency "myrack", ">= 1.0.0" end - build_gem "rack", "1.1.0" + build_gem "myrack", "1.1.0" build_gem "rails", "3.0.0.rc4" do |s| - s.add_dependency "rack", "~> 1.1.0" + s.add_dependency "myrack", "~> 1.1.0" end - build_gem "rack", "1.2.1" + build_gem "myrack", "1.2.1" build_gem "rails", "3.0.0" do |s| - s.add_dependency "rack", "~> 1.2.1" + s.add_dependency "myrack", "~> 1.2.1" end end end - it "prints the correct error message" do + it "resolves them" do # install Rails 3.0.0.rc install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://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_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "rails", "3.0.0" gem "capybara", "0.3.9" G - - expect(err).to include("Gemfile.lock") + expect(err).to be_empty end end end diff --git a/spec/bundler/install/gems/fund_spec.rb b/spec/bundler/install/gems/fund_spec.rb new file mode 100644 index 0000000000..8a3a51270a --- /dev/null +++ b/spec/bundler/install/gems/fund_spec.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install" do + context "with gem sources" do + before do + build_repo2 do + build_gem "has_funding_and_other_metadata" do |s| + s.metadata = { + "bug_tracker_uri" => "https://example.com/user/bestgemever/issues", + "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md", + "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1", + "homepage_uri" => "https://bestgemever.example.io", + "mailing_list_uri" => "https://groups.example.com/bestgemever", + "funding_uri" => "https://example.com/has_funding_and_other_metadata/funding", + "source_code_uri" => "https://example.com/user/bestgemever", + "wiki_uri" => "https://example.com/user/bestgemever/wiki", + } + end + + build_gem "has_funding", "1.2.3" do |s| + s.metadata = { + "funding_uri" => "https://example.com/has_funding/funding", + } + end + + build_gem "gem_with_dependent_funding", "1.0" do |s| + s.add_dependency "has_funding" + end + end + end + + context "when gems include a fund URI" do + it "displays the plural fund message after installing" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'has_funding_and_other_metadata' + gem 'has_funding' + gem 'myrack-obama' + G + + expect(out).to include("2 installed gems you directly depend on are looking for funding.") + end + + it "displays the singular fund message after installing" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'has_funding' + gem 'myrack-obama' + G + + expect(out).to include("1 installed gem you directly depend on is looking for funding.") + end + end + + context "when gems include a fund URI but `ignore_funding_requests` is configured" do + before do + bundle_config "ignore_funding_requests true" + end + + it "does not display the plural fund message after installing" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'has_funding_and_other_metadata' + gem 'has_funding' + gem 'myrack-obama' + G + + expect(out).not_to include("2 installed gems you directly depend on are looking for funding.") + end + + it "does not display the singular fund message after installing" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'has_funding' + gem 'myrack-obama' + G + + expect(out).not_to include("1 installed gem you directly depend on is looking for funding.") + end + end + + context "when gems do not include fund messages" do + it "does not display any fund messages" do + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + G + + expect(out).not_to include("gem you depend on") + end + end + + context "when a dependency includes a fund message" do + it "does not display the fund message" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'gem_with_dependent_funding' + G + + expect(out).not_to include("gem you depend on") + end + end + end + + context "with git sources" do + context "when gems include fund URI" do + it "displays the fund message after installing" do + build_git "also_has_funding" do |s| + s.metadata = { + "funding_uri" => "https://example.com/also_has_funding/funding", + } + end + install_gemfile <<-G + source "https://gem.repo1" + gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.0")}' + G + + expect(out).to include("1 installed gem you directly depend on is looking for funding.") + end + + it "displays the fund message if repo is updated" do + build_git "also_has_funding" do |s| + s.metadata = { + "funding_uri" => "https://example.com/also_has_funding/funding", + } + end + install_gemfile <<-G + source "https://gem.repo1" + gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.0")}' + G + + build_git "also_has_funding", "1.1" do |s| + s.metadata = { + "funding_uri" => "https://example.com/also_has_funding/funding", + } + end + install_gemfile <<-G + source "https://gem.repo1" + gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.1")}' + G + + expect(out).to include("1 installed gem you directly depend on is looking for funding.") + end + + it "displays the fund message if repo is not updated" do + build_git "also_has_funding" do |s| + s.metadata = { + "funding_uri" => "https://example.com/also_has_funding/funding", + } + end + gemfile <<-G + source "https://gem.repo1" + gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.0")}' + G + + bundle :install + expect(out).to include("1 installed gem you directly depend on is looking for funding.") + + bundle :install + expect(out).to include("1 installed gem you directly depend on is looking for funding.") + end + end + end +end diff --git a/spec/bundler/install/gems/gemfile_source_header_spec.rb b/spec/bundler/install/gems/gemfile_source_header_spec.rb new file mode 100644 index 0000000000..dc35c8d741 --- /dev/null +++ b/spec/bundler/install/gems/gemfile_source_header_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.describe "fetching dependencies with a mirrored source" do + let(:mirror) { "https://server.example.org" } + + before do + build_repo2 + + gemfile <<-G + source "#{mirror}" + gem 'weakling' + G + + bundle_config "mirror.#{mirror} https://gem.repo2" + end + + it "sets the 'X-Gemfile-Source' and 'User-Agent' headers and bundles successfully" do + bundle :install, artifice: "endpoint_mirror_source" + + expect(out).to include("Installing weakling") + expect(out).to include("Bundle complete") + expect(the_bundle).to include_gems "weakling 0.0.3" + end +end diff --git a/spec/bundler/install/gems/mirror_probe_spec.rb b/spec/bundler/install/gems/mirror_probe_spec.rb new file mode 100644 index 0000000000..564062ccf6 --- /dev/null +++ b/spec/bundler/install/gems/mirror_probe_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +RSpec.describe "fetching dependencies with a not available mirror" do + before do + build_repo2 + + gemfile <<-G + source "https://gem.repo2" + gem 'weakling' + G + end + + context "with a specific fallback timeout" do + before do + bundle_config_global("BUNDLE_MIRROR__HTTPS://GEM__REPO2/__FALLBACK_TIMEOUT/" => "true", + "BUNDLE_MIRROR__HTTPS://GEM__REPO2/" => "https://gem.mirror") + end + + it "install a gem using the original uri when the mirror is not responding" do + bundle :install, env: { "BUNDLER_SPEC_FAKE_RESOLVE" => "gem.mirror" }, verbose: true + + 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 + bundle_config_global("BUNDLE_MIRROR__ALL__FALLBACK_TIMEOUT/" => "1", + "BUNDLE_MIRROR__ALL" => "https://gem.mirror") + end + + it "install a gem using the original uri when the mirror is not responding" do + bundle :install, env: { "BUNDLER_SPEC_FAKE_RESOLVE" => "gem.mirror" } + + 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 + bundle_config_global("BUNDLE_MIRROR__HTTPS://GEM__REPO2/" => "https://gem.mirror") + end + + it "fails to install the gem with a timeout error when the mirror is not responding" do + bundle :install, artifice: "compact_index_mirror_down", raise_on_error: false + + expect(out).to be_empty + expect(err).to eq("Could not reach host gem.mirror. Check your network connection and try again.") + end + end + + context "with a global mirror without a fallback timeout" do + before do + bundle_config_global("BUNDLE_MIRROR__ALL" => "https://gem.mirror") + end + + it "fails to install the gem with a timeout error when the mirror is not responding" do + bundle :install, artifice: "compact_index_mirror_down", raise_on_error: false + + expect(out).to be_empty + expect(err).to eq("Could not reach host gem.mirror. Check your network connection and try again.") + end + end +end diff --git a/spec/bundler/install/gems/mirror_spec.rb b/spec/bundler/install/gems/mirror_spec.rb index 9611973701..e1fbeac454 100644 --- a/spec/bundler/install/gems/mirror_spec.rb +++ b/spec/bundler/install/gems/mirror_spec.rb @@ -4,17 +4,17 @@ 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_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack" + gem "myrack" G - bundle "config set --local mirror.http://gems.example.org http://gem-mirror.example.org" + bundle_config "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_uri_for(gem_repo1)}") - expect(the_bundle).to include_gems "rack 1.0" + expect(out).to include("Fetching gem metadata from https://gem.repo1") + expect(the_bundle).to include_gems "myrack 1.0" end end @@ -22,18 +22,18 @@ RSpec.describe "bundle install with a mirror configured" do before :each do gemfile <<-G # This source is bogus and doesn't have the gem we're looking for - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" - gem "rack" + gem "myrack" G - bundle "config set --local mirror.#{file_uri_for(gem_repo2)} #{file_uri_for(gem_repo1)}" + bundle_config "mirror.https://gem.repo2 https://gem.repo1" end it "installs the gem from the mirror" do - bundle :install - expect(out).to include("Fetching source index from #{file_uri_for(gem_repo1)}") - expect(out).not_to include("Fetching source index from #{file_uri_for(gem_repo2)}") - expect(the_bundle).to include_gems "rack 1.0" + bundle :install, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s } + expect(out).to include("Fetching gem metadata from https://gem.repo1") + expect(out).not_to include("Fetching gem metadata from https://gem.repo2") + expect(the_bundle).to include_gems "myrack 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 index 3e59a3cebd..d5b10d2c8f 100644 --- a/spec/bundler/install/gems/native_extensions_spec.rb +++ b/spec/bundler/install/gems/native_extensions_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe "installing a gem with native extensions", :ruby_repo do +RSpec.describe "installing a gem with native extensions" do it "installs" do build_repo2 do build_gem "c_extension" do |s| @@ -9,7 +9,7 @@ RSpec.describe "installing a gem with native extensions", :ruby_repo do require "mkmf" name = "c_extension_bundle" dir_config(name) - raise "OMG" unless with_config("c_extension") == "hello" + raise ArgumentError unless with_config("c_extension") == "hello" create_makefile(name) E @@ -33,14 +33,13 @@ RSpec.describe "installing a gem with native extensions", :ruby_repo do end gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "c_extension" G - bundle "config set build.c_extension --with-c_extension=hello" + 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" @@ -54,7 +53,7 @@ RSpec.describe "installing a gem with native extensions", :ruby_repo do require "mkmf" name = "c_extension_bundle" dir_config(name) - raise "OMG" unless with_config("c_extension") == "hello" + raise ArgumentError unless with_config("c_extension") == "hello" create_makefile(name) E @@ -76,18 +75,73 @@ RSpec.describe "installing a gem with native extensions", :ruby_repo do C end - bundle! "config set build.c_extension --with-c_extension=hello" + bundle_config "build.c_extension --with-c_extension=hello" - install_gemfile! <<-G + install_gemfile <<-G + source "https://gem.repo1" gem "c_extension", :git => #{lib_path("c_extension-1.0").to_s.dump} G - expect(out).not_to include("extconf.rb failed") + expect(err).to_not include("warning: conflicting chdir during another chdir block") - run! "Bundler.require; puts CExtension.new.its_true" + run "Bundler.require; puts CExtension.new.its_true" expect(out).to eq("true") end + it "installs correctly from git when multiple gems with extensions share one repository" do + build_repo2 do + ["one", "two"].each do |n| + build_lib "c_extension_#{n}", "1.0", path: lib_path("gems/c_extension_#{n}") do |s| + s.extensions = ["ext/extconf.rb"] + s.write "ext/extconf.rb", <<-E + require "mkmf" + name = "c_extension_bundle_#{n}" + dir_config(name) + raise ArgumentError unless with_config("c_extension_#{n}") == "#{n}" + create_makefile(name) + E + + s.write "ext/c_extension_#{n}.c", <<-C + #include "ruby.h" + + VALUE c_extension_#{n}_value(VALUE self) { + return rb_str_new_cstr("#{n}"); + } + + void Init_c_extension_bundle_#{n}() { + VALUE c_Extension = rb_define_class("CExtension_#{n}", rb_cObject); + rb_define_method(c_Extension, "value", c_extension_#{n}_value, 0); + } + C + + s.write "lib/c_extension_#{n}.rb", <<-C + require "c_extension_bundle_#{n}" + C + end + end + build_git "gems", path: lib_path("gems"), gemspec: false + end + + bundle_config "build.c_extension_one --with-c_extension_one=one" + bundle_config "build.c_extension_two --with-c_extension_two=two" + + # 1st time, require only one gem -- only one of the extensions gets built. + install_gemfile <<-G + source "https://gem.repo1" + gem "c_extension_one", :git => #{lib_path("gems").to_s.dump} + G + + # 2nd time, require both gems -- we need both extensions to be built now. + install_gemfile <<-G + source "https://gem.repo1" + gem "c_extension_one", :git => #{lib_path("gems").to_s.dump} + gem "c_extension_two", :git => #{lib_path("gems").to_s.dump} + G + + run "Bundler.require; puts CExtension_one.new.value; puts CExtension_two.new.value" + expect(out).to eq("one\ntwo") + end + it "install with multiple build flags" do build_git "c_extension" do |s| s.extensions = ["ext/extconf.rb"] @@ -95,7 +149,7 @@ RSpec.describe "installing a gem with native extensions", :ruby_repo do require "mkmf" name = "c_extension_bundle" dir_config(name) - raise "OMG" unless with_config("c_extension") == "hello" && with_config("c_extension_bundle-dir") == "hola" + raise ArgumentError unless with_config("c_extension") == "hello" && with_config("c_extension_bundle-dir") == "hola" create_makefile(name) E @@ -117,15 +171,14 @@ RSpec.describe "installing a gem with native extensions", :ruby_repo do C end - bundle! "config set build.c_extension --with-c_extension=hello --with-c_extension_bundle-dir=hola" + bundle_config "build.c_extension --with-c_extension=hello --with-c_extension_bundle-dir=hola" - install_gemfile! <<-G + install_gemfile <<-G + source "https://gem.repo1" gem "c_extension", :git => #{lib_path("c_extension-1.0").to_s.dump} G - expect(out).not_to include("extconf.rb failed") - - run! "Bundler.require; puts CExtension.new.its_true" + run "Bundler.require; puts CExtension.new.its_true" expect(out).to eq("true") end end diff --git a/spec/bundler/install/gems/no_build_extension_spec.rb b/spec/bundler/install/gems/no_build_extension_spec.rb new file mode 100644 index 0000000000..31f0170433 --- /dev/null +++ b/spec/bundler/install/gems/no_build_extension_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with --no-build-extension" do + before do + build_repo2 do + build_gem "with_extension" do |s| + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("lib", __dir__) + FileUtils.mkdir_p(path) + File.open("\#{path}/with_extension.rb", "w") do |f| + f.puts "WITH_EXTENSION = 'YES'" + end + end + RUBY + end + end + end + + it "skips building native extensions and warns when no_build_extension is set" do + bundle_config "no_build_extension true" + + gemfile <<-G + source "https://gem.repo2" + gem "with_extension" + gem "rake" + G + + bundle :install + + build_complete = default_bundle_path("extensions").join( + Gem::Platform.local.to_s, + Gem.extension_api_version.to_s, + "with_extension-1.0", + "gem.build_complete" + ) + expect(build_complete).not_to exist + expect(err).to include("with_extension-1.0 contains native extensions that were not built") + expect(err).to include("unset no_build_extension and run `bundle pristine with_extension`") + end + + it "builds native extensions by default" do + gemfile <<-G + source "https://gem.repo2" + gem "with_extension" + gem "rake" + G + + bundle :install + + expect(out).to include("Installing with_extension 1.0 with native extensions") + end +end diff --git a/spec/bundler/install/gems/no_install_plugin_spec.rb b/spec/bundler/install/gems/no_install_plugin_spec.rb new file mode 100644 index 0000000000..e040e6b813 --- /dev/null +++ b/spec/bundler/install/gems/no_install_plugin_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with --no-install-plugin" do + before do + build_repo2 do + build_gem "with_plugin", "1.0" do |s| + s.write "lib/rubygems_plugin.rb", "# plugin code" + end + + build_gem "with_plugin", "2.0" + end + end + + let(:plugin_path) { default_bundle_path("plugins", "with_plugin_plugin.rb") } + + it "does not generate the plugin wrapper and warns when no_install_plugin is set" do + bundle_config "no_install_plugin true" + + install_gemfile <<-G + source "https://gem.repo2" + gem "with_plugin", "1.0" + G + + expect(plugin_path).not_to exist + expect(err).to include("with_plugin-1.0 contains plugins that were not installed") + expect(err).to include("unset no_install_plugin and run `bundle pristine with_plugin`") + end + + it "removes a stale plugin wrapper from a prior version when no_install_plugin is set" do + install_gemfile <<-G + source "https://gem.repo2" + gem "with_plugin", "1.0" + G + expect(plugin_path).to exist + + bundle_config "no_install_plugin true" + install_gemfile <<-G + source "https://gem.repo2" + gem "with_plugin", "2.0" + G + + expect(plugin_path).not_to exist + end + + it "generates the plugin wrapper by default" do + install_gemfile <<-G + source "https://gem.repo2" + gem "with_plugin", "1.0" + G + + expect(plugin_path).to exist + end +end diff --git a/spec/bundler/install/gems/post_install_spec.rb b/spec/bundler/install/gems/post_install_spec.rb index 3f6d7ce42c..e49fd2a9a3 100644 --- a/spec/bundler/install/gems/post_install_spec.rb +++ b/spec/bundler/install/gems/post_install_spec.rb @@ -5,26 +5,26 @@ RSpec.describe "bundle install" do context "when gems include post install messages" do it "should display the post-install messages after installing" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' gem 'thin' - gem 'rack-obama' + gem 'myrack-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 myrack:") + expect(out).to include("Myrack'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") + expect(out).to include("Post-install message from myrack-obama:") + expect(out).to include("Myrack-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_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "activesupport" G @@ -33,16 +33,16 @@ RSpec.describe "bundle install" do end end - context "when a dependecy includes a post install message" do + context "when a dependency includes a post install message" do it "should display the post install message" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack_middleware' + source "https://gem.repo1" + gem 'myrack_middleware' 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 myrack:") + expect(out).to include("Myrack's post install message") end end end @@ -54,7 +54,7 @@ RSpec.describe "bundle install" do s.post_install_message = "Foo's post install message" end gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem 'foo', :git => '#{lib_path("foo-1.0")}' G @@ -68,7 +68,7 @@ RSpec.describe "bundle install" do s.post_install_message = "Foo's post install message" end gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem 'foo', :git => '#{lib_path("foo-1.0")}' G bundle :install @@ -77,7 +77,7 @@ RSpec.describe "bundle install" do s.post_install_message = "Foo's 1.1 post install message" end gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem 'foo', :git => '#{lib_path("foo-1.1")}' G bundle :install @@ -91,7 +91,7 @@ RSpec.describe "bundle install" do s.post_install_message = "Foo's post install message" end gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem 'foo', :git => '#{lib_path("foo-1.0")}' G @@ -110,7 +110,7 @@ RSpec.describe "bundle install" do s.post_install_message = nil end gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem 'foo', :git => '#{lib_path("foo-1.0")}' G @@ -123,11 +123,11 @@ RSpec.describe "bundle install" do context "when ignore post-install messages for gem is set" do it "doesn't display any post-install messages" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "config set ignore_messages.rack true" + bundle_config "ignore_messages.myrack true" bundle :install expect(out).not_to include("Post-install message") @@ -137,11 +137,11 @@ RSpec.describe "bundle install" do context "when ignore post-install messages for all gems" do it "doesn't display any post-install messages" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "config set ignore_messages true" + bundle_config "ignore_messages true" bundle :install expect(out).not_to include("Post-install message") diff --git a/spec/bundler/install/gems/resolving_spec.rb b/spec/bundler/install/gems/resolving_spec.rb index 26ff40f7aa..111d361aab 100644 --- a/spec/bundler/install/gems/resolving_spec.rb +++ b/spec/bundler/install/gems/resolving_spec.rb @@ -1,9 +1,72 @@ # frozen_string_literal: true RSpec.describe "bundle install with install-time dependencies" do + before do + build_repo2 do + build_gem "with_implicit_rake_dep" do |s| + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("lib", __dir__) + 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", __dir__) + 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 + + # 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", __dir__) + 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 + end + end + it "installs gems with implicit rake dependencies" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo2" gem "with_implicit_rake_dep" gem "another_implicit_rake_dep" gem "rake" @@ -18,30 +81,51 @@ RSpec.describe "bundle install with install-time dependencies" do expect(out).to eq("YES\nYES") end - it "installs gems with a dependency with no type" do + it "installs gems with implicit rake dependencies without rake previously installed" do + with_path_as("") do + install_gemfile <<-G + source "https://gem.repo2" + gem "with_implicit_rake_dep" + gem "another_implicit_rake_dep" + gem "rake" + G + end + + 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 "does not install 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(Bundler.rubygems.inflate(File.read(path))) + spec = Marshal.load(Bundler.rubygems.inflate(File.binread(path))) spec.dependencies.each do |d| - d.instance_variable_set(:@type, :fail) + d.instance_variable_set(:@type, "fail") end - File.open(path, "w") do |f| + File.open(path, "wb") do |f| f.write Gem.deflate(Marshal.dump(spec)) end - install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" gem "actionpack", "2.3.2" G - expect(the_bundle).to include_gems "actionpack 2.3.2", "activesupport 2.3.2" + expect(err).to include("Downloading actionpack-2.3.2 revealed dependencies not in the API (activesupport (= 2.3.2)).") + + expect(the_bundle).not_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_uri_for(gem_repo1)}" + source "https://gem.repo2" gem "net_b" G @@ -49,8 +133,8 @@ RSpec.describe "bundle install with install-time dependencies" do end it "installs plugins depended on by other plugins" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G, env: { "DEBUG" => "1" } + source "https://gem.repo2" gem "net_a" G @@ -58,8 +142,8 @@ RSpec.describe "bundle install with install-time dependencies" do end it "installs multiple levels of dependencies" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G, env: { "DEBUG" => "1" } + source "https://gem.repo2" gem "net_c" gem "net_e" G @@ -67,34 +151,48 @@ RSpec.describe "bundle install with install-time dependencies" do 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['BUNDLER_DEBUG_RESOLVER'] set" do + it "produces debug output" do + gemfile <<-G + source "https://gem.repo2" + gem "net_c" + gem "net_e" + G + + bundle :install, env: { "BUNDLER_DEBUG_RESOLVER" => "1", "DEBUG" => "1" } + + expect(out).to include("Resolving dependencies...") + end + end + context "with ENV['DEBUG_RESOLVER'] set" do it "produces debug output" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo2" gem "net_c" gem "net_e" G - bundle :install, :env => { "DEBUG_RESOLVER" => "1" } + bundle :install, env: { "DEBUG_RESOLVER" => "1", "DEBUG" => "1" } - expect(err).to include("Creating possibility state for net_c") + expect(out).to include("Resolving dependencies...") end end context "with ENV['DEBUG_RESOLVER_TREE'] set" do it "produces debug output" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo2" gem "net_c" gem "net_e" G - bundle :install, :env => { "DEBUG_RESOLVER_TREE" => "1" } + bundle :install, env: { "DEBUG_RESOLVER_TREE" => "1", "DEBUG" => "1" } - expect(err).to include(" net_b"). - and include("Starting resolution"). - and include("Finished resolution"). - and include("Attempting to activate") + expect(out).to include(" net_b"). + and include("Resolving dependencies..."). + and include("Solution found after 1 attempts:"). + and include("selected net_b 1.0") end end end @@ -103,39 +201,438 @@ RSpec.describe "bundle install with install-time dependencies" 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| + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + + build_gem "myrack", "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' + install_gemfile <<-G + ruby "#{Gem.ruby_version}" + source "https://gem.repo2" + gem 'myrack' + G + + expect(err).to_not include("myrack-9001.0.0 requires ruby version > 9000") + expect(the_bundle).to include_gems("myrack 1.2") + end + + it "installs the older version when using servers not implementing the compact index API" do + build_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + + build_gem "myrack", "9001.0.0" do |s| + s.required_ruby_version = "> 9000" + end + end + + install_gemfile <<-G, artifice: "endpoint" + ruby "#{Gem.ruby_version}" + source "https://gem.repo2" + gem 'myrack' + G + + expect(err).to_not include("myrack-9001.0.0 requires ruby version > 9000") + expect(the_bundle).to include_gems("myrack 1.2") + end + + context "when there is a lockfile using the newer incompatible version" do + before do + build_repo2 do + build_gem "parallel_tests", "3.7.0" do |s| + s.required_ruby_version = ">= #{current_ruby_minor}" + end + + build_gem "parallel_tests", "3.8.0" do |s| + s.required_ruby_version = ">= #{next_ruby_minor}" + end + end + + gemfile <<-G + source "https://gem.repo2" + gem 'parallel_tests' + G + + checksums = checksums_section do |c| + c.checksum gem_repo2, "parallel_tests", "3.8.0" + end + + lockfile <<~L + GEM + remote: https://gem.repo2/ + specs: + parallel_tests (3.8.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + parallel_tests + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "automatically updates lockfile to use the older version" do + bundle "install --verbose" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "parallel_tests", "3.7.0" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo2/ + specs: + parallel_tests (3.7.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + parallel_tests + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "gives a meaningful error if we're in frozen mode" do + expect do + bundle "install", env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false + end.not_to change { lockfile } + + expect(err).to eq("parallel_tests-3.8.0 requires ruby version >= #{next_ruby_minor}, which is incompatible with the current version, #{Gem.ruby_version}") + end + end + + context "with transitive dependencies in a lockfile" do + before do + build_repo2 do + build_gem "rubocop", "1.28.2" do |s| + s.required_ruby_version = ">= #{current_ruby_minor}" + + s.add_dependency "rubocop-ast", ">= 1.17.0", "< 2.0" + end + + build_gem "rubocop", "1.35.0" do |s| + s.required_ruby_version = ">= #{next_ruby_minor}" + + s.add_dependency "rubocop-ast", ">= 1.20.1", "< 2.0" + end + + build_gem "rubocop-ast", "1.17.0" do |s| + s.required_ruby_version = ">= #{current_ruby_minor}" + end + + build_gem "rubocop-ast", "1.21.0" do |s| + s.required_ruby_version = ">= #{next_ruby_minor}" + end + end + + gemfile <<-G + source "https://gem.repo2" + gem 'rubocop' + G + + checksums = checksums_section do |c| + c.checksum gem_repo2, "rubocop", "1.35.0" + c.checksum gem_repo2, "rubocop-ast", "1.21.0" + end + + lockfile <<~L + GEM + remote: https://gem.repo2/ + specs: + rubocop (1.35.0) + rubocop-ast (>= 1.20.1, < 2.0) + rubocop-ast (1.21.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rubocop + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "automatically updates lockfile to use the older compatible versions" do + bundle "install --verbose" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "rubocop", "1.28.2" + c.checksum gem_repo2, "rubocop-ast", "1.17.0" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo2/ + specs: + rubocop (1.28.2) + rubocop-ast (>= 1.17.0, < 2.0) + rubocop-ast (1.17.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rubocop + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "with a Gemfile and lockfile that don't resolve under the current platform" do + before do + build_repo4 do + build_gem "sorbet", "0.5.10554" do |s| + s.add_dependency "sorbet-static", "0.5.10554" + end + + build_gem "sorbet-static", "0.5.10554" do |s| + s.platform = "universal-darwin-21" + end + end + + gemfile <<~G + source "https://gem.repo4" + gem 'sorbet', '= 0.5.10554' + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet (0.5.10554) + sorbet-static (= 0.5.10554) + sorbet-static (0.5.10554-universal-darwin-21) + + PLATFORMS + arm64-darwin-21 + + DEPENDENCIES + sorbet (= 0.5.10554) + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "raises a proper error" do + simulate_platform "aarch64-linux" do + bundle "install", raise_on_error: false + end + + nice_error = <<~E.strip + Could not find gems matching 'sorbet-static (= 0.5.10554)' valid for all resolution platforms (arm64-darwin-21, aarch64-linux) in rubygems repository https://gem.repo4/ or installed locally. + + The source contains the following gems matching 'sorbet-static (= 0.5.10554)': + * sorbet-static-0.5.10554-universal-darwin-21 + E + expect(err).to include(nice_error) + expect(err).to include("Your current platform (aarch64-linux) is not included in the lockfile's platforms (arm64-darwin-21)") + expect(err).to include("bundle lock --add-platform aarch64-linux") + end + end + + context "when adding a new gem that does not resolve under all locked platforms" do + before do + simulate_platform "x86_64-linux" do + build_repo4 do + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "x86_64-linux" + end + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "arm-linux" + end + + build_gem "sorbet-static", "0.5.10696" do |s| + s.platform = "x86_64-linux" + end + end + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.0-arm-linux) + nokogiri (1.14.0-x86_64-linux) + + PLATFORMS + arm-linux + x86_64-linux + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + gem "sorbet-static" + G + + bundle "lock", raise_on_error: false + end + end + + it "raises a proper error" do + nice_error = <<~E.strip + Could not find gems matching 'sorbet-static' valid for all resolution platforms (arm-linux, x86_64-linux) in rubygems repository https://gem.repo4/ or installed locally. + + The source contains the following gems matching 'sorbet-static': + * sorbet-static-0.5.10696-x86_64-linux + E + expect(err).to end_with(nice_error) + end + end + + context "when locked generic variant supports current Ruby, but locked specific variant does not" do + let(:original_lockfile) do + <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.16.3) + nokogiri (1.16.3-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + before do + build_repo4 do + build_gem "nokogiri", "1.16.3" + build_gem "nokogiri", "1.16.3" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + G + + lockfile original_lockfile + end + + it "keeps both variants in the lockfile when installing, and uses the generic one since it's compatible" do + simulate_platform "x86_64-linux" do + bundle "install --verbose" + + expect(lockfile).to eq(original_lockfile) + expect(the_bundle).to include_gems("nokogiri 1.16.3") + end + end + + it "keeps both variants in the lockfile when updating, and uses the generic one since it's compatible" do + simulate_platform "x86_64-linux" do + bundle "update --verbose" + + expect(lockfile).to eq(original_lockfile) + expect(the_bundle).to include_gems("nokogiri 1.16.3") + end + end + end + + it "gives a meaningful error on ruby version mismatches between dependencies" do + build_repo4 do + build_gem "requires-old-ruby" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + build_lib("foo", path: bundled_app) do |s| + s.required_ruby_version = ">= #{Gem.ruby_version}" + + s.add_dependency "requires-old-ruby" + end + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo4" + gemspec G - expect(out).to_not include("rack-9001.0.0 requires ruby version > 9000") - expect(the_bundle).to include_gems("rack 1.2") + expect(err).to end_with <<~E.strip + Could not find compatible versions + + Because every version of foo depends on requires-old-ruby >= 0 + and every version of requires-old-ruby depends on Ruby < #{Gem.ruby_version}, + every version of foo requires Ruby < #{Gem.ruby_version}. + So, because Gemfile depends on foo >= 0 + and current Ruby version is = #{Gem.ruby_version}, + version solving has failed. + E end it "installs the older version under rate limiting conditions" do build_repo4 do - build_gem "rack", "9001.0.0" do |s| + build_gem "myrack", "9001.0.0" do |s| s.required_ruby_version = "> 9000" end - build_gem "rack", "1.2" + build_gem "myrack", "1.2" build_gem "foo1", "1.0" end - install_gemfile <<-G, :artifice => "compact_index_rate_limited", :env => { "BUNDLER_SPEC_GEM_REPO" => gem_repo4 } - ruby "#{RUBY_VERSION}" - source "http://localgemserver.test/" - gem 'rack' + install_gemfile <<-G, artifice: "compact_index_rate_limited" + ruby "#{Gem.ruby_version}" + source "https://gem.repo4" + gem 'myrack' gem 'foo1' G - expect(out).to_not include("rack-9001.0.0 requires ruby version > 9000") - expect(the_bundle).to include_gems("rack 1.2") + expect(err).to_not include("myrack-9001.0.0 requires ruby version > 9000") + expect(the_bundle).to include_gems("myrack 1.2") + end + + it "installs the older not platform specific version" do + build_repo4 do + build_gem "myrack", "9001.0.0" do |s| + s.required_ruby_version = "> 9000" + end + build_gem "myrack", "1.2" do |s| + s.platform = "x86-mingw32" + s.required_ruby_version = "> 9000" + end + build_gem "myrack", "1.2" + end + + simulate_platform "x86-mingw32" do + install_gemfile <<-G, artifice: "compact_index" + ruby "#{Gem.ruby_version}" + source "https://gem.repo4" + gem 'myrack' + G + end + + expect(err).to_not include("myrack-9001.0.0 requires ruby version > 9000") + expect(err).to_not include("myrack-1.2-#{Bundler.local_platform} requires ruby version > 9000") + expect(the_bundle).to include_gems("myrack 1.2") end end @@ -148,28 +645,47 @@ RSpec.describe "bundle install with install-time dependencies" do end end - let(:ruby_requirement) { %("#{RUBY_VERSION}") } - let(:error_message_requirement) { "~> #{RUBY_VERSION}.0" } + let(:ruby_requirement) { %("#{Gem.ruby_version}") } + let(:error_message_requirement) { "= #{Gem.ruby_version}" } + + it "raises a proper error that mentions the current Ruby version during resolution" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gem 'require_ruby' + G + + expect(out).to_not include("Gem::InstallError: require_ruby requires Ruby version > 9000") + + nice_error = <<~E.strip + Could not find compatible versions + + Because every version of require_ruby depends on Ruby > 9000 + and Gemfile depends on require_ruby >= 0, + Ruby > 9000 is required. + So, because current Ruby version is #{error_message_requirement}, + version solving has failed. + E + expect(err).to end_with(nice_error) + end 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/" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" 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 - Bundler found conflicting requirements for the Ruby\0 version: - In Gemfile: - Ruby\0 (#{error_message_requirement}) + nice_error = <<~E.strip + Could not find compatible versions - require_ruby was resolved to 1.0, which depends on - Ruby\0 (> 9000) - - Ruby\0 (> 9000), which is required by gem 'require_ruby', is not available in the local ruby installation + Because every version of require_ruby depends on Ruby > 9000 + and Gemfile depends on require_ruby >= 0, + Ruby > 9000 is required. + So, because current Ruby version is #{error_message_requirement}, + version solving has failed. E expect(err).to end_with(nice_error) end @@ -179,7 +695,6 @@ RSpec.describe "bundle install with install-time dependencies" do describe "with a < requirement" do let(:ruby_requirement) { %("< 5000") } - let(:error_message_requirement) { "< 5000" } it_behaves_like "ruby version conflicts" end @@ -187,7 +702,6 @@ RSpec.describe "bundle install with install-time dependencies" do describe "with a compound requirement" do let(:reqs) { ["> 0.1", "< 5000"] } let(:ruby_requirement) { reqs.map(&:dump).join(", ") } - let(:error_message_requirement) { Gem::Requirement.new(reqs).to_s } it_behaves_like "ruby version conflicts" end @@ -202,13 +716,71 @@ RSpec.describe "bundle install with install-time dependencies" do end end - install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" gem 'require_rubygems' G expect(err).to_not include("Gem::InstallError: require_rubygems requires RubyGems version > 9000") - expect(err).to include("require_rubygems-1.0 requires rubygems version > 9000, which is incompatible with the current version, #{Gem::VERSION}") + nice_error = <<~E.strip + Because every version of require_rubygems depends on RubyGems > 9000 + and Gemfile depends on require_rubygems >= 0, + RubyGems > 9000 is required. + So, because current RubyGems version is = #{Gem::VERSION}, + version solving has failed. + E + expect(err).to end_with(nice_error) + end + end + + context "when non platform specific gems bring more dependencies", :truffleruby_only do + before do + build_repo4 do + build_gem "foo", "1.0" do |s| + s.add_dependency "bar" + end + + build_gem "foo", "2.0" do |s| + s.platform = "x86_64-linux" + end + + build_gem "bar" + end + + gemfile <<-G + source "https://gem.repo4" + gem "foo" + G + end + + it "locks both ruby and current platform, and resolve to ruby variants that install on truffleruby" do + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "foo", "1.0" + c.checksum gem_repo4, "bar", "1.0" + end + + simulate_platform "x86_64-linux" do + bundle "install" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + bar (1.0) + foo (1.0) + bar + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + foo + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end end end end diff --git a/spec/bundler/install/gems/standalone_spec.rb b/spec/bundler/install/gems/standalone_spec.rb index f1d5c8b505..96a305bb76 100644 --- a/spec/bundler/install/gems/standalone_spec.rb +++ b/spec/bundler/install/gems/standalone_spec.rb @@ -1,12 +1,18 @@ # frozen_string_literal: true -RSpec.shared_examples "bundle install --standalone" do +RSpec.describe "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 "still makes system gems unavailable to normal bundler" do + system_gems "myrack-1.0.0" + + expect(the_bundle).to_not include_gems("myrack") + end + it "generates a bundle/bundler/setup.rb" do expect(bundled_app("bundle/bundler/setup.rb")).to exist end @@ -21,15 +27,66 @@ RSpec.shared_examples "bundle install --standalone" do testrb << "\nrequire \"#{k}\"" testrb << "\nputs #{k.upcase}" end - Dir.chdir(bundled_app) do - ruby testrb, :no_lib => true + ruby testrb + + expect(out).to eq(expected_gems.values.join("\n")) + end + + it "makes the gems available without bundler nor rubygems" 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 + in_bundled_app %(#{Gem.ruby} --disable-gems -w -e #{testrb.shellescape}) expect(out).to eq(expected_gems.values.join("\n")) end + it "makes the gems available without bundler via Kernel.require" do + testrb = String.new <<-RUBY + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + RUBY + expected_gems.each do |k, _| + testrb << "\nKernel.require \"#{k}\"" + testrb << "\nputs #{k.upcase}" + end + ruby testrb + + expect(out).to eq(expected_gems.values.join("\n")) + end + + it "makes system gems unavailable without bundler" do + system_gems "myrack-1.0.0" + + testrb = String.new <<-RUBY + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + begin + require "myrack" + rescue LoadError + puts "LoadError" + end + RUBY + ruby testrb + + expect(out).to eq("LoadError") + end + it "works on a different system" do - FileUtils.mv(bundled_app, "#{bundled_app}2") + begin + FileUtils.mv(bundled_app, "#{bundled_app}2") + rescue Errno::ENOTEMPTY + puts "Couldn't rename test app since the target folder has these files: #{Dir.glob("#{bundled_app}2/*")}" + raise + end testrb = String.new <<-RUBY $:.unshift File.expand_path("bundle") @@ -40,9 +97,23 @@ RSpec.shared_examples "bundle install --standalone" do testrb << "\nrequire \"#{k}\"" testrb << "\nputs #{k.upcase}" end - Dir.chdir("#{bundled_app}2") do - ruby testrb, :no_lib => true + ruby testrb, dir: "#{bundled_app}2" + + expect(out).to eq(expected_gems.values.join("\n")) + end + + it "skips activating gems" do + testrb = String.new <<-RUBY + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + gem "do_not_activate_me" + RUBY + expected_gems.each do |k, _| + testrb << "\nrequire \"#{k}\"" + testrb << "\nputs #{k.upcase}" end + in_bundled_app %(#{Gem.ruby} -w -e #{testrb.shellescape}) expect(out).to eq(expected_gems.values.join("\n")) end @@ -51,10 +122,11 @@ RSpec.shared_examples "bundle install --standalone" do describe "with simple gems" do before do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rails" G - bundle! :install, forgotten_command_line_options(:path => bundled_app("bundle")).merge(:standalone => true) + bundle_config "path #{bundled_app("bundle")}" + bundle :install, standalone: true, dir: cwd end let(:expected_gems) do @@ -67,28 +139,162 @@ RSpec.shared_examples "bundle install --standalone" do include_examples "common functionality" end - describe "with gems with native extension", :ruby_repo do + describe "with default gems and a lockfile", :ruby_repo do + it "works and points to the vendored copies, not to the default copies" do + base_system_gems "stringio", "psych", "etc", path: scoped_gem_path(bundled_app("bundle")) + + build_gem "foo", "1.0.0", to_system: true, default: true do |s| + s.add_dependency "bar" + end + + build_gem "bar", "1.0.0", to_system: true, default: true + + build_repo4 do + build_gem "foo", "1.0.0" do |s| + s.add_dependency "bar" + end + + build_gem "bar", "1.0.0" + end + + gemfile <<-G + source "https://gem.repo4" + gem "foo" + G + + bundle "lock", dir: cwd + + bundle_config "path #{bundled_app("bundle")}" + + bundle :install, standalone: true, dir: cwd, env: { "BUNDLER_GEM_DEFAULT_DIR" => system_gem_path.to_s } + + load_path_lines = bundled_app("bundle/bundler/setup.rb").read.split("\n").select {|line| line.start_with?("$:.unshift") } + + expect(load_path_lines).to eq [ + '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/bar-1.0.0/lib")', + '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/foo-1.0.0/lib")', + ] + end + + it "works for gems with extensions and points to the vendored copies, not to the default copies" do + simulate_platform "arm64-darwin-23" do + base_system_gems "stringio", "psych", "etc", "shellwords", "open3", path: scoped_gem_path(bundled_app("bundle")) + + build_gem "baz", "1.0.0", to_system: true, default: true, &:add_c_extension + + build_repo4 do + build_gem "baz", "1.0.0", &:add_c_extension + end + + gemfile <<-G + source "https://gem.repo4" + gem "baz" + G + + bundle_config "path #{bundled_app("bundle")}" + + bundle "lock", dir: cwd + + bundle :install, standalone: true, dir: cwd, env: { "BUNDLER_GEM_DEFAULT_DIR" => system_gem_path.to_s } + end + + load_path_lines = bundled_app("bundle/bundler/setup.rb").read.split("\n").select {|line| line.start_with?("$:.unshift") } + + expect(load_path_lines).to eq [ + '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/extensions/arm64-darwin-23/#{Gem.extension_api_version}/baz-1.0.0")', + '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/baz-1.0.0/lib")', + ] + end + end + + describe "with Gemfiles using absolute path sources and resulting bundle moved to a folder hierarchy with different nesting" do + before do + build_lib "minitest", "1.0.0", path: lib_path("minitest") + + Dir.mkdir bundled_app("app") + + gemfile bundled_app("app/Gemfile"), <<-G + source "https://gem.repo1" + gem "minitest", :path => "#{lib_path("minitest")}" + G + + bundle "install", standalone: true, dir: bundled_app("app") + + Dir.mkdir tmp("one_more_level") + FileUtils.mv bundled_app, tmp("one_more_level") + end + + it "also works" do + ruby <<-RUBY, dir: tmp("one_more_level/bundled_app/app") + require "./bundle/bundler/setup" + + require "minitest" + puts MINITEST + RUBY + + expect(out).to eq("1.0.0") + expect(err).to be_empty + end + end + + let(:cwd) { bundled_app } + + describe "with Gemfiles using relative path sources and app moved to a different root" do before do - install_gemfile <<-G, forgotten_command_line_options(:path => bundled_app("bundle")).merge(:standalone => true) - source "#{file_uri_for(gem_repo1)}" + FileUtils.mkdir_p bundled_app("app/vendor") + + build_lib "minitest", "1.0.0", path: bundled_app("app/vendor/minitest") + + gemfile bundled_app("app/Gemfile"), <<-G + source "https://gem.repo1" + gem "minitest", :path => "vendor/minitest" + G + + bundle "install", standalone: true, dir: bundled_app("app") + + FileUtils.mv(bundled_app("app"), bundled_app2("app")) + end + + it "also works" do + ruby <<-RUBY, dir: bundled_app2("app") + require "./bundle/bundler/setup" + + require "minitest" + puts MINITEST + RUBY + + expect(out).to eq("1.0.0") + expect(err).to be_empty + end + end + + describe "with gems with native extension" do + before do + bundle_config "path #{bundled_app("bundle")}" + install_gemfile <<-G, standalone: true, dir: cwd + source "https://gem.repo1" gem "very_simple_binary" G end it "generates a bundle/bundler/setup.rb with the proper paths" do expected_path = bundled_app("bundle/bundler/setup.rb") - extension_line = File.read(expected_path).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"' + script_content = File.read(expected_path) + expect(script_content).to include("def self.ruby_api_version") + expect(script_content).to include("def self.extension_api_version") + extension_line = script_content.each_line.find {|line| line.include? "/extensions/" }.strip + platform = Gem::Platform.local + expect(extension_line).to start_with '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/extensions/' + expect(extension_line).to end_with platform.to_s + '/#{Gem.extension_api_version}/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| + 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__) + lib = File.expand_path('lib/', __dir__) $:.unshift lib unless $:.include?(lib) require 'bar/version' @@ -102,14 +308,16 @@ RSpec.shared_examples "bundle install --standalone" do end G end - install_gemfile <<-G, forgotten_command_line_options(:path => bundled_app("bundle")).merge(:standalone => true) + bundle_config "path #{bundled_app("bundle")}" + install_gemfile <<-G, standalone: true, dir: cwd, raise_on_error: false + source "https://gem.repo1" gem "bar", :git => "#{lib_path("bar-1.0")}" G end it "outputs a helpful error message" do expect(err).to include("You have one or more invalid gemspecs that need to be fixed.") - expect(err).to include("bar 1.0 has an invalid gemspec") + expect(err).to include("bar.gemspec is not valid") end end @@ -118,11 +326,12 @@ RSpec.shared_examples "bundle install --standalone" do build_git "devise", "1.0" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rails" gem "devise", :git => "#{lib_path("devise-1.0")}" G - bundle! :install, forgotten_command_line_options(:path => bundled_app("bundle")).merge(:standalone => true) + bundle_config "path #{bundled_app("bundle")}" + bundle :install, standalone: true, dir: cwd end let(:expected_gems) do @@ -141,15 +350,16 @@ RSpec.shared_examples "bundle install --standalone" do build_git "devise", "1.0" gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rails" group :test do gem "rspec" - gem "rack-test" + gem "myrack-test" end G - bundle! :install, forgotten_command_line_options(:path => bundled_app("bundle")).merge(:standalone => true) + bundle_config "path #{bundled_app("bundle")}" + bundle :install, standalone: true, dir: cwd end let(:expected_gems) do @@ -162,91 +372,72 @@ RSpec.shared_examples "bundle install --standalone" do include_examples "common functionality" it "allows creating a standalone file with limited groups" do - bundle! :install, forgotten_command_line_options(:path => bundled_app("bundle")).merge(: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 + bundle_config "path #{bundled_app("bundle")}" + bundle :install, standalone: "default", dir: cwd - it "allows --without to limit the groups used in a standalone" do - bundle! :install, forgotten_command_line_options(:path => bundled_app("bundle"), :without => "test").merge(:standalone => true) - - Dir.chdir(bundled_app) do - load_error_ruby <<-RUBY, "spec", :no_lib => true - $:.unshift File.expand_path("bundle") - require "bundler/setup" + load_error_ruby <<-RUBY, "spec" + $:.unshift File.expand_path("bundle") + require "bundler/setup" - require "actionpack" - puts ACTIONPACK - require "spec" - RUBY - end + require "actionpack" + puts ACTIONPACK + require "spec" + RUBY expect(out).to eq("2.3.2") - expect(err).to eq("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- spec/) end - it "allows --path to change the location of the standalone bundle", :bundler => "< 3" do - bundle! "install", forgotten_command_line_options(:path => "path/to/bundle").merge(:standalone => true) + it "allows `without` configuration to limit the groups used in a standalone" do + bundle_config "path #{bundled_app("bundle")}" + bundle_config "without test" + bundle :install, standalone: true, dir: cwd - Dir.chdir(bundled_app) do - ruby <<-RUBY, :no_lib => true - $:.unshift File.expand_path("path/to/bundle") - require "bundler/setup" + load_error_ruby <<-RUBY, "spec" + $:.unshift File.expand_path("bundle") + require "bundler/setup" - require "actionpack" - puts ACTIONPACK - RUBY - end + require "actionpack" + puts ACTIONPACK + require "spec" + RUBY expect(out).to eq("2.3.2") + expect(err_without_deprecations).to match(/cannot load such file -- spec/) end - it "allows --path to change the location of the standalone bundle", :bundler => "3" do - bundle! "install", forgotten_command_line_options(:path => "path/to/bundle").merge(:standalone => true) - path = File.expand_path("path/to/bundle") + it "allows `path` configuration to change the location of the standalone bundle" do + bundle_config "path path/to/bundle" + bundle "install", standalone: true, dir: cwd - Dir.chdir(bundled_app) do - ruby <<-RUBY, :no_lib => true - $:.unshift File.expand_path(#{path.dump}) - require "bundler/setup" + ruby <<-RUBY + $:.unshift File.expand_path("path/to/bundle") + require "bundler/setup" - require "actionpack" - puts ACTIONPACK - RUBY - end + require "actionpack" + puts ACTIONPACK + RUBY expect(out).to eq("2.3.2") end - it "allows remembered --without to limit the groups used in a standalone" do - bundle! :install, forgotten_command_line_options(:without => "test") - bundle! :install, forgotten_command_line_options(:path => bundled_app("bundle")).merge(:standalone => true) + it "allows `without` to limit the groups used in a standalone" do + bundle_config "without test" + bundle :install, dir: cwd + bundle_config "path #{bundled_app("bundle")}" + bundle :install, standalone: true, dir: cwd - Dir.chdir(bundled_app) do - load_error_ruby <<-RUBY, "spec", :no_lib => true - $:.unshift File.expand_path("bundle") - require "bundler/setup" + load_error_ruby <<-RUBY, "spec" + $:.unshift File.expand_path("bundle") + require "bundler/setup" - require "actionpack" - puts ACTIONPACK - require "spec" - RUBY - end + require "actionpack" + puts ACTIONPACK + require "spec" + RUBY expect(out).to eq("2.3.2") - expect(err).to eq("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- spec/) end end @@ -259,7 +450,8 @@ RSpec.shared_examples "bundle install --standalone" do source "#{source_uri}" gem "rails" G - bundle! :install, forgotten_command_line_options(:path => bundled_app("bundle")).merge(:standalone => true, :artifice => "endpoint") + bundle_config "path #{bundled_app("bundle")}" + bundle :install, standalone: true, artifice: "endpoint", dir: cwd end let(:expected_gems) do @@ -272,66 +464,61 @@ RSpec.shared_examples "bundle install --standalone" do include_examples "common functionality" end end +end - describe "with --binstubs", :bundler => "< 3" do - before do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rails" - G - bundle! :install, forgotten_command_line_options(:path => bundled_app("bundle")).merge(:standalone => true, :binstubs => true) - end +RSpec.describe "bundle install --standalone run in a subdirectory" do + let(:cwd) { bundled_app("bob").tap(&:mkpath) } - let(:expected_gems) do - { - "actionpack" => "2.3.2", - "rails" => "2.3.2", - } - end + before do + gemfile <<-G + source "https://gem.repo1" + gem "rails" + G + end - include_examples "common functionality" + it "generates the script in the proper place" do + bundle :install, standalone: true, dir: cwd - 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 + expect(bundled_app("bundle/bundler/setup.rb")).to exist + 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 + context "when path set to a relative path" do + before do + bundle_config "path bundle" end - it "creates stubs that can be symlinked" do - pending "File.symlink is unsupported on Windows" if Bundler::WINDOWS + it "generates the script in the proper place" do + bundle :install, standalone: true, dir: cwd - 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) + expect(bundled_app("bundle/bundler/setup.rb")).to exist 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 +RSpec.describe "bundle install --standalone --local" do before do - Dir.chdir(bundled_app("bob").tap(&:mkpath)) + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + system_gems "myrack-1.0.0", path: default_bundle_path end - include_examples("bundle install --standalone") + it "generates script pointing to system gems" do + bundle "install --standalone --local --verbose" + + expect(out).to include("Using myrack 1.0.0") + + load_error_ruby <<-RUBY, "spec" + require "./bundler/setup" + + require "myrack" + puts MYRACK + require "spec" + RUBY + + expect(out).to eq("1.0.0") + expect(err_without_deprecations).to match(/cannot load such file -- spec/) + end end diff --git a/spec/bundler/install/gems/sudo_spec.rb b/spec/bundler/install/gems/sudo_spec.rb deleted file mode 100644 index fcafe4a907..0000000000 --- a/spec/bundler/install/gems/sudo_spec.rb +++ /dev/null @@ -1,187 +0,0 @@ -# frozen_string_literal: true - -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 - let(:subdir) do - system_gem_path("cache") - end - - before do - bundle! "config set path.system true" - subdir.mkpath - sudo "chmod u-w #{subdir}" - end - - after do - sudo "chmod u+w #{subdir}" - end - - it "installs" do - install_gemfile <<-G - source "#{file_uri_for(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 - bundle! "config set path.system true" - chown_system_gems_to_root - end - - it "installs" do - install_gemfile <<-G - source "#{file_uri_for(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_uri_for(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_uri_for(gem_repo1)}" - gem "rack", '1.0' - G - - expect(bundle_path.join(Bundler.ruby_scope, "gems/rack-1.0.0")).to exist - expect(bundle_path.join(Bundler.ruby_scope, "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_uri_for(gem_repo1)}" - gem "rack", '1.0' - G - - expect(bundle_path.join(Bundler.ruby_scope, "gems/rack-1.0.0")).to exist - expect(bundle_path.join(Bundler.ruby_scope, "gems/rack-1.0.0").stat.uid).to eq(0) - expect(the_bundle).to include_gems "rack 1.0" - end - - it "installs extensions/" do - install_gemfile <<-G - source "#{file_uri_for(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 - - after do - sudo "chmod ugo+w #{default_bundle_path}" - end - - it "installs" do - install_gemfile <<-G - source "#{file_uri_for(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_uri_for(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 - bundle! "config set path.system true" - gem_home = tmp("sudo_gem_home") - sudo "mkdir -p #{gem_home}" - sudo "chmod ugo-w #{gem_home}" - - gemfile <<-G - source "#{file_uri_for(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 } - - sudo "rm -rf #{tmp("sudo_gem_home")}" - end - end - - describe "and root runs install" do - let(:warning) { "Don't run Bundler as root." } - - before do - gemfile %(source "#{file_uri_for(gem_repo1)}") - end - - it "warns against that" do - bundle :install, :sudo => true - expect(err).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(err).to_not include(warning) - end - end - - context "when silence_root_warning = false" do - it "warns against that" do - bundle :install, :sudo => true, :env => { "BUNDLE_SILENCE_ROOT_WARNING" => "false" } - expect(err).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 index 01edcca803..be37673aa1 100644 --- a/spec/bundler/install/gems/win32_spec.rb +++ b/spec/bundler/install/gems/win32_spec.rb @@ -2,25 +2,24 @@ RSpec.describe "bundle install with win32-generated lockfile" do it "should read lockfile" do - File.open(bundled_app("Gemfile.lock"), "wb") do |f| + File.open(bundled_app_lock, "wb") do |f| f << "GEM\r\n" - f << " remote: #{file_uri_for(gem_repo1)}/\r\n" + f << " remote: https://gem.repo1/\r\n" f << " specs:\r\n" f << "\r\n" - f << " rack (1.0.0)\r\n" + f << " myrack (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" + f << " myrack\r\n" end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" - gem "rack" + gem "myrack" 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 index 4c00caa60c..fb2271c830 100644 --- a/spec/bundler/install/gemspecs_spec.rb +++ b/spec/bundler/install/gemspecs_spec.rb @@ -4,13 +4,13 @@ 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 + build_gem "yaml_spec", gemspec: :yaml end end it "still installs correctly" do gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "yaml_spec" G bundle :install @@ -18,9 +18,10 @@ RSpec.describe "bundle install" do end it "still installs correctly when using path" do - build_lib "yaml_spec", :gemspec => :yaml + build_lib "yaml_spec", gemspec: :yaml install_gemfile <<-G + source "https://gem.repo1" gem 'yaml_spec', :path => "#{lib_path("yaml_spec-1.0")}" G expect(err).to be_empty @@ -29,21 +30,23 @@ RSpec.describe "bundle install" do it "should use gemspecs in the system cache when available" do gemfile <<-G - source "http://localtestserver.gem" - gem 'rack' + source "http://localgemserver.test" + gem 'myrack' G + system_gems "myrack-1.0.0", path: default_bundle_path + FileUtils.mkdir_p "#{default_bundle_path}/specifications" - File.open("#{default_bundle_path}/specifications/rack-1.0.0.gemspec", "w+") do |f| + File.open("#{default_bundle_path}/specifications/myrack-1.0.0.gemspec", "w+") do |f| spec = Gem::Specification.new do |s| - s.name = "rack" + s.name = "myrack" s.version = "1.0.0" - s.add_runtime_dependency "activesupport", "2.3.2" + s.add_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" + bundle :install, artifice: "endpoint_marshal_fail" # force gemspec load + expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.2" end it "does not hang when gemspec has incompatible encoding" do @@ -56,7 +59,8 @@ RSpec.describe "bundle install" do end G - install_gemfile <<-G, :env => { "LANG" => "C" } + install_gemfile <<-G, env: { "LANG" => "C" } + source "https://gem.repo1" gemspec G @@ -82,6 +86,7 @@ RSpec.describe "bundle install" do G install_gemfile <<-G + source "https://gem.repo1" gemspec G @@ -89,63 +94,86 @@ RSpec.describe "bundle install" do 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| + it "installs when patch level is not specified and the version matches", + if: RUBY_PATCHLEVEL >= 0 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' + source "https://gem.repo1" 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| + 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 + install_gemfile <<-G, raise_on_error: false ruby '#{RUBY_VERSION}', :engine_version => '#{RUBY_VERSION}', :engine => 'ruby', :patchlevel => '#{RUBY_PATCHLEVEL}' + source "https://gem.repo1" 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 + it "installs gems ignoring the mismatch even when patchlevel is mismatch", + if: RUBY_PATCHLEVEL >= 0 do patchlevel = RUBY_PATCHLEVEL.to_i + 1 - build_lib("foo", :path => bundled_app) do |s| + build_lib("foo", path: bundled_app) do |s| s.required_ruby_version = "#{RUBY_VERSION}.#{patchlevel}" end - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false ruby '#{RUBY_VERSION}', :engine_version => '#{RUBY_VERSION}', :engine => 'ruby', :patchlevel => '#{patchlevel}' + source "https://gem.repo1" gemspec G - expect(err).to include("Ruby patchlevel") - expect(err).to include("but your Gemfile specified") - expect(exitstatus).to eq(18) if exitstatus + expect(the_bundle).to include_gems "foo 1.0" 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| + build_lib("foo", path: bundled_app) do |s| s.required_ruby_version = version end - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false ruby '#{version}', :engine_version => '#{version}', :engine => 'ruby' + source "https://gem.repo1" gemspec G expect(err).to include("Ruby version") expect(err).to include("but your Gemfile specified") - expect(exitstatus).to eq(18) if exitstatus + expect(exitstatus).to eq(18) + end + + it "validates gemspecs just once when everything installed and lockfile up to date" do + build_lib "foo" + + install_gemfile <<-G + source "https://gem.repo1" + gemspec path: "#{lib_path("foo-1.0")}" + + module Monkey + def validate(spec) + puts "Validate called on \#{spec.full_name}" + end + end + Bundler.rubygems.extend(Monkey) + G + + bundle "install" + + expect(out).to include("Validate called on foo-1.0").once end end end diff --git a/spec/bundler/install/git_spec.rb b/spec/bundler/install/git_spec.rb index c16285241f..1172d661ae 100644 --- a/spec/bundler/install/git_spec.rb +++ b/spec/bundler/install/git_spec.rb @@ -2,50 +2,78 @@ RSpec.describe "bundle install" do context "git sources" do - it "displays the revision hash of the gem repository", :bundler => "< 3" do - build_git "foo", "1.0", :path => lib_path("foo") + it "displays the revision hash of the gem repository" do + build_git "foo", "1.0", path: lib_path("foo") - install_gemfile <<-G + install_gemfile <<-G, verbose: true + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo")}" + G + + expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at main@#{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 revision hash of the gem repository when passed a relative local path" do + build_git "foo", "1.0", path: lib_path("foo") + + relative_path = lib_path("foo").relative_path_from(bundled_app) + install_gemfile <<-G, verbose: true + source "https://gem.repo1" + gem "foo", :git => "#{relative_path}" + G + + expect(out).to include("Using foo 1.0 from #{relative_path} (at main@#{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 correct default branch", git: ">= 2.28.0" do + build_git "foo", "1.0", path: lib_path("foo"), default_branch: "non-standard" + + install_gemfile <<-G, verbose: true + source "https://gem.repo1" 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")}" + expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at non-standard@#{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", :bundler => "< 3" do - build_git "foo", "1.0", :path => lib_path("foo") + it "displays the ref of the gem repository when using branch~num as a ref" do + skip "maybe branch~num notation doesn't work on Windows' git" if Gem.win_platform? + + 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 + 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 + update_git "foo", "3.0", path: lib_path("foo"), gemspec: true - install_gemfile! <<-G - gem "foo", :git => "#{lib_path("foo")}", :ref => "master~2" + install_gemfile <<-G, verbose: true + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo")}", :ref => "main~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")}" + expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at main~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 + update_git "foo", "4.0", path: lib_path("foo"), gemspec: true - bundle! :update, :all => true - 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")}" + bundle :update, all: true, verbose: true + expect(out).to include("Using foo 2.0 (was 1.0) from #{lib_path("foo")} (at main~2@#{rev2})") + expect(the_bundle).to include_gems "foo 2.0", source: "git@#{lib_path("foo")}" end - it "should allows git repos that are missing but not being installed" do + it "allows git repos that are missing but not being installed" do revision = build_git("foo").ref_for("HEAD") gemfile <<-G - gem "foo", :git => "#{file_uri_for(lib_path("foo-1.0"))}", :group => :development + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :group => :development G lockfile <<-L GIT - remote: #{file_uri_for(lib_path("foo-1.0"))} + remote: #{lib_path("foo-1.0")} revision: #{revision} specs: foo (1.0) @@ -57,9 +85,285 @@ RSpec.describe "bundle install" do foo! L - bundle! :install, forgotten_command_line_options(:path => "vendor/bundle", :without => "development") + bundle_config "path vendor/bundle" + bundle_config "without development" + bundle :install expect(out).to include("Bundle complete!") end + + it "allows multiple gems from the same git source" do + build_repo2 do + build_lib "foo", "1.0", path: lib_path("gems/foo") + build_lib "zebra", "2.0", path: lib_path("gems/zebra") + build_git "gems", path: lib_path("gems"), gemspec: false + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "foo", :git => "#{lib_path("gems")}", :glob => "foo/*.gemspec" + gem "zebra", :git => "#{lib_path("gems")}", :glob => "zebra/*.gemspec" + G + + bundle "info foo" + expect(out).to include("* foo (1.0 #{revision_for(lib_path("gems"))[0..6]})") + + bundle "info zebra" + expect(out).to include("* zebra (2.0 #{revision_for(lib_path("gems"))[0..6]})") + end + + it "should always sort dependencies in the same order" do + # This Gemfile + lockfile had a problem where the first + # `bundle install` would change the order, but the second would + # change it back. + + # NOTE: both gems MUST have the same path! It has to be two gems in one repo. + + test = build_git "test", "1.0.0", path: lib_path("test-and-other") + other = build_git "other", "1.0.0", path: lib_path("test-and-other") + test_ref = test.ref_for("HEAD") + other_ref = other.ref_for("HEAD") + + gemfile <<-G + source "https://gem.repo1" + + gem "test", git: #{test.path.to_s.inspect} + gem "other", ref: #{other_ref.inspect}, git: #{other.path.to_s.inspect} + G + + lockfile <<-L + GIT + remote: #{test.path} + revision: #{test_ref} + specs: + test (1.0.0) + + GIT + remote: #{other.path} + revision: #{other_ref} + ref: #{other_ref} + specs: + other (1.0.0) + + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + ruby + + DEPENDENCIES + other! + test! + + BUNDLED WITH + #{Bundler::VERSION} + L + + # If GH#6743 is present, the first `bundle install` will change the + # lockfile, by flipping the order (`other` would be moved to the top). + # + # The second `bundle install` would then change the lockfile back + # to the original. + # + # The fix makes it so it may change it once, but it will not change + # it a second time. + # + # So, we run `bundle install` once, and store the value of the + # modified lockfile. + bundle :install + modified_lockfile = lockfile + + # If GH#6743 is present, the second `bundle install` would change the + # lockfile back to what it was originally. + # + # This `expect` makes sure it doesn't change a second time. + bundle :install + expect(lockfile).to eq(modified_lockfile) + + expect(out).to include("Bundle complete!") + end + + it "allows older revisions of git source when clean true" do + build_git "foo", "1.0", path: lib_path("foo") + rev = revision_for(lib_path("foo")) + + bundle_config "path vendor/bundle" + bundle_config "clean true" + install_gemfile <<-G, verbose: true + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo")}" + G + + expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at main@#{rev[0..6]})") + expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}" + + old_lockfile = lockfile + + update_git "foo", "2.0", path: lib_path("foo"), gemspec: true + rev2 = revision_for(lib_path("foo")) + + bundle :update, all: true, verbose: true + expect(out).to include("Using foo 2.0 (was 1.0) from #{lib_path("foo")} (at main@#{rev2[0..6]})") + expect(out).to include("Removing foo (#{rev[0..11]})") + expect(the_bundle).to include_gems "foo 2.0", source: "git@#{lib_path("foo")}" + + lockfile(old_lockfile) + + bundle :install, verbose: true + expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at main@#{rev[0..6]})") + expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}" + end + + context "when install directory exists" do + let(:checkout_confirmation_log_message) { "Checking out revision" } + let(:using_foo_confirmation_log_message) { "Using foo 1.0 from #{lib_path("foo")} (at main@#{revision_for(lib_path("foo"))[0..6]})" } + + context "and no contents besides .git directory are present" do + it "reinstalls gem" do + build_git "foo", "1.0", path: lib_path("foo") + + gemfile = <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo")}" + G + + install_gemfile gemfile, verbose: true + + expect(out).to include(checkout_confirmation_log_message) + expect(out).to include(using_foo_confirmation_log_message) + expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}" + + # validate that the installed directory exists and has some expected contents + install_directory = default_bundle_path("bundler/gems/foo-#{revision_for(lib_path("foo"))[0..11]}") + dot_git_directory = install_directory.join(".git") + lib_directory = install_directory.join("lib") + gemspec = install_directory.join("foo.gemspec") + expect([install_directory, dot_git_directory, lib_directory, gemspec]).to all exist + + # remove all elements in the install directory except .git directory + FileUtils.rm_r(lib_directory) + gemspec.delete + + expect(dot_git_directory).to exist + expect(lib_directory).not_to exist + expect(gemspec).not_to exist + + # rerun bundle install + install_gemfile gemfile, verbose: true + + expect(out).to include(checkout_confirmation_log_message) + expect(out).to include(using_foo_confirmation_log_message) + expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}" + + # validate that it reinstalls all components + expect([install_directory, dot_git_directory, lib_directory, gemspec]).to all exist + end + end + + context "and contents besides .git directory are present" do + # we want to confirm that the change to try to detect partial installs and reinstall does not + # result in repeatedly reinstalling the gem when it is fully installed + it "does not reinstall gem" do + build_git "foo", "1.0", path: lib_path("foo") + + gemfile = <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo")}" + G + + install_gemfile gemfile, verbose: true + + expect(out).to include(checkout_confirmation_log_message) + expect(out).to include(using_foo_confirmation_log_message) + expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}" + + # rerun bundle install + install_gemfile gemfile, verbose: true + + # it isn't altogether straight-forward to validate that bundle didn't do soething on the second run, however, + # the presence of the 2nd log message confirms install got past the point that it would have logged the above if + # it was going to + expect(out).not_to include(checkout_confirmation_log_message) + expect(out).to include(using_foo_confirmation_log_message) + end + end + end + end + + describe "with excluded groups" do + it "works if you exclude a group with a git gem", ruby: ">= 3.3" do + build_git "production_gem", "1.0" + build_git "development_gem", "1.0" + + gemfile <<-G + source "https://gem.repo1" + + gem "production_gem", :git => "#{lib_path("production_gem-1.0")}" + + group :development do + gem "development_gem", :git => "#{lib_path("development_gem-1.0")}" + end + G + + # First install all groups to create lockfile + bundle :install + + # Set without and reinstall + bundle_config "without development" + bundle :install + + # Verify only production gem is available + expect(the_bundle).to include_gems("production_gem 1.0") + expect(the_bundle).not_to include_gems("development_gem 1.0") + end + + it "resolves indirect dependencies from a git source not in the requested groups" do + build_lib "activesupport", "1.0", path: lib_path("rails/activesupport") + build_git "activerecord", "1.0", path: lib_path("rails") do |s| + s.add_dependency "activesupport", "= 1.0" + end + + gemfile <<-G + source "https://gem.repo1" + + gem "activerecord", :git => "#{lib_path("rails")}" + + group :ci do + gem "myrack" + end + G + + bundle_config "only ci" + bundle :install + + expect(the_bundle).to include_gems("myrack 1.0.0") + expect(the_bundle).not_to include_gems("activerecord 1.0") + end + + it "resolves indirect dependencies from a git source not in the requested groups (without compact_index dependency API)" do + build_lib "activesupport", "1.0", path: lib_path("rails/activesupport") + build_git "activerecord", "1.0", path: lib_path("rails") do |s| + s.add_dependency "activesupport", "= 1.0" + end + + gemfile <<-G + source "https://gem.repo1" + + gem "activerecord", :git => "#{lib_path("rails")}" + + group :ci do + gem "myrack" + end + G + + # Force the RubygemsAggregate code path in find_source_requirements by + # making the dependency API unavailable. + bundle_config "only ci" + bundle :install, artifice: "endpoint_api_forbidden" + + expect(the_bundle).to include_gems("myrack 1.0.0") + expect(the_bundle).not_to include_gems("activerecord 1.0") + end end end diff --git a/spec/bundler/install/global_cache_spec.rb b/spec/bundler/install/global_cache_spec.rb index 023e52b060..4cffa65b2a 100644 --- a/spec/bundler/install/global_cache_spec.rb +++ b/spec/bundler/install/global_cache_spec.rb @@ -1,231 +1,301 @@ # frozen_string_literal: true RSpec.describe "global gem caching" do - before { bundle! "config set global_gem_cache true" } + # Uses subprocess because this setting must apply across multiple app directories (bundled_app and bundled_app2) + before { bundle "config set global_gem_cache true" } describe "using the cross-application user cache" do let(:source) { "http://localgemserver.test" } let(:source2) { "http://gemserver.example.org" } + def cache_base + # Use the unified global gem cache path if available (from RubyGems), + # otherwise fall back to the Bundler-specific cache location + if Gem.respond_to?(:global_gem_cache_path) + Pathname.new(Gem.global_gem_cache_path) + else + home(".bundle", "cache", "gems") + end + end + def source_global_cache(*segments) - home(".bundle", "cache", "gems", "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", *segments) + cache_base.join("localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", *segments) end def source2_global_cache(*segments) - home(".bundle", "cache", "gems", "gemserver.example.org.80.1ae1663619ffe0a3c9d97712f44c705b", *segments) + cache_base.join("gemserver.example.org.80.1ae1663619ffe0a3c9d97712f44c705b", *segments) end it "caches gems into the global cache on download" do - install_gemfile! <<-G, :artifice => "compact_index" + install_gemfile <<-G, artifice: "compact_index" source "#{source}" - gem "rack" + gem "myrack" G - expect(the_bundle).to include_gems "rack 1.0.0" - expect(source_global_cache("rack-1.0.0.gem")).to exist + expect(the_bundle).to include_gems "myrack 1.0.0" + expect(source_global_cache("myrack-1.0.0.gem")).to exist end it "uses globally cached gems if they exist" do source_global_cache.mkpath - FileUtils.cp(gem_repo1("gems/rack-1.0.0.gem"), source_global_cache("rack-1.0.0.gem")) + FileUtils.cp(gem_repo1("gems/myrack-1.0.0.gem"), source_global_cache("myrack-1.0.0.gem")) - install_gemfile! <<-G, :artifice => "compact_index_no_gem" + install_gemfile <<-G, artifice: "compact_index_no_gem" source "#{source}" - gem "rack" + gem "myrack" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "shows a proper error message if a cached gem is corrupted" do + skip "This example is not working on ruby/ruby repo" if ruby_core? + + source_global_cache.mkpath + FileUtils.touch(source_global_cache("myrack-1.0.0.gem")) + + install_gemfile <<-G, artifice: "compact_index_no_gem", raise_on_error: false + source "#{source}" + gem "myrack" + G + + expect(err).to include("Gem::Package::FormatError: package metadata is missing in #{source_global_cache("myrack-1.0.0.gem")}") + end + + it "uses a shorter path for the cache to not hit filesystem limits" do + install_gemfile <<-G, artifice: "compact_index", verbose: true + source "http://#{"a" * 255}.test" + gem "myrack" G - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" + source_segment = "a" * 222 + ".a3cb26de2edfce9f509a65c611d99c4b" + source_cache = cache_base.join(source_segment) + cached_gem = source_cache.join("myrack-1.0.0.gem") + expect(cached_gem).to exist + ensure + # We cleanup dummy files created by this spec manually because due to a + # Ruby on Windows bug, `FileUtils.rm_rf` (run in our global after hook) + # cannot traverse directories with such long names. So we delete + # everything explicitly to workaround the bug. An alternative workaround + # would be to shell out to `rm -rf`. That also works fine, but I went with + # the more verbose and explicit approach. This whole ensure block can be + # removed once/if https://bugs.ruby-lang.org/issues/21177 is fixed, and + # once the fix propagates to all supported rubies. + File.delete cached_gem + Dir.rmdir source_cache + + File.delete compact_index_cache_path.join(source_segment, "info", "myrack") + Dir.rmdir compact_index_cache_path.join(source_segment, "info") + File.delete compact_index_cache_path.join(source_segment, "info-etags", "myrack-92f3313ce5721296f14445c3a6b9c073") + Dir.rmdir compact_index_cache_path.join(source_segment, "info-etags") + Dir.rmdir compact_index_cache_path.join(source_segment, "info-special-characters") + File.delete compact_index_cache_path.join(source_segment, "versions") + File.delete compact_index_cache_path.join(source_segment, "versions.etag") + Dir.rmdir compact_index_cache_path.join(source_segment) end describe "when the same gem from different sources is installed" do it "should use the appropriate one from the global cache" do - install_gemfile! <<-G, :artifice => "compact_index" + bundle_config "path.system true" + + install_gemfile <<-G, artifice: "compact_index" source "#{source}" - gem "rack" + gem "myrack" G - FileUtils.rm_r(default_bundle_path) - expect(the_bundle).not_to include_gems "rack 1.0.0" - expect(source_global_cache("rack-1.0.0.gem")).to exist - # rack 1.0.0 is not installed and it is in the global cache + pristine_system_gems + expect(the_bundle).not_to include_gems "myrack 1.0.0" + expect(source_global_cache("myrack-1.0.0.gem")).to exist + # myrack 1.0.0 is not installed and it is in the global cache - install_gemfile! <<-G, :artifice => "compact_index" + install_gemfile <<-G, artifice: "compact_index" source "#{source2}" - gem "rack", "0.9.1" + gem "myrack", "0.9.1" G - FileUtils.rm_r(default_bundle_path) - expect(the_bundle).not_to include_gems "rack 0.9.1" - expect(source2_global_cache("rack-0.9.1.gem")).to exist - # rack 0.9.1 is not installed and it is in the global cache + pristine_system_gems + expect(the_bundle).not_to include_gems "myrack 0.9.1" + expect(source2_global_cache("myrack-0.9.1.gem")).to exist + # myrack 0.9.1 is not installed and it is in the global cache gemfile <<-G source "#{source}" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" G - bundle! :install, :artifice => "compact_index_no_gem" - # rack 1.0.0 is installed and rack 0.9.1 is not - expect(the_bundle).to include_gems "rack 1.0.0" - expect(the_bundle).not_to include_gems "rack 0.9.1" - FileUtils.rm_r(default_bundle_path) + bundle :install, artifice: "compact_index_no_gem" + # myrack 1.0.0 is installed and myrack 0.9.1 is not + expect(the_bundle).to include_gems "myrack 1.0.0" + expect(the_bundle).not_to include_gems "myrack 0.9.1" + pristine_system_gems gemfile <<-G source "#{source2}" - gem "rack", "0.9.1" + gem "myrack", "0.9.1" G - bundle! :install, :artifice => "compact_index_no_gem" - # rack 0.9.1 is installed and rack 1.0.0 is not - expect(the_bundle).to include_gems "rack 0.9.1" - expect(the_bundle).not_to include_gems "rack 1.0.0" + bundle :install, artifice: "compact_index_no_gem" + # myrack 0.9.1 is installed and myrack 1.0.0 is not + expect(the_bundle).to include_gems "myrack 0.9.1" + expect(the_bundle).not_to include_gems "myrack 1.0.0" end it "should not install if the wrong source is provided" do + bundle_config "path.system true" + gemfile <<-G source "#{source}" - gem "rack" + gem "myrack" G - bundle! :install, :artifice => "compact_index" - FileUtils.rm_r(default_bundle_path) - expect(the_bundle).not_to include_gems "rack 1.0.0" - expect(source_global_cache("rack-1.0.0.gem")).to exist - # rack 1.0.0 is not installed and it is in the global cache + bundle :install, artifice: "compact_index" + pristine_system_gems + expect(the_bundle).not_to include_gems "myrack 1.0.0" + expect(source_global_cache("myrack-1.0.0.gem")).to exist + # myrack 1.0.0 is not installed and it is in the global cache gemfile <<-G source "#{source2}" - gem "rack", "0.9.1" + gem "myrack", "0.9.1" G - bundle! :install, :artifice => "compact_index" - FileUtils.rm_r(default_bundle_path) - expect(the_bundle).not_to include_gems "rack 0.9.1" - expect(source2_global_cache("rack-0.9.1.gem")).to exist - # rack 0.9.1 is not installed and it is in the global cache + bundle :install, artifice: "compact_index" + pristine_system_gems + expect(the_bundle).not_to include_gems "myrack 0.9.1" + expect(source2_global_cache("myrack-0.9.1.gem")).to exist + # myrack 0.9.1 is not installed and it is in the global cache gemfile <<-G source "#{source2}" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" G - expect(source_global_cache("rack-1.0.0.gem")).to exist - expect(source2_global_cache("rack-0.9.1.gem")).to exist - bundle :install, :artifice => "compact_index_no_gem" + expect(source_global_cache("myrack-1.0.0.gem")).to exist + expect(source2_global_cache("myrack-0.9.1.gem")).to exist + bundle :install, artifice: "compact_index_no_gem", raise_on_error: false expect(err).to include("Internal Server Error 500") - # rack 1.0.0 is not installed and rack 0.9.1 is not - expect(the_bundle).not_to include_gems "rack 1.0.0" - expect(the_bundle).not_to include_gems "rack 0.9.1" + expect(err).not_to include("ERROR REPORT TEMPLATE") + + # myrack 1.0.0 is not installed and myrack 0.9.1 is not + expect(the_bundle).not_to include_gems "myrack 1.0.0" + expect(the_bundle).not_to include_gems "myrack 0.9.1" gemfile <<-G source "#{source}" - gem "rack", "0.9.1" + gem "myrack", "0.9.1" G - expect(source_global_cache("rack-1.0.0.gem")).to exist - expect(source2_global_cache("rack-0.9.1.gem")).to exist - bundle :install, :artifice => "compact_index_no_gem" + expect(source_global_cache("myrack-1.0.0.gem")).to exist + expect(source2_global_cache("myrack-0.9.1.gem")).to exist + bundle :install, artifice: "compact_index_no_gem", raise_on_error: false expect(err).to include("Internal Server Error 500") - # rack 0.9.1 is not installed and rack 1.0.0 is not - expect(the_bundle).not_to include_gems "rack 0.9.1" - expect(the_bundle).not_to include_gems "rack 1.0.0" + expect(err).not_to include("ERROR REPORT TEMPLATE") + + # myrack 0.9.1 is not installed and myrack 1.0.0 is not + expect(the_bundle).not_to include_gems "myrack 0.9.1" + expect(the_bundle).not_to include_gems "myrack 1.0.0" end end describe "when installing gems from a different directory" do it "uses the global cache as a source" do - install_gemfile! <<-G, :artifice => "compact_index" + bundle_config "path.system true" + + install_gemfile <<-G, artifice: "compact_index" source "#{source}" - gem "rack" + gem "myrack" gem "activesupport" G # Both gems are installed and in the global cache - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" expect(the_bundle).to include_gems "activesupport 2.3.5" - expect(source_global_cache("rack-1.0.0.gem")).to exist + expect(source_global_cache("myrack-1.0.0.gem")).to exist expect(source_global_cache("activesupport-2.3.5.gem")).to exist - FileUtils.rm_r(default_bundle_path) + pristine_system_gems # Both gems are now only in the global cache - expect(the_bundle).not_to include_gems "rack 1.0.0" + expect(the_bundle).not_to include_gems "myrack 1.0.0" expect(the_bundle).not_to include_gems "activesupport 2.3.5" - install_gemfile! <<-G, :artifice => "compact_index_no_gem" + install_gemfile <<-G, artifice: "compact_index_no_gem" source "#{source}" - gem "rack" + gem "myrack" G - # rack is installed and both are in the global cache - expect(the_bundle).to include_gems "rack 1.0.0" + # myrack is installed and both are in the global cache + expect(the_bundle).to include_gems "myrack 1.0.0" expect(the_bundle).not_to include_gems "activesupport 2.3.5" - expect(source_global_cache("rack-1.0.0.gem")).to exist + expect(source_global_cache("myrack-1.0.0.gem")).to exist expect(source_global_cache("activesupport-2.3.5.gem")).to exist - Dir.chdir bundled_app2 do - create_file bundled_app2("gems.rb"), <<-G - source "#{source}" - gem "activesupport" - G - - # Neither gem is installed and both are in the global cache - expect(the_bundle).not_to include_gems "rack 1.0.0" - expect(the_bundle).not_to include_gems "activesupport 2.3.5" - expect(source_global_cache("rack-1.0.0.gem")).to exist - expect(source_global_cache("activesupport-2.3.5.gem")).to exist - - # Install using the global cache instead of by downloading the .gem - # from the server - bundle! :install, :artifice => "compact_index_no_gem" - - # activesupport is installed and both are in the global cache - expect(the_bundle).not_to include_gems "rack 1.0.0" - expect(the_bundle).to include_gems "activesupport 2.3.5" - expect(source_global_cache("rack-1.0.0.gem")).to exist - expect(source_global_cache("activesupport-2.3.5.gem")).to exist - end + create_file bundled_app2("gems.rb"), <<-G + source "#{source}" + gem "activesupport" + G + + # Neither gem is installed and both are in the global cache + expect(the_bundle).not_to include_gems "myrack 1.0.0", dir: bundled_app2 + expect(the_bundle).not_to include_gems "activesupport 2.3.5", dir: bundled_app2 + expect(source_global_cache("myrack-1.0.0.gem")).to exist + expect(source_global_cache("activesupport-2.3.5.gem")).to exist + + # Install using the global cache instead of by downloading the .gem + # from the server + bundle :install, artifice: "compact_index_no_gem", dir: bundled_app2 + + # activesupport is installed and both are in the global cache + expect(the_bundle).not_to include_gems "myrack 1.0.0", dir: bundled_app2 + expect(the_bundle).to include_gems "activesupport 2.3.5", dir: bundled_app2 + + expect(source_global_cache("myrack-1.0.0.gem")).to exist + expect(source_global_cache("activesupport-2.3.5.gem")).to exist end end end - describe "extension caching", :ruby_repo do + describe "extension caching" do it "works" do + skip "gets incorrect ref in path" if Gem.win_platform? + skip "fails for unknown reason when run by ruby-core" if ruby_core? + build_git "very_simple_git_binary", &:add_c_extension build_lib "very_simple_path_binary", &:add_c_extension revision = revision_for(lib_path("very_simple_git_binary-1.0"))[0, 12] - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G + source "https://gem.repo1" gem "very_simple_binary" gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}" gem "very_simple_path_binary", :path => "#{lib_path("very_simple_path_binary-1.0")}" G - gem_binary_cache = home(".bundle", "cache", "extensions", specific_local_platform.to_s, Bundler.ruby_scope, - Digest(:MD5).hexdigest("#{gem_repo1}/"), "very_simple_binary-1.0") - git_binary_cache = home(".bundle", "cache", "extensions", specific_local_platform.to_s, Bundler.ruby_scope, + gem_binary_cache = home(".bundle", "cache", "extensions", local_platform.to_s, Bundler.ruby_scope, + "gem.repo1.443.#{Digest(:MD5).hexdigest("gem.repo1.443./")}", "very_simple_binary-1.0") + git_binary_cache = home(".bundle", "cache", "extensions", local_platform.to_s, Bundler.ruby_scope, "very_simple_git_binary-1.0-#{revision}", "very_simple_git_binary-1.0") cached_extensions = Pathname.glob(home(".bundle", "cache", "extensions", "*", "*", "*", "*", "*")).sort expect(cached_extensions).to eq [gem_binary_cache, git_binary_cache].sort - run! <<-R + run <<-R require 'very_simple_binary_c'; puts ::VERY_SIMPLE_BINARY_IN_C require 'very_simple_git_binary_c'; puts ::VERY_SIMPLE_GIT_BINARY_IN_C R expect(out).to eq "VERY_SIMPLE_BINARY_IN_C\nVERY_SIMPLE_GIT_BINARY_IN_C" - FileUtils.rm Dir[home(".bundle", "cache", "extensions", "**", "*binary_c*")] + FileUtils.rm_r Dir[home(".bundle", "cache", "extensions", "**", "*binary_c*")] gem_binary_cache.join("very_simple_binary_c.rb").open("w") {|f| f << "puts File.basename(__FILE__)" } git_binary_cache.join("very_simple_git_binary_c.rb").open("w") {|f| f << "puts File.basename(__FILE__)" } - bundle! "config set --local path different_path" - bundle! :install + bundle_config "path different_path" + bundle :install expect(Dir[home(".bundle", "cache", "extensions", "**", "*binary_c*")]).to all(end_with(".rb")) - run! <<-R + run <<-R require 'very_simple_binary_c' require 'very_simple_git_binary_c' R diff --git a/spec/bundler/install/path_spec.rb b/spec/bundler/install/path_spec.rb index 5240c5820c..49360e511e 100644 --- a/spec/bundler/install/path_spec.rb +++ b/spec/bundler/install/path_spec.rb @@ -1,96 +1,87 @@ # frozen_string_literal: true RSpec.describe "bundle install" do - describe "with --path" do + describe "with path configured" do before :each do - build_gem "rack", "1.0.0", :to_system => true do |s| - s.write "lib/rack.rb", "puts 'FAIL'" + build_gem "myrack", "1.0.0", to_system: true do |s| + s.write "lib/myrack.rb", "puts 'FAIL'" end gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G end - it "does not use available system gems with bundle --path vendor/bundle", :bundler => "< 3" do - bundle! :install, forgotten_command_line_options(:path => "vendor/bundle") - expect(the_bundle).to include_gems "rack 1.0.0" + it "does not use available system gems with `vendor/bundle" do + bundle_config "path vendor/bundle" + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "uses system gems with `path.system` configured with more priority than `path`" do + bundle_config "path.system true" + bundle_config_global "path vendor/bundle" + bundle :install + run "require 'myrack'", raise_on_error: false + expect(out).to include("FAIL") end it "handles paths with regex characters in them" do dir = bundled_app("bun++dle") dir.mkpath - Dir.chdir(dir) do - bundle! :install, forgotten_command_line_options(:path => dir.join("vendor/bundle")) - expect(out).to include("installed into `./vendor/bundle`") - end + bundle_config "path #{dir.join("vendor/bundle")}" + bundle :install, dir: dir + expect(out).to include("installed into `./vendor/bundle`") - dir.rmtree + FileUtils.rm_rf dir end - it "prints a warning to let the user know what has happened with bundle --path vendor/bundle" do - bundle! :install, forgotten_command_line_options(:path => "vendor/bundle") + it "prints a message to let the user know where gems where installed" do + bundle_config "path vendor/bundle" + bundle :install expect(out).to include("gems are installed into `./vendor/bundle`") end - it "disallows --path vendor/bundle --system", :bundler => "< 3" do - bundle "install --path vendor/bundle --system" - expect(err).to include("Please choose only one option.") - expect(exitstatus).to eq(15) if exitstatus + it "installs the bundle relatively to repository root, when Bundler run from the same directory" do + bundle "config set path vendor/bundle", dir: bundled_app.parent + bundle "install --gemfile='#{bundled_app}/Gemfile'", dir: bundled_app.parent + expect(out).to include("installed into `./bundled_app/vendor/bundle`") + expect(bundled_app("vendor/bundle")).to be_directory + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "remembers to disable system gems after the first time with bundle --path vendor/bundle", :bundler => "< 3" 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" + it "installs the bundle relatively to repository root, when Bundler run from a different directory" do + bundle "config set path vendor/bundle", dir: bundled_app + bundle "install --gemfile='#{bundled_app}/Gemfile'", dir: bundled_app.parent + expect(out).to include("installed into `./bundled_app/vendor/bundle`") + expect(bundled_app("vendor/bundle")).to be_directory + expect(the_bundle).to include_gems "myrack 1.0.0" end - context "with path_relative_to_cwd set to true" do - before { bundle! "config set path_relative_to_cwd true" } - - it "installs the bundle relatively to current working directory", :bundler => "< 3" do - Dir.chdir(bundled_app.parent) do - bundle! "install --gemfile='#{bundled_app}/Gemfile' --path vendor/bundle" - expect(out).to include("installed into `./vendor/bundle`") - expect(bundled_app("../vendor/bundle")).to be_directory - end - expect(the_bundle).to include_gems "rack 1.0.0" - end - - it "installs the standalone bundle relative to the cwd" do - Dir.chdir(bundled_app.parent) do - bundle! :install, :gemfile => bundled_app("Gemfile"), :standalone => true - expect(out).to include("installed into `./bundled_app/bundle`") - expect(bundled_app("bundle")).to be_directory - expect(bundled_app("bundle/ruby")).to be_directory - end - - bundle! "config unset path" + it "installs the standalone bundle relative to the cwd" do + bundle :install, gemfile: bundled_app_gemfile, standalone: true, dir: bundled_app.parent + expect(out).to include("installed into `./bundled_app/bundle`") + expect(bundled_app("bundle")).to be_directory + expect(bundled_app("bundle/ruby")).to be_directory - Dir.chdir(bundled_app("subdir").tap(&:mkpath)) do - bundle! :install, :gemfile => bundled_app("Gemfile"), :standalone => true - expect(out).to include("installed into `../bundle`") - expect(bundled_app("bundle")).to be_directory - expect(bundled_app("bundle/ruby")).to be_directory - end - end + bundle :install, gemfile: bundled_app_gemfile, standalone: true, dir: bundled_app("subdir").tap(&:mkpath) + expect(out).to include("installed into `../bundle`") + expect(bundled_app("bundle")).to be_directory + expect(bundled_app("bundle/ruby")).to be_directory 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'" + build_lib "myrack", "1.0.0", to_system: true do |s| + s.write "lib/myrack.rb", "raise 'FAIL'" end gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G end @@ -98,7 +89,7 @@ RSpec.describe "bundle install" do if type == :env ENV["BUNDLE_PATH"] = location elsif type == :global - bundle! "config set path #{location}", "no-color" => nil + bundle "config set path #{location}", "no-color" => nil end end @@ -106,115 +97,117 @@ RSpec.describe "bundle install" do context "when set via #{type}" do it "installs gems to a path if one is specified" do set_bundle_path(type, bundled_app("vendor2").to_s) - bundle! :install, forgotten_command_line_options(:path => "vendor/bundle") + bundle_config "path vendor/bundle" + bundle :install - expect(vendored_gems("gems/rack-1.0.0")).to be_directory + expect(vendored_gems("gems/myrack-1.0.0")).to be_directory expect(bundled_app("vendor2")).not_to be_directory - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "installs gems to ." do set_bundle_path(type, ".") - bundle! "config set --global disable_shared_gems true" + bundle_config_global "disable_shared_gems true" - bundle! :install + bundle :install - paths_to_exist = %w[cache/rack-1.0.0.gem gems/rack-1.0.0 specifications/rack-1.0.0.gemspec].map {|path| bundled_app(Bundler.ruby_scope, path) } + paths_to_exist = %w[cache/myrack-1.0.0.gem gems/myrack-1.0.0 specifications/myrack-1.0.0.gemspec].map {|path| bundled_app(Bundler.ruby_scope, path) } expect(paths_to_exist).to all exist - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "installs gems to the path" do set_bundle_path(type, bundled_app("vendor").to_s) - bundle! :install + bundle :install - expect(bundled_app("vendor", Bundler.ruby_scope, "gems/rack-1.0.0")).to be_directory - expect(the_bundle).to include_gems "rack 1.0.0" + expect(bundled_app("vendor", Bundler.ruby_scope, "gems/myrack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "myrack 1.0.0" end it "installs gems to the 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 + bundle :install, dir: bundled_app("lol") - expect(bundled_app("vendor", Bundler.ruby_scope, "gems/rack-1.0.0")).to be_directory - expect(the_bundle).to include_gems "rack 1.0.0" + expect(bundled_app("vendor", Bundler.ruby_scope, "gems/myrack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "myrack 1.0.0" end end end it "installs gems to BUNDLE_PATH from .bundle/config" do - config "BUNDLE_PATH" => bundled_app("vendor/bundle").to_s + bundle_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" + expect(vendored_gems("gems/myrack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "myrack 1.0.0" end it "sets BUNDLE_PATH as the first argument to bundle install" do - bundle! :install, forgotten_command_line_options(:path => "./vendor/bundle") + bundle_config "path ./vendor/bundle" + bundle :install - expect(vendored_gems("gems/rack-1.0.0")).to be_directory - expect(the_bundle).to include_gems "rack 1.0.0" + expect(vendored_gems("gems/myrack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "myrack 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, forgotten_command_line_options(:path => "./vendor/bundle") + build_gem "myrack", "1.1.0", to_system: true + bundle_config "path ./vendor/bundle" + bundle :install - expect(vendored_gems("gems/rack-1.0.0")).to be_directory - expect(the_bundle).to include_gems "rack 1.0.0" + expect(vendored_gems("gems/myrack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "re-installs gems whose extensions have been deleted", :ruby_repo do - build_lib "very_simple_binary", "1.0.0", :to_system => true do |s| + it "re-installs gems whose extensions have been deleted" 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_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "very_simple_binary" G - bundle! :install, forgotten_command_line_options(:path => "./vendor/bundle") + bundle_config "path ./vendor/bundle" + bundle :install 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" + expect(the_bundle).to include_gems "very_simple_binary 1.0", source: "remote1" - vendored_gems("extensions").rmtree + FileUtils.rm_rf vendored_gems("extensions") - run "require 'very_simple_binary_c'" + run "require 'very_simple_binary_c'", raise_on_error: false expect(err).to include("Bundler::GemNotFound") - bundle :install, forgotten_command_line_options(:path => "./vendor/bundle") + bundle_config "path ./vendor/bundle" + bundle :install 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" + 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 - FileUtils.touch "bundle" - end + FileUtils.touch bundled_app("bundle") end it "reports the file exists" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle :install, forgotten_command_line_options(:path => "bundle") + bundle_config "path bundle" + bundle :install, raise_on_error: false expect(err).to include("file already exists") end end diff --git a/spec/bundler/install/prereleases_spec.rb b/spec/bundler/install/prereleases_spec.rb index fb01220ed7..9f764d127c 100644 --- a/spec/bundler/install/prereleases_spec.rb +++ b/spec/bundler/install/prereleases_spec.rb @@ -1,10 +1,19 @@ # frozen_string_literal: true RSpec.describe "bundle install" do + before do + build_repo2 do + build_gem "not_released", "1.0.pre" + + build_gem "has_prerelease", "1.0" + build_gem "has_prerelease", "1.1.pre" + end + end + describe "when prerelease gems are available" do it "finds prereleases" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo2" gem "not_released" G expect(the_bundle).to include_gems "not_released 1.0.pre" @@ -12,7 +21,7 @@ RSpec.describe "bundle install" do it "uses regular releases if available" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo2" gem "has_prerelease" G expect(the_bundle).to include_gems "has_prerelease 1.0" @@ -20,7 +29,7 @@ RSpec.describe "bundle install" do it "uses prereleases if requested" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo2" gem "has_prerelease", "1.1.pre" G expect(the_bundle).to include_gems "has_prerelease 1.1.pre" @@ -29,13 +38,17 @@ RSpec.describe "bundle install" do describe "when prerelease gems are not available" do it "still works" do - build_repo3 + build_repo3 do + build_gem "myrack" + end + FileUtils.rm_r Dir[gem_repo3("prerelease*")] + install_gemfile <<-G - source "#{file_uri_for(gem_repo3)}" - gem "rack" + source "https://gem.repo3" + gem "myrack" G - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 1.0" end end end diff --git a/spec/bundler/install/process_lock_spec.rb b/spec/bundler/install/process_lock_spec.rb index cab4ba0819..b096291d1a 100644 --- a/spec/bundler/install/process_lock_spec.rb +++ b/spec/bundler/install/process_lock_spec.rb @@ -8,28 +8,106 @@ RSpec.describe "process lock spec" do thread = Thread.new do Bundler::ProcessLock.lock(default_bundle_path) do sleep 1 # ignore quality_spec - expect(the_bundle).not_to include_gems "rack 1.0" + expect(the_bundle).not_to include_gems "myrack 1.0" end end - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" G thread.join - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 1.0" end context "when creating a lock raises Errno::ENOTSUP" do before { allow(File).to receive(:open).and_raise(Errno::ENOTSUP) } - it "skips creating the lock file and yields" do + it "skips creating the lockfile and yields" do processed = false Bundler::ProcessLock.lock(default_bundle_path) { processed = true } expect(processed).to eq true end end + + context "when creating a lock raises Errno::EPERM" do + before { allow(File).to receive(:open).and_raise(Errno::EPERM) } + + it "skips creating the lockfile and yields" do + processed = false + Bundler::ProcessLock.lock(default_bundle_path) { processed = true } + + expect(processed).to eq true + end + end + + context "when creating a lock raises Errno::EROFS" do + before { allow(File).to receive(:open).and_raise(Errno::EROFS) } + + it "skips creating the lockfile and yields" do + processed = false + Bundler::ProcessLock.lock(default_bundle_path) { processed = true } + + expect(processed).to eq true + end + end + + it "refreshes gem specification cache after waiting for lock" do + build_repo2 do + build_gem "myrack", "1.0.0" + end + + gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + + # First, install the gem so it's available + bundle "install" + expect(out).to include("Installing myrack") + + # Queue for thread-safe communication + lock_acquired = Queue.new + can_release_lock = Queue.new + install_output = Queue.new + + # Thread holds lock (simulating another bundle process that just finished installing) + thread = Thread.new do + Bundler::ProcessLock.lock(default_bundle_path) do + # Signal that we have the lock + lock_acquired << true + # Wait until main thread signals we can release + can_release_lock.pop + end + end + + # Wait for thread to acquire lock + lock_acquired.pop + + # Start another install in a thread - it will wait for the lock + install_thread = Thread.new do + bundle "install", verbose: true + install_output << out + end + + # Give subprocess time to start and begin waiting for lock + sleep 0.5 + + # Signal thread to release the lock + can_release_lock << true + + # Wait for both threads to complete + thread.join + install_thread.join + + second_install_out = install_output.pop + + expect(the_bundle).to include_gems "myrack 1.0.0" + # The second install should have refreshed its cache after acquiring + # the lock and seen that myrack was already installed + expect(second_install_out).to include("Using myrack") + end end end diff --git a/spec/bundler/install/redownload_spec.rb b/spec/bundler/install/redownload_spec.rb deleted file mode 100644 index 818c33bd61..0000000000 --- a/spec/bundler/install/redownload_spec.rb +++ /dev/null @@ -1,90 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe "bundle install" do - before :each do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - G - end - - shared_examples_for "an option to force redownloading gems" do - 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, flag => true - - 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, flag => true - - 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, flag => true - - 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, flag => true - - expect(the_bundle).to include_gems "foo 1.0" - end - end - end - - describe "with --force", :bundler => 2 do - it_behaves_like "an option to force redownloading gems" do - let(:flag) { "force" } - end - - it "shows a deprecation when single flag passed" do - bundle! "install --force" - expect(err).to include "[DEPRECATED] The `--force` option has been renamed to `--redownload`" - end - - it "shows a deprecation when multiple flags passed" do - bundle! "install --no-color --force" - expect(err).to include "[DEPRECATED] The `--force` option has been renamed to `--redownload`" - end - end - - describe "with --redownload" do - it_behaves_like "an option to force redownloading gems" do - let(:flag) { "redownload" } - end - - it "does not show a deprecation when single flag passed" do - bundle! "install --redownload" - expect(err).not_to include "[DEPRECATED] The `--force` option has been renamed to `--redownload`" - end - - it "does not show a deprecation when single multiple flags passed" do - bundle! "install --no-color --redownload" - expect(err).not_to include "[DEPRECATED] The `--force` option has been renamed to `--redownload`" - end - end -end diff --git a/spec/bundler/install/security_policy_spec.rb b/spec/bundler/install/security_policy_spec.rb index 28c34d9ce7..e7f64dc227 100644 --- a/spec/bundler/install/security_policy_spec.rb +++ b/spec/bundler/install/security_policy_spec.rb @@ -9,37 +9,35 @@ RSpec.describe "policies with unsigned gems" do before do build_security_repo gemfile <<-G - source "#{file_uri_for(security_repo)}" - gem "rack" + source "https://gems.security" + gem "myrack" gem "signed_gem" G end it "will work after you try to deploy without a lock" do - bundle "install --deployment" + bundle "install --deployment", raise_on_error: false bundle :install - expect(exitstatus).to eq(0) if exitstatus - expect(the_bundle).to include_gems "rack 1.0", "signed_gem 1.0" + expect(the_bundle).to include_gems "myrack 1.0", "signed_gem 1.0" end it "will fail when given invalid security policy" do - bundle "install --trust-policy=InvalidPolicyName" + bundle "install --trust-policy=InvalidPolicyName", raise_on_error: false expect(err).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" + bundle "install --trust-policy=HighSecurity", raise_on_error: false expect(err).to include("security policy didn't allow") end it "will fail with Medium Security setting due to presence of unsigned gem" do - bundle "install --trust-policy=MediumSecurity" + bundle "install --trust-policy=MediumSecurity", raise_on_error: false expect(err).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 @@ -47,30 +45,28 @@ RSpec.describe "policies with signed gems and no CA" do before do build_security_repo gemfile <<-G - source "#{file_uri_for(security_repo)}" + source "https://gems.security" gem "signed_gem" G end it "will fail with High Security setting, gem is self-signed" do - bundle "install --trust-policy=HighSecurity" + bundle "install --trust-policy=HighSecurity", raise_on_error: false expect(err).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" + bundle "install --trust-policy=MediumSecurity", raise_on_error: false expect(err).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 index 80729b3f5b..c92af7bfb0 100644 --- a/spec/bundler/install/yanked_spec.rb +++ b/spec/bundler/install/yanked_spec.rb @@ -1,40 +1,127 @@ # frozen_string_literal: true RSpec.context "when installing a bundle that includes yanked gems" do - before(:each) do + it "throws an error when the original gem version is yanked" 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_uri_for(gem_repo4)} + remote: https://gem.repo4 specs: foo (10.0.0) PLATFORMS - ruby + #{lockfile_platforms} DEPENDENCIES foo (= 10.0.0) L - install_gemfile <<-G - source "#{file_uri_for(gem_repo4)}" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo4" gem "foo", "10.0.0" G expect(err).to include("Your bundle is locked to foo (10.0.0)") end + context "when a platform specific yanked version is included in the lockfile, and a generic variant is available remotely" do + let(:original_lockfile) do + <<~L + GEM + remote: https://gem.repo4/ + specs: + actiontext (6.1.6) + nokogiri (>= 1.8) + foo (1.0.0) + nokogiri (1.13.8-#{Bundler.local_platform}) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + actiontext (= 6.1.6) + foo (= 1.0.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + before do + skip "Materialization on Windows is not yet strict, so the example does not detect the gem has been yanked" if Gem.win_platform? + + build_repo4 do + build_gem "foo", "1.0.0" + build_gem "foo", "1.0.1" + build_gem "actiontext", "6.1.7" do |s| + s.add_dependency "nokogiri", ">= 1.8" + end + build_gem "actiontext", "6.1.6" do |s| + s.add_dependency "nokogiri", ">= 1.8" + end + build_gem "actiontext", "6.1.7" do |s| + s.add_dependency "nokogiri", ">= 1.8" + end + build_gem "nokogiri", "1.13.8" + end + + gemfile <<~G + source "https://gem.repo4" + gem "foo", "1.0.0" + gem "actiontext", "6.1.6" + G + + lockfile original_lockfile + end + + context "and a re-resolve is necessary" do + before do + gemfile gemfile.sub('"foo", "1.0.0"', '"foo", "1.0.1"') + end + + it "reresolves, and replaces the yanked gem with the generic version, printing a warning, when the old index is used" do + bundle "install", artifice: "endpoint", verbose: true + + expect(out).to include("Installing nokogiri 1.13.8").and include("Installing foo 1.0.1") + expect(lockfile).to eq(original_lockfile.sub("nokogiri (1.13.8-#{Bundler.local_platform})", "nokogiri (1.13.8)").gsub("1.0.0", "1.0.1")) + expect(err).to include("Some locked specs have possibly been yanked (nokogiri-1.13.8-#{Bundler.local_platform}). Ignoring them...") + end + + it "reresolves, and replaces the yanked gem with the generic version, printing a warning, when the compact index API is used" do + bundle "install", artifice: "compact_index", verbose: true + + expect(out).to include("Installing nokogiri 1.13.8").and include("Installing foo 1.0.1") + expect(lockfile).to eq(original_lockfile.sub("nokogiri (1.13.8-#{Bundler.local_platform})", "nokogiri (1.13.8)").gsub("1.0.0", "1.0.1")) + expect(err).to include("Some locked specs have possibly been yanked (nokogiri-1.13.8-#{Bundler.local_platform}). Ignoring them...") + end + end + + it "reports the yanked gem properly when the old index is used" do + bundle "install", artifice: "endpoint", raise_on_error: false + + expect(err).to include("Your bundle is locked to nokogiri (1.13.8-#{Bundler.local_platform})") + end + + it "reports the yanked gem properly when the compact index API is used" do + bundle "install", artifice: "compact_index", raise_on_error: false + + expect(err).to include("Your bundle is locked to nokogiri (1.13.8-#{Bundler.local_platform})") + end + end + it "throws the original error when only the Gemfile specifies a gem version that doesn't exist" do - bundle "config set force_ruby_platform true" + build_repo4 do + build_gem "foo", "9.0.0" + end - install_gemfile <<-G - source "#{file_uri_for(gem_repo4)}" + bundle_config "force_ruby_platform true" + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo4" gem "foo", "10.0.0" G @@ -43,31 +130,125 @@ RSpec.context "when installing a bundle that includes yanked gems" do end end +RSpec.context "when resolving a bundle that includes yanked gems, but unlocking an unrelated gem" do + before(:each) do + build_repo4 do + build_gem "foo", "10.0.0" + + build_gem "bar", "1.0.0" + build_gem "bar", "2.0.0" + end + + lockfile <<-L + GEM + remote: https://gem.repo4 + specs: + foo (9.0.0) + bar (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo + bar + + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile <<-G + source "https://gem.repo4" + gem "foo" + gem "bar" + G + end + + it "does not update the yanked gem" do + bundle "lock --update bar" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + bar (2.0.0) + foo (9.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + bar + foo + + BUNDLED WITH + #{Bundler::VERSION} + L + 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_uri_for(gem_repo1)}" - gem "rack", "0.9.1" + source "https://gem.repo1" + gem "myrack", "0.9.1" + G + + lockfile <<-L + GEM + remote: https://gem.repo1 + specs: + myrack (0.9.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack (= 0.9.1) + L + + bundle :list, raise_on_error: false + + expect(err).to include("Could not find myrack-0.9.1 in locally installed gems") + expect(err).to_not include("Your bundle is locked to myrack (0.9.1) from") + expect(err).to_not include("If you haven't changed sources, that means the author of myrack (0.9.1) has removed it.") + expect(err).to_not include("You'll need to update your bundle to a different version of myrack (0.9.1) that hasn't been removed in order to install.") + + # Check error message is still correct when multiple platforms are locked + lockfile lockfile.gsub(/PLATFORMS\n #{lockfile_platforms}/m, "PLATFORMS\n #{lockfile_platforms("ruby")}") + + bundle :list, raise_on_error: false + expect(err).to include("Could not find myrack-0.9.1 in locally installed gems") + end + + it "does not suggest the author has yanked the gem when using more than one gem, but shows all gems that couldn't be found in the source" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + gem "myrack_middleware", "1.0" G lockfile <<-L GEM - remote: #{file_uri_for(gem_repo1)} + remote: https://gem.repo1 specs: - rack (0.9.1) + myrack (0.9.1) + myrack_middleware (1.0) PLATFORMS - ruby + #{lockfile_platforms} DEPENDENCIES - rack (= 0.9.1) + myrack (= 0.9.1) + myrack_middleware (1.0) L - bundle :list + bundle :list, raise_on_error: false - expect(err).to include("Could not find rack-0.9.1 in any of the sources") - expect(err).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(err).to_not include("If you haven't changed sources, that means the author of rack (0.9.1) has removed it.") - expect(err).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.") + expect(err).to include("Could not find myrack-0.9.1, myrack_middleware-1.0 in locally installed gems") + expect(err).to include("Install missing gems with `bundle install`.") + expect(err).to_not include("Your bundle is locked to myrack (0.9.1) from") + expect(err).to_not include("If you haven't changed sources, that means the author of myrack (0.9.1) has removed it.") + expect(err).to_not include("You'll need to update your bundle to a different version of myrack (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 index 14b80483ee..c9f76115dc 100644 --- a/spec/bundler/lock/git_spec.rb +++ b/spec/bundler/lock/git_spec.rb @@ -1,19 +1,76 @@ # frozen_string_literal: true RSpec.describe "bundle lock with git gems" do - before :each do + let(:install_gemfile_with_foo_as_a_git_dependency) do build_git "foo" install_gemfile <<-G + source "https://gem.repo1" gem 'foo', :git => "#{lib_path("foo-1.0")}" G end it "doesn't break right after running lock" do + install_gemfile_with_foo_as_a_git_dependency + expect(the_bundle).to include_gems "foo 1.0.0" end + it "doesn't print errors even if running lock after removing the cache" do + install_gemfile_with_foo_as_a_git_dependency + + FileUtils.rm_r(Dir[default_cache_path("git/foo-1.0-*")].first) + + bundle "lock --verbose" + + expect(err).to be_empty + end + + it "prints a proper error when changing a locked Gemfile to point to a bad branch" do + install_gemfile_with_foo_as_a_git_dependency + + gemfile <<-G + source "https://gem.repo1" + gem 'foo', :git => "#{lib_path("foo-1.0")}", :branch => "bad" + G + + bundle "lock --update foo", env: { "LANG" => "en" }, raise_on_error: false + + expect(err).to include("Revision bad does not exist in the repository") + end + + it "prints a proper error when installing a Gemfile with a locked ref that does not exist" do + install_gemfile_with_foo_as_a_git_dependency + + lockfile <<~L + GIT + remote: #{lib_path("foo-1.0")} + revision: #{"a" * 40} + specs: + foo (1.0) + + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install", raise_on_error: false + + expect(err).to include("Revision #{"a" * 40} does not exist in the repository") + end + it "locks a git source to the current ref" do + install_gemfile_with_foo_as_a_git_dependency + update_git "foo" bundle :install @@ -25,10 +82,177 @@ RSpec.describe "bundle lock with git gems" do expect(out).to eq("WIN") end + it "properly clones a git source locked to an out of date ref" do + install_gemfile_with_foo_as_a_git_dependency + + update_git "foo" + + bundle :install, env: { "BUNDLE_PATH" => "foo" } + expect(err).to be_empty + end + + it "properly fetches a git source locked to an unreachable ref" do + install_gemfile_with_foo_as_a_git_dependency + + # Create a commit and make it unreachable + git "checkout -b foo ", lib_path("foo-1.0") + unreachable_sha = update_git("foo").ref_for("HEAD") + git "checkout main ", lib_path("foo-1.0") + git "branch -D foo ", lib_path("foo-1.0") + + gemfile <<-G + source "https://gem.repo1" + gem 'foo', :git => "#{lib_path("foo-1.0")}" + G + + lockfile <<-L + GIT + remote: #{lib_path("foo-1.0")} + revision: #{unreachable_sha} + specs: + foo (1.0) + + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install" + + expect(err).to be_empty + end + + it "properly fetches a git source locked to an annotated tag" do + install_gemfile_with_foo_as_a_git_dependency + + # Create an annotated tag + git("tag -a v1.0 -m 'Annotated v1.0'", lib_path("foo-1.0")) + annotated_tag = git("rev-parse v1.0", lib_path("foo-1.0")) + + gemfile <<-G + source "https://gem.repo1" + gem 'foo', :git => "#{lib_path("foo-1.0")}" + G + + lockfile <<-L + GIT + remote: #{lib_path("foo-1.0")} + revision: #{annotated_tag} + specs: + foo (1.0) + + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install" + + expect(err).to be_empty + end + it "provides correct #full_gem_path" do + install_gemfile_with_foo_as_a_git_dependency + run <<-RUBY puts Bundler.rubygems.find_name('foo').first.full_gem_path RUBY expect(out).to eq(bundle("info foo --path")) end + + it "does not lock versions that don't exist in the repository when changing a GEM transitive dep to a GIT direct dep" do + build_repo4 do + build_gem "activesupport", "8.0.0" do |s| + s.add_dependency "securerandom" + end + + build_gem "securerandom", "0.3.1" + end + + path = lib_path("securerandom") + + build_git "securerandom", "0.3.2", path: path + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + activesupport (8.0.0) + securerandom + securerandom (0.3.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + activesupport + + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile <<~G + source "https://gem.repo4" + + gem "activesupport" + gem "securerandom", git: "#{path}" + G + + bundle "lock" + + expect(lockfile).to include("securerandom (0.3.2)") + end + + it "does not lock versions that don't exist in the repository when changing a GIT direct dep to a GEM direct dep" do + build_repo4 do + build_gem "ruby-lsp", "0.16.1" + end + + path = lib_path("ruby-lsp") + revision = build_git("ruby-lsp", "0.16.2", path: path).ref_for("HEAD") + + lockfile <<~L + GIT + remote: #{path} + revision: #{revision} + specs: + ruby-lsp (0.16.2) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ruby-lsp! + + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile <<~G + source "https://gem.repo4" + gem "ruby-lsp" + G + + bundle "lock" + + expect(lockfile).to include("ruby-lsp (0.16.1)") + end end diff --git a/spec/bundler/lock/lockfile_spec.rb b/spec/bundler/lock/lockfile_spec.rb index ddab4831a5..654ac02aa7 100644 --- a/spec/bundler/lock/lockfile_spec.rb +++ b/spec/bundler/lock/lockfile_spec.rb @@ -1,33 +1,41 @@ # frozen_string_literal: true RSpec.describe "the lockfile format" do - include Bundler::GemHelpers + before do + build_repo2 + end it "generates a simple lockfile for a single source, gem" do + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo2, "myrack", "1.0.0") + end + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo2" - gem "rack" + gem "myrack" G - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack - + myrack + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end - it "updates the lockfile's bundler version if current ver. is newer" do + it "updates the lockfile's bundler version if current ver. is newer, and version was forced through BUNDLER_VERSION" do + system_gems "bundler-1.8.2" + lockfile <<-L GIT remote: git://github.com/nex3/haml.git @@ -35,323 +43,373 @@ RSpec.describe "the lockfile format" do specs: GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES omg! - rack + myrack BUNDLED WITH 1.8.2 L - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G, verbose: true, env: { "BUNDLER_VERSION" => Bundler::VERSION } + source "https://gem.repo2" - gem "rack" + gem "myrack" G - lockfile_should_be <<-G + expect(out).not_to include("Bundler #{Bundler::VERSION} is running, but your lockfile was generated with 1.8.2.") + expect(out).to include("Using bundler #{Bundler::VERSION}") + + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack + myrack BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end - it "does not update the lockfile's bundler version if nothing changed during bundle install" do - version = "#{Bundler::VERSION.split(".").first}.0.0.a" + it "does not update the lockfile's bundler version if nothing changed during bundle install, but uses the locked version" do + version = "2.3.0" + + build_repo4 do + build_gem "myrack", "1.0.0" + + build_bundler version + end lockfile <<-L GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo4/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack + myrack BUNDLED WITH #{version} L - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G, verbose: true + source "https://gem.repo4" - gem "rack" + gem "myrack" G - lockfile_should_be <<-G + expect(out).to include("Bundler #{Bundler::VERSION} is running, but your lockfile was generated with #{version}.") + expect(out).to include("Using bundler #{version}") + + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo4/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack + myrack BUNDLED WITH #{version} G end - it "updates the lockfile's bundler version if not present" do + it "adds the BUNDLED WITH section if not present" do lockfile <<-L GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack + myrack L install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo2" - gem "rack", "> 0" + gem "myrack", "> 0" G - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack (> 0) + myrack (> 0) BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end - it "warns if the current is older than lockfile's bundler version" do + it "update the bundler major version just fine" do current_version = Bundler::VERSION - newer_minor = bump_minor(current_version) + older_major = previous_major(current_version) + + system_gems "bundler-#{older_major}" lockfile <<-L GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack + myrack BUNDLED WITH - #{newer_minor} + #{older_major} L - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G, env: { "BUNDLER_VERSION" => Bundler::VERSION } + source "https://gem.repo2/" - gem "rack" + gem "myrack" G - pre_flag = prerelease?(newer_minor) ? " --pre" : "" - warning_message = "the running version of Bundler (#{current_version}) is older " \ - "than the version that created the lockfile (#{newer_minor}). " \ - "We suggest you to upgrade to the version that created the " \ - "lockfile by running `gem install bundler:#{newer_minor}#{pre_flag}`." - expect(err).to include warning_message + expect(err).to be_empty - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack + myrack BUNDLED WITH - #{newer_minor} + #{current_version} G end - it "warns when updating bundler major version" do - current_version = Bundler::VERSION - older_major = previous_major(current_version) - - lockfile <<-L - GEM - remote: #{file_uri_for(gem_repo1)}/ - specs: - rack (1.0.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - rack - - BUNDLED WITH - #{older_major} - L - + it "generates a simple lockfile for a single source, gem with dependencies" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" + source "https://gem.repo2/" - gem "rack" + gem "myrack-obama" G - expect(err).to include( - "Warning: the lockfile is being updated to Bundler " \ - "#{current_version.split(".").first}, after which you will be unable to return to Bundler #{older_major.split(".").first}." - ) + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "myrack", "1.0.0" + c.checksum gem_repo2, "myrack-obama", "1.0" + end - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) + myrack (1.0.0) + myrack-obama (1.0) + myrack PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack - + myrack-obama + #{checksums} BUNDLED WITH - #{current_version} + #{Bundler::VERSION} G end - it "generates a simple lockfile for a single source, gem with dependencies" do + it "generates a simple lockfile for a single source, gem with a version requirement" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" + source "https://gem.repo2/" - gem "rack-obama" + gem "myrack-obama", ">= 1.0" G - lockfile_should_be <<-G + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "myrack", "1.0.0" + c.checksum gem_repo2, "myrack-obama", "1.0" + end + + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) - rack-obama (1.0) - rack + myrack (1.0.0) + myrack-obama (1.0) + myrack PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack-obama - + myrack-obama (>= 1.0) + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end - it "generates a simple lockfile for a single source, gem with a version requirement" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" + it "generates a lockfile without credentials" do + bundle "config set https://localgemserver.test/ user:pass" - gem "rack-obama", ">= 1.0" + install_gemfile(<<-G, artifice: "endpoint_strict_basic_authentication", quiet: true) + source "https://gem.repo1" + + source "https://localgemserver.test/" do + + end + + source "https://user:pass@othergemserver.test/" do + gem "myrack-obama", ">= 1.0" + end G - lockfile_should_be <<-G + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "myrack", "1.0.0" + c.checksum gem_repo2, "myrack-obama", "1.0" + end + + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: - rack (1.0.0) - rack-obama (1.0) - rack + + GEM + remote: https://localgemserver.test/ + specs: + + GEM + remote: https://othergemserver.test/ + specs: + myrack (1.0.0) + myrack-obama (1.0) + myrack PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack-obama (>= 1.0) - + myrack-obama (>= 1.0)! + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end - it "generates a lockfile without credentials for a configured source", :bundler => "< 3" do + it "does not add credentials to lockfile when it does not have them already" do bundle "config set http://localgemserver.test/ user:pass" - install_gemfile(<<-G, :artifice => "endpoint_strict_basic_authentication", :quiet => true) + gemfile <<~G + source "https://gem.repo1" + source "http://localgemserver.test/" do end source "http://user:pass@othergemserver.test/" do - gem "rack-obama", ">= 1.0" + gem "myrack-obama", ">= 1.0" end G - lockfile_should_be <<-G + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "myrack", "1.0.0" + c.checksum gem_repo2, "myrack-obama", "1.0" + end + + lockfile_without_credentials = <<~L GEM remote: http://localgemserver.test/ - remote: http://user:pass@othergemserver.test/ specs: - rack (1.0.0) - rack-obama (1.0) - rack + + GEM + remote: http://othergemserver.test/ + specs: + myrack (1.0.0) + myrack-obama (1.0) + myrack + + GEM + remote: https://gem.repo1/ + specs: PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack-obama (>= 1.0)! - + myrack-obama (>= 1.0)! + #{checksums} BUNDLED WITH - #{Bundler::VERSION} - G + #{Bundler::VERSION} + L + + lockfile lockfile_without_credentials + + # when not re-resolving + bundle "install", artifice: "endpoint_strict_basic_authentication", quiet: true + expect(lockfile).to eq lockfile_without_credentials + + # when re-resolving with full unlock + bundle "update", artifice: "endpoint_strict_basic_authentication" + expect(lockfile).to eq lockfile_without_credentials + + # when re-resolving without ful unlocking + bundle "update myrack-obama", artifice: "endpoint_strict_basic_authentication" + expect(lockfile).to eq lockfile_without_credentials end - it "generates a lockfile without credentials for a configured source", :bundler => "3" do + it "keeps credentials in lockfile if already there" do bundle "config set http://localgemserver.test/ user:pass" - install_gemfile(<<-G, :artifice => "endpoint_strict_basic_authentication", :quiet => true) + gemfile <<~G + source "https://gem.repo1" + source "http://localgemserver.test/" do end source "http://user:pass@othergemserver.test/" do - gem "rack-obama", ">= 1.0" + gem "myrack-obama", ">= 1.0" end G - lockfile_should_be <<-G - GEM - specs: + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "myrack", "1.0.0" + c.checksum gem_repo2, "myrack-obama", "1.0" + end + lockfile_with_credentials = <<~L GEM remote: http://localgemserver.test/ specs: @@ -359,30 +417,45 @@ RSpec.describe "the lockfile format" do GEM remote: http://user:pass@othergemserver.test/ specs: - rack (1.0.0) - rack-obama (1.0) - rack + myrack (1.0.0) + myrack-obama (1.0) + myrack + + GEM + remote: https://gem.repo1/ + specs: PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack-obama (>= 1.0)! - + myrack-obama (>= 1.0)! + #{checksums} BUNDLED WITH - #{Bundler::VERSION} - G + #{Bundler::VERSION} + L + + lockfile lockfile_with_credentials + + bundle "install", artifice: "endpoint_strict_basic_authentication", quiet: true + + expect(lockfile).to eq lockfile_with_credentials end it "generates lockfiles with multiple requirements" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" + source "https://gem.repo2/" gem "net-sftp" G - lockfile_should_be <<-G + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "net-sftp", "1.1.1" + c.checksum gem_repo2, "net-ssh", "1.0" + end + + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: net-sftp (1.1.1) net-ssh (>= 1.0.0, < 1.99.0) @@ -393,57 +466,35 @@ RSpec.describe "the lockfile format" do DEPENDENCIES net-sftp - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{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", :bundler => "< 3" 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 - #{lockfile_platforms} - - DEPENDENCIES - foo! - - BUNDLED WITH - #{Bundler::VERSION} - G - end - it "generates a simple lockfile for a single pinned source, gem with a version requirement" do git = build_git "foo" install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G - lockfile_should_be <<-G + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + end + + expect(lockfile).to eq <<~G GIT remote: #{lib_path("foo-1.0")} - revision: #{git.ref_for("master")} + revision: #{git.ref_for("main")} specs: foo (1.0) GEM + remote: https://gem.repo1/ specs: PLATFORMS @@ -451,23 +502,23 @@ RSpec.describe "the lockfile format" do DEPENDENCIES foo! - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{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") + build_lib "omg", path: lib_path("omg") gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" + source "https://gem.repo2/" platforms :#{not_local_tag} do gem "omg", :path => "#{lib_path("omg")}" end - gem "rack" + gem "myrack" G lockfile <<-L @@ -477,42 +528,48 @@ RSpec.describe "the lockfile format" do specs: GEM - remote: #{file_uri_for(gem_repo1)}// + remote: https://gem.repo2// specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS #{not_local} DEPENDENCIES omg! - rack + myrack BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L - bundle! "install" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle "install" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "serializes global git sources" do git = build_git "foo" + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + end + install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}" do gem "foo" end G - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GIT remote: #{lib_path("foo-1.0")} - revision: #{git.ref_for("master")} + revision: #{git.ref_for("main")} specs: foo (1.0) GEM + remote: https://gem.repo1/ specs: PLATFORMS @@ -520,21 +577,26 @@ RSpec.describe "the lockfile format" do DEPENDENCIES foo! - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{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" + update_git "foo", branch: "omg" + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + end install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "omg" G - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GIT remote: #{lib_path("foo-1.0")} revision: #{git.ref_for("omg")} @@ -543,6 +605,7 @@ RSpec.describe "the lockfile format" do foo (1.0) GEM + remote: https://gem.repo1/ specs: PLATFORMS @@ -550,21 +613,26 @@ RSpec.describe "the lockfile format" do DEPENDENCIES foo! - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{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" + update_git "foo", tag: "omg" + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + end install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}", :tag => "omg" G - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GIT remote: #{lib_path("foo-1.0")} revision: #{git.ref_for("omg")} @@ -573,6 +641,7 @@ RSpec.describe "the lockfile format" do foo (1.0) GEM + remote: https://gem.repo1/ specs: PLATFORMS @@ -580,26 +649,118 @@ RSpec.describe "the lockfile format" do DEPENDENCIES foo! - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end + it "is conservative with dependencies of git gems" do + build_repo4 do + build_gem "orm_adapter", "0.4.1" + build_gem "orm_adapter", "0.5.0" + end + + FileUtils.mkdir_p lib_path("ckeditor/lib") + + @remote = build_git("ckeditor_remote", bare: true) + + build_git "ckeditor", path: lib_path("ckeditor") do |s| + s.write "lib/ckeditor.rb", "CKEDITOR = '4.0.7'" + s.version = "4.0.7" + s.add_dependency "orm_adapter" + end + + update_git "ckeditor", path: lib_path("ckeditor"), remote: @remote.path + update_git "ckeditor", path: lib_path("ckeditor"), tag: "v4.0.7" + old_git = update_git "ckeditor", path: lib_path("ckeditor"), push: "v4.0.7" + + update_git "ckeditor", path: lib_path("ckeditor"), gemspec: true do |s| + s.write "lib/ckeditor.rb", "CKEDITOR = '4.0.8'" + s.version = "4.0.8" + s.add_dependency "orm_adapter" + end + update_git "ckeditor", path: lib_path("ckeditor"), tag: "v4.0.8" + + new_git = update_git "ckeditor", path: lib_path("ckeditor"), push: "v4.0.8" + + gemfile <<-G + source "https://gem.repo4" + gem "ckeditor", :git => "#{@remote.path}", :tag => "v4.0.8" + G + + lockfile <<~L + GIT + remote: #{@remote.path} + revision: #{old_git.ref_for("v4.0.7")} + tag: v4.0.7 + specs: + ckeditor (4.0.7) + orm_adapter + + GEM + remote: https://gem.repo4/ + specs: + orm_adapter (0.4.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ckeditor! + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock" + + # Bumps the git gem, but keeps its dependency locked + expect(lockfile).to eq <<~L + GIT + remote: #{@remote.path} + revision: #{new_git.ref_for("v4.0.8")} + tag: v4.0.8 + specs: + ckeditor (4.0.8) + orm_adapter + + GEM + remote: https://gem.repo4/ + specs: + orm_adapter (0.4.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ckeditor! + + BUNDLED WITH + #{Bundler::VERSION} + L + end + it "serializes pinned path sources to the lockfile" do build_lib "foo" + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + end + install_gemfile <<-G + source "https://gem.repo1" gem "foo", :path => "#{lib_path("foo-1.0")}" G - lockfile_should_be <<-G + expect(lockfile).to eq <<~G PATH remote: #{lib_path("foo-1.0")} specs: foo (1.0) GEM + remote: https://gem.repo1/ specs: PLATFORMS @@ -607,30 +768,35 @@ RSpec.describe "the lockfile format" do DEPENDENCIES foo! - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end it "serializes pinned path sources to the lockfile even when packaging" do build_lib "foo" - install_gemfile! <<-G + install_gemfile <<-G + source "https://gem.repo1" gem "foo", :path => "#{lib_path("foo-1.0")}" G - bundle "config set cache_all true" - bundle! :cache - bundle! :install, :local => true + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + end + + bundle :cache + bundle :install, local: true - lockfile_should_be <<-G + expect(lockfile).to eq <<~G PATH remote: #{lib_path("foo-1.0")} specs: foo (1.0) GEM + remote: https://gem.repo1/ specs: PLATFORMS @@ -638,9 +804,9 @@ RSpec.describe "the lockfile format" do DEPENDENCIES foo! - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end @@ -648,18 +814,24 @@ RSpec.describe "the lockfile format" do build_lib "foo" bar = build_git "bar" + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.no_checksum "bar", "1.0" + c.checksum gem_repo2, "myrack", "1.0.0" + end + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" + source "https://gem.repo2/" - gem "rack" + gem "myrack" gem "foo", :path => "#{lib_path("foo-1.0")}" gem "bar", :git => "#{lib_path("bar-1.0")}" G - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GIT remote: #{lib_path("bar-1.0")} - revision: #{bar.ref_for("master")} + revision: #{bar.ref_for("main")} specs: bar (1.0) @@ -669,9 +841,9 @@ RSpec.describe "the lockfile format" do foo (1.0) GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS #{lockfile_platforms} @@ -679,58 +851,104 @@ RSpec.describe "the lockfile format" do DEPENDENCIES bar! foo! - rack + myrack + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "removes redundant sources" do + install_gemfile <<-G + source "https://gem.repo2/" + + gem "myrack", :source => "https://gem.repo2/" + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "myrack", "1.0.0" + end + + expect(lockfile).to eq <<~G + GEM + remote: https://gem.repo2/ + specs: + myrack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + DEPENDENCIES + myrack! + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end it "lists gems alphabetically" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" + source "https://gem.repo2/" gem "thin" gem "actionpack" - gem "rack-obama" + gem "myrack-obama" G - lockfile_should_be <<-G + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "actionpack", "2.3.2" + c.checksum gem_repo2, "activesupport", "2.3.2" + c.checksum gem_repo2, "myrack", "1.0.0" + c.checksum gem_repo2, "myrack-obama", "1.0" + c.checksum gem_repo2, "thin", "1.0" + end + + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: actionpack (2.3.2) activesupport (= 2.3.2) activesupport (2.3.2) - rack (1.0.0) - rack-obama (1.0) - rack + myrack (1.0.0) + myrack-obama (1.0) + myrack thin (1.0) - rack + myrack PLATFORMS #{lockfile_platforms} DEPENDENCIES actionpack - rack-obama + myrack-obama thin - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end it "orders dependencies' dependencies in alphabetical order" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" + source "https://gem.repo2/" gem "rails" G - lockfile_should_be <<-G + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "actionmailer", "2.3.2" + c.checksum gem_repo2, "actionpack", "2.3.2" + c.checksum gem_repo2, "activerecord", "2.3.2" + c.checksum gem_repo2, "activeresource", "2.3.2" + c.checksum gem_repo2, "activesupport", "2.3.2" + c.checksum gem_repo2, "rails", "2.3.2" + c.checksum gem_repo2, "rake", rake_version + end + + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: actionmailer (2.3.2) activesupport (= 2.3.2) @@ -746,29 +964,44 @@ RSpec.describe "the lockfile format" do actionpack (= 2.3.2) activerecord (= 2.3.2) activeresource (= 2.3.2) - rake (= 12.3.2) - rake (12.3.2) + rake (= #{rake_version}) + rake (#{rake_version}) PLATFORMS #{lockfile_platforms} DEPENDENCIES rails - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end it "orders dependencies by version" do + update_repo2 do + # 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/ruby/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 + end + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" + source "https://gem.repo2" gem 'double_deps' G - lockfile_should_be <<-G + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "double_deps", "1.0" + c.checksum gem_repo2, "net-ssh", "1.0" + end + + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: double_deps (1.0) net-ssh @@ -780,80 +1013,96 @@ RSpec.describe "the lockfile format" do DEPENDENCIES double_deps - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end it "does not add the :require option to the lockfile" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" + source "https://gem.repo2/" - gem "rack-obama", ">= 1.0", :require => "rack/obama" + gem "myrack-obama", ">= 1.0", :require => "myrack/obama" G - lockfile_should_be <<-G + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "myrack", "1.0.0" + c.checksum gem_repo2, "myrack-obama", "1.0" + end + + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) - rack-obama (1.0) - rack + myrack (1.0.0) + myrack-obama (1.0) + myrack PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack-obama (>= 1.0) - + myrack-obama (>= 1.0) + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end it "does not add the :group option to the lockfile" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" + source "https://gem.repo2/" - gem "rack-obama", ">= 1.0", :group => :test + gem "myrack-obama", ">= 1.0", :group => :test G - lockfile_should_be <<-G + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "myrack", "1.0.0" + c.checksum gem_repo2, "myrack-obama", "1.0" + end + + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) - rack-obama (1.0) - rack + myrack (1.0.0) + myrack-obama (1.0) + myrack PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack-obama (>= 1.0) - + myrack-obama (>= 1.0) + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{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") + build_lib "foo", path: bundled_app("foo") + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + end install_gemfile <<-G + source "https://gem.repo1" path "foo" do gem "foo" end G - lockfile_should_be <<-G + expect(lockfile).to eq <<~G PATH remote: foo specs: foo (1.0) GEM + remote: https://gem.repo1/ specs: PLATFORMS @@ -861,28 +1110,34 @@ RSpec.describe "the lockfile format" do DEPENDENCIES foo! - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{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")) + build_lib "foo", path: bundled_app(File.join("..", "foo")) + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + end install_gemfile <<-G + source "https://gem.repo1" path "../foo" do gem "foo" end G - lockfile_should_be <<-G + expect(lockfile).to eq <<~G PATH remote: ../foo specs: foo (1.0) GEM + remote: https://gem.repo1/ specs: PLATFORMS @@ -890,28 +1145,34 @@ RSpec.describe "the lockfile format" do DEPENDENCIES foo! - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{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") + build_lib "foo", path: bundled_app("foo") install_gemfile <<-G - path File.expand_path("../foo", __FILE__) do + source "https://gem.repo1" + path File.expand_path("foo", __dir__) do gem "foo" end G - lockfile_should_be <<-G + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + end + + expect(lockfile).to eq <<~G PATH remote: foo specs: foo (1.0) GEM + remote: https://gem.repo1/ specs: PLATFORMS @@ -919,26 +1180,32 @@ RSpec.describe "the lockfile format" do DEPENDENCIES foo! - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end it "stores relative paths when the path is provided for gemspec" do - build_lib("foo", :path => tmp.join("foo")) + build_lib("foo", path: tmp("foo")) + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + end install_gemfile <<-G + source "https://gem.repo1" gemspec :path => "../foo" G - lockfile_should_be <<-G + expect(lockfile).to eq <<~G PATH remote: ../foo specs: foo (1.0) GEM + remote: https://gem.repo1/ specs: PLATFORMS @@ -946,471 +1213,1143 @@ RSpec.describe "the lockfile format" do DEPENDENCIES foo! - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end - it "keeps existing platforms in the lockfile", :bundler => "< 3" do + it "keeps existing platforms in the lockfile" do + checksums = checksums_section_when_enabled do |c| + c.no_checksum "myrack", "1.0.0" + end + lockfile <<-G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS java DEPENDENCIES - rack - + myrack + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" + source "https://gem.repo2/" - gem "rack" + gem "myrack" G - lockfile_should_be <<-G + checksums.checksum(gem_repo2, "myrack", "1.0.0") + + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS - java - #{generic_local_platform} + #{lockfile_platforms("java", local_platform, defaults: [])} DEPENDENCIES - rack - + myrack + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end - it "keeps existing platforms in the lockfile", :bundler => "3" do - lockfile <<-G + it "adds compatible platform specific variants to the lockfile, even if resolution fallback to ruby due to some other incompatible platform specific variant" do + simulate_platform "arm64-darwin-23" do + build_repo4 do + build_gem "google-protobuf", "3.25.1" + build_gem "google-protobuf", "3.25.1" do |s| + s.platform = "arm64-darwin-23" + end + build_gem "google-protobuf", "3.25.1" do |s| + s.platform = "x64-mingw-ucrt" + s.required_ruby_version = "> #{Gem.ruby_version}" + end + end + + gemfile <<-G + source "https://gem.repo4" + gem "google-protobuf" + G + bundle "lock --add-platform x64-mingw-ucrt" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "google-protobuf", "3.25.1" + c.checksum gem_repo4, "google-protobuf", "3.25.1", "arm64-darwin-23" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + google-protobuf (3.25.1) + google-protobuf (3.25.1-arm64-darwin-23) + + PLATFORMS + arm64-darwin-23 + ruby + x64-mingw-ucrt + + DEPENDENCIES + google-protobuf + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "persists the spec's specific platform to the lockfile" do + build_repo2 do + build_gem "platform_specific", "1.0" do |s| + s.platform = Gem::Platform.new("universal-java-16") + end + end + + simulate_platform "universal-java-16" do + install_gemfile <<-G + source "https://gem.repo2" + gem "platform_specific" + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "platform_specific", "1.0", "universal-java-16" + end + + expect(lockfile).to eq <<~G + GEM + remote: https://gem.repo2/ + specs: + platform_specific (1.0-universal-java-16) + + PLATFORMS + universal-java-16 + + DEPENDENCIES + platform_specific + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G + end + end + + it "does not add duplicate gems" do + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo2, "activesupport", "2.3.5") + c.checksum(gem_repo2, "myrack", "1.0.0") + end + + install_gemfile <<-G + source "https://gem.repo2/" + gem "myrack" + G + + install_gemfile <<-G + source "https://gem.repo2/" + gem "myrack" + gem "activesupport" + G + + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) + activesupport (2.3.5) + myrack (1.0.0) PLATFORMS - java + #{lockfile_platforms} DEPENDENCIES - rack - + activesupport + myrack + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G + end - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" + it "does not add duplicate dependencies" do + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo2, "myrack", "1.0.0") + end - gem "rack" + install_gemfile <<-G + source "https://gem.repo2/" + gem "myrack" + gem "myrack" G - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS - java - #{generic_local_platform} - #{specific_local_platform} + #{lockfile_platforms} DEPENDENCIES - rack - + myrack + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end - it "persists the spec's platform to the lockfile", :bundler => "< 3" do - build_repo2 do - build_gem "platform_specific", "1.0" do |s| - s.platform = Gem::Platform.new("universal-java-16") - end + it "does not add duplicate dependencies with versions" do + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo2, "myrack", "1.0.0") end - simulate_platform "universal-java-16" - - install_gemfile! <<-G - source "#{file_uri_for(gem_repo2)}" - gem "platform_specific" + install_gemfile <<-G + source "https://gem.repo2/" + gem "myrack", "1.0" + gem "myrack", "1.0" G - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo2)}/ + remote: https://gem.repo2/ specs: - platform_specific (1.0-java) + myrack (1.0.0) PLATFORMS - java + #{lockfile_platforms} DEPENDENCIES - platform_specific - + myrack (= 1.0) + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end - it "persists the spec's platform and specific platform to the lockfile", :bundler => "3" do - build_repo2 do - build_gem "platform_specific", "1.0" do |s| - s.platform = Gem::Platform.new("universal-java-16") - end + it "does not add duplicate dependencies in different groups" do + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo2, "myrack", "1.0.0") end - simulate_platform "universal-java-16" - - install_gemfile! <<-G - source "#{file_uri_for(gem_repo2)}" - gem "platform_specific" + install_gemfile <<-G + source "https://gem.repo2/" + gem "myrack", "1.0", :group => :one + gem "myrack", "1.0", :group => :two G - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo2)}/ + remote: https://gem.repo2/ specs: - platform_specific (1.0-java) - platform_specific (1.0-universal-java-16) + myrack (1.0.0) PLATFORMS - java - universal-java-16 + #{lockfile_platforms} DEPENDENCIES - platform_specific - + myrack (= 1.0) + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end - it "does not add duplicate gems" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" - gem "rack" + it "raises if two different versions are used" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2/" + gem "myrack", "1.0" + gem "myrack", "1.1" + G + + expect(bundled_app_lock).not_to exist + expect(err).to include "myrack (= 1.0) and myrack (= 1.1)" + end + + it "raises if two different sources are used" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2/" + gem "myrack" + gem "myrack", :git => "git://hubz.com" G + expect(bundled_app_lock).not_to exist + expect(err).to include "myrack (>= 0) should come from an unspecified source and git://hubz.com" + end + + it "works correctly with multiple version dependencies" do + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo2, "myrack", "0.9.1") + end + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" - gem "rack" - gem "activesupport" + source "https://gem.repo2/" + gem "myrack", "> 0.9", "< 1.0" G - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - activesupport (2.3.5) - rack (1.0.0) + myrack (0.9.1) PLATFORMS #{lockfile_platforms} DEPENDENCIES - activesupport - rack - + myrack (> 0.9, < 1.0) + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end - it "does not add duplicate dependencies" do + it "captures the Ruby version in the lockfile" do + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo2, "myrack", "0.9.1") + end + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" - gem "rack" - gem "rack" + source "https://gem.repo2/" + ruby '#{Gem.ruby_version}' + gem "myrack", "> 0.9", "< 1.0" G - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) + myrack (0.9.1) PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack + myrack (> 0.9, < 1.0) + #{checksums} + RUBY VERSION + #{Bundler::RubyVersion.system} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end - it "does not add duplicate dependencies with versions" do + it "automatically fixes the lockfile when it's missing deps" do + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + myrack_middleware (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack_middleware + + BUNDLED WITH + #{Bundler::VERSION} + L + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" - gem "rack", "1.0" - gem "rack", "1.0" + source "https://gem.repo2" + gem "myrack_middleware" G - lockfile_should_be <<-G + expect(lockfile).to eq <<~L GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) + myrack (0.9.1) + myrack_middleware (1.0) + myrack (= 0.9.1) PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack (= 1.0) + myrack_middleware BUNDLED WITH - #{Bundler::VERSION} - G + #{Bundler::VERSION} + L end - it "does not add duplicate dependencies in different groups" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" - gem "rack", "1.0", :group => :one - gem "rack", "1.0", :group => :two + it "raises a clear error when frozen mode is set and lockfile is missing deps, and does not install any gems" do + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + myrack_middleware (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack_middleware + + BUNDLED WITH + #{Bundler::VERSION} + L + + install_gemfile <<-G, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false + source "https://gem.repo2" + gem "myrack_middleware" G - lockfile_should_be <<-G + expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack_middleware-1.0") + expect(err).to include("myrack: gemspec specifies = 0.9.1, not in lockfile") + expect(the_bundle).not_to include_gems "myrack_middleware 1.0" + end + + it "raises a clear error when frozen mode is set and lockfile is missing entries in CHECKSUMS section, and does not install any gems" do + lockfile <<-L GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) + myrack_middleware (1.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack (= 1.0) + myrack_middleware + + CHECKSUMS BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} + L + + install_gemfile <<-G, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false + source "https://gem.repo2" + gem "myrack_middleware" G + + expect(err).to eq <<~L.strip + Your lockfile is missing a CHECKSUMS entry for \"myrack_middleware\", but can't be updated because frozen mode is set + + Run `bundle install` elsewhere and add the updated Gemfile.lock to version control. + L + + expect(the_bundle).not_to include_gems "myrack_middleware 1.0" end - it "raises if two different versions are used" do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" - gem "rack", "1.0" - gem "rack", "1.1" + it "raises a clear error when frozen mode is set and lockfile has empty checksums in CHECKSUMS section, and does not install any gems" do + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + myrack (0.9.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + + CHECKSUMS + myrack (0.9.1) + + BUNDLED WITH + #{Bundler::VERSION} + L + + install_gemfile <<-G, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false + source "https://gem.repo2" + gem "myrack" G - expect(bundled_app("Gemfile.lock")).not_to exist - expect(err).to include "rack (= 1.0) and rack (= 1.1)" + expect(err).to eq <<~L.strip + Your lockfile has an empty CHECKSUMS entry for \"myrack\", but can't be updated because frozen mode is set + + Run `bundle install` elsewhere and add the updated Gemfile.lock to version control. + L + + expect(the_bundle).not_to include_gems "myrack 0.9.1" end - it "raises if two different sources are used" do + it "automatically fixes the lockfile when it's missing deps, they conflict with other locked deps, but conflicts are fixable" do + build_repo4 do + build_gem "other_dep", "0.9" + build_gem "other_dep", "1.0" + + build_gem "myrack", "0.9.1" + + build_gem "myrack_middleware", "1.0" do |s| + s.add_dependency "myrack", "= 0.9.1" + s.add_dependency "other_dep", "= 0.9" + end + end + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack_middleware (1.0) + other_dep (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack_middleware + other_dep + + BUNDLED WITH + #{Bundler::VERSION} + L + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" - gem "rack" - gem "rack", :git => "git://hubz.com" + source "https://gem.repo4" + gem "myrack_middleware" + gem "other_dep" G - expect(bundled_app("Gemfile.lock")).not_to exist - expect(err).to include "rack (>= 0) should come from an unspecified source and git://hubz.com (at master)" + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (0.9.1) + myrack_middleware (1.0) + myrack (= 0.9.1) + other_dep (= 0.9) + other_dep (0.9) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack_middleware + other_dep + + BUNDLED WITH + #{Bundler::VERSION} + L end - it "works correctly with multiple version dependencies" do + it "automatically fixes the lockfile when it's missing multiple deps, they conflict with other locked deps, but conflicts are fixable" do + build_repo4 do + build_gem "other_dep", "0.9" + build_gem "other_dep", "1.0" + + build_gem "myrack", "0.9.1" + + build_gem "myrack_middleware", "1.0" do |s| + s.add_dependency "myrack", "= 0.9.1" + end + + build_gem "another_dep_middleware", "1.0" do |s| + s.add_dependency "other_dep", "= 0.9" + end + end + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack_middleware (1.0) + another_dep_middleware (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack_middleware + another_dep_middleware + + BUNDLED WITH + #{Bundler::VERSION} + L + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" - gem "rack", "> 0.9", "< 1.0" + source "https://gem.repo4" + gem "myrack_middleware" + gem "another_dep_middleware" G - lockfile_should_be <<-G + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + another_dep_middleware (1.0) + other_dep (= 0.9) + myrack (0.9.1) + myrack_middleware (1.0) + myrack (= 0.9.1) + other_dep (0.9) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + another_dep_middleware + myrack_middleware + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "raises a resolution error when lockfile is missing deps, they conflict with other locked deps, and conflicts are not fixable" do + build_repo4 do + build_gem "other_dep", "0.9" + build_gem "other_dep", "1.0" + + build_gem "myrack", "0.9.1" + + build_gem "myrack_middleware", "1.0" do |s| + s.add_dependency "myrack", "= 0.9.1" + s.add_dependency "other_dep", "= 0.9" + end + end + + lockfile <<~L GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo4/ specs: - rack (0.9.1) + myrack_middleware (1.0) + other_dep (1.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack (> 0.9, < 1.0) + myrack_middleware + other_dep BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} + L + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo4" + gem "myrack_middleware" + gem "other_dep", "1.0" G + + expect(err).to eq <<~ERROR.strip + Could not find compatible versions + + Because every version of myrack_middleware depends on other_dep = 0.9 + and Gemfile depends on myrack_middleware >= 0, + other_dep = 0.9 is required. + So, because Gemfile depends on other_dep = 1.0, + version solving has failed. + ERROR end - it "captures the Ruby version in the lockfile" do + it "regenerates a lockfile with no specs" do + build_repo4 do + build_gem "indirect_dependency", "1.2.3" do |s| + s.metadata["funding_uri"] = "https://example.com/donate" + end + + build_gem "direct_dependency", "4.5.6" do |s| + s.add_dependency "indirect_dependency", ">= 0" + end + end + + lockfile <<-G + GEM + remote: https://gem.repo4/ + specs: + + PLATFORMS + ruby + + DEPENDENCIES + direct_dependency + + BUNDLED WITH + #{Bundler::VERSION} + G + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" - ruby '#{RUBY_VERSION}' - gem "rack", "> 0.9", "< 1.0" + source "https://gem.repo4" + + gem "direct_dependency" G - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo4/ specs: - rack (0.9.1) + direct_dependency (4.5.6) + indirect_dependency + indirect_dependency (1.2.3) PLATFORMS - #{lockfile_platforms} + #{lockfile_platforms(generic_default_locked_platform || local_platform, defaults: ["ruby"])} DEPENDENCIES - rack (> 0.9, < 1.0) + direct_dependency - RUBY VERSION - ruby #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "automatically fixes the lockfile when it's missing deps and the full index is in use" do + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + myrack_middleware (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack_middleware BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} + L + + install_gemfile <<-G + source "#{file_uri_for(gem_repo2)}" + gem "myrack_middleware" G + + expect(lockfile).to eq <<~L + GEM + remote: #{file_uri_for(gem_repo2)}/ + specs: + myrack (0.9.1) + myrack_middleware (1.0) + myrack (= 0.9.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack_middleware + + BUNDLED WITH + #{Bundler::VERSION} + L 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")) + it "automatically fixes the lockfile when it includes a gem under the correct GIT section, but also under an incorrect GEM section, with a higher version, and with no explicit Gemfile requirement" do + git = build_git "foo" - gemfile <<-G - source "#{file_uri_for(gem_repo1)}/" - gem "omg", :git => "#{lib_path("omg")}", :branch => 'master' + gemfile <<~G + source "https://gem.repo1/" + gem "foo", git: "#{lib_path("foo-1.0")}" G - bundle! :install, forgotten_command_line_options(:path => "vendor") - expect(the_bundle).to include_gems "omg 1.0" + # If the lockfile erroneously lists platform versions of the gem + # that don't match the locked version of the git repo we should remove them. - # Create a Gemfile.lock that has duplicate GIT sections - lockfile <<-L + lockfile <<~L GIT - remote: #{lib_path("omg")} - revision: #{revision} - branch: master + remote: #{lib_path("foo-1.0")} + revision: #{git.ref_for("main")} + specs: + foo (1.0) + + GEM + remote: https://gem.repo1/ specs: - omg (1.0) + foo (1.1-x86_64-linux-gnu) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install" + + expect(lockfile).to eq <<~L GIT - remote: #{lib_path("omg")} - revision: #{revision} - branch: master + remote: #{lib_path("foo-1.0")} + revision: #{git.ref_for("main")} specs: - omg (1.0) + foo (1.0) GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: PLATFORMS #{lockfile_platforms} DEPENDENCIES - omg! + foo! + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "automatically fixes the lockfile when it includes a gem under the correct GIT section, but also under an incorrect GEM section, with a higher version" do + git = build_git "foo" + + gemfile <<~G + source "https://gem.repo1/" + gem "foo", "= 1.0", git: "#{lib_path("foo-1.0")}" + G + + # If the lockfile erroneously lists platform versions of the gem + # that don't match the locked version of the git repo we should remove them. + + lockfile <<~L + GIT + remote: #{lib_path("foo-1.0")} + revision: #{git.ref_for("main")} + specs: + foo (1.0) + + GEM + remote: https://gem.repo1/ + specs: + foo (1.1-x86_64-linux-gnu) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo (= 1.0)! BUNDLED WITH - #{Bundler::VERSION} + #{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 - lockfile_should_be(<<-L) + expect(lockfile).to eq <<~L GIT - remote: #{lib_path("omg")} - revision: #{revision} - branch: master + remote: #{lib_path("foo-1.0")} + revision: #{git.ref_for("main")} specs: - omg (1.0) + foo (1.0) GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: PLATFORMS #{lockfile_platforms} DEPENDENCIES - omg! + foo (= 1.0)! BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L end - it "raises a helpful error message when the lockfile is missing deps" do - lockfile <<-L + it "automatically fixes the lockfile when it has incorrect deps, keeping the locked version" do + build_repo4 do + build_gem "net-smtp", "0.5.0" do |s| + s.add_dependency "net-protocol" + end + + build_gem "net-smtp", "0.5.1" do |s| + s.add_dependency "net-protocol" + end + + build_gem "net-protocol", "0.2.2" + end + + gemfile <<~G + source "#{file_uri_for(gem_repo4)}" + gem "net-smtp" + G + + lockfile <<~L GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: #{file_uri_for(gem_repo4)}/ specs: - rack_middleware (1.0) + net-protocol (0.2.2) + net-smtp (0.5.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack_middleware + net-smtp + + BUNDLED WITH + #{Bundler::VERSION} L - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack_middleware" + bundle "install" + + expect(lockfile).to eq <<~L + GEM + remote: #{file_uri_for(gem_repo4)}/ + specs: + net-protocol (0.2.2) + net-smtp (0.5.0) + net-protocol + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + net-smtp + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "successfully updates the lockfile when a new gem is added in the Gemfile includes a gem that shouldn't be included" do + build_repo4 do + build_gem "logger", "1.7.0" + build_gem "rack", "3.2.0" + build_gem "net-smtp", "0.5.0" + end + + gemfile <<~G + source "#{file_uri_for(gem_repo4)}" + gem "logger" + gem "net-smtp" + + install_if -> { false } do + gem 'rack', github: 'rack/rack' + end + G + + lockfile <<~L + GIT + remote: https://github.com/rack/rack.git + revision: 2fface9ac09fc582a81386becd939c987ad33f99 + specs: + rack (3.2.0) + + GEM + remote: #{file_uri_for(gem_repo4)}/ + specs: + logger (1.7.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + logger + rack! + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install" + + expect(lockfile).to eq <<~L + GIT + remote: https://github.com/rack/rack.git + revision: 2fface9ac09fc582a81386becd939c987ad33f99 + specs: + rack (3.2.0) + + GEM + remote: #{file_uri_for(gem_repo4)}/ + specs: + logger (1.7.0) + net-smtp (0.5.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + logger + net-smtp + rack! + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + shared_examples_for "a lockfile missing dependent specs" do + it "auto-heals" do + build_repo4 do + build_gem "minitest-bisect", "1.6.0" do |s| + s.add_dependency "path_expander", "~> 1.1" + end + + build_gem "path_expander", "1.1.1" + end + + gemfile <<~G + source "https://gem.repo4" + gem "minitest-bisect" + G + + # Corrupt lockfile (completely missing path_expander) + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + minitest-bisect (1.6.0) + + PLATFORMS + #{platforms} + + DEPENDENCIES + minitest-bisect + + BUNDLED WITH + #{Bundler::VERSION} + L + + cache_gems "minitest-bisect-1.6.0", "path_expander-1.1.1", gem_repo: gem_repo4 + bundle :install + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + minitest-bisect (1.6.0) + path_expander (~> 1.1) + path_expander (1.1.1) + + PLATFORMS + #{platforms} + + DEPENDENCIES + minitest-bisect + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "with just specific platform" do + let(:platforms) { lockfile_platforms } + + it_behaves_like "a lockfile missing dependent specs" + end + + context "with both ruby and specific platform" do + let(:platforms) { lockfile_platforms("ruby") } + + it_behaves_like "a lockfile missing dependent specs" + end + + it "auto-heals when the lockfile is missing specs" do + build_repo4 do + build_gem "minitest-bisect", "1.6.0" do |s| + s.add_dependency "path_expander", "~> 1.1" + end + + build_gem "path_expander", "1.1.1" + end + + gemfile <<~G + source "https://gem.repo4" + gem "minitest-bisect" G - expect(err).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.") + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + minitest-bisect (1.6.0) + path_expander (~> 1.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + minitest-bisect + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install --verbose" + expect(out).to include("re-resolving dependencies because your lockfile includes \"minitest-bisect\" but not some of its dependencies") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + minitest-bisect (1.6.0) + path_expander (~> 1.1) + path_expander (1.1.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + minitest-bisect + + BUNDLED WITH + #{Bundler::VERSION} + L 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")) + File.utime(time, time, bundled_app_lock) end before(:each) do build_repo2 install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "rack" + source "https://gem.repo2" + gem "myrack" 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" + expect(File.read(bundled_app_lock)).not_to match("\r\n") + expect(the_bundle).to include_gems "myrack 1.0" end context "during updates" do it "preserves Gemfile.lock \\n line endings" do - update_repo2 - - expect { bundle "update", :all => true }.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" + update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + end + + expect { bundle "update", all: true }.to change { File.mtime(bundled_app_lock) } + expect(File.read(bundled_app_lock)).not_to match("\r\n") + expect(the_bundle).to include_gems "myrack 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) } + skip "needs to be adapted" if Gem.win_platform? + + update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + end + + win_lock = File.read(bundled_app_lock).gsub(/\n/, "\r\n") + File.open(bundled_app_lock, "wb") {|f| f.puts(win_lock) } set_lockfile_mtime_to_known_value - expect { bundle "update", :all => true }.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" + expect { bundle "update", all: true }.to change { File.mtime(bundled_app_lock) } + expect(File.read(bundled_app_lock)).to match("\r\n") + + expect(the_bundle).to include_gems "myrack 1.2" end end @@ -1421,12 +2360,12 @@ RSpec.describe "the lockfile format" do require 'bundler' Bundler.setup RUBY - end.not_to change { File.mtime(bundled_app("Gemfile.lock")) } + end.not_to change { File.mtime(bundled_app_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) } + win_lock = File.read(bundled_app_lock).gsub(/\n/, "\r\n") + File.open(bundled_app_lock, "wb") {|f| f.puts(win_lock) } set_lockfile_mtime_to_known_value expect do @@ -1434,7 +2373,7 @@ RSpec.describe "the lockfile format" do require 'bundler' Bundler.setup RUBY - end.not_to change { File.mtime(bundled_app("Gemfile.lock")) } + end.not_to change { File.mtime(bundled_app_lock) } end end end @@ -1442,48 +2381,36 @@ RSpec.describe "the lockfile format" do it "refuses to install if Gemfile.lock contains conflict markers" do lockfile <<-L GEM - remote: #{file_uri_for(gem_repo1)}// + remote: https://gem.repo2// specs: <<<<<<< - rack (1.0.0) + myrack (1.0.0) ======= - rack (1.0.1) + myrack (1.0.1) >>>>>>> PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack + myrack BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L - install_gemfile(<<-G) - source "#{file_uri_for(gem_repo1)}/" - gem "rack" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2/" + gem "myrack" G expect(err).to match(/your Gemfile.lock contains merge conflicts/i) expect(err).to match(/git checkout HEAD -- Gemfile.lock/i) end -private - - def prerelease?(version) - Gem::Version.new(version).prerelease? - end + private def previous_major(version) version.split(".").map.with_index {|v, i| i == 0 ? v.to_i - 1 : v }.join(".") end - - def bump_minor(version) - bump(version, 1) - end - - def bump(version, segment) - version.split(".").map.with_index {|v, i| i == segment ? v.to_i + 1 : v }.join(".") - end end diff --git a/spec/bundler/other/cli_dispatch_spec.rb b/spec/bundler/other/cli_dispatch_spec.rb index 0082606d7e..a2c745b070 100644 --- a/spec/bundler/other/cli_dispatch_spec.rb +++ b/spec/bundler/other/cli_dispatch_spec.rb @@ -2,19 +2,19 @@ RSpec.describe "bundle command names" do it "work when given fully" do - bundle "install" + bundle "install", raise_on_error: false expect(err).to eq("Could not locate Gemfile") - expect(last_command.stdboth).not_to include("Ambiguous command") + expect(stdboth).not_to include("Ambiguous command") end it "work when not ambiguous" do - bundle "ins" + bundle "ins", raise_on_error: false expect(err).to eq("Could not locate Gemfile") - expect(last_command.stdboth).not_to include("Ambiguous command") + expect(stdboth).not_to include("Ambiguous command") end it "print a friendly error when ambiguous" do - bundle "in" - expect(err).to eq("Ambiguous command in matches [info, init, inject, install]") + bundle "in", raise_on_error: false + expect(err).to eq("Ambiguous command in matches [info, init, install]") end end diff --git a/spec/bundler/other/cli_man_pages_spec.rb b/spec/bundler/other/cli_man_pages_spec.rb new file mode 100644 index 0000000000..4e8f155309 --- /dev/null +++ b/spec/bundler/other/cli_man_pages_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +RSpec.describe "bundle commands" do + it "expects all commands to have all options and subcommands documented" do + check_commands!(Bundler::CLI) + + Bundler::CLI.subcommand_classes.each_value do |klass| + check_commands!(klass) + end + end + + private + + def check_commands!(command_class) + command_class.commands.each do |command_name, command| + if command.is_a?(Bundler::Thor::HiddenCommand) + man_page = man_page(command_name) + expect(man_page).not_to exist + expect(main_man_page.read).not_to include("bundle #{command_name}") + elsif command_class == Bundler::CLI + man_page = man_page(command_name) + expect(man_page).to exist + + check_options!(command, man_page) + else + man_page = man_page(command.ancestor_name) + expect(man_page).to exist + + check_options!(command, man_page) + check_subcommand!(command_name, man_page) + end + end + end + + def check_options!(command, man_page) + command.options.each do |_, option| + check_option!(option, man_page) + end + end + + def check_option!(option, man_page) + man_page_content = man_page.read + + aliases = option.aliases + formatted_aliases = aliases.sort.map {|name| "`#{name}`" }.join(", ") if aliases + + help = if option.type == :boolean + "* #{append_aliases("`#{option.switch_name}`", formatted_aliases)}:" + elsif option.enum + formatted_aliases = "`#{option.switch_name}`" if aliases.empty? && option.lazy_default + "* #{prepend_aliases(option.enum.sort.map {|enum| "`#{option.switch_name}=#{enum}`" }.join(", "), formatted_aliases)}:" + else + names = [option.switch_name, *aliases] + value = + case option.type + when :array then "<list>" + when :numeric then "<number>" + else option.name.upcase + end + + value = option.type != :numeric && option.lazy_default ? "[=#{value}]" : "=#{value}" + + "* #{names.map {|name| "`#{name}#{value}`" }.join(", ")}:" + end + + if option.banner.include?("(removed)") + expect(man_page_content).not_to include(help) + else + expect(man_page_content).to include(help) + end + end + + def check_subcommand!(name, man_page) + expect(man_page.read).to match(name) + end + + def append_aliases(text, aliases) + return text if aliases.empty? + + "#{text}, #{aliases}" + end + + def prepend_aliases(text, aliases) + return text if aliases.empty? + + "#{aliases}, #{text}" + end + + def man_page_content(command_name) + man_page(command_name).read + end + + def man_page(command_name) + source_root.join("lib/bundler/man/bundle-#{command_name}.1.ronn") + end + + def main_man_page + source_root.join("lib/bundler/man/bundle.1.ronn") + end +end diff --git a/spec/bundler/other/ext_spec.rb b/spec/bundler/other/ext_spec.rb index f2a512e629..a883eefe06 100644 --- a/spec/bundler/other/ext_spec.rb +++ b/spec/bundler/other/ext_spec.rb @@ -1,61 +1,50 @@ # frozen_string_literal: true -RSpec.describe "Gem::Specification#match_platform" do +RSpec.describe "Gem::Specification#installable_on_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) + expect(darwin.installable_on_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) + expect(lazy_spec.installable_on_platform?(pl("x86-mingw32"))).to eq(true) + expect(lazy_spec.installable_on_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_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G end it "does not explode when called" do - run "Gem.source_index.refresh!" - run "Gem::SourceIndex.new([]).refresh!" + run "Gem.source_index.refresh!", raise_on_error: false + run "Gem::SourceIndex.new([]).refresh!", raise_on_error: false + end +end + +RSpec.describe "Gem::NameTuple" do + describe "#initialize" do + it "creates a Gem::NameTuple with equality regardless of platform type" do + gem_platform = Gem::NameTuple.new "a", v("1"), pl("x86_64-linux") + str_platform = Gem::NameTuple.new "a", v("1"), "x86_64-linux" + expect(gem_platform).to eq(str_platform) + expect(gem_platform.hash).to eq(str_platform.hash) + expect(gem_platform.to_a).to eq(str_platform.to_a) + end + end + + describe "#lock_name" do + it "returns the lock name" do + expect(Gem::NameTuple.new("a", v("1.0.0"), pl("x86_64-linux")).lock_name).to eq("a (1.0.0-x86_64-linux)") + expect(Gem::NameTuple.new("a", v("1.0.0"), "ruby").lock_name).to eq("a (1.0.0)") + expect(Gem::NameTuple.new("a", v("1.0.0")).lock_name).to eq("a (1.0.0)") + end end end diff --git a/spec/bundler/other/major_deprecation_spec.rb b/spec/bundler/other/major_deprecation_spec.rb index f743bccb92..ab7589d698 100644 --- a/spec/bundler/other/major_deprecation_spec.rb +++ b/spec/bundler/other/major_deprecation_spec.rb @@ -5,96 +5,92 @@ RSpec.describe "major deprecations" do describe "Bundler" do before do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" G end describe ".clean_env" do before do source = "Bundler.clean_env" - bundle "exec ruby -e #{source.dump}" + bundle "exec ruby -e #{source.dump}", raise_on_error: false end - it "is deprecated in favor of .unbundled_env", :bundler => "2" do - expect(deprecations).to include \ - "`Bundler.clean_env` has been deprecated in favor of `Bundler.unbundled_env`. " \ - "If you instead want the environment before bundler was originally loaded, use `Bundler.original_env` " \ - "(called at -e:1)" + it "is removed in favor of .unbundled_env and shows a helpful error message about it" do + expect(err).to include \ + "`Bundler.clean_env` has been removed in favor of `Bundler.unbundled_env`. " \ + "If you instead want the environment before bundler was originally loaded, use `Bundler.original_env`" \ end - - pending "is removed and shows a helpful error message about it", :bundler => "3" end describe ".with_clean_env" do before do source = "Bundler.with_clean_env {}" - bundle "exec ruby -e #{source.dump}" + bundle "exec ruby -e #{source.dump}", raise_on_error: false end - it "is deprecated in favor of .unbundled_env", :bundler => "2" do - expect(deprecations).to include( - "`Bundler.with_clean_env` has been deprecated in favor of `Bundler.with_unbundled_env`. " \ - "If you instead want the environment before bundler was originally loaded, use `Bundler.with_original_env` " \ - "(called at -e:1)" + it "is removed in favor of .unbundled_env and shows a helpful error message about it" do + expect(err).to include( + "`Bundler.with_clean_env` has been removed in favor of `Bundler.with_unbundled_env`. " \ + "If you instead want the environment before bundler was originally loaded, use `Bundler.with_original_env`" ) end - - pending "is removed and shows a helpful error message about it", :bundler => "3" end describe ".clean_system" do before do source = "Bundler.clean_system('ls')" - bundle "exec ruby -e #{source.dump}" + bundle "exec ruby -e #{source.dump}", raise_on_error: false end - it "is deprecated in favor of .unbundled_system", :bundler => "2" do - expect(deprecations).to include( - "`Bundler.clean_system` has been deprecated in favor of `Bundler.unbundled_system`. " \ - "If you instead want to run the command in the environment before bundler was originally loaded, use `Bundler.original_system` " \ - "(called at -e:1)" + it "is removed in favor of .unbundled_system and shows a helpful error message about it" do + expect(err).to include( + "`Bundler.clean_system` has been removed in favor of `Bundler.unbundled_system`. " \ + "If you instead want to run the command in the environment before bundler was originally loaded, use `Bundler.original_system`" \ ) end - - pending "is removed and shows a helpful error message about it", :bundler => "3" end describe ".clean_exec" do before do source = "Bundler.clean_exec('ls')" - bundle "exec ruby -e #{source.dump}" + bundle "exec ruby -e #{source.dump}", raise_on_error: false end - it "is deprecated in favor of .unbundled_exec", :bundler => "2" do - expect(deprecations).to include( - "`Bundler.clean_exec` has been deprecated in favor of `Bundler.unbundled_exec`. " \ - "If you instead want to exec to a command in the environment before bundler was originally loaded, use `Bundler.original_exec` " \ - "(called at -e:1)" + it "is removed in favor of .unbundled_exec and shows a helpful error message about it" do + expect(err).to include( + "`Bundler.clean_exec` has been removed in favor of `Bundler.unbundled_exec`. " \ + "If you instead want to exec to a command in the environment before bundler was originally loaded, use `Bundler.original_exec`" \ ) end - - pending "is removed and shows a helpful error message about it", :bundler => "3" end describe ".environment" do before do source = "Bundler.environment" - bundle "exec ruby -e #{source.dump}" + bundle "exec ruby -e #{source.dump}", raise_on_error: false end - it "is deprecated in favor of .load", :bundler => "2" do - expect(deprecations).to include "Bundler.environment has been removed in favor of Bundler.load (called at -e:1)" + it "is removed in favor of .load and shows a helpful error message about it" do + expect(err).to include "Bundler.environment has been removed in favor of Bundler.load" end + end + end - pending "is removed and shows a helpful error message about it", :bundler => "3" + describe "bundle exec --no-keep-file-descriptors" do + before do + bundle "exec --no-keep-file-descriptors -e 1", raise_on_error: false + end + + it "is removed and shows a helpful error message about it" do + expect(err).to include "The `--no-keep-file-descriptors` has been removed. `bundle exec` no longer mess with your file descriptors. Close them in the exec'd script if you need to" end end describe "bundle update --quiet" do it "does not print any deprecations" do - bundle :update, :quiet => true + bundle :update, quiet: true, raise_on_error: false expect(deprecations).to be_empty end end @@ -102,164 +98,320 @@ RSpec.describe "major deprecations" do context "bundle check --path" do before do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "check --path vendor/bundle" + bundle "check --path vendor/bundle", raise_on_error: false end - it "should print a deprecation warning", :bundler => "2" do - expect(deprecations).to include( - "The `--path` flag is deprecated because it relies on being " \ - "remembered across bundler invocations, which bundler will no " \ - "longer do in future versions. Instead please use `bundle config set " \ - "path 'vendor/bundle'`, and stop using this flag" + it "fails with a helpful error" do + expect(err).to include( + "The `--path` flag has been removed because it relied on being " \ + "remembered across bundler invocations, which bundler no longer " \ + "does. Instead please use `bundle config set path 'vendor/bundle'`, " \ + "and stop using this flag" ) end + end + + context "bundle check --path=" do + before do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle "check --path=vendor/bundle", raise_on_error: false + end - pending "should fail with a helpful error", :bundler => "3" + it "fails with a helpful error" do + expect(err).to include( + "The `--path` flag has been removed because it relied on being " \ + "remembered across bundler invocations, which bundler no longer " \ + "does. Instead please use `bundle config set path 'vendor/bundle'`, " \ + "and stop using this flag" + ) + end + end + + context "bundle binstubs --path=" do + before do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle "binstubs myrack --path=binpath", raise_on_error: false + end + + it "fails with a helpful error" do + expect(err).to include( + "The `--path` flag has been removed because it relied on being " \ + "remembered across bundler invocations, which bundler no longer " \ + "does. Instead please use `bundle config set bin 'binpath'`, " \ + "and stop using this flag" + ) + end + end + + context "bundle cache --all" do + before do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle "cache --all --verbose", raise_on_error: false + end + + it "fails with a helpful error" do + expect(err).to include( + "The `--all` flag has been removed because it relied on being " \ + "remembered across bundler invocations, which bundler no longer " \ + "does. Instead please use `bundle config set cache_all true`, " \ + "and stop using this flag" + ) + end + end + + context "bundle cache --no-all" do + before do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle "cache --no-all", raise_on_error: false + end + + it "fails with a helpful error" do + expect(err).to include( + "The `--no-all` flag has been removed because it relied on being " \ + "remembered across bundler invocations, which bundler no longer " \ + "does. Instead please use `bundle config set cache_all false`, " \ + "and stop using this flag" + ) + end + end + + context "bundle cache --path" do + before do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle "cache --path foo", raise_on_error: false + end + + it "should print a removal error" do + expect(err).to include( + "The `--path` flag has been removed because its semantics were unclear. " \ + "Use `bundle config cache_path` to configure the path of your cache of gems, " \ + "and `bundle config path` to configure the path where your gems are installed, " \ + "and stop using this flag" + ) + end + end + + context "bundle cache --path=" do + before do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle "cache --path=foo", raise_on_error: false + end + + it "should print a deprecation warning" do + expect(err).to include( + "The `--path` flag has been removed because its semantics were unclear. " \ + "Use `bundle config cache_path` to configure the path of your cache of gems, " \ + "and `bundle config path` to configure the path where your gems are installed, " \ + "and stop using this flag" + ) + end + end + + context "bundle cache --frozen" do + before do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle "cache --frozen", raise_on_error: false + end + + it "fails with a helpful error" do + expect(err).to include( + "The `--frozen` flag has been removed because it relied on being " \ + "remembered across bundler invocations, which bundler no longer " \ + "does. Instead please use `bundle config set frozen true`, " \ + "and stop using this flag" + ) + end + end + + context "bundle cache --no-prune" do + before do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle "cache --no-prune", raise_on_error: false + end + + it "fails with a helpful error" do + expect(err).to include( + "The `--no-prune` flag has been removed because it relied on being " \ + "remembered across bundler invocations, which bundler no longer " \ + "does. Instead please use `bundle config set no_prune true`, " \ + "and stop using this flag" + ) + end end describe "bundle config" do describe "old list interface" do before do - bundle! "config" + bundle "config" end - it "warns", :bundler => "2" do + it "warns", bundler: "4" do expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config list` instead.") end - pending "fails with a helpful error", :bundler => "3" + pending "fails with a helpful error", bundler: "5" end describe "old get interface" do before do - bundle! "config waka" + bundle "config waka", raise_on_error: false end - it "warns", :bundler => "2" do + it "warns", bundler: "4" do expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config get waka` instead.") end - pending "fails with a helpful error", :bundler => "3" + pending "fails with a helpful error", bundler: "5" end describe "old set interface" do before do - bundle! "config waka wakapun" + bundle "config waka wakapun" end - it "warns", :bundler => "2" do + it "warns", bundler: "4" do expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config set waka wakapun` instead.") end - pending "fails with a helpful error", :bundler => "3" + pending "fails with a helpful error", bundler: "5" end describe "old set interface with --local" do before do - bundle! "config --local waka wakapun" + bundle "config --local waka wakapun" end - it "warns", :bundler => "2" do + it "warns", bundler: "4" do expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config set --local waka wakapun` instead.") end - pending "fails with a helpful error", :bundler => "3" + pending "fails with a helpful error", bundler: "5" end describe "old set interface with --global" do before do - bundle! "config --global waka wakapun" + bundle "config --global waka wakapun" end - it "warns", :bundler => "2" do + it "warns", bundler: "4" do expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config set --global waka wakapun` instead.") end - pending "fails with a helpful error", :bundler => "3" + pending "fails with a helpful error", bundler: "5" end describe "old unset interface" do before do - bundle! "config --delete waka" + bundle "config --delete waka" end - it "warns", :bundler => "2" do + it "warns", bundler: "4" do expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config unset waka` instead.") end - pending "fails with a helpful error", :bundler => "3" + pending "fails with a helpful error", bundler: "5" end describe "old unset interface with --local" do before do - bundle! "config --delete --local waka" + bundle "config --delete --local waka" end - it "warns", :bundler => "2" do + it "warns", bundler: "4" do expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config unset --local waka` instead.") end - pending "fails with a helpful error", :bundler => "3" + pending "fails with a helpful error", bundler: "5" end describe "old unset interface with --global" do before do - bundle! "config --delete --global waka" + bundle "config --delete --global waka" end - it "warns", :bundler => "2" do + it "warns", bundler: "4" do expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config unset --global waka` instead.") end - pending "fails with a helpful error", :bundler => "3" + pending "fails with a helpful error", bundler: "5" end end describe "bundle update" do before do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G end - it "warns when no options are given", :bundler => "2" do - bundle! "update" + it "warns when no options are given", bundler: "4" do + bundle "update" expect(deprecations).to include("Pass --all to `bundle update` to update everything") end - pending "fails with a helpful error when no options are given", :bundler => "3" + pending "fails with a helpful error when no options are given", bundler: "5" it "does not warn when --all is passed" do - bundle! "update --all" + bundle "update --all" expect(deprecations).to be_empty end end describe "bundle install --binstubs" do before do - install_gemfile <<-G, :binstubs => true - source "#{file_uri_for(gem_repo1)}" - gem "rack" + install_gemfile <<-G, binstubs: true, raise_on_error: false + source "https://gem.repo1" + gem "myrack" G end - it "should output a deprecation warning", :bundler => "2" do - expect(deprecations).to include("The --binstubs option will be removed in favor of `bundle binstubs`") + it "fails with a helpful error" do + expect(err).to include("The --binstubs option has been removed in favor of `bundle binstubs --all`") end - - pending "fails with a helpful error", :bundler => "3" end context "bundle install with both gems.rb and Gemfile present" do it "should not warn about gems.rb" do - create_file "gems.rb", <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + gemfile "gems.rb", <<-G + source "https://gem.repo1" + gem "myrack" G bundle :install @@ -267,96 +419,217 @@ RSpec.describe "major deprecations" do end it "should print a proper warning, and use gems.rb" do - create_file "gems.rb" - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + gemfile "gems.rb", "source 'https://gem.repo1'" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" G expect(warnings).to include( - "Multiple gemfiles (gems.rb and Gemfile) detected. Make sure you remove Gemfile and Gemfile.lock since bundler is ignoring them in favor of gems.rb and gems.rb.locked." + "Multiple gemfiles (gems.rb and Gemfile) detected. Make sure you remove Gemfile and Gemfile.lock since bundler is ignoring them in favor of gems.rb and gems.locked." ) - expect(the_bundle).not_to include_gem "rack 1.0" + expect(the_bundle).not_to include_gem "myrack 1.0" end end context "bundle install with flags" do before do - bundle "config set --local path vendor/bundle" + bundle_config "path vendor/bundle" install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G end { - :clean => true, - :deployment => true, - :frozen => true, - :"no-cache" => true, - :"no-prune" => true, - :path => "vendor/bundle", - :shebang => "ruby27", - :system => true, - :without => "development", - :with => "development", - }.each do |name, value| + "clean" => ["clean", "true"], + "deployment" => ["deployment", "true"], + "frozen" => ["frozen", "true"], + "no-deployment" => ["deployment", "false"], + "no-prune" => ["no_prune", "true"], + "path" => ["path", "'vendor/bundle'"], + "shebang" => ["shebang", "'ruby27'"], + "system" => ["path.system", "true"], + "without" => ["without", "'development'"], + "with" => ["with", "'development'"], + }.each do |name, expectations| + option_name, value = *expectations flag_name = "--#{name}" + args = %w[true false].include?(value) ? flag_name : "#{flag_name} #{value}" context "with the #{flag_name} flag" do before do bundle "install" # to create a lockfile, which deployment or frozen need - bundle "install #{flag_name} #{value}" + + bundle "install #{args}", raise_on_error: false end - it "should print a deprecation warning", :bundler => "2" do - expect(deprecations).to include( - "The `#{flag_name}` flag is deprecated because it relies on " \ - "being remembered across bundler invocations, which bundler " \ - "will no longer do in future versions. Instead please use " \ - "`bundle config set #{name} '#{value}'`, and stop using this flag" + it "fails with a helpful error" do + expect(err).to include( + "The `#{flag_name}` flag has been removed because it relied on " \ + "being remembered across bundler invocations, which bundler no " \ + "longer does. Instead please use `bundle config set " \ + "#{option_name} #{value}`, and stop using this flag" ) end - - pending "should fail with a helpful error", :bundler => "3" end end end context "bundle install with multiple sources" do before do - install_gemfile <<-G - source "#{file_uri_for(gem_repo3)}" - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo3" + source "https://gem.repo1" G end - it "shows a deprecation", :bundler => "2" do - expect(deprecations).to include( - "Your Gemfile contains multiple primary sources. " \ - "Using `source` more than once without a block is a security risk, and " \ - "may result in installing unexpected gems. To resolve this warning, use " \ - "a block to indicate which gems should come from the secondary source. " \ - "To upgrade this warning to an error, run `bundle config set " \ - "disable_multisource true`." + it "fails with a helpful error" do + expect(err).to include( + "This Gemfile contains multiple global sources. " \ + "Each source after the first must include a block to indicate which gems " \ + "should come from that source" ) end - pending "should fail with a helpful error", :bundler => "3" + it "doesn't show lockfile deprecations if there's a lockfile" do + lockfile <<~L + GEM + remote: https://gem.repo3/ + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + + BUNDLED WITH + #{Bundler::VERSION} + L + bundle "install", raise_on_error: false + + expect(err).to include( + "This Gemfile contains multiple global sources. " \ + "Each source after the first must include a block to indicate which gems " \ + "should come from that source" + ) + expect(err).not_to include( + "Your lockfile contains a single rubygems source section with multiple remotes, which is insecure. " \ + "Make sure you run `bundle install` in non frozen mode and commit the result to make your lockfile secure." + ) + bundle_config "frozen true" + bundle "install", raise_on_error: false + + expect(err).to include( + "This Gemfile contains multiple global sources. " \ + "Each source after the first must include a block to indicate which gems " \ + "should come from that source" + ) + expect(err).not_to include( + "Your lockfile contains a single rubygems source section with multiple remotes, which is insecure. " \ + "Make sure you run `bundle install` in non frozen mode and commit the result to make your lockfile secure." + ) + end + end + + context "bundle install with a lockfile with a single rubygems section with multiple remotes" do + before do + build_repo3 do + build_gem "myrack", "0.9.1" + end + + gemfile <<-G + source "https://gem.repo1" + source "https://gem.repo3" do + gem 'myrack' + end + G + + lockfile <<~L + GEM + remote: https://gem.repo1/ + remote: https://gem.repo3/ + specs: + myrack (0.9.1) + + PLATFORMS + ruby + + DEPENDENCIES + myrack! + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "shows an error" do + bundle "install", raise_on_error: false + + expect(err).to include("Your lockfile contains a single rubygems source section with multiple remotes, which is insecure. Make sure you run `bundle install` in non frozen mode and commit the result to make your lockfile secure.") + end + end + + context "bundle install with a lockfile including X64_MINGW_LEGACY platform" do + before do + gemfile <<~G + source "https://gem.repo1" + gem "rake" + G + + lockfile <<~L + GEM + remote: https://rubygems.org/ + specs: + rake (10.3.2) + + PLATFORMS + ruby + x64-mingw32 + + DEPENDENCIES + rake + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "warns a helpful error" do + bundle "install", raise_on_error: false + + expect(err).to include("Found x64-mingw32 in lockfile, which is deprecated and will be removed in the future.") + end + end + + context "with a global path source" do + before do + build_lib "foo" + + install_gemfile <<-G, raise_on_error: false + path "#{lib_path("foo-1.0")}" + gem 'foo' + G + end + + it "shows an error" do + expect(err).to include("You can no longer specify a path source by itself") + end end context "when Bundler.setup is run in a ruby script" do before do - create_file "gems.rb" - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :group => :test + create_file "gems.rb", "source 'https://gem.repo1'" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :group => :test G ruby <<-RUBY - require '#{lib_dir}/bundler' + require 'bundler' Bundler.setup Bundler.setup @@ -365,198 +638,161 @@ RSpec.describe "major deprecations" do it "should print a single deprecation warning" do expect(warnings).to include( - "Multiple gemfiles (gems.rb and Gemfile) detected. Make sure you remove Gemfile and Gemfile.lock since bundler is ignoring them in favor of gems.rb and gems.rb.locked." + "Multiple gemfiles (gems.rb and Gemfile) detected. Make sure you remove Gemfile and Gemfile.lock since bundler is ignoring them in favor of gems.rb and gems.locked." ) end end context "when `bundler/deployment` is required in a ruby script" do before do - ruby(<<-RUBY) + ruby <<-RUBY, raise_on_error: false require 'bundler/deployment' RUBY end - it "should print a capistrano deprecation warning", :bundler => "2" do - expect(deprecations).to include("Bundler no longer integrates " \ + it "should print a capistrano deprecation warning" do + expect(err).to include("Bundler no longer integrates " \ "with Capistrano, but Capistrano provides " \ "its own integration with Bundler via the " \ "capistrano-bundler gem. Use it instead.") end - - pending "should fail with a helpful error", :bundler => "3" end - describe Bundler::Dsl do + context "when `bundler/capistrano` is required in a ruby script" do before do - @rubygems = double("rubygems") - allow(Bundler::Source::Rubygems).to receive(:new) { @rubygems } - end - - context "with github gems" do - it "warns about removal", :bundler => "2" do - msg = <<-EOS -The :github git source is deprecated, and will be removed in the future. Change any "reponame" :github sources to "username/reponame". Add this code to the top of your Gemfile to ensure it continues to work: - - git_source(:github) {|repo_name| "https://github.com/\#{repo_name}.git" } - - EOS - expect(Bundler::SharedHelpers).to receive(:major_deprecation).with(3, msg) - subject.gem("sparks", :github => "indirect/sparks") - github_uri = "https://github.com/indirect/sparks.git" - expect(subject.dependencies.first.source.uri).to eq(github_uri) - end - - pending "should fail with a helpful error", :bundler => "3" + ruby <<-RUBY, raise_on_error: false + require 'bundler/capistrano' + RUBY end - context "with bitbucket gems" do - it "warns about removal", :bundler => "2" do - allow(Bundler.ui).to receive(:deprecate) - msg = <<-EOS -The :bitbucket git source is deprecated, and will be removed in the future. Add this code to the top of your Gemfile to ensure it continues to work: - - git_source(:bitbucket) do |repo_name| - user_name, repo_name = repo_name.split("/") - repo_name ||= user_name - "https://\#{user_name}@bitbucket.org/\#{user_name}/\#{repo_name}.git" + it "fails with a helpful error" do + expect(err).to include("[REMOVED] The Bundler task for Capistrano. Please use https://github.com/capistrano/bundler") end + end - EOS - expect(Bundler::SharedHelpers).to receive(:major_deprecation).with(3, msg) - subject.gem("not-really-a-gem", :bitbucket => "mcorp/flatlab-rails") - end - - pending "should fail with a helpful error", :bundler => "3" + context "when `bundler/vlad` is required in a ruby script" do + before do + ruby <<-RUBY, raise_on_error: false + require 'bundler/vlad' + RUBY end - context "with gist gems" do - it "warns about removal", :bundler => "2" do - allow(Bundler.ui).to receive(:deprecate) - msg = <<-EOS -The :gist git source is deprecated, and will be removed in the future. Add this code to the top of your Gemfile to ensure it continues to work: - - git_source(:gist) {|repo_name| "https://gist.github.com/\#{repo_name}.git" } - - EOS - expect(Bundler::SharedHelpers).to receive(:major_deprecation).with(3, msg) - subject.gem("not-really-a-gem", :gist => "1234") - end - - pending "should fail with a helpful error", :bundler => "3" + it "fails with a helpful error" do + expect(err).to include("[REMOVED] The Bundler task for Vlad") end end context "bundle show" do before do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" G end - context "without flags" do + context "with --outdated flag" do before do - bundle! :show + bundle "show --outdated", raise_on_error: false end - it "prints a deprecation warning recommending `bundle list`", :bundler => "2" do - expect(deprecations).to include("use `bundle list` instead of `bundle show`") + it "fails with a helpful message" do + expect(err).to include("the `--outdated` flag to `bundle show` has been removed in favor of `bundle show --verbose`") end + end + end - pending "fails with a helpful message", :bundler => "3" + context "bundle remove" do + before do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G end - context "with --outdated flag" do - before do - bundle! "show --outdated" - end + context "with --install" do + it "fails with a helpful message" do + bundle "remove myrack --install", raise_on_error: false - it "prints a deprecation warning informing about its removal", :bundler => "2" do - expect(deprecations).to include("the `--outdated` flag to `bundle show` was undocumented and will be removed without replacement") + expect(err).to include "The `--install` flag has been removed. `bundle install` is triggered by default." end + end + end - pending "fails with a helpful message", :bundler => "3" + context "bundle viz" do + before do + bundle "viz", raise_on_error: false end - context "with --verbose flag" do - before do - bundle! "show --verbose" - end + it "fails with a helpful message" do + expect(err).to include "The `viz` command has been renamed to `graph` and moved to a plugin. See https://github.com/rubygems/bundler-graph" + end + end - it "prints a deprecation warning informing about its removal", :bundler => "2" do - expect(deprecations).to include("the `--verbose` flag to `bundle show` was undocumented and will be removed without replacement") - end + context "bundle inject" do + before do + bundle "inject", raise_on_error: false + end - pending "fails with a helpful message", :bundler => "3" + it "fails with a helpful message" do + expect(err).to include "The `inject` command has been replaced by the `add` command" end + end - context "with a gem argument" do - before do - bundle! "show rack" + context "bundle plugin install --local_git" do + before do + build_git "foo" do |s| + s.write "plugins.rb" end + end - it "prints a deprecation warning recommending `bundle info`", :bundler => "2" do - expect(deprecations).to include("use `bundle info rack` instead of `bundle show rack`") - end + it "fails with a helpful message" do + bundle "plugin install foo --local_git #{lib_path("foo-1.0")}", raise_on_error: false - pending "fails with a helpful message", :bundler => "3" + expect(err).to include "--local_git has been removed, use --git" + end + end + + describe "removing rubocop" do + before do + bundle_config_global "gem.mit false" + bundle_config_global "gem.test false" + bundle_config_global "gem.coc false" + bundle_config_global "gem.ci false" + bundle_config_global "gem.changelog false" end - context "with the --paths option" do + context "bundle gem --rubocop" do before do - bundle "show --paths" + bundle "gem my_new_gem --rubocop", raise_on_error: false end - it "prints a deprecation warning recommending `bundle list`", :bundler => "2" do - expect(deprecations).to include("use `bundle list` instead of `bundle show --paths`") + it "prints an error" do + expect(err).to include \ + "--rubocop has been removed, use --linter=rubocop" end - - pending "fails with a helpful message", :bundler => "3" end - context "with a gem argument and the --paths option" do + context "bundle gem --no-rubocop" do before do - bundle "show rack --paths" + bundle "gem my_new_gem --no-rubocop", raise_on_error: false end - it "prints deprecation warning recommending `bundle info`", :bundler => "2" do - expect(deprecations).to include("use `bundle info rack --path` instead of `bundle show rack --paths`") + it "prints an error" do + expect(err).to include \ + "--no-rubocop has been removed, use --no-linter" end - - pending "fails with a helpful message", :bundler => "3" end end - context "bundle console" do - before do - bundle "console" - end - - it "prints a deprecation warning", :bundler => "2" do - expect(deprecations).to include \ - "bundle console will be replaced by `bin/console` generated by `bundle gem <name>`" - end - - pending "fails with a helpful message", :bundler => "3" - end - - context "bundle viz" 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 - create_file "gems.rb" - bundle "viz" + context " bundle gem --ext parameter with no value" do + it "prints error when used before gem name" do + bundle "gem --ext foo", raise_on_error: false + expect(err).to include "Extensions can now be generated using C or Rust, so `--ext` with no arguments has been removed. Please select a language, e.g. `--ext=rust` to generate a Rust extension." end - it "prints a deprecation warning", :bundler => "2" do - expect(deprecations).to include "The `viz` command has been moved to the `bundle-viz` gem, see https://github.com/bundler/bundler-viz" + it "prints error when used after gem name" do + bundle "gem foo --ext", raise_on_error: false + expect(err).to include "Extensions can now be generated using C or Rust, so `--ext` with no arguments has been removed. Please select a language, e.g. `--ext=rust` to generate a Rust extension." end - - pending "fails with a helpful message", :bundler => "3" end end diff --git a/spec/bundler/other/platform_spec.rb b/spec/bundler/other/platform_spec.rb deleted file mode 100644 index 8b02505ad8..0000000000 --- a/spec/bundler/other/platform_spec.rb +++ /dev/null @@ -1,1306 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe "bundle platform" do - context "without flags" do - let(:bundle_platform_platforms_string) do - local_platforms.reverse.map {|pl| "* #{pl}" }.join("\n") - end - - it "returns all the output" do - gemfile <<-G - source "#{file_uri_for(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: -#{bundle_platform_platforms_string} - -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_uri_for(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: -#{bundle_platform_platforms_string} - -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_uri_for(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: -#{bundle_platform_platforms_string} - -Your Gemfile does not specify a Ruby version requirement. -G - end - - it "doesn't match the ruby version requirement" do - gemfile <<-G - source "#{file_uri_for(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: -#{bundle_platform_platforms_string} - -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_uri_for(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_uri_for(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_uri_for(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_uri_for(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 "handles truffleruby" do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - ruby "2.5.1", :engine => 'truffleruby', :engine_version => '1.0.0-rc6' - - gem "foo" - G - - bundle "platform --ruby" - - expect(out).to eq("ruby 2.5.1 (truffleruby 1.0.0-rc6)") - end - - it "raises an error if engine is used but engine version is not" do - gemfile <<-G - source "#{file_uri_for(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_uri_for(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_uri_for(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_uri_for(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(err).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(err).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(err).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(err).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(err).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_uri_for(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_uri_for(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_uri_for(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_uri_for(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_uri_for(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_uri_for(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_uri_for(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_uri_for(gem_repo1)}" - gem "rack" - G - - gemfile <<-G - source "#{file_uri_for(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_uri_for(gem_repo1)}" - gem "rack" - G - - gemfile <<-G - source "#{file_uri_for(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_uri_for(gem_repo1)}" - gem "rack" - G - - gemfile <<-G - source "#{file_uri_for(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_uri_for(gem_repo1)}" - gem "rack" - G - - gemfile <<-G - source "#{file_uri_for(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_uri_for(gem_repo1)}" - gem "rack" - G - - gemfile <<-G - source "#{file_uri_for(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_uri_for(gem_repo1)}" - gem "rack" - G - - gemfile <<-G - source "#{file_uri_for(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_uri_for(gem_repo2)}" - gem "activesupport" - gem "rack-obama" - G - end - - it "updates successfully when the ruby version matches" do - gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "activesupport" - gem "rack-obama" - - #{ruby_version_correct} - G - update_repo2 do - build_gem "activesupport", "3.0" - end - - bundle "update", :all => true - 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_uri_for(gem_repo2)}" - gem "activesupport" - gem "rack-obama" - - #{ruby_version_correct_engineless} - G - update_repo2 do - build_gem "activesupport", "3.0" - end - - bundle "update", :all => true - 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_uri_for(gem_repo2)}" - gem "activesupport" - gem "rack-obama" - - #{ruby_version_incorrect} - G - update_repo2 do - build_gem "activesupport", "3.0" - end - - bundle :update, :all => true - should_be_ruby_version_incorrect - end - - it "fails when ruby engine doesn't match" do - gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "activesupport" - gem "rack-obama" - - #{engine_incorrect} - G - update_repo2 do - build_gem "activesupport", "3.0" - end - - bundle :update, :all => true - should_be_engine_incorrect - end - - it "fails when ruby engine version doesn't match" do - simulate_ruby_engine "jruby" do - gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - gem "activesupport" - gem "rack-obama" - - #{engine_version_incorrect} - G - update_repo2 do - build_gem "activesupport", "3.0" - end - - bundle :update, :all => true - should_be_engine_version_incorrect - end - end - - it "fails when patchlevel doesn't match" do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - - #{patchlevel_incorrect} - G - update_repo2 do - build_gem "activesupport", "3.0" - end - - bundle :update, :all => true - should_be_patchlevel_incorrect - end - end - - context "bundle info" do - before do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rails" - G - end - - it "prints path if ruby version is correct" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rails" - - #{ruby_version_correct} - G - - bundle "info rails --path" - 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 - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rails" - - #{ruby_version_correct_engineless} - G - - bundle "info rails --path" - expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s) - end - end - - it "fails if ruby version doesn't match", :bundler => "< 3" do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rails" - - #{ruby_version_incorrect} - G - - bundle "show rails" - should_be_ruby_version_incorrect - end - - it "fails if engine doesn't match", :bundler => "< 3" do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rails" - - #{engine_incorrect} - G - - bundle "show rails" - should_be_engine_incorrect - end - - it "fails if engine version doesn't match", :bundler => "< 3" do - simulate_ruby_engine "jruby" do - gemfile <<-G - source "#{file_uri_for(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", :bundler => "< 3" do - gemfile <<-G - source "#{file_uri_for(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 - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack' - G - 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 - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - 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_uri_for(gem_repo1)}" - gem "rack" - - #{patchlevel_incorrect} - G - - bundle :cache - should_be_patchlevel_incorrect - end - end - - context "bundle pack" do - before do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack' - G - 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 any engine" do - simulate_ruby_engine "jruby" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - 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_uri_for(gem_repo1)}" - gem "rack" - - #{patchlevel_incorrect} - G - - bundle :cache - 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", :path => :bundle_path - 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 include("0.9.1") - end - - it "activates the correct gem when ruby version matches any engine" do - simulate_ruby_engine "jruby" do - system_gems "rack-1.0.0", "rack-0.9.1", :path => :bundle_path - gemfile <<-G - gem "rack", "0.9.1" - - #{ruby_version_correct_engineless} - G - - bundle "exec rackup" - expect(out).to include("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_uri_for(gem_repo1)}" - gem "rack" - - #{patchlevel_incorrect} - G - - bundle "exec rackup" - should_be_patchlevel_incorrect - end - end - - context "bundle console", :bundler => "< 3" do - before do - install_gemfile <<-G - source "#{file_uri_for(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_uri_for(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_uri_for(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_uri_for(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_uri_for(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_uri_for(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_uri_for(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_uri_for(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_uri_for(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_uri_for(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_uri_for(gem_repo1)}" - gem "yard" - gem "rack" - - #{ruby_version_incorrect} - G - - FileUtils.rm(bundled_app("Gemfile.lock")) - - ruby <<-R - 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_uri_for(gem_repo1)}" - gem "yard" - gem "rack" - - #{engine_incorrect} - G - - FileUtils.rm(bundled_app("Gemfile.lock")) - - ruby <<-R - 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_uri_for(gem_repo1)}" - gem "yard" - gem "rack" - - #{engine_version_incorrect} - G - - FileUtils.rm(bundled_app("Gemfile.lock")) - - ruby <<-R - 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_uri_for(gem_repo1)}" - gem "yard" - gem "rack" - - #{patchlevel_incorrect} - G - - FileUtils.rm(bundled_app("Gemfile.lock")) - - ruby <<-R - 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_uri_for(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_uri_for(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 - bundle! :install - update_repo2 do - build_gem "activesupport", "3.0" - update_git "foo", :path => lib_path("foo") - end - - gemfile <<-G - source "#{file_uri_for(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_uri_for(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_uri_for(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_uri_for(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_uri_for(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_uri_for(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/plugins/command_spec.rb b/spec/bundler/plugins/command_spec.rb index 4728f66f5f..05d535a70c 100644 --- a/spec/bundler/plugins/command_spec.rb +++ b/spec/bundler/plugins/command_spec.rb @@ -18,7 +18,7 @@ RSpec.describe "command plugins" do end end - bundle "plugin install command-mah --source #{file_uri_for(gem_repo2)}" + bundle "plugin install command-mah --source https://gem.repo2" end it "executes without arguments" do @@ -29,7 +29,7 @@ RSpec.describe "command plugins" do end it "accepts the arguments" do - build_repo2 do + update_repo2 do build_plugin "the-echoer" do |s| s.write "plugins.rb", <<-RUBY module Resonance @@ -46,15 +46,49 @@ RSpec.describe "command plugins" do end end - bundle "plugin install the-echoer --source #{file_uri_for(gem_repo2)}" + bundle "plugin install the-echoer --source https://gem.repo2" expect(out).to include("Installed plugin the-echoer") bundle "echo tacos tofu lasange" expect(out).to eq("You gave me tacos, tofu, lasange") end + it "passes help flag to plugin" do + update_repo2 do + build_plugin "helpful" do |s| + s.write "plugins.rb", <<-RUBY + module Helpful + class Command + Bundler::Plugin::API.command "greet", self + + def exec(command, args) + if args.include?("--help") || args.include?("-h") + puts "Usage: bundle greet [NAME]" + else + puts "Hello" + end + end + end + end + RUBY + end + end + + bundle "plugin install helpful --source https://gem.repo2" + expect(out).to include("Installed plugin helpful") + + bundle "greet --help" + expect(out).to eq("Usage: bundle greet [NAME]") + + bundle "greet -h" + expect(out).to eq("Usage: bundle greet [NAME]") + + bundle "help greet" + expect(out).to eq("Usage: bundle greet [NAME]") + end + it "raises error on redeclaration of command" do - build_repo2 do + update_repo2 do build_plugin "copycat" do |s| s.write "plugins.rb", <<-RUBY module CopyCat @@ -69,12 +103,10 @@ RSpec.describe "command plugins" do end end - bundle "plugin install copycat --source #{file_uri_for(gem_repo2)}" + bundle "plugin install copycat --source https://gem.repo2", raise_on_error: false expect(out).not_to include("Installed plugin copycat") - expect(err).to include("Failed to install plugin") - - expect(err).to include("Command(s) `mahcommand` declared by copycat are already registered.") + expect(err).to include("Failed to install plugin `copycat`, due to Bundler::Plugin::Index::CommandConflict (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 index 72feb14d84..ad8a4daeff 100644 --- a/spec/bundler/plugins/hook_spec.rb +++ b/spec/bundler/plugins/hook_spec.rb @@ -13,17 +13,17 @@ RSpec.describe "hook plugins" do end end - bundle "plugin install before-install-all-plugin --source #{file_uri_for(gem_repo2)}" + bundle "plugin install before-install-all-plugin --source https://gem.repo2" end it "runs before all rubygems are installed" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rake" - gem "rack" + gem "myrack" G - expect(out).to include "gems to be installed rake, rack" + expect(out).to include "gems to be installed rake, myrack" end end @@ -39,18 +39,18 @@ RSpec.describe "hook plugins" do end end - bundle "plugin install before-install-plugin --source #{file_uri_for(gem_repo2)}" + bundle "plugin install before-install-plugin --source https://gem.repo2" end it "runs before each rubygem is installed" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rake" - gem "rack" + gem "myrack" G expect(out).to include "installing gem rake" - expect(out).to include "installing gem rack" + expect(out).to include "installing gem myrack" end end @@ -66,17 +66,17 @@ RSpec.describe "hook plugins" do end end - bundle "plugin install after-install-all-plugin --source #{file_uri_for(gem_repo2)}" + bundle "plugin install after-install-all-plugin --source https://gem.repo2" end - it "runs after each rubygem is installed" do + it "runs after each all rubygems are installed" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rake" - gem "rack" + gem "myrack" G - expect(out).to include "installed gems rake, rack" + expect(out).to include "installed gems rake, myrack" end end @@ -92,18 +92,240 @@ RSpec.describe "hook plugins" do end end - bundle "plugin install after-install-plugin --source #{file_uri_for(gem_repo2)}" + bundle "plugin install after-install-plugin --source https://gem.repo2" end it "runs after each rubygem is installed" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rake" - gem "rack" + gem "myrack" G expect(out).to include "installed gem rake : installed" - expect(out).to include "installed gem rack : installed" + expect(out).to include "installed gem myrack : installed" + end + end + + context "before-require-all hook" do + before do + build_repo2 do + build_plugin "before-require-all-plugin" do |s| + s.write "plugins.rb", <<-RUBY + Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_REQUIRE_ALL do |deps| + puts "gems to be required \#{deps.map(&:name).join(", ")}" + end + RUBY + end + end + + bundle "plugin install before-require-all-plugin --source https://gem.repo2" + end + + it "runs before all rubygems are required" do + install_gemfile_and_bundler_require + expect(out).to include "gems to be required rake, myrack" + end + end + + context "before-require hook" do + before do + build_repo2 do + build_plugin "before-require-plugin" do |s| + s.write "plugins.rb", <<-RUBY + Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_REQUIRE do |dep| + puts "requiring gem \#{dep.name}" + end + RUBY + end + end + + bundle "plugin install before-require-plugin --source https://gem.repo2" + end + + it "runs before each rubygem is required" do + install_gemfile_and_bundler_require + expect(out).to include "requiring gem rake" + expect(out).to include "requiring gem myrack" end end + + context "after-require-all hook" do + before do + build_repo2 do + build_plugin "after-require-all-plugin" do |s| + s.write "plugins.rb", <<-RUBY + Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_REQUIRE_ALL do |deps| + puts "required gems \#{deps.map(&:name).join(", ")}" + end + RUBY + end + end + + bundle "plugin install after-require-all-plugin --source https://gem.repo2" + end + + it "runs after all rubygems are required" do + install_gemfile_and_bundler_require + expect(out).to include "required gems rake, myrack" + end + end + + context "after-require hook" do + before do + build_repo2 do + build_plugin "after-require-plugin" do |s| + s.write "plugins.rb", <<-RUBY + Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_REQUIRE do |dep| + puts "required gem \#{dep.name}" + end + RUBY + end + end + + bundle "plugin install after-require-plugin --source https://gem.repo2" + end + + it "runs after each rubygem is required" do + install_gemfile_and_bundler_require + expect(out).to include "required gem rake" + expect(out).to include "required gem myrack" + end + end + + context "before-eval hook" do + before do + build_repo2 do + build_plugin "before-eval-plugin" do |s| + s.write "plugins.rb", <<-RUBY + Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_EVAL do |gemfile, lockfile| + puts "hooked eval start of \#{File.basename(gemfile)} to \#{File.basename(lockfile)}" + end + RUBY + end + end + + bundle "plugin install before-eval-plugin --source https://gem.repo2" + end + + it "runs before the Gemfile is evaluated" do + install_gemfile <<-G + source "https://gem.repo1" + gem "rake" + G + + expect(out).to include "hooked eval start of Gemfile to Gemfile.lock" + end + end + + context "after-eval hook" do + before do + build_repo2 do + build_plugin "after-eval-plugin" do |s| + s.write "plugins.rb", <<-RUBY + Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_EVAL do |defn| + puts "hooked eval after with gems \#{defn.dependencies.map(&:name).join(", ")}" + end + RUBY + end + end + + bundle "plugin install after-eval-plugin --source https://gem.repo2" + end + + it "runs after the Gemfile is evaluated" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "rake" + G + + expect(out).to include "hooked eval after with gems myrack, rake" + end + end + + context "before-fetch and after-fetch hooks" do + before do + build_repo2 do + build_plugin "fetch-timing-plugin" do |s| + s.write "plugins.rb", <<-RUBY + @timing_start = nil + Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_FETCH do |spec| + @timing_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + puts "gem \#{spec.name} started fetch at \#{@timing_start}" + end + Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_FETCH do |spec| + timing_end = Process.clock_gettime(Process::CLOCK_MONOTONIC) + puts "gem \#{spec.name} took \#{timing_end - @timing_start} to fetch" + @timing_start = nil + end + RUBY + end + end + + bundle "plugin install fetch-timing-plugin --source https://gem.repo2" + end + + it "runs around each gem download" do + install_gemfile <<-G + source "https://gem.repo1" + gem "rake" + gem "myrack" + G + + expect(out).to include "gem rake started fetch at" + expect(out).to match(/gem rake took \d+\.\d+ to fetch/) + expect(out).to include "gem myrack started fetch at" + expect(out).to match(/gem myrack took \d+\.\d+ to fetch/) + end + end + + context "before-git-fetch and after-git-fetch hooks" do + before do + build_repo2 do + build_plugin "git-fetch-timing-plugin" do |s| + s.write "plugins.rb", <<-RUBY + @timing_start = nil + Bundler::Plugin::API.hook Bundler::Plugin::Events::GIT_BEFORE_FETCH do |source| + @timing_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + puts "git source \#{source.name} started fetch at \#{@timing_start}" + end + Bundler::Plugin::API.hook Bundler::Plugin::Events::GIT_AFTER_FETCH do |source| + timing_end = Process.clock_gettime(Process::CLOCK_MONOTONIC) + puts "git source \#{source.name} took \#{timing_end - @timing_start} to fetch" + @timing_start = nil + end + RUBY + end + end + + bundle "plugin install git-fetch-timing-plugin --source https://gem.repo2" + end + + it "runs around each git source fetch" do + build_git "foo", "1.0", path: lib_path("foo") + + relative_path = lib_path("foo").relative_path_from(bundled_app) + install_gemfile <<-G, verbose: true + source "https://gem.repo1" + gem "foo", :git => "#{relative_path}" + G + + expect(out).to include "git source foo started fetch at" + expect(out).to match(/git source foo took \d+\.\d+ to fetch/) + end + end + + def install_gemfile_and_bundler_require + install_gemfile <<-G + source "https://gem.repo1" + gem "rake" + gem "myrack" + G + + ruby <<-RUBY + require "bundler" + Bundler.require + RUBY + end end diff --git a/spec/bundler/plugins/install_spec.rb b/spec/bundler/plugins/install_spec.rb index 669ed09fb5..dcacf764be 100644 --- a/spec/bundler/plugins/install_spec.rb +++ b/spec/bundler/plugins/install_spec.rb @@ -9,33 +9,54 @@ RSpec.describe "bundler plugin install" do end it "shows proper message when gem in not found in the source" do - bundle "plugin install no-foo --source #{file_uri_for(gem_repo1)}" + bundle "plugin install no-foo --source https://gem.repo1", raise_on_error: false expect(err).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_uri_for(gem_repo2)}" + bundle "plugin install foo --source https://gem.repo2" expect(out).to include("Installed plugin foo") plugin_should_be_installed("foo") end + it "installs from rubygems source in frozen mode" do + bundle "plugin install foo --source https://gem.repo2", env: { "BUNDLE_DEPLOYMENT" => "true" } + + expect(out).to include("Installed plugin foo") + plugin_should_be_installed("foo") + end + + it "installs from sources configured as Gem.sources without any flags" do + bundle "plugin install foo", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_SOURCES" => "https://gem.repo2" } + + expect(out).to include("Installed plugin foo") + plugin_should_be_installed("foo") + end + + it "shows help when --help flag is given" do + bundle "plugin install --help" + + # The help message defined in ../../lib/bundler/man/bundle-plugin.1.ronn will be output. + expect(out).to include("You can install, uninstall, and list plugin(s)") + end + context "plugin is already installed" do before do - bundle "plugin install foo --source #{file_uri_for(gem_repo2)}" + bundle "plugin install foo --source https://gem.repo2" end it "doesn't install plugin again" do - bundle "plugin install foo --source #{file_uri_for(gem_repo2)}" + bundle "plugin install foo --source https://gem.repo2" expect(out).not_to include("Installing plugin foo") expect(out).not_to include("Installed plugin foo") end end it "installs multiple plugins" do - bundle "plugin install foo kung-foo --source #{file_uri_for(gem_repo2)}" + bundle "plugin install foo kung-foo --source https://gem.repo2" expect(out).to include("Installed plugin foo") expect(out).to include("Installed plugin kung-foo") @@ -49,13 +70,50 @@ RSpec.describe "bundler plugin install" do build_plugin "kung-foo", "1.1" end - bundle "plugin install foo kung-foo --version '1.0' --source #{file_uri_for(gem_repo2)}" + bundle "plugin install foo kung-foo --version '1.0' --source https://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 "installs the latest version if not installed" do + update_repo2 do + build_plugin "foo", "1.1" + end + + bundle "plugin install foo --version 1.0 --source https://gem.repo2 --verbose" + expect(out).to include("Installing foo 1.0") + + bundle "plugin install foo --source https://gem.repo2 --verbose" + expect(out).to include("Installing foo 1.1") + + bundle "plugin install foo --source https://gem.repo2 --verbose" + expect(out).to include("Using foo 1.1") + end + + it "raises an error when when --branch specified" do + bundle "plugin install foo --branch main --source https://gem.repo2", raise_on_error: false + + expect(out).not_to include("Installed plugin foo") + + expect(err).to include("--branch can only be used with git sources") + end + + it "raises an error when --ref specified" do + bundle "plugin install foo --ref v1.2.3 --source https://gem.repo2", raise_on_error: false + + expect(err).to include("--ref can only be used with git sources") + end + + it "raises error when both --branch and --ref options are specified" do + bundle "plugin install foo --source https://gem.repo2 --branch main --ref v1.2.3", raise_on_error: false + + expect(out).not_to include("Installed plugin foo") + + expect(err).to include("You cannot specify `--branch` and `--ref` at the same time.") + end + it "works with different load paths" do build_repo2 do build_plugin "testing" do |s| @@ -73,7 +131,7 @@ RSpec.describe "bundler plugin install" do s.write("src/fubar.rb") end end - bundle "plugin install testing --source #{file_uri_for(gem_repo2)}" + bundle "plugin install testing --source https://gem.repo2" bundle "check2", "no-color" => false expect(out).to eq("mate") @@ -86,19 +144,19 @@ RSpec.describe "bundler plugin install" do build_plugin "kung-foo", "1.1" end - bundle "plugin install foo kung-foo --version '1.0' --source #{file_uri_for(gem_repo2)}" + bundle "plugin install foo kung-foo --version '1.0' --source https://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 + update_repo2 do build_gem "charlie" end - bundle "plugin install charlie --source #{file_uri_for(gem_repo2)}" + bundle "plugin install charlie --source https://gem.repo2", raise_on_error: false - expect(err).to include("plugins.rb was not found") + expect(err).to include("Failed to install plugin `charlie`, due to Bundler::Plugin::MalformattedPlugin (plugins.rb was not found in the plugin.)") expect(global_plugin_gem("charlie-1.0")).not_to be_directory @@ -110,12 +168,12 @@ RSpec.describe "bundler plugin install" do build_repo2 do build_plugin "chaplin" do |s| s.write "plugins.rb", <<-RUBY - raise "I got you man" + raise RuntimeError, "threw exception on load" RUBY end end - bundle "plugin install chaplin --source #{file_uri_for(gem_repo2)}" + bundle "plugin install chaplin --source https://gem.repo2", raise_on_error: false expect(global_plugin_gem("chaplin-1.0")).not_to be_directory @@ -129,7 +187,7 @@ RSpec.describe "bundler plugin install" do s.write "plugins.rb" end - bundle "plugin install foo --git #{file_uri_for(lib_path("foo-1.0"))}" + bundle "plugin install foo --git #{lib_path("foo-1.0")}" expect(out).to include("Installed plugin foo") plugin_should_be_installed("foo") @@ -140,26 +198,61 @@ RSpec.describe "bundler plugin install" do s.write "plugins.rb" end - bundle "plugin install foo --local_git #{lib_path("foo-1.0")}" + bundle "plugin install foo --git #{lib_path("foo-1.0")}" expect(out).to include("Installed plugin foo") plugin_should_be_installed("foo") end + end - it "raises an error when both git and local git sources are specified" do - bundle "plugin install foo --local_git /phony/path/project --git git@gitphony.com:/repo/project" + context "path plugins" do + it "installs from a path source" do + build_lib "path_plugin" do |s| + s.write "plugins.rb" + end + bundle "plugin install path_plugin --path #{lib_path("path_plugin-1.0")}" - expect(exitstatus).not_to eq(0) if exitstatus - expect(err).to eq("Remote and local plugin git sources can't be both specified") + expect(out).to include("Installed plugin path_plugin") + plugin_should_be_installed("path_plugin") + end + + it "installs from a relative path source" do + build_lib "path_plugin" do |s| + s.write "plugins.rb" + end + path = lib_path("path_plugin-1.0").relative_path_from(bundled_app) + bundle "plugin install path_plugin --path #{path}" + + expect(out).to include("Installed plugin path_plugin") + plugin_should_be_installed("path_plugin") + end + + it "installs from a relative path source when inside an app" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + gemfile "" + + build_lib "ga-plugin" do |s| + s.write "plugins.rb" + end + + path = lib_path("ga-plugin-1.0").relative_path_from(bundled_app) + bundle "plugin install ga-plugin --path #{path}" + + plugin_should_be_installed("ga-plugin") + expect(local_plugin_gem("foo-1.0")).not_to be_directory end end context "Gemfile eval" do + before do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + end + it "installs plugins listed in gemfile" do gemfile <<-G - source '#{file_uri_for(gem_repo2)}' + source 'https://gem.repo2' plugin 'foo' - gem 'rack', "1.0.0" + gem 'myrack', "1.0.0" G bundle "install" @@ -168,17 +261,54 @@ RSpec.describe "bundler plugin install" do expect(out).to include("Bundle complete!") - expect(the_bundle).to include_gems("rack 1.0.0") + expect(the_bundle).to include_gems("myrack 1.0.0") plugin_should_be_installed("foo") end + it "overrides the index with the new plugin version" do + gemfile <<-G + source 'https://gem.repo2' + plugin 'foo', "1.0" + gem 'myrack', "1.0.0" + G + + bundle "install" + + update_repo2 do + build_plugin "foo", "2.0.0" + end + + gemfile <<-G + source 'https://gem.repo2' + plugin 'foo', "2.0" + gem 'myrack', "1.0.0" + G + + bundle "install" + + expected = local_plugin_gem("foo-2.0.0", "lib").to_s + expect(Bundler::Plugin.index.load_paths("foo")).to eq([expected]) + end + + it "respects bundler groups" do + gemfile <<-G + source 'https://gem.repo2' + plugin 'foo' + gem 'myrack', "1.0.0" + G + + bundle "install", env: { "BUNDLE_WITHOUT" => "default" } + + expect(out).to include("Bundle complete! 1 Gemfile dependency, 0 gems now installed.") + end + it "accepts plugin version" do update_repo2 do build_plugin "foo", "1.1.0" end gemfile <<-G - source '#{file_uri_for(gem_repo2)}' + source 'https://gem.repo2' plugin 'foo', "1.0" G @@ -197,6 +327,7 @@ RSpec.describe "bundler plugin install" do end install_gemfile <<-G + source "https://gem.repo1" plugin 'ga-plugin', :git => "#{lib_path("ga-plugin-1.0")}" G @@ -204,24 +335,54 @@ RSpec.describe "bundler plugin install" do plugin_should_be_installed("ga-plugin") end + it "accepts path sources" do + build_lib "ga-plugin" do |s| + s.write "plugins.rb" + end + + install_gemfile <<-G + source "https://gem.repo1" + plugin 'ga-plugin', :path => "#{lib_path("ga-plugin-1.0")}" + G + + expect(out).to include("Installed plugin ga-plugin") + plugin_should_be_installed("ga-plugin") + end + + it "accepts relative path sources" do + build_lib "ga-plugin" do |s| + s.write "plugins.rb" + end + + path = lib_path("ga-plugin-1.0").relative_path_from(bundled_app) + install_gemfile <<-G + source "https://gem.repo1" + plugin 'ga-plugin', :path => "#{path}" + G + + expect(out).to include("Installed plugin ga-plugin") + plugin_should_be_installed("ga-plugin") + end + context "in deployment mode" do it "installs plugins" do - install_gemfile! <<-G - source '#{file_uri_for(gem_repo2)}' - gem 'rack', "1.0.0" + install_gemfile <<-G + source 'https://gem.repo2' + gem 'myrack', "1.0.0" G - install_gemfile! <<-G, forgotten_command_line_options(:deployment => true) - source '#{file_uri_for(gem_repo2)}' + bundle_config "deployment true" + install_gemfile <<-G + source 'https://gem.repo2' plugin 'foo' - gem 'rack', "1.0.0" + gem 'myrack', "1.0.0" G expect(out).to include("Installed plugin foo") expect(out).to include("Bundle complete!") - expect(the_bundle).to include_gems("rack 1.0.0") + expect(the_bundle).to include_gems("myrack 1.0.0") plugin_should_be_installed("foo") end end @@ -233,20 +394,21 @@ RSpec.describe "bundler plugin install" do require "bundler/inline" gemfile do - source '#{file_uri_for(gem_repo2)}' + source 'https://gem.repo2' plugin 'foo' end RUBY - ruby code + ruby code, artifice: "compact_index", env: { "BUNDLER_VERSION" => Bundler::VERSION } 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 + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) gemfile "" - bundle "plugin install foo --source #{file_uri_for(gem_repo2)}" + bundle "plugin install foo --source https://gem.repo2" plugin_should_be_installed("foo") expect(local_plugin_gem("foo-1.0")).to be_directory @@ -269,7 +431,7 @@ RSpec.describe "bundler plugin install" do end # inside the app - gemfile "source '#{file_uri_for(gem_repo2)}'\nplugin 'fubar'" + gemfile "source 'https://gem.repo2'\nplugin 'fubar'" bundle "install" update_repo2 do @@ -287,21 +449,16 @@ RSpec.describe "bundler plugin install" do end # outside the app - Dir.chdir tmp - bundle "plugin install fubar --source #{file_uri_for(gem_repo2)}" + bundle "plugin install fubar --source https://gem.repo2", dir: tmp 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" + bundle "shout", dir: tmp expect(out).to eq("global_one") end end diff --git a/spec/bundler/plugins/list_spec.rb b/spec/bundler/plugins/list_spec.rb index 4a686415ad..30e3f82467 100644 --- a/spec/bundler/plugins/list_spec.rb +++ b/spec/bundler/plugins/list_spec.rb @@ -38,7 +38,7 @@ RSpec.describe "bundler plugin list" do context "single plugin installed" do it "shows plugin name with commands list" do - bundle "plugin install foo --source #{file_uri_for(gem_repo2)}" + bundle "plugin install foo --source https://gem.repo2" plugin_should_be_installed("foo") bundle "plugin list" @@ -49,7 +49,7 @@ RSpec.describe "bundler plugin list" do context "multiple plugins installed" do it "shows plugin names with commands list" do - bundle "plugin install foo bar --source #{file_uri_for(gem_repo2)}" + bundle "plugin install foo bar --source https://gem.repo2" plugin_should_be_installed("foo", "bar") bundle "plugin list" diff --git a/spec/bundler/plugins/source/example_spec.rb b/spec/bundler/plugins/source/example_spec.rb index 64002d8f46..4cd4a1a931 100644 --- a/spec/bundler/plugins/source/example_spec.rb +++ b/spec/bundler/plugins/source/example_spec.rb @@ -33,6 +33,7 @@ RSpec.describe "real source plugins" do def install(spec, opts) mkdir_p(install_path.parent) + require 'fileutils' FileUtils.cp_r(path, install_path) spec_path = install_path.join("\#{spec.full_name}.gemspec") @@ -51,7 +52,7 @@ RSpec.describe "real source plugins" do build_lib "a-path-gem" gemfile <<-G - source "#{file_uri_for(gem_repo2)}" # plugin source + source "https://gem.repo2" # plugin source source "#{lib_path("a-path-gem-1.0")}", :type => :mpath do gem "a-path-gem" end @@ -66,35 +67,14 @@ RSpec.describe "real source plugins" do expect(the_bundle).to include_gems("a-path-gem 1.0") end - it "writes to lock file", :bundler => "< 3" do + it "writes to lockfile" 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_uri_for(gem_repo2)}/ - specs: - - PLATFORMS - #{generic_local_platform} - - DEPENDENCIES - a-path-gem! - - BUNDLED WITH - #{Bundler::VERSION} - G - end - - it "writes to lock file", :bundler => "3" do - bundle "install" + checksums = checksums_section_when_enabled do |c| + c.no_checksum "a-path-gem", "1.0" + end - lockfile_should_be <<-G + expect(lockfile).to eq <<~G PLUGIN SOURCE remote: #{lib_path("a-path-gem-1.0")} type: mpath @@ -102,7 +82,7 @@ RSpec.describe "real source plugins" do a-path-gem (1.0) GEM - remote: #{file_uri_for(gem_repo2)}/ + remote: https://gem.repo2/ specs: PLATFORMS @@ -110,9 +90,9 @@ RSpec.describe "real source plugins" do DEPENDENCIES a-path-gem! - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end @@ -125,14 +105,14 @@ RSpec.describe "real source plugins" do end it "installs the gem executables" do - build_lib "gem-with-bin" do |s| + build_lib "gem_with_bin" do |s| s.executables = ["foo"] end install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" # plugin source - source "#{lib_path("gem-with-bin-1.0")}", :type => :mpath do - gem "gem-with-bin" + source "https://gem.repo2" # plugin source + source "#{lib_path("gem_with_bin-1.0")}", :type => :mpath do + gem "gem_with_bin" end G @@ -144,36 +124,35 @@ RSpec.describe "real source plugins" 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 "config set cache_all true" bundle :cache 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") + FileUtils.rm_r 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, forgotten_command_line_options(:path => "vendor/bundle") - bundle "config set cache_all true" - bundle! :cache + it "copies repository to vendor cache and uses it even when installed with `path` configured" do + bundle_config "path vendor/bundle" + bundle :install + bundle :cache expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist - FileUtils.rm_rf lib_path("a-path-gem-1.0") + FileUtils.rm_r 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, forgotten_command_line_options(:path => "vendor/bundle") - bundle "config set cache_all true" - bundle! :cache + bundle_config "path vendor/bundle" + bundle :install + bundle :cache expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist - FileUtils.rm_rf lib_path("a-path-gem-1.0") + FileUtils.rm_r lib_path("a-path-gem-1.0") expect(the_bundle).to include_gems("a-path-gem 1.0") end end @@ -188,7 +167,7 @@ RSpec.describe "real source plugins" do a-path-gem (1.0) GEM - remote: #{file_uri_for(gem_repo2)}/ + remote: https://gem.repo2/ specs: PLATFORMS @@ -198,12 +177,12 @@ RSpec.describe "real source plugins" do a-path-gem! BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end it "installs" do - bundle! "install" + bundle "install" expect(the_bundle).to include_gems("a-path-gem 1.0") end @@ -215,6 +194,8 @@ RSpec.describe "real source plugins" do build_repo2 do build_plugin "bundler-source-gitp" do |s| s.write "plugins.rb", <<-RUBY + require "open3" + class SPlugin < Bundler::Plugin::API source "gitp" @@ -223,7 +204,7 @@ RSpec.describe "real source plugins" do def initialize(opts) super - @ref = options["ref"] || options["branch"] || options["tag"] || "master" + @ref = options["ref"] || options["branch"] || options["tag"] || "main" @unlocked = false end @@ -254,9 +235,7 @@ RSpec.describe "real source plugins" do 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 + Open3.capture2e("git reset --hard \#{revision}", :chdir => install_path) spec_path = install_path.join("\#{spec.full_name}.gemspec") spec_path.open("wb") {|f| f.write spec.to_ruby } @@ -269,7 +248,7 @@ RSpec.describe "real source plugins" do def options_to_lock opts = {"revision" => revision} - opts["ref"] = ref if ref != "master" + opts["ref"] = ref if ref != "main" opts end @@ -310,9 +289,8 @@ RSpec.describe "real source plugins" do cache_repo end - Dir.chdir cache_path do - `git rev-parse --verify \#{@ref}`.strip - end + output, _status = Open3.capture2e("git rev-parse --verify \#{@ref}", :chdir => cache_path) + output.strip end def base_name @@ -327,13 +305,7 @@ RSpec.describe "real source plugins" do @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 + gem_install_dir.join(git_scope) end end @@ -348,8 +320,8 @@ RSpec.describe "real source plugins" do build_git "ma-gitp-gem" gemfile <<-G - source "#{file_uri_for(gem_repo2)}" # plugin source - source "#{file_uri_for(lib_path("ma-gitp-gem-1.0"))}", :type => :gitp do + source "https://gem.repo2" # plugin source + source "#{lib_path("ma-gitp-gem-1.0")}", :type => :gitp do gem "ma-gitp-gem" end G @@ -361,47 +333,24 @@ RSpec.describe "real source plugins" do expect(the_bundle).to include_gems("ma-gitp-gem 1.0") end - it "writes to lock file", :bundler => "< 3" do + it "writes to lockfile" do revision = revision_for(lib_path("ma-gitp-gem-1.0")) bundle "install" - lockfile_should_be <<-G - PLUGIN SOURCE - remote: #{file_uri_for(lib_path("ma-gitp-gem-1.0"))} - type: gitp - revision: #{revision} - specs: - ma-gitp-gem (1.0) - - GEM - remote: #{file_uri_for(gem_repo2)}/ - specs: - - PLATFORMS - #{generic_local_platform} - - DEPENDENCIES - ma-gitp-gem! - - BUNDLED WITH - #{Bundler::VERSION} - G - end - - it "writes to lock file", :bundler => "3" do - revision = revision_for(lib_path("ma-gitp-gem-1.0")) - bundle "install" + checksums = checksums_section_when_enabled do |c| + c.no_checksum "ma-gitp-gem", "1.0" + end - lockfile_should_be <<-G + expect(lockfile).to eq <<~G PLUGIN SOURCE - remote: #{file_uri_for(lib_path("ma-gitp-gem-1.0"))} + remote: #{lib_path("ma-gitp-gem-1.0")} type: gitp revision: #{revision} specs: ma-gitp-gem (1.0) GEM - remote: #{file_uri_for(gem_repo2)}/ + remote: https://gem.repo2/ specs: PLATFORMS @@ -409,9 +358,9 @@ RSpec.describe "real source plugins" do DEPENDENCIES ma-gitp-gem! - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end @@ -420,14 +369,14 @@ RSpec.describe "real source plugins" do revision = revision_for(lib_path("ma-gitp-gem-1.0")) lockfile <<-G PLUGIN SOURCE - remote: #{file_uri_for(lib_path("ma-gitp-gem-1.0"))} + remote: #{lib_path("ma-gitp-gem-1.0")} type: gitp revision: #{revision} specs: ma-gitp-gem (1.0) GEM - remote: #{file_uri_for(gem_repo2)}/ + remote: https://gem.repo2/ specs: PLATFORMS @@ -437,7 +386,7 @@ RSpec.describe "real source plugins" do ma-gitp-gem! BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end @@ -451,7 +400,7 @@ RSpec.describe "real source plugins" do bundle "install" run <<-RUBY - require 'ma-gitp-gem' + require 'ma/gitp/gem' puts "WIN" unless defined?(MAGITPGEM_PREV_REF) RUBY expect(out).to eq("WIN") @@ -462,17 +411,17 @@ RSpec.describe "real source plugins" do bundle "update ma-gitp-gem" run <<-RUBY - require 'ma-gitp-gem' + 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 + update_git "ma-gitp-gem", "1.1", path: lib_path("ma-gitp-gem-1.0"), gemspec: true gemfile <<-G - source "#{file_uri_for(gem_repo2)}" # plugin source - source "#{file_uri_for(lib_path("ma-gitp-gem-1.0"))}", :type => :gitp do + source "https://gem.repo2" # plugin source + source "#{lib_path("ma-gitp-gem-1.0")}", :type => :gitp do gem "ma-gitp-gem", "1.1" end G @@ -485,22 +434,21 @@ RSpec.describe "real source plugins" do 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) + ref = git.ref_for("main", 11) install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" # plugin source + source "https://gem.repo2" # plugin source source '#{lib_path("foo-1.0")}', :type => :gitp do gem "foo" end G - bundle "config set cache_all true" bundle :cache 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") + FileUtils.rm_r lib_path("foo-1.0") expect(the_bundle).to include_gems "foo 1.0" end end diff --git a/spec/bundler/plugins/source_spec.rb b/spec/bundler/plugins/source_spec.rb index c8deee96b1..995e50e653 100644 --- a/spec/bundler/plugins/source_spec.rb +++ b/spec/bundler/plugins/source_spec.rb @@ -16,11 +16,12 @@ RSpec.describe "bundler source plugin" do it "installs bundler-source-* gem when no handler for source is present" do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - source "#{file_uri_for(lib_path("gitp"))}", :type => :psource do + source "https://gem.repo2" + source "#{lib_path("gitp")}", :type => :psource do end G + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) plugin_should_be_installed("bundler-source-psource") end @@ -37,8 +38,8 @@ RSpec.describe "bundler source plugin" do end install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" - source "#{file_uri_for(lib_path("gitp"))}", :type => :psource do + source "https://gem.repo2" + source "#{lib_path("gitp")}", :type => :psource do end G @@ -61,11 +62,11 @@ RSpec.describe "bundler source plugin" do context "explicit presence in gemfile" do before do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" plugin "another-psource" - source "#{file_uri_for(lib_path("gitp"))}", :type => :psource do + source "#{lib_path("gitp")}", :type => :psource do end G end @@ -75,6 +76,7 @@ RSpec.describe "bundler source plugin" do end it "installs the explicit one" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) plugin_should_be_installed("another-psource") end @@ -86,11 +88,11 @@ RSpec.describe "bundler source plugin" do context "explicit default source" do before do install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" plugin "bundler-source-psource" - source "#{file_uri_for(lib_path("gitp"))}", :type => :psource do + source "#{lib_path("gitp")}", :type => :psource do end G end @@ -100,6 +102,7 @@ RSpec.describe "bundler source plugin" do end it "installs the default one" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) plugin_should_be_installed("bundler-source-psource") end end diff --git a/spec/bundler/plugins/uninstall_spec.rb b/spec/bundler/plugins/uninstall_spec.rb new file mode 100644 index 0000000000..dedcc9f37c --- /dev/null +++ b/spec/bundler/plugins/uninstall_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +RSpec.describe "bundler plugin uninstall" do + before do + build_repo2 do + build_plugin "foo" + build_plugin "kung-foo" + end + end + + it "shows proper error message when plugins are not specified" do + bundle "plugin uninstall" + expect(err).to include("No plugins to uninstall") + end + + it "uninstalls specified plugins" do + bundle "plugin install foo kung-foo --source https://gem.repo2" + plugin_should_be_installed("foo") + plugin_should_be_installed("kung-foo") + + bundle "plugin uninstall foo" + expect(out).to include("Uninstalled plugin foo") + plugin_should_not_be_installed("foo") + plugin_should_be_installed("kung-foo") + end + + it "shows proper message when plugin is not installed" do + bundle "plugin uninstall foo" + expect(err).to include("Plugin foo is not installed") + plugin_should_not_be_installed("foo") + end + + it "doesn't wipe out path plugins" do + build_lib "path_plugin" do |s| + s.write "plugins.rb" + end + path = lib_path("path_plugin-1.0") + expect(path).to be_a_directory + + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + + install_gemfile <<-G + source 'https://gem.repo2' + plugin 'path_plugin', :path => "#{path}" + gem 'myrack', '1.0.0' + G + + plugin_should_be_installed("path_plugin") + expect(Bundler::Plugin.index.plugin_path("path_plugin")).to eq path + + bundle "plugin uninstall path_plugin" + expect(out).to include("Uninstalled plugin path_plugin") + plugin_should_not_be_installed("path_plugin") + # the actual gem still exists though + expect(path).to be_a_directory + end + + describe "with --all" do + it "uninstalls all installed plugins" do + bundle "plugin install foo kung-foo --source https://gem.repo2" + plugin_should_be_installed("foo") + plugin_should_be_installed("kung-foo") + + bundle "plugin uninstall --all" + plugin_should_not_be_installed("foo") + plugin_should_not_be_installed("kung-foo") + end + + it "shows proper no plugins installed message when no plugins installed" do + bundle "plugin uninstall --all" + expect(out).to include("No plugins installed") + end + end +end diff --git a/spec/bundler/quality_es_spec.rb b/spec/bundler/quality_es_spec.rb index 4238ac7452..e68674c030 100644 --- a/spec/bundler/quality_es_spec.rb +++ b/spec/bundler/quality_es_spec.rb @@ -17,7 +17,7 @@ RSpec.describe "La biblioteca si misma" do ] pattern = /\b#{Regexp.union(useless_words)}\b/i - File.readlines(filename).each_with_index do |line, number| + File.readlines(File.expand_path(filename, source_root)).each_with_index do |line, number| next unless word_found = pattern.match(line) failing_line_message << "#{filename}:#{number.succ} contiene '#{word_found}'. Esta palabra tiene un significado subjetivo y es mejor obviarla en textos técnicos." end @@ -29,7 +29,7 @@ RSpec.describe "La biblioteca si misma" do failing_line_message = [] specific_pronouns = /\b(él|ella|ellos|ellas)\b/i - File.readlines(filename).each_with_index do |line, number| + File.readlines(File.expand_path(filename, source_root)).each_with_index do |line, number| next unless word_found = specific_pronouns.match(line) failing_line_message << "#{filename}:#{number.succ} contiene '#{word_found}'. Use pronombres más genéricos en la documentación." end @@ -40,12 +40,10 @@ RSpec.describe "La biblioteca si misma" do it "mantiene la calidad de lenguaje de la documentación" do included = /ronn/ error_messages = [] - Dir.chdir(root) do - `git ls-files -z -- man`.split("\x0").each do |filename| - next unless filename =~ included - error_messages << check_for_expendable_words(filename) - error_messages << check_for_specific_pronouns(filename) - end + man_tracked_files.each do |filename| + next unless filename&.match?(included) + error_messages << check_for_expendable_words(filename) + error_messages << check_for_specific_pronouns(filename) end expect(error_messages.compact).to be_well_formed end @@ -53,12 +51,10 @@ RSpec.describe "La biblioteca si misma" do it "mantiene la calidad de lenguaje de oraciones usadas en el código fuente" do error_messages = [] exempt = /vendor/ - Dir.chdir(root) do - lib_tracked_files.split("\x0").each do |filename| - next if filename =~ exempt - error_messages << check_for_expendable_words(filename) - error_messages << check_for_specific_pronouns(filename) - end + lib_tracked_files.each do |filename| + next if filename&.match?(exempt) + error_messages << check_for_expendable_words(filename) + error_messages << check_for_specific_pronouns(filename) end expect(error_messages.compact).to be_well_formed end diff --git a/spec/bundler/quality_spec.rb b/spec/bundler/quality_spec.rb index 09e59d88ae..16b7f18788 100644 --- a/spec/bundler/quality_spec.rb +++ b/spec/bundler/quality_spec.rb @@ -3,25 +3,6 @@ require "set" RSpec.describe "The library itself" do - def check_for_debugging_mechanisms(filename) - debugging_mechanisms_regex = / - (binding\.pry)| - (debugger)| - (sleep\s*\(?\d+)| - (fit\s*\(?("|\w)) - /x - - failing_lines = [] - each_line(filename) 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 = / <<<<<<<| @@ -31,7 +12,7 @@ RSpec.describe "The library itself" do failing_lines = [] each_line(filename) do |line, number| - failing_lines << number + 1 if line =~ merge_conflicts_regex + failing_lines << number + 1 if line&.match?(merge_conflicts_regex) end return if failing_lines.empty? @@ -39,9 +20,12 @@ RSpec.describe "The library itself" do end def check_for_tab_characters(filename) + # Because Go uses hard tabs + return if filename.end_with?(".go.tt") + failing_lines = [] each_line(filename) do |line, number| - failing_lines << number + 1 if line =~ /\t/ + failing_lines << number + 1 if line.include?("\t") end return if failing_lines.empty? @@ -51,24 +35,22 @@ RSpec.describe "The library itself" do def check_for_extra_spaces(filename) failing_lines = [] each_line(filename) do |line, number| - next if line =~ /^\s+#.*\s+\n$/ - failing_lines << number + 1 if line =~ /\s+\n$/ + next if /^\s+#.*\s+\n$/.match?(line) + failing_lines << number + 1 if /\s+\n$/.match?(line) end return if failing_lines.empty? "#{filename} has spaces on the EOL on lines #{failing_lines.join(", ")}" end - def check_for_straneous_quotes(filename) - return if File.expand_path(filename) == __FILE__ - + def check_for_extraneous_quotes(filename) failing_lines = [] each_line(filename) do |line, number| - failing_lines << number + 1 if line =~ /’/ + failing_lines << number + 1 if /\u{2019}/.match?(line) end return if failing_lines.empty? - "#{filename} has an straneous quote on lines #{failing_lines.join(", ")}" + "#{filename} has an extraneous quote on lines #{failing_lines.join(", ")}" end def check_for_expendable_words(filename) @@ -105,89 +87,69 @@ RSpec.describe "The library itself" do end it "has no malformed whitespace" do - exempt = /\.gitmodules|fixtures|vendor|LICENSE|vcr_cassettes|rbreadline\.diff|\.txt$/ + exempt = /\.gitmodules|fixtures|vendor|LICENSE|vcr_cassettes|rbreadline\.diff|index\.txt$/ error_messages = [] - Dir.chdir(root) do - tracked_files.split("\x0").each do |filename| - next if filename =~ exempt - error_messages << check_for_tab_characters(filename) - error_messages << check_for_extra_spaces(filename) - end + tracked_files.each do |filename| + next if filename&.match?(exempt) + error_messages << check_for_tab_characters(filename) + error_messages << check_for_extra_spaces(filename) end expect(error_messages.compact).to be_well_formed end - it "has no estraneous quotes" do + it "has no extraneous quotes" do exempt = /vendor|vcr_cassettes|LICENSE|rbreadline\.diff/ error_messages = [] - Dir.chdir(root) do - tracked_files.split("\x0").each do |filename| - next if filename =~ exempt - error_messages << check_for_straneous_quotes(filename) - end - end - expect(error_messages.compact).to be_well_formed - end - - it "does not include any leftover debugging or development mechanisms" do - exempt = %r{quality_spec.rb|support/helpers|vcr_cassettes|\.md|\.ronn|\.txt|\.5|\.1} - error_messages = [] - Dir.chdir(root) do - tracked_files.split("\x0").each do |filename| - next if filename =~ exempt - error_messages << check_for_debugging_mechanisms(filename) - end + tracked_files.each do |filename| + next if filename&.match?(exempt) + error_messages << check_for_extraneous_quotes(filename) end expect(error_messages.compact).to be_well_formed end it "does not include any unresolved merge conflicts" do error_messages = [] - exempt = %r{lock/lockfile_spec|quality_spec|vcr_cassettes|\.ronn|lockfile_parser\.rb} - Dir.chdir(root) do - tracked_files.split("\x0").each do |filename| - next if filename =~ exempt - error_messages << check_for_git_merge_conflicts(filename) - end + exempt = %r{lock/lockfile_spec|quality_spec|vcr_cassettes|\.ronn|lockfile_parser} + tracked_files.each do |filename| + next if filename&.match?(exempt) + error_messages << check_for_git_merge_conflicts(filename) end expect(error_messages.compact).to be_well_formed end it "maintains language quality of the documentation" do - included = /ronn/ error_messages = [] - Dir.chdir(root) do - `git ls-files -z -- man`.split("\x0").each do |filename| - next unless filename =~ included - error_messages << check_for_expendable_words(filename) - error_messages << check_for_specific_pronouns(filename) - end + man_tracked_files.each do |filename| + error_messages << check_for_expendable_words(filename) + error_messages << check_for_specific_pronouns(filename) end expect(error_messages.compact).to be_well_formed end it "maintains language quality of sentences used in source code" do error_messages = [] - exempt = /vendor|vcr_cassettes/ - Dir.chdir(root) do - lib_tracked_files.split("\x0").each do |filename| - next if filename =~ exempt - error_messages << check_for_expendable_words(filename) - error_messages << check_for_specific_pronouns(filename) - end + exempt = /vendor|vcr_cassettes|CODE_OF_CONDUCT/ + lib_tracked_files.each do |filename| + next if filename&.match?(exempt) + error_messages << check_for_expendable_words(filename) + error_messages << check_for_specific_pronouns(filename) end expect(error_messages.compact).to be_well_formed end it "documents all used settings" do exemptions = %w[ - auto_config_jobs - deployment_means_frozen - forget_cli_options + gem.changelog + gem.ci gem.coc + gem.linter gem.mit + gem.bundle + gem.rubocop + gem.test + git.allow_insecure inline - use_gem_version_promoter_for_major_updates + trust-policy ] all_settings = Hash.new {|h, k| h[k] = [] } @@ -196,16 +158,16 @@ RSpec.describe "The library itself" do 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" } Bundler::Settings::ARRAY_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::ARRAY_KEYS" } + Bundler::Settings::STRING_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::STRING_KEYS" } - Dir.chdir(root) do - key_pattern = /([a-z\._-]+)/i - lib_tracked_files.split("\x0").each do |filename| - each_line(filename) do |line, number| - line.scan(/Bundler\.settings\[:#{key_pattern}\]/).flatten.each {|s| all_settings[s] << "referenced at `#{filename}:#{number.succ}`" } - end + key_pattern = /([a-z\._-]+)/i + lib_tracked_files.each do |filename| + each_line(filename) do |line, number| + line.scan(/Bundler\.settings\[:#{key_pattern}\]/).flatten.each {|s| all_settings[s] << "referenced at `#{filename}:#{number.succ}`" } end - documented_settings = File.read("man/bundle-config.ronn")[/LIST OF AVAILABLE KEYS.*/m].scan(/^\* `#{key_pattern}`/).flatten end + settings_section = File.read(source_root.join("lib/bundler/man/bundle-config.1.ronn")).split(/^## /).find {|section| section.start_with?("LIST OF AVAILABLE KEYS") } + documented_settings = settings_section.scan(/^\* `#{key_pattern}`/).flatten documented_settings.each do |s| all_settings.delete(s) @@ -225,65 +187,75 @@ RSpec.describe "The library itself" do end it "can still be built" do - with_built_bundler do |_gem_path| - expect(err).to be_empty, "bundler should build as a gem without warnings, but\n#{err}" + with_built_bundler do |gem_path| + expect(File.exist?(gem_path)).to be true end end it "ships the correct set of files" do - Dir.chdir(root) do - git_list = shipped_files.split("\x0") + git_list = tracked_files.reject {|f| f.start_with?("spec/") } - gem_list = Gem::Specification.load(gemspec.to_s).files + gem_list = loaded_gemspec.files + gem_list.map! {|f| f.sub(%r{\Aexe/}, "libexec/") } if ruby_core? - expect(git_list.to_set).to eq(gem_list.to_set) - end + expect(git_list).to match_array(gem_list) end it "does not contain any warnings" do - Dir.chdir(root) do - exclusions = %w[ - lib/bundler/capistrano.rb - lib/bundler/deployment.rb - lib/bundler/gem_tasks.rb - lib/bundler/vlad.rb - lib/bundler/templates/gems.rb - ] - files_to_require = lib_tracked_files.split("\x0").grep(/\.rb$/) - exclusions - files_to_require.reject! {|f| f.start_with?("lib/bundler/vendor") } - files_to_require.map! {|f| f.chomp(".rb") } - sys_exec!("ruby -w -Ilib") do |input, _, _| - files_to_require.each do |f| - input.puts "require '#{f.sub(%r{\Alib/}, "")}'" - end + exclusions = %w[ + lib/bundler/capistrano.rb + lib/bundler/deployment.rb + lib/bundler/gem_tasks.rb + lib/bundler/vlad.rb + ] + files_to_require = lib_tracked_files.grep(/\.rb$/) - exclusions + files_to_require.reject! {|f| f.start_with?("lib/bundler/vendor") } + files_to_require.map! {|f| File.expand_path(f, source_root) } + files_to_require.sort! + sys_exec("ruby -w") do |input, _, _| + files_to_require.each do |f| + input.puts "require '#{f}'" end + end - warnings = last_command.stdboth.split("\n") - # ignore warnings around deprecated Object#=~ method in RubyGems - warnings.reject! {|w| w =~ %r{rubygems\/version.rb.*deprecated\ Object#=~} } + warnings = stdboth.split("\n") + # ignore warnings around deprecated Object#=~ method in RubyGems + warnings.reject! {|w| w =~ %r{rubygems\/version.rb.*deprecated\ Object#=~} } - expect(warnings).to be_well_formed - end + expect(warnings).to be_well_formed end it "does not use require internally, but require_relative" do - Dir.chdir(root) do - exempt = %r{templates/|vendor/} - all_bad_requires = [] - lib_tracked_files.split("\x0").each do |filename| - next if filename =~ exempt - each_line(filename) do |line, number| - line.scan(/^ *require "bundler/).each { all_bad_requires << "#{filename}:#{number.succ}" } - end + exempt = %r{templates/|\.5|\.1|vendor/} + all_bad_requires = [] + lib_tracked_files.each do |filename| + next if filename&.match?(exempt) + each_line(filename) do |line, number| + line.scan(/^ *require "bundler/).each { all_bad_requires << "#{filename}:#{number.succ}" } end - - expect(all_bad_requires).to be_empty, "#{all_bad_requires.size} internal requires that should use `require_relative`: #{all_bad_requires}" end + + expect(all_bad_requires).to be_empty, "#{all_bad_requires.size} internal requires that should use `require_relative`: #{all_bad_requires}" + end + + # We don't want our artifice code to activate bundler, but it needs to use the + # namespaced implementation of `Net::HTTP`. So we duplicate the file in + # bundler that loads that. + it "keeps vendored_net_http spec code in sync with the lib implementation" do + lib_implementation_path = File.join(source_lib_dir, "bundler", "vendored_net_http.rb") + expect(File.exist?(lib_implementation_path)).to be_truthy + lib_code = File.read(lib_implementation_path) + + spec_implementation_path = File.join(spec_dir, "support", "vendored_net_http.rb") + expect(File.exist?(spec_implementation_path)).to be_truthy + spec_code = File.read(spec_implementation_path) + + expect(lib_code).to eq(spec_code) end -private + private def each_line(filename, &block) - File.readlines(filename, :encoding => "UTF-8").each_with_index(&block) + File.readlines(File.expand_path(filename, source_root), encoding: "UTF-8").each_with_index(&block) end end diff --git a/spec/bundler/realworld/dependency_api_spec.rb b/spec/bundler/realworld/dependency_api_spec.rb deleted file mode 100644 index dea8329a4d..0000000000 --- a/spec/bundler/realworld/dependency_api_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require_relative "../support/silent_logger" - -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_relative "../support/artifice/endpoint_timeout" - - @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) - bundle! "config set timeout 1" - end - - after do - Artifice.deactivate - @t.kill - @t.join - end - - it "times out and falls back on the modern index" do - install_gemfile! <<-G, :artifice => nil - source "#{@server_uri}" - gem "rack" - G - - 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/double_check_spec.rb b/spec/bundler/realworld/double_check_spec.rb index 90cf298b33..0741560395 100644 --- a/spec/bundler/realworld/double_check_spec.rb +++ b/spec/bundler/realworld/double_check_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe "double checking sources", :realworld => true, :sometimes => true do +RSpec.describe "double checking sources", realworld: true do it "finds already-installed gems" do create_file("rails.gemspec", <<-RUBY) Gem::Specification.new do |s| @@ -25,16 +25,16 @@ RSpec.describe "double checking sources", :realworld => true, :sometimes => true RUBY cmd = <<-RUBY - require "#{lib_dir}/bundler" + require "bundler" require "#{spec_dir}/support/artifice/vcr" - require "#{lib_dir}/bundler/inline" + require "bundler/inline" gemfile(true) do source "https://rubygems.org" gem "rails", path: "." end RUBY - ruby! cmd - ruby! cmd + ruby cmd + ruby cmd end end diff --git a/spec/bundler/realworld/edgecases_spec.rb b/spec/bundler/realworld/edgecases_spec.rb index 53d9f9a026..391aa0cef6 100644 --- a/spec/bundler/realworld/edgecases_spec.rb +++ b/spec/bundler/realworld/edgecases_spec.rb @@ -1,20 +1,22 @@ # frozen_string_literal: true -RSpec.describe "real world edgecases", :realworld => true, :sometimes => true do +RSpec.describe "real world edgecases", realworld: true do def rubygems_version(name, requirement) - ruby! <<-RUBY + ruby <<-RUBY require "#{spec_dir}/support/artifice/vcr" - require "#{lib_dir}/bundler" - require "#{lib_dir}/bundler/source/rubygems/remote" - require "#{lib_dir}/bundler/fetcher" + require "bundler" + require "bundler/source/rubygems/remote" + require "bundler/fetcher" rubygem = Bundler.ui.silence do - source = Bundler::Source::Rubygems::Remote.new(URI("https://rubygems.org")) - fetcher = Bundler::Fetcher.new(source) - index = fetcher.specs([#{name.dump}], nil) - index.search(Gem::Dependency.new(#{name.dump}, #{requirement.dump})).last + remote = Bundler::Source::Rubygems::Remote.new(Gem::URI("https://rubygems.org")) + source = Bundler::Source::Rubygems.new + fetcher = Bundler::Fetcher.new(remote) + index = fetcher.specs([#{name.dump}], source) + requirement = Gem::Requirement.create(#{requirement.dump}) + index.search(#{name.dump}).select {|spec| requirement.satisfied_by?(spec.version) }.last end if rubygem.nil? - raise "Could not find #{name} (#{requirement}) on rubygems.org!\n" \ + raise ArgumentError, "Could not find #{name} (#{requirement}) on rubygems.org!\n" \ "Found specs:\n\#{index.send(:specs).inspect}" end puts "#{name} (\#{rubygem.version})" @@ -29,7 +31,7 @@ RSpec.describe "real world edgecases", :realworld => true, :sometimes => true do gem 'capybara', '~> 2.2.0' gem 'rack-cache', '1.2.0' # last version that works on Ruby 1.9 G - bundle! :lock + bundle :lock expect(lockfile).to include(rubygems_version("rails", "~> 5.0")) expect(lockfile).to include("capybara (2.2.1)") end @@ -43,7 +45,7 @@ RSpec.describe "real world edgecases", :realworld => true, :sometimes => true do 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 + bundle :lock expect(lockfile).to include("gxapi_rails (0.0.6)") end @@ -56,292 +58,18 @@ RSpec.describe "real world edgecases", :realworld => true, :sometimes => true do gem "activerecord", "~> 3.0" gem "builder", "~> 2.1.2" G - bundle! :lock + 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" 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, < 3.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) - 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 "config set --local path vendor/bundle" - bundle! :install - expect(err).not_to include("Could not find rake") - expect(err).to be_empty - end - - it "checks out git repos when the lockfile is corrupted" do - gemfile <<-G - source "https://rubygems.org" - git_source(:github) {|repo| "https://github.com/\#{repo}.git" } - - 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: https://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: https://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: https://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 be_empty - end - - it "outputs a helpful error message when gems have invalid gemspecs" do - install_gemfile <<-G, :standalone => true + it "outputs a helpful warning when gems have a gemspec with invalid `require_paths`" do + install_gemfile <<-G, standalone: true, env: { "BUNDLE_FORCE_RUBY_PLATFORM" => "1" } source 'https://rubygems.org' gem "resque-scheduler", "2.2.0" + gem "redis-namespace", "1.6.0" # for a consistent resolution including ruby 2.3.0 + gem "ruby2_keywords", "0.0.5" G - expect(err).to include("You have one or more invalid gemspecs that need to be fixed.") - expect(err).to include("resque-scheduler 2.2.0 has an invalid gemspec") + expect(err).to include("resque-scheduler 2.2.0 includes a gemspec with `require_paths` set to an array of arrays. Newer versions of this gem might've already fixed this").once end end diff --git a/spec/bundler/realworld/ffi_spec.rb b/spec/bundler/realworld/ffi_spec.rb new file mode 100644 index 0000000000..bede372b41 --- /dev/null +++ b/spec/bundler/realworld/ffi_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +RSpec.describe "loading dynamically linked library on a bundle exec context", realworld: true do + it "passes ENV right after argv in memory" do + create_file "foo.rb", <<~RUBY + require 'ffi' + + module FOO + extend FFI::Library + ffi_lib './libfoo.so' + + attach_function :Hello, [], :void + end + + FOO.Hello() + RUBY + + create_file "libfoo.c", <<~'C' + #include <stdio.h> + + static int foo_init(int argc, char** argv, char** envp) { + if (argv[argc+1] == NULL) { + printf("FAIL\n"); + } else { + printf("OK\n"); + } + + return 0; + } + + #if defined(__APPLE__) && defined(__MACH__) + __attribute__((section("__DATA,__mod_init_func"), used, aligned(sizeof(void*)))) + #else + __attribute__((section(".init_array"))) + #endif + static void *ctr = &foo_init; + + extern char** environ; + + void Hello() { + return; + } + C + + in_bundled_app "gcc -g -o libfoo.so -shared -fpic libfoo.c" + + install_gemfile <<-G + source "https://rubygems.org" + + gem 'ffi', force_ruby_platform: true + G + + bundle "exec ruby foo.rb" + + expect(out).to eq("OK") + end +end diff --git a/spec/bundler/realworld/fixtures/tapioca/Gemfile b/spec/bundler/realworld/fixtures/tapioca/Gemfile new file mode 100644 index 0000000000..447d715706 --- /dev/null +++ b/spec/bundler/realworld/fixtures/tapioca/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem "tapioca" diff --git a/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock b/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock new file mode 100644 index 0000000000..c2df2f9229 --- /dev/null +++ b/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock @@ -0,0 +1,49 @@ +GEM + remote: https://rubygems.org/ + specs: + erubi (1.13.1) + netrc (0.11.0) + parallel (1.26.3) + prism (1.3.0) + rbi (0.2.2) + prism (~> 1.0) + sorbet-runtime (>= 0.5.9204) + sorbet (0.5.11725) + sorbet-static (= 0.5.11725) + sorbet-runtime (0.5.11725) + sorbet-static (0.5.11725-aarch64-linux) + sorbet-static (0.5.11725-universal-darwin) + sorbet-static (0.5.11725-x86_64-linux) + sorbet-static-and-runtime (0.5.11725) + sorbet (= 0.5.11725) + sorbet-runtime (= 0.5.11725) + spoom (1.5.0) + erubi (>= 1.10.0) + prism (>= 0.28.0) + sorbet-static-and-runtime (>= 0.5.10187) + thor (>= 0.19.2) + tapioca (0.16.6) + bundler (>= 2.2.25) + netrc (>= 0.11.0) + parallel (>= 1.21.0) + rbi (~> 0.2) + sorbet-static-and-runtime (>= 0.5.11087) + spoom (>= 1.2.0) + thor (>= 1.2.0) + yard-sorbet + thor (1.4.0) + yard (0.9.42) + yard-sorbet (0.9.0) + sorbet-runtime + yard + +PLATFORMS + aarch64-linux + universal-darwin + x86_64-linux + +DEPENDENCIES + tapioca + +BUNDLED WITH + 4.1.0.dev diff --git a/spec/bundler/realworld/fixtures/warbler/.gitignore b/spec/bundler/realworld/fixtures/warbler/.gitignore new file mode 100644 index 0000000000..d392f0e82c --- /dev/null +++ b/spec/bundler/realworld/fixtures/warbler/.gitignore @@ -0,0 +1 @@ +*.jar diff --git a/spec/bundler/realworld/fixtures/warbler/Gemfile b/spec/bundler/realworld/fixtures/warbler/Gemfile new file mode 100644 index 0000000000..5687bbd975 --- /dev/null +++ b/spec/bundler/realworld/fixtures/warbler/Gemfile @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem "demo", path: "./demo" +gem "jruby-jars", "~> 10.0" +gem "warbler", "~> 2.1" diff --git a/spec/bundler/realworld/fixtures/warbler/Gemfile.lock b/spec/bundler/realworld/fixtures/warbler/Gemfile.lock new file mode 100644 index 0000000000..05f3bc4e3f --- /dev/null +++ b/spec/bundler/realworld/fixtures/warbler/Gemfile.lock @@ -0,0 +1,35 @@ +PATH + remote: demo + specs: + demo (1.0) + +GEM + remote: https://rubygems.org/ + specs: + jruby-jars (10.0.0.1) + jruby-rack (1.2.7) + ostruct (0.6.3) + rake (13.3.0) + rexml (3.4.2) + rubyzip (3.3.0) + warbler (2.1.0) + jruby-jars (>= 9.4, < 10.1) + jruby-rack (>= 1.2.3, < 1.3) + ostruct (~> 0.6.2) + rake (~> 13.0, >= 13.0.3) + rexml (~> 3.0) + rubyzip (>= 3.0.0) + +PLATFORMS + arm64-darwin + java + ruby + universal-java + +DEPENDENCIES + demo! + jruby-jars (~> 10.0) + warbler (~> 2.1) + +BUNDLED WITH + 4.1.0.dev diff --git a/spec/bundler/realworld/fixtures/warbler/bin/warbler-example.rb b/spec/bundler/realworld/fixtures/warbler/bin/warbler-example.rb new file mode 100644 index 0000000000..25f614ecc2 --- /dev/null +++ b/spec/bundler/realworld/fixtures/warbler/bin/warbler-example.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +puts require "bundler/setup" diff --git a/spec/bundler/realworld/fixtures/warbler/demo/demo.gemspec b/spec/bundler/realworld/fixtures/warbler/demo/demo.gemspec new file mode 100644 index 0000000000..ed5a0dc080 --- /dev/null +++ b/spec/bundler/realworld/fixtures/warbler/demo/demo.gemspec @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +Gem::Specification.new do |spec| + spec.name = "demo" + spec.version = "1.0" + spec.author = "Somebody" + spec.summary = "A demo gem" + spec.license = "MIT" + spec.homepage = "https://example.org" +end diff --git a/spec/bundler/realworld/gemfile_source_header_spec.rb b/spec/bundler/realworld/gemfile_source_header_spec.rb deleted file mode 100644 index 3f507b056a..0000000000 --- a/spec/bundler/realworld/gemfile_source_header_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require_relative "../support/silent_logger" - -RSpec.describe "fetching dependencies with a mirrored source", :realworld => true do - let(:mirror) { "https://server.example.org" } - let(:original) { "http://127.0.0.1:#{@port}" } - - before do - setup_server - bundle "config set --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, :artifice => nil - - 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_relative "../support/artifice/endpoint_mirror_source" - - @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/git_spec.rb b/spec/bundler/realworld/git_spec.rb new file mode 100644 index 0000000000..9eff74f1c9 --- /dev/null +++ b/spec/bundler/realworld/git_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +RSpec.describe "github source", realworld: true do + it "properly fetches PRs" do + install_gemfile <<-G + source "https://rubygems.org" + + gem "reline", github: "https://github.com/ruby/reline/pull/488" + G + end +end diff --git a/spec/bundler/realworld/mirror_probe_spec.rb b/spec/bundler/realworld/mirror_probe_spec.rb deleted file mode 100644 index 735fb2b3dd..0000000000 --- a/spec/bundler/realworld/mirror_probe_spec.rb +++ /dev/null @@ -1,144 +0,0 @@ -# frozen_string_literal: true - -require_relative "../support/silent_logger" - -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, :artifice => nil - - 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, :artifice => nil - - 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, :artifice => nil - - expect(out).to include("Fetching source index from #{mirror}") - expect(err).to include("Retrying fetcher due to error (2/4): Bundler::HTTPError Could not fetch specs from #{mirror}") - expect(err).to include("Retrying fetcher due to error (3/4): Bundler::HTTPError Could not fetch specs from #{mirror}") - expect(err).to include("Retrying fetcher due to error (4/4): Bundler::HTTPError Could not fetch specs from #{mirror}") - expect(err).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, :artifice => nil - - expect(out).to include "Fetching source index from #{mirror}/" - expect(err).to include <<-EOS.strip -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}/ - EOS - 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, :artifice => nil - - expect(out).to include("Fetching source index from #{mirror}") - expect(err).to include("Retrying fetcher due to error (2/4): Bundler::HTTPError Could not fetch specs from #{mirror}") - expect(err).to include("Retrying fetcher due to error (3/4): Bundler::HTTPError Could not fetch specs from #{mirror}") - expect(err).to include("Retrying fetcher due to error (4/4): Bundler::HTTPError Could not fetch specs from #{mirror}") - expect(err).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_relative "../support/artifice/endpoint" - - @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 index 7738b46aac..b57fdfd0ee 100644 --- a/spec/bundler/realworld/parallel_spec.rb +++ b/spec/bundler/realworld/parallel_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe "parallel", :realworld => true, :sometimes => true do +RSpec.describe "parallel", realworld: true do it "installs" do gemfile <<-G source "https://rubygems.org" @@ -9,7 +9,7 @@ RSpec.describe "parallel", :realworld => true, :sometimes => true do gem 'i18n', '~> 0.6.0' # Because 0.7+ requires Ruby 1.9.3+ G - bundle :install, :jobs => 4, :env => { "DEBUG" => "1" } + bundle :install, jobs: 4, env: { "DEBUG" => "1" } expect(out).to match(/[1-3]: /) @@ -34,7 +34,7 @@ RSpec.describe "parallel", :realworld => true, :sometimes => true do gem 'i18n', '~> 0.6.0' # Because 0.7+ requires Ruby 1.9.3+ G - bundle :update, :jobs => 4, :env => { "DEBUG" => "1" }, :all => true + bundle :update, jobs: 4, env: { "DEBUG" => "1" }, all: true expect(out).to match(/[1-3]: /) @@ -46,14 +46,14 @@ RSpec.describe "parallel", :realworld => true, :sometimes => true do end it "works with --standalone" do - gemfile <<-G, :standalone => true + gemfile <<-G source "https://rubygems.org" gem "diff-lcs" G - bundle :install, :standalone => true, :jobs => 4 + bundle :install, standalone: true, jobs: 4 - ruby <<-RUBY, :no_lib => true + ruby <<-RUBY $:.unshift File.expand_path("bundle") require "bundler/setup" diff --git a/spec/bundler/realworld/slow_perf_spec.rb b/spec/bundler/realworld/slow_perf_spec.rb new file mode 100644 index 0000000000..5d36ba7455 --- /dev/null +++ b/spec/bundler/realworld/slow_perf_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "bundle install with complex dependencies", realworld: true do + it "resolves quickly" do + gemfile <<-G + source 'https://rubygems.org' + + gem "actionmailer" + gem "mongoid", ">= 0.10.2" + G + + bundle "lock", env: { "DEBUG_RESOLVER" => "1" } + expect(out).to include("Solution found after 1 attempts") + end + + it "resolves quickly (case 2)" do + gemfile <<-G + source "https://rubygems.org" + + gem 'metasploit-erd' + gem 'rails-erd' + gem 'yard' + + gem 'coveralls' + gem 'rails' + gem 'simplecov' + gem 'rspec-rails' + G + + bundle "lock", env: { "DEBUG_RESOLVER" => "1" } + expect(out).to include("Solution found after 2 attempts") + end +end diff --git a/spec/bundler/resolver/basic_spec.rb b/spec/bundler/resolver/basic_spec.rb index 57897f89b4..185df1b1c7 100644 --- a/spec/bundler/resolver/basic_spec.rb +++ b/spec/bundler/resolver/basic_spec.rb @@ -6,15 +6,15 @@ RSpec.describe "Resolving" do end it "resolves a single gem" do - dep "rack" + dep "myrack" - should_resolve_as %w[rack-1.1] + should_resolve_as %w[myrack-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] + should_resolve_as %w[actionpack-2.3.5 activesupport-2.3.5 myrack-1.0] end it "resolves a conflicting index" do @@ -84,7 +84,7 @@ RSpec.describe "Resolving" do dep "activesupport", "= 3.0.0.beta" dep "actionpack" - should_resolve_as %w[activesupport-3.0.0.beta actionpack-3.0.0.beta rack-1.1 rack-mount-0.6] + should_resolve_as %w[activesupport-3.0.0.beta actionpack-3.0.0.beta myrack-1.1 myrack-mount-0.6] end it "prefers non-pre-releases when doing conservative updates" do @@ -100,11 +100,20 @@ RSpec.describe "Resolving" do end it "raises an exception if a child dependency is not resolved" do - @index = a_unresovable_child_index + @index = a_unresolvable_child_index dep "chef_app_error" expect do resolve - end.to raise_error(Bundler::VersionConflict) + end.to raise_error(Bundler::SolveFailure) + end + + it "does not try to re-resolve including prereleases if gems involved don't have prereleases" do + @index = a_unresolvable_child_index + dep "chef_app_error" + expect(Bundler.ui).not_to receive(:debug).with("Retrying resolution...", any_args) + expect do + resolve + end.to raise_error(Bundler::SolveFailure) end it "raises an exception with the minimal set of conflicting dependencies" do @@ -118,14 +127,15 @@ RSpec.describe "Resolving" do 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) + end.to raise_error(Bundler::SolveFailure, <<~E.strip) + Could not find compatible versions + + Because every version of c depends on a < 1 + and every version of b depends on a >= 2, + every version of c is incompatible with b >= 0. + So, because Gemfile depends on b >= 0 + and Gemfile depends on c >= 0, + version solving has failed. E end @@ -134,7 +144,7 @@ Bundler could not find compatible versions for gem "a": dep "circular_app" expect do - resolve + Bundler::SpecSet.new(resolve).sort end.to raise_error(Bundler::CyclicDependencyError, /please remove either gem 'bar' or gem 'foo'/i) end @@ -174,12 +184,7 @@ Bundler could not find compatible versions for gem "a": 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], [[]] + should_resolve_and_include %w[foo-1.0.0 bar-1.0.0] end context "conservative" do @@ -206,12 +211,12 @@ Bundler could not find compatible versions for gem "a": 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] + should_conservative_resolve_and_include :patch, true, %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] + should_conservative_resolve_and_include [:patch, :strict], true, %w[foo-1.4.4 bar-2.0.5] end it "resolves foo only to latest patch - same dependency case" do @@ -233,7 +238,7 @@ Bundler could not find compatible versions for gem "a": 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 + @base = Bundler::SpecSet.new([Bundler::LazySpecification.new("bar", Gem::Version.new("2.0.3"), nil)]) should_conservative_resolve_and_include :patch, ["foo"], %w[foo-1.4.4 bar-2.0.3] end @@ -251,20 +256,20 @@ Bundler could not find compatible versions for gem "a": 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] + should_conservative_resolve_and_include :minor, true, %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] + should_conservative_resolve_and_include [:minor, :strict], true, %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] + should_conservative_resolve_and_include :major, true, %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] + should_conservative_resolve_and_include [:major, :strict], true, %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 @@ -287,22 +292,131 @@ Bundler could not find compatible versions for gem "a": 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] + should_conservative_resolve_and_include :patch, true, %w[foo-1.4.4 bar-2.1.1] end it "cannot revert to a previous version in strict mode level patch" do # fall back to the locked resolution since strict means we can't regress either version - should_conservative_resolve_and_include [:patch, :strict], [], %w[foo-1.4.3 bar-2.2.3] + should_conservative_resolve_and_include [:patch, :strict], true, %w[foo-1.4.3 bar-2.2.3] 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] + should_conservative_resolve_and_include :minor, true, %w[foo-1.5.0 bar-2.0.5] end it "cannot revert to a previous version in strict mode level minor" do # fall back to the locked resolution since strict means we can't regress either version - should_conservative_resolve_and_include [:minor, :strict], [], %w[foo-1.4.3 bar-2.2.3] + should_conservative_resolve_and_include [:minor, :strict], true, %w[foo-1.4.3 bar-2.2.3] + end + end + end + + it "handles versions that redundantly depend on themselves" do + @index = build_index do + gem "myrack", "3.0.0" + + gem "standalone_migrations", "7.1.0" do + dep "myrack", "~> 2.0" + end + + gem "standalone_migrations", "2.0.4" do + dep "standalone_migrations", ">= 0" + end + + gem "standalone_migrations", "1.0.13" do + dep "myrack", ">= 0" + end + end + + dep "myrack", "~> 3.0" + dep "standalone_migrations" + + should_resolve_as %w[myrack-3.0.0 standalone_migrations-2.0.4] + end + + it "ignores versions that incorrectly depend on themselves" do + @index = build_index do + gem "myrack", "3.0.0" + + gem "standalone_migrations", "7.1.0" do + dep "myrack", "~> 2.0" + end + + gem "standalone_migrations", "2.0.4" do + dep "standalone_migrations", ">= 2.0.5" + end + + gem "standalone_migrations", "1.0.13" do + dep "myrack", ">= 0" + end + end + + dep "myrack", "~> 3.0" + dep "standalone_migrations" + + should_resolve_as %w[myrack-3.0.0 standalone_migrations-1.0.13] + end + + it "does not ignore versions that incorrectly depend on themselves when dependency_api is not available" do + @index = build_index do + gem "myrack", "3.0.0" + + gem "standalone_migrations", "7.1.0" do + dep "myrack", "~> 2.0" + end + + gem "standalone_migrations", "2.0.4" do + dep "standalone_migrations", ">= 2.0.5" + end + + gem "standalone_migrations", "1.0.13" do + dep "myrack", ">= 0" + end + end + + dep "myrack", "~> 3.0" + dep "standalone_migrations" + + should_resolve_without_dependency_api %w[myrack-3.0.0 standalone_migrations-2.0.4] + end + + it "resolves fine cases that need joining unbounded disjoint ranges" do + @index = build_index do + gem "inspec", "5.22.3" do + dep "ruby", ">= 3.2.2" + dep "train-kubernetes", ">= 0.1.7" + end + + gem "ruby", "3.2.2" + + gem "train-kubernetes", "0.1.12" do + dep "k8s-ruby", ">= 0.14.0" + end + + gem "train-kubernetes", "0.1.10" do + dep "k8s-ruby", "= 0.10.5" + end + + gem "train-kubernetes", "0.1.7" do + dep "k8s-ruby", ">= 0.10.5" + end + + gem "k8s-ruby", "0.10.5" do + dep "ruby","< 3.2.2" + end + + gem "k8s-ruby", "0.11.0" do + dep "ruby", ">= 3.2.2" + end + + gem "k8s-ruby", "0.14.0" do + dep "ruby", "< 3.2.2" end end + + dep "inspec", "5.22.3" + dep "ruby", "3.2.2" + + should_resolve_as %w[inspec-5.22.3 ruby-3.2.2 train-kubernetes-0.1.7 k8s-ruby-0.11.0] end end diff --git a/spec/bundler/resolver/platform_spec.rb b/spec/bundler/resolver/platform_spec.rb index fee0cf1f1c..a1d095d024 100644 --- a/spec/bundler/resolver/platform_spec.rb +++ b/spec/bundler/resolver/platform_spec.rb @@ -28,10 +28,321 @@ RSpec.describe "Resolving platform craziness" do end end + it "resolves multiplatform gems with redundant platforms correctly" do + @index = build_index do + gem "zookeeper", "1.4.11" + gem "zookeeper", "1.4.11", "java" do + dep "slyphon-log4j", "= 1.2.15" + dep "slyphon-zookeeper_jar", "= 3.3.5" + end + gem "slyphon-log4j", "1.2.15" + gem "slyphon-zookeeper_jar", "3.3.5", "java" + end + + dep "zookeeper" + platforms "java", "ruby", "universal-java-11" + + should_resolve_as %w[zookeeper-1.4.11 zookeeper-1.4.11-java slyphon-log4j-1.2.15 slyphon-zookeeper_jar-3.3.5-java] + end + + it "takes the latest ruby gem, even if an older platform specific version is available" do + @index = build_index do + gem "foo", "1.0.0" + gem "foo", "1.0.0", "x64-mingw-ucrt" + gem "foo", "1.1.0" + end + dep "foo" + platforms "x64-mingw-ucrt" + + should_resolve_as %w[foo-1.1.0] + end + + it "takes the ruby version if the platform version is incompatible" do + @index = build_index do + gem "bar", "1.0.0" + gem "foo", "1.0.0" + gem "foo", "1.0.0", "x64-mingw-ucrt" do + dep "bar", "< 1" + end + end + dep "foo" + platforms "x64-mingw-ucrt" + + should_resolve_as %w[foo-1.0.0] + end + + it "prefers the platform specific gem to the ruby version" do + @index = build_index do + gem "foo", "1.0.0" + gem "foo", "1.0.0", "x64-mingw-ucrt" + end + dep "foo" + platforms "x64-mingw-ucrt" + + should_resolve_as %w[foo-1.0.0-x64-mingw-ucrt] + end + + describe "on a linux platform" do + # Ruby's platform is *-linux => platform's libc is glibc, so not musl + # Ruby's platform is *-linux-musl => platform's libc is musl, so not glibc + # Gem's platform is *-linux => gem is glibc + maybe musl compatible + # Gem's platform is *-linux-musl => gem is musl compatible but not glibc + + it "favors the platform version-specific gem on a version-specifying linux platform" do + @index = build_index do + gem "foo", "1.0.0" + gem "foo", "1.0.0", "x86_64-linux" + gem "foo", "1.0.0", "x86_64-linux-musl" + end + dep "foo" + platforms "x86_64-linux-musl" + + should_resolve_as %w[foo-1.0.0-x86_64-linux-musl] + end + + it "favors the version-less gem over the version-specific gem on a gnu linux platform" do + @index = build_index do + gem "foo", "1.0.0" + gem "foo", "1.0.0", "x86_64-linux" + gem "foo", "1.0.0", "x86_64-linux-musl" + end + dep "foo" + platforms "x86_64-linux" + + should_resolve_as %w[foo-1.0.0-x86_64-linux] + end + + it "ignores the platform version-specific gem on a gnu linux platform" do + @index = build_index do + gem "foo", "1.0.0", "x86_64-linux-musl" + end + dep "foo" + platforms "x86_64-linux" + + should_not_resolve + end + + it "falls back to the platform version-less gem on a linux platform with a version" do + @index = build_index do + gem "foo", "1.0.0" + gem "foo", "1.0.0", "x86_64-linux" + end + dep "foo" + platforms "x86_64-linux-musl" + + should_resolve_as %w[foo-1.0.0-x86_64-linux] + end + + it "falls back to the ruby platform gem on a gnu linux platform when only a version-specifying gem is available" do + @index = build_index do + gem "foo", "1.0.0" + gem "foo", "1.0.0", "x86_64-linux-musl" + end + dep "foo" + platforms "x86_64-linux" + + should_resolve_as %w[foo-1.0.0] + end + + it "falls back to the platform version-less gem on a version-specifying linux platform and no ruby platform gem is available" do + @index = build_index do + gem "foo", "1.0.0", "x86_64-linux" + end + dep "foo" + platforms "x86_64-linux-musl" + + should_resolve_as %w[foo-1.0.0-x86_64-linux] + end + end + + context "when the platform specific gem doesn't match the required_ruby_version" do + before do + @index = build_index do + gem "foo", "1.0.0" + gem "foo", "1.0.0", "x64-mingw-ucrt" + gem "foo", "1.1.0" + gem "foo", "1.1.0", "x64-mingw-ucrt" do |s| + s.required_ruby_version = [">= 2.0", "< 2.4"] + end + gem "Ruby\0", "2.5.1" + end + dep "Ruby\0", "2.5.1" + platforms "x64-mingw-ucrt" + end + + it "takes the latest ruby gem" do + dep "foo" + + should_resolve_as %w[foo-1.1.0] + end + + it "takes the latest ruby gem, even if requirement does not match previous versions with the same ruby requirement" do + dep "foo", "1.1.0" + + should_resolve_as %w[foo-1.1.0] + end + end + + it "takes the latest ruby gem with required_ruby_version if the platform specific gem doesn't match the required_ruby_version" do + @index = build_index do + gem "foo", "1.0.0" + gem "foo", "1.0.0", "x64-mingw-ucrt" + gem "foo", "1.1.0" do |s| + s.required_ruby_version = [">= 2.0"] + end + gem "foo", "1.1.0", "x64-mingw-ucrt" do |s| + s.required_ruby_version = [">= 2.0", "< 2.4"] + end + gem "Ruby\0", "2.5.1" + end + dep "foo" + dep "Ruby\0", "2.5.1" + platforms "x64-mingw-ucrt" + + should_resolve_as %w[foo-1.1.0] + end + + it "takes the latest ruby gem if the platform specific gem doesn't match the required_ruby_version with multiple platforms" do + @index = build_index do + gem "foo", "1.0.0" + gem "foo", "1.0.0", "x64-mingw-ucrt" + gem "foo", "1.1.0" do |s| + s.required_ruby_version = [">= 2.0"] + end + gem "foo", "1.1.0", "x64-mingw-ucrt" do |s| + s.required_ruby_version = [">= 2.0", "< 2.4"] + end + gem "Ruby\0", "2.5.1" + end + dep "foo" + dep "Ruby\0", "2.5.1" + platforms "x86_64-linux", "x64-mingw-ucrt" + + should_resolve_as %w[foo-1.1.0] + end + + it "includes gems needed for at least one platform" do + @index = build_index do + gem "empyrean", "0.1.0" + gem "coderay", "1.1.2" + gem "method_source", "0.9.0" + + gem "spoon", "0.0.6" do + dep "ffi", ">= 0" + end + + gem "pry", "0.11.3", "java" do + dep "coderay", "~> 1.1.0" + dep "method_source", "~> 0.9.0" + dep "spoon", "~> 0.0" + end + + gem "pry", "0.11.3" do + dep "coderay", "~> 1.1.0" + dep "method_source", "~> 0.9.0" + end + + gem "ffi", "1.9.23", "java" + gem "ffi", "1.9.23" + + gem "extra", "1.0.0" do + dep "ffi", ">= 0" + end + end + + dep "empyrean", "0.1.0" + dep "pry" + dep "extra" + + platforms "ruby", "java" + + should_resolve_as %w[coderay-1.1.2 empyrean-0.1.0 extra-1.0.0 ffi-1.9.23 ffi-1.9.23-java method_source-0.9.0 pry-0.11.3 pry-0.11.3-java spoon-0.0.6] + end + + it "includes gems needed for at least one platform even when the platform specific requirement is processed earlier than the generic requirement" do + @index = build_index do + gem "empyrean", "0.1.0" + gem "coderay", "1.1.2" + gem "method_source", "0.9.0" + + gem "spoon", "0.0.6" do + dep "ffi", ">= 0" + end + + gem "pry", "0.11.3", "java" do + dep "coderay", "~> 1.1.0" + dep "method_source", "~> 0.9.0" + dep "spoon", "~> 0.0" + end + + gem "pry", "0.11.3" do + dep "coderay", "~> 1.1.0" + dep "method_source", "~> 0.9.0" + end + + gem "ffi", "1.9.23", "java" + gem "ffi", "1.9.23" + + gem "extra", "1.0.0" do + dep "extra2", ">= 0" + end + + gem "extra2", "1.0.0" do + dep "extra3", ">= 0" + end + + gem "extra3", "1.0.0" do + dep "ffi", ">= 0" + end + end + + dep "empyrean", "0.1.0" + dep "pry" + dep "extra" + + platforms "ruby", "java" + + should_resolve_as %w[coderay-1.1.2 empyrean-0.1.0 extra-1.0.0 extra2-1.0.0 extra3-1.0.0 ffi-1.9.23 ffi-1.9.23-java method_source-0.9.0 pry-0.11.3 pry-0.11.3-java spoon-0.0.6] + end + + it "properly adds platforms when platform requirements come from different dependencies" do + @index = build_index do + gem "ffi", "1.9.14" + gem "ffi", "1.9.14", "universal-mingw32" + + gem "gssapi", "0.1" + gem "gssapi", "0.2" + gem "gssapi", "0.3" + gem "gssapi", "1.2.0" do + dep "ffi", ">= 1.0.1" + end + + gem "mixlib-shellout", "2.2.6" + gem "mixlib-shellout", "2.2.6", "universal-mingw32" do + dep "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| + gem "win32-process", v do + dep "ffi", ">= 1.0.0" + end + end + end + + dep "mixlib-shellout" + dep "gssapi" + + platforms "universal-mingw32", "ruby" + + should_resolve_as %w[ffi-1.9.14 ffi-1.9.14-universal-mingw32 gssapi-1.2.0 mixlib-shellout-2.2.6 mixlib-shellout-2.2.6-universal-mingw32 win32-process-0.8.3] + end + describe "with mingw32" do before :each do @index = build_index do - platforms "mingw32 mswin32 x64-mingw32" do |platform| + platforms "mingw32 mswin32 x64-mingw-ucrt" do |platform| gem "thin", "1.2.7", platform end gem "win32-api", "1.5.1", "universal-mingw32" @@ -52,10 +363,10 @@ RSpec.describe "Resolving platform craziness" do should_resolve_as %w[thin-1.2.7-mingw32] end - it "finds x64-mingw gems" do - platforms "x64-mingw32" + it "finds x64-mingw-ucrt gems" do + platforms "x64-mingw-ucrt" dep "thin" - should_resolve_as %w[thin-1.2.7-x64-mingw32] + should_resolve_as %w[thin-1.2.7-x64-mingw-ucrt] end it "finds universal-mingw gems on x86-mingw" do @@ -65,7 +376,19 @@ RSpec.describe "Resolving platform craziness" do end it "finds universal-mingw gems on x64-mingw" do - platform "x64-mingw32" + platform "x64-mingw-ucrt" + dep "win32-api" + should_resolve_as %w[win32-api-1.5.1-universal-mingw32] + end + + it "finds x64-mingw-ucrt gems" do + platforms "x64-mingw-ucrt" + dep "thin" + should_resolve_as %w[thin-1.2.7-x64-mingw-ucrt] + end + + it "finds universal-mingw gems on x64-mingw-ucrt" do + platform "x64-mingw-ucrt" dep "win32-api" should_resolve_as %w[win32-api-1.5.1-universal-mingw32] end @@ -90,11 +413,11 @@ RSpec.describe "Resolving platform craziness" do end end - it "reports on the conflict" do + it "takes the ruby version as fallback" do platforms "ruby", "java" dep "foo" - should_conflict_on "baz" + should_resolve_as %w[bar-1.0.0 baz-1.0.0 foo-1.0.0] end end end diff --git a/spec/bundler/rubygems/rubygems.rb b/spec/bundler/rubygems/rubygems.rb deleted file mode 100644 index 6fa63013bf..0000000000 --- a/spec/bundler/rubygems/rubygems.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require_relative "../support/rubygems_version_manager" - -RubygemsVersionManager.new(ENV["RGV"]).switch - -$:.delete("#{Spec::Path.spec_dir}/rubygems") - -require "rubygems" diff --git a/spec/bundler/runtime/env_helpers_spec.rb b/spec/bundler/runtime/env_helpers_spec.rb new file mode 100644 index 0000000000..c4ebdd1fd2 --- /dev/null +++ b/spec/bundler/runtime/env_helpers_spec.rb @@ -0,0 +1,236 @@ +# frozen_string_literal: true + +RSpec.describe "env helpers" do + def bundle_exec_ruby(args, options = {}) + build_bundler_context options.dup + bundle "exec '#{Gem.ruby}' #{args}", options + end + + def build_bundler_context(options = {}) + bundle "config set path vendor/bundle", options.dup + gemfile "source 'https://gem.repo1'" + bundle "install", options + end + + def run_bundler_script(env, script) + system(env, "ruby", "-I#{lib_dir}", "-rbundler", script.to_s) + end + + describe "Bundler.original_env" do + it "should return the PATH present before bundle was activated" do + create_file("source.rb", <<-RUBY) + print Bundler.original_env["PATH"] + RUBY + path = `getconf PATH`.strip + "#{File::PATH_SEPARATOR}/foo" + with_path_as(path) do + bundle_exec_ruby(bundled_app("source.rb").to_s) + expect(stdboth).to eq(path) + end + end + + it "should return the GEM_PATH present before bundle was activated" do + create_file("source.rb", <<-RUBY) + print Bundler.original_env['GEM_PATH'] + RUBY + gem_path = ENV["GEM_PATH"] + "#{File::PATH_SEPARATOR}/foo" + with_gem_path_as(gem_path) do + bundle_exec_ruby(bundled_app("source.rb").to_s) + expect(stdboth).to eq(gem_path) + end + end + + it "works with nested bundle exec invocations", :ruby_repo do + create_file("exe.rb", <<-'RUBY') + count = ARGV.first.to_i + exit if count < 0 + STDERR.puts "#{count} #{ENV["PATH"].end_with?("#{File::PATH_SEPARATOR}/foo")}" + if count == 2 + ENV["PATH"] = "#{ENV["PATH"]}#{File::PATH_SEPARATOR}/foo" + end + exec(Gem.ruby, __FILE__, (count - 1).to_s) + RUBY + path = `getconf PATH`.strip + File::PATH_SEPARATOR + File.dirname(Gem.ruby) + with_path_as(path) do + build_bundler_context + bundle_exec_ruby("#{bundled_app("exe.rb")} 2") + end + expect(err).to eq <<-EOS.strip +2 false +1 true +0 true + EOS + end + + it "removes variables that bundler added", :ruby_repo do + original = ruby('puts ENV.to_a.map {|e| e.join("=") }.sort.join("\n")', artifice: "fail") + create_file("source.rb", <<-RUBY) + puts Bundler.original_env.to_a.map {|e| e.join("=") }.sort.join("\n") + RUBY + bundle_exec_ruby bundled_app("source.rb"), artifice: "fail" + expect(out).to eq original + end + end + + describe "Bundler.unbundled_env" do + it "should delete BUNDLE_PATH" do + create_file("source.rb", <<-RUBY) + print Bundler.unbundled_env.has_key?('BUNDLE_PATH') + RUBY + ENV["BUNDLE_PATH"] = "./foo" + bundle_exec_ruby bundled_app("source.rb") + expect(stdboth).to include "false" + end + + it "should remove absolute path to 'bundler/setup' from RUBYOPT even if it was present in original env" do + create_file("source.rb", <<-RUBY) + print Bundler.unbundled_env['RUBYOPT'] + RUBY + setup_require = "-r#{lib_dir}/bundler/setup" + ENV["BUNDLER_ORIG_RUBYOPT"] = "-W2 #{setup_require} #{ENV["RUBYOPT"]}" + bundle_exec_ruby bundled_app("source.rb") + expect(stdboth).not_to include(setup_require) + end + + it "should remove relative path to 'bundler/setup' from RUBYOPT even if it was present in original env" do + create_file("source.rb", <<-RUBY) + print Bundler.unbundled_env['RUBYOPT'] + RUBY + ENV["BUNDLER_ORIG_RUBYOPT"] = "-W2 -rbundler/setup #{ENV["RUBYOPT"]}" + bundle_exec_ruby bundled_app("source.rb") + expect(stdboth).not_to include("-rbundler/setup") + end + + it "should delete BUNDLER_SETUP even if it was present in original env" do + create_file("source.rb", <<-RUBY) + print Bundler.unbundled_env.has_key?('BUNDLER_SETUP') + RUBY + ENV["BUNDLER_ORIG_BUNDLER_SETUP"] = system_gem_path("gems/bundler-#{Bundler::VERSION}/lib/bundler/setup").to_s + bundle_exec_ruby bundled_app("source.rb") + expect(stdboth).to include "false" + end + + it "should restore RUBYLIB", :ruby_repo do + create_file("source.rb", <<-RUBY) + print Bundler.unbundled_env['RUBYLIB'] + RUBY + ENV["RUBYLIB"] = lib_dir.to_s + File::PATH_SEPARATOR + "/foo" + ENV["BUNDLER_ORIG_RUBYLIB"] = lib_dir.to_s + File::PATH_SEPARATOR + "/foo-original" + bundle_exec_ruby bundled_app("source.rb") + expect(stdboth).to include("/foo-original") + end + + it "should restore the original MANPATH" do + create_file("source.rb", <<-RUBY) + print Bundler.unbundled_env['MANPATH'] + RUBY + ENV["MANPATH"] = "/foo" + ENV["BUNDLER_ORIG_MANPATH"] = "/foo-original" + bundle_exec_ruby bundled_app("source.rb") + expect(stdboth).to include("/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_unbundled_env" do + it "should set ENV to unbundled_env in the block" do + expected = Bundler.unbundled_env + actual = Bundler.with_unbundled_env { ENV.to_hash } + expect(actual).to eq(expected) + end + + it "should restore the environment after execution" do + Bundler.with_unbundled_env do + ENV["FOO"] = "hello" + end + + expect(ENV).not_to have_key("FOO") + end + end + + describe "Bundler.original_system" do + before do + create_file("source.rb", <<-'RUBY') + Bundler.original_system("ruby", "-e", "exit(42) if ENV['BUNDLE_FOO'] == 'bar'") + + exit $?.exitstatus + RUBY + end + + it "runs system inside with_original_env" do + run_bundler_script({ "BUNDLE_FOO" => "bar" }, bundled_app("source.rb")) + expect($?.exitstatus).to eq(42) + end + end + + describe "Bundler.unbundled_system" do + before do + create_file("source.rb", <<-'RUBY') + Bundler.unbundled_system("ruby", "-e", "exit(42) unless ENV['BUNDLE_FOO'] == 'bar'") + + exit $?.exitstatus + RUBY + end + + it "runs system inside with_unbundled_env" do + run_bundler_script({ "BUNDLE_FOO" => "bar" }, bundled_app("source.rb")) + expect($?.exitstatus).to eq(42) + end + end + + describe "Bundler.original_exec" do + before do + create_file("source.rb", <<-'RUBY') + Process.fork do + exit Bundler.original_exec(%(test "\$BUNDLE_FOO" = "bar")) + end + + _, status = Process.wait2 + + exit(status.exitstatus) + RUBY + end + + it "runs exec inside with_original_env" do + skip "Fork not implemented" if Gem.win_platform? + + run_bundler_script({ "BUNDLE_FOO" => "bar" }, bundled_app("source.rb")) + expect($?.exitstatus).to eq(0) + end + end + + describe "Bundler.unbundled_exec" do + before do + create_file("source.rb", <<-'RUBY') + Process.fork do + exit Bundler.unbundled_exec(%(test "\$BUNDLE_FOO" = "bar")) + end + + _, status = Process.wait2 + + exit(status.exitstatus) + RUBY + end + + it "runs exec inside with_unbundled_env" do + skip "Fork not implemented" if Gem.win_platform? + + run_bundler_script({ "BUNDLE_FOO" => "bar" }, bundled_app("source.rb")) + expect($?.exitstatus).to eq(1) + end + end +end diff --git a/spec/bundler/runtime/executable_spec.rb b/spec/bundler/runtime/executable_spec.rb index 003be97cd6..89cee21b00 100644 --- a/spec/bundler/runtime/executable_spec.rb +++ b/spec/bundler/runtime/executable_spec.rb @@ -2,164 +2,133 @@ RSpec.describe "Running bin/* commands" do before :each do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" G end it "runs the bundled command when in the bundle" do - bundle! "binstubs rack" + bundle "binstubs myrack" - build_gem "rack", "2.0", :to_system => true do |s| - s.executables = "rackup" + build_gem "myrack", "2.0", to_system: true do |s| + s.executables = "myrackup" end - gembin "rackup" + gembin "myrackup" expect(out).to eq("1.0.0") end - it "allows the location of the gem stubs to be specified" do - bundle! "binstubs rack", :path => "gbin" + it "allows the location of the gem stubs to be configured" do + bundle_config "bin gbin" + bundle "binstubs myrack" expect(bundled_app("bin")).not_to exist - expect(bundled_app("gbin/rackup")).to exist + expect(bundled_app("gbin/myrackup")).to exist - gembin bundled_app("gbin/rackup") + gembin bundled_app("gbin/myrackup") expect(out).to eq("1.0.0") end it "allows absolute paths as a specification of where to install bin stubs" do - bundle! "binstubs rack", :path => tmp("bin") + bundle_config "bin #{tmp("bin")}" + bundle "binstubs myrack" - gembin tmp("bin/rackup") + gembin tmp("bin/myrackup") expect(out).to eq("1.0.0") end it "uses the default ruby install name when shebang is not specified" do - bundle! "binstubs rack" - expect(File.open("bin/rackup").gets).to eq("#!/usr/bin/env #{RbConfig::CONFIG["ruby_install_name"]}\n") + bundle "binstubs myrack" + expect(File.readlines(bundled_app("bin/myrackup")).first).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! "binstubs rack", :shebang => "ruby-foo" - expect(File.open("bin/rackup").gets).to eq("#!/usr/bin/env ruby-foo\n") + bundle "binstubs myrack", shebang: "ruby-foo" + expect(File.readlines(bundled_app("bin/myrackup")).first).to eq("#!/usr/bin/env ruby-foo\n") end it "runs the bundled command when out of the bundle" do - bundle! "binstubs rack" + bundle "binstubs myrack" - build_gem "rack", "2.0", :to_system => true do |s| - s.executables = "rackup" + build_gem "myrack", "2.0", to_system: true do |s| + s.executables = "myrackup" end - Dir.chdir(tmp) do - gembin "rackup" - expect(out).to eq("1.0.0") - end + gembin "myrackup", dir: tmp + expect(out).to eq("1.0.0") end it "works with gems in path" do - build_lib "rack", :path => lib_path("rack") do |s| - s.executables = "rackup" + build_lib "myrack", path: lib_path("myrack") do |s| + s.executables = "myrackup" end gemfile <<-G - gem "rack", :path => "#{lib_path("rack")}" + source "https://gem.repo1" + gem "myrack", :path => "#{lib_path("myrack")}" G - bundle! "binstubs rack" + bundle "binstubs myrack" - build_gem "rack", "2.0", :to_system => true do |s| - s.executables = "rackup" + build_gem "myrack", "2.0", to_system: true do |s| + s.executables = "myrackup" end - gembin "rackup" + gembin "myrackup" expect(out).to eq("1.0") end - it "creates a bundle binstub" do - build_gem "bundler", Bundler::VERSION, :to_system => true do |s| - s.executables = "bundle" - end - + it "does not create a bundle binstub" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "bundler" G - bundle! "binstubs bundler" - - expect(bundled_app("bin/bundle")).to exist - end + bundle "binstubs bundler" - it "does not generate bin stubs if the option was not specified" do - bundle! "install" + expect(bundled_app("bin/bundle")).not_to exist - expect(bundled_app("bin/rackup")).not_to exist + expect(err).to include("Bundler itself does not use binstubs because its version is selected by RubyGems") end - it "allows you to stop installing binstubs", :bundler => "< 3" 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", :bundler => "< 3" do - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "activesupport" - G - - bundle! :install, :binstubs => "bin" - - gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "activesupport" - gem "rack" - G - + it "does not generate bin stubs if the option was not specified" do bundle "install" - expect(bundled_app("bin/rackup")).to exist + expect(bundled_app("bin/myrackup")).not_to exist end - it "rewrites bins on binstubs (to maintain backwards compatibility)" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + it "rewrites bins on binstubs with --force option" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" G - create_file("bin/rackup", "OMG") + create_file("bin/myrackup", "OMG") - bundle! "binstubs rack" + bundle "binstubs myrack", { force: true } - expect(bundled_app("bin/rackup").read).to_not eq("OMG") + expect(bundled_app("bin/myrackup").read.strip).to_not eq("OMG") end it "use BUNDLE_GEMFILE gemfile for binstub" do # context with bin/bundler w/ default Gemfile - bundle! "binstubs bundler" + bundle "binstubs bundler" # generate other Gemfile with executable gem build_repo2 do build_gem("bindir") {|s| s.executables = "foo" } end - create_file("OtherGemfile", <<-G) - source "#{file_uri_for(gem_repo2)}" + gemfile("OtherGemfile", <<-G) + source "https://gem.repo2" gem 'bindir' G # generate binstub for executable from non default Gemfile (other then bin/bundler version) ENV["BUNDLE_GEMFILE"] = "OtherGemfile" bundle "install" - bundle! "binstubs bindir" + bundle "binstubs bindir" # remove user settings ENV["BUNDLE_GEMFILE"] = nil @@ -167,7 +136,6 @@ RSpec.describe "Running bin/* commands" do # run binstub for non default Gemfile gembin "foo" - expect(exitstatus).to eq(0) if exitstatus expect(out).to eq("1.0") end end diff --git a/spec/bundler/runtime/gem_tasks_spec.rb b/spec/bundler/runtime/gem_tasks_spec.rb index 4760b6a749..b855142e60 100644 --- a/spec/bundler/runtime/gem_tasks_spec.rb +++ b/spec/bundler/runtime/gem_tasks_spec.rb @@ -1,28 +1,74 @@ # frozen_string_literal: true RSpec.describe "require 'bundler/gem_tasks'" do - before :each do + let(:define_local_gem_using_gem_tasks) do bundled_app("foo.gemspec").open("w") do |f| f.write <<-GEMSPEC Gem::Specification.new do |s| s.name = "foo" + s.version = "1.0" + s.summary = "dummy" + s.author = "Perry Mason" end GEMSPEC end + bundled_app("Rakefile").open("w") do |f| f.write <<-RAKEFILE - $:.unshift("#{lib_dir}") require "bundler/gem_tasks" RAKEFILE end + + install_gemfile <<-G + source "https://gem.repo1" + + gem "rake" + G end - it "includes the relevant tasks" do - with_gem_path_as(Spec::Path.base_system_gems.to_s) do - sys_exec "#{rake} -T", "RUBYOPT" => "-I#{lib_dir}" + let(:define_local_gem_with_extensions_using_gem_tasks_and_gemspec_dsl) do + bundled_app("foo.gemspec").open("w") do |f| + f.write <<-GEMSPEC + Gem::Specification.new do |s| + s.name = "foo" + s.version = "1.0" + s.summary = "dummy" + s.author = "Perry Mason" + s.extensions = "ext/extconf.rb" + end + GEMSPEC + end + + bundled_app("Rakefile").open("w") do |f| + f.write <<-RAKEFILE + require "bundler/gem_tasks" + RAKEFILE + end + + Dir.mkdir bundled_app("ext") + + bundled_app("ext/extconf.rb").open("w") do |f| + f.write <<-EXTCONF + require "mkmf" + File.write("Makefile", dummy_makefile($srcdir).join) + EXTCONF end - expect(err).to eq("") + install_gemfile <<-G + source "https://gem.repo1" + + gemspec + + gem "rake" + G + end + + it "includes the relevant tasks" do + define_local_gem_using_gem_tasks + + in_bundled_app "rake -T" + + expect(err).to be_empty expected_tasks = [ "rake build", "rake clean", @@ -32,13 +78,81 @@ RSpec.describe "require 'bundler/gem_tasks'" do ] 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 - with_gem_path_as(Spec::Path.base_system_gems.to_s) do - sys_exec! %(#{rake} -e 'load "Rakefile"; puts CLOBBER.inspect') + it "defines a working `rake install` task", :ruby_repo do + define_local_gem_using_gem_tasks + + in_bundled_app "rake install" + + expect(err).to be_empty + + bundle "exec rake install" + + expect(err).to be_empty + end + + it "defines a working `rake install` task for local gems with extensions", :ruby_repo do + define_local_gem_with_extensions_using_gem_tasks_and_gemspec_dsl + + bundle "exec rake install" + + expect(err).to be_empty + end + + context "rake build when path has spaces", :ruby_repo do + before do + define_local_gem_using_gem_tasks + + spaced_bundled_app = tmp("bundled app") + FileUtils.cp_r bundled_app, spaced_bundled_app + bundle "exec rake build", dir: spaced_bundled_app + end + + it "still runs successfully" do + expect(err).to be_empty + end + end + + context "rake build when path has brackets", :ruby_repo do + before do + define_local_gem_using_gem_tasks + + bracketed_bundled_app = tmp("bundled[app") + FileUtils.cp_r bundled_app, bracketed_bundled_app + bundle "exec rake build", dir: bracketed_bundled_app + end + + it "still runs successfully" do + expect(err).to be_empty + end + end + + context "bundle path configured locally" do + before do + define_local_gem_using_gem_tasks + + bundle_config "path vendor/bundle" end + + it "works", :ruby_repo do + install_gemfile <<-G + source "https://gem.repo1" + + gem "rake" + G + + bundle "exec rake -T" + + expect(err).to be_empty + end + end + + it "adds 'pkg' to rake/clean's CLOBBER" do + define_local_gem_using_gem_tasks + + in_bundled_app %(rake -e 'load "Rakefile"; puts CLOBBER.inspect') + expect(out).to eq '["pkg"]' end end diff --git a/spec/bundler/runtime/inline_spec.rb b/spec/bundler/runtime/inline_spec.rb index 06be2ef83d..c6f9bbdbd7 100644 --- a/spec/bundler/runtime/inline_spec.rb +++ b/spec/bundler/runtime/inline_spec.rb @@ -2,10 +2,9 @@ RSpec.describe "bundler/inline#gemfile" do def script(code, options = {}) - requires = ["#{lib_dir}/bundler/inline"] - requires.unshift "#{spec_dir}/support/artifice/" + options.delete(:artifice) if options.key?(:artifice) - requires = requires.map {|r| "require '#{r}'" }.join("\n") - @out = ruby("#{requires}\n\n" + code, options) + options[:artifice] ||= "compact_index" + options[:env] ||= { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s } + ruby("require 'bundler/inline'\n\n" + code, options) end before :each do @@ -28,7 +27,7 @@ RSpec.describe "bundler/inline#gemfile" do s.write "lib/four.rb", "puts 'four'" end - build_lib "five", "1.0.0", :no_default => true do |s| + build_lib "five", "1.0.0", no_default: true do |s| s.write "lib/mofive.rb", "puts 'five'" end @@ -48,6 +47,7 @@ RSpec.describe "bundler/inline#gemfile" do it "requires the gems" do script <<-RUBY gemfile do + source "https://gem.repo1" path "#{lib_path}" do gem "two" end @@ -55,10 +55,10 @@ RSpec.describe "bundler/inline#gemfile" do RUBY expect(out).to eq("two") - expect(exitstatus).to be_zero if exitstatus - script <<-RUBY + script <<-RUBY, raise_on_error: false gemfile do + source "https://gem.repo1" path "#{lib_path}" do gem "eleven" end @@ -72,53 +72,51 @@ RSpec.describe "bundler/inline#gemfile" do script <<-RUBY gemfile(true) do - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" end RUBY - expect(out).to include("Rack's post install message") - expect(exitstatus).to be_zero if exitstatus + expect(out).to include("Myrack's post install message") - script <<-RUBY, :artifice => "endpoint" + script <<-RUBY, artifice: "endpoint" gemfile(true) do - source "https://notaserver.com" + source "https://notaserver.test" 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_lines = err.split("\n") - err_lines.reject!{|line| line =~ /\.rb:\d+: warning: / } + err_lines.reject! {|line| line =~ /\.rb:\d+: warning: / } expect(err_lines).to be_empty - expect(exitstatus).to be_zero if exitstatus end it "lets me use my own ui object" do - script <<-RUBY, :artifice => "endpoint" - require '#{lib_dir}/bundler' - class MyBundlerUI < Bundler::UI::Silent + script <<-RUBY, artifice: "endpoint" + require 'bundler' + class MyBundlerUI < Bundler::UI::Shell def confirm(msg, newline = nil) puts "CONFIRMED!" end end - gemfile(true, :ui => MyBundlerUI.new) do - source "https://notaserver.com" + my_ui = MyBundlerUI.new + my_ui.level = "confirm" + gemfile(true, :ui => my_ui) do + source "https://notaserver.test" gem "activesupport", :require => true end RUBY expect(out).to eq("CONFIRMED!\nCONFIRMED!") - expect(exitstatus).to be_zero if exitstatus end it "has an option for quiet installation" do - script <<-RUBY, :artifice => "endpoint" - require '#{lib_dir}/bundler/inline' + script <<-RUBY, artifice: "endpoint" + require 'bundler/inline' gemfile(true, :quiet => true) do - source "https://notaserver.com" + source "https://notaserver.test" gem "activesupport", :require => true end RUBY @@ -127,7 +125,7 @@ RSpec.describe "bundler/inline#gemfile" do end it "raises an exception if passed unknown arguments" do - script <<-RUBY + script <<-RUBY, raise_on_error: false gemfile(true, :arglebargle => true) do path "#{lib_path}" gem "two" @@ -141,9 +139,10 @@ RSpec.describe "bundler/inline#gemfile" do it "does not mutate the option argument" do script <<-RUBY - require '#{lib_dir}/bundler' + require 'bundler' options = { :ui => Bundler::UI::Shell.new } gemfile(false, options) do + source "https://gem.repo1" path "#{lib_path}" do gem "two" end @@ -152,22 +151,68 @@ RSpec.describe "bundler/inline#gemfile" do 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_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" + end + + puts MYRACK + RUBY + + expect(out).to eq("1.0.0") + expect(err).to be_empty + end + + it "installs subdependencies quietly if necessary when the install option is not set" do + build_repo4 do + build_gem "myrack" do |s| + s.add_dependency "myrackdep" + end + + build_gem "myrackdep", "1.0.0" + end + + script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + gemfile do + source "https://gem.repo4" + gem "myrack" + end + + require "myrackdep" + puts MYRACKDEP + RUBY + + expect(out).to eq("1.0.0") + expect(err).to be_empty + end + + it "installs subdependencies quietly if necessary when the install option is not set, and multiple sources used" do + build_repo4 do + build_gem "myrack" do |s| + s.add_dependency "myrackdep" + end + + build_gem "myrackdep", "1.0.0" + end + + script <<-RUBY, artifice: "compact_index_extra_api" + gemfile do + source "https://test.repo" + source "https://test.repo/extra" do + gem "myrack" + end end - puts RACK + require "myrackdep" + puts MYRACKDEP 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 @@ -175,6 +220,7 @@ RSpec.describe "bundler/inline#gemfile" do baz_ref = build_git("baz", "2.0.0").ref_for("HEAD") script <<-RUBY gemfile do + source "https://gem.repo1" 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 @@ -185,19 +231,20 @@ RSpec.describe "bundler/inline#gemfile" do 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 + source "https://gem.repo1" gem "two" end end gemfile do path "#{lib_path}" do + source "https://gem.repo1" gem "four" end end @@ -205,12 +252,118 @@ RSpec.describe "bundler/inline#gemfile" do expect(out).to eq("two\nfour") expect(err).to be_empty - expect(exitstatus).to be_zero if exitstatus + end + + it "doesn't reinstall already installed gems" do + system_gems "myrack-1.0.0" + + script <<-RUBY + require 'bundler' + ui = Bundler::UI::Shell.new + ui.level = "confirm" + + gemfile(true, ui: ui) do + source "https://gem.repo1" + gem "activesupport" + gem "myrack" + end + RUBY + + expect(out).to include("Installing activesupport") + expect(out).not_to include("Installing myrack") + expect(err).to be_empty + end + + it "installs gems in later gemfile calls" do + system_gems "myrack-1.0.0" + + script <<-RUBY + require 'bundler' + ui = Bundler::UI::Shell.new + ui.level = "confirm" + gemfile(true, ui: ui) do + source "https://gem.repo1" + gem "myrack" + end + + gemfile(true, ui: ui) do + source "https://gem.repo1" + gem "activesupport" + end + RUBY + + expect(out).to include("Installing activesupport") + expect(out).not_to include("Installing myrack") + expect(err).to be_empty + end + + it "doesn't reinstall already installed gems in later gemfile calls" do + system_gems "myrack-1.0.0" + + script <<-RUBY + require 'bundler' + ui = Bundler::UI::Shell.new + ui.level = "confirm" + gemfile(true, ui: ui) do + source "https://gem.repo1" + gem "activesupport" + end + + gemfile(true, ui: ui) do + source "https://gem.repo1" + gem "myrack" + end + RUBY + + expect(out).to include("Installing activesupport") + expect(out).not_to include("Installing myrack") + expect(err).to be_empty + end + + it "installs gems with native extensions in later gemfile calls" do + system_gems "myrack-1.0.0" + + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("lib", __dir__) + FileUtils.mkdir_p(path) + File.open("\#{path}/foo.rb", "w") do |f| + f.puts "FOO = 'YES'" + end + end + RUBY + end + + script <<-RUBY + require 'bundler' + ui = Bundler::UI::Shell.new + ui.level = "confirm" + gemfile(true, ui: ui) do + source "https://gem.repo1" + gem "myrack" + end + + gemfile(true, ui: ui) do + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + end + + require 'foo' + puts FOO + puts $:.grep(/ext/) + RUBY + + expect(out).to include("YES") + expect(out).to include(Pathname.glob(default_bundle_path("bundler/gems/extensions/**/foo-1.0-*")).first.to_s) + expect(err).to be_empty end it "installs inline gems when a Gemfile.lock is present" do gemfile <<-G - source "https://notaserver.com" + source "https://notaserver.test" gem "rake" G @@ -227,54 +380,94 @@ RSpec.describe "bundler/inline#gemfile" do rake BUNDLED WITH - 1.13.6 + #{Bundler::VERSION} G - in_app_root do - script <<-RUBY - gemfile do - source "#{file_uri_for(gem_repo1)}" - gem "rack" - end + script <<-RUBY + gemfile do + source "https://gem.repo1" + gem "myrack" + end - puts RACK - RUBY - end + puts MYRACK + RUBY + + expect(err).to be_empty + end + + it "does not leak Gemfile.lock versions to the installation output" do + gemfile <<-G + source "https://notaserver.test" + gem "rake" + G + + lockfile <<-G + GEM + remote: https://rubygems.org/ + specs: + rake (11.3.0) + + PLATFORMS + ruby + + DEPENDENCIES + rake + + BUNDLED WITH + #{Bundler::VERSION} + G + script <<-RUBY + gemfile(true) do + source "https://gem.repo1" + gem "rake", "#{rake_version}" + end + RUBY + + expect(out).to include("Installing rake #{rake_version}") + expect(out).not_to include("was 11.3.0") expect(err).to be_empty - expect(exitstatus).to be_zero if exitstatus end it "installs inline gems when frozen is set" do - script <<-RUBY, :env => { "BUNDLE_FROZEN" => "true" } + script <<-RUBY, env: { "BUNDLE_FROZEN" => "true", "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s } + gemfile do + source "https://gem.repo1" + gem "myrack" + end + + puts MYRACK + RUBY + + expect(last_command.stderr).to be_empty + end + + it "installs inline gems when deployment is set" do + script <<-RUBY, env: { "BUNDLE_DEPLOYMENT" => "true", "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s } gemfile do - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" end - puts RACK + puts MYRACK RUBY expect(last_command.stderr).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_uri_for(gem_repo1)}" - gem "rack" - end + script <<-RUBY + gemfile do + source "https://gem.repo1" + gem "myrack" + end - puts RACK - RUBY - end + puts MYRACK + RUBY expect(err).to be_empty - expect(exitstatus).to be_zero if exitstatus end it "installs inline gems when BUNDLE_BIN is set" do @@ -282,11 +475,11 @@ RSpec.describe "bundler/inline#gemfile" do script <<-RUBY gemfile do - source "#{file_uri_for(gem_repo1)}" - gem "rack" # has the rackup executable + source "https://gem.repo1" + gem "myrack" # has the myrackup executable end - puts RACK + puts MYRACK RUBY expect(last_command).to be_success expect(out).to eq "1.0.0" @@ -294,27 +487,265 @@ RSpec.describe "bundler/inline#gemfile" do context "when BUNDLE_PATH is set" do it "installs inline gems to the system path regardless" do - script <<-RUBY, :env => { "BUNDLE_PATH" => "./vendor/inline" } + script <<-RUBY, env: { "BUNDLE_PATH" => "./vendor/inline", "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s } gemfile(true) do - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" end RUBY expect(last_command).to be_success - expect(system_gem_path("gems/rack-1.0.0")).to exist + expect(system_gem_path("gems/myrack-1.0.0")).to exist end end it "skips platform warnings" do - simulate_platform "ruby" + bundle_config "force_ruby_platform true" + + script <<-RUBY + gemfile(true) do + source "https://gem.repo1" + gem "myrack", platform: :jruby + end + RUBY + + expect(err).to be_empty + end + + it "still installs if the application has `bundle package` no_install config set" do + bundle_config "no_install true" + + script <<-RUBY + gemfile do + source "https://gem.repo1" + gem "myrack" + end + RUBY + + expect(last_command).to be_success + expect(system_gem_path("gems/myrack-1.0.0")).to exist + end + + it "preserves previous BUNDLE_GEMFILE value" do + ENV["BUNDLE_GEMFILE"] = "" + script <<-RUBY + gemfile do + source "https://gem.repo1" + gem "myrack" + end + + puts "BUNDLE_GEMFILE is empty" if ENV["BUNDLE_GEMFILE"].empty? + system("#{Gem.ruby} -w -e '42'") # this should see original value of BUNDLE_GEMFILE + exit $?.exitstatus + RUBY + + expect(last_command).to be_success + expect(out).to include("BUNDLE_GEMFILE is empty") + end + + it "resets BUNDLE_GEMFILE to the empty string if it wasn't set previously" do + ENV["BUNDLE_GEMFILE"] = nil + script <<-RUBY + gemfile do + source "https://gem.repo1" + gem "myrack" + end + + puts "BUNDLE_GEMFILE is empty" if ENV["BUNDLE_GEMFILE"].empty? + system("#{Gem.ruby} -w -e '42'") # this should see original value of BUNDLE_GEMFILE + exit $?.exitstatus + RUBY + + expect(last_command).to be_success + expect(out).to include("BUNDLE_GEMFILE is empty") + end + + it "does not error out if library requires optional dependencies" do + Dir.mkdir tmp("path_without_gemfile") + + foo_code = <<~RUBY + begin + gem "bar" + rescue LoadError + end + + puts "WIN" + RUBY + + build_lib "foo", "1.0.0" do |s| + s.write "lib/foo.rb", foo_code + end + + script <<-RUBY, dir: tmp("path_without_gemfile"), env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s } + gemfile do + source "https://gem.repo2" + path "#{lib_path}" do + gem "foo", require: false + end + end + + require "foo" + RUBY + + expect(out).to eq("WIN") + expect(err).to be_empty + end + + it "does not load default timeout", rubygems: ">= 3.5.0" do + default_timeout_version = ruby "gem 'timeout', '< 999999'; require 'timeout'; puts Timeout::VERSION", raise_on_error: false + skip "timeout isn't a default gem" if default_timeout_version.empty? + + build_repo4 do + build_gem "timeout", "999" + end + + script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + require "bundler/inline" + + gemfile(true) do + source "https://gem.repo4" + + gem "timeout" + end + RUBY + + expect(out).to include("Installing timeout 999") + end + + it "does not upcase ENV" do + script <<-RUBY + require 'bundler/inline' + + ENV['Test_Variable'] = 'value string' + puts("before: \#{ENV.each_key.select { |key| key.match?(/test_variable/i) }}") + + gemfile do + source "https://gem.repo1" + end + + puts("after: \#{ENV.each_key.select { |key| key.match?(/test_variable/i) }}") + RUBY + + expect(out).to include("before: [\"Test_Variable\"]") + expect(out).to include("after: [\"Test_Variable\"]") + end + + it "does not create a lockfile" do + script <<-RUBY + require 'bundler/inline' + + gemfile do + source "https://gem.repo1" + end + + puts Dir.glob("Gemfile.lock") + RUBY + + expect(out).to be_empty + end + it "does not reset ENV" do script <<-RUBY + require 'bundler/inline' + + gemfile do + source "https://gem.repo1" + + ENV['FOO'] = 'bar' + end + + puts ENV['FOO'] + RUBY + + expect(out).to eq("bar") + end + + it "does not load specified version of psych and stringio", :ruby_repo do + build_repo4 do + build_gem "psych", "999" + build_gem "stringio", "999" + end + + script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + require "bundler/inline" + + gemfile(true) do + source "https://gem.repo4" + + gem "psych" + gem "stringio" + end + RUBY + + expect(out).to include("Installing psych 999") + expect(out).to include("Installing stringio 999") + if Gem.respond_to?(:use_psych?) && Gem.use_psych? + expect(out).to include("The psych gem was resolved to 999") + expect(out).to include("The stringio gem was resolved to 999") + end + end + + it "installs a conflicting default gem and non-default gems together" do + build_repo4 do + build_gem "securerandom", "999" + build_gem "myrack", "1.0.0" + end + + script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + gemfile(true) do + source "https://gem.repo4" + gem "securerandom" + gem "myrack" + end + + puts MYRACK + RUBY + + expect(out).to include("Installing securerandom 999") + expect(out).to include("Installing myrack 1.0.0") + expect(out).to include("1.0.0") + expect(err).to be_empty + end + + it "installs a conflicting default gem alongside git sources" do + build_repo4 do + build_gem "securerandom", "999" + end + + build_git "foo", "1.0.0" + + script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } gemfile(true) do - source "#{file_uri_for(gem_repo1)}" - gem "rack", platform: :jruby + source "https://gem.repo4" + gem "securerandom" + gem "foo", :git => #{lib_path("foo-1.0.0").to_s.dump} end + + puts FOO RUBY + expect(out).to include("Installing securerandom 999") + expect(out).to include("1.0.0") expect(err).to be_empty end + + it "leaves a lockfile in the same directory as the inline script alone" do + install_gemfile <<~G + source "https://gem.repo1" + gem "foo" + G + + original_lockfile = lockfile + + script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s } + require "bundler/inline" + + gemfile(true) do + source "https://gem.repo1" + + gem "myrack" + end + RUBY + + expect(lockfile).to eq(original_lockfile) + end end diff --git a/spec/bundler/runtime/load_spec.rb b/spec/bundler/runtime/load_spec.rb index 7de67e247c..472cde87c5 100644 --- a/spec/bundler/runtime/load_spec.rb +++ b/spec/bundler/runtime/load_spec.rb @@ -3,18 +3,19 @@ RSpec.describe "Bundler.load" do describe "with a gemfile" do before(:each) do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" G + allow(Bundler::SharedHelpers).to receive(:pwd).and_return(bundled_app) end it "provides a list of the env dependencies" do - expect(Bundler.load.dependencies).to have_dep("rack", ">= 0") + expect(Bundler.load.dependencies).to have_dep("myrack", ">= 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}") + expect(Bundler.load.gems).to have_gem("myrack-1.0.0", "bundler-#{Bundler::VERSION}") end it "ignores blank BUNDLE_GEMFILEs" do @@ -27,19 +28,20 @@ RSpec.describe "Bundler.load" do describe "with a gems.rb file" do before(:each) do - create_file "gems.rb", <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + gemfile "gems.rb", <<-G + source "https://gem.repo1" + gem "myrack" G - bundle! :install + bundle :install + allow(Bundler::SharedHelpers).to receive(:pwd).and_return(bundled_app) end it "provides a list of the env dependencies" do - expect(Bundler.load.dependencies).to have_dep("rack", ">= 0") + expect(Bundler.load.dependencies).to have_dep("myrack", ">= 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}") + expect(Bundler.load.gems).to have_gem("myrack-1.0.0", "bundler-#{Bundler::VERSION}") end end @@ -66,24 +68,24 @@ RSpec.describe "Bundler.load" do begin expect { Bundler.load }.to raise_error(Bundler::GemfileNotFound) ensure - bundler_gemfile.rmtree if @remove_bundler_gemfile + FileUtils.rm_rf bundler_gemfile if @remove_bundler_gemfile end end end describe "when called twice" do it "doesn't try to load the runtime twice" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" gem "activesupport", :group => :test G - ruby! <<-RUBY - require "#{lib_dir}/bundler" + ruby <<-RUBY + require "bundler" Bundler.setup :default Bundler.require :default - puts RACK + puts MYRACK begin require "activesupport" rescue LoadError @@ -97,11 +99,11 @@ RSpec.describe "Bundler.load" do describe "not hurting brittle rubygems" do it "does not inject #source into the generated YAML of the gem specs" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" + install_gemfile <<-G + source "https://gem.repo1" gem "activerecord" G - + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) Bundler.load.specs.each do |spec| expect(spec.to_yaml).not_to match(/^\s+source:/) expect(spec.to_yaml).not_to match(/^\s+groups:/) diff --git a/spec/bundler/runtime/platform_spec.rb b/spec/bundler/runtime/platform_spec.rb index f7e93eacf1..6d96758956 100644 --- a/spec/bundler/runtime/platform_spec.rb +++ b/spec/bundler/runtime/platform_spec.rb @@ -3,21 +3,21 @@ RSpec.describe "Bundler.setup with multi platform stuff" do it "raises a friendly error when gems are missing locally" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G lockfile <<-G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: - rack (1.0) + myrack (1.0) PLATFORMS #{local_tag} DEPENDENCIES - rack + myrack G ruby <<-R @@ -35,7 +35,7 @@ RSpec.describe "Bundler.setup with multi platform stuff" do it "will resolve correctly on the current platform when the lockfile was targeted for a different one" do lockfile <<-G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: nokogiri (1.4.2-java) weakling (= 0.0.3) @@ -48,19 +48,147 @@ RSpec.describe "Bundler.setup with multi platform stuff" do nokogiri G - simulate_platform "x86-darwin-10" - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" + simulate_platform "x86-darwin-10" do + install_gemfile <<-G + source "https://gem.repo1" + gem "nokogiri" + G + + expect(the_bundle).to include_gems "nokogiri 1.4.2" + end + end + + it "will keep both platforms when both ruby and a specific ruby platform are locked and the bundle is unlocked" do + build_repo4 do + build_gem "nokogiri", "1.11.1" do |s| + s.add_dependency "mini_portile2", "~> 2.5.0" + s.add_dependency "racca", "~> 1.5.2" + end + + build_gem "nokogiri", "1.11.1" do |s| + s.platform = Bundler.local_platform + s.add_dependency "racca", "~> 1.4" + end + + build_gem "mini_portile2", "2.5.0" + build_gem "racca", "1.5.2" + end + + checksums = checksums_section do |c| + c.checksum gem_repo4, "mini_portile2", "2.5.0" + c.checksum gem_repo4, "nokogiri", "1.11.1" + c.checksum gem_repo4, "nokogiri", "1.11.1", Bundler.local_platform + c.checksum gem_repo4, "racca", "1.5.2" + end + + good_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + mini_portile2 (2.5.0) + nokogiri (1.11.1) + mini_portile2 (~> 2.5.0) + racca (~> 1.5.2) + nokogiri (1.11.1-#{Bundler.local_platform}) + racca (~> 1.4) + racca (1.5.2) + + PLATFORMS + #{lockfile_platforms("ruby")} + + DEPENDENCIES + nokogiri (~> 1.11) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile <<-G + source "https://gem.repo4" + gem "nokogiri", "~> 1.11" + G + + lockfile good_lockfile + + bundle "update nokogiri" + + expect(lockfile).to eq(good_lockfile) + end + + it "will not try to install platform specific gems when they don't match the current ruby if locked only to ruby" do + build_repo4 do + build_gem "nokogiri", "1.11.1" + + build_gem "nokogiri", "1.11.1" do |s| + s.platform = Bundler.local_platform + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + gemfile <<-G + source "https://gem.repo4" gem "nokogiri" G - expect(the_bundle).to include_gems "nokogiri 1.4.2" + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.11.1) + + PLATFORMS + ruby + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install" + + expect(out).to include("Fetching nokogiri 1.11.1") + expect(the_bundle).to include_gems "nokogiri 1.11.1" + expect(the_bundle).not_to include_gems "nokogiri 1.11.1 #{Bundler.local_platform}" + end + + it "will use the java platform if both generic java and generic ruby platforms are locked", :jruby_only do + gemfile <<-G + source "https://gem.repo1" + gem "nokogiri" + G + + lockfile <<-G + GEM + remote: https://gem.repo1/ + specs: + nokogiri (1.4.2) + nokogiri (1.4.2-java) + weakling (>= 0.0.3) + weakling (0.0.3) + + PLATFORMS + java + ruby + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + G + + bundle "install" + + expect(out).to include("Fetching nokogiri 1.4.2 (java)") + expect(the_bundle).to include_gems "nokogiri 1.4.2 java" end it "will add the resolve for the current platform" do lockfile <<-G GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: nokogiri (1.4.2-java) weakling (= 0.0.3) @@ -73,65 +201,207 @@ RSpec.describe "Bundler.setup with multi platform stuff" do nokogiri G - simulate_platform "x86-darwin-100" + simulate_platform "x86-darwin-100" do + install_gemfile <<-G + source "https://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 + end - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" + it "allows specifying only-ruby-platform on jruby", :jruby_only do + install_gemfile <<-G + source "https://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" + 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" do - simulate_platform "java" + gemfile <<-G + source "https://gem.repo1" + gem "nokogiri" + gem "platform_specific" + G + + bundle_config "force_ruby_platform true" + + bundle "install" - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" + expect(the_bundle).to include_gems "nokogiri 1.4.2", "platform_specific 1.0 ruby" + end + + it "allows specifying only-ruby-platform even if the lockfile is locked to a specific compatible platform" do + install_gemfile <<-G + source "https://gem.repo1" gem "nokogiri" gem "platform_specific" G - bundle! "config set force_ruby_platform true" + bundle_config "force_ruby_platform true" + + bundle "install" + + expect(the_bundle).to include_gems "nokogiri 1.4.2", "platform_specific 1.0 ruby" + end - bundle! "install" + it "doesn't pull platform specific gems on truffleruby", :truffleruby_only do + install_gemfile <<-G + source "https://gem.repo1" + gem "platform_specific" + G - expect(the_bundle).to include_gems "nokogiri 1.4.2", "platform_specific 1.0 RUBY" + expect(the_bundle).to include_gems "platform_specific 1.0 ruby" + end + + it "doesn't pull platform specific gems on truffleruby (except when whitelisted) even if lockfile was generated with an older version that declared ruby as platform", :truffleruby_only do + gemfile <<-G + source "https://gem.repo1" + gem "platform_specific" + G + + lockfile <<-L + GEM + remote: https://gem.repo1/ + specs: + platform_specific (1.0) + + PLATFORMS + ruby + + DEPENDENCIES + platform_specific + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install" + + expect(the_bundle).to include_gems "platform_specific 1.0 ruby" + + simulate_platform "x86_64-linux" do + build_repo4 do + build_gem "libv8" + + build_gem "libv8" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<-G + source "https://gem.repo4" + gem "libv8" + G + + lockfile <<-L + GEM + remote: https://gem.repo4/ + specs: + libv8 (1.0) + + PLATFORMS + ruby + + DEPENDENCIES + libv8 + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install" + + expect(the_bundle).to include_gems "libv8 1.0 x86_64-linux" + end + end + + it "doesn't pull platform specific gems on truffleruby, even if lockfile only includes those", :truffleruby_only do + gemfile <<-G + source "https://gem.repo1" + gem "platform_specific" + G + + lockfile <<-L + GEM + remote: https://gem.repo1/ + specs: + platform_specific (1.0-x86-darwin-100) + + PLATFORMS + x86-darwin-100 + + DEPENDENCIES + platform_specific + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install" + + expect(the_bundle).to include_gems "platform_specific 1.0 ruby" + end + + it "pulls platform specific gems correctly on musl" do + build_repo4 do + build_gem "nokogiri", "1.13.8" do |s| + s.platform = "aarch64-linux" + end + end + + simulate_platform "aarch64-linux-musl" do + install_gemfile <<-G, verbose: true + source "https://gem.repo4" + gem "nokogiri" + G + end + + expect(out).to include("Fetching nokogiri 1.13.8 (aarch64-linux)") end it "allows specifying only-ruby-platform on windows with dependency platforms" do - simulate_windows do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "nokogiri", :platforms => [:mingw, :mswin, :x64_mingw, :jruby] + simulate_platform "x86-mswin32" do + install_gemfile <<-G + source "https://gem.repo1" + gem "nokogiri", :platforms => [:windows, :jruby] gem "platform_specific" G - bundle! "config set force_ruby_platform true" + bundle_config "force_ruby_platform true" - bundle! "install" + bundle "install" - expect(the_bundle).to include_gems "platform_specific 1.0 RUBY" + expect(the_bundle).to include_gems "platform_specific 1.0 ruby" + expect(the_bundle).to not_include_gems "nokogiri" end end it "allows specifying only-ruby-platform on windows with gemspec dependency" do - build_lib("foo", "1.0", :path => ".") do |s| - s.add_dependency "rack" + build_lib("foo", "1.0", path: bundled_app) do |s| + s.add_dependency "myrack" end gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gemspec G - bundle! :lock + bundle :lock - simulate_windows do - bundle! "config set force_ruby_platform true" - bundle! "install" + simulate_platform "x86-mswin32" do + bundle_config "force_ruby_platform true" + bundle "install" - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 1.0" end end @@ -141,29 +411,58 @@ RSpec.describe "Bundler.setup with multi platform stuff" do s.add_dependency "platform_specific" end end - simulate_windows x64_mingw do + simulate_platform "x64-mingw-ucrt" do lockfile <<-L GEM - remote: #{file_uri_for(gem_repo2)}/ + remote: https://gem.repo2/ specs: platform_specific (1.0-x86-mingw32) requires_platform_specific (1.0) platform_specific PLATFORMS - x64-mingw32 + x64-mingw-ucrt x86-mingw32 DEPENDENCIES requires_platform_specific L - install_gemfile! <<-G, :verbose => true - source "#{file_uri_for(gem_repo2)}" + install_gemfile <<-G, verbose: true + source "https://gem.repo2" gem "requires_platform_specific" G - expect(the_bundle).to include_gem "platform_specific 1.0 x64-mingw32" + expect(out).to include("lockfile does not have all gems needed for the current platform") + expect(the_bundle).to include_gem "platform_specific 1.0 x64-mingw-ucrt" + end + end + + %w[x86-mswin32 x64-mswin64 x86-mingw32 x64-mingw-ucrt aarch64-mingw-ucrt].each do |platform| + it "allows specifying platform windows on #{platform} platform" do + simulate_platform platform do + lockfile <<-L + GEM + remote: https://gem.repo1/ + specs: + platform_specific (1.0-#{platform}) + requires_platform_specific (1.0) + platform_specific + + PLATFORMS + #{platform} + + DEPENDENCIES + requires_platform_specific + L + + install_gemfile <<-G + source "https://gem.repo1" + gem "platform_specific", :platforms => [:windows] + G + + expect(the_bundle).to include_gems "platform_specific 1.0 #{platform}" + end end end end diff --git a/spec/bundler/runtime/require_spec.rb b/spec/bundler/runtime/require_spec.rb index 490b8c7631..46613286d2 100644 --- a/spec/bundler/runtime/require_spec.rb +++ b/spec/bundler/runtime/require_spec.rb @@ -21,7 +21,7 @@ RSpec.describe "Bundler.require" do s.write "lib/four.rb", "puts 'four'" end - build_lib "five", "1.0.0", :no_default => true do |s| + build_lib "five", "1.0.0", no_default: true do |s| s.write "lib/mofive.rb", "puts 'five'" end @@ -46,6 +46,7 @@ RSpec.describe "Bundler.require" do end gemfile <<-G + source "https://gem.repo1" path "#{lib_path}" do gem "one", :group => :bar, :require => %w[baz qux] gem "two" @@ -112,16 +113,15 @@ RSpec.describe "Bundler.require" do it "raises an exception if a require is specified but the file does not exist" do gemfile <<-G + source "https://gem.repo1" path "#{lib_path}" do gem "two", :require => 'fail' end G - load_error_run <<-R, "fail" - Bundler.require - R + run "Bundler.require", raise_on_error: false - expect(err_without_deprecations).to eq("ZOMG LOAD ERROR") + expect(err_without_deprecations).to include("cannot load such file -- fail") end it "displays a helpful message if the required gem throws an error" do @@ -130,12 +130,13 @@ RSpec.describe "Bundler.require" do end gemfile <<-G + source "https://gem.repo1" path "#{lib_path}" do gem "faulty" end G - run "Bundler.require" + run "Bundler.require", raise_on_error: false expect(err).to match("error while trying to load the gem 'faulty'") expect(err).to match("Gem Internal Error Message") end @@ -146,21 +147,15 @@ RSpec.describe "Bundler.require" do end gemfile <<-G + source "https://gem.repo1" path "#{lib_path}" do gem "loadfuuu" end G - cmd = <<-RUBY - begin - Bundler.require - rescue LoadError => e - $stderr.puts "ZOMG LOAD ERROR: \#{e.message}" - end - RUBY - run(cmd) + run "Bundler.require", raise_on_error: false - expect(err_without_deprecations).to eq("ZOMG LOAD ERROR: cannot load such file -- load-bar") + expect(err_without_deprecations).to include("cannot load such file -- load-bar") end describe "with namespaced gems" do @@ -168,11 +163,11 @@ RSpec.describe "Bundler.require" 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 + source "https://gem.repo1" path '#{lib_path}' do gem 'jquery-rails' end @@ -183,17 +178,19 @@ RSpec.describe "Bundler.require" do end it "silently passes if the require fails" do - build_lib "bcrypt-ruby", "1.0.0", :no_default => true do |s| + build_lib "bcrypt-ruby", "1.0.0", no_default: true do |s| s.write "lib/brcrypt.rb", "BCrypt = '1.0.0'" end gemfile <<-G + source "https://gem.repo1" + path "#{lib_path}" do gem "bcrypt-ruby" end G cmd = <<-RUBY - require '#{lib_dir}/bundler' + require 'bundler' Bundler.require RUBY ruby(cmd) @@ -203,15 +200,15 @@ RSpec.describe "Bundler.require" do it "does not mangle explicitly given requires" do gemfile <<-G + source "https://gem.repo1" path "#{lib_path}" do gem 'jquery-rails', :require => 'jquery-rails' end G - load_error_run <<-R, "jquery-rails" - Bundler.require - R - expect(err_without_deprecations).to eq("ZOMG LOAD ERROR") + run "Bundler.require", raise_on_error: false + + expect(err_without_deprecations).to include("cannot load such file -- jquery-rails") end it "handles the case where regex fails" do @@ -220,45 +217,32 @@ RSpec.describe "Bundler.require" do end gemfile <<-G + source "https://gem.repo1" path "#{lib_path}" do gem "load-fuuu" end 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) + run "Bundler.require", raise_on_error: false - expect(err_without_deprecations).to eq("ZOMG LOAD ERROR") + expect(err_without_deprecations).to include("libfuuu-1.0").and include("cannot open shared object file") 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 + source "https://gem.repo1" path "#{lib_path}" do gem "load-fuuu" end G - cmd = <<-RUBY - begin - Bundler.require - rescue LoadError => e - $stderr.puts "ZOMG LOAD ERROR: \#{e.message}" - end - RUBY - run(cmd) + run "Bundler.require", raise_on_error: false - expect(err_without_deprecations).to eq("ZOMG LOAD ERROR: cannot load such file -- load-bar") + expect(err_without_deprecations).to include("cannot load such file -- load-bar") end end @@ -302,6 +286,7 @@ RSpec.describe "Bundler.require" do it "works when the gems are in the Gemfile in the correct order" do gemfile <<-G + source "https://gem.repo1" path "#{lib_path}" do gem "two" gem "one" @@ -314,12 +299,13 @@ RSpec.describe "Bundler.require" do describe "a gem with different requires for different envs" do before(:each) do - build_gem "multi_gem", :to_bundle => true do |s| + build_gem "multi_gem", to_bundle: true do |s| s.write "lib/one.rb", "puts 'ONE'" s.write "lib/two.rb", "puts 'TWO'" end install_gemfile <<-G + source "https://gem.repo1" gem "multi_gem", :require => "one", :group => :one gem "multi_gem", :require => "two", :group => :two G @@ -343,6 +329,7 @@ RSpec.describe "Bundler.require" do it "fails when the gems are in the Gemfile in the wrong order" do gemfile <<-G + source "https://gem.repo1" path "#{lib_path}" do gem "one" gem "two" @@ -355,30 +342,30 @@ RSpec.describe "Bundler.require" do describe "with busted gems" do it "should be busted" do - build_gem "busted_require", :to_bundle => true do |s| + build_gem "busted_require", to_bundle: true do |s| s.write "lib/busted_require.rb", "require 'no_such_file_omg'" end install_gemfile <<-G + source "https://gem.repo1" gem "busted_require" G - load_error_run <<-R, "no_such_file_omg" - Bundler.require - R - expect(err_without_deprecations).to eq("ZOMG LOAD ERROR") + run "Bundler.require", raise_on_error: false + + expect(err_without_deprecations).to include("cannot load such file -- no_such_file_omg") end end end it "does not load rubygems gemspecs that are used" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" G - run! <<-R - path = File.join(Gem.dir, "specifications", "rack-1.0.0.gemspec") + run <<-R + path = File.join(Gem.dir, "specifications", "myrack-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| @@ -386,7 +373,7 @@ RSpec.describe "Bundler.require" do end R - run! <<-R + run <<-R Bundler.require puts "WIN" R @@ -397,11 +384,12 @@ RSpec.describe "Bundler.require" do it "does not load git gemspecs that are used" do build_git "foo" - install_gemfile! <<-G + install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G - run! <<-R + 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 @@ -410,7 +398,47 @@ RSpec.describe "Bundler.require" do end R - run! <<-R + run <<-R + Bundler.require + puts "WIN" + R + + expect(out).to eq("WIN") + end + + it "does not load plugins" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + create_file "plugins/rubygems_plugin.rb", "puts 'FAIL'" + + run <<~R, env: { "RUBYLIB" => rubylib.unshift(bundled_app("plugins").to_s).join(File::PATH_SEPARATOR) } + Bundler.require + puts "WIN" + R + + expect(out).to eq("WIN") + end + + it "does not extract gemspecs from application cache packages" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle :cache + + path = cached_gem("myrack-1.0.0") + + run <<-R + File.open("#{path}", "w") do |f| + f.write "broken package" + end + R + + run <<-R Bundler.require puts "WIN" R @@ -422,13 +450,13 @@ 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_uri_for(gem_repo1)}" + source "https://gem.repo1" platforms :#{not_local_tag} do - gem "fail", :require => "omgomg" + gem "platform_specific", :require => "omgomg" end - gem "rack", "1.0.0" + gem "myrack", "1.0.0" G run "Bundler.require" @@ -437,14 +465,14 @@ RSpec.describe "Bundler.require with platform specific dependencies" do it "requires gems pinned to multiple platforms, including the current one" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" platforms :#{not_local_tag}, :#{local_tag} do - gem "rack", :require => "rack" + gem "myrack", :require => "myrack" end G - run "Bundler.require; puts RACK" + run "Bundler.require; puts MYRACK" expect(out).to eq("1.0.0") expect(err).to be_empty diff --git a/spec/bundler/runtime/requiring_spec.rb b/spec/bundler/runtime/requiring_spec.rb new file mode 100644 index 0000000000..f0e0aeacaf --- /dev/null +++ b/spec/bundler/runtime/requiring_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +RSpec.describe "Requiring bundler" do + it "takes care of requiring rubygems when entrypoint is bundler/setup" do + sys_exec("#{Gem.ruby} -I#{lib_dir} -rbundler/setup -e'puts true'", env: { "RUBYOPT" => "--disable=gems" }) + + expect(stdboth).to eq("true") + end + + it "takes care of requiring rubygems when requiring just bundler" do + sys_exec("#{Gem.ruby} -I#{lib_dir} -rbundler -e'puts true'", env: { "RUBYOPT" => "--disable=gems" }) + + expect(stdboth).to eq("true") + end +end diff --git a/spec/bundler/runtime/self_management_spec.rb b/spec/bundler/runtime/self_management_spec.rb new file mode 100644 index 0000000000..176c2a3121 --- /dev/null +++ b/spec/bundler/runtime/self_management_spec.rb @@ -0,0 +1,279 @@ +# frozen_string_literal: true + +RSpec.describe "Self management" do + describe "auto switching" do + let(:previous_minor) do + "9.3.0" + end + + let(:current_version) do + "9.4.0" + end + + before do + build_repo4 do + build_bundler previous_minor + + build_bundler current_version + + build_gem "myrack", "1.0.0" + end + + gemfile <<-G + source "https://gem.repo4" + + gem "myrack" + G + + pristine_system_gems "bundler-#{current_version}" + end + + it "installs locked version when using system path and uses it" do + lockfile_bundled_with(previous_minor) + + bundle_config "path.system true" + bundle "install" + expect(out).to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.") + + # It uninstalls the older system bundler + bundle "clean --force", artifice: nil + expect(out).to eq("Removing bundler (#{current_version})") + + # App now uses locked version + bundle "-v", artifice: nil + expect(out).to eq(previous_minor) + + # ruby-core test setup has always "lib" in $LOAD_PATH so `require "bundler/setup"` always activate the local version rather than using RubyGems gem activation stuff + unless ruby_core? + # App now uses locked version, even when not using the CLI directly + file = bundled_app("bin/bundle_version.rb") + create_file file, <<-RUBY + #!#{Gem.ruby} + require 'bundler/setup' + puts '#{previous_minor}' + RUBY + file.chmod(0o777) + cmd = Gem.win_platform? ? "#{Gem.ruby} bin/bundle_version.rb" : "bin/bundle_version.rb" + in_bundled_app cmd + expect(out).to eq(previous_minor) + end + + # Subsequent installs use the locked version without reinstalling + bundle "install --verbose", artifice: nil + expect(out).to include("Using bundler #{previous_minor}") + expect(out).not_to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.") + end + + it "installs locked version when using local path and uses it" do + lockfile_bundled_with(previous_minor) + + bundle_config "path vendor/bundle" + bundle "install" + expect(out).to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.") + expect(vendored_gems("gems/bundler-#{previous_minor}")).to exist + + # It does not uninstall the locked bundler + bundle "clean" + expect(out).to be_empty + + # App now uses locked version + bundle "-v" + expect(out).to eq(previous_minor) + + # Preserves original gem home when auto-switching + bundle "exec ruby -e 'puts Bundler.original_env[\"GEM_HOME\"]'" + expect(out).to eq(ENV["GEM_HOME"]) + + # ruby-core test setup has always "lib" in $LOAD_PATH so `require "bundler/setup"` always activate the local version rather than using RubyGems gem activation stuff + unless ruby_core? + # App now uses locked version, even when not using the CLI directly + file = bundled_app("bin/bundle_version.rb") + create_file file, <<-RUBY + #!#{Gem.ruby} + require 'bundler/setup' + puts '#{previous_minor}' + RUBY + file.chmod(0o777) + cmd = Gem.win_platform? ? "#{Gem.ruby} bin/bundle_version.rb" : "bin/bundle_version.rb" + in_bundled_app cmd + expect(out).to eq(previous_minor) + end + + # Subsequent installs use the locked version without reinstalling + bundle "install --verbose" + expect(out).to include("Using bundler #{previous_minor}") + expect(out).not_to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.") + end + + it "installs locked version when using deployment option and uses it" do + lockfile_bundled_with(previous_minor) + + bundle_config "deployment true" + bundle "install" + expect(out).to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.") + expect(vendored_gems("gems/bundler-#{previous_minor}")).to exist + + # It does not uninstall the locked bundler + bundle "clean" + expect(out).to be_empty + + # App now uses locked version + bundle "-v" + expect(out).to eq(previous_minor) + + # Subsequent installs use the locked version without reinstalling + bundle "install --verbose" + expect(out).to include("Using bundler #{previous_minor}") + expect(out).not_to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.") + end + + it "does not try to install a development version" do + lockfile_bundled_with("#{previous_minor}.dev") + + bundle "install --verbose" + expect(out).not_to match(/restarting using that version/) + + bundle "-v" + expect(out).to eq(current_version) + end + + it "does not try to install when --local is passed" do + lockfile_bundled_with(previous_minor) + system_gems "myrack-1.0.0", path: local_gem_path + + bundle "install --local" + expect(out).not_to match(/Installing Bundler/) + + bundle "-v" + expect(out).to eq(current_version) + end + + it "shows a discrete message if locked bundler does not exist" do + missing_minor = "#{current_version[0]}.999.999" + + lockfile_bundled_with(missing_minor) + + bundle "install" + expect(err).to eq("Your lockfile is locked to a version of bundler (#{missing_minor}) that doesn't exist at https://rubygems.org/. Going on using #{current_version}") + + bundle "-v" + expect(out).to eq(current_version) + end + + it "installs BUNDLE_VERSION version when using bundle config version x.y.z" do + lockfile_bundled_with(current_version) + + bundle_config "version #{previous_minor}" + bundle "install" + expect(out).to include("Bundler #{current_version} is running, but your configuration was #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.") + + bundle "-v" + expect(out).to eq(previous_minor) + end + + it "requires the right bundler version from the config and run bundle CLI without re-exec" do + unless Bundler.rubygems.provides?(">= 4.1.0.dev") + skip "This spec can only run when Gem::BundlerVersionFinder.bundler_versions reads bundler configs" + end + + lockfile_bundled_with(current_version) + + bundle_config "version #{previous_minor}" + bundle_config "path.system true" + bundle "install" + + script = bundled_app("script.rb") + create_file(script, "p 'executed once'") + + bundle "-v", env: { "RUBYOPT" => "-r#{script}" } + expect(out).to eq(%("executed once"\n9.3.0)) + end + + it "does not try to install when using bundle config version global" do + lockfile_bundled_with(previous_minor) + + bundle_config "version system" + bundle "install" + expect(out).not_to match(/restarting using that version/) + + bundle "-v" + expect(out).to eq(current_version) + end + + it "does not try to install when using bundle config version <dev-version>" do + lockfile_bundled_with(previous_minor) + + bundle_config "version #{previous_minor}.dev" + bundle "install" + expect(out).not_to match(/restarting using that version/) + + bundle "-v" + expect(out).to eq(current_version) + end + + it "ignores malformed lockfile version" do + lockfile_bundled_with("2.3.") + + bundle "install --verbose" + expect(out).to include("Using bundler #{current_version}") + end + + it "uses the right original script when re-execing, if `$0` has been changed to something that's not a script", :ruby_repo do + system_gems "bundler-9.9.9", path: local_gem_path + + test = bundled_app("test.rb") + + create_file test, <<~RUBY + $0 = "this is the program name" + require "bundler/setup" + RUBY + + lockfile_bundled_with("9.9.9") + + in_bundled_app "#{Gem.ruby} #{test}", raise_on_error: false + expect(err).to include("Could not find myrack-1.0.0") + expect(err).not_to include("this is the program name") + end + + it "uses modified $0 when re-execing, if `$0` has been changed to a script", :ruby_repo do + system_gems "bundler-9.9.9", path: local_gem_path + + runner = bundled_app("runner.rb") + + create_file runner, <<~RUBY + $0 = ARGV.shift + load $0 + RUBY + + script = bundled_app("script.rb") + create_file script, <<~RUBY + require "bundler/setup" + RUBY + + lockfile_bundled_with("9.9.9") + + in_bundled_app "#{Gem.ruby} #{runner} #{script}", raise_on_error: false + expect(err).to include("Could not find myrack-1.0.0") + end + + private + + def lockfile_bundled_with(version) + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + + BUNDLED WITH + #{version} + L + end + end +end diff --git a/spec/bundler/runtime/setup_spec.rb b/spec/bundler/runtime/setup_spec.rb index 4a754945b7..ceb6fcf66a 100644 --- a/spec/bundler/runtime/setup_spec.rb +++ b/spec/bundler/runtime/setup_spec.rb @@ -1,22 +1,21 @@ # frozen_string_literal: true require "tmpdir" -require "tempfile" RSpec.describe "Bundler.setup" do describe "with no arguments" do it "makes all groups available" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :group => :test + source "https://gem.repo1" + gem "myrack", :group => :test G ruby <<-RUBY - require '#{lib_dir}/bundler' + require 'bundler' Bundler.setup - require 'rack' - puts RACK + require 'myrack' + puts MYRACK RUBY expect(err).to be_empty expect(out).to eq("1.0.0") @@ -26,19 +25,19 @@ RSpec.describe "Bundler.setup" do describe "when called with groups" do before(:each) do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "yard" - gem "rack", :group => :test + gem "myrack", :group => :test G end it "doesn't make all groups available" do ruby <<-RUBY - require '#{lib_dir}/bundler' + require 'bundler' Bundler.setup(:default) begin - require 'rack' + require 'myrack' rescue LoadError puts "WIN" end @@ -49,11 +48,11 @@ RSpec.describe "Bundler.setup" do it "accepts string for group name" do ruby <<-RUBY - require '#{lib_dir}/bundler' + require 'bundler' Bundler.setup(:default, 'test') - require 'rack' - puts RACK + require 'myrack' + puts MYRACK RUBY expect(err).to be_empty expect(out).to eq("1.0.0") @@ -61,12 +60,12 @@ RSpec.describe "Bundler.setup" do it "leaves all groups available if they were already" do ruby <<-RUBY - require '#{lib_dir}/bundler' + require 'bundler' Bundler.setup Bundler.setup(:default) - require 'rack' - puts RACK + require 'myrack' + puts MYRACK RUBY expect(err).to be_empty expect(out).to eq("1.0.0") @@ -74,7 +73,7 @@ RSpec.describe "Bundler.setup" do it "leaves :default available if setup is called twice" do ruby <<-RUBY - require '#{lib_dir}/bundler' + require 'bundler' Bundler.setup(:default) Bundler.setup(:default, :test) @@ -90,16 +89,16 @@ RSpec.describe "Bundler.setup" do end it "handles multiple non-additive invocations" do - ruby <<-RUBY - require '#{lib_dir}/bundler' + ruby <<-RUBY, raise_on_error: false + require 'bundler' Bundler.setup(:default, :test) Bundler.setup(:default) - require 'rack' + require 'myrack' puts "FAIL" RUBY - expect(err).to match("rack") + expect(err).to match("myrack") expect(err).to match("LoadError") expect(out).not_to match("FAIL") end @@ -107,44 +106,45 @@ RSpec.describe "Bundler.setup" do context "load order" do def clean_load_path(lp) - without_bundler_load_path = ruby!("puts $LOAD_PATH").split("\n") - lp -= without_bundler_load_path - lp.map! {|p| p.sub(/^#{Regexp.union system_gem_path.to_s, default_bundle_path.to_s, lib_dir.to_s}/i, "") } + without_bundler_load_path = ruby("puts $LOAD_PATH").split("\n") + lp -= [*without_bundler_load_path, lib_dir.to_s] + lp.map! {|p| p.sub(system_gem_path.to_s, "") } end it "puts loaded gems after -I and RUBYLIB", :ruby_repo do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G ENV["RUBYOPT"] = "#{ENV["RUBYOPT"]} -Idash_i_dir" ENV["RUBYLIB"] = "rubylib_dir" ruby <<-RUBY - require '#{lib_dir}/bundler' + require 'bundler' Bundler.setup puts $LOAD_PATH RUBY load_path = out.split("\n") - rack_load_order = load_path.index {|path| path.include?("rack") } + myrack_load_order = load_path.index {|path| path.include?("myrack") } - expect(err).to eq("") + expect(err).to be_empty expect(load_path).to include(a_string_ending_with("dash_i_dir"), "rubylib_dir") - expect(rack_load_order).to be > 0 + expect(myrack_load_order).to be > 0 end it "orders the load path correctly when there are dependencies" do - system_gems :bundler + bundle_config "path.system true" install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "rails" G - ruby! <<-RUBY - require '#{lib_dir}/bundler' + ruby <<-RUBY + require 'bundler' + gem "bundler", "#{Bundler::VERSION}" if #{ruby_core?} Bundler.setup puts $LOAD_PATH RUBY @@ -153,26 +153,27 @@ RSpec.describe "Bundler.setup" do expect(load_path).to start_with( "/gems/rails-2.3.2/lib", - "/gems/bundler-#{Bundler::VERSION}/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-12.3.2/lib" + "/gems/rake-#{rake_version}/lib" ) end it "falls back to order the load path alphabetically for backwards compatibility" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" + bundle_config "path.system true" + + install_gemfile <<-G + source "https://gem.repo1" gem "weakling" gem "duradura" gem "terranova" G - ruby! <<-RUBY - require '#{lib_dir}/bundler/setup' + ruby <<-RUBY + require 'bundler/setup' puts $LOAD_PATH RUBY @@ -188,12 +189,12 @@ RSpec.describe "Bundler.setup" do it "raises if the Gemfile was not yet installed" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G ruby <<-R - require '#{lib_dir}/bundler' + require 'bundler' begin Bundler.setup @@ -208,66 +209,66 @@ RSpec.describe "Bundler.setup" do it "doesn't create a Gemfile.lock if the setup fails" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - ruby <<-R - require '#{lib_dir}/bundler' + ruby <<-R, raise_on_error: false + require 'bundler' Bundler.setup R - expect(bundled_app("Gemfile.lock")).not_to exist + expect(bundled_app_lock).not_to exist end it "doesn't change the Gemfile.lock if the setup fails" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - lockfile = File.read(bundled_app("Gemfile.lock")) + lockfile = File.read(bundled_app_lock) gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" gem "nosuchgem", "10.0" G - ruby <<-R - require '#{lib_dir}/bundler' + ruby <<-R, raise_on_error: false + require 'bundler' Bundler.setup R - expect(File.read(bundled_app("Gemfile.lock"))).to eq(lockfile) + expect(File.read(bundled_app_lock)).to eq(lockfile) end it "makes a Gemfile.lock if setup succeeds" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - File.read(bundled_app("Gemfile.lock")) + File.read(bundled_app_lock) - FileUtils.rm(bundled_app("Gemfile.lock")) + FileUtils.rm(bundled_app_lock) run "1" - expect(bundled_app("Gemfile.lock")).to exist + expect(bundled_app_lock).to exist end describe "$BUNDLE_GEMFILE" do context "user provides an absolute path" do it "uses BUNDLE_GEMFILE to locate the gemfile if present" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G gemfile bundled_app("4realz"), <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "activesupport", "2.3.5" G @@ -279,17 +280,17 @@ RSpec.describe "Bundler.setup" do end context "an absolute path is not provided" do - it "uses BUNDLE_GEMFILE to locate the gemfile if present" do + it "uses BUNDLE_GEMFILE to locate the gemfile if present and doesn't fail in deployment mode" do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" G bundle "install" - bundle "install --deployment" + bundle_config "deployment true" ENV["BUNDLE_GEMFILE"] = "Gemfile" ruby <<-R - require '#{lib_dir}/bundler' + require 'bundler' begin Bundler.setup @@ -302,28 +303,54 @@ RSpec.describe "Bundler.setup" do expect(out).to eq("WIN") end end + + context "user sets it via `config set --local gemfile`" do + it "uses the value in the config" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + gemfile bundled_app("CustomGemfile"), <<-G + source "https://gem.repo1" + gem "activesupport", "2.3.5" + G + + bundle_config "gemfile #{bundled_app("CustomGemfile")}" + bundle "install" + + ruby <<-R + require 'bundler' + Bundler.setup + require 'activesupport' + puts ACTIVESUPPORT + R + + expect(out).to eq("2.3.5") + end + end 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_uri_for(gem_repo1)}" - gem "rack", "1.0.0" + source "https://gem.repo1" + gem "myrack", "1.0.0" G - build_gem "rack", "1.0", :to_system => true do |s| - s.write "lib/rack.rb", "RACK = 'FAIL'" + build_gem "myrack", "1.0", to_system: true do |s| + s.write "lib/myrack.rb", "MYRACK = 'FAIL'" end - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end describe "integrate with rubygems" do describe "by replacing #gem" do before :each do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "0.9.1" + source "https://gem.repo1" + gem "myrack", "0.9.1" G end @@ -340,23 +367,10 @@ RSpec.describe "Bundler.setup" do 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 be_empty - end - it "replaces #gem but raises when the version is wrong" do run <<-R begin - gem "rack", "1.0.0" + gem "myrack", "1.0.0" puts "FAIL" rescue LoadError puts "WIN" @@ -365,39 +379,27 @@ RSpec.describe "Bundler.setup" do 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 be_empty - end end describe "by hiding system gems" do before :each do system_gems "activesupport-2.3.5" install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://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") + expect(out).to include("bundler-#{Bundler::VERSION}").and include("yard-1.0") + expect(out).not_to include("activesupport-2.3.5") 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 } + substring = "/" + $LOAD_PATH.find {|p| p.include?("vendor_ruby") }.split("/")[2] + run "puts 'worked!'", env: { "GEM_PATH" => substring } expect(out).to eq("worked!") end end @@ -406,36 +408,37 @@ RSpec.describe "Bundler.setup" do describe "with paths" do it "activates the gems in the path source" do - system_gems "rack-1.0.0" + system_gems "myrack-1.0.0" - build_lib "rack", "1.0.0" do |s| - s.write "lib/rack.rb", "puts 'WIN'" + build_lib "myrack", "1.0.0" do |s| + s.write "lib/myrack.rb", "puts 'WIN'" end gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - path "#{lib_path("rack-1.0.0")}" do - gem "rack" + source "https://gem.repo1" + path "#{lib_path("myrack-1.0.0")}" do + gem "myrack" end G - run "require 'rack'" + run "require 'myrack'" expect(out).to eq("WIN") end end describe "with git" do before do - build_git "rack", "1.0.0" + build_git "myrack", "1.0.0" gemfile <<-G - gem "rack", :git => "#{lib_path("rack-1.0.0")}" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-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) + run "1", raise_on_error: false + expect(err).to match(/the git source #{lib_path("myrack-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 @@ -444,7 +447,7 @@ RSpec.describe "Bundler.setup" do break_git! ruby <<-R - require '#{lib_dir}/bundler' + require 'bundler' begin Bundler.setup @@ -460,12 +463,12 @@ RSpec.describe "Bundler.setup" do it "provides a good exception if the lockfile is unavailable" do bundle "install" - FileUtils.rm(bundled_app("Gemfile.lock")) + FileUtils.rm(bundled_app_lock) break_git! ruby <<-R - require "#{lib_dir}/bundler" + require "bundler" begin Bundler.setup @@ -475,122 +478,125 @@ RSpec.describe "Bundler.setup" do end R - run "puts 'FAIL'" + run "puts 'FAIL'", raise_on_error: false 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, forgotten_command_line_options(:path => "vendor/bundle") - FileUtils.rm_rf vendored_gems("cache") - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install + FileUtils.rm_r default_cache_path + expect(the_bundle).to include_gems "myrack 1.0.0" end it "does not randomly change the path when specifying --path and the bundle directory becomes read only" do - bundle! :install, forgotten_command_line_options(:path => "vendor/bundle") + bundle_config "path vendor/bundle" + bundle :install - with_read_only("**/*") do - expect(the_bundle).to include_gems "rack 1.0.0" + with_read_only("#{bundled_app}/**/*") do + expect(the_bundle).to include_gems "myrack 1.0.0" end end it "finds git gem when default bundle path becomes read only" do + bundle_config "path .bundle" bundle "install" - with_read_only("#{Bundler.bundle_path}/**/*") do - expect(the_bundle).to include_gems "rack 1.0.0" + with_read_only("#{bundled_app(".bundle")}/**/*") do + expect(the_bundle).to include_gems "myrack 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" + build_git "myrack", "0.8" - FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - bundle %(config set local.rack #{lib_path("local-rack")}) - bundle! :install + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install - 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/) + FileUtils.rm_r(lib_path("local-myrack")) + run "require 'myrack'", raise_on_error: false + expect(err).to match(/Cannot use local override for myrack-0.8 because #{Regexp.escape(lib_path("local-myrack").to_s)} does not exist/) end it "explodes if branch is not given on runtime" do - build_git "rack", "0.8" + build_git "myrack", "0.8" - FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - bundle %(config set local.rack #{lib_path("local-rack")}) - bundle! :install + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}" G - run "require 'rack'" + run "require 'myrack'", raise_on_error: false 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" + build_git "myrack", "0.8" - FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - bundle %(config set local.rack #{lib_path("local-rack")}) - bundle! :install + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "changed" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "changed" G - run "require 'rack'" - expect(err).to match(/is using branch master but Gemfile specifies changed/) + run "require 'myrack'", raise_on_error: false + expect(err).to match(/is using branch main but Gemfile specifies changed/) end it "explodes on refs with different branches on runtime" do - build_git "rack", "0.8" + build_git "myrack", "0.8" - FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :ref => "master", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :ref => "main", :branch => "main" G gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :ref => "master", :branch => "nonexistant" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :ref => "main", :branch => "nonexistent" G - bundle %(config set local.rack #{lib_path("local-rack")}) - run "require 'rack'" - expect(err).to match(/is using branch master but Gemfile specifies nonexistant/) + bundle %(config set local.myrack #{lib_path("local-myrack")}) + run "require 'myrack'", raise_on_error: false + expect(err).to match(/is using branch main but Gemfile specifies nonexistent/) end end describe "when excluding groups" do it "doesn't change the resolve if --without is used" do - install_gemfile <<-G, forgotten_command_line_options(:without => :rails) - source "#{file_uri_for(gem_repo1)}" + bundle_config "without rails" + install_gemfile <<-G + source "https://gem.repo1" gem "activesupport" group :rails do @@ -598,14 +604,15 @@ RSpec.describe "Bundler.setup" do end G - install_gems "activesupport-2.3.5" + system_gems "activesupport-2.3.5" - expect(the_bundle).to include_gems "activesupport 2.3.2", :groups => :default + 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, forgotten_command_line_options(:without => :rails) - source "#{file_uri_for(gem_repo1)}" + bundle_config "without rails" + install_gemfile <<-G + source "https://gem.repo1" gem "activesupport" group :rails do @@ -613,18 +620,85 @@ RSpec.describe "Bundler.setup" do end G - install_gems "activesupport-2.3.5" + system_gems "activesupport-2.3.5" expect(the_bundle).to include_gems "activesupport 2.3.2" end + it "remembers --without and does not bail on bare Bundler.setup, even in the case of path gems no longer available" do + bundle_config "without development" + + path = bundled_app(File.join("vendor", "foo")) + build_lib "foo", path: path + + install_gemfile <<-G + source "https://gem.repo1" + gem "activesupport", "2.3.2" + gem 'foo', :path => 'vendor/foo', :group => :development + G + + FileUtils.rm_r(path) + + ruby "require 'bundler'; Bundler.setup", env: { "DEBUG" => "1" } + expect(out).to include("Assuming that source at `vendor/foo` has not changed since fetching its specs errored") + expect(out).to include("Found no changes, using resolution from the lockfile") + expect(err).to be_empty + end + + it "doesn't re-resolve when a pre-release bundler is used and a dependency includes a dependency on bundler" do + system_gems "bundler-9.99.9.beta1" + + build_repo4 do + build_gem "depends_on_bundler", "1.0" do |s| + s.add_dependency "bundler", ">= 1.5.0" + end + end + + install_gemfile <<~G + source "https://gem.repo4" + gem "depends_on_bundler" + G + + ruby "require '#{system_gem_path("gems/bundler-9.99.9.beta1/lib/bundler.rb")}'; Bundler.setup", env: { "DEBUG" => "1" } + expect(out).to include("Found no changes, using resolution from the lockfile") + expect(out).not_to include("lockfile does not have all gems needed for the current platform") + expect(err).to be_empty + end + + it "doesn't fail in frozen mode when bundler is a Gemfile dependency" do + install_gemfile <<~G + source "https://gem.repo4" + gem "bundler" + G + + bundle "install --verbose", env: { "BUNDLE_FROZEN" => "true" } + expect(err).to be_empty + end + + it "doesn't re-resolve when deleting dependencies" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "actionpack" + G + + install_gemfile <<-G, verbose: true + source "https://gem.repo1" + gem "myrack" + G + + expect(out).to include("Some dependencies were deleted, using a subset of the resolution from the lockfile") + expect(err).to be_empty + end + it "remembers --without and does not include groups passed to Bundler.setup" do - install_gemfile <<-G, forgotten_command_line_options(:without => :rails) - source "#{file_uri_for(gem_repo1)}" + bundle_config "without rails" + install_gemfile <<-G + source "https://gem.repo1" gem "activesupport" - group :rack do - gem "rack" + group :myrack do + gem "myrack" end group :rails do @@ -632,19 +706,19 @@ RSpec.describe "Bundler.setup" do 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 + expect(the_bundle).not_to include_gems "activesupport 2.3.2", groups: :myrack + expect(the_bundle).to include_gems "myrack 1.0.0", groups: :myrack 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 + build_git "no-gemspec", gemspec: false install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" gem "foo", :git => "#{lib_path("foo-1.0")}" gem "no-gemspec", "1.0", :git => "#{lib_path("no-gemspec-1.0")}" G @@ -658,38 +732,80 @@ RSpec.describe "Bundler.setup" do expect(out).to be_empty end + it "has gem_dir pointing to local repo" do + build_lib "foo", "1.0", path: bundled_app + + install_gemfile <<-G + source "https://gem.repo1" + gemspec + G + + run <<-R + puts Gem.loaded_specs['foo'].gem_dir + R + + expect(out).to eq(bundled_app.to_s) + end + it "does not load all gemspecs" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" G - run! <<-R - File.open(File.join(Gem.dir, "specifications", "broken.gemspec"), "w") do |f| + run <<-R + File.open(File.join(Gem.dir, "specifications", "invalid.gemspec"), "w") do |f| f.write <<-RUBY # -*- encoding: utf-8 -*- -# stub: broken 1.0.0 ruby lib +# stub: invalid 1.0.0 ruby lib Gem::Specification.new do |s| - s.name = "broken" + s.name = "invalid" s.version = "1.0.0" - raise "BROKEN GEMSPEC" + s.authors = ["Invalid Author"] + s.files = ["lib/invalid.rb"] + s.add_dependency "nonexistent-gem", "~> 999.999.999" + s.validate! end RUBY end R - run! <<-R - puts "WIN" + run <<-R + File.open(File.join(Gem.dir, "specifications", "invalid-ext.gemspec"), "w") do |f| + f.write <<-RUBY +# -*- encoding: utf-8 -*- +# stub: invalid-ext 1.0.0 ruby lib +# stub: a.ext\\0b.ext + +Gem::Specification.new do |s| + s.name = "invalid-ext" + s.version = "1.0.0" + s.authors = ["Invalid Author"] + s.files = ["lib/invalid.rb"] + s.required_ruby_version = "~> 0.8.0" + s.validate! +end + RUBY + end + # Need to write the gem.build_complete file, + # otherwise the full spec is loaded to check the installed_by_version + extensions_dir = Gem.default_ext_dir_for(Gem.dir) || File.join(Gem.dir, "extensions", Gem::Platform.local.to_s, Gem.extension_api_version) + Bundler::FileUtils.mkdir_p(File.join(extensions_dir, "invalid-ext-1.0.0")) + File.open(File.join(extensions_dir, "invalid-ext-1.0.0", "gem.build_complete"), "w") {} R - expect(out).to eq("WIN") + run <<-R + puts "Success" + R + + expect(out).to eq("Success") end it "ignores empty gem paths" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G ENV["GEM_HOME"] = "" @@ -698,41 +814,80 @@ end expect(err).to be_empty end - describe "$MANPATH" do - before do + it "can require rubygems without warnings, when using a local cache", :truffleruby do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle "package" + bundle %(exec ruby -w -e "require 'rubygems'") + + expect(err).to be_empty + end + + context "when the user has `MANPATH` set", :man do + before { ENV["MANPATH"] = "/foo#{File::PATH_SEPARATOR}" } + + it "adds the gem's man dir to the MANPATH" do build_repo4 do build_gem "with_man" do |s| s.write("man/man1/page.1", "MANPAGE") end end + + install_gemfile <<-G + source "https://gem.repo4" + gem "with_man" + G + + run "puts ENV['MANPATH']" + expect(out).to eq("#{default_bundle_path("gems/with_man-1.0/man")}#{File::PATH_SEPARATOR}/foo") end + end - context "when the user has one set" do - before { ENV["MANPATH"] = "/foo:" } + context "when the user does not have `MANPATH` set", :man do + before { ENV.delete("MANPATH") } - it "adds the gem's man dir to the MANPATH" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo4)}" - gem "with_man" - G + it "adds the gem's man dir to the MANPATH, leaving : in the end so that system man pages still work" do + build_repo4 do + build_gem "with_man" do |s| + s.write("man/man1/page.1", "MANPAGE") + end - run! "puts ENV['MANPATH']" - expect(out).to eq("#{default_bundle_path("gems/with_man-1.0/man")}:/foo") + build_gem "with_man_overriding_system_man" do |s| + s.write("man/man1/ls.1", "LS MANPAGE") + end end - end - context "when the user does not have one set" do - before { ENV.delete("MANPATH") } + install_gemfile <<-G + source "https://gem.repo4" + gem "with_man" + G - it "adds the gem's man dir to the MANPATH" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo4)}" - gem "with_man" - G + run <<~RUBY + puts ENV['MANPATH'] + require "open3" + puts Open3.capture2e("man", "ls")[1].success? + RUBY - run! "puts ENV['MANPATH']" - expect(out).to eq(default_bundle_path("gems/with_man-1.0/man").to_s) - end + expect(out).to eq("#{default_bundle_path("gems/with_man-1.0/man")}#{File::PATH_SEPARATOR}\ntrue") + + install_gemfile <<-G + source "https://gem.repo4" + gem "with_man_overriding_system_man" + G + + run <<~RUBY + puts ENV['MANPATH'] + require "open3" + puts Open3.capture2e({ "LC_ALL" => "C" }, "man", "ls")[0] + RUBY + + lines = out.split("\n") + + expect(lines).to include("#{default_bundle_path("gems/with_man_overriding_system_man-1.0/man")}#{File::PATH_SEPARATOR}") + expect(lines).to include("LS MANPAGE") end end @@ -746,7 +901,7 @@ end end install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" gem "requirepaths", :require => nil G @@ -754,27 +909,18 @@ end expect(out).to eq("yay") end - it "should clean $LOAD_PATH properly", :ruby_repo do + it "should clean $LOAD_PATH properly" do gem_name = "very_simple_binary" full_gem_name = gem_name + "-1.0" - ext_dir = File.join(tmp("extensions", full_gem_name)) - install_gems full_gem_name + system_gems full_gem_name install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://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 '#{lib_dir}/bundler' + require 'bundler' gem '#{gem_name}' puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} >= 2 @@ -789,21 +935,21 @@ end context "with bundler is located in symlinked GEM_HOME" do let(:gem_home) { Dir.mktmpdir } - let(:symlinked_gem_home) { Tempfile.new("gem_home").path } + let(:symlinked_gem_home) { tmp("gem_home-symlink").to_s } let(:full_name) { "bundler-#{Bundler::VERSION}" } before do - FileUtils.ln_sf(gem_home, symlinked_gem_home) + File.symlink(gem_home, symlinked_gem_home) gems_dir = File.join(gem_home, "gems") specifications_dir = File.join(gem_home, "specifications") Dir.mkdir(gems_dir) Dir.mkdir(specifications_dir) - FileUtils.ln_s(root, File.join(gems_dir, full_name)) + File.symlink(source_root, File.join(gems_dir, full_name)) gemspec_content = File.binread(gemspec). sub("Bundler::VERSION", %("#{Bundler::VERSION}")). - lines.reject {|line| line =~ %r{lib/bundler/version} }.join + lines.reject {|line| line.include?("lib/bundler/version") }.join File.open(File.join(specifications_dir, "#{full_name}.gemspec"), "wb") do |f| f.write(gemspec_content) @@ -811,11 +957,11 @@ end end it "should not remove itself from the LOAD_PATH and require a different copy of 'bundler/setup'" do - install_gemfile "" + install_gemfile "source 'https://gem.repo1'" - ruby <<-R, :env => { "GEM_PATH" => symlinked_gem_home }, :no_lib => true + ruby <<-R, env: { "GEM_PATH" => symlinked_gem_home } TracePoint.trace(:class) do |tp| - if tp.path.include?("bundler") && !tp.path.start_with?("#{root}") + if tp.path.include?("bundler") && !tp.path.start_with?("#{source_root}") puts "OMG. Defining a class from another bundler at \#{tp.path}:\#{tp.lineno}" end end @@ -828,64 +974,68 @@ end end it "does not reveal system gems even when Gem.refresh is called" do - system_gems "rack-1.0.0" + system_gems "myrack-1.0.0" install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "activesupport" G run <<-R - puts Bundler.rubygems.all_specs.map(&:name) + puts Bundler.rubygems.installed_specs.map(&:name) Gem.refresh - puts Bundler.rubygems.all_specs.map(&:name) + puts Bundler.rubygems.installed_specs.map(&:name) R expect(out).to eq("activesupport\nbundler\nactivesupport\nbundler") end describe "when a vendored gem specification uses the :path option" do + let(:filesystem_root) do + current = Pathname.new(Dir.pwd) + current = current.parent until current == current.parent + current + end + it "should resolve paths relative to the Gemfile" do path = bundled_app(File.join("vendor", "foo")) - build_lib "foo", :path => path + 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 + source "https://gem.repo1" 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 + run <<-R, env: { "BUNDLE_GEMFILE" => bundled_app_gemfile.to_s }, dir: bundled_app.parent + require 'foo' + R expect(err).to be_empty 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") + relative_path = File.join("vendor", Dir.pwd.gsub(/^#{filesystem_root}/, "")) absolute_path = bundled_app(relative_path) FileUtils.mkdir_p(absolute_path) - build_lib "foo", :path => 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 + source "https://gem.repo1" 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 + run <<-R, env: { "BUNDLE_GEMFILE" => bundled_app_gemfile.to_s }, dir: bundled_app.parent + require 'foo' + R expect(err).to be_empty end @@ -893,17 +1043,18 @@ end describe "with git gems that don't have gemspecs" do before :each do - build_git "no-gemspec", :gemspec => false + build_git "no_gemspec", gemspec: false install_gemfile <<-G - gem "no-gemspec", "1.0", :git => "#{lib_path("no-gemspec-1.0")}" + source "https://gem.repo1" + 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 + require 'no_gemspec' + puts NO_GEMSPEC R expect(out).to eq("1.0") @@ -912,10 +1063,10 @@ end describe "with bundled and system gems" do before :each do - system_gems "rack-1.0.0" + system_gems "myrack-1.0.0" install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" + source "https://gem.repo1" gem "activesupport", "2.3.5" G @@ -924,7 +1075,7 @@ end it "does not pull in system gems" do run <<-R begin; - require 'rack' + require 'myrack' rescue LoadError puts 'WIN' end @@ -946,13 +1097,13 @@ end it "raises an exception if gem is used to invoke a system gem not in the bundle" do run <<-R begin - gem 'rack' + gem 'myrack' rescue LoadError => e puts e.message end R - expect(out).to eq("rack is not part of the bundle. Add it to your Gemfile.") + expect(out).to eq("myrack is not part of the bundle. Add it to your Gemfile.") end it "sets GEM_HOME appropriately" do @@ -963,12 +1114,12 @@ end describe "with system gems in the bundle" do before :each do - bundle! "config set path.system true" - system_gems "rack-1.0.0" + bundle_config "path.system true" + system_gems "myrack-1.0.0" install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", "1.0.0" + source "https://gem.repo1" + gem "myrack", "1.0.0" gem "activesupport", "2.3.5" G end @@ -982,7 +1133,7 @@ end describe "with a gemspec that requires other files" do before :each do - build_git "bar", :gemspec => false do |s| + build_git "bar", gemspec: false do |s| s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0') s.write "bar.gemspec", <<-G require_relative 'lib/bar/version' @@ -998,6 +1149,7 @@ end end gemfile <<-G + source "https://gem.repo1" gem "bar", :git => "#{lib_path("bar-1.0")}" G end @@ -1009,14 +1161,15 @@ end end it "error intelligently if the gemspec has a LoadError" do - ref = update_git "bar", :gemspec => false do |s| + skip "whitespace issue?" if Gem.win_platform? + + ref = update_git "bar", gemspec: false do |s| s.write "bar.gemspec", "require 'foobarbaz'" end.ref_for("HEAD") - bundle :install + bundle :install, raise_on_error: false expect(err.lines.map(&:chomp)).to include( a_string_starting_with("[!] There was an error while loading `bar.gemspec`:"), - 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'" ) @@ -1026,21 +1179,24 @@ end bundle "install" ruby <<-RUBY - require '#{lib_dir}/bundler' + require 'bundler' + bundler_module = class << Bundler; self; end + bundler_module.send(:remove_method, :require) def Bundler.require(path) - raise "LOSE" + raise StandardError, "didn't use binding from top level" end Bundler.load RUBY expect(err).to be_empty - expect(out).to eq("") + expect(out).to be_empty end end describe "when Bundler is bundled" do it "doesn't blow up" do install_gemfile <<-G + source "https://gem.repo1" gem "bundler", :path => "#{root}" G @@ -1051,56 +1207,59 @@ end describe "when BUNDLED WITH" do def lock_with(bundler_version = nil) - lock = <<-L + lock = <<~L GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack + myrack L if bundler_version - lock += "\n BUNDLED WITH\n #{bundler_version}\n" + lock += "\nBUNDLED WITH\n #{bundler_version}\n" end lock end before do + bundle_config "path.system true" + install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G end context "is not present" do it "does not change the lock" do lockfile lock_with(nil) - ruby "require '#{lib_dir}/bundler/setup'" - lockfile_should_be lock_with(nil) + ruby "require 'bundler/setup'" + expect(lockfile).to eq 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 '#{lib_dir}/bundler/setup'" - expect(out).to eq("") - expect(err).to eq("") - lockfile_should_be lock_with(Bundler::VERSION.succ) + ruby "require 'bundler/setup'" + expect(out).to be_empty + expect(err).to be_empty + expect(lockfile).to eq lock_with(Bundler::VERSION.succ) end end context "is older" do it "does not change the lock" do + system_gems "bundler-1.10.1" lockfile lock_with("1.10.1") - ruby "require '#{lib_dir}/bundler/setup'" - lockfile_should_be lock_with("1.10.1") + ruby "require 'bundler/setup'" + expect(lockfile).to eq lock_with("1.10.1") end end end @@ -1109,27 +1268,32 @@ end let(:ruby_version) { nil } def lock_with(ruby_version = nil) - lock = <<-L + checksums = checksums_section do |c| + c.checksum gem_repo1, "myrack", "1.0.0" + end + + lock = <<~L GEM - remote: #{file_uri_for(gem_repo1)}/ + remote: https://gem.repo1/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES - rack + myrack + #{checksums} L if ruby_version - lock += "\n RUBY VERSION\n ruby #{ruby_version}\n" + lock += "\nRUBY VERSION\n ruby #{ruby_version}\n" end - lock += <<-L + lock += <<~L BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L lock @@ -1138,40 +1302,78 @@ end before do install_gemfile <<-G ruby ">= 0" - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G lockfile lock_with(ruby_version) end context "is not present" do - it "does not change the lock" do - expect { ruby! "require '#{lib_dir}/bundler/setup'" }.not_to change { lockfile } + # Skipped on ruby-core because `ruby "require 'bundler/setup'"` does not + # activate bundler as a gem there, so Source::Metadata falls back to a + # synthetic spec whose cache_file does not exist on disk and + # LockfileGenerator#bundler_checksum drops the bundler checksum, while + # the on-disk lockfile still has it. + it "does not change the lock", :ruby_repo 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 '#{lib_dir}/bundler/setup'" }.not_to change { lockfile } - expect(out).to eq("") - expect(err).to eq("") + expect { ruby "require 'bundler/setup'" }.not_to change { lockfile } + expect(out).to be_empty + expect(err).to be_empty end end context "is older" do let(:ruby_version) { "1.0.0" } it "does not change the lock" do - expect { ruby! "require '#{lib_dir}/bundler/setup'" }.not_to change { lockfile } + expect { ruby "require 'bundler/setup'" }.not_to change { lockfile } end end end describe "with gemified standard libraries" do + it "does not load Digest", :ruby_repo do + build_git "bar", gemspec: false do |s| + s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0') + s.write "bar.gemspec", <<-G + require_relative 'lib/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' + + s.add_dependency 'digest' + end + G + end + + gemfile <<-G + source "https://gem.repo1" + gem "bar", :git => "#{lib_path("bar-1.0")}" + G + + bundle :install, env: { "BUNDLE_LOCKFILE_CHECKSUMS" => "false" } + + ruby <<-RUBY, artifice: nil + require 'bundler/setup' + puts defined?(::Digest) ? "Digest defined" : "Digest undefined" + require 'digest' + RUBY + expect(out).to eq("Digest undefined") + end + it "does not load Psych" do - gemfile "" + gemfile "source 'https://gem.repo1'" ruby <<-RUBY - require '#{lib_dir}/bundler/setup' + require 'bundler/setup' puts defined?(Psych::VERSION) ? Psych::VERSION : "undefined" require 'psych' puts Psych::VERSION @@ -1182,9 +1384,9 @@ end end it "does not load openssl" do - install_gemfile! "" - ruby! <<-RUBY - require "#{lib_dir}/bundler/setup" + install_gemfile "source 'https://gem.repo1'" + ruby <<-RUBY, artifice: nil + require "bundler/setup" puts defined?(OpenSSL) || "undefined" require "openssl" puts defined?(OpenSSL) || "undefined" @@ -1192,29 +1394,73 @@ end expect(out).to eq("undefined\nconstant") end + it "does not load uri while reading gemspecs", rubygems: ">= 3.6.0.dev" do + Dir.mkdir bundled_app("test") + + create_file(bundled_app("test/test.gemspec"), <<-G) + Gem::Specification.new do |s| + s.name = "test" + s.version = "1.0.0" + s.summary = "test" + s.authors = ['John Doe'] + s.homepage = 'https://example.com' + end + G + + install_gemfile <<-G + source "https://gem.repo1" + gem "test", path: "#{bundled_app("test")}" + G + + ruby <<-RUBY, artifice: nil + require "bundler/setup" + puts defined?(URI) || "undefined" + require "uri" + puts defined?(URI) || "undefined" + RUBY + expect(out).to eq("undefined\nconstant") + end + + it "activates default gems when they are part of the bundle, but not installed explicitly", :ruby_repo do + default_delegate_version = ruby "gem 'delegate'; require 'delegate'; puts Delegator::VERSION" + + build_repo2 do + build_gem "delegate", default_delegate_version + end + + gemfile "source \"https://gem.repo2\"; gem 'delegate'" + + ruby <<-RUBY + require "bundler/setup" + require "delegate" + puts defined?(::Delegator) ? "Delegator defined" : "Delegator undefined" + RUBY + + expect(out).to eq("Delegator defined") + expect(err).to be_empty + end + describe "default gem activation" do let(:exemptions) do - if Gem::Version.new(Gem::VERSION) >= Gem::Version.new("2.7") - [] - else - %w[io-console openssl] - end << "bundler" + exempts = %w[did_you_mean bundler uri pathname] + exempts << "error_highlight" # added in Ruby 3.1 as a default gem + exempts << "ruby2_keywords" # added in Ruby 3.1 as a default gem + exempts << "syntax_suggest" # added in Ruby 3.2 as a default gem + exempts end - let(:activation_warning_hack) { strip_whitespace(<<-RUBY) } + let(:activation_warning_hack) { <<~RUBY } require #{spec_dir.join("support/hax").to_s.dump} - 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 + 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 RUBY @@ -1223,7 +1469,7 @@ end "-r#{bundled_app("activation_warning_hack.rb")} #{ENV["RUBYOPT"]}" end - let(:code) { strip_whitespace(<<-RUBY) } + let(:code) { <<~RUBY } require "pp" loaded_specs = Gem.loaded_specs.dup #{exemptions.inspect}.each {|s| loaded_specs.delete(s) } @@ -1237,120 +1483,228 @@ end RUBY it "activates no gems with -rbundler/setup" do - install_gemfile! "" - ruby! code, :env => { :RUBYOPT => activation_warning_hack_rubyopt + " -r#{lib_dir}/bundler/setup" } + install_gemfile "source 'https://gem.repo1'" + ruby code, env: { "RUBYOPT" => activation_warning_hack_rubyopt + " -rbundler/setup" }, artifice: nil expect(out).to eq("{}") end it "activates no gems with bundle exec" do - install_gemfile! "" + install_gemfile "source 'https://gem.repo1'" create_file("script.rb", code) - bundle! "exec ruby ./script.rb", :env => { :RUBYOPT => activation_warning_hack_rubyopt } + bundle "exec ruby ./script.rb", env: { "RUBYOPT" => activation_warning_hack_rubyopt } expect(out).to eq("{}") end it "activates no gems with bundle exec that is loaded" do - install_gemfile! "" + skip "not executable" if Gem.win_platform? + + install_gemfile "source 'https://gem.repo1'" create_file("script.rb", "#!/usr/bin/env ruby\n\n#{code}") FileUtils.chmod(0o777, bundled_app("script.rb")) - bundle! "exec ./script.rb", :artifice => nil, :env => { :RUBYOPT => activation_warning_hack_rubyopt } + bundle "exec ./script.rb", env: { "RUBYOPT" => activation_warning_hack_rubyopt } 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 + it "does not load net-http-pipeline too early" do build_repo4 do - default_gems.each do |g| - build_gem g, "999999" - end + build_gem "net-http-pipeline", "1.0.1" end - default_gems.reject! {|g| exemptions.include?(g) } + system_gems "net-http-pipeline-1.0.1", gem_repo: gem_repo4 - install_gemfile! <<-G - source "#{file_uri_for(gem_repo4)}" - #{default_gems}.each do |g| - gem g, "999999" - end + gemfile <<-G + source "https://gem.repo4" + gem "net-http-pipeline", "1.0.1" G - expect(the_bundle).to include_gems(*default_gems.map {|g| "#{g} 999999" }) + bundle_config "path vendor/bundle" + + bundle :install + + bundle :check + + expect(out).to eq("The Gemfile's dependencies are satisfied") end - it "activates older versions of default gems" do - build_repo4 do - default_gems.each do |g| - build_gem g, "0.0.0.a" + Gem::Specification.select(&:default_gem?).map(&:name).each do |g| + it "activates newer versions of #{g}", :ruby_repo do + skip if exemptions.include?(g) + + build_repo4 do + build_gem g, "999999" end + + install_gemfile <<-G + source "https://gem.repo4" + gem "#{g}", "999999" + G + + expect(the_bundle).to include_gem("#{g} 999999", env: { "RUBYOPT" => activation_warning_hack_rubyopt }, artifice: nil) end - default_gems.reject! {|g| exemptions.include?(g) } + it "activates older versions of #{g}", :ruby_repo do + skip if exemptions.include?(g) - install_gemfile! <<-G - source "#{file_uri_for(gem_repo4)}" - #{default_gems}.each do |g| - gem g, "0.0.0.a" + build_repo4 do + build_gem g, "0.0.0.a" end - G - expect(the_bundle).to include_gems(*default_gems.map {|g| "#{g} 0.0.0.a" }) + install_gemfile <<-G + source "https://gem.repo4" + gem "#{g}", "0.0.0.a" + G + + expect(the_bundle).to include_gem("#{g} 0.0.0.a", env: { "RUBYOPT" => activation_warning_hack_rubyopt }, artifice: nil) + end end end end describe "after setup" do - it "allows calling #gem on random objects", :bundler => "< 3" do + it "keeps Kernel#gem private" do install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - ruby! <<-RUBY - require "#{lib_dir}/bundler/setup" - Object.new.gem "rack" - puts Gem.loaded_specs["rack"].full_name + ruby <<-RUBY, raise_on_error: false + require "bundler/setup" + Object.new.gem "myrack" + puts "FAIL" RUBY - expect(out).to eq("rack-1.0.0") + expect(stdboth).not_to include "FAIL" + expect(err).to match(/private method [`']gem'/) end - it "keeps Kernel#gem private", :bundler => "3" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + it "keeps Kernel#require private" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" G - ruby <<-RUBY - require "#{lib_dir}/bundler/setup" - Object.new.gem "rack" + ruby <<-RUBY, raise_on_error: false + require "bundler/setup" + Object.new.require "myrack" puts "FAIL" RUBY - expect(last_command.stdboth).not_to include "FAIL" - expect(err).to include "private method `gem'" + expect(stdboth).not_to include "FAIL" + expect(err).to match(/private method [`']require'/) end - it "keeps Kernel#require private" do - install_gemfile! <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" + it "memoizes initial set of specs when requiring bundler/setup, so that even if further code mutates dependencies, Bundler.definition.specs is not affected" do + install_gemfile <<~G + source "https://gem.repo1" + gem "yard" + gem "myrack", :group => :test G + ruby <<-RUBY, raise_on_error: false + require "bundler/setup" + Bundler.require(:test).select! {|d| (d.groups & [:test]).any? } + puts Bundler.definition.specs.map(&:name).join(", ") + RUBY + + expect(out).to include("myrack, yard") + end + + it "does not cause double loads when higher versions of default gems are activated before bundler" do + build_repo2 do + build_gem "json", "999.999.999" do |s| + s.write "lib/json.rb", <<~RUBY + module JSON + VERSION = "999.999.999" + end + RUBY + end + end + + system_gems "json-999.999.999", gem_repo: gem_repo2 + + install_gemfile "source 'https://gem.repo1'" ruby <<-RUBY - require "#{lib_dir}/bundler/setup" - Object.new.require "rack" - puts "FAIL" + require "json" + require "bundler/setup" + require "json" RUBY - expect(last_command.stdboth).not_to include "FAIL" - expect(err).to include "private method `require'" + expect(err).to be_empty + end + end + + it "does not undo the Kernel.require decorations", rubygems: ">= 3.4.6" do + install_gemfile "source 'https://gem.repo1'" + script = bundled_app("bin/script") + create_file(script, <<~RUBY) + module Kernel + module_function + + alias_method :require_before_extra_monkeypatches, :require + + def require(path) + puts "requiring \#{path} used the monkeypatch" + + require_before_extra_monkeypatches(path) + end + end + + require "bundler/setup" + + require "foo" + RUBY + + sys_exec "#{Gem.ruby} #{script}", raise_on_error: false + expect(out).to include("requiring foo used the monkeypatch") + end + + it "performs an automatic bundle install" do + build_repo4 do + build_gem "myrack", "1.0.0" + end + + gemfile <<-G + source "https://gem.repo1" + gem "myrack", :group => :test + G + + bundle_config "auto_install 1" + + ruby <<-RUBY, artifice: "compact_index" + require 'bundler/setup' + RUBY + expect(err).to be_empty + expect(out).to include("Installing myrack 1.0.0") + end + + context "in a read-only filesystem" do + before do + gemfile <<-G + source "https://gem.repo4" + G + + lockfile <<-L + GEM + remote: https://gem.repo4/ + + PLATFORMS + x86_64-darwin-19 + + DEPENDENCIES + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "should fail loudly if the lockfile platforms don't include the current platform" do + simulate_platform "x86_64-linux" do + ruby <<-RUBY, raise_on_error: false, env: { "BUNDLER_SPEC_READ_ONLY" => "true", "BUNDLER_FORCE_TTY" => "true" } + require "bundler/setup" + RUBY + end + + expect(err).to include("Your lockfile is missing the current platform, but can't be updated because file system is read-only") end end end diff --git a/spec/bundler/runtime/with_unbundled_env_spec.rb b/spec/bundler/runtime/with_unbundled_env_spec.rb deleted file mode 100644 index 4aaf9d499c..0000000000 --- a/spec/bundler/runtime/with_unbundled_env_spec.rb +++ /dev/null @@ -1,270 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe "Bundler.with_env helpers" do - def bundle_exec_ruby!(code, options = {}) - build_bundler_context options - bundle! "exec '#{Gem.ruby}' -e #{code}", options - end - - def build_bundler_context(options = {}) - bundle "config set path vendor/bundle" - gemfile "" - bundle "install", options - end - - describe "Bundler.original_env" do - it "should return the PATH present before bundle was activated" do - code = "print Bundler.original_env['PATH']" - path = `getconf PATH`.strip + "#{File::PATH_SEPARATOR}/foo" - with_path_as(path) do - bundle_exec_ruby!(code.dump) - expect(last_command.stdboth).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 - bundle_exec_ruby!(code.dump) - expect(last_command.stdboth).to eq(gem_path) - end - end - - it "works with nested bundle exec invocations" 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(Gem.ruby, __FILE__, (count - 1).to_s) - RB - path = `getconf PATH`.strip + File::PATH_SEPARATOR + File.dirname(Gem.ruby) - with_path_as(path) do - build_bundler_context - bundle! "exec '#{Gem.ruby}' #{bundled_app("exe.rb")} 2" - end - expect(err).to eq <<-EOS.strip -2 false -1 true -0 true - EOS - end - - it "removes variables that bundler added", :ruby_repo do - # Simulate bundler has not yet been loaded - ENV.replace(ENV.to_hash.delete_if {|k, _v| k.start_with?(Bundler::EnvironmentPreserver::BUNDLER_PREFIX) }) - - original = ruby!('puts ENV.to_a.map {|e| e.join("=") }.sort.join("\n")') - code = 'puts Bundler.original_env.to_a.map {|e| e.join("=") }.sort.join("\n")' - bundle_exec_ruby! code.dump - expect(out).to eq original - end - end - - shared_examples_for "an unbundling helper" do - it "should delete BUNDLE_PATH" do - code = "print #{modified_env}.has_key?('BUNDLE_PATH')" - ENV["BUNDLE_PATH"] = "./foo" - bundle_exec_ruby! code.dump - expect(last_command.stdboth).to include "false" - end - - it "should remove '-rbundler/setup' from RUBYOPT" do - code = "print #{modified_env}['RUBYOPT']" - ENV["RUBYOPT"] = "-W2 -rbundler/setup #{ENV["RUBYOPT"]}" - bundle_exec_ruby! code.dump, :env => { "BUNDLER_SPEC_DISABLE_DEFAULT_BUNDLER_GEM" => "true" } - expect(last_command.stdboth).not_to include("-rbundler/setup") - end - - it "should restore RUBYLIB", :ruby_repo do - code = "print #{modified_env}['RUBYLIB']" - ENV["RUBYLIB"] = lib_dir.to_s + File::PATH_SEPARATOR + "/foo" - ENV["BUNDLER_ORIG_RUBYLIB"] = lib_dir.to_s + File::PATH_SEPARATOR + "/foo-original" - bundle_exec_ruby! code.dump - expect(last_command.stdboth).to include("/foo-original") - end - - it "should restore the original MANPATH" do - code = "print #{modified_env}['MANPATH']" - ENV["MANPATH"] = "/foo" - ENV["BUNDLER_ORIG_MANPATH"] = "/foo-original" - bundle_exec_ruby! code.dump - expect(last_command.stdboth).to include("/foo-original") - end - end - - describe "Bundler.unbundled_env" do - let(:modified_env) { "Bundler.unbundled_env" } - - it_behaves_like "an unbundling helper" - end - - describe "Bundler.clean_env", :bundler => 2 do - let(:modified_env) { "Bundler.clean_env" } - - it_behaves_like "an unbundling helper" - 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", :bundler => 2 do - it "should set ENV to unbundled_env in the block" do - expected = Bundler.unbundled_env - - actual = Bundler.ui.silence do - Bundler.with_clean_env { ENV.to_hash } - end - - expect(actual).to eq(expected) - end - - it "should restore the environment after execution" do - Bundler.ui.silence do - Bundler.with_clean_env { ENV["FOO"] = "hello" } - end - - expect(ENV).not_to have_key("FOO") - end - end - - describe "Bundler.with_unbundled_env" do - it "should set ENV to unbundled_env in the block" do - expected = Bundler.unbundled_env - actual = Bundler.with_unbundled_env { ENV.to_hash } - expect(actual).to eq(expected) - end - - it "should restore the environment after execution" do - Bundler.with_unbundled_env do - ENV["FOO"] = "hello" - end - - expect(ENV).not_to have_key("FOO") - end - end - - describe "Bundler.original_system" do - let(:code) do - <<~RUBY - Bundler.original_system(%([ "\$BUNDLE_FOO" = "bar" ] && exit 42)) - - exit $?.exitstatus - RUBY - end - - it "runs system inside with_original_env" do - system({ "BUNDLE_FOO" => "bar" }, "ruby -I#{lib_dir} -rbundler -e '#{code}'") - expect($?.exitstatus).to eq(42) - end - end - - describe "Bundler.clean_system", :bundler => 2 do - let(:code) do - <<~RUBY - Bundler.ui.silence { Bundler.clean_system(%([ "\$BUNDLE_FOO" = "bar" ] || exit 42)) } - - exit $?.exitstatus - RUBY - end - - it "runs system inside with_clean_env" do - system({ "BUNDLE_FOO" => "bar" }, "ruby -I#{lib_dir} -rbundler -e '#{code}'") - expect($?.exitstatus).to eq(42) - end - end - - describe "Bundler.unbundled_system" do - let(:code) do - <<~RUBY - Bundler.unbundled_system(%([ "\$BUNDLE_FOO" = "bar" ] || exit 42)) - - exit $?.exitstatus - RUBY - end - - it "runs system inside with_unbundled_env" do - system({ "BUNDLE_FOO" => "bar" }, "ruby -I#{lib_dir} -rbundler -e '#{code}'") - expect($?.exitstatus).to eq(42) - end - end - - describe "Bundler.original_exec" do - let(:code) do - <<~RUBY - Process.fork do - exit Bundler.original_exec(%(test "\$BUNDLE_FOO" = "bar")) - end - - _, status = Process.wait2 - - exit(status.exitstatus) - RUBY - end - - it "runs exec inside with_original_env" do - skip "Fork not implemented" if Gem.win_platform? - - system({ "BUNDLE_FOO" => "bar" }, "ruby -I#{lib_dir} -rbundler -e '#{code}'") - expect($?.exitstatus).to eq(0) - end - end - - describe "Bundler.clean_exec", :bundler => 2 do - let(:code) do - <<~RUBY - Process.fork do - exit Bundler.ui.silence { Bundler.clean_exec(%(test "\$BUNDLE_FOO" = "bar")) } - end - - _, status = Process.wait2 - - exit(status.exitstatus) - RUBY - end - - it "runs exec inside with_clean_env" do - skip "Fork not implemented" if Gem.win_platform? - - system({ "BUNDLE_FOO" => "bar" }, "ruby -I#{lib_dir} -rbundler -e '#{code}'") - expect($?.exitstatus).to eq(1) - end - end - - describe "Bundler.unbundled_exec" do - let(:code) do - <<~RUBY - Process.fork do - exit Bundler.unbundled_exec(%(test "\$BUNDLE_FOO" = "bar")) - end - - _, status = Process.wait2 - - exit(status.exitstatus) - RUBY - end - - it "runs exec inside with_clean_env" do - skip "Fork not implemented" if Gem.win_platform? - - system({ "BUNDLE_FOO" => "bar" }, "ruby -I#{lib_dir} -rbundler -e '#{code}'") - expect($?.exitstatus).to eq(1) - end - end -end diff --git a/spec/bundler/spec_helper.rb b/spec/bundler/spec_helper.rb index 9702ab71f5..27ddc6a771 100644 --- a/spec/bundler/spec_helper.rb +++ b/spec/bundler/spec_helper.rb @@ -1,32 +1,75 @@ # frozen_string_literal: true -require_relative "support/path" - -$:.unshift Spec::Path.spec_dir.to_s -$:.unshift Spec::Path.lib_dir.to_s - -require "bundler/psyched_yaml" +require "psych" require "bundler/vendored_fileutils" -require "uri" +require "bundler/vendored_uri" require "digest" 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 +# Bundler CLI will have different help text depending on whether any of these +# variables is set, since the `-e` flag `bundle gem` with require an explicit +# value if they are not set, but will use their value by default if set. So make +# sure they are `nil` before loading bundler to get a consistent help text, +# since some tests rely on that. +ENV["EDITOR"] = nil +ENV["VISUAL"] = nil +ENV["BUNDLER_EDITOR"] = nil require "bundler" -require "rspec" + +# If we use shared GEM_HOME and install multiple versions, it may cause +# unexpected test failures. +gem "diff-lcs", "< 2.0" + +require "rspec/core" +require "rspec/expectations" +require "rspec/mocks" +require "rspec/support/differ" +gem "rubygems-generate_index" +require "rubygems/indexer" require_relative "support/builders" +require_relative "support/checksums" require_relative "support/filters" require_relative "support/helpers" require_relative "support/indexes" require_relative "support/matchers" -require_relative "support/parallel" require_relative "support/permissions" require_relative "support/platforms" -require_relative "support/sometimes" -require_relative "support/sudo" +require_relative "support/shards" + +begin + raise LoadError if File.exist?(File.expand_path("../../lib/bundler/bundler.gemspec", __dir__)) + + gem "simplecov_json_formatter" + require "simplecov" + + SimpleCov.start do + command_name "bundler:#{Process.pid}" + root File.expand_path("../bundler", __dir__) + coverage_dir File.expand_path("../coverage", __dir__) + + add_filter "/spec/" + add_filter "/test/" + add_filter "/lib/rubygems/" + add_filter "/lib/bundler/vendor/" + add_filter "/tool/" + add_filter "/tmp/" + add_filter ".gemspec" + end + + SimpleCov.print_error_status = false + SimpleCov.at_exit do + $stdout = File.open(File::NULL, "w") + SimpleCov.result.format! + ensure + $stdout = STDOUT + end +rescue LoadError + # SimpleCov is not installed +end $debug = false @@ -38,17 +81,23 @@ end RSpec.configure do |config| config.include Spec::Builders + config.include Spec::Checksums config.include Spec::Helpers config.include Spec::Indexes config.include Spec::Matchers config.include Spec::Path config.include Spec::Platforms - config.include Spec::Sudo config.include Spec::Permissions + config.include Spec::Shards # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = ".rspec_status" + config.silence_filter_announcements = !ENV["TEST_ENV_NUMBER"].nil? + + config.backtrace_exclusion_patterns << + %r{./spec/(spec_helper\.rb|support/.+)} + config.disable_monkey_patching! # Since failures cause us to keep a bunch of long strings in memory, stop @@ -59,72 +108,85 @@ RSpec.configure do |config| config.bisect_runner = :shell - original_wd = Dir.pwd - original_env = ENV.to_hash - config.expect_with :rspec do |c| c.syntax = :expect + + c.max_formatted_output_length = 1000 end config.mock_with :rspec do |mocks| mocks.allow_message_expectations_on_nil = false end - config.around :each do |example| - if ENV["RUBY"] - orig_ruby = Gem.ruby - Gem.ruby = ENV["RUBY"] - end - example.run - Gem.ruby = orig_ruby if ENV["RUBY"] - end - config.before :suite do + Gem.ruby = ENV["RUBY"] if ENV["RUBY"] + require_relative "support/rubygems_ext" - Spec::Rubygems.setup - ENV["RUBYOPT"] = "#{ENV["RUBYOPT"]} -I#{Spec::Path.spec_dir}/rubygems -r#{Spec::Path.spec_dir}/support/hax.rb" - ENV["BUNDLE_SPEC_RUN"] = "true" + Spec::Rubygems.test_setup + + # Disable retry delays in tests to speed them up + Bundler::Retry.default_base_delay = 0 + + # Simulate bundler has not yet been loaded + ENV.replace(ENV.to_hash.delete_if {|k, _v| k.start_with?(Bundler::EnvironmentPreserver::BUNDLER_PREFIX) }) + + ENV["BUNDLER_SPEC_RUN"] = "true" ENV["BUNDLE_USER_CONFIG"] = ENV["BUNDLE_USER_CACHE"] = ENV["BUNDLE_USER_PLUGIN"] = nil + ENV["BUNDLE_APP_CONFIG"] = nil + ENV["BUNDLE_SILENCE_ROOT_WARNING"] = nil + ENV["RUBYGEMS_GEMDEPS"] = nil + ENV["XDG_CONFIG_HOME"] = nil + ENV["XDG_CACHE_HOME"] = nil ENV["GEMRC"] = nil + # Prevent tests from modifying the user's global git config. + # GIT_CONFIG_GLOBAL and GIT_CONFIG_NOSYSTEM are available since Git 2.32. + git_version = `git --version`[/(\d+\.\d+\.\d+)/, 1] + if Gem::Version.new(git_version) >= Gem::Version.new("2.32") + ENV["GIT_CONFIG_GLOBAL"] = File.join(ENV["HOME"], ".gitconfig") + ENV["GIT_CONFIG_NOSYSTEM"] = "1" + end + # Don't wrap output in tests ENV["THOR_COLUMNS"] = "10000" - original_env = ENV.to_hash + extend(Spec::Builders) - if ENV["RUBY"] - FileUtils.cp_r Spec::Path.bindir, File.join(Spec::Path.root, "lib", "exe") - end - end - - config.before :all do build_repo1 + + reset! end config.around :each do |example| - ENV.replace(original_env) - reset! - system_gems [] - in_app_root - @command_executions = [] - - Bundler.ui.silence { example.run } - - all_output = @command_executions.map(&:to_s_verbose).join("\n\n") - 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 + default_system_gems + + with_gem_path_as(system_gem_path) do + Bundler.ui.silence { example.run } + + all_output = all_commands_output + if example.exception && !all_output.empty? + message = all_output + "\n" + example.exception.message + (class << example.exception; self; end).send(:define_method, :message) do + message + end end end - - Dir.chdir(original_wd) + ensure + reset! end - config.after :suite do - if ENV["RUBY"] - FileUtils.rm_rf File.join(Spec::Path.root, "lib", "exe") + Spec::Shards::EXAMPLE_MAPPINGS.each do |tag, file_paths| + file_pattern = Regexp.union(file_paths.map {|path| Regexp.new(Regexp.escape(path) + "$") }) + + config.define_derived_metadata(file_path: file_pattern) do |metadata| + metadata[tag] = true end end + + config.before(:context) do |example| + metadata = example.class.metadata + if metadata[:type] != :aruba && !metadata[:realworld] && metadata.keys.none? {|k| Spec::Shards::EXAMPLE_MAPPINGS.keys.include?(k) } + warn "#{metadata[:file_path]} is not assigned to any shard. see spec/support/shards.rb for details." + end + end unless Spec::Path.ruby_core? end diff --git a/spec/bundler/support/activate.rb b/spec/bundler/support/activate.rb new file mode 100644 index 0000000000..143b77833d --- /dev/null +++ b/spec/bundler/support/activate.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "rubygems" +Gem.instance_variable_set(:@ruby, ENV["RUBY"]) if ENV["RUBY"] + +require_relative "path" +bundler_gemspec = Spec::Path.loaded_gemspec +bundler_gemspec.instance_variable_set(:@full_gem_path, Spec::Path.source_root.to_s) +bundler_gemspec.activate if bundler_gemspec.respond_to?(:activate) diff --git a/spec/bundler/support/artifice/compact_index.rb b/spec/bundler/support/artifice/compact_index.rb index 89362c4dbc..ebc4d0ae5b 100644 --- a/spec/bundler/support/artifice/compact_index.rb +++ b/spec/bundler/support/artifice/compact_index.rb @@ -1,118 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -$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(Bundler.rubygems.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 StandardError => e - puts e - puts e.backtrace - raise - end - - def not_modified?(checksum) - etags = parse_etags(request.env["HTTP_IF_NONE_MATCH"]) - - return unless etags.include?(checksum) - headers "ETag" => quote(checksum) - status 304 - body "" - end - - def requested_range_for(response_body) - ranges = Rack::Utils.byte_ranges(env, response_body.bytesize) - - if ranges - status 206 - body ranges.map! {|range| slice_body(response_body, range) }.join - else - status 200 - body response_body - end - end - - def quote(string) - %("#{string}") - end - - def parse_etags(value) - value ? value.split(/, ?/).select {|s| s.sub!(/"(.*)"/, '\1') } : [] - end - - def slice_body(body, range) - body.byteslice(range) - end - - def gems(gem_repo = GEM_REPO) - @gems ||= {} - @gems[gem_repo] ||= begin - specs = Bundler::Deprecate.skip_during do - %w[specs.4.8 prerelease_specs.4.8].map do |filename| - Marshal.load(File.open(gem_repo.join(filename)).read).map do |name, version, platform| - load_spec(name, version, platform, gem_repo) - end - end.flatten - end - - specs.group_by(&:name).map do |name, versions| - gem_versions = versions.map do |spec| - deps = spec.dependencies.select {|d| d.type == :runtime }.map do |d| - reqs = d.requirement.requirements.map {|r| r.join(" ") }.join(", ") - CompactIndex::Dependency.new(d.name, reqs) - end - checksum = begin - Digest(:SHA256).file("#{GEM_REPO}/gems/#{spec.original_name}.gem").base64digest - rescue StandardError - nil - end - CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, checksum, nil, - deps, spec.required_ruby_version, 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 +require_relative "helpers/compact_index" +require_relative "helpers/artifice" Artifice.activate_with(CompactIndexAPI) diff --git a/spec/bundler/support/artifice/compact_index_api_missing.rb b/spec/bundler/support/artifice/compact_index_api_missing.rb index fdd342bc08..f771f7d1f0 100644 --- a/spec/bundler/support/artifice/compact_index_api_missing.rb +++ b/spec/bundler/support/artifice/compact_index_api_missing.rb @@ -1,18 +1,13 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexApiMissing < CompactIndexAPI get "/fetch/actual/gem/:id" do - warn params[:id] - if params[:id] == "rack-1.0.gemspec.rz" - halt 404 - else - File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") - end + halt 404 end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexApiMissing) diff --git a/spec/bundler/support/artifice/compact_index_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_basic_authentication.rb index 775f1a3977..b9115cdd86 100644 --- a/spec/bundler/support/artifice/compact_index_basic_authentication.rb +++ b/spec/bundler/support/artifice/compact_index_basic_authentication.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexBasicAuthentication < CompactIndexAPI before do @@ -12,4 +10,6 @@ class CompactIndexBasicAuthentication < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexBasicAuthentication) diff --git a/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb index 1abe64236c..83b147d2ae 100644 --- a/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb +++ b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexChecksumMismatch < CompactIndexAPI get "/versions" do - headers "ETag" => quote("123") + headers "Repr-Digest" => "sha-256=:ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=:" headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" content_type "text/plain" - body "" + body "content does not match the checksum" end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexChecksumMismatch) diff --git a/spec/bundler/support/artifice/compact_index_concurrent_download.rb b/spec/bundler/support/artifice/compact_index_concurrent_download.rb index 7f989a3f37..5d55b8a72b 100644 --- a/spec/bundler/support/artifice/compact_index_concurrent_download.rb +++ b/spec/bundler/support/artifice/compact_index_concurrent_download.rb @@ -1,19 +1,18 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexConcurrentDownload < CompactIndexAPI get "/versions" do versions = File.join(Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions") - # Verify the original (empty) content hasn't been deleted, e.g. on a retry - File.read(versions) == "" || raise("Original file should be present and empty") + # Verify the original content hasn't been deleted, e.g. on a retry + data = File.binread(versions) + data == "created_at" || raise("Original file should be present with expected content") # Verify this is only requested once for a partial download - env["HTTP_RANGE"] || raise("Missing Range header for expected partial download") + env["HTTP_RANGE"] == "bytes=#{data.bytesize - 1}-" || raise("Missing Range header for expected partial download") # Overwrite the file in parallel, which should be then overwritten # after a successful download to prevent corruption @@ -21,7 +20,7 @@ class CompactIndexConcurrentDownload < CompactIndexAPI etag_response do file = tmp("versions.list") - file.delete if file.file? + FileUtils.rm_f(file) file = CompactIndex::VersionsFile.new(file.to_s) file.create(gems) file.contents @@ -29,4 +28,6 @@ class CompactIndexConcurrentDownload < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexConcurrentDownload) diff --git a/spec/bundler/support/artifice/compact_index_cooldown.rb b/spec/bundler/support/artifice/compact_index_cooldown.rb new file mode 100644 index 0000000000..85e3173c98 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_cooldown.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index_cooldown" +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexCooldownAPI) diff --git a/spec/bundler/support/artifice/compact_index_creds_diff_host.rb b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb index 6c3442e14b..282e9c8961 100644 --- a/spec/bundler/support/artifice/compact_index_creds_diff_host.rb +++ b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexCredsDiffHost < CompactIndexAPI helpers do @@ -26,14 +24,16 @@ class CompactIndexCredsDiffHost < CompactIndexAPI end get "/gems/:id" do - redirect "http://diffhost.com/no/creds/#{params[:id]}" + redirect "http://diffhost.test/no/creds/#{params[:id]}" end get "/no/creds/:id" do if request.host.include?("diffhost") && !auth.provided? - File.read("#{gem_repo1}/gems/#{params[:id]}") + File.binread("#{gem_repo1}/gems/#{params[:id]}") end end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexCredsDiffHost) diff --git a/spec/bundler/support/artifice/compact_index_etag_match.rb b/spec/bundler/support/artifice/compact_index_etag_match.rb new file mode 100644 index 0000000000..6c62166051 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_etag_match.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexEtagMatch < CompactIndexAPI + get "/versions" do + raise ArgumentError, "ETag header should be present" unless env["HTTP_IF_NONE_MATCH"] + headers "ETag" => env["HTTP_IF_NONE_MATCH"] + status 304 + body "" + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexEtagMatch) diff --git a/spec/bundler/support/artifice/compact_index_extra.rb b/spec/bundler/support/artifice/compact_index_extra.rb index 3a09afd06f..cd41b3ecca 100644 --- a/spec/bundler/support/artifice/compact_index_extra.rb +++ b/spec/bundler/support/artifice/compact_index_extra.rb @@ -1,37 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate - -class CompactIndexExtra < CompactIndexAPI - get "/extra/versions" do - halt 404 - end - - get "/extra/api/v1/dependencies" do - halt 404 - end - - get "/extra/specs.4.8.gz" do - File.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 +require_relative "helpers/compact_index_extra" +require_relative "helpers/artifice" Artifice.activate_with(CompactIndexExtra) diff --git a/spec/bundler/support/artifice/compact_index_extra_api.rb b/spec/bundler/support/artifice/compact_index_extra_api.rb index 3c716763c0..8b9d304ab4 100644 --- a/spec/bundler/support/artifice/compact_index_extra_api.rb +++ b/spec/bundler/support/artifice/compact_index_extra_api.rb @@ -1,52 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate - -class CompactIndexExtraApi < CompactIndexAPI - get "/extra/names" do - etag_response do - CompactIndex.names(gems(gem_repo4).map(&:name)) - end - end - - get "/extra/versions" do - etag_response do - file = tmp("versions.list") - 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 +require_relative "helpers/compact_index_extra_api" +require_relative "helpers/artifice" Artifice.activate_with(CompactIndexExtraApi) diff --git a/spec/bundler/support/artifice/compact_index_extra_api_missing.rb b/spec/bundler/support/artifice/compact_index_extra_api_missing.rb index 6bd24ddbb4..df6ede584c 100644 --- a/spec/bundler/support/artifice/compact_index_extra_api_missing.rb +++ b/spec/bundler/support/artifice/compact_index_extra_api_missing.rb @@ -1,17 +1,17 @@ # frozen_string_literal: true -require_relative "compact_index_extra_api" - -Artifice.deactivate +require_relative "helpers/compact_index_extra_api" class CompactIndexExtraAPIMissing < CompactIndexExtraApi get "/extra/fetch/actual/gem/:id" do if params[:id] == "missing-1.0.gemspec.rz" halt 404 else - File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") end end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexExtraAPIMissing) diff --git a/spec/bundler/support/artifice/compact_index_extra_missing.rb b/spec/bundler/support/artifice/compact_index_extra_missing.rb index 4758b785ac..255c89afdb 100644 --- a/spec/bundler/support/artifice/compact_index_extra_missing.rb +++ b/spec/bundler/support/artifice/compact_index_extra_missing.rb @@ -1,17 +1,17 @@ # frozen_string_literal: true -require_relative "compact_index_extra" - -Artifice.deactivate +require_relative "helpers/compact_index_extra" class CompactIndexExtraMissing < CompactIndexExtra get "/extra/fetch/actual/gem/:id" do if params[:id] == "missing-1.0.gemspec.rz" halt 404 else - File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") end end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexExtraMissing) diff --git a/spec/bundler/support/artifice/compact_index_forbidden.rb b/spec/bundler/support/artifice/compact_index_forbidden.rb index 3eebe0fbd8..18c30ed9a2 100644 --- a/spec/bundler/support/artifice/compact_index_forbidden.rb +++ b/spec/bundler/support/artifice/compact_index_forbidden.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexForbidden < CompactIndexAPI get "/versions" do @@ -10,4 +8,6 @@ class CompactIndexForbidden < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexForbidden) diff --git a/spec/bundler/support/artifice/compact_index_host_redirect.rb b/spec/bundler/support/artifice/compact_index_host_redirect.rb index 304c897d68..4f82bf3812 100644 --- a/spec/bundler/support/artifice/compact_index_host_redirect.rb +++ b/spec/bundler/support/artifice/compact_index_host_redirect.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexHostRedirect < CompactIndexAPI - get "/fetch/actual/gem/:id", :host_name => "localgemserver.test" do + get "/fetch/actual/gem/:id", host_name: "localgemserver.test" do redirect "http://bundler.localgemserver.test#{request.path_info}" end @@ -18,4 +16,6 @@ class CompactIndexHostRedirect < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexHostRedirect) diff --git a/spec/bundler/support/artifice/compact_index_mirror_down.rb b/spec/bundler/support/artifice/compact_index_mirror_down.rb new file mode 100644 index 0000000000..88983c715d --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_mirror_down.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" +require_relative "helpers/artifice" +require_relative "helpers/rack_request" + +module Artifice + module Net + class HTTPMirrorDown < HTTP + def connect + raise SocketError if address == "gem.mirror" + + super + end + end + + HTTP.endpoint = CompactIndexAPI + end + + replace_net_http(Net::HTTPMirrorDown) +end diff --git a/spec/bundler/support/artifice/compact_index_no_checksums.rb b/spec/bundler/support/artifice/compact_index_no_checksums.rb new file mode 100644 index 0000000000..ecb7fc7d7c --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_no_checksums.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexNoChecksums < CompactIndexAPI + get "/info/:name" do + etag_response do + gem = gems.find {|g| g.name == params[:name] } + gem.versions.map(&:number).join("\n") + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexNoChecksums) diff --git a/spec/bundler/support/artifice/compact_index_no_gem.rb b/spec/bundler/support/artifice/compact_index_no_gem.rb index 0a4be08a46..71f6629688 100644 --- a/spec/bundler/support/artifice/compact_index_no_gem.rb +++ b/spec/bundler/support/artifice/compact_index_no_gem.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexNoGem < CompactIndexAPI get "/gems/:id" do @@ -10,4 +8,6 @@ class CompactIndexNoGem < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexNoGem) diff --git a/spec/bundler/support/artifice/compact_index_partial_update.rb b/spec/bundler/support/artifice/compact_index_partial_update.rb index 6e7c05d423..f111d91ef9 100644 --- a/spec/bundler/support/artifice/compact_index_partial_update.rb +++ b/spec/bundler/support/artifice/compact_index_partial_update.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexPartialUpdate < CompactIndexAPI # Stub the server to never return 304s. This simulates the behaviour of @@ -18,21 +16,23 @@ class CompactIndexPartialUpdate < CompactIndexAPI ) # Verify a cached copy of the versions file exists - unless File.read(cached_versions_path).start_with?("created_at: ") + unless File.binread(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.") + unless env["HTTP_RANGE"] == "bytes=#{File.binread(cached_versions_path).bytesize - 1}-" + raise("Range header should be present, and start from the index of the final byte of the cache. #{env["HTTP_RANGE"].inspect}") end etag_response do # Return the exact contents of the cache. - File.read(cached_versions_path) + File.binread(cached_versions_path) end end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexPartialUpdate) diff --git a/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb b/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb new file mode 100644 index 0000000000..ac04336636 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +# The purpose of this Artifice is to test that an incremental response is invalidated +# and a second request is issued for the full content. +class CompactIndexPartialUpdateBadDigest < CompactIndexAPI + def partial_update_bad_digest + response_body = yield + if request.env["HTTP_RANGE"] + headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest("wrong digest on ranged request")}:" + else + headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest(response_body)}:" + end + headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" + content_type "text/plain" + requested_range_for(response_body) + end + + get "/versions" do + partial_update_bad_digest do + file = tmp("versions.list") + FileUtils.rm_f(file) + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + file.contents([], calculate_info_checksums: true) + end + end + + get "/info/:name" do + partial_update_bad_digest do + gem = gems.find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexPartialUpdateBadDigest) diff --git a/spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb b/spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb new file mode 100644 index 0000000000..99bae039f0 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +# The purpose of this Artifice is to test that an incremental response is ignored +# when the digest is not present to verify that the partial response is valid. +class CompactIndexPartialUpdateNoDigestNotIncremental < CompactIndexAPI + def partial_update_no_digest + response_body = yield + headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" + content_type "text/plain" + requested_range_for(response_body) + end + + get "/versions" do + partial_update_no_digest do + file = tmp("versions.list") + FileUtils.rm_f(file) + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + lines = file.contents([], calculate_info_checksums: true).split("\n") + name, versions, checksum = lines.last.split(" ") + + # shuffle versions so new versions are not appended to the end + [*lines[0..-2], [name, versions.split(",").reverse.join(","), checksum].join(" ")].join("\n") + end + end + + get "/info/:name" do + partial_update_no_digest do + gem = gems.find {|g| g.name == params[:name] } + lines = CompactIndex.info(gem ? gem.versions : []).split("\n") + + # shuffle versions so new versions are not appended to the end + [lines.first, lines.last, *lines[1..-2]].join("\n") + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexPartialUpdateNoDigestNotIncremental) diff --git a/spec/bundler/support/artifice/compact_index_precompiled_before.rb b/spec/bundler/support/artifice/compact_index_precompiled_before.rb new file mode 100644 index 0000000000..b5f72f546a --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_precompiled_before.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexPrecompiledBefore < CompactIndexAPI + get "/info/:name" do + etag_response do + gem = gems.find {|g| g.name == params[:name] } + move_ruby_variant_to_the_end(CompactIndex.info(gem ? gem.versions : [])) + end + end + + private + + def move_ruby_variant_to_the_end(response) + lines = response.split("\n") + ruby = lines.find {|line| /\A\d+\.\d+\.\d* \|/.match(line) } + lines.delete(ruby) + lines.push(ruby).join("\n") + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexPrecompiledBefore) diff --git a/spec/bundler/support/artifice/compact_index_range_ignored.rb b/spec/bundler/support/artifice/compact_index_range_ignored.rb new file mode 100644 index 0000000000..2303682c1f --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_range_ignored.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexRangeIgnored < CompactIndexAPI + # Stub the server to not return 304 so that we don't bypass all the logic + def not_modified?(_checksum) + false + end + + get "/versions" do + cached_versions_path = File.join( + Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions" + ) + + # Verify a cached copy of the versions file exists + unless File.binread(cached_versions_path).size > 0 + raise("Cached versions file should be present and have content") + end + + # Verify that a partial request is made, starting from the index of the + # final byte of the cached file. + unless env.delete("HTTP_RANGE") + raise("Expected client to write the full response on the first try") + end + + etag_response do + file = tmp("versions.list") + FileUtils.rm_f(file) + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + file.contents + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexRangeIgnored) diff --git a/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb b/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb index 788f9d6f99..8a7c4b79b0 100644 --- a/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb +++ b/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexRangeNotSatisfiable < CompactIndexAPI get "/versions" do @@ -11,7 +9,7 @@ class CompactIndexRangeNotSatisfiable < CompactIndexAPI else etag_response do file = tmp("versions.list") - file.delete if file.file? + FileUtils.rm_f(file) file = CompactIndex::VersionsFile.new(file.to_s) file.create(gems) file.contents @@ -31,4 +29,6 @@ class CompactIndexRangeNotSatisfiable < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexRangeNotSatisfiable) diff --git a/spec/bundler/support/artifice/compact_index_rate_limited.rb b/spec/bundler/support/artifice/compact_index_rate_limited.rb index ba17476045..4495491635 100644 --- a/spec/bundler/support/artifice/compact_index_rate_limited.rb +++ b/spec/bundler/support/artifice/compact_index_rate_limited.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexRateLimited < CompactIndexAPI class RequestCounter def self.queue - @queue ||= Queue.new + @queue ||= Thread::Queue.new end def self.size @@ -45,4 +43,6 @@ class CompactIndexRateLimited < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexRateLimited) diff --git a/spec/bundler/support/artifice/compact_index_redirects.rb b/spec/bundler/support/artifice/compact_index_redirects.rb index 99adc797bf..f7ba393239 100644 --- a/spec/bundler/support/artifice/compact_index_redirects.rb +++ b/spec/bundler/support/artifice/compact_index_redirects.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexRedirect < CompactIndexAPI get "/fetch/actual/gem/:id" do @@ -18,4 +16,6 @@ class CompactIndexRedirect < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexRedirect) diff --git a/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb index 7d427b5382..96259385e7 100644 --- a/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb +++ b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexStrictBasicAuthentication < CompactIndexAPI before do @@ -12,9 +10,11 @@ class CompactIndexStrictBasicAuthentication < CompactIndexAPI # Only accepts password == "password" unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz" - halt 403, "Authentication failed" + halt 401, "Authentication failed" end end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexStrictBasicAuthentication) diff --git a/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb index 036fac70b3..15850599b6 100644 --- a/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb +++ b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexWrongDependencies < CompactIndexAPI get "/info/:name" do @@ -14,4 +12,6 @@ class CompactIndexWrongDependencies < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexWrongDependencies) diff --git a/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb index 8add32b88f..9bd2ca0a9d 100644 --- a/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb +++ b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb @@ -1,15 +1,14 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexWrongGemChecksum < CompactIndexAPI get "/info/:name" do etag_response do name = params[:name] gem = gems.find {|g| g.name == name } - checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") { "ab" * 22 } + # This generates the hexdigest "2222222222222222222222222222222222222222222222222222222222222222" + checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") { "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI=" } versions = gem ? gem.versions : [] versions.each {|v| v.checksum = checksum } CompactIndex.info(versions) @@ -17,4 +16,6 @@ class CompactIndexWrongGemChecksum < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexWrongGemChecksum) diff --git a/spec/bundler/support/artifice/endpoint.rb b/spec/bundler/support/artifice/endpoint.rb index bf26c56503..15242a7942 100644 --- a/spec/bundler/support/artifice/endpoint.rb +++ b/spec/bundler/support/artifice/endpoint.rb @@ -1,100 +1,6 @@ # frozen_string_literal: true -require_relative "../path" -require Spec::Path.lib_dir.join("bundler/deprecate") -include Spec::Path - -$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gems.join("gems/{artifice,mustermann,rack,tilt,sinatra}-*/lib")].map(&:to_s)) - -require "artifice" -require "sinatra/base" - -ALL_REQUESTS = [] # rubocop:disable Style/MutableConstant -ALL_REQUESTS_MUTEX = Mutex.new - -at_exit do - if expected = ENV["BUNDLER_SPEC_ALL_REQUESTS"] - expected = expected.split("\n").sort - actual = ALL_REQUESTS.sort - - unless expected == actual - raise "Unexpected requests!\nExpected:\n\t#{expected.join("\n\t")}\n\nActual:\n\t#{actual.join("\n\t")}" - end - end -end - -class Endpoint < Sinatra::Base - def self.all_requests - @all_requests ||= [] - end - - GEM_REPO = Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"] || Spec::Path.gem_repo1) - set :raise_errors, true - set :show_exceptions, false - - def call!(*) - super.tap do - ALL_REQUESTS_MUTEX.synchronize do - ALL_REQUESTS << @request.url - end - end - end - - helpers do - def dependencies_for(gem_names, gem_repo = GEM_REPO) - return [] if gem_names.nil? || gem_names.empty? - - require "#{Spec::Path.lib_dir}/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(Bundler.rubygems.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 +require_relative "helpers/endpoint" +require_relative "helpers/artifice" Artifice.activate_with(Endpoint) diff --git a/spec/bundler/support/artifice/endpoint_500.rb b/spec/bundler/support/artifice/endpoint_500.rb index f98e7e3bc2..9dd373bbf6 100644 --- a/spec/bundler/support/artifice/endpoint_500.rb +++ b/spec/bundler/support/artifice/endpoint_500.rb @@ -1,19 +1,17 @@ # frozen_string_literal: true require_relative "../path" -include Spec::Path -$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gems.join("gems/{artifice,mustermann,rack,tilt,sinatra}-*/lib")].map(&:to_s)) +$LOAD_PATH.unshift(*Spec::Path.sinatra_dependency_paths) -require "artifice" require "sinatra/base" -Artifice.deactivate - class Endpoint500 < Sinatra::Base before do halt 500 end end +require_relative "helpers/artifice" + Artifice.activate_with(Endpoint500) diff --git a/spec/bundler/support/artifice/endpoint_api_forbidden.rb b/spec/bundler/support/artifice/endpoint_api_forbidden.rb index edc2463424..6bdc5896d6 100644 --- a/spec/bundler/support/artifice/endpoint_api_forbidden.rb +++ b/spec/bundler/support/artifice/endpoint_api_forbidden.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointApiForbidden < Endpoint get "/api/v1/dependencies" do @@ -10,4 +8,6 @@ class EndpointApiForbidden < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointApiForbidden) diff --git a/spec/bundler/support/artifice/endpoint_api_missing.rb b/spec/bundler/support/artifice/endpoint_api_missing.rb deleted file mode 100644 index 8dafde7362..0000000000 --- a/spec/bundler/support/artifice/endpoint_api_missing.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require_relative "endpoint" - -Artifice.deactivate - -class EndpointApiMissing < Endpoint - get "/fetch/actual/gem/:id" do - warn 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 index ff3d1493d6..e8e3569e63 100644 --- a/spec/bundler/support/artifice/endpoint_basic_authentication.rb +++ b/spec/bundler/support/artifice/endpoint_basic_authentication.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointBasicAuthentication < Endpoint before do @@ -12,4 +10,6 @@ class EndpointBasicAuthentication < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_creds_diff_host.rb b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb index f20ef74ac6..9cbb4de61a 100644 --- a/spec/bundler/support/artifice/endpoint_creds_diff_host.rb +++ b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointCredsDiffHost < Endpoint helpers do @@ -26,14 +24,16 @@ class EndpointCredsDiffHost < Endpoint end get "/gems/:id" do - redirect "http://diffhost.com/no/creds/#{params[:id]}" + redirect "http://diffhost.test/no/creds/#{params[:id]}" end get "/no/creds/:id" do if request.host.include?("diffhost") && !auth.provided? - File.read("#{gem_repo1}/gems/#{params[:id]}") + File.binread("#{gem_repo1}/gems/#{params[:id]}") end end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointCredsDiffHost) diff --git a/spec/bundler/support/artifice/endpoint_extra.rb b/spec/bundler/support/artifice/endpoint_extra.rb index 31f6822161..021fd435fe 100644 --- a/spec/bundler/support/artifice/endpoint_extra.rb +++ b/spec/bundler/support/artifice/endpoint_extra.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointExtra < Endpoint get "/extra/api/v1/dependencies" do @@ -10,11 +8,11 @@ class EndpointExtra < Endpoint end get "/extra/specs.4.8.gz" do - File.read("#{gem_repo2}/specs.4.8.gz") + File.binread("#{gem_repo2}/specs.4.8.gz") end get "/extra/prerelease_specs.4.8.gz" do - File.read("#{gem_repo2}/prerelease_specs.4.8.gz") + File.binread("#{gem_repo2}/prerelease_specs.4.8.gz") end get "/extra/quick/Marshal.4.8/:id" do @@ -22,12 +20,14 @@ class EndpointExtra < Endpoint end get "/extra/fetch/actual/gem/:id" do - File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") end get "/extra/gems/:id" do - File.read("#{gem_repo2}/gems/#{params[:id]}") + File.binread("#{gem_repo2}/gems/#{params[:id]}") end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointExtra) diff --git a/spec/bundler/support/artifice/endpoint_extra_api.rb b/spec/bundler/support/artifice/endpoint_extra_api.rb index 213b8e5895..a965af6e73 100644 --- a/spec/bundler/support/artifice/endpoint_extra_api.rb +++ b/spec/bundler/support/artifice/endpoint_extra_api.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointExtraApi < Endpoint get "/extra/api/v1/dependencies" do @@ -11,11 +9,11 @@ class EndpointExtraApi < Endpoint end get "/extra/specs.4.8.gz" do - File.read("#{gem_repo4}/specs.4.8.gz") + File.binread("#{gem_repo4}/specs.4.8.gz") end get "/extra/prerelease_specs.4.8.gz" do - File.read("#{gem_repo4}/prerelease_specs.4.8.gz") + File.binread("#{gem_repo4}/prerelease_specs.4.8.gz") end get "/extra/quick/Marshal.4.8/:id" do @@ -23,12 +21,14 @@ class EndpointExtraApi < Endpoint end get "/extra/fetch/actual/gem/:id" do - File.read("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}") + File.binread("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}") end get "/extra/gems/:id" do - File.read("#{gem_repo4}/gems/#{params[:id]}") + File.binread("#{gem_repo4}/gems/#{params[:id]}") end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointExtraApi) diff --git a/spec/bundler/support/artifice/endpoint_extra_missing.rb b/spec/bundler/support/artifice/endpoint_extra_missing.rb index ee129025ff..73e2defb32 100644 --- a/spec/bundler/support/artifice/endpoint_extra_missing.rb +++ b/spec/bundler/support/artifice/endpoint_extra_missing.rb @@ -1,17 +1,17 @@ # frozen_string_literal: true -require_relative "endpoint_extra" - -Artifice.deactivate +require_relative "helpers/endpoint_extra" class EndpointExtraMissing < EndpointExtra get "/extra/fetch/actual/gem/:id" do if params[:id] == "missing-1.0.gemspec.rz" halt 404 else - File.read("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") end end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointExtraMissing) diff --git a/spec/bundler/support/artifice/endpoint_fallback.rb b/spec/bundler/support/artifice/endpoint_fallback.rb index 08edf232e3..742e563f07 100644 --- a/spec/bundler/support/artifice/endpoint_fallback.rb +++ b/spec/bundler/support/artifice/endpoint_fallback.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointFallback < Endpoint DEPENDENCY_LIMIT = 60 @@ -16,4 +14,6 @@ class EndpointFallback < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointFallback) diff --git a/spec/bundler/support/artifice/endpoint_host_redirect.rb b/spec/bundler/support/artifice/endpoint_host_redirect.rb index 338cbcad00..6ce51bed93 100644 --- a/spec/bundler/support/artifice/endpoint_host_redirect.rb +++ b/spec/bundler/support/artifice/endpoint_host_redirect.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointHostRedirect < Endpoint - get "/fetch/actual/gem/:id", :host_name => "localgemserver.test" do + get "/fetch/actual/gem/:id", host_name: "localgemserver.test" do redirect "http://bundler.localgemserver.test#{request.path_info}" end @@ -14,4 +12,6 @@ class EndpointHostRedirect < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointHostRedirect) diff --git a/spec/bundler/support/artifice/endpoint_marshal_fail.rb b/spec/bundler/support/artifice/endpoint_marshal_fail.rb index 22c13e3e17..74ce321de6 100644 --- a/spec/bundler/support/artifice/endpoint_marshal_fail.rb +++ b/spec/bundler/support/artifice/endpoint_marshal_fail.rb @@ -1,13 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint_fallback" - -Artifice.deactivate - -class EndpointMarshalFail < EndpointFallback - get "/api/v1/dependencies" do - "f0283y01hasf" - end -end +require_relative "helpers/endpoint_marshal_fail" +require_relative "helpers/artifice" Artifice.activate_with(EndpointMarshalFail) diff --git a/spec/bundler/support/artifice/endopint_marshal_fail_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb index c341c3993f..ea4cfbe965 100644 --- a/spec/bundler/support/artifice/endopint_marshal_fail_basic_authentication.rb +++ b/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint_marshal_fail" - -Artifice.deactivate +require_relative "helpers/endpoint_marshal_fail" class EndpointMarshalFailBasicAuthentication < EndpointMarshalFail before do @@ -12,4 +10,6 @@ class EndpointMarshalFailBasicAuthentication < EndpointMarshalFail end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointMarshalFailBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_mirror_source.rb b/spec/bundler/support/artifice/endpoint_mirror_source.rb index 318866e420..fed7a746b9 100644 --- a/spec/bundler/support/artifice/endpoint_mirror_source.rb +++ b/spec/bundler/support/artifice/endpoint_mirror_source.rb @@ -1,15 +1,17 @@ # frozen_string_literal: true -require_relative "endpoint" +require_relative "helpers/endpoint" class EndpointMirrorSource < Endpoint get "/gems/:id" do - if request.env["HTTP_X_GEMFILE_SOURCE"] == "https://server.example.org/" - File.read("#{gem_repo1}/gems/#{params[:id]}") + if request.env["HTTP_X_GEMFILE_SOURCE"] == "https://server.example.org/" && request.env["HTTP_USER_AGENT"].start_with?("bundler") + File.binread("#{gem_repo1}/gems/#{params[:id]}") else halt 500 end end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointMirrorSource) diff --git a/spec/bundler/support/artifice/endpoint_redirect.rb b/spec/bundler/support/artifice/endpoint_redirect.rb index ee97fccf64..84f546ba9d 100644 --- a/spec/bundler/support/artifice/endpoint_redirect.rb +++ b/spec/bundler/support/artifice/endpoint_redirect.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointRedirect < Endpoint get "/fetch/actual/gem/:id" do @@ -14,4 +12,6 @@ class EndpointRedirect < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointRedirect) diff --git a/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb index 4d4da08770..dff360c5c5 100644 --- a/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb +++ b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointStrictBasicAuthentication < Endpoint before do @@ -12,9 +10,11 @@ class EndpointStrictBasicAuthentication < Endpoint # Only accepts password == "password" unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz" - halt 403, "Authentication failed" + halt 401, "Authentication failed" end end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointStrictBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_timeout.rb b/spec/bundler/support/artifice/endpoint_timeout.rb index c118da1893..86b793e499 100644 --- a/spec/bundler/support/artifice/endpoint_timeout.rb +++ b/spec/bundler/support/artifice/endpoint_timeout.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint_fallback" - -Artifice.deactivate +require_relative "helpers/endpoint_fallback" class EndpointTimeout < EndpointFallback SLEEP_TIMEOUT = 3 @@ -12,4 +10,6 @@ class EndpointTimeout < EndpointFallback end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointTimeout) diff --git a/spec/bundler/support/artifice/fail.rb b/spec/bundler/support/artifice/fail.rb index 1059c6df4e..5ddbc4e590 100644 --- a/spec/bundler/support/artifice/fail.rb +++ b/spec/bundler/support/artifice/fail.rb @@ -1,20 +1,11 @@ # 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 +require_relative "../vendored_net_http" -class Fail < Net::HTTP - # Net::HTTP uses a @newimpl instance variable to decide whether +class Fail < Gem::Net::HTTP + # Gem::Net::HTTP uses a @newimpl instance variable to decide whether # to use a legacy implementation. Since we are subclassing - # Net::HTTP, we must set it + # Gem::Net::HTTP, we must set it @newimpl = true def request(req, body = nil, &block) @@ -26,14 +17,11 @@ class Fail < Net::HTTP end def exception(req) - name = ENV.fetch("BUNDLER_SPEC_EXCEPTION") { "Errno::ENETUNREACH" } - const = name.split("::").reduce(Object) {|mod, sym| mod.const_get(sym) } - const.new("host down: Bundler spec artifice fail! #{req["PATH_INFO"]}") + Errno::ENETUNREACH.new("host down: Bundler spec artifice fail! #{req["PATH_INFO"]}") end end -# Replace Net::HTTP with our failing subclass -::Net.class_eval do - remove_const(:HTTP) - const_set(:HTTP, ::Fail) -end +require_relative "helpers/artifice" + +# Replace Gem::Net::HTTP with our failing subclass +Artifice.replace_net_http(::Fail) diff --git a/spec/bundler/support/artifice/helpers/artifice.rb b/spec/bundler/support/artifice/helpers/artifice.rb new file mode 100644 index 0000000000..788268295c --- /dev/null +++ b/spec/bundler/support/artifice/helpers/artifice.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# This module was initially borrowed from https://github.com/wycats/artifice +module Artifice + # Activate Artifice with a particular Rack endpoint. + # + # Calling this method will replace the Gem::Net::HTTP system + # with a replacement that routes all requests to the + # Rack endpoint. + # + # @param [#call] endpoint A valid Rack endpoint + def self.activate_with(endpoint) + require_relative "rack_request" + + Net::HTTP.endpoint = endpoint + replace_net_http(Artifice::Net::HTTP) + end + + # Deactivate the Artifice replacement. + def self.deactivate + replace_net_http(::Gem::Net::HTTP) + end + + def self.replace_net_http(value) + ::Gem::Net.class_eval do + remove_const(:HTTP) + const_set(:HTTP, value) + end + end +end diff --git a/spec/bundler/support/artifice/helpers/compact_index.rb b/spec/bundler/support/artifice/helpers/compact_index.rb new file mode 100644 index 0000000000..e684aa8628 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/compact_index.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require_relative "endpoint" + +$LOAD_PATH.unshift Spec::Path.tmp_root.join("compact_index/lib").to_s +require "compact_index" +require "digest" + +class CompactIndexAPI < Endpoint + helpers do + include Spec::Path + + def load_spec(name, version, platform, gem_repo) + full_name = "#{name}-#{version}" + full_name += "-#{platform}" if platform != "ruby" + Marshal.load(Bundler.rubygems.inflate(File.binread(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")))) + end + + def etag_response + response_body = yield + etag = Digest::MD5.hexdigest(response_body) + headers "ETag" => quote(etag) + return if not_modified?(etag) + headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest(response_body)}:" + headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" + content_type "text/plain" + requested_range_for(response_body) + rescue StandardError => e + puts e + puts e.backtrace + raise + end + + def not_modified?(etag) + etags = parse_etags(request.env["HTTP_IF_NONE_MATCH"]) + + return unless etags.include?(etag) + status 304 + body "" + end + + def requested_range_for(response_body) + ranges = Rack::Utils.get_byte_ranges(env["HTTP_RANGE"], response_body.bytesize) + + if ranges + status 206 + body ranges.map! {|range| slice_body(response_body, range) }.join + else + status 200 + body response_body + end + end + + def quote(string) + %("#{string}") + end + + def parse_etags(value) + value ? value.split(/, ?/).select {|s| s.sub!(/"(.*)"/, '\1') } : [] + end + + def slice_body(body, range) + body.byteslice(range) + end + + def gems(gem_repo = default_gem_repo) + @gems ||= {} + @gems[gem_repo] ||= begin + specs = Bundler::Deprecate.skip_during do + %w[specs.4.8 prerelease_specs.4.8].flat_map do |filename| + spec_index = gem_repo.join(filename) + next [] unless File.exist?(spec_index) + + Marshal.load(File.binread(spec_index)).map do |name, version, platform| + load_spec(name, version, platform, gem_repo) + end + end + end + + specs.group_by(&:name).map do |name, versions| + gem_versions = versions.map do |spec| + deps = spec.runtime_dependencies.map do |d| + reqs = d.requirement.requirements.map {|r| r.join(" ") }.join(", ") + CompactIndex::Dependency.new(d.name, reqs) + end + begin + checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") do + Digest(:SHA256).file("#{gem_repo}/gems/#{spec.original_name}.gem").hexdigest + end + rescue StandardError + checksum = nil + end + build_gem_version(spec, deps, checksum) + end + CompactIndex::Gem.new(name, gem_versions) + end + end + end + + def build_gem_version(spec, deps, checksum) + CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, checksum, nil, + deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s) + end + end + + get "/names" do + etag_response do + CompactIndex.names(gems.map(&:name)) + end + end + + get "/versions" do + etag_response do + file = tmp("versions.list") + FileUtils.rm_f(file) + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + file.contents + end + end + + get "/info/:name" do + etag_response do + gem = gems.find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end +end diff --git a/spec/bundler/support/artifice/helpers/compact_index_cooldown.rb b/spec/bundler/support/artifice/helpers/compact_index_cooldown.rb new file mode 100644 index 0000000000..9920fd2c95 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/compact_index_cooldown.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "compact_index" + +class CompactIndexCooldownAPI < CompactIndexAPI + helpers do + def build_gem_version(spec, deps, checksum) + created_at = spec.date&.utc&.iso8601 + CompactIndex::GemVersionV2.new(spec.version.version, spec.platform.to_s, checksum, nil, + deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s, created_at) + end + end +end diff --git a/spec/bundler/support/artifice/helpers/compact_index_extra.rb b/spec/bundler/support/artifice/helpers/compact_index_extra.rb new file mode 100644 index 0000000000..9e742630dd --- /dev/null +++ b/spec/bundler/support/artifice/helpers/compact_index_extra.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative "compact_index" + +class CompactIndexExtra < CompactIndexAPI + get "/extra/versions" do + halt 404 + end + + get "/extra/api/v1/dependencies" do + halt 404 + end + + get "/extra/specs.4.8.gz" do + File.binread("#{gem_repo2}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.binread("#{gem_repo2}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.binread("#{gem_repo2}/gems/#{params[:id]}") + end +end diff --git a/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb b/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb new file mode 100644 index 0000000000..d9a7d83d23 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require_relative "compact_index" + +class CompactIndexExtraApi < CompactIndexAPI + get "/extra/names" do + etag_response do + CompactIndex.names(gems(gem_repo4).map(&:name)) + end + end + + get "/extra/versions" do + etag_response do + file = tmp("versions.list") + FileUtils.rm_f(file) + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems(gem_repo4)) + file.contents + end + end + + get "/extra/info/:name" do + etag_response do + gem = gems(gem_repo4).find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end + + get "/extra/specs.4.8.gz" do + File.binread("#{gem_repo4}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.binread("#{gem_repo4}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.binread("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.binread("#{gem_repo4}/gems/#{params[:id]}") + end +end diff --git a/spec/bundler/support/artifice/helpers/endpoint.rb b/spec/bundler/support/artifice/helpers/endpoint.rb new file mode 100644 index 0000000000..9590611dfe --- /dev/null +++ b/spec/bundler/support/artifice/helpers/endpoint.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require_relative "../../path" + +$LOAD_PATH.unshift(*Spec::Path.sinatra_dependency_paths) + +require "sinatra/base" + +ALL_REQUESTS = [] # rubocop:disable Style/MutableConstant +ALL_REQUESTS_MUTEX = Thread::Mutex.new + +at_exit do + if expected = ENV["BUNDLER_SPEC_ALL_REQUESTS"] + expected = expected.split("\n").sort + actual = ALL_REQUESTS.sort + + unless expected == actual + raise "Unexpected requests!\nExpected:\n\t#{expected.join("\n\t")}\n\nActual:\n\t#{actual.join("\n\t")}" + end + end +end + +class Endpoint < Sinatra::Base + def self.all_requests + @all_requests ||= [] + end + + set :raise_errors, true + set :show_exceptions, false + set :host_authorization, permitted_hosts: [".example.org", ".local", ".mirror", ".repo", ".repo1", ".repo2", ".repo3", ".repo4", ".rubygems.org", ".security", ".source", ".test", "127.0.0.1"] + + def call!(*) + super.tap do + ALL_REQUESTS_MUTEX.synchronize do + ALL_REQUESTS << @request.url + end + end + end + + helpers do + include Spec::Path + + def default_gem_repo + if ENV["BUNDLER_SPEC_GEM_REPO"] + Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"]) + else + case request.host + when "gem.repo1" + Spec::Path.gem_repo1 + when "gem.repo2" + Spec::Path.gem_repo2 + when "gem.repo3" + Spec::Path.gem_repo3 + when "gem.repo4" + Spec::Path.gem_repo4 + else + Spec::Path.gem_repo1 + end + end + end + + def dependencies_for(gem_names, gem_repo = default_gem_repo) + return [] if gem_names.nil? || gem_names.empty? + + all_specs = %w[specs.4.8 prerelease_specs.4.8].map do |filename| + Marshal.load(File.binread(gem_repo.join(filename))) + end.inject(:+) + + all_specs.filter_map do |name, version, platform| + spec = load_spec(name, version, platform, gem_repo) + next unless gem_names.include?(spec.name) + { + name: spec.name, + number: spec.version.version, + platform: spec.platform.to_s, + dependencies: spec.runtime_dependencies.map do |dep| + [dep.name, dep.requirement.requirements.map {|a| a.join(" ") }.join(", ")] + end, + } + end + end + + def load_spec(name, version, platform, gem_repo) + full_name = "#{name}-#{version}" + full_name += "-#{platform}" if platform != "ruby" + Marshal.load(Bundler.rubygems.inflate(File.binread(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")))) + end + end + + get "/quick/Marshal.4.8/:id" do + redirect "/fetch/actual/gem/#{params[:id]}" + end + + get "/fetch/actual/gem/:id" do + File.binread("#{default_gem_repo}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/gems/:id" do + File.binread("#{default_gem_repo}/gems/#{params[:id]}") + end + + get "/api/v1/dependencies" do + Marshal.dump(dependencies_for(params[:gems])) + end + + get "/specs.4.8.gz" do + File.binread("#{default_gem_repo}/specs.4.8.gz") + end + + get "/prerelease_specs.4.8.gz" do + File.binread("#{default_gem_repo}/prerelease_specs.4.8.gz") + end +end diff --git a/spec/bundler/support/artifice/helpers/endpoint_extra.rb b/spec/bundler/support/artifice/helpers/endpoint_extra.rb new file mode 100644 index 0000000000..ad08495b50 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/endpoint_extra.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative "endpoint" + +class EndpointExtra < Endpoint + get "/extra/api/v1/dependencies" do + halt 404 + end + + get "/extra/specs.4.8.gz" do + File.binread("#{gem_repo2}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.binread("#{gem_repo2}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.binread("#{gem_repo2}/gems/#{params[:id]}") + end +end diff --git a/spec/bundler/support/artifice/helpers/endpoint_fallback.rb b/spec/bundler/support/artifice/helpers/endpoint_fallback.rb new file mode 100644 index 0000000000..a232930b67 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/endpoint_fallback.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative "endpoint" + +class EndpointFallback < Endpoint + DEPENDENCY_LIMIT = 60 + + get "/api/v1/dependencies" do + if params[:gems] && params[:gems].size <= DEPENDENCY_LIMIT + Marshal.dump(dependencies_for(params[:gems])) + else + halt 413, "Too many gems to resolve, please request less than #{DEPENDENCY_LIMIT} gems" + end + end +end diff --git a/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb b/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb new file mode 100644 index 0000000000..c409d39d99 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require_relative "endpoint_fallback" + +class EndpointMarshalFail < EndpointFallback + get "/api/v1/dependencies" do + "f0283y01hasf" + end +end diff --git a/spec/bundler/support/artifice/helpers/rack_request.rb b/spec/bundler/support/artifice/helpers/rack_request.rb new file mode 100644 index 0000000000..05ff034463 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/rack_request.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require "rack/test" +require_relative "../../vendored_net_http" + +module Artifice + module Net + # This is an internal object that can receive Rack requests + # to the application using the Rack::Test API + class RackRequest + include Rack::Test::Methods + attr_reader :app + + def initialize(app) + @app = app + end + end + + class HTTP < ::Gem::Net::HTTP + class << self + attr_accessor :endpoint + end + + # Gem::Net::HTTP uses a @newimpl instance variable to decide whether + # to use a legacy implementation. Since we are subclassing + # Gem::Net::HTTP, we must set it + @newimpl = true + + # We don't need to connect, so blank out this method + def connect + end + + # Replace the Gem::Net::HTTP request method with a method + # that converts the request into a Rack request and + # dispatches it to the Rack endpoint. + # + # @param [Net::HTTPRequest] req A Gem::Net::HTTPRequest + # object, or one if its subclasses + # @param [optional, String, #read] body This should + # be sent as "rack.input". If it's a String, it will + # be converted to a StringIO. + # @return [Net::HTTPResponse] + # + # @yield [Net::HTTPResponse] If a block is provided, + # this method will yield the Gem::Net::HTTPResponse to + # it after the body is read. + def request(req, body = nil, &block) + rack_request = RackRequest.new(self.class.endpoint) + + req.each_header do |header, value| + rack_request.header(header, value) + end + + scheme = use_ssl? ? "https" : "http" + prefix = "#{scheme}://#{addr_port}" + body_stream_contents = req.body_stream.read if req.body_stream + + response = rack_request.request("#{prefix}#{req.path}", + { method: req.method, input: body || req.body || body_stream_contents }) + + make_net_http_response(response, &block) + end + + private + + # This method takes a Rack response and creates a Gem::Net::HTTPResponse + # Instead of trying to mock HTTPResponse directly, we just convert + # the Rack response into a String that looks like a normal HTTP + # response and call Gem::Net::HTTPResponse.read_new + # + # @param [Array(#to_i, Hash, #each)] response a Rack response + # @return [Net::HTTPResponse] + # @yield [Net::HTTPResponse] If a block is provided, yield the + # response to it after the body is read + def make_net_http_response(response) + status = response.status + headers = response.headers + body = response.body + + response_string = [] + response_string << "HTTP/1.1 #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}" + + headers.each do |header, value| + response_string << "#{header}: #{value}" + end + + response_string << "" << body + + response_io = ::Gem::Net::BufferedIO.new(StringIO.new(response_string.join("\n"))) + res = ::Gem::Net::HTTPResponse.read_new(response_io) + + res.reading_body(response_io, true) do + yield res if block_given? + end + + res + end + end + end +end diff --git a/spec/bundler/support/artifice/vcr.rb b/spec/bundler/support/artifice/vcr.rb index a46f8e9391..0bf5ade8f6 100644 --- a/spec/bundler/support/artifice/vcr.rb +++ b/spec/bundler/support/artifice/vcr.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true -require "net/http" +require_relative "../vendored_net_http" require_relative "../path" -CASSETTE_PATH = "#{Spec::Path.spec_dir}/support/artifice/vcr_cassettes" +CASSETTE_PATH = "#{Spec::Path.spec_dir}/support/artifice/vcr_cassettes".freeze +USED_CASSETTES_PATH = "#{Spec::Path.spec_dir}/support/artifice/used_cassettes.txt".freeze CASSETTE_NAME = ENV.fetch("BUNDLER_SPEC_VCR_CASSETTE_NAME") { "realworld" } -class BundlerVCRHTTP < Net::HTTP +class BundlerVCRHTTP < Gem::Net::HTTP class RequestHandler attr_reader :http, :request, :body, :response_block def initialize(http, request, body = nil, &response_block) @@ -22,6 +23,10 @@ class BundlerVCRHTTP < Net::HTTP @__vcr_request_handler = handler end + File.open(USED_CASSETTES_PATH, "a+") do |f| + f.puts request_pair_paths.map {|path| Pathname.new(path).relative_path_from(Spec::Path.git_root).to_s }.join("\n") + end + if recorded_response? recorded_response else @@ -31,19 +36,18 @@ class BundlerVCRHTTP < Net::HTTP def recorded_response? return true if ENV["BUNDLER_SPEC_PRE_RECORDED"] - return false if ENV["BUNDLER_SPEC_FORCE_RECORD"] request_pair_paths.all? {|f| File.exist?(f) } end def recorded_response File.open(request_pair_paths.last, "rb:ASCII-8BIT") do |response_file| - response_io = ::Net::BufferedIO.new(response_file) - ::Net::HTTPResponse.read_new(response_io).tap do |response| + response_io = ::Gem::Net::BufferedIO.new(response_file) + ::Gem::Net::HTTPResponse.read_new(response_io).tap do |response| response.decode_content = request.decode_content if request.respond_to?(:decode_content) - response.uri = request.uri if request.respond_to?(:uri) + response.uri = request.uri response.reading_body(response_io, request.response_body_permitted?) do - response_block.call(response) if response_block + response_block&.call(response) end end end @@ -57,6 +61,7 @@ class BundlerVCRHTTP < Net::HTTP response = http.request_without_vcr(request, body, &response_block) @recording = false unless @recording + require "fileutils" FileUtils.mkdir_p(File.dirname(request_path)) binwrite(request_path, request_to_string(request)) binwrite(response_path, response_to_string(response)) @@ -69,30 +74,13 @@ class BundlerVCRHTTP < Net::HTTP end def file_name_for_key(key) - key.join("/").gsub(/[\:*?"<>|]/, "-") + File.join(*key).gsub(/[\:*?"<>|]/, "-") end def request_pair_paths %w[request response].map do |kind| - File.join(CASSETTE_PATH, CASSETTE_NAME, file_name_for_key(key + [kind])) - end - end - - def read_stored_request(path) - contents = File.read(path) - headers = {} - method = nil - path = nil - contents.lines.grep(/^> /).each do |line| - if line =~ /^> (GET|HEAD|POST|PATCH|PUT|DELETE) (.*)/ - method = $1 - path = $2.strip - elsif line =~ /^> (.*?): (.*)/ - headers[$1] = $2 - end + File.join(CASSETTE_PATH, CASSETTE_NAME, file_name_for_key(key), kind) end - body = contents =~ /^([^>].*)/m && $1 - Net::HTTP.const_get(method.capitalize).new(path, headers).tap {|r| r.body = body if body } end def request_to_string(request) @@ -133,6 +121,19 @@ class BundlerVCRHTTP < Net::HTTP end end + def start_with_vcr + if ENV["BUNDLER_SPEC_PRE_RECORDED"] + raise IOError, "HTTP session already opened" if @started + @socket = nil + @started = true + else + start_without_vcr + end + end + + alias_method :start_without_vcr, :start + alias_method :start, :start_with_vcr + def request_with_vcr(request, *args, &block) handler = request.instance_eval do remove_instance_variable(:@__vcr_request_handler) if defined?(@__vcr_request_handler) @@ -145,8 +146,7 @@ class BundlerVCRHTTP < Net::HTTP alias_method :request, :request_with_vcr end -# Replace Net::HTTP with our VCR subclass -::Net.class_eval do - remove_const(:HTTP) - const_set(:HTTP, BundlerVCRHTTP) -end +require_relative "helpers/artifice" + +# Replace Gem::Net::HTTP with our VCR subclass +Artifice.replace_net_http(BundlerVCRHTTP) diff --git a/spec/bundler/support/artifice/windows.rb b/spec/bundler/support/artifice/windows.rb index ce7455b86c..3056540beb 100644 --- a/spec/bundler/support/artifice/windows.rb +++ b/spec/bundler/support/artifice/windows.rb @@ -1,21 +1,17 @@ # frozen_string_literal: true require_relative "../path" -include Spec::Path -$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gems.join("gems/{artifice,mustermann,rack,tilt,sinatra}-*/lib")].map(&:to_s)) +$LOAD_PATH.unshift(*Spec::Path.sinatra_dependency_paths) -require "artifice" require "sinatra/base" -Artifice.deactivate - class Windows < Sinatra::Base set :raise_errors, true set :show_exceptions, false helpers do - def gem_repo + def default_gem_repo Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"] || Spec::Path.gem_repo1) end end @@ -27,7 +23,7 @@ class Windows < Sinatra::Base files.each do |file| get "/#{file}" do - File.read gem_repo.join(file) + File.binread default_gem_repo.join(file) end end @@ -44,4 +40,6 @@ class Windows < Sinatra::Base end end +require_relative "helpers/artifice" + Artifice.activate_with(Windows) diff --git a/spec/bundler/support/build_metadata.rb b/spec/bundler/support/build_metadata.rb new file mode 100644 index 0000000000..2eade4137b --- /dev/null +++ b/spec/bundler/support/build_metadata.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require_relative "path" +require_relative "helpers" + +module Spec + module BuildMetadata + include Spec::Path + include Spec::Helpers + + def write_build_metadata(dir: source_root, version: Bundler::VERSION) + build_metadata = { + git_commit_sha: git_commit_sha, + built_at: release_date_for(version, dir: dir), + } + + replace_build_metadata(build_metadata, dir: dir) + end + + def reset_build_metadata(dir: source_root) + build_metadata = { + built_at: nil, + } + + replace_build_metadata(build_metadata, dir: dir) + end + + private + + def replace_build_metadata(build_metadata, dir:) + build_metadata_file = File.expand_path("lib/bundler/build_metadata.rb", dir) + + ivars = build_metadata.sort.map do |k, v| + " @#{k} = #{loaded_gemspec.send(:ruby_code, v)}" + end.join("\n") + + contents = File.read(build_metadata_file) + contents.sub!(/^(\s+# begin ivars).+(^\s+# end ivars)/m, "\\1\n#{ivars}\n\\2") + File.open(build_metadata_file, "w") {|f| f << contents } + end + + def git_commit_sha + ruby_core_tarball? ? "unknown" : git("rev-parse --short HEAD", source_root).strip + end + + def release_date_for(version, dir:) + changelog = File.expand_path("CHANGELOG.md", dir) + File.readlines(changelog)[2].scan(/^## #{Regexp.escape(version)} \((.*)\)/).first&.first if File.exist?(changelog) + end + + extend self + end +end diff --git a/spec/bundler/support/builders.rb b/spec/bundler/support/builders.rb index c7f299487c..43ab7e053d 100644 --- a/spec/bundler/support/builders.rb +++ b/spec/bundler/support/builders.rb @@ -2,9 +2,18 @@ require "bundler/shared_helpers" require "shellwords" +require "fileutils" +require "rubygems/package" + +require_relative "build_metadata" module Spec module Builders + def self.extended(mod) + mod.extend Path + mod.extend Helpers + end + def self.constantize(name) name.delete("-").upcase end @@ -19,28 +28,35 @@ module Spec 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" + FileUtils.cp rake_path, "#{gem_repo1}/gems/" + + build_gem "coffee-script-source" + build_gem "git" + build_gem "puma" + build_gem "minitest" + + build_gem "myrack", %w[0.9.1 1.0.0] do |s| + s.executables = "myrackup" + s.post_install_message = "Myrack's post install message" end build_gem "thin" do |s| - s.add_dependency "rack" + s.add_dependency "myrack" 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" + build_gem "myrack-obama" do |s| + s.add_dependency "myrack" + s.post_install_message = "Myrack-obama's post install message" end - build_gem "rack_middleware", "1.0" do |s| - s.add_dependency "rack", "0.9.1" + build_gem "myrack_middleware", "1.0" do |s| + s.add_dependency "myrack", "0.9.1" end build_gem "rails", "2.3.2" do |s| s.executables = "rails" - s.add_dependency "rake", "12.3.2" + s.add_dependency "rake", rake_version s.add_dependency "actionpack", "2.3.2" s.add_dependency "activerecord", "2.3.2" s.add_dependency "actionmailer", "2.3.2" @@ -64,40 +80,28 @@ module Spec 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| + 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}'" + build_gem "myrack-test", no_default: true do |s| + s.write "lib/myrack/test.rb", "MYRACK_TEST = '1.0'" 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 = "x64-mswin64" end build_gem "platform_specific" do |s| @@ -105,39 +109,37 @@ module Spec end build_gem "platform_specific" do |s| - s.platform = "x64-mingw32" + s.platform = "x64-mingw-ucrt" + end + + build_gem "platform_specific" do |s| + s.platform = "aarch64-mingw-ucrt" 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" @@ -146,55 +148,12 @@ module Spec 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 "simple_binary", &:add_c_extension build_gem "bundler", "0.9" do |s| s.executables = "bundle" - s.write "bin/bundle", "puts 'FAIL'" + s.write "bin/bundle", "#!/usr/bin/env ruby\nputs 'FAIL'" end # The bundler 0.8 gem has a rubygems plugin that always loads :( @@ -203,10 +162,6 @@ module Spec 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 @@ -216,131 +171,54 @@ module Spec 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? + def build_repo2(**kwargs, &blk) + FileUtils.cp_r gem_repo1, gem_repo2, remove_destination: true + update_repo2(**kwargs, &blk) if block_given? end - def build_repo3 - build_repo gem_repo3 do - build_gem "rack" + # A repo that has no pre-installed gems included. (The caller completely + # determines the contents with the block.) + # + # If the repo already exists, `#update_repo` will be called. + def build_repo3(**kwargs, &blk) + if File.exist?(gem_repo3) + update_repo(gem_repo3, &blk) + else + build_repo gem_repo3, **kwargs, &blk 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) + # Like build_repo3, this is a repo that has no pre-installed gems included. + # + # If the repo already exists, `#udpate_repo` will be called + def build_repo4(**kwargs, &blk) + if File.exist?(gem_repo4) + update_repo gem_repo4, &blk + else + build_repo gem_repo4, **kwargs, &blk + end end - def update_repo4(&blk) - update_repo(gem_repo4, &blk) + def update_repo2(**kwargs, &blk) + update_repo(gem_repo2, **kwargs, &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 + def update_repo3(&blk) + update_repo(gem_repo3, &blk) end def build_security_repo build_repo security_repo do - build_gem "rack" + build_gem "myrack" build_gem "signed_gem" do |s| cert = "signing-cert.pem" @@ -353,37 +231,60 @@ module Spec end end - def build_repo(path, &blk) - return if File.directory?(path) - rake_path = Dir["#{Path.base_system_gems}/**/rake*.gem"].first + # A minimal fake irb console + def build_dummy_irb(version = "9.9.9") + build_gem "irb", version do |s| + s.write "lib/irb.rb", <<-RUBY + class IRB + class << self + def toplevel_binding + unless defined?(@toplevel_binding) && @toplevel_binding + TOPLEVEL_BINDING.eval %{ + def self.__irb__; binding; end + IRB.instance_variable_set(:@toplevel_binding, __irb__) + class << self; undef __irb__; end + } + end + @toplevel_binding.eval('private') + @toplevel_binding + end - if rake_path.nil? - Spec::Path.base_system_gems.rmtree - Spec::Rubygems.setup - rake_path = Dir["#{Path.base_system_gems}/**/rake*.gem"].first + def __irb__ + 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 __irb__ + end + end + RUBY end + 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 + def build_repo(path, **kwargs, &blk) + return if File.directory?(path) + + FileUtils.mkdir_p("#{path}/gems") - update_repo(path, &blk) + update_repo(path,**kwargs, &blk) end - def update_repo(path) - if path == gem_repo1 && caller.first.split(" ").last == "`build_repo`" + def update_repo(path, build_compact_index: true) + exempted_caller = Gem.ruby_version >= Gem::Version.new("3.4.0.dev") && RUBY_ENGINE != "jruby" ? "#{Module.nesting.first}#build_repo" : "build_repo" + if path == gem_repo1 && caller_locations(1, 1).first.label != exempted_caller 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 + options = { build_compact: build_compact_index } + Gem::Indexer.new(path, options).generate_index ensure @_build_path = nil @_build_repo = nil @@ -408,14 +309,14 @@ module Spec end end - def build_dep(name, requirements = Gem::Requirement.default, type = :runtime) - Bundler::Dependency.new(name, :version => requirements) - end - def build_lib(name, *args, &blk) build_with(LibBuilder, name, args, &blk) end + def build_bundler(*args, &blk) + build_with(BundlerBuilder, "bundler", args, &blk) + end + def build_gem(name, *args, &blk) build_with(GemBuilder, name, args, &blk) end @@ -424,20 +325,20 @@ module Spec 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)) + GitReader.new(self, 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)) + GitReader.new(self, opts[:path] || lib_path(spec.full_name)) end def build_plugin(name, *args, &blk) build_with(PluginBuilder, name, args, &blk) end - private + private def build_with(builder, name, args, &blk) @_build_path ||= nil @@ -451,7 +352,6 @@ module Spec 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 @@ -522,6 +422,58 @@ module Spec alias_method :dep, :runtime end + class BundlerBuilder + def initialize(context, name, version) + @context = context + @spec = Spec::Path.loaded_gemspec.dup + @spec.version = version || Bundler::VERSION + end + + def required_ruby_version + @spec.required_ruby_version + end + + def required_ruby_version=(x) + @spec.required_ruby_version = x + end + + def _build(options = {}) + full_name = "bundler-#{@spec.version}" + build_path = (options[:build_path] || @context.tmp) + full_name + bundler_path = build_path + "#{full_name}.gem" + + FileUtils.mkdir_p build_path + + @context.shipped_files.each do |shipped_file| + target_shipped_file = shipped_file + target_shipped_file = shipped_file.sub(/\Alibexec/, "exe") if @context.ruby_core? + target_shipped_file = build_path + target_shipped_file + target_shipped_dir = File.dirname(target_shipped_file) + FileUtils.mkdir_p target_shipped_dir unless File.directory?(target_shipped_dir) + FileUtils.cp File.expand_path(shipped_file, @context.source_root), target_shipped_file, preserve: true + end + + @context.replace_version_file(@spec.version, dir: build_path) + @context.replace_changelog(@spec.version, dir: build_path) if options[:released] + + Spec::BuildMetadata.write_build_metadata(dir: build_path, version: @spec.version.to_s) + + Dir.chdir build_path do + Gem::DefaultUserInteraction.use_ui(Gem::SilentUI.new) do + Gem::Package.build(@spec) + end + end + + if block_given? + yield(bundler_path) + else + FileUtils.mv bundler_path, options[:path] + end + ensure + FileUtils.rm_rf build_path + end + end + class LibBuilder def initialize(context, name, version) @context = context @@ -535,6 +487,7 @@ module Spec s.email = "foo@bar.baz" s.homepage = "http://example.com" s.license = "MIT" + s.required_ruby_version = ">= 3.0" end @files = {} end @@ -551,25 +504,17 @@ module Spec @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 + shebang = "#!/usr/bin/env ruby\n" @spec.files << executable write executable, "#{shebang}require_relative '../lib/#{@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 = "#{name}_c" if extra_lib_dir = with_config("ext-lib") # add extra libpath if --with-ext-lib is @@ -583,7 +528,7 @@ module Spec write "ext/#{name}.c", <<-C #include "ruby.h" - void Init_#{name}_c() { + void Init_#{name}_c(void) { rb_define_module("#{Builders.constantize(name)}_IN_C"); } C @@ -594,11 +539,20 @@ module Spec if options[:rubygems_version] @spec.rubygems_version = options[:rubygems_version] - def @spec.mark_version; end def @spec.validate(*); end end + unless options[:no_default] + gem_source = options[:source] || "path@#{path}" + @files = _default_files. + merge("lib/#{entrypoint}/source.rb" => "#{Builders.constantize(name)}_SOURCE = #{gem_source.to_s.dump}"). + merge(@files) + end + + @spec.authors = ["no one"] + @spec.files += @files.keys + case options[:gemspec] when false # do nothing @@ -608,141 +562,118 @@ module Spec @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 } + full_path = Pathname.new(path).join(file) + FileUtils.mkdir_p(full_path.dirname) + File.open(full_path, "w") {|f| f.puts source } + FileUtils.chmod("+x", full_path) if @spec.executables.map {|exe| "#{@spec.bindir}/#{exe}" }.include?(file) 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 + @_default_files ||= { "lib/#{entrypoint}.rb" => "#{Builders.constantize(name)} = '#{version}#{platform_string}'" } + end + + def entrypoint + name.tr("-", "/") end def _default_path @context.tmp("libs", @spec.full_name) end + + def platform_string + " #{@spec.platform}" unless @spec.platform == Gem::Platform::RUBY + end end class GitBuilder < LibBuilder def _build(options) + default_branch = options[:default_branch] || "main" 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 config commit.gpgsign false` - `git commit -m "OMG INITIAL COMMIT"` - end + super(options.merge(path: path, source: source)) + @context.git("config --global init.defaultBranch #{default_branch}", path) + @context.git("init", path) + @context.git("add *", path) + @context.git("config user.email lol@wut.com", path) + @context.git("config user.name lolwut", path) + @context.git("config commit.gpgsign false", path) + @context.git("commit -m OMG_INITIAL_COMMIT", path) 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 + super(options.merge(path: path)) + @context.git("init --bare", path) 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 #{options[:remote]}") - elsif options[:push] - silently("git push origin #{options[:push]}") - end + if branch = options[:branch] + @context.git("checkout -b #{Shellwords.shellescape(branch)}", libpath) + elsif tag = options[:tag] + @context.git("tag #{Shellwords.shellescape(tag)}", libpath) + elsif options[:remote] + @context.git("remote add origin #{options[:remote]}", libpath) + elsif options[:push] + @context.git("push origin #{options[:push]}", libpath) + 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"` + current_ref = @context.git("rev-parse HEAD", libpath).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)) + @context.git("commit -am BUMP", libpath) end end class GitReader - attr_reader :path + attr_reader :context, :path - def initialize(path) + def initialize(context, path) + @context = context @path = path end def ref_for(ref, len = nil) - ref = git "rev-parse #{ref}" + ref = context.git "rev-parse #{ref}", path 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])) + lib_path = opts[:lib_path] || @context.tmp(".tmp/#{@spec.full_name}") + lib_path = super(opts.merge(path: lib_path, no_default: opts[:no_default])) destination = opts[:path] || _default_path - Dir.chdir(lib_path) do - FileUtils.mkdir_p(destination) + FileUtils.mkdir_p(lib_path.join(destination)) - @spec.authors = ["that guy"] if !@spec.authors || @spec.authors.empty? - - Bundler.rubygems.build(@spec, opts[:skip_validation]) + if [:yaml, false].include?(opts[:gemspec]) + Dir.chdir(lib_path) do + Bundler.rubygems.build(@spec, opts[:skip_validation]) + end + elsif opts[:skip_validation] + Dir.chdir(lib_path) { Gem::Package.build(@spec, true) } + else + Dir.chdir(lib_path) { Gem::Package.build(@spec) } end + gem_path = File.expand_path("#{@spec.full_name}.gem", lib_path) if opts[:to_system] - @context.system_gems gem_path, :keep_path => true + @context.system_gems gem_path, default: opts[:default] elsif opts[:to_bundle] - @context.system_gems gem_path, :path => :bundle_path, :keep_path => true + @context.system_gems gem_path, path: @context.default_bundle_path else FileUtils.mv(gem_path, destination) end @@ -755,60 +686,63 @@ module Spec class PluginBuilder < GemBuilder def _default_files - @_default_files ||= super.merge("plugins.rb" => "") + @_default_files ||= { + "lib/#{name}.rb" => "#{Builders.constantize(name)} = '#{version}#{platform_string}'", + "plugins.rb" => "", + } end end - TEST_CERT = <<-CERT.gsub(/^\s*/, "") + TEST_CERT = <<~CERT -----BEGIN CERTIFICATE----- - MIIDMjCCAhqgAwIBAgIBATANBgkqhkiG9w0BAQUFADAnMQwwCgYDVQQDDAN5b3Ux + MIIDNTCCAh2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAnMQwwCgYDVQQDDAN5b3Ux 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 + ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMkupYkg3Nd1oXM3fo0d + mVJBWNrni88lKDuIIQXwcKe6XCgiloZG708ecLTOws9+o9MkTl9Wtpf/WGXT98NK + EPUYakd2Fv1SuD1jWYlP7iDR6hB3RkWBm5ziujYftVJ4ZrPD42PLjDASvlh75Tvr + MeM7yq/qkcgNsd9dQyUvMNPks3tla9je7Dt7Auli2IN3CNXys7gIOfwJH0Bb/M6t + y7oUfpoUKAfLzwe61abztgDu1lSNgdFBM1kcxYflyh/FkX5TlAcWeAXzLrnxAXGR + UxXrxW4oPC+kZi/pDRBd7X4zQDx7bCmr1+FsS3M05i3w5E08Tt9iKRk4V8nCmE4i + k6UCAwEAAaNsMGowCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYE + FOOOFw5TNAqt/TcRRZEU3Dg/58XuMBYGA1UdEQQPMA2BC3lvdUBleGFtcGxlMBYG + A1UdEgQPMA2BC3lvdUBleGFtcGxlMA0GCSqGSIb3DQEBCwUAA4IBAQAy3xnmobxU + 1SyhHvoIXTJmG0wt1DQ/Dqwjy362LpEf1UHt29wtg1Mph58eVtl93z5Vd2t4/O77 + E2BHpSu9ujc6/Br4+2uA/Qk/xRyLBtZAwty6J4uFvOOg985HonN+RCUZbKSUTmtA + TZvNtIDAZFQ8Tu75K4gIBxDcz7biGi4i1VJ3F3GNCNeossr9IQwKvb+UWFq14U5R + IzUnGgMIzcjUG2kKQvddRD1CjS+egtcLvShbOfm5bs4w4rfQ2FPF+Aaf9v7fxa/c + Jrf3K+cB19eAy7O4nlPG1xurvnZd0QpqRk++werrBuKe1Pgga7YBLePfJhzwqcZv + wVOSsB870yeO -----END CERTIFICATE----- CERT - TEST_PKEY = <<-PKEY.gsub(/^\s*/, "") + TEST_PKEY = <<~PKEY -----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 + MIIEowIBAAKCAQEAyS6liSDc13Whczd+jR2ZUkFY2ueLzyUoO4ghBfBwp7pcKCKW + hkbvTx5wtM7Cz36j0yROX1a2l/9YZdP3w0oQ9RhqR3YW/VK4PWNZiU/uINHqEHdG + RYGbnOK6Nh+1Unhms8PjY8uMMBK+WHvlO+sx4zvKr+qRyA2x311DJS8w0+Sze2Vr + 2N7sO3sC6WLYg3cI1fKzuAg5/AkfQFv8zq3LuhR+mhQoB8vPB7rVpvO2AO7WVI2B + 0UEzWRzFh+XKH8WRflOUBxZ4BfMuufEBcZFTFevFbig8L6RmL+kNEF3tfjNAPHts + KavX4WxLczTmLfDkTTxO32IpGThXycKYTiKTpQIDAQABAoIBABpyrHEWRed5X7aN + kXCBzKSN/LLChT8VNnB6bppLnV501yVbmV2hDlg2EJZkfCMvwIptwnPcKs2uqZ4G + u2gMC6X9Bgkg/YK4u4nZJBiIzoMNYEUL48wYGYS1dcokaapO3nQ8M1+XjyAexrFL + 5btL1IIisScRTQWiGe6FtzcN43sSNkBISyDF5zG4Kodynqi0ekITmMl2q5XLWcsM + KBnmZcRFEmFae2YYczVy8SXNApkZEvN69znvAX1iDNnZ3sJFchXo1nRPt4stOOKw + mydgIYqaNQ22aF3OkblvoA4Y4m+X2Qt1sfkryKa5xTT7DSE81GmmazNI64EWqtES + 6Xde6P0CgYEA+V1vuSnE5fWX188abWMbVwNMC71WfHbntFmI+qwWYPEpickm+RGX + DDfXs5unlVX4KUmjfplgavO29op1GZTuD9TlRnUAV0+0aJnNq4DY6XsHfD84qsBr + gQGEHeJ1cMGNDnZR/EV3eudMalj9Qjpx9NoXNzMykb0/SUYZQemiqwcCgYEAzokC + s0GoHVJqan4dfU0h0G5QPncrajW9DGG1ySxK/A2eqbVB8W2ZQx39OS26/Gydb31p + cR7zm8PZpNbzLqlIMEbD4F6q22xxvYVtDx/HHPjxHMi87yxwQ9uLDUHoMa/LciTO + djv3D1xTDDGxbpjmsdmINetunAs3htxku7JY5PMCgYBs3/TVvXzwgmhHm28Ib4sS + VKgxP/uw4CGORsFd4SDsNp9SP3c6rAltFjyheMaUlzKApFwz/DdyuvIZdp5mCvZe + BzALsS3y8SPtv6lixiDu3/6GqvvM4bKOYuESQzvPfVJfDB4DrTjben2MuUnqTqZO + p6IXQc1EgIJPNcH1W1LgpQKBgAKZlPAevngIBpDqn4JpSyititMOevxuSr/yJvCu + Xw9HOJ0YTAk3APvoT7y9h6IP1/eEU6R56EUotP+vOQZ4WRFKgsK7TllOxyvElzfe + hYom1BoxqLc2Dv+7rsdu8fZWKTB5qCOy44xM9DquEXa79AN/IojTOuQ5++v1sErw + ls/jAoGBANneGe9ogN51mYkrLyg1fhU1i24gFRq+sPGEvsCUoE6Vjw/lawQQ80T8 + v45TFqvhoGpgznqy3qxDJyguquZg6HN2yW6HE2Dvk7uk3XogcjdXgNDmWqb2j0eE + z9pKzHCqfwNVPuYf44Znyo2YeyZ2kHn42MU73oXuFshUs3QHcH+P -----END RSA PRIVATE KEY----- PKEY end diff --git a/spec/bundler/support/bundle b/spec/bundler/support/bundle new file mode 100755 index 0000000000..8f8b535295 --- /dev/null +++ b/spec/bundler/support/bundle @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative "../bundler/support/activate" + +load File.expand_path("bundle", Spec::Path.exedir) diff --git a/spec/bundler/support/bundle.rb b/spec/bundler/support/bundle.rb new file mode 100644 index 0000000000..aa7b121706 --- /dev/null +++ b/spec/bundler/support/bundle.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require_relative "path" + +warn "#{__FILE__} is deprecated. Please use #{Spec::Path.dev_binstub} instead" + +load Spec::Path.dev_binstub diff --git a/spec/bundler/support/checksums.rb b/spec/bundler/support/checksums.rb new file mode 100644 index 0000000000..7b69bba668 --- /dev/null +++ b/spec/bundler/support/checksums.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +module Spec + module Checksums + class ChecksumsBuilder + attr_reader :bundler_registered + + def initialize(enabled = true, &block) + @enabled = enabled + @checksums = {} + yield self if block_given? + end + + def initialize_copy(original) + super + @checksums = @checksums.dup + end + + def checksum(repo, name, version, platform = Gem::Platform::RUBY, folder = "gems") + @bundler_registered = true if name == "bundler" + + name_tuple = Gem::NameTuple.new(name, version, platform) + gem_file = File.join(repo, folder, "#{name_tuple.full_name}.gem") + File.open(gem_file, "rb") do |f| + register(name_tuple, Bundler::Checksum.from_gem(f, "#{gem_file} (via ChecksumsBuilder#checksum)")) + end + end + + def no_checksum(name, version, platform = Gem::Platform::RUBY) + name_tuple = Gem::NameTuple.new(name, version, platform) + register(name_tuple, nil) + end + + def delete(name, platform = nil) + @checksums.reject! {|k, _| k.name == name && (platform.nil? || k.platform == platform) } + end + + def to_s + return "" unless @enabled + + locked_checksums = @checksums.map do |name_tuple, checksum| + checksum &&= " #{checksum.to_lock}" + " #{name_tuple.lock_name}#{checksum}\n" + end + + "\nCHECKSUMS\n#{locked_checksums.sort.join}" + end + + private + + def register(name_tuple, checksum) + delete(name_tuple.name, name_tuple.platform) + @checksums[name_tuple] = checksum + end + end + + def checksums_section(enabled = true, bundler_checksum: true, &block) + ChecksumsBuilder.new(enabled, &block).tap do |builder| + next if builder.bundler_registered || !bundler_checksum + + next if Bundler::VERSION.to_s.end_with?(".dev") + builder.checksum(system_gem_path, "bundler", Bundler::VERSION, Gem::Platform::RUBY, "cache") + end + end + + def checksums_section_when_enabled(target_lockfile = nil, &block) + begin + enabled = (target_lockfile || lockfile).match?(/^CHECKSUMS$/) + rescue Errno::ENOENT + enabled = true + end + checksums_section(enabled, &block) + end + + def checksum_to_lock(*args) + checksums_section(true, bundler_checksum: false) do |c| + c.checksum(*args) + end.to_s.sub(/^CHECKSUMS\n/, "").strip + end + + def checksum_digest(*args) + checksum_to_lock(*args).split(Bundler::Checksum::ALGO_SEPARATOR, 2).last + end + + # if prefixes is given, removes all checksums where the line + # has any of the prefixes on the line before the checksum + # otherwise, removes all checksums from the lockfile + def remove_checksums_from_lockfile(lockfile, *prefixes) + head, remaining = lockfile.split(/^CHECKSUMS$/, 2) + return lockfile unless remaining + checksums, tail = remaining.split("\n\n", 2) + + prefixes = + if prefixes.empty? + nil + else + /(#{prefixes.map {|p| Regexp.escape(p) }.join("|")})/ + end + + checksums = checksums.each_line.map do |line| + if prefixes.nil? || line.match?(prefixes) + line.gsub(/ sha256=[a-f0-9]{64}/i, "") + else + line + end + end + + head.concat( + "CHECKSUMS", + checksums.join, + "\n\n", + tail + ) + end + + def remove_checksums_section_from_lockfile(lockfile) + head, remaining = lockfile.split(/^CHECKSUMS$/, 2) + return lockfile unless remaining + _checksums, tail = remaining.split("\n\n", 2) + head.concat(tail) + end + + def checksum_from_package(gem_file, name, version) + name_tuple = Gem::NameTuple.new(name, version) + + checksum = nil + + File.open(gem_file, "rb") do |f| + checksum = Bundler::Checksum.from_gem(f, gemfile) + end + + "#{name_tuple.lock_name} #{checksum.to_lock}" + end + end +end diff --git a/spec/bundler/support/command_execution.rb b/spec/bundler/support/command_execution.rb index b3c289979f..e2915b996d 100644 --- a/spec/bundler/support/command_execution.rb +++ b/spec/bundler/support/command_execution.rb @@ -1,20 +1,38 @@ # frozen_string_literal: true module Spec - CommandExecution = Struct.new(:command, :working_directory, :exitstatus, :stdout, :stderr) do - def to_s - c = Shellwords.shellsplit(command.strip).map {|s| s.include?("\n") ? " \\\n <<EOS\n#{s.gsub(/^/, " ").chomp}\nEOS" : Shellwords.shellescape(s) } - c = c.reduce("") do |acc, elem| - concat = acc + " " + elem - - last_line = concat.match(/.*\z/)[0] - if last_line.size >= 100 - acc + " \\\n " + elem - else - concat - end + class CommandExecution + def initialize(command, timeout:) + @command = command + @timeout = timeout + @original_stdout = String.new + @original_stderr = String.new + end + + attr_accessor :exitstatus, :command, :original_stdout, :original_stderr + attr_reader :timeout + attr_writer :failure_reason + + def raise_error! + return unless failure? + + error_header = if failure_reason == :timeout + "Invoking `#{command}` was aborted after #{timeout} seconds with output:" + else + "Invoking `#{command}` failed with output:" end - "$ #{c.strip}" + + raise <<~ERROR + #{error_header} + + ---------------------------------------------------------------------- + #{stdboth} + ---------------------------------------------------------------------- + ERROR + end + + def to_s + "$ #{command}" end alias_method :inspect, :to_s @@ -22,6 +40,14 @@ module Spec @stdboth ||= [stderr, stdout].join("\n").strip end + def stdout + normalize(original_stdout) + end + + def stderr + normalize(original_stderr) + end + def to_s_verbose [ to_s, @@ -40,5 +66,13 @@ module Spec return true unless exitstatus exitstatus > 0 end + + private + + attr_reader :failure_reason + + def normalize(string) + string.dup.force_encoding(Encoding::UTF_8).scrub.strip.gsub("\r\n", "\n") + end end end diff --git a/spec/bundler/support/env.rb b/spec/bundler/support/env.rb new file mode 100644 index 0000000000..0899bd82a3 --- /dev/null +++ b/spec/bundler/support/env.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Spec + module Env + def ruby_core? + File.exist?(File.expand_path("../../../lib/bundler/bundler.gemspec", __dir__)) + end + + def rubylib + ENV["RUBYLIB"].to_s.split(File::PATH_SEPARATOR) + end + end +end diff --git a/spec/bundler/support/filters.rb b/spec/bundler/support/filters.rb index 4ce6648cdc..2be25b4a78 100644 --- a/spec/bundler/support/filters.rb +++ b/spec/bundler/support/filters.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true -require_relative "sudo" - class RequirementChecker < Proc - def self.against(present) - provided = Gem::Version.new(present) - + def self.against(provided) new do |required| - !Gem::Requirement.new(required).satisfied_by?(provided) + requirement = Gem::Requirement.new(required) + + !requirement.satisfied_by?(provided) end.tap do |checker| checker.provided = provided end @@ -16,30 +14,29 @@ class RequirementChecker < Proc attr_accessor :provided def inspect - "\"!= #{provided}\"" + "\"#{provided}\"" end end -RSpec.configure do |config| - if ENV["BUNDLER_SUDO_TESTS"] && Spec::Sudo.present? - config.filter_run :sudo => true - else - config.filter_run_excluding :sudo => true - end +git_version = Gem::Version.new(`git --version`[/(\d+\.\d+\.\d+)/, 1]) - 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 :rubygems => RequirementChecker.against(Gem::VERSION) - config.filter_run_excluding :git => RequirementChecker.against(git_version) - config.filter_run_excluding :bundler => RequirementChecker.against(Bundler::VERSION.split(".")[0]) - config.filter_run_excluding :ruby_repo => !ENV["GEM_COMMAND"].nil? - config.filter_run_excluding :no_color_tty => Gem.win_platform? || !ENV["GITHUB_ACTION"].nil? +RSpec.configure do |config| + config.filter_run_excluding realworld: true + + config.filter_run_excluding rubygems: RequirementChecker.against(Gem.rubygems_version) + config.filter_run_excluding git: RequirementChecker.against(git_version) + config.filter_run_excluding ruby_repo: !ENV["GEM_COMMAND"].nil? + config.filter_run_excluding no_color_tty: Gem.win_platform? || !ENV["GITHUB_ACTION"].nil? + config.filter_run_excluding permissions: Gem.win_platform? + config.filter_run_excluding readline: Gem.win_platform? + config.filter_run_excluding jruby_only: RUBY_ENGINE != "jruby" + config.filter_run_excluding truffleruby_only: RUBY_ENGINE != "truffleruby" + config.filter_run_excluding man: Gem.win_platform? + config.filter_run_excluding mri_only: RUBY_ENGINE != "ruby" config.filter_run_when_matching :focus unless ENV["CI"] + + config.before(:each, :bundler) do |example| + bundle_config "simulate_version #{example.metadata[:bundler]}" + end end diff --git a/spec/bundler/support/hax.rb b/spec/bundler/support/hax.rb index c18470acd2..46718f5fa4 100644 --- a/spec/bundler/support/hax.rb +++ b/spec/bundler/support/hax.rb @@ -1,5 +1,10 @@ # frozen_string_literal: true +if ENV["BUNDLER_SPEC_RUBY_PLATFORM"] + Object.send(:remove_const, :RUBY_PLATFORM) + RUBY_PLATFORM = ENV["BUNDLER_SPEC_RUBY_PLATFORM"] +end + module Gem def self.ruby=(ruby) @ruby = ruby @@ -9,60 +14,61 @@ module Gem Gem.ruby = ENV["RUBY"] end - if version = ENV["BUNDLER_SPEC_RUBYGEMS_VERSION"] - remove_const(:VERSION) if const_defined?(:VERSION) - VERSION = version + if ENV["BUNDLER_GEM_DEFAULT_DIR"] + @default_dir = ENV["BUNDLER_GEM_DEFAULT_DIR"] + @default_specifications_dir = nil end - class Platform - @local = new(ENV["BUNDLER_SPEC_PLATFORM"]) if ENV["BUNDLER_SPEC_PLATFORM"] - end - @platforms = [Gem::Platform::RUBY, Gem::Platform.local] + spec_platform = ENV["BUNDLER_SPEC_PLATFORM"] + if spec_platform + if /mingw|mswin/.match?(spec_platform) + @@win_platform = nil # rubocop:disable Style/ClassVars + RbConfig::CONFIG["host_os"] = spec_platform.gsub(/^[^-]+-/, "").tr("-", "_") + end - # We only need this hack for rubygems versions without the BundlerVersionFinder - if Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") || ENV["BUNDLER_SPEC_DISABLE_DEFAULT_BUNDLER_GEM"] - @path_to_default_spec_map.delete_if do |_path, spec| - spec.name == "bundler" + RbConfig::CONFIG["arch"] = spec_platform + + class Platform + @local = nil end + @platforms = [] end -end - -if ENV["BUNDLER_SPEC_VERSION"] - require_relative "path" - require "#{Spec::Path.lib_dir}/bundler/version" - module Bundler - remove_const(:VERSION) if const_defined?(:VERSION) - VERSION = ENV["BUNDLER_SPEC_VERSION"].dup + if ENV["BUNDLER_SPEC_GEM_SOURCES"] + self.sources = [ENV["BUNDLER_SPEC_GEM_SOURCES"]] end -end -if ENV["BUNDLER_SPEC_WINDOWS"] == "true" - require_relative "path" - require "#{Spec::Path.lib_dir}/bundler/constants" + if ENV["BUNDLER_SPEC_READ_ONLY"] + module ReadOnly + def open(file, mode) + if file != IO::NULL && mode == "wb" + raise Errno::EROFS + else + super + end + end + end - module Bundler - remove_const :WINDOWS if defined?(WINDOWS) - WINDOWS = true + File.singleton_class.prepend ReadOnly end -end -class Object - if ENV["BUNDLER_SPEC_RUBY_ENGINE"] - if 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 + if ENV["BUNDLER_SPEC_FAKE_RESOLVE"] + module FakeResolv + def getaddrinfo(host, port) + if host == ENV["BUNDLER_SPEC_FAKE_RESOLVE"] + [["AF_INET", port, "127.0.0.1", "127.0.0.1", 2, 2, 17]] + else + super + end end end - remove_const :RUBY_ENGINE - RUBY_ENGINE = ENV["BUNDLER_SPEC_RUBY_ENGINE"] - - remove_const :RUBY_ENGINE_VERSION - RUBY_ENGINE_VERSION = ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] + Socket.singleton_class.prepend FakeResolv end end + +# mise installed rubygems_plugin.rb to system wide `site_ruby` directory. +# This empty module avoid to call `mise` command. +module ReshimInstaller + def self.reshim; end +end diff --git a/spec/bundler/support/helpers.rb b/spec/bundler/support/helpers.rb index 911f734d8b..b0d4b5008b 100644 --- a/spec/bundler/support/helpers.rb +++ b/spec/bundler/support/helpers.rb @@ -1,94 +1,61 @@ # frozen_string_literal: true -require "open3" - -require_relative "command_execution" require_relative "the_bundle" +require_relative "path" +require_relative "options" +require_relative "subprocess" module Spec module Helpers + include Spec::Path + include Spec::Options + include Spec::Subprocess + + def self.extended(mod) + mod.extend Spec::Path + mod.extend Spec::Options + mod.extend Spec::Subprocess + end + 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 + next if %w[base base_system remote1 rubocop standard gems rubygems . ..].include?(File.basename(dir)) + FileUtils.rm_r(dir) end FileUtils.mkdir_p(home) FileUtils.mkdir_p(tmpdir) Bundler.reset! + Bundler::Source::Git::GitProxy.reset + Gem.clear_paths end - def self.bang(method) - define_method("#{method}!") do |*args, &blk| - send(method, *args, &blk).tap do - unless last_command.success? - raise RuntimeError, - "Invoking #{method}!(#{args.map(&:inspect).join(", ")}) failed:\n#{last_command.stdboth}", - caller.drop_while {|bt| bt.start_with?(__FILE__) } - end - end - end - end - - def the_bundle(*args) - TheBundle.new(*args) - end - - def last_command - @command_executions.last || raise("There is no last command") - end - - def out - last_command.stdout - end - - def err - last_command.stderr + def the_bundle + TheBundle.new end - MAJOR_DEPRECATION = /^\[DEPRECATED\]\s*/.freeze + MAJOR_DEPRECATION = /^\[DEPRECATED\]\s*/ def err_without_deprecations err.gsub(/#{MAJOR_DEPRECATION}.+[\n]?/, "") end def deprecations - err.split("\n").select {|l| l =~ MAJOR_DEPRECATION }.join("\n").split(MAJOR_DEPRECATION) - end - - def exitstatus - last_command.exitstatus - 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) + err.split("\n").filter_map {|l| l.sub(MAJOR_DEPRECATION, "") if l.match?(MAJOR_DEPRECATION) } end def run(cmd, *args) opts = args.last.is_a?(Hash) ? args.pop : {} groups = args.map(&:inspect).join(", ") - setup = "require '#{lib_dir}/bundler' ; Bundler.ui.silence { Bundler.setup(#{groups}) }\n" - ruby(setup + cmd, opts) + setup = "require 'bundler' ; Bundler.ui.silence { Bundler.setup(#{groups}) }" + ruby([setup, cmd].join(" ; "), 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}") + warn e.message if e.message.include?("-- #{name}") end RUBY opts = args.last.is_a?(Hash) ? args.pop : {} @@ -96,44 +63,35 @@ module Spec run(cmd, *args) end - def bundle(cmd, options = {}) - with_sudo = options.delete(:sudo) - sudo = with_sudo == :preserve_env ? "sudo -E" : "sudo" if with_sudo - - bundle_bin = options.delete("bundle_bin") || bindir.join("bundle") + def in_bundled_app(cmd, options = {}) + sys_exec(cmd, dir: bundled_app, raise_on_error: options[:raise_on_error]) + end - if system_bundler = options.delete(:system_bundler) - bundle_bin = system_gem_path.join("bin/bundler") - end + def bundle(cmd, options = {}, &block) + bundle_bin = options.delete(:bundle_bin) + bundle_bin ||= installed_bindir.join("bundle") env = options.delete(:env) || {} - env["PATH"].gsub!("#{Path.root}/exe", "") if env["PATH"] && system_bundler requires = options.delete(:requires) || [] - requires << "support/hax" - - artifice = options.delete(:artifice) do - if RSpec.current_example.metadata[:realworld] - "vcr" - else - "fail" - end - end - if artifice - requires << "support/artifice/#{artifice}" - end - requires_str = requires.map {|r| "-r#{r}" }.join(" ") + dir = options.delete(:dir) || bundled_app + custom_load_path = options.delete(:load_path) load_path = [] - load_path << lib_dir unless system_bundler - load_path << spec_dir - load_path_str = "-I#{load_path.join(File::PATH_SEPARATOR)}" + load_path << custom_load_path if custom_load_path + + build_env_options = { load_path: load_path, requires: requires, env: env } + build_env_options.merge!(artifice: options.delete(:artifice)) if options.key?(:artifice) || cmd.start_with?("exec") + + match_source(cmd) + + env = build_env(build_env_options) + + raise_on_error = options.delete(:raise_on_error) args = options.map do |k, v| case v - when nil - next when true " --#{k}" when false @@ -143,132 +101,173 @@ module Spec end end.join - cmd = "#{sudo} #{Gem.ruby} #{load_path_str} #{requires_str} #{bundle_bin} #{cmd}#{args}" - sys_exec(cmd, env) {|i, o, thr| yield i, o, thr if block_given? } + cmd = "#{Gem.ruby} #{bundle_bin} #{cmd}#{args}" + sys_exec(cmd, { env: env, dir: dir, raise_on_error: raise_on_error }, &block) end - bang :bundle - def forgotten_command_line_options(options) - remembered = Bundler::VERSION.split(".", 2).first == "2" - options = options.map do |k, v| - v = '""' if v && v.to_s.empty? - [k, v] - end - return Hash[options] if remembered - options.each do |k, v| - if v.nil? - bundle! "config unset #{k}" - else - bundle! "config set --local #{k} #{v}" - end - end - {} + def main_source(dir) + gemfile = File.expand_path("Gemfile", dir) + return unless File.exist?(gemfile) + + match = File.readlines(gemfile).first.match(/source ["'](?<source>[^"']+)["']/) + return unless match + + match[:source] end def bundler(cmd, options = {}) - options["bundle_bin"] = bindir.join("bundler") + options[:bundle_bin] = system_gem_path("bin/bundler") bundle(cmd, options) end def ruby(ruby, options = {}) - env = options.delete(:env) || {} - ruby = ruby.gsub(/["`\$]/) {|m| "\\#{m}" } - lib_option = options[:no_lib] ? "" : " -I#{lib_dir}" - sys_exec(%(#{Gem.ruby}#{lib_option} -e "#{ruby}"), env) + env = build_env({ artifice: nil }.merge(options)) + escaped_ruby = ruby.shellescape + options[:env] = env if env + options[:dir] ||= bundled_app + sys_exec(%(#{Gem.ruby} -w -e #{escaped_ruby}), options) 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}") + warn e.message if e.message.include?("-- #{name}") end R end - def gembin(cmd) - old = ENV["RUBYOPT"] - ENV["RUBYOPT"] = "#{ENV["RUBYOPT"]} -I#{lib_dir}" - cmd = bundled_app("bin/#{cmd}") unless cmd.to_s.include?("/") - sys_exec(cmd.to_s) - ensure - ENV["RUBYOPT"] = old - end + def build_env(options = {}) + env = options.delete(:env) || {} + libs = options.delete(:load_path) || [] + env["RUBYOPT"] = opt_add("-I#{libs.join(File::PATH_SEPARATOR)}", env["RUBYOPT"]) if libs.any? - def gem_command(command, args = "") - sys_exec("#{Path.gem_bin} #{command} #{args}") - end - bang :gem_command + current_example = RSpec.current_example + + main_source = @gemfile_source if defined?(@gemfile_source) + compact_index_main_source = main_source&.start_with?("https://gem.repo", "https://gems.security") - def rake - "#{Gem.ruby} -S #{ENV["GEM_PATH"]}/bin/rake" + requires = options.delete(:requires) || [] + requires << hax + + artifice = options.delete(:artifice) do + if current_example && current_example.metadata[:realworld] + "vcr" + elsif compact_index_main_source + env["BUNDLER_SPEC_GEM_REPO"] ||= + case main_source + when "https://gem.repo1" then gem_repo1.to_s + when "https://gem.repo2" then gem_repo2.to_s + when "https://gem.repo3" then gem_repo3.to_s + when "https://gem.repo4" then gem_repo4.to_s + when "https://gems.security" then security_repo.to_s + end + + "compact_index" + else + "fail" + end + end + if artifice + requires << "#{Path.spec_dir}/support/artifice/#{artifice}.rb" + end + + requires.each {|r| env["RUBYOPT"] = opt_add("-r#{r}", env["RUBYOPT"]) } + + env end - def sys_exec(cmd, env = {}) - command_execution = CommandExecution.new(cmd.to_s, Dir.pwd) + def gembin(cmd, options = {}) + cmd = bundled_app("bin/#{cmd}") unless cmd.to_s.include?("/") + sys_exec(cmd.to_s, options) + end - env = env.map {|k, v| [k.to_s, v.to_s] }.to_h # convert env keys and values to string + def sys_exec(cmd, options = {}, &block) + env = options[:env] || {} + env["RUBYOPT"] = opt_add(opt_add("-r#{spec_dir}/support/switch_rubygems.rb", env["RUBYOPT"]), ENV["RUBYOPT"]) + options[:env] = env - Open3.popen3(env, cmd.to_s) do |stdin, stdout, stderr, wait_thr| - yield stdin, stdout, wait_thr if block_given? - stdin.close + sh(cmd, options, &block) + end - command_execution.stdout = Thread.new { stdout.read }.value.strip - command_execution.stderr = Thread.new { stderr.read }.value.strip - command_execution.exitstatus = wait_thr && wait_thr.value.exitstatus + def bundle_config(config = nil, path = bundled_app(".bundle/config")) + if config.is_a?(String) + key, value = config.split(" ", 2) + config = { Bundler::Settings.key_for(key) => value } end - (@command_executions ||= []) << command_execution + current = File.exist?(path) ? Psych.load_file(path) : {} + return current unless config - command_execution.stdout - end - bang :sys_exec + current = {} if current.empty? - 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 + + new_config = current.merge(config).compact + + File.open(path, "w+") do |f| + f.puts new_config.to_yaml end - config + + new_config end - def global_config(config = nil) - config(config, home(".bundle/config")) + def bundle_config_global(config = nil) + bundle_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 || "" + def create_file(path, contents = "") + contents = strip_whitespace(contents) + path = Pathname.new(path).expand_path(bundled_app) unless path.is_a?(Pathname) path.dirname.mkpath - File.open(path.to_s, "w") do |f| - f.puts strip_whitespace(str) + path.write(contents) + + # if the file is a script, create respective bat file on Windows + if contents.start_with?("#!") + path.chmod(0o755) + if Gem.win_platform? + path.sub_ext(".bat").write <<~SCRIPT + @ECHO OFF + @"ruby.exe" "%~dpn0" %* + SCRIPT + end end end def gemfile(*args) - contents = args.shift + contents = args.pop if contents.nil? - File.open("Gemfile", "r", &:read) + read_gemfile else - create_file("Gemfile", contents, *args) + match_source(contents) + create_file(args.pop || "Gemfile", contents) end end def lockfile(*args) - contents = args.shift + contents = args.pop if contents.nil? - File.open("Gemfile.lock", "r", &:read) + read_lockfile else - create_file("Gemfile.lock", contents, *args) + create_file(args.pop || "Gemfile.lock", contents) end end + def read_gemfile(file = "Gemfile") + read_bundled_app_file(file) + end + + def read_lockfile(file = "Gemfile.lock") + read_bundled_app_file(file) + end + + def read_bundled_app_file(file) + bundled_app(file).read + end + def strip_whitespace(str) # Trim the leading spaces spaces = str[/\A\s+/, 0] || "" @@ -276,75 +275,193 @@ module Spec end def install_gemfile(*args) + opts = args.last.is_a?(Hash) ? args.pop : {} 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) + def base_system_gems(*names, **options) + system_gems names.map {|name| find_base_path(name) }, **options + end + + def system_gems(*gems) + gems = gems.flatten options = gems.last.is_a?(Hash) ? gems.pop : {} - gem_repo = options.fetch(:gem_repo) { gem_repo1 } + install_dir = options.fetch(:path, system_gem_path) + default = options.fetch(:default, false) gems.each do |g| - if g == :bundler - with_built_bundler {|gem_path| install_gem(gem_path) } - elsif g.to_s =~ %r{\A(?:[A-Z]:)?/.*\.gem\z} - install_gem(g) + gem_name = g.to_s + bundler = gem_name.match(/\Abundler-(?<version>.*)\z/) + + if bundler + with_built_bundler(bundler[:version], released: options.fetch(:released, false)) {|gem_path| install_gem(gem_path, install_dir, default) } + elsif %r{\A(?:[a-zA-Z]:)?/.*\.gem\z}.match?(gem_name) + install_gem(gem_name, install_dir, default) else - install_gem("#{gem_repo}/gems/#{g}.gem") + gem_repo = options.fetch(:gem_repo, gem_repo1) + install_gem("#{gem_repo}/gems/#{gem_name}.gem", install_dir, default) end end end - def install_gem(path) - raise "OMG `#{path}` does not exist!" unless File.exist?(path) + def self.install_dev_bundler + extend self + + with_built_bundler(nil, build_path: tmp_root) {|gem_path| install_gem(gem_path, pristine_system_gem_path) } + end + + def install_gem(path, install_dir, default = false) + raise ArgumentError, "`#{path}` does not exist!" unless File.exist?(path) - gem_command! :install, "--no-document --ignore-dependencies '#{path}'" + require "rubygems/installer" + + with_simulated_platform do + installer = Gem::Installer.at( + path.to_s, + install_dir: install_dir.to_s, + document: [], + ignore_dependencies: true, + wrappers: true, + env_shebang: true, + force: true + ) + installer.install + end + + if default + gem = Pathname.new(path).basename.to_s.match(/(.*)\.gem/)[1] + + # Revert Gem::Installer#write_spec and apply Gem::Installer#write_default_spec + FileUtils.mkdir_p File.join(install_dir, "specifications", "default") + File.rename File.join(install_dir, "specifications", gem + ".gemspec"), + File.join(install_dir, "specifications", "default", gem + ".gemspec") + + # Revert Gem::Installer#write_cache_file + File.delete File.join(install_dir, "cache", gem + ".gem") + end end - def with_built_bundler - with_root_gemspec do |gemspec| - Dir.chdir(root) { gem_command! :build, gemspec.to_s } + def uninstall_gem(name, options = {}) + require "rubygems/uninstaller" + + gem_home = options.dig(:env, "GEM_HOME") || system_gem_path.to_s + + with_env_vars("GEM_HOME" => gem_home) do + Gem.clear_paths + + uninstaller = Gem::Uninstaller.new( + name, + ignore: true, + executables: true, + all: true + ) + uninstaller.uninstall + ensure + Gem.clear_paths end + end - bundler_path = root + "bundler-#{Bundler::VERSION}.gem" + def installed_gems_list(options = {}) + gem_home = options.dig(:env, "GEM_HOME") || system_gem_path.to_s + + # Temporarily set GEM_HOME for the command + old_gem_home = ENV["GEM_HOME"] + ENV["GEM_HOME"] = gem_home + Gem.clear_paths begin - yield(bundler_path) + require "rubygems/commands/list_command" + + # Capture output from the list command + require "stringio" + output_io = StringIO.new + cmd = Gem::Commands::ListCommand.new + cmd.ui = Gem::StreamUI.new(StringIO.new, output_io, StringIO.new, false) + cmd.invoke + output = output_io.string.strip ensure - bundler_path.rmtree + ENV["GEM_HOME"] = old_gem_home + Gem.clear_paths end + + # Create a fake command execution so `out` helper works + command_execution = Spec::CommandExecution.new("gem list", timeout: 60) + command_execution.original_stdout << output + command_execution.exitstatus = 0 + command_executions << command_execution + + output + end + + def with_built_bundler(version = nil, opts = {}, &block) + require_relative "builders" + + Builders::BundlerBuilder.new(self, "bundler", version)._build(opts, &block) end def with_gem_path_as(path) + without_env_side_effects do + ENV["GEM_HOME"] = path.to_s + ENV["GEM_PATH"] = path.to_s + ENV["BUNDLER_ORIG_GEM_HOME"] = nil + ENV["BUNDLER_ORIG_GEM_PATH"] = nil + yield + end + end + + def with_path_as(path) + without_env_side_effects do + ENV["PATH"] = path.to_s + ENV["BUNDLER_ORIG_PATH"] = nil + yield + end + end + + def without_env_side_effects 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 + # Simulate the platform set by BUNDLER_SPEC_PLATFORM for in-process + # operations, mirroring what hax.rb does for subprocesses. + def with_simulated_platform + spec_platform = ENV["BUNDLER_SPEC_PLATFORM"] + unless spec_platform + return yield + end + + old_arch = RbConfig::CONFIG["arch"] + old_host_os = RbConfig::CONFIG["host_os"] + + if /mingw|mswin/.match?(spec_platform) + Gem.class_variable_set(:@@win_platform, nil) # rubocop:disable Style/ClassVars + RbConfig::CONFIG["host_os"] = spec_platform.gsub(/^[^-]+-/, "").tr("-", "_") + end + + RbConfig::CONFIG["arch"] = spec_platform + Gem::Platform.instance_variable_set(:@local, nil) + Gem.instance_variable_set(:@platforms, []) + yield ensure - ENV.replace(backup) + if spec_platform + RbConfig::CONFIG["arch"] = old_arch + RbConfig::CONFIG["host_os"] = old_host_os + Gem::Platform.instance_variable_set(:@local, nil) + Gem.instance_variable_set(:@platforms, []) + end end def with_path_added(path) - with_path_as(path.to_s + ":" + ENV["PATH"]) do + with_path_as([path.to_s, ENV["PATH"]].join(File::PATH_SEPARATOR)) do yield end end @@ -360,154 +477,74 @@ module Spec 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 + create_file(tmp("fake_man/man"), <<~SCRIPT) + #!/usr/bin/env ruby + puts ARGV.inspect + SCRIPT with_path_added(tmp("fake_man")) { yield } end - def system_gems(*gems) - opts = gems.last.is_a?(Hash) ? gems.last : {} - path = opts.fetch(:path, system_gem_path) - if path == :bundle_path - path = ruby!(<<-RUBY) - require "bundler" - begin - puts Bundler.bundle_path - rescue Bundler::GemfileNotFound - ENV["BUNDLE_GEMFILE"] = "Gemfile" - retry - end - - RUBY - end - gems = gems.flatten - - unless opts[:keep_path] - FileUtils.rm_rf(path) - FileUtils.mkdir_p(path) - end - - Gem.clear_paths - - env_backup = ENV.to_hash - ENV["GEM_HOME"] = path.to_s - ENV["GEM_PATH"] = 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) + def pristine_system_gems(*gems) + FileUtils.rm_r(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-document #{gem}" - end - return unless block_given? - begin - yield - ensure - ENV["GEM_HOME"] = gem_home - ENV["GEM_PATH"] = gem_path - ENV["PATH"] = path + if gems.any? + system_gems(*gems) + else + default_system_gems end end - def cache_gems(*gems) + def cache_gems(*gems, gem_repo: gem_repo1) gems = gems.flatten - FileUtils.rm_rf("#{bundled_app}/vendor/cache") FileUtils.mkdir_p("#{bundled_app}/vendor/cache") gems.each do |g| - path = "#{gem_repo1}/gems/#{g}.gem" - raise "OMG `#{path}` does not exist!" unless File.exist?(path) + path = "#{gem_repo}/gems/#{g}.gem" + raise ArgumentError, "`#{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 system_gem_path - FileUtils.rm_rf bundled_app(".bundle") + FileUtils.rm_r bundled_app(".bundle") + pristine_system_gems 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? + def default_system_gems + FileUtils.cp_r pristine_system_gem_path, system_gem_path 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? + def simulate_ruby_platform(ruby_platform) + old = ENV["BUNDLER_SPEC_RUBY_PLATFORM"] + ENV["BUNDLER_SPEC_RUBY_PLATFORM"] = ruby_platform.to_s + yield ensure - ENV["BUNDLER_SPEC_RUBY_VERSION"] = old if block_given? + ENV["BUNDLER_SPEC_RUBY_PLATFORM"] = old 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? + def simulate_platform(platform) + old = ENV["BUNDLER_SPEC_PLATFORM"] + ENV["BUNDLER_SPEC_PLATFORM"] = platform.to_s + yield ensure - ENV["BUNDLER_SPEC_RUBY_ENGINE"] = old if block_given? - ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] = old_version if block_given? + ENV["BUNDLER_SPEC_PLATFORM"] = old 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? + def current_ruby_minor + Gem.ruby_version.segments.tap {|s| s.delete_at(2) }.join(".") end - def simulate_rubygems_version(version) - old = ENV["BUNDLER_SPEC_RUBYGEMS_VERSION"] - ENV["BUNDLER_SPEC_RUBYGEMS_VERSION"] = version.to_s - yield if block_given? - ensure - ENV["BUNDLER_SPEC_RUBYGEMS_VERSION"] = old if block_given? + def next_ruby_minor + ruby_major_minor.map.with_index {|s, i| i == 1 ? s + 1 : s }.join(".") end - def simulate_windows(platform = mswin) - old = ENV["BUNDLER_SPEC_WINDOWS"] - ENV["BUNDLER_SPEC_WINDOWS"] = "true" - simulate_platform platform do - yield - end - ensure - ENV["BUNDLER_SPEC_WINDOWS"] = old + def ruby_major_minor + Gem.ruby_version.segments[0..1] end def revision_for(path) - Dir.chdir(path) { `git rev-parse HEAD`.strip } + git("rev-parse HEAD", path).strip end def with_read_only(pattern) @@ -529,11 +566,11 @@ module Spec process_file(pathname) do |line| case line when /spec\.metadata\["(?:allowed_push_host|homepage_uri|source_code_uri|changelog_uri)"\]/, /spec\.homepage/ - line.gsub(/\=.*$/, "= 'http://example.org'") + line.gsub(/\=.*$/, '= "http://example.org"') when /spec\.summary/ - line.gsub(/\=.*$/, "= %q{A short summary of my new gem.}") + line.gsub(/\=.*$/, '= "A short summary of my new gem."') when /spec\.description/ - line.gsub(/\=.*$/, "= %q{A longer description of my new gem.}") + line.gsub(/\=.*$/, '= "A longer description of my new gem."') else line end @@ -559,32 +596,36 @@ module Spec end end - def require_rack - # need to hack, so we can require rack + def require_rack_test + # need to hack, so we can require rack for testing old_gem_home = ENV["GEM_HOME"] - ENV["GEM_HOME"] = Spec::Path.base_system_gems.to_s - require "rack" + ENV["GEM_HOME"] = Spec::Path.scoped_base_system_gem_path.to_s + require "rack/test" 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 StandardError => e - raise(e) if tries > (seconds * 2) - tries += 1 - retry + def exit_status_for_signal(signal_number) + # For details see: https://en.wikipedia.org/wiki/Exit_status#Shell_and_scripts + 128 + signal_number end - def find_unused_port - port = 21_453 - begin - port += 1 while TCPSocket.new("127.0.0.1", port) - rescue StandardError - false - end - port + def empty_repo4 + FileUtils.rm_r gem_repo4 + + build_repo4 {} + end + + private + + def match_source(contents) + match = /source ["']?(?<source>http[^"']+)["']?/.match(contents) + return unless match + + @gemfile_source = match[:source] + end + + def git_root_dir? + root.to_s == `git rev-parse --show-toplevel`.chomp end end end diff --git a/spec/bundler/support/indexes.rb b/spec/bundler/support/indexes.rb index b76f493d01..1fbdd49abe 100644 --- a/spec/bundler/support/indexes.rb +++ b/spec/bundler/support/indexes.rb @@ -14,19 +14,25 @@ module Spec alias_method :platforms, :platform - def resolve(args = []) + def resolve(args = [], dependency_api_available: true) @platforms ||= ["ruby"] - deps = [] - default_source = instance_double("Bundler::Source::Rubygems", :specs => @index) - source_requirements = { :default => default_source } + default_source = instance_double("Bundler::Source::Rubygems", specs: @index, to_s: "locally install gems", dependency_api_available?: dependency_api_available) + source_requirements = { default: default_source } + base = args[0] || Bundler::SpecSet.new([]) + base.each {|ls| ls.source = default_source } + gem_version_promoter = args[1] || Bundler::GemVersionPromoter.new + originally_locked = args[2] || Bundler::SpecSet.new([]) + unlock = args[3] || [] @deps.each do |d| - @platforms.each do |p| - source_requirements[d.name] = d.source = default_source - deps << Bundler::DepProxy.new(d, p) - end + name = d.name + source_requirements[name] = d.source = default_source end - source_requirements ||= {} - Bundler::Resolver.resolve(deps, @index, source_requirements, *args) + packages = Bundler::Resolver::Base.new(source_requirements, @deps, base, @platforms, locked_specs: originally_locked, unlock: unlock) + Bundler::Resolver.new(packages, gem_version_promoter).start + end + + def should_not_resolve + expect { resolve }.to raise_error(Bundler::GemNotFound) end def should_resolve_as(specs) @@ -35,6 +41,12 @@ module Spec expect(got).to eq(specs.sort) end + def should_resolve_without_dependency_api(specs) + got = resolve(dependency_api_available: false) + got = got.map(&:full_name).sort + expect(got).to eq(specs.sort) + end + def should_resolve_and_include(specs, args = []) got = resolve(args) got = got.map(&:full_name).sort @@ -43,13 +55,6 @@ module Spec 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 @@ -61,20 +66,18 @@ module Spec 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| + search = Bundler::GemVersionPromoter.new.tap do |s| s.level = opts.first s.strict = opts.include?(:strict) - s.prerelease_specified = Hash[@deps.map {|d| [d.name, d.requirement.prerelease?] }] end - should_resolve_and_include specs, [@base, search] + should_resolve_and_include specs, [@base, search, @locked, unlock] end def an_awesome_index 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] + gem "myrack", %w[0.8 0.9 0.9.1 0.9.2 1.0 1.1] + gem "myrack-mount", %w[0.4 0.5 0.5.1 0.5.2 0.6] # --- Pre-release support gem "RubyGems\0", ["1.3.2"] @@ -85,10 +88,10 @@ module Spec 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" + dep "myrack", "~> 1.1" + dep "myrack-mount", ">= 0.5" + elsif version > v("2.3") then dep "myrack", "~> 1.0.0" + elsif version > v("2.0.0") then dep "myrack", "~> 0.9.0" end end gem "activerecord", version do @@ -119,11 +122,11 @@ module Spec 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| + platforms "ruby java mswin32 mingw32 x64-mingw-ucrt" 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") + dep "weakling", ">= 0.0.3" if platform =~ pl("java") # rubocop:disable Performance/RegexpMatch end end end @@ -300,7 +303,7 @@ module Spec end end - def a_unresovable_child_index + def a_unresolvable_child_index build_index do gem "json", %w[1.8.0] @@ -363,7 +366,7 @@ module Spec def a_circular_index build_index do - gem "rack", "1.0.1" + gem "myrack", "1.0.1" gem("foo", "0.2.6") do dep "bar", ">= 0" end diff --git a/spec/bundler/support/matchers.rb b/spec/bundler/support/matchers.rb index e1a08a30cc..5a3c38a4db 100644 --- a/spec/bundler/support/matchers.rb +++ b/spec/bundler/support/matchers.rb @@ -52,7 +52,7 @@ module Spec end def self.define_compound_matcher(matcher, preconditions, &declarations) - raise "Must have preconditions to define a compound matcher" if preconditions.empty? + raise ArgumentError, "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), @@ -75,16 +75,6 @@ module Spec 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 - RSpec::Matchers.define :be_sorted do diffable attr_reader :expected @@ -124,62 +114,87 @@ module Spec 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+/) - require_path = name == "bundler" ? "#{lib_dir}/bundler" : name + groups = Array(opts.delete(:groups)).map(&:inspect).join(", ") + opts[:raise_on_error] = false + @errors = names.filter_map do |full_name| + name, version, platform = full_name.split(/\s+/) + platform ||= "ruby" + require_path = name.tr("-", "/") version_const = name == "bundler" ? "Bundler::VERSION" : Spec::Builders.constantize(name) - begin - run! "require '#{require_path}.rb'; puts #{version_const}", *groups - rescue StandardError => e - next "#{name} is not installed:\n#{indent(e)}" - end - actual_version, actual_platform = out.strip.split(/\s+/, 2) - unless Gem::Version.new(actual_version) == Gem::Version.new(version) + source_const = "#{Spec::Builders.constantize(name)}_SOURCE" + ruby <<~R, opts + require 'bundler' + Bundler.setup(#{groups}) + + require '#{require_path}' + actual_version, actual_platform = #{version_const}.split(/\s+/, 2) + actual_platform ||= "ruby" + unless Gem::Version.new(actual_version) == Gem::Version.new('#{version}') + puts actual_version + exit 64 + end + unless actual_platform.to_s == '#{platform}' + puts actual_platform + exit 65 + end + require '#{require_path}/source' + exit 0 if #{source.nil?} + actual_source = #{source_const} + unless actual_source == '#{source}' + puts actual_source + exit 66 + end + R + next if exitstatus == 0 + if exitstatus == 64 + actual_version = out.split("\n").last next "#{name} was expected to be at version #{version} but was #{actual_version}" end - unless actual_platform == platform + if exitstatus == 65 + actual_platform = out.split("\n").last 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 StandardError - next "#{name} does not have a source defined:\n#{indent(e)}" - end - unless out.strip == source - next "Expected #{name} (#{version}) to be installed from `#{source}`, was actually from `#{out}`" + if exitstatus == 66 + actual_source = out.split("\n").last + next "Expected #{name} (#{version}) to be installed from `#{source}`, was actually from `#{actual_source}`" end - end.compact + next "Command to check for inclusion of gem #{full_name} failed" + end @errors.empty? end match_when_negated do opts = names.last.is_a?(Hash) ? names.pop : {} - groups = Array(opts[:groups]) || [] - @errors = names.map do |name| + groups = Array(opts.delete(:groups)).map(&:inspect).join(", ") + opts[:raise_on_error] = false + @errors = names.filter_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" + ruby <<-R, opts + begin + require 'bundler' + Bundler.setup(#{groups}) + rescue Bundler::GemNotFound, Bundler::GitError + exit 0 + end + + begin + require '#{name}' + name_constant = #{Spec::Builders.constantize(name)} + if #{version.nil?} || name_constant == '#{version}' + exit 64 + else + exit 0 end - R - rescue StandardError => e - next "checking for #{name} failed:\n#{e}\n#{e.backtrace.join("\n")}" - end - next if out == "WIN" + rescue LoadError, NameError + exit 0 + end + R + next if exitstatus == 0 + next "command to check version of #{name} installed failed" unless exitstatus == 64 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 + next "expected #{name} (#{version}) not to be installed, but it was" + end @errors.empty? end @@ -195,10 +210,6 @@ module Spec 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) @@ -212,13 +223,5 @@ module Spec expect(Bundler::Plugin).not_to be_installed(name) end end - - def lockfile_should_be(expected) - expect(bundled_app("Gemfile.lock")).to have_lockfile(expected) - end - - def gemfile_should_be(expected) - expect(bundled_app("Gemfile")).to read_as(strip_whitespace(expected)) - end end end diff --git a/spec/bundler/support/options.rb b/spec/bundler/support/options.rb new file mode 100644 index 0000000000..551fa1acd8 --- /dev/null +++ b/spec/bundler/support/options.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Spec + module Options + def opt_add(option, options) + [option.strip, options].compact.reject(&:empty?).join(" ") + end + + def opt_remove(option, options) + return unless options + + options.split(" ").reject {|opt| opt.strip == option.strip }.join(" ") + end + end +end diff --git a/spec/bundler/support/parallel.rb b/spec/bundler/support/parallel.rb deleted file mode 100644 index 8763cb9ec4..0000000000 --- a/spec/bundler/support/parallel.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -RSpec.configure do |config| - config.silence_filter_announcements = true -end diff --git a/spec/bundler/support/path.rb b/spec/bundler/support/path.rb index 6f78490fe8..2e6486412f 100644 --- a/spec/bundler/support/path.rb +++ b/spec/bundler/support/path.rb @@ -1,57 +1,137 @@ # frozen_string_literal: true -require "pathname" +require "pathname" unless defined?(Pathname) require "rbconfig" +require_relative "env" + module Spec module Path + include Spec::Env + + def source_root + @source_root ||= Pathname.new(ruby_core? ? "../../.." : "../../bundler").expand_path(__dir__) + end + def root - @root ||= Pathname.new(ruby_core? ? "../../../.." : "../../..").expand_path(__FILE__) + @root ||= system_gem_path("gems/bundler-#{Bundler::VERSION}") end def gemspec - @gemspec ||= root.join(ruby_core? ? "lib/bundler/bundler.gemspec" : "bundler.gemspec") + @gemspec ||= source_root.join(relative_gemspec) end - def gemspec_dir - @gemspec_dir ||= gemspec.parent + def relative_gemspec + @relative_gemspec ||= ruby_core? ? "lib/bundler/bundler.gemspec" : "bundler.gemspec" + end + + def loaded_gemspec + @loaded_gemspec ||= Dir.chdir(source_root) { Gem::Specification.load(gemspec.to_s) } + end + + def test_gemfile + @test_gemfile ||= tool_dir.join("test_gems.rb") + end + + def rubocop_gemfile + @rubocop_gemfile ||= source_root.join(rubocop_gemfile_basename) + end + + def standard_gemfile + @standard_gemfile ||= source_root.join(standard_gemfile_basename) + end + + def dev_gemfile + @dev_gemfile ||= tool_dir.join("dev_gems.rb") + end + + def dev_binstub + @dev_binstub ||= bindir.join("bundle") end def bindir - @bindir ||= root.join(ruby_core? ? "libexec" : "exe") + @bindir ||= source_root.join(ruby_core? ? "spec/bin" : "../bin") + end + + def exedir + @exedir ||= source_root.join(ruby_core? ? "libexec" : "exe") + end + + def installed_bindir + @installed_bindir ||= system_gem_path("bin") + end + + def gem_cmd + @gem_cmd ||= ruby_core? ? source_root.join("bin/gem") : "gem" end def gem_bin - @gem_bin ||= ruby_core? ? ENV["GEM_COMMAND"] : "#{Gem.ruby} -I#{spec_dir}/rubygems -S gem --backtrace" + @gem_bin ||= ENV["GEM_COMMAND"] || "gem" + end + + def path + env_path = ENV["PATH"] + env_path = env_path.split(File::PATH_SEPARATOR).reject {|path| path == exedir.to_s }.join(File::PATH_SEPARATOR) if ruby_core? + env_path end def spec_dir - @spec_dir ||= root.join(ruby_core? ? "spec/bundler" : "spec") + @spec_dir ||= source_root.join(ruby_core? ? "spec/bundler" : "../spec") + end + + def man_dir + @man_dir ||= lib_dir.join("bundler/man") + end + + def hax + @hax ||= spec_dir.join("support/hax.rb") end def tracked_files - if root != `git rev-parse --show-toplevel` - skip 'not in git working directory' - end - @tracked_files ||= ruby_core? ? `git ls-files -z -- lib/bundler lib/bundler.rb spec/bundler man/bundler*` : `git ls-files -z` + @tracked_files ||= git_ls_files(tracked_files_glob) end def shipped_files - if root != `git rev-parse --show-toplevel` - skip 'not in git working directory' + @shipped_files ||= if ruby_core_tarball? + loaded_gemspec.files.map {|f| f.gsub(%r{^exe/}, "libexec/") } + elsif ruby_core? + tracked_files + else + loaded_gemspec.files end - @shipped_files ||= ruby_core? ? `git ls-files -z -- lib/bundler lib/bundler.rb man/bundler* libexec/bundle*` : `git ls-files -z -- lib man exe CHANGELOG.md LICENSE.md README.md bundler.gemspec` end def lib_tracked_files - if root != `git rev-parse --show-toplevel` - skip 'not in git working directory' - end - @lib_tracked_files ||= ruby_core? ? `git ls-files -z -- lib/bundler lib/bundler.rb` : `git ls-files -z -- lib` + @lib_tracked_files ||= git_ls_files(lib_tracked_files_glob) + end + + def man_tracked_files + @man_tracked_files ||= git_ls_files(man_tracked_files_glob) end def tmp(*path) - root.join("tmp", scope, *path) + tmp_root.join("#{test_env_version}.#{scope}").join(*path) + end + + def tmp_root + if ruby_core? && (tmpdir = ENV["TMPDIR"]) + # Use realpath to resolve any symlinks in TMPDIR (e.g., on macOS /var -> /private/var) + real = begin + File.realpath(tmpdir) + rescue Errno::ENOENT, Errno::EACCES + tmpdir + end + Pathname(real) + else + (ruby_core? ? source_root : source_root.parent).join("tmp") + end + end + + # Bump this version whenever you make a breaking change to the spec setup + # that requires regenerating tmp/. + + def test_env_version + 2 end def scope @@ -62,84 +142,124 @@ module Spec end def home(*path) - tmp.join("home", *path) + tmp("home", *path) end def default_bundle_path(*path) - if Bundler::VERSION.split(".").first.to_i < 3 - system_gem_path(*path) - else - bundled_app(*[".bundle", ENV.fetch("BUNDLER_SPEC_RUBY_ENGINE", Gem.ruby_engine), RbConfig::CONFIG["ruby_version"], *path].compact) - end + system_gem_path(*path) + end + + def default_cache_path(*path) + default_bundle_path("cache/bundler", *path) + end + + def compact_index_cache_path + home(".bundle/cache/compact_index") end def bundled_app(*path) - root = tmp.join("bundled_app") + root = tmp("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") + root = tmp("bundled_app2") FileUtils.mkdir_p(root) root.join(*path) end def vendored_gems(path = nil) - bundled_app(*["vendor/bundle", Gem.ruby_engine, RbConfig::CONFIG["ruby_version"], path].compact) + scoped_gem_path(bundled_app("vendor/bundle")).join(*[path].compact) end def cached_gem(path) bundled_app("vendor/cache/#{path}.gem") end - def base_system_gems - tmp.join("gems/base") + def bundled_app_gemfile + bundled_app("Gemfile") + end + + def bundled_app_lock + bundled_app("Gemfile.lock") + end + + def scoped_base_system_gem_path + scoped_gem_path(base_system_gem_path) + end + + def base_system_gem_path + tmp_root.join("gems/base") + end + + def rubocop_gem_path + tmp_root.join("gems/rubocop") + end + + def standard_gem_path + tmp_root.join("gems/standard") end def file_uri_for(path) protocol = "file://" root = Gem.win_platform? ? "/" : "" - return protocol + "localhost" + root + path.to_s if RUBY_VERSION < "2.5" - protocol + root + path.to_s end def gem_repo1(*args) - tmp("gems/remote1", *args) + gem_path("remote1", *args) end def gem_repo_missing(*args) - tmp("gems/missing", *args) + gem_path("missing", *args) end def gem_repo2(*args) - tmp("gems/remote2", *args) + gem_path("remote2", *args) end def gem_repo3(*args) - tmp("gems/remote3", *args) + gem_path("remote3", *args) end def gem_repo4(*args) - tmp("gems/remote4", *args) + gem_path("remote4", *args) end def security_repo(*args) - tmp("gems/security_repo", *args) + gem_path("security_repo", *args) end def system_gem_path(*path) - tmp("gems/system", *path) + gem_path("system", *path) + end + + def pristine_system_gem_path + tmp_root.join("gems/pristine_system") + end + + def local_gem_path(*path, base: bundled_app) + scoped_gem_path(base.join(".bundle")).join(*path) + end + + def scoped_gem_path(base) + base.join(Gem.ruby_engine, RbConfig::CONFIG["ruby_version"]) + end + + def gem_path(*args) + tmp("gems", *args) end def lib_path(*args) tmp("libs", *args) end + def source_lib_dir + source_root.join("lib") + end + def lib_dir root.join("lib") end @@ -156,29 +276,104 @@ module Spec tmp "tmpdir", *args end - def with_root_gemspec - if ruby_core? - root_gemspec = root.join("bundler.gemspec") - # Dir.chdir(root) for Dir.glob in gemspec - spec = Dir.chdir(root) { Gem::Specification.load(gemspec.to_s) } - spec.bindir = "libexec" - File.open(root_gemspec.to_s, "w") {|f| f.write spec.to_ruby } - yield(root_gemspec) - FileUtils.rm(root_gemspec) - else - yield(gemspec) - end + def replace_version_file(version, dir: source_root) + version_file = File.expand_path("lib/bundler/version.rb", dir) + contents = File.read(version_file) + contents.sub!(/(^\s+VERSION\s*=\s*).*$/, %(\\1"#{version}")) + File.open(version_file, "w") {|f| f << contents } + end + + def replace_required_ruby_version(version, dir:) + gemspec_file = File.expand_path("bundler.gemspec", dir) + contents = File.read(gemspec_file) + contents.sub!(/(^\s+s\.required_ruby_version\s*=\s*)"[^"]+"/, %(\\1"#{version}")) + File.open(gemspec_file, "w") {|f| f << contents } + end + + def replace_changelog(version, dir:) + changelog = File.expand_path("CHANGELOG.md", dir) + contents = File.readlines(changelog) + contents = [contents[0], contents[1], "## #{version} (2100-01-01)\n", *contents[3..-1]].join + File.open(changelog, "w") {|f| f << contents } + end + + def git_root + ruby_core? ? source_root : source_root.parent + end + + def rake_path + find_base_path("rake") end - def ruby_core? - # avoid to warnings - @ruby_core ||= nil + def rake_version + File.basename(rake_path).delete_prefix("rake-").delete_suffix(".gem") + end - if @ruby_core.nil? - @ruby_core = true & ENV["GEM_COMMAND"] + def sinatra_dependency_paths + deps = %w[ + mustermann + rack + rack-protection + rack-session + tilt + sinatra + base64 + logger + compact_index + ] + path = if deps.all? {|dep| !Dir[scoped_base_system_gem_path.join("gems/#{dep}-*")].empty? } + scoped_base_system_gem_path + elsif ruby_core? && deps.all? {|dep| !Dir[source_root.join(".bundle/gems/#{dep}-*")].empty? } + source_root.join(".bundle") else - @ruby_core + scoped_base_system_gem_path end + + Dir[path.join("gems/{#{deps.join(",")}}-*/lib")].map(&:to_s) + end + + private + + def find_base_path(name) + Dir["#{scoped_base_system_gem_path}/**/#{name}-*.gem"].first + end + + def git_ls_files(glob) + skip "Not running on a git context, since running tests from a tarball" if ruby_core_tarball? + + git("ls-files -z -- #{glob}", source_root).split("\x0") + end + + def tracked_files_glob + ruby_core? ? "libexec/bundle* lib/bundler lib/bundler.rb spec/bundler man/bundle*" : "lib exe CHANGELOG.md LICENSE.md README.md bundler.gemspec" + end + + def lib_tracked_files_glob + ruby_core? ? "lib/bundler lib/bundler.rb" : "lib" + end + + def man_tracked_files_glob + "lib/bundler/man/bundle*.1.ronn lib/bundler/man/gemfile*.5.ronn" + end + + def ruby_core_tarball? + !git_root.join(".git").directory? + end + + def rubocop_gemfile_basename + tool_dir.join("rubocop_gems.rb") + end + + def standard_gemfile_basename + tool_dir.join("standard_gems.rb") + end + + def tool_dir + ruby_core? ? source_root.join("tool/bundler") : source_root.join("../tool/bundler") + end + + def templates_dir + lib_dir.join("bundler", "templates") end extend self diff --git a/spec/bundler/support/platforms.rb b/spec/bundler/support/platforms.rb index f4d63c8ded..56a0843005 100644 --- a/spec/bundler/support/platforms.rb +++ b/spec/bundler/support/platforms.rb @@ -2,81 +2,43 @@ 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 + def not_local + generic_local_platform == Gem::Platform::RUBY ? "java" : Gem::Platform::RUBY end - def specific_local_platform + def local_platform Bundler.local_platform end - def not_local - all_platforms.find {|p| p != generic_local_platform } + def generic_local_platform + Gem::Platform.generic(local_platform) end def local_tag - if RUBY_PLATFORM == "java" + if Gem.java_platform? :jruby + elsif Gem.win_platform? + :windows else :ruby end end def not_local_tag - [:ruby, :jruby].find {|tag| tag != local_tag } + [:jruby, :windows, :ruby].find {|tag| tag != local_tag } end def local_ruby_engine - ENV["BUNDLER_SPEC_RUBY_ENGINE"] || RUBY_ENGINE + RUBY_ENGINE end def local_engine_version - return ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] if ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] - - RUBY_ENGINE_VERSION + RUBY_ENGINE == "ruby" ? Gem.ruby_version : RUBY_ENGINE_VERSION end def not_local_engine_version case not_local_tag - when :ruby + when :ruby, :windows not_local_ruby_version when :jruby "1.6.1" @@ -91,16 +53,23 @@ module Spec 9999 end - def lockfile_platforms - local_platforms.map(&:to_s).sort.join("\n ") + def default_platform_list(*extra, defaults: default_locked_platforms) + defaults.concat(extra).map(&:to_s).uniq end - def local_platforms - if Bundler.feature_flag.specific_platform? - [local, specific_local_platform] - else - [local] - end + def lockfile_platforms(*extra, defaults: default_locked_platforms) + platforms = default_platform_list(*extra, defaults: defaults) + platforms.sort.join("\n ") + end + + def default_locked_platforms + [local_platform, generic_default_locked_platform].compact + end + + def generic_default_locked_platform + return unless Bundler::MatchPlatform.generic_local_platform_is_ruby? + + Gem::Platform::RUBY end end end diff --git a/spec/bundler/support/rubygems_ext.rb b/spec/bundler/support/rubygems_ext.rb index d237897b67..812dc4deaa 100644 --- a/spec/bundler/support/rubygems_ext.rb +++ b/spec/bundler/support/rubygems_ext.rb @@ -1,105 +1,222 @@ # frozen_string_literal: true +abort "RubyGems only supports Ruby 3.2 or higher" if RUBY_VERSION < "3.2.0" + require_relative "path" +$LOAD_PATH.unshift(Spec::Path.source_lib_dir.to_s) + module Spec module Rubygems - DEV_DEPS = { - "automatiek" => "~> 0.2.0", - "parallel_tests" => "~> 2.29", - "rake" => "~> 12.0", - "ronn" => "~> 0.7.3", - "rspec" => "~> 3.8", - "rubocop" => "= 0.74.0", - "rubocop-performance" => "= 1.4.0", - }.freeze - - DEPS = { - "rack" => "~> 2.0", - "rack-test" => "~> 1.1", - "artifice" => "~> 0.6.0", - "compact_index" => "~> 0.11.0", - "sinatra" => "~> 2.0", - # Rake version has to be consistent for tests to pass - "rake" => "12.3.2", - "builder" => "~> 3.2", - # ruby-graphviz is used by the viz tests - "ruby-graphviz" => ">= 0.a", - }.freeze - extend self - def dev_setup - deps = DEV_DEPS - - # JRuby can't build ronn, so we skip that - deps.delete("ronn") if RUBY_ENGINE == "jruby" + def gem_load(gem_name, bin_container) + require_relative "switch_rubygems" - install_gems(deps) + gem_load_and_activate(gem_name, bin_container) end - def gem_load(gem_name, bin_container) - require_relative "../rubygems/rubygems" - gem_load_and_activate(gem_name, bin_container) + def gem_load_and_possibly_install(gem_name, bin_container) + require_relative "switch_rubygems" + + gem_load_activate_and_possibly_install(gem_name, bin_container) end - def gem_require(gem_name) + def gem_require(gem_name, entrypoint) gem_activate(gem_name) - require gem_name + require entrypoint end - def setup - require "fileutils" + def test_setup + # Install test dependencies unless parallel-rspec is being used, since in that case they should be setup already + install_test_deps unless ENV["RSPEC_FORMATTER_OUTPUT_ID"] - Gem.clear_paths + setup_test_paths - ENV["BUNDLE_PATH"] = nil - ENV["GEM_HOME"] = ENV["GEM_PATH"] = Path.base_system_gems.to_s - ENV["PATH"] = [Path.bindir, Path.system_gem_path.join("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.join("manifest.txt") - # it's OK if there are extra gems - if !manifest_path.file? || !(manifest - manifest_path.readlines).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) - manifest_path.open("w") {|f| f << manifest.join } - end + require "fileutils" FileUtils.mkdir_p(Path.home) FileUtils.mkdir_p(Path.tmpdir) ENV["HOME"] = Path.home.to_s - ENV["TMPDIR"] = Path.tmpdir.to_s + # Remove "RUBY_CODESIGN", which is used by mkmf-generated Makefile to + # sign extension bundles on macOS, to avoid trying to find the specified key + # from the fake $HOME/Library/Keychains directory. + ENV.delete "RUBY_CODESIGN" + if Path.ruby_core? + if (tmpdir = ENV["TMPDIR"]) + tmpdir_real = begin + File.realpath(tmpdir) + rescue Errno::ENOENT, Errno::EACCES + tmpdir + end + ENV["TMPDIR"] = tmpdir_real if tmpdir_real != tmpdir + end + else + ENV["TMPDIR"] = Path.tmpdir.to_s + end require "rubygems/user_interaction" Gem::DefaultUserInteraction.ui = Gem::SilentUI.new end - private + def setup_test_paths + ENV["BUNDLE_PATH"] = nil + ENV["PATH"] = [Path.system_gem_path("bin"), ENV["PATH"]].join(File::PATH_SEPARATOR) + ENV["PATH"] = [Path.exedir, ENV["PATH"]].join(File::PATH_SEPARATOR) if Path.ruby_core? + end + + def install_test_deps + dev_bundle("install", gemfile: test_gemfile, path: Path.base_system_gem_path.to_s) + dev_bundle("install", gemfile: rubocop_gemfile, path: Path.rubocop_gem_path.to_s) + dev_bundle("install", gemfile: standard_gemfile, path: Path.standard_gem_path.to_s) + + require_relative "helpers" + Helpers.install_dev_bundler + + install_vendored_compact_index + end + + # Vendor `rubygems/rubygems.org#lib/compact_index/` under `tmp/compact_index/` + # so the artifice can serve compact-index responses without a runtime gem + # dependency. Pinned to a reviewed commit; override with COMPACT_INDEX_REF + # to refresh against another ref (the existing vendor copy is discarded). + def install_vendored_compact_index + target_root = Path.tmp_root.join("compact_index") + require "fileutils" + FileUtils.mkdir_p(Path.tmp_root) + + files = %w[ + lib/compact_index.rb + lib/compact_index/dependency.rb + lib/compact_index/gem.rb + lib/compact_index/gem_version.rb + lib/compact_index/versions_file.rb + ] + + # Serialize installs so parallel test setups don't race on the same + # vendor tree, and only skip the download when every file is present so + # an interrupted run can't leave a partial copy behind. + File.open(Path.tmp_root.join("compact_index.lock"), File::CREAT | File::RDWR) do |lock| + lock.flock(File::LOCK_EX) + + FileUtils.rm_rf(target_root) if ENV["COMPACT_INDEX_REF"] + + next if files.all? {|path| File.exist?(target_root.join(path)) } + + require "open-uri" + ref = ENV["COMPACT_INDEX_REF"] || "7c68a7b39761c61a66f9299f85b889ec39afc02c" + files.each do |path| + url = "https://raw.githubusercontent.com/rubygems/rubygems.org/#{ref}/#{path}" + target = target_root.join(path) + FileUtils.mkdir_p(File.dirname(target)) + tmp = "#{target}.tmp" + File.write(tmp, URI.parse(url).open(&:read)) + File.rename(tmp, target) + end + end + end + + def check_source_control_changes(success_message:, error_message:) + require "open3" + + output, status = Open3.capture2e("git status --porcelain") + + if status.success? && output.empty? + puts + puts success_message + puts + else + system("git diff") + + puts + puts error_message + puts + + exit(1) + end + end + + def dev_bundle(*args, gemfile: dev_gemfile, path: nil) + old_gemfile = ENV["BUNDLE_GEMFILE"] + old_orig_gemfile = ENV["BUNDLER_ORIG_BUNDLE_GEMFILE"] + ENV["BUNDLE_GEMFILE"] = gemfile.to_s + ENV["BUNDLER_ORIG_BUNDLE_GEMFILE"] = nil + + if path + old_path = ENV["BUNDLE_PATH"] + ENV["BUNDLE_PATH"] = path + else + old_path__system = ENV["BUNDLE_PATH__SYSTEM"] + ENV["BUNDLE_PATH__SYSTEM"] = "true" + end + + require "shellwords" + # We don't use `Open3` here because it does not work on JRuby + Windows + output = `ruby #{Path.dev_binstub} #{args.shelljoin}` + raise output unless $?.success? + output + ensure + if path + ENV["BUNDLE_PATH"] = old_path + else + ENV["BUNDLE_PATH__SYSTEM"] = old_path__system + end + + ENV["BUNDLER_ORIG_BUNDLE_GEMFILE"] = old_orig_gemfile + ENV["BUNDLE_GEMFILE"] = old_gemfile + end + + private def gem_load_and_activate(gem_name, bin_container) gem_activate(gem_name) load Gem.bin_path(gem_name, bin_container) rescue Gem::LoadError => e - abort "We couln't activate #{gem_name} (#{e.requirement}). Run `gem install #{gem_name}:'#{e.requirement}'`" + abort "We couldn't activate #{gem_name} (#{e.requirement}). Run `gem install #{gem_name}:'#{e.requirement}'`" + end + + def gem_load_activate_and_possibly_install(gem_name, bin_container) + gem_activate_and_possibly_install(gem_name) + load Gem.bin_path(gem_name, bin_container) + end + + def gem_activate_and_possibly_install(gem_name) + gem_activate(gem_name) + rescue Gem::LoadError => e + Gem.install(gem_name, e.requirement) + retry end def gem_activate(gem_name) - gem_requirement = DEV_DEPS[gem_name] + require_relative "activate" + require "bundler" + gem_requirement = Bundler::LockfileParser.new(File.read(dev_lockfile)).specs.find {|spec| spec.name == gem_name }.version gem gem_name, gem_requirement end - def install_gems(gems) - reqs, no_reqs = gems.partition {|_, req| !req.nil? && !req.split(" ").empty? } - no_reqs.map!(&:first) - reqs.map! {|name, req| "'#{name}:#{req}'" } - deps = reqs.concat(no_reqs).join(" ") - gem = Path.gem_bin - cmd = "#{gem} install #{deps} --no-document --conservative" - system(cmd) || raise("Installing gems #{deps} for the tests to use failed!") + def test_gemfile + Path.test_gemfile + end + + def rubocop_gemfile + Path.rubocop_gemfile + end + + def standard_gemfile + Path.standard_gemfile + end + + def dev_gemfile + Path.dev_gemfile + end + + def dev_lockfile + lockfile_for(dev_gemfile) + end + + def lockfile_for(gemfile) + Pathname.new("#{gemfile.expand_path}.lock") end end end diff --git a/spec/bundler/support/rubygems_version_manager.rb b/spec/bundler/support/rubygems_version_manager.rb index 356d391c08..c174c461f0 100644 --- a/spec/bundler/support/rubygems_version_manager.rb +++ b/spec/bundler/support/rubygems_version_manager.rb @@ -1,43 +1,64 @@ # frozen_string_literal: true -require "pathname" -require_relative "helpers" -require_relative "path" +require_relative "options" +require_relative "env" +require_relative "subprocess" class RubygemsVersionManager - include Spec::Helpers - include Spec::Path + include Spec::Options + include Spec::Env + include Spec::Subprocess - def initialize(env_version) - @env_version = env_version + def initialize(source) + @source = source end def switch return if use_system? - unrequire_rubygems_if_needed + assert_system_features_not_loaded! switch_local_copy_if_needed - prepare_environment + reexec_if_needed end -private + def assert_system_features_not_loaded! + at_exit do + rubylibdir = RbConfig::CONFIG["rubylibdir"] + + rubygems_path = rubylibdir + "/rubygems" + rubygems_default_path = rubygems_path + "/defaults" + + bundler_path = rubylibdir + "/bundler" + + bad_loaded_features = $LOADED_FEATURES.select do |loaded_feature| + (loaded_feature.start_with?(rubygems_path) && !loaded_feature.start_with?(rubygems_default_path)) || + loaded_feature.start_with?(bundler_path) + end + + errors = if bad_loaded_features.any? + all_commands_output + "the following features were incorrectly loaded:\n#{bad_loaded_features.join("\n")}" + end + + raise errors if errors + end + end + + private def use_system? - @env_version.nil? + @source.nil? end - def unrequire_rubygems_if_needed + def reexec_if_needed return unless rubygems_unrequire_needed? require "rbconfig" - ruby = File.join(RbConfig::CONFIG["bindir"], RbConfig::CONFIG["ruby_install_name"]) - ruby << RbConfig::CONFIG["EXEEXT"] + cmd = [RbConfig.ruby, $0, *ARGV].compact - cmd = [ruby, $0, *ARGV].compact - cmd[1, 0] = "--disable-gems" + ENV["RUBYOPT"] = opt_add("-I#{File.join(local_copy_path, "lib")}", opt_remove("--disable-gems", ENV["RUBYOPT"])) exec(ENV, *cmd) end @@ -45,37 +66,26 @@ private def switch_local_copy_if_needed return unless local_copy_switch_needed? - Dir.chdir(local_copy_path) do - sys_exec!("git remote update") - sys_exec!("git checkout #{target_tag_version} --quiet") - end - end + git("checkout #{target_tag}", local_copy_path) - def prepare_environment - $:.unshift File.expand_path("lib", local_copy_path) + ENV["RGV"] = local_copy_path end def rubygems_unrequire_needed? - defined?(Gem::VERSION) && Gem::VERSION != target_gem_version + require "rubygems" + !$LOADED_FEATURES.include?(File.join(local_copy_path, "lib/rubygems.rb")) end def local_copy_switch_needed? - !env_version_is_path? && target_gem_version != local_copy_version + !source_is_path? && target_tag != local_copy_tag end - def target_gem_version - @target_gem_version ||= resolve_target_gem_version + def target_tag + @target_tag ||= resolve_target_tag end - def target_tag_version - @target_tag_version ||= resolve_target_tag_version - end - - def local_copy_version - gemspec_contents = File.read(local_copy_path.join("lib/rubygems.rb")) - version_regexp = /VERSION = ["'](.*)["']/ - - version_regexp.match(gemspec_contents)[1] + def local_copy_tag + git("rev-parse --abbrev-ref HEAD", local_copy_path) end def local_copy_path @@ -83,45 +93,32 @@ private end def resolve_local_copy_path - return expanded_env_version if env_version_is_path? + return expanded_source if source_is_path? - rubygems_path = root.join("tmp/rubygems") + rubygems_path = File.join(source_root, "tmp/rubygems") - unless rubygems_path.directory? - rubygems_path.parent.mkpath - sys_exec!("git clone https://github.com/rubygems/rubygems.git #{rubygems_path}") + unless File.directory?(rubygems_path) + git("clone .. #{rubygems_path}", source_root) end rubygems_path end - def env_version_is_path? - expanded_env_version.directory? + def source_is_path? + File.directory?(expanded_source) end - def expanded_env_version - @expanded_env_version ||= Pathname.new(@env_version).expand_path(root) + def expanded_source + @expanded_source ||= File.expand_path(@source, source_root) end - def resolve_target_tag_version - return "v#{@env_version}" if @env_version.match(/^\d/) - - return "master" if @env_version == master_gem_version - - @env_version + def source_root + @source_root ||= File.expand_path(ruby_core? ? "../../.." : "../..", __dir__) end - def resolve_target_gem_version - return local_copy_version if env_version_is_path? - - return @env_version[1..-1] if @env_version.match(/^v/) - - return master_gem_version if @env_version == "master" - - @env_version - end + def resolve_target_tag + return "v#{@source}" if @source.match?(/^\d/) - def master_gem_version - "3.1.0.pre1" + @source end end diff --git a/spec/bundler/support/setup.rb b/spec/bundler/support/setup.rb new file mode 100644 index 0000000000..4ac2e5b472 --- /dev/null +++ b/spec/bundler/support/setup.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require_relative "switch_rubygems" + +require_relative "rubygems_ext" +Spec::Rubygems.install_test_deps + +require_relative "path" +$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__)) if Spec::Path.ruby_core? diff --git a/spec/bundler/support/shards.rb b/spec/bundler/support/shards.rb new file mode 100644 index 0000000000..ce33896539 --- /dev/null +++ b/spec/bundler/support/shards.rb @@ -0,0 +1,200 @@ +# frozen_string_literal: true + +# This classifies test files into 4 shards by running `bin/rspec --profile 10000` +# to ensure balanced execution times. When adding new test files, it is recommended to +# re-aggregate and adjust the shards to keep them balanced. +# For now, please add new files to shard 'shard_d'. + +module Spec + module Shards + EXAMPLE_MAPPINGS = { + shard_a: [ + "spec/runtime/setup_spec.rb", + "spec/commands/install_spec.rb", + "spec/commands/add_spec.rb", + "spec/install/gems/compact_index_spec.rb", + "spec/commands/config_spec.rb", + "spec/commands/pristine_spec.rb", + "spec/install/gemfile/path_spec.rb", + "spec/update/git_spec.rb", + "spec/commands/open_spec.rb", + "spec/commands/remove_spec.rb", + "spec/commands/show_spec.rb", + "spec/plugins/source/example_spec.rb", + "spec/commands/console_spec.rb", + "spec/runtime/require_spec.rb", + "spec/runtime/env_helpers_spec.rb", + "spec/runtime/gem_tasks_spec.rb", + "spec/install/gemfile_spec.rb", + "spec/commands/fund_spec.rb", + "spec/commands/init_spec.rb", + "spec/bundler/ruby_dsl_spec.rb", + "spec/bundler/mirror_spec.rb", + "spec/bundler/source/git/git_proxy_spec.rb", + "spec/bundler/source_list_spec.rb", + "spec/bundler/plugin/installer_spec.rb", + "spec/bundler/errors_spec.rb", + "spec/bundler/friendly_errors_spec.rb", + "spec/resolver/platform_spec.rb", + "spec/bundler/fetcher/downloader_spec.rb", + "spec/update/force_spec.rb", + "spec/bundler/env_spec.rb", + "spec/install/gems/mirror_spec.rb", + "spec/install/failure_spec.rb", + "spec/bundler/yaml_serializer_spec.rb", + "spec/bundler/environment_preserver_spec.rb", + "spec/install/gemfile/install_if_spec.rb", + "spec/install/gems/gemfile_source_header_spec.rb", + "spec/bundler/fetcher/base_spec.rb", + "spec/bundler/rubygems_integration_spec.rb", + "spec/bundler/worker_spec.rb", + "spec/bundler/dependency_spec.rb", + "spec/bundler/ui_spec.rb", + "spec/bundler/plugin/source_list_spec.rb", + "spec/bundler/source/path_spec.rb", + ], + shard_b: [ + "spec/install/gemfile/git_spec.rb", + "spec/install/gems/standalone_spec.rb", + "spec/commands/lock_spec.rb", + "spec/cache/gems_spec.rb", + "spec/other/major_deprecation_spec.rb", + "spec/install/gems/dependency_api_spec.rb", + "spec/install/gemfile/gemspec_spec.rb", + "spec/plugins/install_spec.rb", + "spec/commands/binstubs_spec.rb", + "spec/install/gems/flex_spec.rb", + "spec/runtime/inline_spec.rb", + "spec/commands/post_bundle_message_spec.rb", + "spec/runtime/executable_spec.rb", + "spec/lock/git_spec.rb", + "spec/plugins/hook_spec.rb", + "spec/install/allow_offline_install_spec.rb", + "spec/install/gems/post_install_spec.rb", + "spec/install/gemfile/ruby_spec.rb", + "spec/install/security_policy_spec.rb", + "spec/install/yanked_spec.rb", + "spec/update/gemfile_spec.rb", + "spec/runtime/load_spec.rb", + "spec/plugins/command_spec.rb", + "spec/commands/version_spec.rb", + "spec/install/prereleases_spec.rb", + "spec/bundler/uri_credentials_filter_spec.rb", + "spec/bundler/plugin_spec.rb", + "spec/install/gems/mirror_probe_spec.rb", + "spec/plugins/list_spec.rb", + "spec/bundler/compact_index_client/parser_spec.rb", + "spec/bundler/gem_version_promoter_spec.rb", + "spec/other/cli_dispatch_spec.rb", + "spec/bundler/source/rubygems_spec.rb", + "spec/cache/platform_spec.rb", + "spec/update/gems/fund_spec.rb", + "spec/bundler/stub_specification_spec.rb", + "spec/bundler/retry_spec.rb", + "spec/bundler/installer/spec_installation_spec.rb", + "spec/bundler/spec_set_spec.rb", + "spec/quality_es_spec.rb", + "spec/bundler/index_spec.rb", + "spec/other/cli_man_pages_spec.rb", + ], + shard_c: [ + "spec/commands/newgem_spec.rb", + "spec/commands/exec_spec.rb", + "spec/commands/clean_spec.rb", + "spec/commands/platform_spec.rb", + "spec/cache/git_spec.rb", + "spec/install/gemfile/groups_spec.rb", + "spec/commands/cache_spec.rb", + "spec/commands/check_spec.rb", + "spec/commands/list_spec.rb", + "spec/install/path_spec.rb", + "spec/bundler/cli_spec.rb", + "spec/install/bundler_spec.rb", + "spec/install/git_spec.rb", + "spec/commands/doctor_spec.rb", + "spec/bundler/dsl_spec.rb", + "spec/install/gems/fund_spec.rb", + "spec/install/gems/env_spec.rb", + "spec/bundler/ruby_version_spec.rb", + "spec/bundler/definition_spec.rb", + "spec/install/gemfile/eval_gemfile_spec.rb", + "spec/plugins/source_spec.rb", + "spec/install/gems/dependency_api_fallback_spec.rb", + "spec/plugins/uninstall_spec.rb", + "spec/bundler/plugin/index_spec.rb", + "spec/bundler/bundler_spec.rb", + "spec/bundler/fetcher_spec.rb", + "spec/bundler/source/rubygems/remote_spec.rb", + "spec/bundler/lockfile_parser_spec.rb", + "spec/cache/cache_path_spec.rb", + "spec/bundler/source/git_spec.rb", + "spec/bundler/source_spec.rb", + "spec/commands/ssl_spec.rb", + "spec/bundler/fetcher/compact_index_spec.rb", + "spec/bundler/plugin/api_spec.rb", + "spec/bundler/endpoint_specification_spec.rb", + "spec/bundler/fetcher/index_spec.rb", + "spec/bundler/settings/validator_spec.rb", + "spec/bundler/build_metadata_spec.rb", + "spec/bundler/current_ruby_spec.rb", + "spec/bundler/installer/gem_installer_spec.rb", + "spec/bundler/installer/parallel_installer_spec.rb", + "spec/bundler/cli_common_spec.rb", + "spec/bundler/ci_detector_spec.rb", + ], + shard_d: [ + "spec/bundler/rubygems_ext_spec.rb", + "spec/bundler/resolver/cooldown_spec.rb", + "spec/install/cooldown_spec.rb", + "spec/commands/outdated_spec.rb", + "spec/commands/update_spec.rb", + "spec/lock/lockfile_spec.rb", + "spec/install/deploy_spec.rb", + "spec/install/gemfile/sources_spec.rb", + "spec/runtime/self_management_spec.rb", + "spec/install/gemfile/specific_platform_spec.rb", + "spec/commands/info_spec.rb", + "spec/install/gems/resolving_spec.rb", + "spec/install/gemfile/platform_spec.rb", + "spec/bundler/gem_helper_spec.rb", + "spec/install/global_cache_spec.rb", + "spec/runtime/platform_spec.rb", + "spec/update/gems/post_install_spec.rb", + "spec/install/gems/native_extensions_spec.rb", + "spec/install/force_spec.rb", + "spec/cache/path_spec.rb", + "spec/install/gemspecs_spec.rb", + "spec/commands/help_spec.rb", + "spec/bundler/shared_helpers_spec.rb", + "spec/bundler/settings_spec.rb", + "spec/resolver/basic_spec.rb", + "spec/install/gemfile/force_ruby_platform_spec.rb", + "spec/commands/licenses_spec.rb", + "spec/install/gemfile/lockfile_spec.rb", + "spec/bundler/fetcher/dependency_spec.rb", + "spec/quality_spec.rb", + "spec/bundler/remote_specification_spec.rb", + "spec/install/process_lock_spec.rb", + "spec/install/binstubs_spec.rb", + "spec/bundler/compact_index_client/updater_spec.rb", + "spec/bundler/ui/shell_spec.rb", + "spec/other/ext_spec.rb", + "spec/commands/issue_spec.rb", + "spec/update/path_spec.rb", + "spec/bundler/plugin/api/source_spec.rb", + "spec/install/gems/win32_spec.rb", + "spec/bundler/plugin/dsl_spec.rb", + "spec/runtime/requiring_spec.rb", + "spec/bundler/plugin/events_spec.rb", + "spec/bundler/resolver/candidate_spec.rb", + "spec/bundler/digest_spec.rb", + "spec/bundler/fetcher/gem_remote_fetcher_spec.rb", + "spec/bundler/uri_normalizer_spec.rb", + "spec/install/gems/no_build_extension_spec.rb", + "spec/install/gems/no_install_plugin_spec.rb", + "spec/bundler/override_spec.rb", + "spec/install/gemfile/override_spec.rb", + ], + }.freeze + end +end diff --git a/spec/bundler/support/silent_logger.rb b/spec/bundler/support/silent_logger.rb deleted file mode 100644 index 8665beb2c9..0000000000 --- a/spec/bundler/support/silent_logger.rb +++ /dev/null @@ -1,10 +0,0 @@ -# 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 deleted file mode 100644 index 65a95ed59c..0000000000 --- a/spec/bundler/support/sometimes.rb +++ /dev/null @@ -1,57 +0,0 @@ -# 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 deleted file mode 100644 index a947eebf6f..0000000000 --- a/spec/bundler/support/streams.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require "stringio" - -def capture(*args) - opts = args.pop if args.last.is_a?(Hash) - opts ||= {} - - args.map!(&:to_s) - begin - result = StringIO.new - result.close if opts[:closed] - args.each {|stream| eval "$#{stream} = result" } - yield - ensure - args.each {|stream| eval("$#{stream} = #{stream.upcase}") } - end - result.string -end diff --git a/spec/bundler/support/subprocess.rb b/spec/bundler/support/subprocess.rb new file mode 100644 index 0000000000..91db80da48 --- /dev/null +++ b/spec/bundler/support/subprocess.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require_relative "command_execution" + +module Spec + module Subprocess + class TimeoutExceeded < StandardError; end + + def command_executions + @command_executions ||= [] + end + + def last_command + command_executions.last || raise("There is no last command") + end + + def out + last_command.stdout + end + + def err + last_command.stderr + end + + def stdboth + last_command.stdboth + end + + def exitstatus + last_command.exitstatus + end + + def git(cmd, path = Dir.pwd, options = {}) + sh("git #{cmd}", options.merge(dir: path)) + end + + def sh(cmd, options = {}) + dir = options[:dir] + env = options[:env] || {} + + command_execution = CommandExecution.new(cmd.to_s, timeout: options[:timeout] || 60) + + open3_opts = {} + open3_opts[:chdir] = dir if dir + + require "open3" + require "shellwords" + Open3.popen3(env, *cmd.shellsplit, **open3_opts) do |stdin, stdout, stderr, wait_thr| + yield stdin, stdout, wait_thr if block_given? + stdin.close + + stdout_handler = ->(data) { command_execution.original_stdout << data } + stderr_handler = ->(data) { command_execution.original_stderr << data } + + stdout_thread = read_stream(stdout, stdout_handler, timeout: command_execution.timeout) + stderr_thread = read_stream(stderr, stderr_handler, timeout: command_execution.timeout) + + stdout_thread.join + stderr_thread.join + + status = wait_thr.value + command_execution.exitstatus = if status.exited? + status.exitstatus + elsif status.signaled? + exit_status_for_signal(status.termsig) + end + rescue TimeoutExceeded + command_execution.failure_reason = :timeout + command_execution.exitstatus = exit_status_for_signal(Signal.list["INT"]) + end + + unless options[:raise_on_error] == false || command_execution.success? + command_execution.raise_error! + end + + command_executions << command_execution + + command_execution.stdout + end + + # Mostly copied from https://github.com/piotrmurach/tty-command/blob/49c37a895ccea107e8b78d20e4cb29de6a1a53c8/lib/tty/command/process_runner.rb#L165-L193 + def read_stream(stream, handler, timeout:) + Thread.new do + Thread.current.report_on_exception = false + cmd_start = Time.now + readers = [stream] + + while readers.any? + ready = IO.select(readers, nil, readers, timeout) + raise TimeoutExceeded if ready.nil? + + ready[0].each do |reader| + chunk = reader.readpartial(16 * 1024) + handler.call(chunk) + + # control total time spent reading + runtime = Time.now - cmd_start + time_left = timeout - runtime + raise TimeoutExceeded if time_left < 0.0 + rescue Errno::EAGAIN, Errno::EINTR + rescue EOFError, Errno::EPIPE, Errno::EIO + readers.delete(reader) + reader.close + end + end + end + end + + def all_commands_output + return "" if command_executions.empty? + + "\n\nCommands:\n#{command_executions.map(&:to_s_verbose).join("\n\n")}" + end + end +end diff --git a/spec/bundler/support/sudo.rb b/spec/bundler/support/sudo.rb deleted file mode 100644 index 04e9443945..0000000000 --- a/spec/bundler/support/sudo.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Spec - module Sudo - def self.present? - @which_sudo ||= Bundler.which("sudo") - end - - def sudo(cmd) - raise "sudo not present" unless Sudo.present? - sys_exec("sudo #{cmd}") - end - - def chown_system_gems_to_root - sudo "chown -R root #{system_gem_path}" - end - end -end diff --git a/spec/bundler/support/switch_rubygems.rb b/spec/bundler/support/switch_rubygems.rb new file mode 100644 index 0000000000..640b9f83b7 --- /dev/null +++ b/spec/bundler/support/switch_rubygems.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require_relative "rubygems_version_manager" +ENV["RGV"] ||= "." +RubygemsVersionManager.new(ENV["RGV"]).switch diff --git a/spec/bundler/support/the_bundle.rb b/spec/bundler/support/the_bundle.rb index f252a4515b..452abd7d41 100644 --- a/spec/bundler/support/the_bundle.rb +++ b/spec/bundler/support/the_bundle.rb @@ -8,10 +8,8 @@ module Spec 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? + def initialize + @bundle_dir = Pathname.new(bundled_app) end def to_s @@ -28,8 +26,16 @@ module Spec end def locked_gems - raise "Cannot read lockfile if it doesn't exist" unless locked? + raise ArgumentError, "Cannot read lockfile if it doesn't exist" unless locked? Bundler::LockfileParser.new(lockfile.read) end + + def locked_specs + locked_gems.specs.map(&:full_name) + end + + def locked_platforms + locked_gems.platforms.map(&:to_s) + end end end diff --git a/spec/bundler/support/vendored_net_http.rb b/spec/bundler/support/vendored_net_http.rb new file mode 100644 index 0000000000..8ff2ccd1fe --- /dev/null +++ b/spec/bundler/support/vendored_net_http.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# This defined? guard can be removed once RubyGems 3.4 support is dropped. +# +# Bundler specs load this code from `spec/support/vendored_net_http.rb` to avoid +# activating the Bundler gem too early. Without this guard, we get redefinition +# warnings once Bundler is actually activated and +# `lib/bundler/vendored_net_http.rb` is required. This is not an issue in +# RubyGems versions including `rubygems/vendored_net_http` since `require` takes +# care of avoiding the double load. +# +unless defined?(Gem::Net) + begin + require "rubygems/vendored_net_http" + rescue LoadError + begin + require "rubygems/net/http" + rescue LoadError + require "net/http" + Gem::Net = Net + end + end +end diff --git a/spec/bundler/update/force_spec.rb b/spec/bundler/update/force_spec.rb new file mode 100644 index 0000000000..325f58088a --- /dev/null +++ b/spec/bundler/update/force_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +RSpec.describe "bundle update" do + before :each do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + end + + it "re-installs installed gems with --force" do + myrack_lib = default_bundle_path("gems/myrack-1.0.0/lib/myrack.rb") + myrack_lib.open("w") {|f| f.write("blah blah blah") } + bundle :update, force: true + + expect(out).to include "Installing myrack 1.0.0" + expect(myrack_lib.open(&:read)).to eq("MYRACK = '1.0.0'\n") + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "re-installs installed gems with --redownload" do + myrack_lib = default_bundle_path("gems/myrack-1.0.0/lib/myrack.rb") + myrack_lib.open("w") {|f| f.write("blah blah blah") } + bundle :update, redownload: true + + expect(out).to include "Installing myrack 1.0.0" + expect(myrack_lib.open(&:read)).to eq("MYRACK = '1.0.0'\n") + expect(the_bundle).to include_gems "myrack 1.0.0" + end +end diff --git a/spec/bundler/update/gemfile_spec.rb b/spec/bundler/update/gemfile_spec.rb index 8c2bd9ccbf..f8849640b6 100644 --- a/spec/bundler/update/gemfile_spec.rb +++ b/spec/bundler/update/gemfile_spec.rb @@ -4,46 +4,44 @@ RSpec.describe "bundle update" do context "with --gemfile" do it "finds the gemfile" do gemfile bundled_app("NotGemfile"), <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' G - bundle! :install, :gemfile => bundled_app("NotGemfile") - bundle! :update, :gemfile => bundled_app("NotGemfile"), :all => true + bundle :install, gemfile: bundled_app("NotGemfile") + bundle :update, gemfile: bundled_app("NotGemfile"), all: true # Specify BUNDLE_GEMFILE for `the_bundle` # to retrieve the proper Gemfile ENV["BUNDLE_GEMFILE"] = "NotGemfile" - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end end context "with gemfile set via config" do before do gemfile bundled_app("NotGemfile"), <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' G - bundle "config set --local gemfile #{bundled_app("NotGemfile")}" - bundle! :install + bundle_config "gemfile #{bundled_app("NotGemfile")}" + bundle :install end it "uses the gemfile to update" do - bundle! "update", :all => true + bundle "update", all: true bundle "list" - expect(out).to include("rack (1.0.0)") + expect(out).to include("myrack (1.0.0)") end it "uses the gemfile while in a subdirectory" do bundled_app("subdir").mkpath - Dir.chdir(bundled_app("subdir")) do - bundle! "update", :all => true - bundle "list" + bundle "update", all: true, dir: bundled_app("subdir") + bundle "list", dir: bundled_app("subdir") - expect(out).to include("rack (1.0.0)") - end + expect(out).to include("myrack (1.0.0)") end end end diff --git a/spec/bundler/update/gems/fund_spec.rb b/spec/bundler/update/gems/fund_spec.rb new file mode 100644 index 0000000000..a5624d3e0a --- /dev/null +++ b/spec/bundler/update/gems/fund_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +RSpec.describe "bundle update" do + before do + build_repo2 do + build_gem "has_funding_and_other_metadata" do |s| + s.metadata = { + "bug_tracker_uri" => "https://example.com/user/bestgemever/issues", + "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md", + "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1", + "homepage_uri" => "https://bestgemever.example.io", + "mailing_list_uri" => "https://groups.example.com/bestgemever", + "funding_uri" => "https://example.com/has_funding_and_other_metadata/funding", + "source_code_uri" => "https://example.com/user/bestgemever", + "wiki_uri" => "https://example.com/user/bestgemever/wiki", + } + end + + build_gem "has_funding", "1.2.3" do |s| + s.metadata = { + "funding_uri" => "https://example.com/has_funding/funding", + } + end + end + + gemfile <<-G + source "https://gem.repo2" + gem 'has_funding_and_other_metadata' + gem 'has_funding', '< 2.0' + G + + bundle :install + end + + context "when listed gems are updated" do + before do + gemfile <<-G + source "https://gem.repo2" + gem 'has_funding_and_other_metadata' + gem 'has_funding' + G + + bundle :update, all: true + end + + it "displays fund message" do + expect(out).to include("2 installed gems you directly depend on are looking for funding.") + end + end +end diff --git a/spec/bundler/update/gems/post_install_spec.rb b/spec/bundler/update/gems/post_install_spec.rb index 5b061eb61b..9c71f6e0e3 100644 --- a/spec/bundler/update/gems/post_install_spec.rb +++ b/spec/bundler/update/gems/post_install_spec.rb @@ -5,22 +5,22 @@ RSpec.describe "bundle update" do before do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack', "< 1.0" + source "https://gem.repo1" + gem 'myrack', "< 1.0" gem 'thin' G - bundle! "config set #{config}" if config + bundle "config set #{config}" if config - bundle! :install + 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" } + let(:config) { "ignore_messages.myrack true" } it "doesn't display gem's post-install message" do - expect(out).not_to include("Rack's post install message") + expect(out).not_to include("Myrack's post install message") end end @@ -35,8 +35,8 @@ RSpec.describe "bundle update" do 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") + expect(out).to include("Post-install message from myrack:") + expect(out).to include("Myrack's post install message") end it "should not display the post-install message for non-updated gems" do @@ -47,12 +47,12 @@ RSpec.describe "bundle update" do context "when listed gem is updated" do before do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' gem 'thin' G - bundle! :update, :all => true + bundle :update, all: true end it_behaves_like "a post-install message outputter" @@ -62,12 +62,12 @@ RSpec.describe "bundle update" do context "when dependency triggers update" do before do gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem 'rack-obama' + source "https://gem.repo1" + gem 'myrack-obama' gem 'thin' G - bundle! :update, :all => true + bundle :update, all: true end it_behaves_like "a post-install message outputter" diff --git a/spec/bundler/update/git_spec.rb b/spec/bundler/update/git_spec.rb index 752033c842..526e988ab7 100644 --- a/spec/bundler/update/git_spec.rb +++ b/spec/bundler/update/git_spec.rb @@ -4,48 +4,51 @@ 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" + update_git "foo", branch: "omg" install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}", :branch => "omg" do gem 'foo' end G - update_git "foo", :branch => "omg" do |s| + update_git "foo" do |s| s.write "lib/foo.rb", "FOO = '1.1'" end - bundle "update", :all => true + bundle "update", all: true 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| + 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 + install_gemfile <<-G + source "https://gem.repo1" gem "rails", :git => "#{lib_path("rails")}" G - bundle! "update rails" + bundle "update rails" 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") + build_git "foo", "1.0", path: lib_path("foo") + update_git "foo", branch: "omg", path: lib_path("foo") install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo")}", :branch => "omg" do gem 'foo' end G - update_git "foo", :branch => "omg", :path => lib_path("foo") do |s| + update_git "foo", path: lib_path("foo") do |s| s.write "lib/foo.rb", "FOO = '1.1'" end @@ -54,18 +57,19 @@ RSpec.describe "bundle update" do 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_bundle => true do |s| + it "floats on main 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_bundle: true do |s| s.add_dependency "foo" end install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo")}" gem "bar" G - update_git "foo", :path => lib_path("foo") do |s| + update_git "foo", path: lib_path("foo") do |s| s.write "lib/foo.rb", "FOO = '1.1'" end @@ -75,16 +79,18 @@ RSpec.describe "bundle update" do 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") + build_git "foo", path: lib_path("foo_one") + build_git "foo", path: lib_path("foo_two") install_gemfile <<-G + source "https://gem.repo1" gem "foo", "1.0", :git => "#{lib_path("foo_one")}" G - FileUtils.rm_rf lib_path("foo_one") + FileUtils.rm_r lib_path("foo_one") install_gemfile <<-G + source "https://gem.repo1" gem "foo", "1.0", :git => "#{lib_path("foo_two")}" G @@ -95,28 +101,33 @@ RSpec.describe "bundle update" do it "fetches tags from the remote" do build_git "foo" - @remote = build_git("bar", :bare => true) - update_git "foo", :remote => file_uri_for(@remote.path) - update_git "foo", :push => "master" + @remote = build_git("bar", bare: true) + update_git "foo", remote: @remote.path + update_git "foo", push: "main" install_gemfile <<-G + source "https://gem.repo1" 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" + update_git "foo", tag: "fubar" + update_git "foo", push: "fubar" gemfile <<-G + source "https://gem.repo1" gem 'foo', :git => "#{@remote.path}", :tag => "fubar" G - bundle "update", :all => true - expect(exitstatus).to eq(0) if exitstatus + bundle "update", all: true + expect(err).to be_empty end describe "with submodules" do before :each do + # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ + system(*%W[git config --global protocol.file.allow always]) + build_repo4 do build_gem "submodule" do |s| s.write "lib/submodule.rb", "puts 'GEM'" @@ -131,15 +142,13 @@ RSpec.describe "bundle update" do 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 + git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") + git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") end it "it unlocks the source when submodules are added to a git source" do install_gemfile <<-G - source "#{file_uri_for(gem_repo4)}" + source "https://gem.repo4" git "#{lib_path("has_submodule-1.0")}" do gem "has_submodule" end @@ -149,7 +158,7 @@ RSpec.describe "bundle update" do expect(out).to eq("GEM") install_gemfile <<-G - source "#{file_uri_for(gem_repo4)}" + source "https://gem.repo4" git "#{lib_path("has_submodule-1.0")}", :submodules => true do gem "has_submodule" end @@ -159,25 +168,25 @@ RSpec.describe "bundle update" do 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 - source "#{file_uri_for(gem_repo4)}" + it "unlocks the source when submodules are removed from git source", git: ">= 2.9.0" do + install_gemfile <<-G + source "https://gem.repo4" git "#{lib_path("has_submodule-1.0")}", :submodules => true do gem "has_submodule" end G - run! "require 'submodule'" + run "require 'submodule'" expect(out).to eq("GIT") - install_gemfile! <<-G - source "#{file_uri_for(gem_repo4)}" + install_gemfile <<-G + source "https://gem.repo4" git "#{lib_path("has_submodule-1.0")}" do gem "has_submodule" end G - run! "require 'submodule'" + run "require 'submodule'" expect(out).to eq("GEM") end end @@ -186,91 +195,80 @@ RSpec.describe "bundle update" do build_git "foo", "1.0" install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G - lib_path("foo-1.0").join(".git").rmtree + FileUtils.rm_rf lib_path("foo-1.0").join(".git") - bundle :update, :all => true + bundle :update, all: true, raise_on_error: false expect(err).to include(lib_path("foo-1.0").to_s). and match(/Git error: command `git fetch.+has failed/) end it "should not explode on invalid revision on update of gem by name" do - build_git "rack", "0.8" + build_git "myrack", "0.8" - build_git "rack", "0.8", :path => lib_path("local-rack") do |s| - s.write "lib/rack.rb", "puts :LOCAL" + build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" end install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - bundle %(config set local.rack #{lib_path("local-rack")}) - bundle "update rack" + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle "update myrack" 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") + build_git "rails", "2.3.2", path: lib_path("rails") install_gemfile <<-G + source "https://gem.repo1" 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 + update_git "rails", "3.0", path: lib_path("rails"), gemspec: true - bundle "update", :all => true - 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]})") + bundle "update", all: true + expect(out).to include("Using rails 3.0 (was 2.3.2) from #{lib_path("rails")} (at main@#{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| + @git = build_git "foo", path: lib_path("foo") do |s| s.executables = "foobar" end install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" git "#{lib_path("foo")}" do gem 'foo' end - gem 'rack' + gem 'myrack' G end it "updates the source" do - update_git "foo", :path => @git.path + 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 + run <<-RUBY + require 'foo' + puts "WIN" if defined?(FOO_PREV_REF) + RUBY - expect(out).to eq("WIN") - end + expect(out).to eq("WIN") end it "unlocks gems that were originally pulled in by the source" do - update_git "foo", "2.0", :path => @git.path + update_git "foo", "2.0", path: @git.path bundle "update --source foo" expect(the_bundle).to include_gems "foo 2.0" @@ -278,76 +276,45 @@ RSpec.describe "bundle update" do it "leaves all other gems frozen" do update_repo2 - update_git "foo", :path => @git.path + update_git "foo", path: @git.path bundle "update --source foo" - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 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") + @git = build_git "foo", path: lib_path("bar") install_gemfile <<-G - source "#{file_uri_for(gem_repo2)}" + source "https://gem.repo2" git "#{lib_path("bar")}" do gem 'foo' end - gem 'rack' + gem 'myrack' G end - it "the --source flag updates version of gems that were originally pulled in by the source", :bundler => "< 3" do + 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| + update_git "foo", "2.0", path: @git.path do |s| s.write "foo.gemspec", spec_lines.join("\n") end - ref = @git.ref_for "master" + ref = @git.ref_for "main" bundle "update --source bar" - lockfile_should_be <<-G - GIT - remote: #{@git.path} - revision: #{ref} - specs: - foo (2.0) - - GEM - remote: #{file_uri_for(gem_repo2)}/ - specs: - rack (1.0.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - foo! - rack - - BUNDLED WITH - #{Bundler::VERSION} - G - end - - it "the --source flag updates version of gems that were originally pulled in by the source", :bundler => "3" 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") + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "2.0" + c.checksum gem_repo2, "myrack", "1.0.0" end - ref = @git.ref_for "master" - - bundle "update --source bar" - - lockfile_should_be <<-G + expect(lockfile).to eq <<~G GIT remote: #{@git.path} revision: #{ref} @@ -355,19 +322,19 @@ RSpec.describe "bundle update" do foo (2.0) GEM - remote: #{file_uri_for(gem_repo2)}/ + remote: https://gem.repo2/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES foo! - rack - + myrack + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G end end diff --git a/spec/bundler/update/path_spec.rb b/spec/bundler/update/path_spec.rb index 38c125e04b..8c76c94e1a 100644 --- a/spec/bundler/update/path_spec.rb +++ b/spec/bundler/update/path_spec.rb @@ -3,13 +3,14 @@ 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") + build_lib "activesupport", "2.3.5", path: lib_path("rails/activesupport") install_gemfile <<-G + source "https://gem.repo1" gem "activesupport", :path => "#{lib_path("rails/activesupport")}" G - build_lib "activesupport", "3.0", :path => lib_path("rails/activesupport") + 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")}`") diff --git a/spec/bundler/update/redownload_spec.rb b/spec/bundler/update/redownload_spec.rb deleted file mode 100644 index b34a02c78c..0000000000 --- a/spec/bundler/update/redownload_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe "bundle update" do - before :each do - install_gemfile <<-G - source "#{file_uri_for(gem_repo1)}" - gem "rack" - G - end - - describe "with --force" do - it "shows a deprecation when single flag passed", :bundler => 2 do - bundle! "update rack --force" - expect(err).to include "[DEPRECATED] The `--force` option has been renamed to `--redownload`" - end - - it "shows a deprecation when multiple flags passed", :bundler => 2 do - bundle! "update rack --no-color --force" - expect(err).to include "[DEPRECATED] The `--force` option has been renamed to `--redownload`" - end - end - - describe "with --redownload" do - it "does not show a deprecation when single flag passed" do - bundle! "update rack --redownload" - expect(err).not_to include "[DEPRECATED] The `--force` option has been renamed to `--redownload`" - end - - it "does not show a deprecation when single multiple flags passed" do - bundle! "update rack --no-color --redownload" - expect(err).not_to include "[DEPRECATED] The `--force` option has been renamed to `--redownload`" - end - end -end |
