ruby/spec/bundler/commands/exec_spec.rb
David Rodríguez 63c4223775 [rubygems/rubygems] Don't create an empty bundled_app when setting up deps
Running everything in `bundled_app` by default causes the `bundled_app`
helper to be used everytime, and that will create a scoped bundled_app
folder if it does not exist. That causes `bin/rake spec:deps` to create
an empty `tmp/2.1/bundled_app` folder which is a bit weird.

This commit changes specs to not switch to a (possibly empty)
bundled_app directory when not necessary (for example, when running
`gem` commands in order to setup test dependencies).

4bf89c0705
2025-07-25 11:10:36 +09:00

1286 lines
36 KiB
Ruby

# 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/rubygems/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/rubygems/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/rubygems/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 set --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 set --local 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 set 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 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 set 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 set 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 set auto_install 1"
bundle "config set --local 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
skip "https://github.com/rubygems/rubygems/issues/3351" if Gem.win_platform?
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 set 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}"
process_title = `ps -o args -p \#{Process.pid}`.split("\n", 2).last.strip
puts "PROCESS: \#{process_title}"
RUBY
before do
bundled_app(path).open("w") {|f| f << executable }
bundled_app(path).chmod(0o755)
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
title = "PROCESS: #{path}"
title += " arg1 arg2"
title
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
skip "https://github.com/rubygems/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
context "the executable exits explicitly" do
let(:executable) { super() << "\nexit #{exit_code}\nputs 'POST_EXIT'\n" }
context "with exit 0" do
it "runs" do
skip "https://github.com/rubygems/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 "with exit 99" do
let(:exit_code) { 99 }
it "runs" do
skip "https://github.com/rubygems/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
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/rubygems/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
skip "https://github.com/rubygems/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 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)}:10:in [`']<top \(required\)>': ERROR \(RuntimeError\)/
end
it "runs like a normally executed executable" do
skip "https://github.com/rubygems/rubygems/issues/3351" if Gem.win_platform?
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
skip "https://github.com/rubygems/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 "when the file uses the current ruby shebang" do
let(:shebang) { "#!#{Gem.ruby}" }
it "runs" do
skip "https://github.com/rubygems/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 "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
skip "https://github.com/rubygems/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 "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
skip "https://github.com/rubygems/rubygems/issues/3351" if Gem.win_platform?
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
skip "https://github.com/rubygems/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 "when disable_exec_load is set" do
let(:exec) { "EXEC: exec" }
let(:process) { "PROCESS: ruby #{path} arg1 arg2" }
before do
bundle "config set disable_exec_load true"
end
it "runs" do
skip "https://github.com/rubygems/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 "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
skip "https://github.com/rubygems/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 "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
skip "https://github.com/rubygems/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 "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
skip "https://github.com/rubygems/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
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]
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 "Didn't receive INT at all"
end.join
rescue Interrupt
puts "foo"
end
RUBY
it "receives the signal" do
skip "https://github.com/rubygems/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/rubygems/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/rubygems/rubygems/issues/3351" if Gem.win_platform?
gemfile <<-G
source "https://gem.repo1"
gem "myrack"
G
bundle "config set 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/rubygems/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
skip "https://github.com/rubygems/rubygems/issues/3351" if Gem.win_platform?
build_repo2 do
build_gem "rails", "6.1.0" do |s|
s.executables = "rails"
end
end
bundle "config set path vendor/bundle"
bundle "config set 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/rubygems/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 "custom openssl should not be loaded, it's not in the gemfile!"
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 set --local 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