diff options
Diffstat (limited to 'spec/bundler/commands')
29 files changed, 20168 insertions, 0 deletions
diff --git a/spec/bundler/commands/add_spec.rb b/spec/bundler/commands/add_spec.rb new file mode 100644 index 0000000000..162650f2e5 --- /dev/null +++ b/spec/bundler/commands/add_spec.rb @@ -0,0 +1,448 @@ +# frozen_string_literal: true + +RSpec.describe "bundle add" do + before :each do + build_repo2 do + build_gem "foo", "1.1" + build_gem "foo", "2.0" + build_gem "baz", "1.2.3" + build_gem "bar", "0.12.3" + build_gem "cat", "0.12.3.pre" + build_gem "dog", "1.1.3.pre" + build_gem "lemur", "3.1.1.pre.2023.1.1" + end + + build_git "foo", "2.0" + + 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", 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 + bundle "add 'bar'" + expect(bundled_app_gemfile.read).to match(/gem "bar", ">= 0.12.3"/) + expect(the_bundle).to include_gems "bar 0.12.3" + end + + it "version requirement becomes >= major.minor when resolved version is > 1.0" do + bundle "add 'baz'" + expect(bundled_app_gemfile.read).to match(/gem "baz", ">= 1.2"/) + expect(the_bundle).to include_gems "baz 1.2.3" + end + + it "version requirement becomes >= major.minor.patch.pre when resolved version is < 1.0" do + bundle "add 'cat'" + expect(bundled_app_gemfile.read).to match(/gem "cat", ">= 0.12.3.pre"/) + expect(the_bundle).to include_gems "cat 0.12.3.pre" + end + + it "version requirement becomes >= major.minor.pre when resolved version is >= 1.0.pre" do + bundle "add 'dog'" + expect(bundled_app_gemfile.read).to match(/gem "dog", ">= 1.1.pre"/) + expect(the_bundle).to include_gems "dog 1.1.3.pre" + end + + 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(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(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(the_bundle).to include_gems "foo 2.0" + end + + it "adds dependency to more than one group" do + bundle "add 'foo' --group='development, test'" + expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2.0", groups: \[:development, :test\]/) + expect(the_bundle).to include_gems "foo 2.0" + end + end + + describe "with --source" do + it "adds dependency with specified source" do + bundle "add 'foo' --source='https://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 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(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" + end + + 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(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(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='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'", 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 "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='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'", 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'", raise_on_error: false + expect(err).to include("Could not fetch specs from file://does/not/exist/") + end + + describe "with --optimistic" do + 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") + expect(the_bundle).to include_gems "foo 2.0" + end + end + + describe "with no option" do + 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 --pessimistic and --strict" do + it "throws error" do + bundle "add 'foo' --strict --pessimistic", raise_on_error: false + + 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" + + 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", 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).") + end + end + + 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 "https://gem.repo2" + gem "myrack", "1.0" + G + + 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 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 "https://gem.repo2" + gem "myrack", "1.0" + G + + 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 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 "https://gem.repo2" + gem "myrack" + G + + 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 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 "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 new file mode 100644 index 0000000000..af4d24a9e8 --- /dev/null +++ b/spec/bundler/commands/binstubs_spec.rb @@ -0,0 +1,333 @@ +# frozen_string_literal: true + +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 "https://gem.repo1" + gem "myrack" + G + + bundle "binstubs myrack" + + expect(bundled_app("bin/myrackup")).to exist + end + + it "does not install other binstubs" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "rails" + G + + bundle "binstubs rails" + + 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 "https://gem.repo1" + gem "myrack" + gem "rails" + G + + bundle "binstubs rails myrack" + + 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 "https://gem.repo1" + gem "rails" + G + + 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 "https://gem.repo1" + gem "myrack" + G + + 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 "https://gem.repo1" + gem "myrack" + G + + 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 + + it "installs binstubs from git gems" do + FileUtils.mkdir_p(lib_path("foo/bin")) + FileUtils.touch(lib_path("foo/bin/foo")) + build_git "foo", "1.0", path: lib_path("foo") do |s| + s.executables = %w[foo] + end + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo")}" + G + + bundle "binstubs foo" + + expect(bundled_app("bin/foo")).to exist + end + + it "installs binstubs from path gems" do + FileUtils.mkdir_p(lib_path("foo/bin")) + FileUtils.touch(lib_path("foo/bin/foo")) + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.executables = %w[foo] + end + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :path => "#{lib_path("foo")}" + G + + bundle "binstubs foo" + + expect(bundled_app("bin/foo")).to exist + end + + it "sets correct permissions for binstubs" do + with_umask(0o002) do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + 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 "https://gem.repo1" + gem "myrack" + G + + bundle "binstubs myrack --shebang jruby" + expect(File.readlines(bundled_app("bin/myrackup")).first).to eq("#!/usr/bin/env jruby\n") + end + end + end + + context "when the gem doesn't exist" do + it "displays an error with correct status" do + install_gemfile <<-G + source "https://gem.repo1" + G + + bundle "binstubs doesnt_exist", raise_on_error: false + + expect(exitstatus).to eq(7) + expect(err).to include("Could not find gem 'doesnt_exist'.") + end + end + + context "with the binstubs dir configured" do + before do + bundle_config "bin exec" + end + + it "creates the binstubs in the configured dir" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle "binstubs myrack" + + expect(bundled_app("exec/myrackup")).to exist + end + end + + context "with --standalone option" do + before do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "rails" + G + end + + it "generates a standalone binstub" do + 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 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 --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 + + 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/myrackup"), "wb") do |file| + file.print "OMG" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle "binstubs myrack" + + 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/myrackup"), "wb") do |file| + file.print "OMG" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle "binstubs myrack --force" + + expect(bundled_app("bin/myrackup")).to exist + expect(File.read(bundled_app("bin/myrackup"))).not_to eq("OMG") + end + end + end + + context "when the gem has no bins" do + it "suggests child gems if they have bins" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack-obama" + G + + 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 "https://gem.repo1" + gem "actionpack" + G + + bundle "binstubs actionpack" + expect(err).to include("no executables for the gem actionpack") + 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 "https://gem.repo2" + gem "with_development_dependency" + G + + bundle "binstubs with_development_dependency" + expect(err).to include("no executables for the gem with_development_dependency") + end + end + + context "when BUNDLE_INSTALL is specified" do + it "performs an automatic bundle install" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + 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 "https://gem.repo1" + gem "myrack" + G + + 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 new file mode 100644 index 0000000000..e223d07f7f --- /dev/null +++ b/spec/bundler/commands/cache_spec.rb @@ -0,0 +1,620 @@ +# 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 "https://gem.repo1" + gem 'myrack' + G + + bundle "cache --gemfile=NotGemfile" + + ENV["BUNDLE_GEMFILE"] = "NotGemfile" + expect(the_bundle).to include_gems "myrack 1.0.0" + end + end + + context "with cache_all configured" do + context "without a gemspec" do + it "caches all dependencies except bundler itself" do + gemfile <<-D + source "https://gem.repo1" + gem 'myrack' + gem 'bundler' + D + + bundle_config "cache_all true" + bundle :cache + + 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 + + context "with a gemspec" do + context "that has the same name as the gem" do + before do + File.open(bundled_app("mygem.gemspec"), "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = "mygem" + s.version = "0.1.1" + s.summary = "" + s.authors = ["gem author"] + s.add_development_dependency "nokogiri", "=1.4.2" + end + G + end + end + + it "caches all dependencies except bundler and the gemspec specified gem" do + gemfile <<-D + source "https://gem.repo1" + gem 'myrack' + gemspec + D + + bundle_config "cache_all true" + bundle :cache + + 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 + end + end + + context "that has a different name as the gem" do + before do + File.open(bundled_app("mygem_diffname.gemspec"), "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = "mygem" + s.version = "0.1.1" + s.summary = "" + s.authors = ["gem author"] + s.add_development_dependency "nokogiri", "=1.4.2" + end + G + end + end + + it "caches all dependencies except bundler and the gemspec specified gem" do + gemfile <<-D + source "https://gem.repo1" + gem 'myrack' + gemspec + D + + bundle_config "cache_all true" + bundle :cache + + 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 + end + end + end + + context "with multiple gemspecs" do + before do + File.open(bundled_app("mygem.gemspec"), "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = "mygem" + s.version = "0.1.1" + s.summary = "" + s.authors = ["gem author"] + s.add_development_dependency "nokogiri", "=1.4.2" + end + G + end + File.open(bundled_app("mygem_client.gemspec"), "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = "mygem_test" + s.version = "0.1.1" + s.summary = "" + s.authors = ["gem author"] + s.add_development_dependency "weakling", "=0.0.3" + end + G + end + end + + it "caches all dependencies except bundler and the gemspec specified gems" do + gemfile <<-D + source "https://gem.repo1" + gem 'myrack' + gemspec :name => 'mygem' + gemspec :name => 'mygem_test' + D + + bundle_config "cache_all true" + bundle :cache + + 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 + expect(bundled_app("vendor/cache/mygem_test-0.1.1.gem")).to_not exist + expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist + end + end + end + + context "with --no-install" do + it "puts the gems in vendor/cache but does not install them" do + gemfile <<-D + source "https://gem.repo1" + gem 'myrack' + D + + bundle "cache --no-install" + + 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 "https://gem.repo1" + gem 'myrack' + D + + bundle "cache --no-install" + bundle "install" + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "does not prevent installing gems with bundle update" do + gemfile <<-D + source "https://gem.repo1" + gem "myrack", "1.0.0" + D + + bundle "cache --no-install" + bundle "update --all" + + 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 "https://gem.repo1" + gem 'myrack', :platforms => [:ruby_20, :windows_20] + D + + bundle "cache --all-platforms" + 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 + build_repo4 do + build_gem "uninstallable", "2.0" do |s| + s.add_development_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", "task(:default) { raise 'CANNOT INSTALL' }" + end + end + + 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 => "https://main.repo/extra" + end + G + + 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 "myrack 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 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 "https://gem.repo1" + gem "myrack" + G + 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 + + 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") + + gemfile <<-G + source "https://gem.repo4" + gem "foo" + gem "bar" + G + 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 + +RSpec.describe "bundle install with gem sources" do + describe "when cached and locked" do + it "does not hit the remote at all" do + build_repo2 + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + + bundle :cache + pristine_system_gems + FileUtils.rm_r gem_repo2 + + bundle "install --local" + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + 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 "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 + + 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 + + # 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 "https://gem.repo1" + gem "myrack" + G + bundle :cache + + 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 "myrack 1.0" + end + + it "ignores cached gems for the wrong platform" do + simulate_platform "java" do + install_gemfile <<-G + source "https://gem.repo1" + gem "platform_specific" + G + bundle :cache + end + + pristine_system_gems + + 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 + + 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 "https://gem.repo1" + gem "myrack" + G + bundled_app("vendor/cache").mkpath + expect(bundled_app("vendor/cache").children).to be_empty + + bundle "install --no-cache" + expect(bundled_app("vendor/cache").children).to be_empty + end + end +end diff --git a/spec/bundler/commands/check_spec.rb b/spec/bundler/commands/check_spec.rb new file mode 100644 index 0000000000..7fe6897ae3 --- /dev/null +++ b/spec/bundler/commands/check_spec.rb @@ -0,0 +1,601 @@ +# frozen_string_literal: true + +RSpec.describe "bundle check" do + it "returns success when the Gemfile is satisfied" do + install_gemfile <<-G + source "https://gem.repo1" + gem "rails" + G + + bundle :check + 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 "https://gem.repo1" + gem "rails" + G + + 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 "https://gem.repo1" + gem "rails" + G + + FileUtils.rm(bundled_app_lock) + + bundle "check" + + expect(bundled_app_lock).to exist + end + + it "does not create a Gemfile.lock if --dry-run was passed" do + install_gemfile <<-G + source "https://gem.repo1" + gem "rails" + G + + FileUtils.rm(bundled_app_lock) + + bundle "check --dry-run" + + expect(bundled_app_lock).not_to exist + end + + it "prints an error that shows missing gems" do + system_gems ["rails-2.3.2"], path: default_bundle_path + + gemfile <<-G + source "https://gem.repo1" + gem "rails" + G + + 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 an error that shows missing gems if a Gemfile.lock does not exist and a toplevel dependency is missing" do + gemfile <<-G + source "https://gem.repo1" + gem "rails" + G + + 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 "https://gem.repo2" + gem 'rails' + G + + gemfile <<-G + source "https://gem.repo2" + gem "rails" + gem "rails_pinned_to_old_activesupport" + G + + bundle :check, raise_on_error: false + expect(err).to include("Bundler can't satisfy your Gemfile's dependencies.") + end + + it "uses the without setting" do + bundle_config "without foo" + install_gemfile <<-G + source "https://gem.repo1" + group :foo do + gem "myrack" + end + G + + bundle "check" + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "ensures that gems are actually installed and not just cached" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack", :group => :foo + G + + bundle_config "without foo" + bundle :install + + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + 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 "https://gem.repo1" + gem "myrack" + G + + 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 "https://gem.repo1" + gem "myrack" + platforms :#{not_local_tag} do + gem "activesupport" + end + G + + system_gems "myrack-1.0.0", path: default_bundle_path + + lockfile <<-G + GEM + remote: https://gem.repo1/ + specs: + activesupport (2.3.5) + myrack (1.0.0) + + PLATFORMS + #{generic_local_platform} + #{not_local} + + DEPENDENCIES + myrack + activesupport + G + + bundle :check + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "works with env conditionals" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + env :NOT_GOING_TO_BE_SET do + gem "activesupport" + end + G + + system_gems "myrack-1.0.0", path: default_bundle_path + + lockfile <<-G + GEM + remote: https://gem.repo1/ + specs: + activesupport (2.3.5) + myrack (1.0.0) + + PLATFORMS + #{generic_local_platform} + #{not_local} + + DEPENDENCIES + myrack + activesupport + G + + bundle :check + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "outputs an error when the default Gemfile is not found" do + bundle :check, 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, raise_on_error: false + expect(exitstatus).to eq(10) + expect(err).not_to include("Unfortunately, a fatal error has occurred. ") + end + + it "fails when there's no lockfile and frozen is set" do + install_gemfile <<-G + source "https://gem.repo1" + gem "foo" + G + + 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 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 + + 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 + + gemfile <<-G + source "https://gem.repo4" + gem "depends_on_myrack" + gem "also_depends_on_myrack" + G + + bundle "lock" + 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 locked under multiple platforms" do + before :each do + build_repo4 do + build_gem "myrack" + end + + 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 + + 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 "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 + + s.add_dependency "awesome_print" + end + + 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 + + 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 + + original_lockfile = lockfile + + # 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 + GEM + remote: https://gem.repo1/ + specs: + myrack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + L + + if bundler_version + lock += "\nBUNDLED WITH\n #{bundler_version}\n" + end + + lock + end + + before do + bundle_config "path vendor/bundle" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + end + + context "is not present" do + it "does not change the lock" do + lockfile lock_with(nil) + bundle :check + expect(lockfile).to eq lock_with(nil) + end + end + + context "is newer" do + it "does not change the lock and does not warn" do + lockfile 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 + system_gems "bundler-1.18.0" + lockfile lock_with("1.18.0") + bundle :check + expect(lockfile).to eq lock_with("1.18.0") + end + end + end +end diff --git a/spec/bundler/commands/clean_spec.rb b/spec/bundler/commands/clean_spec.rb new file mode 100644 index 0000000000..c77859d378 --- /dev/null +++ b/spec/bundler/commands/clean_spec.rb @@ -0,0 +1,936 @@ +# frozen_string_literal: true + +RSpec.describe "bundle clean" do + def should_have_gems(*gems) + gems.each do |g| + expect(vendored_gems("gems/#{g}")).to exist + expect(vendored_gems("specifications/#{g}.gemspec")).to exist + expect(vendored_gems("cache/#{g}.gem")).to exist + end + end + + def should_not_have_gems(*gems) + gems.each do |g| + expect(vendored_gems("gems/#{g}")).not_to exist + expect(vendored_gems("specifications/#{g}.gemspec")).not_to exist + expect(vendored_gems("cache/#{g}.gem")).not_to exist + end + end + + it "removes unused gems that are different" do + gemfile <<-G + source "https://gem.repo1" + + gem "thin" + gem "foo" + G + + bundle_config "path vendor/bundle" + bundle_config "clean false" + bundle "install" + + gemfile <<-G + source "https://gem.repo1" + + gem "thin" + G + bundle "install" + + bundle :clean + + expect(out).to include("Removing foo (1.0)") + + should_have_gems "thin-1.0", "myrack-1.0.0" + should_not_have_gems "foo-1.0" + + expect(vendored_gems("bin/myrackup")).to exist + end + + it "removes old version of gem if unused" do + gemfile <<-G + source "https://gem.repo1" + + gem "myrack", "0.9.1" + gem "foo" + G + + bundle_config "path vendor/bundle" + bundle_config "clean false" + bundle "install" + + gemfile <<-G + source "https://gem.repo1" + + gem "myrack", "1.0.0" + gem "foo" + G + bundle "install" + + bundle :clean + + expect(out).to include("Removing myrack (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/myrackup")).to exist + end + + it "removes new version of gem if unused" do + gemfile <<-G + source "https://gem.repo1" + + gem "myrack", "1.0.0" + gem "foo" + G + + bundle_config "path vendor/bundle" + bundle_config "clean false" + bundle "install" + + gemfile <<-G + source "https://gem.repo1" + + gem "myrack", "0.9.1" + gem "foo" + G + bundle "update myrack" + + bundle :clean + + expect(out).to include("Removing myrack (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/myrackup")).to exist + end + + it "removes gems in bundle without groups" do + gemfile <<-G + source "https://gem.repo1" + + gem "foo" + + group :test_group do + gem "myrack", "1.0.0" + end + G + + bundle_config "path vendor/bundle" + bundle "install" + bundle_config "without test_group" + bundle "install" + bundle :clean + + expect(out).to include("Removing myrack (1.0.0)") + + should_have_gems "foo-1.0" + should_not_have_gems "myrack-1.0.0" + + expect(vendored_gems("bin/myrackup")).to_not exist + end + + it "does not remove cached git dir if it's being used" do + build_git "foo" + revision = revision_for(lib_path("foo-1.0")) + git_path = lib_path("foo-1.0") + + gemfile <<-G + source "https://gem.repo1" + + gem "myrack", "1.0.0" + git "#{git_path}", :ref => "#{revision}" do + gem "foo" + end + G + + bundle_config "path vendor/bundle" + bundle "install" + + bundle :clean + + digest = Digest(:SHA1).hexdigest(git_path.to_s) + 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") + git_path = lib_path("foo") + revision = revision_for(git_path) + + gemfile <<-G + source "https://gem.repo1" + + gem "myrack", "1.0.0" + git "#{git_path}", :ref => "#{revision}" do + gem "foo" + end + G + + bundle_config "path vendor/bundle" + bundle "install" + + gemfile <<-G + source "https://gem.repo1" + + gem "myrack", "1.0.0" + G + bundle "install" + + bundle :clean + + expect(out).to include("Removing foo (#{revision[0..11]})") + + 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/myrack-1.0.0.gemspec")).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") + git_path = lib_path("foo") + revision = revision_for(git_path) + + gemfile <<-G + source "https://gem.repo1" + + gem "myrack", "1.0.0" + git "#{git_path}", :ref => "#{revision}" do + gem "foo" + end + G + + FileUtils.mkdir_p(bundled_app("real-path")) + File.symlink(bundled_app("real-path"), bundled_app("symlink-path")) + + bundle_config "path #{bundled_app("symlink-path")}" + bundle "install" + + bundle :clean + + expect(out).not_to include("Removing foo (#{revision[0..11]})") + + expect(bundled_app("symlink-path/#{Bundler.ruby_scope}/bundler/gems/foo-#{revision[0..11]}")).to exist + end + + 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 "https://gem.repo1" + + gem "myrack", "1.0.0" + git "#{lib_path("foo-bar")}" do + gem "foo-bar" + end + G + + bundle_config "path vendor/bundle" + bundle "install" + + update_git "foo-bar", path: lib_path("foo-bar") + revision2 = revision_for(lib_path("foo-bar")) + + bundle "update", all: true + bundle :clean + + expect(out).to include("Removing foo-bar (#{revision[0..11]})") + + 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/myrack-1.0.0.gemspec")).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| + 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 "path vendor/bundle" + bundle "install" + bundle :clean + expect(out).to include("") + + expect(vendored_gems("bundler/gems/rails-#{revision[0..11]}")).to exist + end + + it "does not remove git sources that are in without groups" do + build_git "foo", path: lib_path("foo") + git_path = lib_path("foo") + revision = revision_for(git_path) + + gemfile <<-G + source "https://gem.repo1" + + gem "myrack", "1.0.0" + group :test do + git "#{git_path}", :ref => "#{revision}" do + gem "foo" + end + end + G + bundle_config "path vendor/bundle" + bundle_config "without test" + bundle "install" + + bundle :clean + + expect(out).to include("") + expect(vendored_gems("bundler/gems/foo-#{revision[0..11]}")).to exist + digest = Digest(:SHA1).hexdigest(git_path.to_s) + expect(vendored_gems("cache/bundler/git/foo-#{digest}")).to_not exist + end + + it "does not blow up when using without groups" do + gemfile <<-G + source "https://gem.repo1" + + gem "myrack" + + group :development do + gem "foo" + end + G + + bundle_config "path vendor/bundle" + bundle_config "without development" + bundle "install" + + bundle :clean + end + + it "displays an error when used without --path" do + bundle_config "path.system true" + install_gemfile <<-G + source "https://gem.repo1" + + gem "myrack", "1.0.0" + G + + bundle :clean, raise_on_error: false + + 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 "https://gem.repo1" + + gem "thin" + gem "foo" + G + + bundle_config "path vendor/bundle" + bundle "install" + + gemfile <<-G + source "https://gem.repo1" + + gem "foo" + G + bundle "install" + + 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", "myrack-1.0" + should_have_gems "foo-1.0" + + expect(vendored_gems("bin/myrackup")).not_to exist + end + + it "does not call clean automatically when using system gems" do + bundle_config "path.system true" + + install_gemfile <<-G + source "https://gem.repo1" + + gem "thin" + gem "myrack" + G + + install_gemfile <<-G + source "https://gem.repo1" + + gem "myrack" + G + + installed_gems_list + expect(out).to include("myrack (1.0.0)").and include("thin (1.0)") + end + + it "does not clean on bundle update when path has not been set" do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + + gem "foo" + G + + update_repo2 do + build_gem "foo", "1.0.1" + end + + bundle "update", all: true + + 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 "will automatically clean on bundle update when path has not been set", bundler: "5" do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + + gem "foo" + G + + update_repo2 do + build_gem "foo", "1.0.1" + end + + bundle "update", all: true + + 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 + /specifications/foo-1.0.1.gemspec + ] + end + + it "does not clean automatically when path configured" do + gemfile <<-G + source "https://gem.repo1" + + gem "thin" + gem "myrack" + G + bundle_config "path vendor/bundle" + bundle "install" + + gemfile <<-G + source "https://gem.repo1" + + gem "myrack" + G + bundle "install" + + should_have_gems "myrack-1.0.0", "thin-1.0" + end + + it "does not clean on bundle update when path configured" do + build_repo2 + + gemfile <<-G + source "https://gem.repo2" + + gem "foo" + G + bundle_config "path vendor/bundle" + bundle "install" + + update_repo2 do + build_gem "foo", "1.0.1" + end + + bundle :update, all: true + should_have_gems "foo-1.0", "foo-1.0.1" + end + + it "does not clean on bundle update when installing to system gems" do + bundle_config "path.system true" + + build_repo2 + + gemfile <<-G + source "https://gem.repo2" + + gem "foo" + G + bundle "install" + + update_repo2 do + build_gem "foo", "1.0.1" + end + bundle :update, all: true + + 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 "path.system true" + + gemfile <<-G + source "https://gem.repo1" + + gem "foo" + gem "myrack" + G + bundle :install + + gemfile <<-G + source "https://gem.repo1" + + gem "myrack" + G + bundle :install + bundle "clean --force" + + expect(out).to include("Removing foo (1.0)") + installed_gems_list + expect(out).not_to include("foo (1.0)") + expect(out).to include("myrack (1.0.0)") + end + + describe "when missing permissions", :permissions do + before { ENV["BUNDLE_PATH__SYSTEM"] = "true" } + let(:system_cache_path) { system_gem_path("cache") } + after do + FileUtils.chmod(0o755, system_cache_path) + end + it "returns a helpful error message" do + gemfile <<-G + source "https://gem.repo1" + + gem "foo" + gem "myrack" + G + bundle :install + + gemfile <<-G + source "https://gem.repo1" + + gem "myrack" + G + bundle :install + + FileUtils.chmod(0o500, system_cache_path) + + bundle :clean, force: true, raise_on_error: false + + expect(err).to include(system_gem_path.to_s) + expect(err).to include("grant write permissions") + + installed_gems_list + expect(out).to include("foo (1.0)") + expect(out).to include("myrack (1.0.0)") + end + end + + it "cleans git gems with a 7 length git revision" do + build_git "foo" + revision = revision_for(lib_path("foo-1.0")) + + gemfile <<-G + source "https://gem.repo1" + + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + bundle_config "path vendor/bundle" + bundle "install" + + # mimic 7 length git revisions in Gemfile.lock + 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_lock, gemfile_lock.join("\n")) + + bundle_config "path vendor/bundle" + bundle "install" + + bundle :clean + + expect(out).not_to include("Removing foo (1.0 #{revision[0..6]})") + + expect(vendored_gems("bundler/gems/foo-1.0-#{revision[0..6]}")).to exist + end + + it "when using --force on system gems, it doesn't remove binaries" do + bundle_config "path.system true" + + build_repo2 do + build_gem "bindir" do |s| + s.bindir = "exe" + s.executables = "foo" + end + end + + gemfile <<-G + source "https://gem.repo2" + + gem "bindir" + G + bundle :install + + bundle "clean --force" + + sys_exec "foo" + + 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) + FileUtils.mkdir_p("#{absolute_path}/lib/bar") + File.open("#{absolute_path}/lib/bar/bar.rb", "wb") do |file| + file.puts "module Bar; end" + end + + gemfile <<-G + source "https://gem.repo1" + + gem "foo" + gem "bar", "1.0", :path => "#{relative_path}" + G + + bundle_config "path vendor/bundle" + bundle "install" + bundle :clean + end + + it "doesn't remove gems in dry-run mode with path set" do + gemfile <<-G + source "https://gem.repo1" + + gem "thin" + gem "foo" + G + + bundle_config "path vendor/bundle" + bundle_config "clean false" + bundle "install" + + gemfile <<-G + source "https://gem.repo1" + + gem "thin" + G + + bundle :install + + bundle "clean --dry-run" + + expect(out).not_to include("Removing foo (1.0)") + expect(out).to include("Would have removed foo (1.0)") + + should_have_gems "thin-1.0", "myrack-1.0.0", "foo-1.0" + + 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 "https://gem.repo1" + + gem "thin" + gem "foo" + G + + bundle_config "path vendor/bundle" + bundle_config "clean false" + bundle "install" + + gemfile <<-G + source "https://gem.repo1" + + gem "thin" + G + + bundle :install + + bundle "clean --dry-run" + + expect(out).not_to include("Removing foo (1.0)") + expect(out).to include("Would have removed foo (1.0)") + + should_have_gems "thin-1.0", "myrack-1.0.0", "foo-1.0" + + expect(vendored_gems("bin/myrackup")).to exist + end + + it "doesn't store dry run as a config setting" do + gemfile <<-G + source "https://gem.repo1" + + gem "thin" + gem "foo" + G + + bundle_config "path vendor/bundle" + bundle_config "clean false" + bundle "install" + bundle_config "dry_run false" + + gemfile <<-G + source "https://gem.repo1" + + gem "thin" + G + + bundle :install + + bundle "clean" + + expect(out).to include("Removing foo (1.0)") + expect(out).not_to include("Would have removed foo (1.0)") + + should_have_gems "thin-1.0", "myrack-1.0.0" + should_not_have_gems "foo-1.0" + + expect(vendored_gems("bin/myrackup")).to exist + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "https://gem.repo1" + + gem "thin" + gem "foo" + G + + bundle_config "path vendor/bundle" + bundle_config "clean false" + bundle "install" + + gemfile <<-G + source "https://gem.repo1" + + gem "thin" + gem "weakling" + G + + bundle_config "auto_install 1" + bundle :clean + expect(out).to include("Installing weakling 0.0.3") + should_have_gems "thin-1.0", "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" do + build_git "very_simple_git_binary", &:add_c_extension + + revision = revision_for(lib_path("very_simple_git_binary-1.0")) + + gemfile <<-G + source "https://gem.repo1" + + gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}" + G + + 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 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" do + gemfile <<-G + source "https://gem.repo1" + + gem "thin" + gem "very_simple_binary" + gem "simple_binary" + G + + bundle_config "path vendor/bundle" + bundle "install" + + very_simple_binary_extensions_dir = + Pathname.glob("#{vendored_gems}/extensions/*/*/very_simple_binary-1.0").first + + simple_binary_extensions_dir = + Pathname.glob("#{vendored_gems}/extensions/*/*/simple_binary-1.0").first + + expect(very_simple_binary_extensions_dir).to exist + expect(simple_binary_extensions_dir).to exist + + gemfile <<-G + source "https://gem.repo1" + + gem "thin" + gem "simple_binary" + G + + 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" 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 "https://gem.repo1" + + gem "thin" + gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}" + G + + 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 + + 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 + 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 + 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" 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 "https://gem.repo1" + + group :development do + gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}" + end + G + + bundle :lock + 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 new file mode 100644 index 0000000000..e8ab32ca5d --- /dev/null +++ b/spec/bundler/commands/config_spec.rb @@ -0,0 +1,646 @@ +# frozen_string_literal: true + +RSpec.describe ".bundle/config" do + describe "config" do + before { bundle "config set foo bar" } + + it "prints a detailed report of local and user configuration" do + bundle "config list" + + expect(out).to include("Settings are listed in order of priority. The top value will be used") + expect(out).to include("foo\nSet for the current user") + expect(out).to include(": \"bar\"") + end + + context "given --parseable flag" do + it "prints a minimal report of local and user configuration" do + bundle "config list --parseable" + expect(out).to include("foo=bar") + end + + context "with global config" do + it "prints config assigned to local scope" do + bundle "config set --local foo bar2" + bundle "config list --parseable" + expect(out).to include("foo=bar2") + end + end + + context "with env overwrite" do + it "prints config with env" do + bundle "config list --parseable", env: { "BUNDLE_FOO" => "bar3" } + expect(out).to include("foo=bar3") + end + end + end + end + + describe "location with a gemfile" do + before :each do + gemfile <<-G + 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 "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 "myrack 1.0.0" + end + + it "can provide a relative path with the environment variable" do + FileUtils.mkdir_p bundled_app("omg") + + ENV["BUNDLE_APP_CONFIG"] = "../foo" + 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 "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 "https://gem.repo1" + gem "myrack", "1.0.0" + G + end + + it "is the default" do + bundle "config set foo global" + run "puts Bundler.settings[:foo]" + expect(out).to eq("global") + end + + it "can also be set explicitly" do + bundle "config set --global foo global" + run "puts Bundler.settings[:foo]" + expect(out).to eq("global") + end + + it "has lower precedence than local" do + bundle "config set --local foo local" + + bundle "config set --global foo global" + expect(out).to match(/Your application has set foo to "local"/) + + run "puts Bundler.settings[:foo]" + expect(out).to eq("local") + end + + it "has lower precedence than env" do + 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"/) + + run "puts Bundler.settings[:foo]" + expect(out).to eq("env") + ensure + ENV.delete("BUNDLE_FOO") + end + + it "can be deleted" do + bundle "config set --global foo global" + bundle "config unset foo" + + run "puts Bundler.settings[:foo] == nil" + expect(out).to eq("true") + end + + it "warns when overriding" do + bundle "config set --global foo previous" + bundle "config set --global foo global" + expect(out).to match(/You are replacing the current global value of foo/) + + run "puts Bundler.settings[:foo]" + expect(out).to eq("global") + end + + it "does not warn when using the same value twice" do + bundle "config set --global foo value" + bundle "config set --global foo value" + expect(out).not_to match(/You are replacing the current global value of foo/) + + run "puts Bundler.settings[:foo]" + expect(out).to eq("value") + end + + it "expands the path at time of setting" do + bundle "config set --global local.foo .." + run "puts Bundler.settings['local.foo']" + expect(out).to eq(File.expand_path(bundled_app.to_s + "/..")) + end + + it "saves with parseable option" do + bundle "config set --global --parseable foo value" + expect(out).to eq("foo=value") + run "puts Bundler.settings['foo']" + expect(out).to eq("value") + end + + context "when replacing a current value with the parseable flag" do + before { bundle "config set --global foo value" } + it "prints the current value in a parseable format" do + bundle "config set --global --parseable foo value2" + expect(out).to eq "foo=value2" + run "puts Bundler.settings['foo']" + expect(out).to eq("value2") + end + end + end + + describe "local" do + before(:each) do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "1.0.0" + G + end + + it "can also be set explicitly" do + bundle "config set --local foo local" + run "puts Bundler.settings[:foo]" + expect(out).to eq("local") + end + + it "has higher precedence than env" do + 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 + bundle "config set --local foo local" + bundle "config unset foo" + + run "puts Bundler.settings[:foo] == nil" + expect(out).to eq("true") + end + + it "warns when overriding" do + bundle "config set --local foo previous" + bundle "config set --local foo local" + expect(out).to match(/You are replacing the current local value of foo/) + + run "puts Bundler.settings[:foo]" + expect(out).to eq("local") + end + + it "expands the path at time of setting" do + bundle "config set --local local.foo .." + run "puts Bundler.settings['local.foo']" + expect(out).to eq(File.expand_path(bundled_app.to_s + "/..")) + end + + it "can be deleted with parseable option" do + bundle "config set --local foo value" + bundle "config unset --parseable foo" + expect(out).to eq "" + run "puts Bundler.settings['foo'] == nil" + expect(out).to eq("true") + end + end + + describe "env" do + before(:each) do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "1.0.0" + G + end + + it "can set boolean properties via the environment" do + ENV["BUNDLE_FROZEN"] = "true" + + run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end" + expect(out).to eq("true") + end + + it "can set negative boolean properties via the environment" do + run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end" + expect(out).to eq("false") + + ENV["BUNDLE_FROZEN"] = "false" + + run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end" + expect(out).to eq("false") + + ENV["BUNDLE_FROZEN"] = "0" + + run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end" + expect(out).to eq("false") + + ENV["BUNDLE_FROZEN"] = "" + + run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end" + expect(out).to eq("false") + end + + it "can set properties with periods via the environment" do + ENV["BUNDLE_FOO__BAR"] = "baz" + + run "puts Bundler.settings['foo.bar']" + expect(out).to eq("baz") + end + end + + describe "parseable option" do + it "prints an empty string" do + bundle "config 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 + bundle "config set foo local" + bundle "config get foo --parseable" + + expect(out).to eq "foo=local" + end + + it "can print global config" do + bundle "config set --global bar value" + bundle "config get bar --parseable" + + expect(out).to eq "bar=value" + end + + it "prefers local config over global" do + bundle "config set --local bar value2" + bundle "config set --global bar value" + bundle "config get bar --parseable" + + expect(out).to eq "bar=value2" + end + end + + describe "gem mirrors" do + before(:each) do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "1.0.0" + G + end + + it "configures mirrors using keys with `mirror.`" do + bundle "config set --local mirror.http://gems.example.org http://gem-mirror.example.org" + run(<<-E) +Bundler.settings.gem_mirrors.each do |k, v| + puts "\#{k} => \#{v}" +end +E + expect(out).to eq("http://gems.example.org/ => http://gem-mirror.example.org/") + end + + 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 "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" + end + + it "saves quotes" do + bundle "config set foo something\\'" + run "puts Bundler.settings[:foo]" + expect(out).to eq("something'") + end + + it "doesn't return quotes around values" do + bundle "config set foo '1'" + run "puts Bundler.settings.send(:local_config_file).read" + expect(out).to include('"1"') + run "puts Bundler.settings[:foo]" + expect(out).to eq("1") + end + + it "doesn't duplicate quotes around values" do + bundled_app(".bundle").mkpath + File.open(bundled_app(".bundle/config"), "w") do |f| + f.write 'BUNDLE_FOO: "$BUILD_DIR"' + end + + bundle "config set bar baz" + run "puts Bundler.settings.send(:local_config_file).read" + + # Starting in Ruby 2.1, YAML automatically adds double quotes + # around some values, including $ and newlines. + expect(out).to include('BUNDLE_FOO: "$BUILD_DIR"') + end + + it "doesn't duplicate quotes around long wrapped values" do + bundle "config set foo #{long_string}" + + run "puts Bundler.settings[:foo]" + expect(out).to eq(long_string) + + bundle "config set bar baz" + + run "puts Bundler.settings[:foo]" + expect(out).to eq(long_string) + end + end + + describe "very long lines" do + before(:each) do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "1.0.0" + G + end + + let(:long_string) do + "--with-xml2-include=/usr/pkg/include/libxml2 --with-xml2-lib=/usr/pkg/lib " \ + "--with-xslt-dir=/usr/pkg" + end + + let(:long_string_without_special_characters) do + "here is quite a long string that will wrap to a second line but will not be " \ + "surrounded by quotes" + end + + it "doesn't wrap values" do + bundle "config set foo #{long_string}" + run "puts Bundler.settings[:foo]" + expect(out).to match(long_string) + end + + it "can read wrapped unquoted values" do + bundle "config set foo #{long_string_without_special_characters}" + run "puts Bundler.settings[:foo]" + expect(out).to match(long_string_without_special_characters) + 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", 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, 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", 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" + expect(out).to eq "foo=foo_val" + + 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" + expect(out).to eq "" + + bundle "config set --local foo 2" + expect(out).to eq "" + + 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" + expect(out).to eq "foo=4" + + 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", 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" + expect(out).to eq "" + + 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 unset foo" + expect(out).to eq "" + 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 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(out).to eq "" + 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 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(out).to eq "" + 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", 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 + end +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 "https://gem.repo1" + gem 'myrack' + G + + bundle "config set --local gemfile #{bundled_app("NotGemfile")}" + 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 new file mode 100644 index 0000000000..a44f607546 --- /dev/null +++ b/spec/bundler/commands/console_spec.rb @@ -0,0 +1,214 @@ +# frozen_string_literal: true + +RSpec.describe "bundle console", readline: true do + before :each do + 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 + + build_dummy_irb + end + end + + 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 + + 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 + end + + 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 + + install_gemfile <<-G + source "https://gem.repo2" + gem "irb" + path "#{lib_path}" do + gem "loadfuuu", require: true + end + G + end + + 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 + end + + 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("__irb__") + end + + 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(":__pry__") + end + + 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 + + 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 + end +end diff --git a/spec/bundler/commands/doctor_spec.rb b/spec/bundler/commands/doctor_spec.rb new file mode 100644 index 0000000000..d350b4b3d1 --- /dev/null +++ b/spec/bundler/commands/doctor_spec.rb @@ -0,0 +1,185 @@ +# frozen_string_literal: true + +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 "https://gem.repo1" + gem "myrack" + G + + @stdout = StringIO.new + + [:error, :warn, :info].each do |method| + allow(Bundler.ui).to receive(method).and_wrap_original do |m, message| + m.call message + @stdout.puts message + end + end + end + + it "succeeds on a sane installation" do + bundle :doctor + 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 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 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(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::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(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 + * 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" 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 { doctor.run }.not_to raise_error + expect(@stdout.string).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).not_to include("No issues") + end + + 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 { 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. 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 { 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. 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 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 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 new file mode 100644 index 0000000000..f9ad9fff14 --- /dev/null +++ b/spec/bundler/commands/help_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +RSpec.describe "bundle help" do + it "uses man when available" do + with_fake_man do + bundle "help gemfile" + end + expect(out).to eq(%(["#{man_dir}/gemfile.5"])) + end + + it "prefixes bundle commands with bundle- when finding the man files" do + with_fake_man do + bundle "help install" + end + expect(out).to eq(%(["#{man_dir}/bundle-install.1"])) + end + + 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 eq(%(["#{man_dir}/bundle-cache.1"])) + end + + 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 + + with_path_added(tmp) do + bundle "help testtasks" + end + + expect(out).to eq("--help") + end + + it "is called when the --help flag is used after the command" do + with_fake_man do + bundle "install --help" + end + 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(%(["#{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(%(["#{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(%(["#{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", raise_on_error: false + end + expect(err).to include('Could not find command "instill".') + end + + it "is called when only using the --help flag" do + with_fake_man do + bundle "--help" + end + expect(out).to eq(%(["#{man_dir}/bundle.1"])) + + with_fake_man do + bundle "-h" + end + 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 new file mode 100644 index 0000000000..a26b1696fb --- /dev/null +++ b/spec/bundler/commands/info_spec.rb @@ -0,0 +1,249 @@ +# frozen_string_literal: true + +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 "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(bundled_app_lock) + + bundle "info rails" + + expect(bundled_app_lock).to exist + end + + it "prints information if gem exists in bundle" do + bundle "info rails" + expect(out).to include "* rails (2.3.2) +\tSummary: This is just a fake gem for testing +\tHomepage: http://example.com +\tPath: #{default_bundle_path("gems", "rails-2.3.2")}" + end + + it "prints path if gem exists in bundle" do + bundle "info rails --path" + expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s) + end + + it "prints the path to the running bundler" do + bundle "info bundler --path" + 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", raise_on_error: false + expect(err).to eq("Could not find gem 'missing'.") + end + + 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 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 + build_gem "rails", "2.3.2" do |s| + s.executables = "rails" + s.summary = "Just another test gem" + end + end + end + + it "excludes the homepage field from the output" do + expect(out).to_not include("Homepage:") + end + end + + context "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 + before :each do + @git = build_git "foo", "1.0" + end + + 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("main", 6)}") + end + + 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" + + bundle "info foo" + expect(out).to include("foo (1.0 #{@git.ref_for("omg", 6)}") + end + + it "doesn't print the branch when tied to a ref" do + sha = revision_for(lib_path("foo-1.0")) + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{sha}" + G + + bundle "info foo" + expect(out).to include("foo (1.0 #{sha[0..6]})") + end + + it "handles when a version is a '-' prerelease" do + @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" + 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", :readline do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + bundle "info rac" + 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 "https://gem.repo1" + gem "rails" + G + + 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 new file mode 100644 index 0000000000..989d6fa812 --- /dev/null +++ b/spec/bundler/commands/init_spec.rb @@ -0,0 +1,207 @@ +# frozen_string_literal: true + +RSpec.describe "bundle init" do + it "generates a Gemfile" do + bundle :init + expect(out).to include("Writing new Gemfile") + 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 + gemfile <<-G + gem "rails" + G + end + + it "does not change existing Gemfiles" do + 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, raise_on_error: false + expect(err).to include("Gemfile already exists") + end + end + + context "when a Gemfile exists in a parent directory" do + let(:subdir) { "child_dir" } + + it "lets users generate a Gemfile in a child directory" do + bundle :init + + FileUtils.mkdir bundled_app(subdir) + + bundle :init, dir: bundled_app(subdir) + + expect(out).to include("Writing new Gemfile") + expect(bundled_app("#{subdir}/Gemfile")).to be_file + end + end + + context "when the dir is not writable by the current user" do + let(:subdir) { "child_dir" } + + 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) + + 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 + end + end + + context "given --gemspec option" do + 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 'myrack', '= 1.0.1' + s.add_development_dependency 'rspec', '1.2' + end + S + end + + bundle :init, gemspec: spec_file + + gemfile = bundled_app_gemfile.read + expect(gemfile).to match(%r{source 'https://rubygems.org'}) + expect(gemfile.scan(/gem "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 + + context "when gemspec file is invalid" do + it "notifies the user that specification is invalid" do + File.open(spec_file, "w") do |file| + file << <<-S + Gem::Specification.new do |s| + s.name = 'test' + s.invalid_method_name + end + S + end + + bundle :init, gemspec: spec_file, 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 "init_gems_rb true" } + + it "generates a gems.rb" do + 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 + gemfile("gems.rb", <<-G) + gem "rails" + G + end + + it "does not change existing Gemfiles" do + 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, raise_on_error: false + expect(err).to include("gems.rb already exists") + end + end + + context "when a gems.rb file exists in a parent directory" do + let(:subdir) { "child_dir" } + + it "lets users generate a Gemfile in a child directory" do + bundle :init + + FileUtils.mkdir bundled_app(subdir) + + bundle :init, dir: bundled_app(subdir) + + expect(out).to include("Writing new gems.rb") + expect(bundled_app("#{subdir}/gems.rb")).to be_file + end + end + + context "given --gemspec option" do + 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 'myrack', '= 1.0.1' + s.add_development_dependency 'rspec', '1.2' + end + S + end + end + + it "should generate from an existing gemspec" do + bundle :init, gemspec: spec_file + + gemfile = bundled_app("gems.rb").read + expect(gemfile).to match(%r{source 'https://rubygems.org'}) + 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 + + 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/install_spec.rb b/spec/bundler/commands/install_spec.rb new file mode 100644 index 0000000000..3b24434dc7 --- /dev/null +++ b/spec/bundler/commands/install_spec.rb @@ -0,0 +1,2115 @@ +# frozen_string_literal: true + +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 "https://gem.repo1" + G + + bundle :install + expect(err).to match(/no dependencies/) + end + + it "does not make a lockfile if the install fails" do + install_gemfile <<-G, raise_on_error: false + raise StandardError, "FAIL" + G + + expect(err).to include('StandardError, "FAIL"') + expect(bundled_app_lock).not_to exist + end + + it "creates a Gemfile.lock" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + expect(bundled_app_lock).to exist + end + + 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 + + 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 + install_gemfile <<-G, env: { "BUNDLE_PATH__SYSTEM" => "true" } + source "https://gem.repo1" + gem "myrack" + G + + expect(bundled_app(".bundle")).not_to exist + end + + it "creates lockfiles based on the Gemfile name" do + gemfile bundled_app("OmgFile"), <<-G + source "https://gem.repo1" + gem "myrack", "1.0" + G + + bundle "install --gemfile OmgFile" + + 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 "https://gem.repo1" + gem 'myrack' + G + + lockfile = File.read(bundled_app_lock) + + install_gemfile <<-G, raise_on_error: false + raise StandardError, "FAIL" + G + + expect(File.read(bundled_app_lock)).to eq(lockfile) + end + + it "does not touch the lockfile if nothing changed" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + expect { run "1" }.not_to change { File.mtime(bundled_app_lock) } + end + + it "fetches gems" do + install_gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + G + + 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 "https://gem.repo1" + gem 'myrack', "> 0.9", "< 1.0" + G + + 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 "https://gem.repo1" + gem 'myrack', "< 1.0", "> 0.9" + G + + 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, raise_on_error: false + source "https://gem.repo1" + gem :myrack + G + expect(exitstatus).to eq(4) + end + + it "pulls in dependencies" do + install_gemfile <<-G + source "https://gem.repo1" + gem "rails" + G + + expect(the_bundle).to include_gems "actionpack 2.3.2", "rails 2.3.2" + end + + it "does the right version" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + G + + 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 "https://gem.repo2" + gem "with_development_dependency" + G + + expect(the_bundle).to include_gems("with_development_dependency 1.0.0"). + and not_include_gems("activesupport 2.3.5") + end + + it "resolves correctly" do + install_gemfile <<-G + source "https://gem.repo1" + gem "activemerchant" + gem "rails" + G + + expect(the_bundle).to include_gems "activemerchant 1.0", "activesupport 2.3.2", "actionpack 2.3.2" + end + + it "activates gem correctly according to the resolved gems" do + install_gemfile <<-G + source "https://gem.repo1" + gem "activesupport", "2.3.5" + G + + install_gemfile <<-G + source "https://gem.repo1" + gem "activemerchant" + gem "rails" + G + + expect(the_bundle).to include_gems "activemerchant 1.0", "activesupport 2.3.2", "actionpack 2.3.2" + end + + it "does not reinstall any gem that is already available locally" do + system_gems "activesupport-2.3.2", path: default_bundle_path + + build_repo2 do + build_gem "activesupport", "2.3.2" do |s| + s.write "lib/activesupport.rb", "ACTIVESUPPORT = 'fail'" + end + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "activerecord", "2.3.2" + G + + expect(the_bundle).to include_gems "activesupport 2.3.2" + end + + it "works when the gemfile specifies gems that only exist in the system" do + build_gem "foo", to_bundle: true + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "foo" + G + + 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 "myrack", "9.0.0", to_bundle: true + + install_gemfile <<-G + 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(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 + simulate_platform "x86-darwin-100" do + install_gemfile <<-G + source "https://gem.repo1" + gem "platform_specific" + G + + 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" do + install_gemfile <<-G + source "https://gem.repo1" + gem "platform_specific" + G + + expect(the_bundle).to include_gems("platform_specific 1.0 ruby") + end + end + + it "installs gems for java" do + 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 "installs gems for windows" do + simulate_platform "x86-mswin32" do + install_gemfile <<-G + source "https://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 + + expect(out).to include("Installing platform_specific 1.0 (aarch64-mingw-ucrt)") + end + end + + 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 + + build_repo2 do + build_gem "my-private-gem" + end + + 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 + + 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 "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 + + 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", "~> 1.37.0", group: :development + G + + 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 "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 be_empty + expect(the_bundle).to include_gems("rubocop 1.36.0") + end + + 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 + + bundle :install + + expect(err).to be_empty + expect(the_bundle).to include_gems("rubocop 1.36.0") + end + + it "errors out if two gemspecs add it with incompatible 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", "~> 2.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, 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 + + 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 + + build_repo4 do + build_gem "activesupport" + end + + 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 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, raise_on_error: false + source "https://gem.repo1" + source "http://0.0.0.0:9384" do + gem 'foo' + end + G + + 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(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, invalid_deps) + end + + build_gem "ruby-ajp", "1.0.0" + end + + install_gemfile <<-G, full_index: true, raise_on_error: false + source "https://gem.repo2" + + gem "ajp-rails", "0.0.0" + G + + 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("Bundler::APIResponseInvalidDependenciesError") + end + + it "doesn't blow up when the local .bundle/config is empty" do + FileUtils.mkdir_p(bundled_app(".bundle")) + FileUtils.touch(bundled_app(".bundle/config")) + + install_gemfile(<<-G) + source "https://gem.repo1" + + gem 'foo' + G + end + + it "doesn't blow up when the global .bundle/config is empty" do + FileUtils.mkdir_p("#{Bundler.rubygems.user_home}/.bundle") + FileUtils.touch("#{Bundler.rubygems.user_home}/.bundle/config") + + install_gemfile(<<-G) + source "https://gem.repo1" + + gem 'foo' + G + end + end + + describe "Ruby version in Gemfile.lock" do + context "and using an unsupported Ruby version" do + it "prints an error" do + install_gemfile <<-G, raise_on_error: false + ruby '~> 1.2' + source "https://gem.repo1" + G + 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 '~> #{Gem.ruby_version}' + source "https://gem.repo1" + G + end + + it "writes current Ruby version to Gemfile.lock" do + checksums = checksums_section_when_enabled + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + #{checksums} + RUBY VERSION + #{Bundler::RubyVersion.system} + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "updates Gemfile.lock with updated yet still compatible ruby version" do + install_gemfile <<-G + ruby '~> #{current_ruby_minor}' + source "https://gem.repo1" + G + + checksums = checksums_section_when_enabled + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + #{checksums} + RUBY VERSION + #{Bundler::RubyVersion.system} + + BUNDLED WITH + #{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 + it "doesn't blow up when using the `gem` DSL" do + root_dir = tmp("foo[]bar") + + FileUtils.mkdir_p(root_dir) + + build_lib "foo" + gemfile = <<-G + source "https://gem.repo1" + gem 'foo', :path => "#{lib_path("foo-1.0")}" + G + File.open("#{root_dir}/Gemfile", "w") do |file| + file.puts gemfile + end + + bundle :install, dir: root_dir + end + + it "doesn't blow up when using the `gemspec` DSL" do + root_dir = tmp("foo[]bar") + + 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 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 + source "https://gem.repo4" + gem 'foo' + G + end + + 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 gems path is world writable (no sticky bit set)", :permissions do + let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") } + + 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(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" + 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 "https://gem.repo1" + gem "myrack" + G + bundle_config "path bundle" + bundle "install", standalone: true + end + + it "includes the standalone path" do + 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 + + describe "when bundle install is executed with unencoded authentication" do + before do + gemfile <<-G + source 'https://rubygems.org/' + gem "." + G + end + + it "should display a helpful message explaining how to fix it" do + 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 new file mode 100644 index 0000000000..346cdedc42 --- /dev/null +++ b/spec/bundler/commands/issue_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +RSpec.describe "bundle issue" do + it "exits with a message" do + install_gemfile <<-G + source "https://gem.repo1" + gem "rails" + G + + bundle "issue" + expect(out).to include "Did you find an issue with Bundler?" + expect(out).to include "## Environment" + expect(out).to include "## Gemfile" + expect(out).to include "## Bundle Doctor" + end +end diff --git a/spec/bundler/commands/licenses_spec.rb b/spec/bundler/commands/licenses_spec.rb new file mode 100644 index 0000000000..ebfad5ed4a --- /dev/null +++ b/spec/bundler/commands/licenses_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +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 "https://gem.repo2" + gem "rails" + gem "with_license" + G + end + + it "prints license information for all gems in the bundle" do + bundle "licenses" + + expect(out).to include("bundler: MIT") + expect(out).to include("with_license: MIT") + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "https://gem.repo2" + gem "rails" + gem "with_license" + gem "foo" + G + + bundle_config "auto_install 1" + bundle :licenses + expect(out).to include("Installing foo 1.0") + end +end diff --git a/spec/bundler/commands/list_spec.rb b/spec/bundler/commands/list_spec.rb new file mode 100644 index 0000000000..c890646a81 --- /dev/null +++ b/spec/bundler/commands/list_spec.rb @@ -0,0 +1,315 @@ +# 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", raise_on_error: false + + expect(err).to eq "The `--name-only` and `--paths` options cannot be used together" + end + end + + context "with without-group and only-group option" do + it "raises an error" do + 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 "https://gem.repo1" + + 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" + + 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", 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 "https://gem.repo1" + + 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" + + 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", 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 "https://gem.repo1" + + gem "myrack" + gem "rspec", :group => [:test] + G + end + + it "prints only the name of the gems in the bundle" do + bundle "list --name-only" + + 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_lib("gemspec_test", path: tmp("gemspec_test")) do |s| + s.add_dependency "bar", "=1.0.0" + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack" + gem "rails" + gem "git_test", :git => "#{lib_path("git_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{.*\/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 "https://gem.repo1" + G + end + + it "prints message saying no gems are in the bundle" 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 "https://gem.repo1" + + gem "myrack" + gem "rspec", :group => [:test] + G + end + + it "lists gems installed in the bundle" do + bundle "list" + 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 "https://gem.repo1" + + gem "myrack" + gem "rspec", :group => [:test] + G + end + + it "runs the list command" do + bundle "ls" + expect(out).to include("Gems included by the bundle") + end + end +end diff --git a/spec/bundler/commands/lock_spec.rb b/spec/bundler/commands/lock_spec.rb new file mode 100644 index 0000000000..8ab3cc7e8d --- /dev/null +++ b/spec/bundler/commands/lock_spec.rb @@ -0,0 +1,2877 @@ +# frozen_string_literal: true + +RSpec.describe "bundle lock" do + 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 + + <<~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 + 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} + 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(expected_lockfile.chomp) + end + + it "prints a lockfile when there is an existing 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 "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(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 + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile outdated_lockfile + + bundle "lock --update" + + expect(read_lockfile).to eq(expected_lockfile) + end + + it "prints an updated lockfile when there is an outdated lockfile using --print --update" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + 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 + 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(out).to eq(expected_lockfile.chomp) + end + + it "works with --gemfile flag" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + gemfile "CustomGemfile", <<-G + source "https://gem.repo4" + gem "foo" + G + bundle "lock --gemfile CustomGemfile" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "foo", "1.0" + end + + lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + 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(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) + end + + it "update specific gems using --update" do + 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(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 + gemfile_with_rails_weakling_and_foo_from_repo4 + + 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(expected_lockfile) + end + + it "can lock without downloading gems" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + gemfile <<-G + source "https://gem.repo1" + + gem "thin" + gem "myrack_middleware", :group => "test" + G + 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 + # to repeat coverage here. + context "conservative updates" do + before do + build_repo4 do + build_gem "foo", %w[1.4.3 1.4.4] do |s| + s.add_dependency "bar", "~> 2.0" + end + build_gem "foo", %w[1.4.5 1.5.0] do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "foo", %w[1.5.1] do |s| + s.add_dependency "bar", "~> 3.0" + end + build_gem "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 "https://gem.repo4" + gem 'foo', '1.4.3' + gem 'bar', '2.0.3' + gem 'qux', '1.0.0' + G + + # remove 1.4.3 requirement and bar altogether + # to setup update specs below + gemfile <<-G + source "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_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_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 + + 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 + + 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 + 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 "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 + gemfile_with_rails_weakling_and_foo_from_repo4 + + 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 match_array(default_platform_list("java", "x86-mingw32")) + + bundle "lock --remove-platform java" + + 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 + 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/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 = "x86-mingw32" + end + + build_gem "gssapi", "0.1" + build_gem "gssapi", "0.2" + build_gem "gssapi", "0.3" + build_gem "gssapi", "1.2.0" do |s| + s.add_dependency "ffi", ">= 1.0.1" + end + + build_gem "mixlib-shellout", "2.2.6" + build_gem "mixlib-shellout", "2.2.6" do |s| + s.platform = "universal-mingw32" + s.add_dependency "win32-process", "~> 0.8.2" + end + + # we need all these versions to get the sorting the same as it would be + # pulling from rubygems.org + %w[0.8.3 0.8.2 0.8.1 0.8.0].each do |v| + build_gem "win32-process", v do |s| + s.add_dependency "ffi", ">= 1.0.0" + end + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "mixlib-shellout" + gem "gssapi" + G + + 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 + + expect(lockfile).to eq <<~G + GEM + remote: https://gem.repo4/ + specs: + ffi (1.9.14-x86-mingw32) + gssapi (1.2.0) + ffi (>= 1.0.1) + mixlib-shellout (2.2.6-universal-mingw32) + win32-process (~> 0.8.2) + win32-process (0.8.3) + ffi (>= 1.0.0) + + PLATFORMS + x86-mingw32 + + DEPENDENCIES + gssapi + mixlib-shellout + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G + + bundle_config "force_ruby_platform true" + bundle :lock + + checksums.checksum gem_repo4, "ffi", "1.9.14" + checksums.checksum gem_repo4, "mixlib-shellout", "2.2.6" + + expect(lockfile).to eq <<~G + GEM + remote: https://gem.repo4/ + specs: + ffi (1.9.14) + ffi (1.9.14-x86-mingw32) + gssapi (1.2.0) + ffi (>= 1.0.1) + mixlib-shellout (2.2.6) + mixlib-shellout (2.2.6-universal-mingw32) + win32-process (~> 0.8.2) + win32-process (0.8.3) + ffi (>= 1.0.0) + + PLATFORMS + ruby + x86-mingw32 + + DEPENDENCIES + gssapi + mixlib-shellout + #{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} + G + + simulate_platform("x86_64-darwin-19") { bundle "lock --update" } + + expect(out).to match(/Writing lockfile to.+Gemfile\.lock/) + end + + 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 + 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" + + 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 + + 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" + + 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(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 new file mode 100644 index 0000000000..65fbad05aa --- /dev/null +++ b/spec/bundler/commands/newgem_spec.rb @@ -0,0 +1,2139 @@ +# frozen_string_literal: true + +RSpec.describe "bundle gem" do + def gem_skeleton_assertions + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to exist + expect(bundled_app("#{gem_name}/README.md")).to exist + expect(bundled_app("#{gem_name}/Gemfile")).to exist + expect(bundled_app("#{gem_name}/Rakefile")).to exist + expect(bundled_app("#{gem_name}/lib/#{gem_name}.rb")).to exist + expect(bundled_app("#{gem_name}/lib/#{gem_name}/version.rb")).to exist + + expect(ignore_paths).to include("bin/") + expect(ignore_paths).to include("Gemfile") + end + + 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 + + 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 + + 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 + + def installed_go? + sys_exec("go version", raise_on_error: true) + true + rescue StandardError + false + end + + let(:generated_gemspec) { Bundler.load_gemspec_uncached(bundled_app(gem_name).join("#{gem_name}.gemspec")) } + + 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 + 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 + + 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 + + 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 "on a path with spaces" do + before do + Dir.mkdir(bundled_app("path with spaces")) + end + + 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" + end + it "generates a gem skeleton with MIT license" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/LICENSE.txt")).to exist + expect(generated_gemspec.license).to eq("MIT") + end + end + + shared_examples_for "--no-mit flag" do + before do + bundle "gem #{gem_name} --no-mit" + end + it "generates a gem skeleton without MIT license" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/LICENSE.txt")).to_not exist + end + end + + shared_examples_for "--coc flag" do + 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 + + 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" + 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 + + 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}" + end + + it "contribute URL set to git 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/bundleuser") + end + end + + context "git config github.user is absent" do + before do + git("config --global --unset github.user") + bundle "gem #{gem_name}" + end + + 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 + + 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 + bundle "gem #{gem_name}" + expect(bundled_app("#{gem_name}/.git")).to exist + end + + context "when git is not available" do + # This spec cannot have `git` available in the test env + before do + bundle "gem #{gem_name}", env: { "PATH" => "" } + end + + it "creates the gem without the need for git" do + expect(bundled_app("#{gem_name}/README.md")).to exist + end + + it "doesn't create a git repo" do + expect(bundled_app("#{gem_name}/.git")).to_not exist + end + + it "doesn't create a .gitignore file" do + expect(bundled_app("#{gem_name}/.gitignore")).to_not exist + end + + 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" + + prepare_gemspec(bundled_app("newgem", "newgem.gemspec")) + + 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(stdboth).not_to include("ERROR") + end + + context "gem naming with relative paths" do + it "resolves ." do + create_temporary_dir("tmp") + + bundle "gem .", dir: bundled_app("tmp") + + expect(bundled_app("tmp/lib/tmp.rb")).to exist + end + + it "resolves .." do + create_temporary_dir("temp/empty_dir") + + bundle "gem ..", dir: bundled_app("temp/empty_dir") + + expect(bundled_app("temp/lib/temp.rb")).to exist + end + + it "resolves relative directory" do + create_temporary_dir("tmp/empty/tmp") + + 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(bundled_app(dir)) + end + end + + 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 + gem_skeleton_assertions + end + + 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 + + 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 + + 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 + + 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 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 + + 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 + + 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 + + context "--bin parameter set" do + before do + bundle "gem #{gem_name} --bin" + end + + it "builds exe skeleton" do + expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist + end + end + + context "no --test parameter" do + before do + bundle "gem #{gem_name}" + end + + it_behaves_like "test framework is absent" + end + + context "--test parameter set to rspec" do + before 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/#{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 "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 + + context "init_gems_rb setting to true" do + before do + bundle_config "init_gems_rb true" + bundle "gem #{gem_name}" + 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 "includes .rspec and spec/ into ignore list" do + expect(ignore_paths).to include(".rspec") + expect(ignore_paths).to include("spec/") + end + end + + 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 + + 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 + + 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 + + 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 + + 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 "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 "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 + + 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 "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 "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 "--bin parameter set" do + before do + bundle "gem #{gem_name} --bin" + end + + 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 "--test parameter set to rspec" do + before do + bundle "gem #{gem_name} --test=rspec" + end + + 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 + expect(bundled_app("#{gem_name}/spec/spec_helper.rb").read).to include(%(require "#{require_path}")) + 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 "--test parameter set to minitest" do + before do + bundle "gem #{gem_name} --test=minitest" + end + + 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 "--test parameter set to test-unit" do + before do + bundle "gem #{gem_name} --test=test-unit" + end + + 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 "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 + + 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 + + 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 + + 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 + + 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 + + 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 "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 + + 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 + + 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 + + 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 + + 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 "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_config_global "gem.github_username different_username" + end + it_behaves_like "--github-username option", "gh_user" + end + + 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 + + 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 "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 "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 + + 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 + + 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" + + 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 + end + + 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(:require_relative_path) { "test_gem" } + + 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 "paths that depend on gem name" + + 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 + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.h")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c")).to exist + end + + 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 = <<~RAKEFILE + # frozen_string_literal: true + + require "bundler/gem_tasks" + require "rake/extensiontask" + + 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 + + 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: :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 + + context "gem naming with dashed" do + let(:gem_name) { "test-gem" } + + let(:require_path) { "test/gem" } + + 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 "paths that depend on gem name" + end + + describe "uncommon gem names" do + it "can deal with two dashes" do + 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", 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 ^", 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'", 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", 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]" + E + end + end + + describe "#ensure_safe_gem_name" do + before do + bundle "gem #{subject}", raise_on_error: false + end + + context "with an existing const name" do + subject { "gem" } + it { expect(err).to include("Invalid gem name #{subject}") } + end + + context "with an existing hyphenated const name" do + subject { "gem-specification" } + 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}") } + end + + context "ending with an existing const name" do + subject { "somenewconstantname-gem" } + it { expect(err).not_to include("Invalid gem name #{subject}") } + end + end + + context "on first run", :readline do + it "asks about test framework" do + 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 = <<~RAKEFILE + # frozen_string_literal: true + + require "bundler/gem_tasks" + require "rspec/core/rake_task" + + RSpec::Core::RakeTask.new(:spec) + + task default: :spec + RAKEFILE + + expect(bundled_app("foobar/Rakefile").read).to eq(rakefile) + expect(bundled_app("foobar/Gemfile").read).to include('gem "rspec"') + end + + 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" + + bundle "gem foobar" do |input, _, _| + input.puts "yes" + end + + expect(bundled_app("foobar/LICENSE.txt")).to exist + expect(out).to include("Using a MIT license means").once + end + + 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(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(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 + end +end diff --git a/spec/bundler/commands/open_spec.rb b/spec/bundler/commands/open_spec.rb new file mode 100644 index 0000000000..664dc58919 --- /dev/null +++ b/spec/bundler/commands/open_spec.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true + +RSpec.describe "bundle open" do + context "when opening a regular gem" do + before do + install_gemfile <<-G + 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" } + expect(out).to include("bundler_editor #{default_bundle_path("gems", "rails-2.3.2")}") + end + + it "opens the gem with VISUAL as 2nd highest priority" do + bundle "open rails", env: { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "" } + expect(out).to include("visual #{default_bundle_path("gems", "rails-2.3.2")}") + end + + it "opens the gem with EDITOR as 3rd highest priority" do + bundle "open rails", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("editor #{default_bundle_path("gems", "rails-2.3.2")}") + end + + it "complains if no EDITOR is set" do + bundle "open rails", env: { "EDITOR" => "", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to eq("To open a bundled gem, set $EDITOR or $BUNDLER_EDITOR") + end + + it "complains if gem not in bundle" do + bundle "open missing", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, 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("main", 11) + + install_gemfile <<-G + 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 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" => "" }, 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" } + + expect(out).to include("bundler_editor #{default_bundle_path("gems", "activerecord-2.3.2")}") + end + + 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 --path lib/activerecord/version.rb", env: env do |input, _, _| + input.puts "2" + end + + 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", :readline do + env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" } + bundle "open active", env: env do |input, _, _| + input.puts "0" + end + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "https://gem.repo1" + gem "rails" + gem "foo" + G + + bundle_config "auto_install 1" + bundle "open rails", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("Installing foo 1.0") + end + + it "opens the editor with a clean env" do + bundle "open", env: { "EDITOR" => "sh -c 'env'", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, 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") + if Gem::Specification.is_a?(Enumerable) + puts Gem::Specification.select(&:default_gem?).map(&:name) + end + RUBY + end + + before do + skip "No default gems available on this test run" if default_gems.empty? + + install_gemfile <<-G + 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" } + 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 +end diff --git a/spec/bundler/commands/outdated_spec.rb b/spec/bundler/commands/outdated_spec.rb new file mode 100644 index 0000000000..28ed51d61e --- /dev/null +++ b/spec/bundler/commands/outdated_spec.rb @@ -0,0 +1,1369 @@ +# frozen_string_literal: true + +RSpec.describe "bundle outdated" do + 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") + end + + 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 + + bundle "outdated", raise_on_error: false + + 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") + end + + bundle "outdated", raise_on_error: false + + expect(exitstatus).to_not be_zero + end + + it "returns success exit status if no outdated gems present" do + bundle "outdated" + end + + it "adds gem group to dependency output when repo is updated" do + install_gemfile <<-G + source "https://gem.repo2" + + gem "terranova", '8' + + group :development, :test do + gem 'activesupport', '2.3.5' + end + G + + update_repo2 { build_gem "activesupport", "3.0" } + update_repo2 { build_gem "terranova", "9" } + + 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 "https://gem.repo2" + + gem "weakling", "~> 0.0.1" + gem "terranova", '8' + group :development, :test do + gem "duradura", '7.0' + gem 'activesupport', '2.3.5' + end + G + end + + 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}", raise_on_error: false + end + + it "works when the bundle is up to date" do + bundle "outdated --group" + 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") + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + terranova 8 9 = 8 default + TABLE + + 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") + + 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 end_with(expected_output) + end + + it "returns a sorted list of outdated gems from one group => 'test'" do + test_group_option("test") + + 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 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 "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 "not outdated gems" do + bundle "outdated --groups" + expect(out).to end_with("Bundle up to date!") + end + + it "returns a sorted list of outdated gems by groups" do + update_repo2 do + build_gem "activesupport", "3.0" + build_gem "terranova", "9" + build_gem "duradura", "8.0" + end + + bundle "outdated --groups", 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 + duradura 7.0 8.0 = 7.0 development, test + terranova 8 9 = 8 default + TABLE + + 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 "clean false" + + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport", "2.3.4" + G + + 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 end_with(expected_output) + end + + it "doesn't hit repo2" do + FileUtils.rm_r(gem_repo2) + + bundle "outdated --local" + expect(out).not_to match(/Fetching (gem|version|dependency) metadata from/) + end + end + + shared_examples_for "a minimal output is desired" do + context "and gems are outdated" do + before do + 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 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 + 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(err).to include("Fetching gem metadata") + end + end + end + + describe "with --parseable option" do + 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", 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") + end + + 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 + build_gem "activesupport", "3.0.0.beta" + end + + bundle "outdated" + + expect(out).to end_with("Bundle up to date!") + end + end + + context "with the --pre option" do + it "includes pre-release versions" do + update_repo2 do + build_gem "activesupport", "3.0.0.beta" + end + + bundle "outdated --pre", 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 + + context "when current gem is a pre-release" do + it "includes the gem" do + update_repo2 do + build_gem "activesupport", "3.0.0.beta.1" + build_gem "activesupport", "3.0.0.beta.2" + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport", "3.0.0.beta.1" + G + + 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 + + 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": 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 "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 "https://gem.repo2" + gem "myrack_middleware", "1.0" + G + + bundle :outdated, "filter-strict": true + + 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 "https://gem.repo2" + gem "activesupport", "~> 2.3" + gem "weakling", ">= 0.0.1" + G + + update_repo2 do + build_gem "activesupport", %w[2.4.0 3.0.0] + build_gem "weakling", "0.0.5" + end + + bundle :outdated, :"filter-strict" => true, "filter-patch" => 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 "only reports gems that match requirement and minor filter level" do + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport", "~> 2.3" + gem "weakling", ">= 0.0.1" + G + + update_repo2 do + build_gem "activesupport", %w[2.3.9] + build_gem "weakling", "0.1.5" + end + + bundle :outdated, :"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 end_with(expected_output) + end + + it "only reports gems that match requirement and major filter level" do + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport", "~> 2.3" + gem "weakling", ">= 0.0.1" + G + + update_repo2 do + build_gem "activesupport", %w[2.4.0 2.5.0] + build_gem "weakling", "1.1.5" + end + + bundle :outdated, :"filter-strict" => true, "filter-major" => true, :raise_on_error => false + + 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", 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", raise_on_error: false + expect(exitstatus).to_not be_zero + 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 :outdated, raise_on_error: false + expect(out).to include("Installing foo 1.0") + end + + context "in deployment mode" do + before do + build_repo2 + + gemfile <<-G + source "https://gem.repo2" + + 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", 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 config unset deployment`.") + end + end + + 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 "https://gem.repo2" + + gem "myrack" + gem "foo" + G + 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", 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 config unset deployment`.") + end + end + + context "update available for a gem on a different platform" do + before do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "laduradura", '= 5.15.2' + G + end + + it "reports that no updates are available" do + bundle "outdated" + 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 "https://gem.repo2" + gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] + G + + bundle "outdated" + expect(out).to end_with("Bundle up to date!") + end + + 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 + + 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 + + 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" + end + end + + it_behaves_like "version update is detected" + end + + context "when on a new machine" 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 + + 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", 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" + end + end + + it_behaves_like "version update is detected" + end + + 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" + end + end + + it_behaves_like "version update is detected" + end + + shared_examples_for "no version updates are detected" do + it "does not detect any version updates" do + subject + expect(out).to 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" + end + end + + it_behaves_like "no version updates are detected" + end + + 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" + end + end + + it_behaves_like "no version updates are detected" + end + + 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" + end + end + + it_behaves_like "no version updates are detected" + end + + describe "with --filter-major option" do + subject { bundle "outdated --filter-major", raise_on_error: false } + + it_behaves_like "major version updates are detected" + it_behaves_like "minor version is ignored" + it_behaves_like "patch version is ignored" + end + + describe "with --filter-minor option" do + subject { bundle "outdated --filter-minor", raise_on_error: false } + + it_behaves_like "minor version updates are detected" + it_behaves_like "major version is ignored" + it_behaves_like "patch version is ignored" + end + + describe "with --filter-patch option" do + subject { bundle "outdated --filter-patch", raise_on_error: false } + + it_behaves_like "patch version updates are detected" + it_behaves_like "major version is ignored" + it_behaves_like "minor version is ignored" + end + + describe "with --filter-minor --filter-patch options" do + subject { bundle "outdated --filter-minor --filter-patch", raise_on_error: false } + + it_behaves_like "minor version updates are detected" + it_behaves_like "patch version updates are detected" + it_behaves_like "major version is ignored" + end + + describe "with --filter-major --filter-minor options" do + subject { bundle "outdated --filter-major --filter-minor", raise_on_error: false } + + it_behaves_like "major version updates are detected" + it_behaves_like "minor version updates are detected" + it_behaves_like "patch version is ignored" + end + + describe "with --filter-major --filter-patch options" do + subject { bundle "outdated --filter-major --filter-patch", raise_on_error: false } + + it_behaves_like "major version updates are detected" + it_behaves_like "patch version updates are detected" + it_behaves_like "minor version is ignored" + end + + describe "with --filter-major --filter-minor --filter-patch options" do + subject { bundle "outdated --filter-major --filter-minor --filter-patch", raise_on_error: false } + + it_behaves_like "major version updates are detected" + it_behaves_like "minor version updates are detected" + it_behaves_like "patch version updates are detected" + end + + context "conservative updates" do + 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 "https://gem.repo4" + gem 'patch', '1.0.0' + gem 'minor', '1.0.0' + gem 'major', '1.0.0' + G + + # remove all version requirements + gemfile <<-G + source "https://gem.repo4" + gem 'patch' + gem 'minor' + gem 'major' + G + end + + it "shows nothing when patching and filtering to minor" do + bundle "outdated --patch --filter-minor" + + expect(out).to end_with("No minor updates to display.") + end + + it "shows all gems when patching and filtering to patch" do + bundle "outdated --patch --filter-patch", raise_on_error: false + + 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 + + expect(out).to end_with(expected_output) + 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 + + 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).to end_with(expected_output) + 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 + + expect(out).not_to include("patch (newest") + expect(out).to include("minor (newest") + expect(out).not_to include("major (newest") + end + end + + 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 + + # 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 + + # 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 + + describe "with --only-explicit" do + it "does not report outdated dependent gems" do + build_repo4 do + build_gem "weakling", %w[0.2 0.3] do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "bar", %w[2.1 2.2] + end + + install_gemfile <<-G + source "https://gem.repo4" + gem 'weakling', '0.2' + gem 'bar', '2.1' + G + + gemfile <<-G + source "https://gem.repo4" + gem 'weakling' + G + + 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 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 new file mode 100644 index 0000000000..088fc29fe1 --- /dev/null +++ b/spec/bundler/commands/post_bundle_message_spec.rb @@ -0,0 +1,183 @@ +# frozen_string_literal: true + +RSpec.describe "post bundle message" do + before :each do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "activesupport", "2.3.5", :group => [:emo, :test] + group :test do + gem "rspec" + end + gem "myrack-obama", :group => :obama + G + end + + let(:bundle_path) { "./.bundle" } + let(:bundle_show_system_message) { "Use `bundle info [gemname]` to see where a bundled gem is installed." } + 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, 4 gems now installed." } + + 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_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) + + 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) + + 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, 2 gems now 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, 1 gem now installed.") + end + + describe "for second bundle install run" do + it "without any options" do + 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 + + describe "with `path` configured" do + let(:bundle_path) { "./vendor" } + + 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) + + 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) + + 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) + + 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 an absolute `path` inside the cwd configured" do + let(:bundle_path) { bundled_app("cache") } + + 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 "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) + end + end + + describe "with misspelled or non-existent gem name" do + before do + bundle_config "force_ruby_platform true" + end + + 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 "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 "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) + + 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) + + 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) + + 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 +end diff --git a/spec/bundler/commands/pristine_spec.rb b/spec/bundler/commands/pristine_spec.rb new file mode 100644 index 0000000000..5f80b9e534 --- /dev/null +++ b/spec/bundler/commands/pristine_spec.rb @@ -0,0 +1,275 @@ +# frozen_string_literal: true + +require "bundler/vendored_fileutils" + +RSpec.describe "bundle pristine" do + before :each do + build_lib "baz", path: bundled_app do |s| + s.version = "1.0.0" + s.add_development_dependency "baz-dev", "=1.0.0" + end + + build_repo2 do + build_gem "weakling" + build_gem "baz-dev", "1.0.0" + build_gem "very_simple_binary", &:add_c_extension + build_git "foo", path: lib_path("foo") + build_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 "https://gem.repo2" + gem "weakling" + gem "very_simple_binary" + 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 = find_spec("weakling") + changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt") + + FileUtils.touch(changes_txt) + expect(changes_txt).to be_file + + bundle "pristine" + expect(changes_txt).to_not be_file + end + + it "does not delete the bundler gem" do + bundle "install" + bundle "pristine" + bundle "-v" + + 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 = 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" + expect(File.read(changed_file)).to_not include(diff) + end + + it "removes added files" do + 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" + 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 = find_spec("baz") + changed_file = Pathname.new(spec.full_gem_path).join("lib/baz.rb") + diff = "#Pristine spec changes" + + File.open(changed_file, "a") {|f| f.puts diff } + expect(File.read(changed_file)).to include(diff) + + bundle "pristine" + expect(File.read(changed_file)).to include(diff) + expect(err).to include("Cannot pristine #{spec.name} (#{spec.version}#{spec.git_version}). Gem is sourced from local path.") + end + + it "reinstall gemspec dependency" do + 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" } + expect(File.read(changed_file)).to include(diff) + + bundle "pristine" + expect(File.read(changed_file)).to_not include(diff) + end + end + + context "when sourced from path" do + it "displays warning and ignores changes when sourced from local path" do + spec = 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 + bundle "pristine" + expect(err).to include("Cannot pristine #{spec.name} (#{spec.version}#{spec.git_version}). Gem is sourced from local path.") + expect(changes_txt).to be_file + end + end + + context "when passing a list of gems to pristine" do + it "resets them" do + 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 = 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 = 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" + + expect(err).to include("Cannot pristine bar (1.0). Gem is sourced from local path.") + expect(out).to include("Installing weakling 1.0") + + expect(weakling_changes_txt).not_to be_file + expect(foo_changes_txt).not_to be_file + expect(bar_changes_txt).to be_file + end + + it "raises when one of them is not in the lockfile" do + 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) { 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 "build.very_simple_binary -- #{build_opt}" } + + # This just verifies that the generated Makefile from the c_ext gem makes + # use of the build_args from the bundle config + it "applies the config when installing the gem" do + bundle "pristine" + + makefile_contents = File.read(c_ext_dir.join("Makefile").to_s) + expect(makefile_contents).to match(/libpath =.*#{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) { 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 "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" + + makefile_contents = File.read(c_ext_dir.join("Makefile").to_s) + 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 new file mode 100644 index 0000000000..8a2e6778ea --- /dev/null +++ b/spec/bundler/commands/remove_spec.rb @@ -0,0 +1,736 @@ +# frozen_string_literal: true + +RSpec.describe "bundle remove" do + context "when no gems are specified" do + it "throws error" do + gemfile <<-G + source "https://gem.repo1" + G + + bundle "remove", raise_on_error: false + + expect(err).to include("Please specify gems to remove.") + end + end + + context "after 'bundle install' is run" do + describe "running 'bundle remove GEM_NAME'" do + it "removes it from the lockfile" do + myrack_dep = <<~L + + DEPENDENCIES + myrack + + L + + gemfile <<-G + source "https://gem.repo1" + + gem "myrack" + G + + bundle "install" + + expect(lockfile).to include(myrack_dep) + + bundle "remove myrack" + + expect(gemfile).to eq <<~G + source "https://gem.repo1" + G + expect(lockfile).to_not include(myrack_dep) + end + end + end + + describe "remove single gem from gemfile" do + context "when gem is present in gemfile" do + it "shows success for removed gem" do + gemfile <<-G + source "https://gem.repo1" + + gem "myrack" + G + + bundle "remove myrack" + + 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 "https://gem.repo1" + G + + bundle "remove myrack", raise_on_error: false + + expect(err).to include("`myrack` is not specified in #{bundled_app_gemfile} so it could not be removed.") + end + end + end + + 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 "https://gem.repo1" + + gem "myrack" + gem "rails" + G + + bundle "remove myrack rails" + + expect(out).to include("myrack was removed.") + expect(out).to include("rails was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + G + end + end + + 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 "https://gem.repo1" + + gem "rails" + gem "minitest" + gem "rspec" + G + + bundle "remove rails myrack minitest", raise_on_error: false + + 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" + gem "rspec" + G + end + end + end + + context "with inline groups" do + it "removes the specified gem" do + gemfile <<-G + source "https://gem.repo1" + + gem "myrack", :group => [:dev] + G + + bundle "remove myrack" + + expect(out).to include("myrack was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + G + end + end + + describe "with group blocks" do + context "when single group block with gem to be removed is present" do + it "removes the group block" do + gemfile <<-G + source "https://gem.repo1" + + group :test do + gem "rspec" + end + G + + bundle "remove rspec" + + expect(out).to include("rspec was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + G + end + end + + context "when gem to be removed is outside block" do + it "does not modify group" do + gemfile <<-G + source "https://gem.repo1" + + gem "myrack" + group :test do + gem "coffee-script-source" + end + G + + bundle "remove myrack" + + expect(out).to include("myrack was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + + group :test do + gem "coffee-script-source" + end + G + end + end + + context "when an empty block is also present" do + it "removes all empty blocks" do + gemfile <<-G + source "https://gem.repo1" + + group :test do + gem "rspec" + end + + group :dev do + end + G + + bundle "remove rspec" + + expect(out).to include("rspec was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + G + end + end + + context "when the gem belongs to multiple groups" do + it "removes the groups" do + gemfile <<-G + source "https://gem.repo1" + + group :test, :serioustest do + gem "rspec" + end + G + + bundle "remove rspec" + + expect(out).to include("rspec was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + G + end + end + + context "when the gem is present in multiple groups" do + it "removes all empty blocks" do + gemfile <<-G + source "https://gem.repo1" + + group :one do + gem "rspec" + end + + group :two do + gem "rspec" + end + G + + bundle "remove rspec" + + expect(out).to include("rspec was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + G + end + end + end + + describe "nested group blocks" do + context "when all the groups will be empty after removal" do + it "removes the empty nested blocks" do + gemfile <<-G + source "https://gem.repo1" + + group :test do + group :serioustest do + gem "rspec" + end + end + G + + bundle "remove rspec" + + expect(out).to include("rspec was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + G + end + end + + context "when outer group will not be empty after removal" do + it "removes only empty blocks" do + install_gemfile <<-G + source "https://gem.repo1" + + group :test do + gem "myrack-test" + + group :serioustest do + gem "rspec" + end + end + G + + bundle "remove rspec" + + expect(out).to include("rspec was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + + group :test do + gem "myrack-test" + + end + G + end + end + + context "when inner group will not be empty after removal" do + it "removes only empty blocks" do + install_gemfile <<-G + source "https://gem.repo1" + + group :test do + group :serioustest do + gem "rspec" + gem "myrack-test" + end + end + G + + bundle "remove rspec" + + expect(out).to include("rspec was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + + group :test do + group :serioustest do + gem "myrack-test" + end + end + G + end + end + end + + describe "arbitrary gemfile" do + context "when multiple gems are present in same line" do + it "shows warning for gems not removed" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack"; gem "rails" + G + + bundle "remove rails", raise_on_error: false + + 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, raise_on_error: false + source "https://gem.repo1" + gem"myrack" + gem"rspec" + gem "rails" + gem "minitest" + G + + bundle "remove rails myrack rspec minitest" + + expect(out).to include("rails was removed.") + expect(out).to include("minitest was removed.") + 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 + end + end + + context "with sources" do + before do + build_repo3 do + build_gem "rspec" + end + end + + it "removes gems and empty source blocks" do + gemfile <<-G + source "https://gem.repo1" + + gem "myrack" + + source "https://gem.repo3" do + gem "rspec" + end + G + + bundle "install" + + bundle "remove rspec" + + expect(out).to include("rspec was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + + gem "myrack" + G + end + end + + describe "with eval_gemfile" do + context "when gems are present in both gemfiles" do + it "removes the gems" do + gemfile "Gemfile-other", <<-G + gem "myrack" + G + + install_gemfile <<-G + source "https://gem.repo1" + + eval_gemfile "Gemfile-other" + + gem "myrack" + G + + bundle "remove myrack" + + expect(out).to include("myrack was removed.") + end + end + + context "when gems are present in other gemfile" do + it "removes the gems" do + gemfile "Gemfile-other", <<-G + gem "myrack" + G + + install_gemfile <<-G + source "https://gem.repo1" + + eval_gemfile "Gemfile-other" + G + + bundle "remove myrack" + + expect(bundled_app("Gemfile-other").read).to_not include("gem \"myrack\"") + expect(out).to include("myrack was removed.") + end + end + + context "when gems to be removed are not specified in any of the gemfiles" do + it "throws error for the gems not present" do + # an empty gemfile + # indicating the gem is not present in the gemfile + create_file "Gemfile-other", <<-G + G + + install_gemfile <<-G + source "https://gem.repo1" + + eval_gemfile "Gemfile-other" + G + + bundle "remove myrack", raise_on_error: false + + 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 + gemfile "Gemfile-other", <<-G + gem "rails" + G + + install_gemfile <<-G + source "https://gem.repo1" + + eval_gemfile "Gemfile-other" + gem "myrack" + G + + bundle "remove myrack", raise_on_error: false + + 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 cannot be removed from other gemfile" do + it "shows error" do + gemfile "Gemfile-other", <<-G + gem "rails"; gem "myrack" + G + + install_gemfile <<-G + source "https://gem.repo1" + + eval_gemfile "Gemfile-other" + gem "myrack" + G + + bundle "remove myrack", raise_on_error: false + + expect(out).to include("myrack was removed.") + expect(err).to include("Gems could not be removed. rails (>= 0) would also have been removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + + eval_gemfile "Gemfile-other" + G + end + end + + context "when gems could not be removed from parent gemfile" do + it "shows error" do + gemfile "Gemfile-other", <<-G + gem "myrack" + G + + install_gemfile <<-G + source "https://gem.repo1" + + eval_gemfile "Gemfile-other" + gem "rails"; gem "myrack" + G + + 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 \"myrack\"") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + + eval_gemfile "Gemfile-other" + 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 cannot be removed" do + gemfile "Gemfile-other", <<-G + gem "myrack" + G + + install_gemfile <<-G + source "https://gem.repo1" + + eval_gemfile "Gemfile-other" + gem"myrack" + G + + bundle "remove myrack" + + expect(out).to include("myrack was removed.") + expect(bundled_app("Gemfile-other").read).to_not include("gem \"myrack\"") + end + end + end + + context "with install_if" do + it "removes gems inside blocks and empty blocks" do + install_gemfile <<-G + source "https://gem.repo1" + + install_if(lambda { false }) do + gem "myrack" + end + G + + bundle "remove myrack" + + expect(out).to include("myrack was removed.") + expect(gemfile).to eq <<~G + source "https://gem.repo1" + G + end + end + + context "with env" do + it "removes gems inside blocks and empty blocks" do + install_gemfile <<-G + source "https://gem.repo1" + + env "BUNDLER_TEST" do + gem "myrack" + end + G + + bundle "remove myrack" + + 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("foo")) do |s| + s.write("foo.gemspec", "") + s.add_dependency "myrack" + end + + install_gemfile(<<-G) + source "https://gem.repo1" + gemspec :path => '#{tmp("foo")}', :name => 'foo' + G + + 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 new file mode 100644 index 0000000000..d0d55ffbb9 --- /dev/null +++ b/spec/bundler/commands/show_spec.rb @@ -0,0 +1,217 @@ +# frozen_string_literal: true + +RSpec.describe "bundle show" do + context "with a standard Gemfile" do + before :each do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "rails" + G + end + + it "creates a Gemfile.lock if one did not exist" do + FileUtils.rm(bundled_app_lock) + + bundle "show" + + expect(bundled_app_lock).to exist + end + + it "creates a Gemfile.lock when invoked with a gem name" do + FileUtils.rm(bundled_app_lock) + + bundle "show rails" + + expect(bundled_app_lock).to exist + end + + it "prints path if gem exists in bundle" do + bundle "show rails" + expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s) + end + + it "prints path if gem exists in bundle (with --paths option)" do + bundle "show rails --paths" + expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s) + end + + 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(/is missing/i) + expect(err).to match(default_bundle_path("gems", "rails-2.3.2").to_s) + end + + it "prints the path to the running bundler" do + bundle "show bundler" + expect(out).to eq(root.to_s) + end + + it "complains if gem not in bundle" do + 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-#{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. + gem_list = out.split.map {|p| p.split("/").last } + expect(gem_list).to eq(gem_list.sort) + end + + it "prints summary of gems" do + bundle "show --verbose" + + expect(out).to include <<~MSG + * actionmailer (2.3.2) + \tSummary: This is just a fake gem for testing + \tHomepage: http://example.com + \tStatus: Up to date + MSG + end + + it "includes bundler in the summary of gems" do + bundle "show --verbose" + + expect(out).to include <<~MSG + * bundler (#{Bundler::VERSION}) + \tSummary: The best way to manage your application's dependencies + \tHomepage: https://bundler.io + \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 + before :each do + @git = build_git "foo", "1.0" + end + + it "prints out git info" do + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + expect(the_bundle).to include_gems "foo 1.0" + + bundle :show + expect(out).to include("foo (1.0 #{@git.ref_for("main", 6)}") + end + + 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 + gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "omg" + G + expect(the_bundle).to include_gems "foo 1.0.omg" + + bundle :show + expect(out).to include("foo (1.0 #{@git.ref_for("omg", 6)}") + end + + it "doesn't print the branch when tied to a ref" do + sha = revision_for(lib_path("foo-1.0")) + install_gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{sha}" + G + + bundle :show + expect(out).to include("foo (1.0 #{sha[0..6]})") + end + + it "handles when a version is a '-' prerelease" do + @git = build_git("foo", "1.0.0-beta.1", path: lib_path("foo")) + install_gemfile <<-G + gem "foo", "1.0.0-beta.1", :git => "#{lib_path("foo")}" + G + expect(the_bundle).to include_gems "foo 1.0.0.pre.beta.1" + + bundle :show + expect(out).to include("foo (1.0.0.pre.beta.1") + end + end + + context "in a fresh gem in a blank git repo" do + before :each do + build_git "foo", path: lib_path("foo") + 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, dir: lib_path("foo") + expect(err_without_deprecations).to be_empty + end + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "https://gem.repo1" + gem "foo" + G + + 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", :readline do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + bundle "show rac" + 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 "https://gem.repo1" + gem "rails" + G + + invalid_regexp = "[]" + + bundle "show #{invalid_regexp}", raise_on_error: false + expect(err).to include("Could not find gem '#{invalid_regexp}'.") + end + end +end + +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 new file mode 100644 index 0000000000..03a3786d80 --- /dev/null +++ b/spec/bundler/commands/update_spec.rb @@ -0,0 +1,2095 @@ +# frozen_string_literal: true + +RSpec.describe "bundle update" do + describe "with no arguments" 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" + expect(out).to include("Bundle updated!") + 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 "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + exit! + G + 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" 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 + expect(out).to include("Bundle updated!") + 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 "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + exit! + G + bundle "update", all: true, raise_on_error: false + expect(bundled_app_lock).to exist + end + end + + describe "with --gemfile" do + it "creates lockfiles based on the Gemfile name" do + gemfile bundled_app("OmgFile"), <<-G + source "https://gem.repo1" + gem "myrack", "1.0" + G + + 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 "update_requires_all_flag true" } + + it "errors when passed nothing" do + 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 "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 "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!") + end + 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 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", raise_on_error: false + expect(err).to include "Could not find gem 'halting-problem-solver'" + end + it "should suggest alternatives" do + 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 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 + 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 + + 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 + + bundle "update", all: true + + expect(the_bundle).to include_gems("slim 3.0.9", "slim-rails 3.1.3", "slim_lint 0.16.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 + + build_gem "sixarm_ruby_unaccent", "1.1.0" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "country_select" + gem "countries" + G + + 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 + + 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 + + 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 + + build_gem "faraday", "2.5.2" + + 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) + + 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 + + # 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 + before do + build_repo2 + + gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + gem "platform_specific" + G + end + + 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 "https://gem.repo2" + gem "activesupport", :group => :development + 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 "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 "https://gem.repo2" + gem "activemerchant", :group => :development + gem "activesupport" + G + update_repo2 do + build_gem "activemerchant", "2.0" + build_gem "activesupport", "3.0" + end + bundle "update --conservative --group development" + expect(the_bundle).to include_gems "activemerchant 2.0" + expect(the_bundle).not_to include_gems "activesupport 3.0" + end + end + + context "when there is a source with the same name as a gem in a group" do + before do + build_git "foo", path: lib_path("activesupport") + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport", :group => :development + gem "foo", :git => "#{lib_path("activesupport")}" + G + end + + it "should not update the gems from that source" do + update_repo2 { build_gem "activesupport", "3.0" } + update_git "foo", "2.0", path: lib_path("activesupport") + + bundle "update --group development" + expect(the_bundle).to include_gems "activesupport 3.0" + expect(the_bundle).not_to include_gems "foo 2.0" + end + end + + context "when bundler itself is a transitive dependency" do + it "executes without error" do + install_gemfile <<-G + source "https://gem.repo1" + gem "activesupport", :group => :development + 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 "myrack 1.2" + end + end + end + + describe "in a frozen bundle" do + 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 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 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 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 + 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).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" 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_gems "activesupport 3.0" + end + end + + context "when there is a child dependency that is also in the gemfile" do + before do + build_repo2 do + build_gem "fred", "1.0" + build_gem "harry", "1.0" do |s| + s.add_dependency "fred" + end + end + + install_gemfile <<-G + source "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" 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 1.0", "fred 1.0" + end + end + + context "when there is a child dependency that appears elsewhere in the dependency graph" do + before do + build_repo2 do + build_gem "fred", "1.0" do |s| + s.add_dependency "george" + end + build_gem "george", "1.0" + build_gem "harry", "1.0" do |s| + s.add_dependency "george" + end + end + + install_gemfile <<-G + source "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" do + update_repo2 do + build_gem "george", "2.0" + build_gem "harry", "2.0" do |s| + s.add_dependency "george" + end + end + + bundle "update --source harry" + expect(the_bundle).to include_gems "harry 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 + + 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 "request_store", "1.1.0" do |s| + s.add_dependency "myrack", "1.0.0" + end + end + + 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 do + build_repo2 + end + + it "will eagerly unlock dependencies of a specified gem" do + install_gemfile <<-G + source "https://gem.repo2" + + gem "thin" + 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 "myrack" + end + end + + bundle "update thin" + 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 "https://gem.repo2" + + gem "thin" + gem "myrack-obama" + G + + update_repo2 do + build_gem("thin", "2.0") {|s| s.add_dependency "myrack" } + build_gem "myrack", "10.0" + end + + 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 + source "https://gem.repo1" + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + update_git "foo" do |s| + s.write "lib/foo2.rb", "puts :foo2" + end + + bundle "update foo" + + expect(stdboth).not_to include "attempted to update" + end + + it "will not warn when changing gem sources but not versions" do + build_git "myrack" + + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack", :git => '#{lib_path("myrack-1.0")}' + G + + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle "update myrack" + + expect(stdboth).not_to include "attempted to update" + end + + it "will update only from pinned source" do + install_gemfile <<-G + source "https://gem.repo2" + + source "https://gem.repo1" do + gem "thin" + end + G + + update_repo2 do + build_gem "thin", "2.0" + end + + 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 + around do |example| + build_repo4 do + build_gem("a", "0.9") + build_gem("a", "0.9") {|s| s.platform = "java" } + build_gem("a", "1.1") + build_gem("a", "1.1") {|s| s.platform = "java" } + end + + gemfile <<-G + source "https://gem.repo4" + gem "a" + G + + lockfile <<-L + GEM + remote: https://gem.repo4 + specs: + a (0.9-java) + + PLATFORMS + java + + DEPENDENCIES + a + L + + simulate_platform "x86_64-linux", &example + end + + it "allows updating" do + bundle :update, all: true + expect(the_bundle).to include_gem "a 1.1" + end + + it "allows updating a specific gem" do + bundle "update a" + expect(the_bundle).to include_gem "a 1.1" + end + end + + context "when the dependency is for a different platform" do + before do + build_repo4 do + build_gem("a", "0.9") {|s| s.platform = "java" } + build_gem("a", "1.1") {|s| s.platform = "java" } + end + + gemfile <<-G + source "https://gem.repo4" + gem "a", platform: :jruby + G + + lockfile <<-L + GEM + remote: https://gem.repo4 + specs: + a (0.9-java) + + PLATFORMS + java + + DEPENDENCIES + a + L + end + + it "is not updated because it is not actually included in the bundle" do + 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 + +RSpec.describe "bundle update without a Gemfile.lock" do + it "should not explode" do + build_repo2 + + gemfile <<-G + source "https://gem.repo2" + + gem "myrack", "1.0" + G + + bundle "update", all: true + + 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 do + build_repo2 do + build_gem "rails", "3.0.1" do |s| + s.add_dependency "bundler", "9.9.9" + end + + build_gem "bundler", "9.9.9" + end + + gemfile <<-G + source "https://gem.repo2" + gem "rails", "3.0.1" + G + end + + 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(/Install the necessary version with `gem install bundler:9\.9\.9`/i) + end +end + +RSpec.describe "bundle update --ruby" do + context "when the Gemfile removes the ruby" do + before do + install_gemfile <<-G + 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" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + #{checksums_section_when_enabled} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when the Gemfile specified an updated Ruby version" do + before do + install_gemfile <<-G + 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" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + #{checksums_section_when_enabled} + RUBY VERSION + #{Bundler::RubyVersion.system} + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when a different Ruby is being used than has been versioned" do + before do + install_gemfile <<-G + ruby '~> #{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", raise_on_error: false + + 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 + 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" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + #{checksums_section_when_enabled} + RUBY VERSION + #{Bundler::RubyVersion.system} + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end +end + +RSpec.describe "bundle update --bundler" do + it "updates the bundler version in the lockfile" do + build_repo4 do + build_gem "bundler", "2.5.9" + build_gem "myrack", "1.0" + end + + 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') + + 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 + + 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} + + 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 + +# these specs are slow and focus on integration and therefore are not exhaustive. unit specs elsewhere handle that. +RSpec.describe "bundle update conservative" do + context "patch and minor options" do + before do + build_repo4 do + build_gem "foo", %w[1.4.3 1.4.4] do |s| + s.add_dependency "bar", "~> 2.0" + end + build_gem "foo", %w[1.4.5 1.5.0] do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "foo", %w[1.5.1] do |s| + s.add_dependency "bar", "~> 3.0" + end + build_gem "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 "https://gem.repo4" + gem 'foo', '1.4.3' + gem 'bar', '2.0.3' + gem 'qux', '1.0.0' + G + + # remove 1.4.3 requirement and bar altogether + # to setup update specs below + gemfile <<-G + source "https://gem.repo4" + gem 'foo' + gem 'qux' + G + end + + context "with patch set as default update level in config" do + it "should do a patch level update" do + 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 + end + + context "patch preferred" do + it "single gem updates dependent gem to minor" do + bundle "update --patch foo" + + expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.0" + end + + it "update all" do + bundle "update --patch", all: true + + expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.1" + end + end + + context "minor preferred" do + it "single gem updates dependent gem to major" do + bundle "update --minor foo" + + expect(the_bundle).to include_gems "foo 1.5.1", "bar 3.0.0", "qux 1.0.0" + end + end + + context "strict" do + it "patch preferred" do + bundle "update --patch foo bar --strict" + + expect(the_bundle).to include_gems "foo 1.4.4", "bar 2.0.5", "qux 1.0.0" + end + + it "minor preferred" do + bundle "update --minor --strict", 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 + before do + build_repo4 do + build_gem "isolated_owner", %w[1.0.1 1.0.2] do |s| + s.add_dependency "isolated_dep", "~> 2.0" + end + build_gem "isolated_dep", %w[2.0.1 2.0.2] + + build_gem "shared_owner_a", %w[3.0.1 3.0.2] do |s| + s.add_dependency "shared_dep", "~> 5.0" + end + build_gem "shared_owner_b", %w[4.0.1 4.0.2] do |s| + s.add_dependency "shared_dep", "~> 5.0" + end + build_gem "shared_dep", %w[5.0.1 5.0.2] + end + + gemfile <<-G + source "https://gem.repo4" + gem 'isolated_owner' + + gem 'shared_owner_a' + gem 'shared_owner_b' + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + isolated_dep (2.0.1) + isolated_owner (1.0.1) + isolated_dep (~> 2.0) + shared_dep (5.0.1) + shared_owner_a (3.0.1) + shared_dep (~> 5.0) + shared_owner_b (4.0.1) + shared_dep (~> 5.0) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + isolated_owner + shared_owner_a + shared_owner_b + + CHECKSUMS + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "should eagerly unlock isolated dependency" do + bundle "update isolated_owner" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.2", "shared_dep 5.0.1", "shared_owner_a 3.0.1", "shared_owner_b 4.0.1" + end + + it "should eagerly unlock shared dependency" do + bundle "update shared_owner_a" + + expect(the_bundle).to include_gems "isolated_owner 1.0.1", "isolated_dep 2.0.1", "shared_dep 5.0.2", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + end + + it "should not eagerly unlock with --conservative" do + bundle "update --conservative shared_owner_a isolated_owner" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.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 "https://gem.repo4" + gem 'isolated_owner', '1.0.2' + + gem 'shared_owner_a', '3.0.2' + gem 'shared_owner_b' + G + + bundle "install" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.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 "source 'https://gem.repo1'" + end + + it "raises if too many flags are provided" do + bundle "update --patch --minor", all: true, raise_on_error: false + + expect(err).to eq "Provide only one of the following options: minor, patch" + end + end +end diff --git a/spec/bundler/commands/version_spec.rb b/spec/bundler/commands/version_spec.rb new file mode 100644 index 0000000000..4320ad0611 --- /dev/null +++ b/spec/bundler/commands/version_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require_relative "../support/path" + +RSpec.describe "bundle version" do + if Spec::Path.ruby_core? + COMMIT_HASH = /unknown|[a-fA-F0-9]{7,}/ + else + COMMIT_HASH = /[a-fA-F0-9]{7,}/ + end + + context "with -v" do + it "outputs the version and virtual version if set" do + bundle "-v" + expect(out).to eq(Bundler::VERSION.to_s) + + 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 and virtual version if set" do + bundle "--version" + expect(out).to eq(Bundler::VERSION.to_s) + + bundle_config "simulate_version 5" + bundle "--version" + expect(out).to eq("#{Bundler::VERSION} (simulating Bundler 5)") + end + end + + context "with version" do + 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 + + 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 |
