diff options
Diffstat (limited to 'spec/bundler/commands/exec_spec.rb')
| -rw-r--r-- | spec/bundler/commands/exec_spec.rb | 1272 |
1 files changed, 1272 insertions, 0 deletions
diff --git a/spec/bundler/commands/exec_spec.rb b/spec/bundler/commands/exec_spec.rb new file mode 100644 index 0000000000..aa35685be8 --- /dev/null +++ b/spec/bundler/commands/exec_spec.rb @@ -0,0 +1,1272 @@ +# frozen_string_literal: true + +RSpec.describe "bundle exec" do + it "works with --gemfile flag" do + 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 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 + source "https://gem.repo1" + gem "myrack", "0.9.1" + G + + 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 + source "https://gem.repo1" + gem "myrack" + G + + bundle "exec myrackup" + expect(out).to eq("1.0.0") + end + + it "works when running from a random directory" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle "exec 'cd #{tmp("gems")} && myrackup'" + + expect(out).to eq("1.0.0") + end + + it "works when exec'ing something else" do + 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 "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") + puts `ps -ocommand= -p#{$$}` + RUBY + 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") + end + + it "accepts --verbose" do + 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 "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") + command.sync = true + command.write <<-G + if ARGV[0] + IO.for_fd(ARGV[0].to_i) + else + require 'tempfile' + io = Tempfile.new("io-test-fd") + args = %W[#{Gem.ruby} -I#{lib_dir} #{bindir.join("bundle")} exec --keep-file-descriptors #{Gem.ruby} #{command.path} \#{io.to_i}] + args << { io.to_i => io } + exec(*args) + end + G + + install_gemfile "source \"https://gem.repo1\"" + in_bundled_app "#{Gem.ruby} #{command.path}" + + expect(out).to be_empty + expect(err).to be_empty + end + + it "accepts --keep-file-descriptors" do + 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 + 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, bundled_app("--verbose")) + with_path_as(".") do + bundle "exec -- --verbose" + end + expect(out).to eq("foobar") + end + + it "handles different versions in different bundles" do + build_repo2 do + build_gem "myrack_two", "1.0.0" do |s| + s.executables = "myrackup" + end + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + G + + install_gemfile bundled_app2("Gemfile"), <<-G, dir: bundled_app2 + source "https://gem.repo2" + gem "myrack_two", "1.0.0" + G + + bundle "exec myrackup" + + expect(out).to eq("0.9.1") + + bundle "exec myrackup", dir: bundled_app2 + expect(out).to eq("1.0.0") + end + + context "with default gems" do + # 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 + install_gemfile "source \"https://gem.repo1\"" + end + + it "uses version provided by ruby" do + bundle "exec erb --version" + + expect(stdboth).to eq(default_erb_version) + end + end + + context "when specified in Gemfile directly" do + let(:specified_erb_version) { "2.0.0" } + + before do + build_repo2 do + build_gem "erb", specified_erb_version do |s| + s.executables = "erb" + end + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "erb", "#{specified_erb_version}" + G + end + + it "uses version specified" do + bundle "exec erb --version" + + expect(stdboth).to eq(specified_erb_version) + end + end + + context "when specified in Gemfile indirectly" do + let(:indirect_erb_version) { "2.0.0" } + + before do + build_repo2 do + build_gem "erb", indirect_erb_version do |s| + s.executables = "erb" + end + + build_gem "gem_depending_on_old_erb" do |s| + s.add_dependency "erb", indirect_erb_version + end + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "gem_depending_on_old_erb" + G + end + + it "uses resolved version" do + 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 "myrack_two", "1.0.0" do |s| + s.executables = "myrackup" + end + end + + bundle_config_global "path.system true" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + G + + install_gemfile bundled_app2("Gemfile"), <<-G, dir: bundled_app2 + source "https://gem.repo2" + gem "myrack_two", "1.0.0" + G + + bundle "exec myrackup" + + expect(last_command.stderr).to eq( + "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 + 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 "myrack_middleware" # myrack_middleware depends on myrack 0.9.1 + end + G + + bundle "exec myrackup" + + expect(out).to eq("0.9.1") + 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 + source "https://gem.repo1" + gem "myrack" + G + + bundler_setup_opt = "-r#{lib_dir}/bundler/setup" + + rubyopt = opt_add(bundler_setup_opt, ENV["RUBYOPT"]) + + bundle "exec echoopt" + expect(out.split(" ").count(bundler_setup_opt)).to eq(1) + + 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 + 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 echolib" + expect(out).to include(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 + source "https://gem.repo1" + gem "myrack" + G + + 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 + source "https://gem.repo1" + gem "myrack" + G + + 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 + source "https://gem.repo1" + gem "myrack" + G + + 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 + 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 "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 + + describe "with help flags" do + each_prefix = proc do |string, &blk| + 1.upto(string.length) {|l| blk.call(string[0, l]) } + end + each_prefix.call("exec") do |exec| + describe "when #{exec} is used" do + before(:each) do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + create_file("print_args", <<-'RUBY') + #!/usr/bin/env ruby + puts "args: #{ARGV.inspect}" + RUBY + end + + it "shows executable's man page when --help is after the executable" do + bundle "#{exec} print_args --help" + expect(out).to eq('args: ["--help"]') + end + + it "shows executable's man page when --help is after the executable and an argument" do + bundle "#{exec} print_args foo --help" + expect(out).to eq('args: ["foo", "--help"]') + + bundle "#{exec} print_args foo bar --help" + expect(out).to eq('args: ["foo", "bar", "--help"]') + + bundle "#{exec} print_args foo --help bar" + expect(out).to eq('args: ["foo", "--help", "bar"]') + end + + it "shows executable's man page when the executable has a -" do + FileUtils.mv(bundled_app("print_args"), bundled_app("docker-template")) + 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 + + it "shows executable's man page when --help is after another flag" do + bundle "#{exec} print_args --bar --help" + expect(out).to eq('args: ["--bar", "--help"]') + end + + it "uses executable's original behavior for -h" do + bundle "#{exec} print_args -h" + expect(out).to eq('args: ["-h"]') + end + + it "shows bundle-exec's man page when --help is between exec and the executable" do + with_fake_man do + bundle "#{exec} --help echo" + end + 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(%(["#{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(%(["#{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(%(["#{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(%(["#{man_dir}/bundle-exec.1"])) + end + end + end + end + + describe "with gem executables" do + describe "run from a random directory" do + before(:each) do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + end + + it "works when unlocked" do + 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")} && 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| + s.executables = "fizz" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "fizz", :path => "#{File.expand_path(home("fizz"))}" + G + end + + it "works when unlocked" do + bundle "exec fizz" + expect(out).to eq("1.0") + end + + it "works when locked" do + expect(the_bundle).to be_locked + + bundle "exec fizz" + expect(out).to eq("1.0") + end + end + + describe "from gems bundled via :git" do + before(:each) do + build_git "fizz_git" do |s| + s.executables = "fizz_git" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "fizz_git", :git => "#{lib_path("fizz_git-1.0")}" + G + end + + it "works when unlocked" do + bundle "exec fizz_git" + expect(out).to eq("1.0") + end + + it "works when locked" do + expect(the_bundle).to be_locked + bundle "exec fizz_git" + expect(out).to eq("1.0") + end + end + + describe "from gems bundled via :git with no gemspec" do + before(:each) do + build_git "fizz_no_gemspec", gemspec: false do |s| + s.executables = "fizz_no_gemspec" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "fizz_no_gemspec", "1.0", :git => "#{lib_path("fizz_no_gemspec-1.0")}" + G + end + + it "works when unlocked" do + bundle "exec fizz_no_gemspec" + expect(out).to eq("1.0") + end + + it "works when locked" do + expect(the_bundle).to be_locked + bundle "exec fizz_no_gemspec" + expect(out).to eq("1.0") + end + end + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + gem "foo" + G + + 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" + + gemspec = lib_path("foo-1.0").join("foo.gemspec").to_s + File.open(gemspec, "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = 'foo' + s.version = '1.0' + s.summary = 'TODO: Add summary' + s.authors = 'Me' + s.rubygems_version = nil + end + G + end + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :path => "#{lib_path("foo-1.0")}" + G + + 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(/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') + end + end + Bundler.rubygems.extend(Monkey) + G + 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.strip } + #{shebang} + + require "myrack" + puts "EXEC: \#{caller.grep(/load/).empty? ? 'exec' : 'load'}" + puts "ARGS: \#{$0} \#{ARGV.join(' ')}" + 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 + create_file(bundled_app(path), executable) + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + end + + let(:exec) { "EXEC: load" } + let(:args) { "ARGS: #{path} arg1 arg2" } + let(:myrack) { "MYRACK: 1.0.0" } + let(:process) do + if Gem.win_platform? + "PROCESS: ruby" + else + "PROCESS: #{path} arg1 arg2" + end + end + let(:exit_code) { 0 } + let(:expected) { [exec, args, myrack, process].join("\n") } + let(:expected_err) { "" } + + subject { bundle "exec #{path} arg1 arg2", raise_on_error: false } + + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end + + context "the executable exits explicitly" do + let(:executable) { super() << "\nexit #{exit_code}\nputs 'POST_EXIT'\n" } + + context "with exit 0" do + 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 "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end + end + end + + context "the executable exits by SignalException" do + let(:executable) do + ex = super() + ex << "\n" + ex << "raise SignalException, 'SIGTERM'\n" + ex + end + let(:expected_err) { "" } + let(:exit_code) do + 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 + end + + context "the executable is empty" do + let(:executable) { "" } + + let(:exit_code) { 0 } + let(:expected_err) { "#{path} is empty" } + let(:expected) { "" } + + 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 + /\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 + 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})\n#{system_gem_path("bin/bundle")}: Err (Err)" } + let(:expected) { super() } + + 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 "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" do + before do + system_gems(%w[myrack-1.0.0 myrack-0.9.1], path: default_bundle_path) + + gemfile <<-G + 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 } + 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 "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 and Gemfile is not the default" do + before do + 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) { "" } + + 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 "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) do + if Gem.win_platform? + "PROCESS: ruby" + else + "PROCESS: ruby #{path} arg1 arg2" + end + end + + before do + bundle_config "disable_exec_load true" + end + + 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 + let(:executable) { super() + <<-'RUBY' } + + puts "$0: #{$0.inspect}" + puts "__FILE__: #{__FILE__.inspect}" + RUBY + + context "when the path is absolute" do + let(:expected) { super() + <<~EOS.chomp } + + $0: #{path.to_s.inspect} + __FILE__: #{path.to_s.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 + + 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 "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(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 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) { <<~RUBY } + #{shebang} + begin + Thread.new do + puts 'Started' # For process sync + STDOUT.flush + sleep 1 # ignore quality_spec + raise RuntimeError, "Didn't receive expected INT" + end.join + rescue Interrupt + puts "foo" + end + RUBY + + it "receives the signal" do + 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 + + expect(out).to eq("foo") + end + end + + context "signals not being trapped by bunder" do + let(:executable) { <<~RUBY } + #{shebang} + + signals = #{test_signals.inspect} + result = signals.map do |sig| + Signal.trap(sig, "IGNORE") + end + puts result.select { |ret| ret == "IGNORE" }.count + 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}") + + expect(out).to eq(test_signals.count.to_s) + end + end + end + end + + 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 "https://gem.repo1" + gem "myrack" + G + bundle_config "path vendor/bundler" + bundle :install + end + + it "correctly shells out" do + file = bundled_app("file_that_bundle_execs.rb") + create_file(file, <<-RUBY) + #!#{Gem.ruby} + puts `bundle exec echo foo` + RUBY + file.chmod(0o777) + 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" } + + it "only leaves the default gem in the stdlib available" do + default_openssl_version = ruby "require 'openssl'; puts OpenSSL::VERSION" + + 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", <<-RUBY) + raise ArgumentError, "custom openssl should not be loaded" + RUBY + end + end + + system_gems("openssl-#{openssl_version}", gem_repo: gem_repo4) + + file = bundled_app("require_openssl.rb") + create_file(file, <<-RUBY) + #!/usr/bin/env ruby + require "openssl" + puts OpenSSL::VERSION + warn Gem.loaded_specs.values.map(&:full_name) + RUBY + file.chmod(0o777) + + env = { "PATH" => path } + aggregate_failures do + 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}", 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 |
