diff options
Diffstat (limited to 'spec/bundler/commands')
32 files changed, 16036 insertions, 2953 deletions
diff --git a/spec/bundler/commands/add_spec.rb b/spec/bundler/commands/add_spec.rb index 4931402c33..162650f2e5 100644 --- a/spec/bundler/commands/add_spec.rb +++ b/spec/bundler/commands/add_spec.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle add" do before :each do @@ -10,100 +9,440 @@ RSpec.describe "bundle add" do build_gem "bar", "0.12.3" build_gem "cat", "0.12.3.pre" build_gem "dog", "1.1.3.pre" + build_gem "lemur", "3.1.1.pre.2023.1.1" end - install_gemfile <<-G - source "file://#{gem_repo2}" + 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 + it "version requirement becomes >= major.minor.patch when resolved version is < 1.0" do bundle "add 'bar'" - expect(bundled_app("Gemfile").read).to match(/gem "bar", "~> 0.12.3"/) + expect(bundled_app_gemfile.read).to match(/gem "bar", ">= 0.12.3"/) expect(the_bundle).to include_gems "bar 0.12.3" end - it "version requirement becomes ~> major.minor when resolved version is > 1.0" do + it "version requirement becomes >= major.minor when resolved version is > 1.0" do bundle "add 'baz'" - expect(bundled_app("Gemfile").read).to match(/gem "baz", "~> 1.2"/) + expect(bundled_app_gemfile.read).to match(/gem "baz", ">= 1.2"/) expect(the_bundle).to include_gems "baz 1.2.3" end - it "version requirement becomes ~> major.minor.patch.pre when resolved version is < 1.0" do + it "version requirement becomes >= major.minor.patch.pre when resolved version is < 1.0" do bundle "add 'cat'" - expect(bundled_app("Gemfile").read).to match(/gem "cat", "~> 0.12.3.pre"/) + expect(bundled_app_gemfile.read).to match(/gem "cat", ">= 0.12.3.pre"/) expect(the_bundle).to include_gems "cat 0.12.3.pre" end - it "version requirement becomes ~> major.minor.pre when resolved version is > 1.0.pre" do + it "version requirement becomes >= major.minor.pre when resolved version is >= 1.0.pre" do bundle "add 'dog'" - expect(bundled_app("Gemfile").read).to match(/gem "dog", "~> 1.1.pre"/) + expect(bundled_app_gemfile.read).to match(/gem "dog", ">= 1.1.pre"/) expect(the_bundle).to include_gems "dog 1.1.3.pre" end + + it "version requirement becomes >= major.minor.pre.tail when resolved version has a very long tail pre version" do + bundle "add 'lemur'" + # the trailing pre purposely matches the release version to ensure that subbing the release doesn't change the pre.version" + expect(bundled_app_gemfile.read).to match(/gem "lemur", ">= 3.1.pre.2023.1.1"/) + expect(the_bundle).to include_gems "lemur 3.1.1.pre.2023.1.1" + end end describe "with --version" do it "adds dependency of specified version and runs install" do bundle "add 'foo' --version='~> 1.0'" - expect(bundled_app("Gemfile").read).to match(/gem "foo", "~> 1.0"/) + expect(bundled_app_gemfile.read).to match(/gem "foo", "~> 1.0"/) expect(the_bundle).to include_gems "foo 1.1" end it "adds multiple version constraints when specified" do - bundle "add 'foo' --version='< 3.0, > 1.1'" - expect(bundled_app("Gemfile").read).to match(/gem "foo", "< 3.0", "> 1.1"/) + 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(bundled_app_gemfile.read).to match(/gem "foo", ">= 2.0", group: :development/) expect(the_bundle).to include_gems "foo 2.0" end it "adds dependency to more than one group" do bundle "add 'foo' --group='development, test'" - expect(bundled_app("Gemfile").read).to match(/gem "foo", "~> 2.0", :groups => \[:development, :test\]/) + expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2.0", groups: \[:development, :test\]/) expect(the_bundle).to include_gems "foo 2.0" end end describe "with --source" do it "adds dependency with specified source" do - bundle "add 'foo' --source='file://#{gem_repo2}'" - expect(bundled_app("Gemfile").read).to match(%r{gem "foo", "~> 2.0", :source => "file:\/\/#{gem_repo2}"}) + 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='file://#{gem_repo2}' -g='development' -v='~>1.0'" - expect(bundled_app("Gemfile").read).to match(%r{gem "foo", "~> 1.0", :group => \[:development\], :source => "file:\/\/#{gem_repo2}"}) + 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'" - expect(out).to match("Invalid gem requirement pattern '~>1 . 0'") + bundle "add 'foo' -v='~>1 . 0'", raise_on_error: false + expect(err).to match("Invalid gem requirement pattern '~>1 . 0'") end it "shows error message when gem cannot be found" do - bundle "add 'werk_it'" - expect(out).to match("Could not find gem 'werk_it' in any of the gem sources listed in your Gemfile.") + bundle_config "force_ruby_platform true" + bundle "add 'werk_it'", raise_on_error: false + expect(err).to match("Could not find gem 'werk_it' in") - bundle "add 'werk_it' -s='file://#{gem_repo2}'" - expect(out).to match("Could not find gem 'werk_it' in rubygems repository") + 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'" - expect(out).to include("Could not reach host badhostasdf. Check your network connection and try again.") + 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" - bundle "add 'baz' --source='file://does/not/exist'" - expect(out).to include("Could not fetch specs from file://does/not/exist/") + 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 index cb0999348e..af4d24a9e8 100644 --- a/spec/bundler/commands/binstubs_spec.rb +++ b/spec/bundler/commands/binstubs_spec.rb @@ -1,74 +1,98 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle binstubs <gem>" do context "when the gem exists in the lockfile" do it "sets up the binstub" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "binstubs rack" + bundle "binstubs myrack" - expect(bundled_app("bin/rackup")).to exist + expect(bundled_app("bin/myrackup")).to exist end it "does not install other binstubs" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" gem "rails" G bundle "binstubs rails" - expect(bundled_app("bin/rackup")).not_to exist + expect(bundled_app("bin/myrackup")).not_to exist expect(bundled_app("bin/rails")).to exist end it "does install multiple binstubs" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" gem "rails" G - bundle "binstubs rails rack" + bundle "binstubs rails myrack" - expect(bundled_app("bin/rackup")).to exist + expect(bundled_app("bin/myrackup")).to exist expect(bundled_app("bin/rails")).to exist end - it "displays an error when used without any gem" do + it "allows installing all binstubs" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "rails" G - bundle "binstubs" - expect(exitstatus).to eq(1) if exitstatus - expect(out).to include("`bundle binstubs` needs at least one gem to run.") + bundle :binstubs, all: true + + expect(bundled_app("bin/rails")).to exist + expect(bundled_app("bin/rake")).to exist end - it "does not bundle the bundler binary" do + it "allows installing binstubs for all platforms" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" + gem "myrack" G - bundle "binstubs bundler" + bundle "binstubs myrack --all-platforms" - expect(bundled_app("bin/bundle")).not_to exist - expect(out).to include("Sorry, Bundler can only be run via Rubygems.") + 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) + 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 @@ -80,10 +104,11 @@ RSpec.describe "bundle binstubs <gem>" do it "installs binstubs from path gems" do FileUtils.mkdir_p(lib_path("foo/bin")) FileUtils.touch(lib_path("foo/bin/foo")) - build_lib "foo", "1.0", :path => lib_path("foo") do |s| - s.executables = %w(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 @@ -95,13 +120,25 @@ RSpec.describe "bundle binstubs <gem>" do it "sets correct permissions for binstubs" do with_umask(0o002) do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + 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 rack" - binary = bundled_app("bin/rackup") - expect(File.stat(binary).mode.to_s(8)).to eq("100775") + bundle "binstubs myrack --shebang jruby" + expect(File.readlines(bundled_app("bin/myrackup")).first).to eq("#!/usr/bin/env jruby\n") end end end @@ -109,94 +146,123 @@ RSpec.describe "bundle binstubs <gem>" do context "when the gem doesn't exist" do it "displays an error with correct status" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" G - bundle "binstubs doesnt_exist" + bundle "binstubs doesnt_exist", raise_on_error: false - expect(exitstatus).to eq(7) if exitstatus - expect(out).to include("Could not find gem 'doesnt_exist'.") + expect(exitstatus).to eq(7) + expect(err).to include("Could not find gem 'doesnt_exist'.") end end - context "--path" do - it "sets the binstubs dir" do + 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 "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "binstubs rack --path exec" + bundle "binstubs myrack" - expect(bundled_app("exec/rackup")).to exist + expect(bundled_app("exec/myrackup")).to exist end + end - it "setting is saved for bundle install" do + context "with --standalone option" do + before do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" gem "rails" G + end - bundle "binstubs rack --path exec" - bundle :install + it "generates a standalone binstub" do + bundle "binstubs myrack --standalone" + expect(bundled_app("bin/myrackup")).to exist + end - expect(bundled_app("exec/rails")).to exist + 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 - end - context "after installing with --standalone" do - before do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - G - bundle "install --standalone" + 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 - it "includes the standalone path" do - bundle "binstubs rack --standalone" - standalone_line = File.read(bundled_app("bin/rackup")).each_line.find {|line| line.include? "$:.unshift" }.strip - expect(standalone_line).to eq %($:.unshift File.expand_path "../../bundle", path.realpath) + 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/rackup"), "wb") do |file| + File.open(bundled_app("bin/myrackup"), "wb") do |file| file.print "OMG" end install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "binstubs rack" + bundle "binstubs myrack" - expect(bundled_app("bin/rackup")).to exist - expect(File.read(bundled_app("bin/rackup"))).to eq("OMG") - expect(out).to include("Skipped rackup") - expect(out).to include("overwrite skipped stubs, use --force") + expect(bundled_app("bin/myrackup")).to exist + expect(File.read(bundled_app("bin/myrackup"))).to eq("OMG") + expect(err).to include("Skipped myrackup") + expect(err).to include("overwrite skipped stubs, use --force") end context "when using --force" do it "overwrites the binstub" do FileUtils.mkdir_p(bundled_app("bin")) - File.open(bundled_app("bin/rackup"), "wb") do |file| + File.open(bundled_app("bin/myrackup"), "wb") do |file| file.print "OMG" end install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "binstubs rack --force" + bundle "binstubs myrack --force" - expect(bundled_app("bin/rackup")).to exist - expect(File.read(bundled_app("bin/rackup"))).not_to eq("OMG") + expect(bundled_app("bin/myrackup")).to exist + expect(File.read(bundled_app("bin/myrackup"))).not_to eq("OMG") end end end @@ -204,58 +270,64 @@ RSpec.describe "bundle binstubs <gem>" do context "when the gem has no bins" do it "suggests child gems if they have bins" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack-obama" + source "https://gem.repo1" + gem "myrack-obama" G - bundle "binstubs rack-obama" - expect(out).to include("rack-obama has no executables") - expect(out).to include("rack has: rackup") + bundle "binstubs myrack-obama" + expect(err).to include("myrack-obama has no executables") + expect(err).to include("myrack has: myrackup") end it "works if child gems don't have bins" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "actionpack" G bundle "binstubs actionpack" - expect(out).to include("no executables for the gem 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 "file://#{gem_repo1}" + source "https://gem.repo2" gem "with_development_dependency" G bundle "binstubs with_development_dependency" - expect(out).to include("no executables for the gem 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 "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "config auto_install 1" - bundle "binstubs rack" - expect(out).to include("Installing rack 1.0.0") - expect(the_bundle).to include_gems "rack 1.0.0" + bundle_config "auto_install 1" + bundle "binstubs myrack" + expect(out).to include("Installing myrack 1.0.0") + expect(the_bundle).to include_gems "myrack 1.0.0" end it "does nothing when already up to date" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "config auto_install 1" - bundle "binstubs rack", :env => { "BUNDLE_INSTALL" => 1 } - expect(out).not_to include("Installing rack 1.0.0") + bundle_config "auto_install 1" + bundle "binstubs myrack", env: { "BUNDLE_INSTALL" => "1" } + expect(out).not_to include("Installing myrack 1.0.0") end end end diff --git a/spec/bundler/commands/cache_spec.rb b/spec/bundler/commands/cache_spec.rb 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 index 532be07c3f..7fe6897ae3 100644 --- a/spec/bundler/commands/check_spec.rb +++ b/spec/bundler/commands/check_spec.rb @@ -1,154 +1,200 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle check" do it "returns success when the Gemfile is satisfied" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "rails" G bundle :check - expect(exitstatus).to eq(0) if exitstatus expect(out).to include("The Gemfile's dependencies are satisfied") end it "works with the --gemfile flag when not in the directory" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "rails" G - Dir.chdir tmp - bundle "check --gemfile bundled_app/Gemfile" + bundle "check --gemfile bundled_app/Gemfile", dir: tmp expect(out).to include("The Gemfile's dependencies are satisfied") end it "creates a Gemfile.lock by default if one does not exist" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "rails" G - FileUtils.rm("Gemfile.lock") + FileUtils.rm(bundled_app_lock) bundle "check" - expect(bundled_app("Gemfile.lock")).to exist + expect(bundled_app_lock).to exist end it "does not create a Gemfile.lock if --dry-run was passed" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "rails" G - FileUtils.rm("Gemfile.lock") + FileUtils.rm(bundled_app_lock) bundle "check --dry-run" - expect(bundled_app("Gemfile.lock")).not_to exist + expect(bundled_app_lock).not_to exist end - it "prints a generic error if the missing gems are unresolvable" do - system_gems ["rails-2.3.2"] + it "prints an error that shows missing gems" do + system_gems ["rails-2.3.2"], path: default_bundle_path gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "rails" G - bundle :check - expect(out).to include("Bundler can't satisfy your Gemfile's dependencies.") + bundle :check, raise_on_error: false + expect(err).to include("The following gems are missing") + expect(err).to include(" * rake (#{rake_version})") + expect(err).to include(" * actionpack (2.3.2)") + expect(err).to include(" * activerecord (2.3.2)") + expect(err).to include(" * actionmailer (2.3.2)") + expect(err).to include(" * activeresource (2.3.2)") + expect(err).to include(" * activesupport (2.3.2)") + expect(err).to include("Install missing gems with `bundle install`") end - it "prints a generic error if a Gemfile.lock does not exist and a toplevel dependency does not exist" do + it "prints an error that shows missing gems if a Gemfile.lock does not exist and a toplevel dependency is missing" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "rails" G - bundle :check - expect(exitstatus).to be > 0 if exitstatus - expect(out).to include("Bundler can't satisfy your Gemfile's dependencies.") + 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 message if you changed your lockfile" do + 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 "file://#{gem_repo1}" - gem 'rails' + source "https://gem.repo1" + gem "foo", git: "#{lib_path("foo")}" G + + FileUtils.rm_r bundled_app("vendor/bundle") + bundle :check, raise_on_error: false + expect(exitstatus).to eq 1 + expect(err).to include("Bundler can't satisfy your Gemfile's dependencies.") + end + + it "prints a generic message if you changed your lockfile" do + build_repo2 do + build_gem "rails_pinned_to_old_activesupport" do |s| + s.add_dependency "activesupport", "= 1.2.3" + end + end + install_gemfile <<-G - source "file://#{gem_repo1}" - gem 'rails_fail' + source "https://gem.repo2" + gem 'rails' G gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo2" gem "rails" - gem "rails_fail" + gem "rails_pinned_to_old_activesupport" G - bundle :check - expect(out).to include("Bundler can't satisfy your Gemfile's dependencies.") + bundle :check, raise_on_error: false + expect(err).to include("Bundler can't satisfy your Gemfile's dependencies.") end - it "remembers --without option from install" do - gemfile <<-G - source "file://#{gem_repo1}" + it "uses the without setting" do + bundle_config "without foo" + install_gemfile <<-G + source "https://gem.repo1" group :foo do - gem "rack" + gem "myrack" end G - bundle "install --without foo" bundle "check" - expect(exitstatus).to eq(0) if exitstatus expect(out).to include("The Gemfile's dependencies are satisfied") end it "ensures that gems are actually installed and not just cached" do gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", :group => :foo + source "https://gem.repo1" + gem "myrack", :group => :foo G - bundle "install --without foo" + bundle_config "without foo" + bundle :install gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "check" - expect(out).to include("* rack (1.0.0)") - expect(exitstatus).to eq(1) if exitstatus + 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 - system_gems "rack-1.0.0" + 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 "file://#{gem_repo1}" - gem "rack" + 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: file:#{gem_repo1}/ + remote: https://gem.repo1/ specs: activesupport (2.3.5) - rack (1.0.0) + myrack (1.0.0) PLATFORMS - #{local} + #{generic_local_platform} #{not_local} DEPENDENCIES - rack + myrack activesupport G @@ -157,29 +203,29 @@ RSpec.describe "bundle check" do end it "works with env conditionals" do - system_gems "rack-1.0.0" - gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + 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: file:#{gem_repo1}/ + remote: https://gem.repo1/ specs: activesupport (2.3.5) - rack (1.0.0) + myrack (1.0.0) PLATFORMS - #{local} + #{generic_local_platform} #{not_local} DEPENDENCIES - rack + myrack activesupport G @@ -188,134 +234,341 @@ RSpec.describe "bundle check" do end it "outputs an error when the default Gemfile is not found" do - bundle :check - expect(exitstatus).to eq(10) if exitstatus - expect(out).to include("Could not locate Gemfile") + bundle :check, raise_on_error: false + expect(exitstatus).to eq(10) + expect(err).to include("Could not locate Gemfile") end it "does not output fatal error message" do - bundle :check - expect(exitstatus).to eq(10) if exitstatus - expect(out).not_to include("Unfortunately, a fatal error has occurred. ") + bundle :check, raise_on_error: false + expect(exitstatus).to eq(10) + expect(err).not_to include("Unfortunately, a fatal error has occurred. ") end - it "should not crash when called multiple times on a new machine" do - gemfile <<-G - gem 'rails', '3.0.0.beta3' - gem 'paperclip', :git => 'git://github.com/thoughtbot/paperclip.git' + it "fails when there's no lockfile and frozen is set" do + install_gemfile <<-G + source "https://gem.repo1" + gem "foo" G - simulate_new_machine - bundle "check" - last_out = out - 3.times do + bundle_config "deployment true" + bundle "install" + FileUtils.rm(bundled_app_lock) + + bundle :check, raise_on_error: false + expect(last_command).to be_failure + end + + describe "when locked" do + before :each do + system_gems "myrack-1.0.0" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "1.0" + G + end + + it "returns success when the Gemfile is satisfied" do + bundle :install bundle :check - expect(out).to eq(last_out) - expect(err).to lack_errors + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "shows what is missing with the current Gemfile if it is not satisfied" do + FileUtils.rm_r default_bundle_path + default_system_gems + bundle :check, raise_on_error: false + expect(err).to match(/The following gems are missing/) + expect(err).to include("* myrack (1.0") end end - it "fails when there's no lock file and frozen is set" do - gemfile <<-G - source "file://#{gem_repo1}" - gem "foo" - G + describe "when locked with multiple dependents with different requirements" do + before :each do + build_repo4 do + build_gem "depends_on_myrack" do |s| + s.add_dependency "myrack", ">= 1.0" + end + build_gem "also_depends_on_myrack" do |s| + s.add_dependency "myrack", "~> 1.0" + end + build_gem "myrack" + end - bundle "install" - bundle "install --deployment" - FileUtils.rm(bundled_app("Gemfile.lock")) + gemfile <<-G + source "https://gem.repo4" + gem "depends_on_myrack" + gem "also_depends_on_myrack" + G - bundle :check - expect(exitstatus).not_to eq(0) if exitstatus + 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 - context "--path" do - before do + describe "when locked under multiple platforms" do + before :each do + build_repo4 do + build_gem "myrack" + end + gemfile <<-G - source "file://#{gem_repo1}" - gem "rails" + source "https://gem.repo4" + gem "myrack" G - bundle "install --path vendor/bundle" - FileUtils.rm_rf(bundled_app(".bundle")) - end + lockfile <<-L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) - it "returns success" do - bundle "check --path vendor/bundle" - expect(exitstatus).to eq(0) if exitstatus - expect(out).to include("The Gemfile's dependencies are satisfied") + PLATFORMS + ruby + #{local_platform} + + DEPENDENCIES + myrack + + BUNDLED WITH + #{Bundler::VERSION} + L end - it "should write to .bundle/config" do - bundle "check --path vendor/bundle" - bundle "check" - expect(exitstatus).to eq(0) if exitstatus + 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 - context "--path vendor/bundle after installing gems in the default directory" do - it "returns false" do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rails" + 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 - bundle "check --path vendor/bundle" - expect(exitstatus).to eq(1) if exitstatus - expect(out).to match(/The following gems are missing/) + 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 locked" do - before :each do - system_gems "rack-1.0.0" - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", "1.0" + 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" do - bundle :install - bundle :check - expect(exitstatus).to eq(0) if exitstatus + 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 - it "shows what is missing with the current Gemfile if it is not satisfied" do - simulate_new_machine - bundle :check - expect(out).to match(/The following gems are missing/) - expect(out).to include("* rack (1.0") + 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 + lock = <<~L GEM - remote: file:#{gem_repo1}/ + remote: https://gem.repo1/ specs: - rack (1.0.0) + myrack (1.0.0) PLATFORMS - #{generic_local_platform} + #{lockfile_platforms} DEPENDENCIES - rack + myrack L if bundler_version - lock += "\n BUNDLED WITH\n #{bundler_version}\n" + lock += "\nBUNDLED WITH\n #{bundler_version}\n" end lock end before do + bundle_config "path vendor/bundle" + install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G end @@ -323,25 +576,25 @@ RSpec.describe "bundle check" do it "does not change the lock" do lockfile lock_with(nil) bundle :check - lockfile_should_be lock_with(nil) + expect(lockfile).to eq lock_with(nil) end end context "is newer" do - it "does not change the lock but warns" do + it "does not change the lock and does not warn" do lockfile lock_with(Bundler::VERSION.succ) bundle :check - expect(out).to include("the running version of Bundler (#{Bundler::VERSION}) is older than the version that created the lockfile (#{Bundler::VERSION.succ})") - expect(err).to lack_errors - lockfile_should_be lock_with(Bundler::VERSION.succ) + expect(err).to be_empty + expect(lockfile).to eq lock_with(Bundler::VERSION.succ) end end context "is older" do it "does not change the lock" do - lockfile lock_with("1.10.1") + system_gems "bundler-1.18.0" + lockfile lock_with("1.18.0") bundle :check - lockfile_should_be lock_with("1.10.1") + expect(lockfile).to eq lock_with("1.18.0") end end end diff --git a/spec/bundler/commands/clean_spec.rb b/spec/bundler/commands/clean_spec.rb index 02d96a0ff7..c77859d378 100644 --- a/spec/bundler/commands/clean_spec.rb +++ b/spec/bundler/commands/clean_spec.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle clean" do def should_have_gems(*gems) @@ -20,16 +19,18 @@ RSpec.describe "bundle clean" do it "removes unused gems that are different" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "thin" gem "foo" G - bundle "install --path vendor/bundle --no-clean" + bundle_config "path vendor/bundle" + bundle_config "clean false" + bundle "install" gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "thin" G @@ -39,89 +40,95 @@ RSpec.describe "bundle clean" do expect(out).to include("Removing foo (1.0)") - should_have_gems "thin-1.0", "rack-1.0.0" + should_have_gems "thin-1.0", "myrack-1.0.0" should_not_have_gems "foo-1.0" - expect(vendored_gems("bin/rackup")).to exist + expect(vendored_gems("bin/myrackup")).to exist end it "removes old version of gem if unused" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack", "0.9.1" + gem "myrack", "0.9.1" gem "foo" G - bundle "install --path vendor/bundle --no-clean" + bundle_config "path vendor/bundle" + bundle_config "clean false" + bundle "install" gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" gem "foo" G bundle "install" bundle :clean - expect(out).to include("Removing rack (0.9.1)") + expect(out).to include("Removing myrack (0.9.1)") - should_have_gems "foo-1.0", "rack-1.0.0" - should_not_have_gems "rack-0.9.1" + should_have_gems "foo-1.0", "myrack-1.0.0" + should_not_have_gems "myrack-0.9.1" - expect(vendored_gems("bin/rackup")).to exist + expect(vendored_gems("bin/myrackup")).to exist end it "removes new version of gem if unused" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" gem "foo" G - bundle "install --path vendor/bundle --no-clean" + bundle_config "path vendor/bundle" + bundle_config "clean false" + bundle "install" gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack", "0.9.1" + gem "myrack", "0.9.1" gem "foo" G - bundle "install" + bundle "update myrack" bundle :clean - expect(out).to include("Removing rack (1.0.0)") + expect(out).to include("Removing myrack (1.0.0)") - should_have_gems "foo-1.0", "rack-0.9.1" - should_not_have_gems "rack-1.0.0" + should_have_gems "foo-1.0", "myrack-0.9.1" + should_not_have_gems "myrack-1.0.0" - expect(vendored_gems("bin/rackup")).to exist + expect(vendored_gems("bin/myrackup")).to exist end it "removes gems in bundle without groups" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "foo" group :test_group do - gem "rack", "1.0.0" + gem "myrack", "1.0.0" end G - bundle "install --path vendor/bundle" - bundle "install --without test_group" + bundle_config "path vendor/bundle" + bundle "install" + bundle_config "without test_group" + bundle "install" bundle :clean - expect(out).to include("Removing rack (1.0.0)") + expect(out).to include("Removing myrack (1.0.0)") should_have_gems "foo-1.0" - should_not_have_gems "rack-1.0.0" + should_not_have_gems "myrack-1.0.0" - expect(vendored_gems("bin/rackup")).to_not exist + expect(vendored_gems("bin/myrackup")).to_not exist end it "does not remove cached git dir if it's being used" do @@ -130,42 +137,45 @@ RSpec.describe "bundle clean" do git_path = lib_path("foo-1.0") gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" git "#{git_path}", :ref => "#{revision}" do gem "foo" end G - bundle "install --path vendor/bundle" + bundle_config "path vendor/bundle" + bundle "install" bundle :clean - digest = Digest::SHA1.hexdigest(git_path.to_s) - expect(vendored_gems("cache/bundler/git/foo-1.0-#{digest}")).to exist + 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") + build_git "foo", path: lib_path("foo") git_path = lib_path("foo") revision = revision_for(git_path) gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" git "#{git_path}", :ref => "#{revision}" do gem "foo" end G - bundle "install --path vendor/bundle" + bundle_config "path vendor/bundle" + bundle "install" gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" G bundle "install" @@ -173,60 +183,90 @@ RSpec.describe "bundle clean" do expect(out).to include("Removing foo (#{revision[0..11]})") - expect(vendored_gems("gems/rack-1.0.0")).to exist + expect(vendored_gems("gems/myrack-1.0.0")).to exist expect(vendored_gems("bundler/gems/foo-#{revision[0..11]}")).not_to exist - digest = Digest::SHA1.hexdigest(git_path.to_s) + digest = Digest(:SHA1).hexdigest(git_path.to_s) expect(vendored_gems("cache/bundler/git/foo-#{digest}")).not_to exist - expect(vendored_gems("specifications/rack-1.0.0.gemspec")).to exist + expect(vendored_gems("specifications/myrack-1.0.0.gemspec")).to exist - expect(vendored_gems("bin/rackup")).to exist + expect(vendored_gems("bin/myrackup")).to exist end - it "removes old git gems" do - build_git "foo-bar", :path => lib_path("foo-bar") + 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 "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" git "#{lib_path("foo-bar")}" do gem "foo-bar" end G - bundle "install --path vendor/bundle" + bundle_config "path vendor/bundle" + bundle "install" - update_git "foo", :path => lib_path("foo-bar") + update_git "foo-bar", path: lib_path("foo-bar") revision2 = revision_for(lib_path("foo-bar")) - bundle "update" + bundle "update", all: true bundle :clean expect(out).to include("Removing foo-bar (#{revision[0..11]})") - expect(vendored_gems("gems/rack-1.0.0")).to exist + expect(vendored_gems("gems/myrack-1.0.0")).to exist expect(vendored_gems("bundler/gems/foo-bar-#{revision[0..11]}")).not_to exist expect(vendored_gems("bundler/gems/foo-bar-#{revision2[0..11]}")).to exist - expect(vendored_gems("specifications/rack-1.0.0.gemspec")).to exist + expect(vendored_gems("specifications/myrack-1.0.0.gemspec")).to exist - expect(vendored_gems("bin/rackup")).to exist + expect(vendored_gems("bin/myrackup")).to exist end it "does not remove nested gems in a git repo" do - build_lib "activesupport", "3.0", :path => lib_path("rails/activesupport") - build_git "rails", "3.0", :path => lib_path("rails") do |s| + build_lib "activesupport", "3.0", path: lib_path("rails/activesupport") + build_git "rails", "3.0", path: lib_path("rails") do |s| s.add_dependency "activesupport", "= 3.0" end revision = revision_for(lib_path("rails")) gemfile <<-G + source "https://gem.repo1" gem "activesupport", :git => "#{lib_path("rails")}", :ref => '#{revision}' G - bundle "install --path vendor/bundle" + bundle_config "path vendor/bundle" + bundle "install" bundle :clean expect(out).to include("") @@ -234,193 +274,220 @@ RSpec.describe "bundle clean" do end it "does not remove git sources that are in without groups" do - build_git "foo", :path => lib_path("foo") + build_git "foo", path: lib_path("foo") git_path = lib_path("foo") revision = revision_for(git_path) gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" group :test do git "#{git_path}", :ref => "#{revision}" do gem "foo" end end G - bundle "install --path vendor/bundle --without test" + 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) + digest = Digest(:SHA1).hexdigest(git_path.to_s) expect(vendored_gems("cache/bundler/git/foo-#{digest}")).to_not exist end it "does not blow up when using without groups" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack" + gem "myrack" group :development do gem "foo" end G - bundle "install --path vendor/bundle --without development" + bundle_config "path vendor/bundle" + bundle_config "without development" + bundle "install" bundle :clean - expect(exitstatus).to eq(0) if exitstatus end it "displays an error when used without --path" do + bundle_config "path.system true" install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" G - bundle :clean + bundle :clean, raise_on_error: false - expect(exitstatus).to eq(1) if exitstatus - expect(out).to include("--force") + expect(exitstatus).to eq(15) + expect(err).to include("--force") end # handling bundle clean upgrade path from the pre's it "removes .gem/.gemspec file even if there's no corresponding gem dir" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "thin" gem "foo" G - bundle "install --path vendor/bundle" + bundle_config "path vendor/bundle" + bundle "install" gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "foo" G bundle "install" - FileUtils.rm(vendored_gems("bin/rackup")) - FileUtils.rm_rf(vendored_gems("gems/thin-1.0")) - FileUtils.rm_rf(vendored_gems("gems/rack-1.0.0")) + FileUtils.rm(vendored_gems("bin/myrackup")) + FileUtils.rm_r(vendored_gems("gems/thin-1.0")) + FileUtils.rm_r(vendored_gems("gems/myrack-1.0.0")) bundle :clean - should_not_have_gems "thin-1.0", "rack-1.0" + should_not_have_gems "thin-1.0", "myrack-1.0" should_have_gems "foo-1.0" - expect(vendored_gems("bin/rackup")).not_to exist + expect(vendored_gems("bin/myrackup")).not_to exist end it "does not call clean automatically when using system gems" do - gemfile <<-G - source "file://#{gem_repo1}" + bundle_config "path.system true" + + install_gemfile <<-G + source "https://gem.repo1" gem "thin" - gem "rack" + gem "myrack" G - bundle :install - gemfile <<-G - source "file://#{gem_repo1}" + install_gemfile <<-G + source "https://gem.repo1" - gem "rack" + gem "myrack" G - bundle :install - sys_exec "gem list" - expect(out).to include("rack (1.0.0)") - expect(out).to include("thin (1.0)") + installed_gems_list + expect(out).to include("myrack (1.0.0)").and include("thin (1.0)") end - it "--clean should override the bundle setting on install" do - gemfile <<-G - source "file://#{gem_repo1}" - - gem "thin" - gem "rack" - G - bundle "install --path vendor/bundle --clean" + it "does not clean on bundle update when path has not been set" do + build_repo2 - gemfile <<-G - source "file://#{gem_repo1}" + install_gemfile <<-G + source "https://gem.repo2" - gem "rack" + gem "foo" G - bundle "install" - should_have_gems "rack-1.0.0" - should_not_have_gems "thin-1.0" + 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 "--clean should override the bundle setting on update" do + it "will automatically clean on bundle update when path has not been set", bundler: "5" do build_repo2 - gemfile <<-G - source "file://#{gem_repo2}" + install_gemfile <<-G + source "https://gem.repo2" gem "foo" G - bundle "install --path vendor/bundle --clean" update_repo2 do build_gem "foo", "1.0.1" end - bundle "update" + bundle "update", all: true - should_have_gems "foo-1.0.1" - should_not_have_gems "foo-1.0" + 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 on --path" do + it "does not clean automatically when path configured" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "thin" - gem "rack" + gem "myrack" G - bundle "install --path vendor/bundle" + bundle_config "path vendor/bundle" + bundle "install" gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack" + gem "myrack" G bundle "install" - should_have_gems "rack-1.0.0", "thin-1.0" + should_have_gems "myrack-1.0.0", "thin-1.0" end - it "does not clean on bundle update with --path" do + it "does not clean on bundle update when path configured" do build_repo2 gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "foo" G - bundle "install --path vendor/bundle" + bundle_config "path vendor/bundle" + bundle "install" update_repo2 do build_gem "foo", "1.0.1" end - bundle :update + bundle :update, all: true should_have_gems "foo-1.0", "foo-1.0.1" end - it "does not clean on bundle update when using --system" do + it "does not clean on bundle update when installing to system gems" do + bundle_config "path.system true" + build_repo2 gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "foo" G @@ -429,66 +496,69 @@ RSpec.describe "bundle clean" do update_repo2 do build_gem "foo", "1.0.1" end - bundle :update + bundle :update, all: true - sys_exec "gem list" + installed_gems_list expect(out).to include("foo (1.0.1, 1.0)") end it "cleans system gems when --force is used" do + bundle_config "path.system true" + gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "foo" - gem "rack" + gem "myrack" G bundle :install gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack" + gem "myrack" G bundle :install bundle "clean --force" expect(out).to include("Removing foo (1.0)") - sys_exec "gem list" + installed_gems_list expect(out).not_to include("foo (1.0)") - expect(out).to include("rack (1.0.0)") + expect(out).to include("myrack (1.0.0)") end - describe "when missing permissions" do + describe "when missing permissions", :permissions do + before { ENV["BUNDLE_PATH__SYSTEM"] = "true" } + let(:system_cache_path) { system_gem_path("cache") } after do - FileUtils.chmod(0o755, default_bundle_path("cache")) + FileUtils.chmod(0o755, system_cache_path) end it "returns a helpful error message" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "foo" - gem "rack" + gem "myrack" G bundle :install gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack" + gem "myrack" G bundle :install - system_cache_path = default_bundle_path("cache") FileUtils.chmod(0o500, system_cache_path) - bundle :clean, :force => true + bundle :clean, force: true, raise_on_error: false - expect(out).to include(system_gem_path.to_s) - expect(out).to include("grant write permissions") + expect(err).to include(system_gem_path.to_s) + expect(err).to include("grant write permissions") - sys_exec "gem list" + installed_gems_list expect(out).to include("foo (1.0)") - expect(out).to include("rack (1.0.0)") + expect(out).to include("myrack (1.0.0)") end end @@ -497,23 +567,23 @@ RSpec.describe "bundle clean" do revision = revision_for(lib_path("foo-1.0")) gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G - bundle "install --path vendor/bundle" + bundle_config "path vendor/bundle" + bundle "install" # mimic 7 length git revisions in Gemfile.lock - gemfile_lock = File.read(bundled_app("Gemfile.lock")).split("\n") + gemfile_lock = File.read(bundled_app_lock).split("\n") gemfile_lock.each_with_index do |line, index| gemfile_lock[index] = line[0..(11 + 7)] if line.include?(" revision:") end - File.open(bundled_app("Gemfile.lock"), "w") do |file| - file.print gemfile_lock.join("\n") - end + lockfile(bundled_app_lock, gemfile_lock.join("\n")) - bundle "install --path vendor/bundle" + bundle_config "path vendor/bundle" + bundle "install" bundle :clean @@ -523,8 +593,9 @@ RSpec.describe "bundle clean" do end it "when using --force on system gems, it doesn't remove binaries" do - build_repo2 - update_repo2 do + bundle_config "path.system true" + + build_repo2 do build_gem "bindir" do |s| s.bindir = "exe" s.executables = "foo" @@ -532,7 +603,7 @@ RSpec.describe "bundle clean" do end gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "bindir" G @@ -542,11 +613,28 @@ RSpec.describe "bundle clean" do sys_exec "foo" - expect(exitstatus).to eq(0) if exitstatus expect(out).to eq("1.0") end - it "doesn't blow up on path gems without a .gempsec" do + 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") @@ -555,30 +643,31 @@ RSpec.describe "bundle clean" do end gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "foo" gem "bar", "1.0", :path => "#{relative_path}" G - bundle "install --path vendor/bundle" + bundle_config "path vendor/bundle" + bundle "install" bundle :clean - - expect(exitstatus).to eq(0) if exitstatus end it "doesn't remove gems in dry-run mode with path set" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "thin" gem "foo" G - bundle "install --path vendor/bundle --no-clean" + bundle_config "path vendor/bundle" + bundle_config "clean false" + bundle "install" gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "thin" G @@ -590,54 +679,56 @@ RSpec.describe "bundle clean" do expect(out).not_to include("Removing foo (1.0)") expect(out).to include("Would have removed foo (1.0)") - should_have_gems "thin-1.0", "rack-1.0.0", "foo-1.0" + should_have_gems "thin-1.0", "myrack-1.0.0", "foo-1.0" - expect(vendored_gems("bin/rackup")).to exist + expect(vendored_gems("bin/myrackup")).to exist end it "doesn't remove gems in dry-run mode with no path set" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "thin" gem "foo" G - bundle "install --path vendor/bundle --no-clean" + bundle_config "path vendor/bundle" + bundle_config "clean false" + bundle "install" gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "thin" G bundle :install - bundle "configuration --delete path" - bundle "clean --dry-run" expect(out).not_to include("Removing foo (1.0)") expect(out).to include("Would have removed foo (1.0)") - should_have_gems "thin-1.0", "rack-1.0.0", "foo-1.0" + should_have_gems "thin-1.0", "myrack-1.0.0", "foo-1.0" - expect(vendored_gems("bin/rackup")).to exist + expect(vendored_gems("bin/myrackup")).to exist end it "doesn't store dry run as a config setting" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "thin" gem "foo" G - bundle "install --path vendor/bundle --no-clean" - bundle "config dry_run false" + bundle_config "path vendor/bundle" + bundle_config "clean false" + bundle "install" + bundle_config "dry_run false" gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "thin" G @@ -649,55 +740,197 @@ RSpec.describe "bundle clean" do expect(out).to include("Removing foo (1.0)") expect(out).not_to include("Would have removed foo (1.0)") - should_have_gems "thin-1.0", "rack-1.0.0" + should_have_gems "thin-1.0", "myrack-1.0.0" should_not_have_gems "foo-1.0" - expect(vendored_gems("bin/rackup")).to exist + expect(vendored_gems("bin/myrackup")).to exist end it "performs an automatic bundle install" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "thin" gem "foo" G - bundle "install --path vendor/bundle --no-clean" + bundle_config "path vendor/bundle" + bundle_config "clean false" + bundle "install" gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "thin" gem "weakling" G - bundle "config auto_install 1" + bundle_config "auto_install 1" bundle :clean expect(out).to include("Installing weakling 0.0.3") - should_have_gems "thin-1.0", "rack-1.0.0", "weakling-0.0.3" + should_have_gems "thin-1.0", "myrack-1.0.0", "weakling-0.0.3" should_not_have_gems "foo-1.0" end - it "doesn't remove extensions artifacts from bundled git gems after clean", :ruby_repo, :rubygems => "2.2" do + it "doesn't remove extensions artifacts from bundled git gems after clean" do build_git "very_simple_git_binary", &:add_c_extension revision = revision_for(lib_path("very_simple_git_binary-1.0")) gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}" G - bundle! "install --path vendor/bundle" + bundle_config "path vendor/bundle" + bundle "install" expect(vendored_gems("bundler/gems/extensions")).to exist expect(vendored_gems("bundler/gems/very_simple_git_binary-1.0-#{revision[0..11]}")).to exist - bundle! :clean - expect(out).to eq("") + bundle :clean + expect(out).to be_empty expect(vendored_gems("bundler/gems/extensions")).to exist expect(vendored_gems("bundler/gems/very_simple_git_binary-1.0-#{revision[0..11]}")).to exist end + + it "removes extension directories" 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 index a3ca696ec1..e8ab32ca5d 100644 --- a/spec/bundler/commands/config_spec.rb +++ b/spec/bundler/commands/config_spec.rb @@ -1,19 +1,11 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe ".bundle/config" do - before :each do - gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", "1.0.0" - G - end - describe "config" do - before { bundle "config foo bar" } + before { bundle "config set foo bar" } it "prints a detailed report of local and user configuration" do - bundle "config" + 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") @@ -22,69 +14,136 @@ RSpec.describe ".bundle/config" do context "given --parseable flag" do it "prints a minimal report of local and user configuration" do - bundle "config --parseable" + 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 --local foo bar2" - bundle "config --parseable" + 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 --parseable", :env => { "BUNDLE_FOO" => "bar3" } + bundle "config list --parseable", env: { "BUNDLE_FOO" => "bar3" } expect(out).to include("foo=bar3") end end end end - describe "BUNDLE_APP_CONFIG" do + 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 "install --path vendor/bundle" + bundle "config set --local path vendor/bundle" + bundle "install" expect(bundled_app(".bundle")).not_to exist expect(tmp("foo/bar/config")).to exist - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "can provide a relative path with the environment variable" do FileUtils.mkdir_p bundled_app("omg") - Dir.chdir bundled_app("omg") ENV["BUNDLE_APP_CONFIG"] = "../foo" - bundle "install --path vendor/bundle" + bundle "config set --local path vendor/bundle" + bundle "install", dir: bundled_app("omg") expect(bundled_app(".bundle")).not_to exist expect(bundled_app("../foo/config")).to exist - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0", dir: bundled_app("omg") + end + end + + describe "location without a gemfile" do + it "is global by default" do + bundle "config set foo bar" + expect(bundled_app(".bundle/config")).not_to exist + expect(home(".bundle/config")).to exist + end + + it "does not list global settings as local" do + bundle "config set --global foo bar" + bundle "config list", dir: home + + expect(out).to include("for the current user") + expect(out).not_to include("for your local app") + end + + it "works with an absolute path" do + ENV["BUNDLE_APP_CONFIG"] = tmp("foo/bar").to_s + bundle "config set --local path vendor/bundle" + + expect(bundled_app(".bundle")).not_to exist + expect(tmp("foo/bar/config")).to exist + end + end + + describe "config location" do + let(:bundle_user_config) { File.join(Dir.home, ".config/bundler") } + + before do + Dir.mkdir File.dirname(bundle_user_config) + end + + it "can be configured through BUNDLE_USER_CONFIG" do + bundle "config set path vendor", env: { "BUNDLE_USER_CONFIG" => bundle_user_config } + bundle "config get path", env: { "BUNDLE_USER_CONFIG" => bundle_user_config } + expect(out).to include("Set for the current user (#{bundle_user_config}): \"vendor\"") + end + + context "when not explicitly configured, but BUNDLE_USER_HOME set" do + let(:bundle_user_home) { bundled_app(".bundle").to_s } + + it "uses the right location" do + bundle "config set path vendor", env: { "BUNDLE_USER_HOME" => bundle_user_home } + bundle "config get path", env: { "BUNDLE_USER_HOME" => bundle_user_home } + expect(out).to include("Set for the current user (#{bundle_user_home}/config): \"vendor\"") + end end end describe "global" do - before(:each) { bundle :install } + before(:each) do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "1.0.0" + G + end it "is the default" do - bundle "config foo global" + 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 --global foo global" - run! "puts Bundler.settings[:foo]" + bundle "config set --global foo global" + run "puts Bundler.settings[:foo]" expect(out).to eq("global") end it "has lower precedence than local" do - bundle "config --local foo local" + bundle "config set --local foo local" - bundle "config --global foo global" + bundle "config set --global foo global" expect(out).to match(/Your application has set foo to "local"/) run "puts Bundler.settings[:foo]" @@ -92,30 +151,28 @@ RSpec.describe ".bundle/config" do end it "has lower precedence than env" do - begin - ENV["BUNDLE_FOO"] = "env" + ENV["BUNDLE_FOO"] = "env" - bundle "config --global foo global" - expect(out).to match(/You have a bundler environment variable for foo set to "env"/) + bundle "config set --global foo global" + expect(out).to match(/You have a bundler environment variable for foo set to "env"/) - run "puts Bundler.settings[:foo]" - expect(out).to eq("env") - ensure - ENV.delete("BUNDLE_FOO") - end + run "puts Bundler.settings[:foo]" + expect(out).to eq("env") + ensure + ENV.delete("BUNDLE_FOO") end it "can be deleted" do - bundle "config --global foo global" - bundle "config --delete foo" + 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 --global foo previous" - bundle "config --global foo global" + 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]" @@ -123,8 +180,8 @@ RSpec.describe ".bundle/config" do end it "does not warn when using the same value twice" do - bundle "config --global foo value" - bundle "config --global foo value" + 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]" @@ -132,22 +189,22 @@ RSpec.describe ".bundle/config" do end it "expands the path at time of setting" do - bundle "config --global local.foo .." + bundle "config set --global local.foo .." run "puts Bundler.settings['local.foo']" - expect(out).to eq(File.expand_path(Dir.pwd + "/..")) + expect(out).to eq(File.expand_path(bundled_app.to_s + "/..")) end it "saves with parseable option" do - bundle "config --global --parseable foo value" + 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 --global foo value" } + before { bundle "config set --global foo value" } it "prints the current value in a parseable format" do - bundle "config --global --parseable foo value2" + bundle "config set --global --parseable foo value2" expect(out).to eq "foo=value2" run "puts Bundler.settings['foo']" expect(out).to eq("value2") @@ -156,37 +213,40 @@ RSpec.describe ".bundle/config" do end describe "local" do - before(:each) { bundle :install } + 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 --local foo local" + bundle "config set --local foo local" run "puts Bundler.settings[:foo]" expect(out).to eq("local") end it "has higher precedence than env" do - begin - ENV["BUNDLE_FOO"] = "env" - bundle "config --local foo local" - - run "puts Bundler.settings[:foo]" - expect(out).to eq("local") - ensure - ENV.delete("BUNDLE_FOO") - end + 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 --local foo local" - bundle "config --delete foo" + 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 --local foo previous" - bundle "config --local foo local" + 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]" @@ -194,14 +254,14 @@ RSpec.describe ".bundle/config" do end it "expands the path at time of setting" do - bundle "config --local local.foo .." + bundle "config set --local local.foo .." run "puts Bundler.settings['local.foo']" - expect(out).to eq(File.expand_path(Dir.pwd + "/..")) + expect(out).to eq(File.expand_path(bundled_app.to_s + "/..")) end it "can be deleted with parseable option" do - bundle "config --local foo value" - bundle "config --delete --parseable foo" + 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") @@ -209,7 +269,12 @@ RSpec.describe ".bundle/config" do end describe "env" do - before(:each) { bundle :install } + 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" @@ -248,39 +313,45 @@ RSpec.describe ".bundle/config" do describe "parseable option" do it "prints an empty string" do - bundle "config foo --parseable" + bundle "config get foo --parseable", raise_on_error: false expect(out).to eq "" + expect(last_command).to be_failure end it "only prints the value of the config" do - bundle "config foo local" - bundle "config foo --parseable" + 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 --global bar value" - bundle "config bar --parseable" + bundle "config set --global bar value" + bundle "config get bar --parseable" expect(out).to eq "bar=value" end - it "preferes local config over global" do - bundle "config --local bar value2" - bundle "config --global bar value" - bundle "config bar --parseable" + 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) { bundle :install } + 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 --local mirror.http://gems.example.org http://gem-mirror.example.org" + 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}" @@ -288,36 +359,42 @@ end E expect(out).to eq("http://gems.example.org/ => http://gem-mirror.example.org/") end + + it "allows configuring fallback timeout for each mirror, and does not duplicate configs", rubygems: ">= 3.5.12" do + bundle "config set --global mirror.https://rubygems.org.fallback_timeout 1" + bundle "config set --global mirror.https://rubygems.org.fallback_timeout 2" + expect(File.read(home(".bundle/config"))).to include("BUNDLE_MIRROR").once + end end describe "quoting" do - before(:each) { gemfile "# no gems" } + before(:each) { gemfile "source 'https://gem.repo1'" } let(:long_string) do "--with-xml2-include=/usr/pkg/include/libxml2 --with-xml2-lib=/usr/pkg/lib " \ "--with-xslt-dir=/usr/pkg" end it "saves quotes" do - bundle "config foo something\\'" + bundle "config set foo something\\'" run "puts Bundler.settings[:foo]" expect(out).to eq("something'") end - it "doesn't return quotes around values", :ruby => "1.9" do - bundle "config foo '1'" - run "puts Bundler.settings.send(:global_config_file).read" + 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", :if => (RUBY_VERSION >= "2.1") do + 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 bar baz" + bundle "config set bar baz" run "puts Bundler.settings.send(:local_config_file).read" # Starting in Ruby 2.1, YAML automatically adds double quotes @@ -326,12 +403,12 @@ E end it "doesn't duplicate quotes around long wrapped values" do - bundle "config foo #{long_string}" + bundle "config set foo #{long_string}" run "puts Bundler.settings[:foo]" expect(out).to eq(long_string) - bundle "config bar baz" + bundle "config set bar baz" run "puts Bundler.settings[:foo]" expect(out).to eq(long_string) @@ -339,7 +416,12 @@ E end describe "very long lines" do - before(:each) { bundle :install } + 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 " \ @@ -352,34 +434,213 @@ E end it "doesn't wrap values" do - bundle "config foo #{long_string}" + 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 foo #{long_string_without_special_characters}" + 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 - File.open(bundled_app("NotGemfile"), "w") do |f| - f.write <<-G - source "file://#{gem_repo1}" - gem 'rack' - G - end + gemfile bundled_app("NotGemfile"), <<-G + source "https://gem.repo1" + gem 'myrack' + G - bundle "config --local gemfile #{bundled_app("NotGemfile")}" - expect(File.exist?(".bundle/config")).to eq(true) + bundle "config set --local gemfile #{bundled_app("NotGemfile")}" + expect(File.exist?(bundled_app(".bundle/config"))).to eq(true) - bundle "config" + bundle "config list" expect(out).to include("NotGemfile") end end end + +RSpec.describe "setting lockfile via config" do + it "persists the lockfile location to .bundle/config" do + gemfile bundled_app("NotGemfile"), <<-G + source "https://gem.repo1" + gem 'myrack' + G + + bundle "config set --local gemfile #{bundled_app("NotGemfile")}" + bundle "config set --local lockfile #{bundled_app("ReallyNotGemfile.lock")}" + expect(File.exist?(bundled_app(".bundle/config"))).to eq(true) + + bundle "config list" + expect(out).to include("NotGemfile") + expect(out).to include("ReallyNotGemfile.lock") + end +end diff --git a/spec/bundler/commands/console_spec.rb b/spec/bundler/commands/console_spec.rb index de14b6db5f..a44f607546 100644 --- a/spec/bundler/commands/console_spec.rb +++ b/spec/bundler/commands/console_spec.rb @@ -1,107 +1,214 @@ # frozen_string_literal: true -require "spec_helper" -RSpec.describe "bundle console" do +RSpec.describe "bundle console", readline: true do before :each do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - gem "activesupport", :group => :test - gem "rack_middleware", :group => :development - G - end + build_repo2 do + # A minimal fake pry console + build_gem "pry" do |s| + s.write "lib/pry.rb", <<-RUBY + class Pry + class << self + def toplevel_binding + unless defined?(@toplevel_binding) && @toplevel_binding + TOPLEVEL_BINDING.eval %{ + def self.__pry__; binding; end + Pry.instance_variable_set(:@toplevel_binding, __pry__) + class << self; undef __pry__; end + } + end + @toplevel_binding.eval('private') + @toplevel_binding + end + + def __pry__ + while line = gets + begin + puts eval(line, toplevel_binding).inspect.sub(/^"(.*)"$/, '=> \\1') + rescue Exception => e + puts "\#{e.class}: \#{e.message}" + puts e.backtrace.first + end + end + end + alias start __pry__ + end + end + RUBY + end - it "starts IRB with the default group loaded" do - bundle "console" do |input, _, _| - input.puts("puts RACK") - input.puts("exit") + build_dummy_irb end - expect(out).to include("0.9.1") end - it "uses IRB as default console" do - bundle "console" do |input, _, _| - input.puts("__method__") - input.puts("exit") + context "when the library requires a non-existent file" do + before do + build_lib "loadfuuu", "1.0.0" do |s| + s.write "lib/loadfuuu.rb", "require_relative 'loadfuuu/bar'" + s.write "lib/loadfuuu/bar.rb", "require 'not-in-bundle'" + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "irb" + path "#{lib_path}" do + gem "loadfuuu", require: true + end + G end - expect(out).to include(":irb_binding") - end - it "starts another REPL if configured as such" do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "pry" - G - bundle "config console pry" + it "does not show the bug report template" do + bundle("console", raise_on_error: false) do |input, _, _| + input.puts("exit") + end - bundle "console" do |input, _, _| - input.puts("__method__") - input.puts("exit") + expect(err).not_to include("ERROR REPORT TEMPLATE") end - expect(out).to include(":__pry__") end - it "falls back to IRB if the other REPL isn't available" do - bundle "config console pry" - # make sure pry isn't there + context "when the library references a non-existent constant" do + before do + build_lib "loadfuuu", "1.0.0" do |s| + s.write "lib/loadfuuu.rb", "Some::NonExistent::Constant" + end - bundle "console" do |input, _, _| - input.puts("__method__") - input.puts("exit") + install_gemfile <<-G + source "https://gem.repo2" + gem "irb" + path "#{lib_path}" do + gem "loadfuuu", require: true + end + G end - expect(out).to include(":irb_binding") - end - it "doesn't load any other groups" do - bundle "console" do |input, _, _| - input.puts("puts ACTIVESUPPORT") - input.puts("exit") + it "does not show the bug report template" do + bundle("console", raise_on_error: false) do |input, _, _| + input.puts("exit") + end + + expect(err).not_to include("ERROR REPORT TEMPLATE") end - expect(out).to include("NameError") end - describe "when given a group" do - it "loads the given group" do - bundle "console test" do |input, _, _| - input.puts("puts ACTIVESUPPORT") + context "when the library does not have any errors" do + before do + install_gemfile <<-G + source "https://gem.repo2" + gem "irb" + gem "myrack" + gem "activesupport", :group => :test + gem "myrack_middleware", :group => :development + G + end + + it "starts IRB with the default group loaded" do + bundle "console" do |input, _, _| + input.puts("puts MYRACK") + input.puts("exit") + end + expect(out).to include("0.9.1") + end + + it "uses IRB as default console" do + skip "Does not work in a ruby-core context if irb is in the default $LOAD_PATH because it enables the real IRB, not our dummy one" if ruby_core? && Gem.ruby_version < Gem::Version.new("3.5.0.a") + + bundle "console" do |input, _, _| + input.puts("__method__") input.puts("exit") end - expect(out).to include("2.3.5") + expect(out).to include("__irb__") end - it "loads the default group" do - bundle "console test" do |input, _, _| - input.puts("puts RACK") + it "starts another REPL if configured as such" do + install_gemfile <<-G + source "https://gem.repo2" + gem "irb" + gem "pry" + G + bundle_config "console pry" + + bundle "console" do |input, _, _| + input.puts("__method__") input.puts("exit") end - expect(out).to include("0.9.1") + expect(out).to include(":__pry__") end - it "doesn't load other groups" do - bundle "console test" do |input, _, _| - input.puts("puts RACK_MIDDLEWARE") + it "falls back to IRB if the other REPL isn't available" do + skip "Does not work in a ruby-core context if irb is in the default $LOAD_PATH because it enables the real IRB, not our dummy one" if ruby_core? && Gem.ruby_version < Gem::Version.new("3.5.0.a") + + bundle_config "console pry" + # make sure pry isn't there + + bundle "console" do |input, _, _| + input.puts("__method__") + input.puts("exit") + end + expect(out).to include("__irb__") + end + + it "does not try IRB twice if no console is configured and IRB is not available" do + create_file("irb.rb", "raise LoadError, 'irb is not available'") + + bundle("console", env: { "RUBYOPT" => "-I#{bundled_app} #{ENV["RUBYOPT"]}" }, raise_on_error: false) do |input, _, _| + input.puts("puts ACTIVESUPPORT") + input.puts("exit") + end + expect(err).not_to include("falling back to irb") + expect(err).to include("irb is not available") + end + + it "doesn't load any other groups" do + bundle "console" do |input, _, _| + input.puts("puts ACTIVESUPPORT") input.puts("exit") end expect(out).to include("NameError") end - end - it "performs an automatic bundle install" do - gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - gem "activesupport", :group => :test - gem "rack_middleware", :group => :development - gem "foo" - G - - bundle "config auto_install 1" - bundle :console do |input, _, _| - input.puts("puts 'hello'") - input.puts("exit") + describe "when given a group" do + it "loads the given group" do + bundle "console test" do |input, _, _| + input.puts("puts ACTIVESUPPORT") + input.puts("exit") + end + expect(out).to include("2.3.5") + end + + it "loads the default group" do + bundle "console test" do |input, _, _| + input.puts("puts MYRACK") + input.puts("exit") + end + expect(out).to include("0.9.1") + end + + it "doesn't load other groups" do + bundle "console test" do |input, _, _| + input.puts("puts MYRACK_MIDDLEWARE") + input.puts("exit") + end + expect(out).to include("NameError") + end + end + + it "performs an automatic bundle install" do + gemfile <<-G + source "https://gem.repo2" + gem "irb" + gem "myrack" + gem "activesupport", :group => :test + gem "myrack_middleware", :group => :development + gem "foo" + G + + bundle_config "auto_install 1" + bundle :console do |input, _, _| + input.puts("puts 'hello'") + input.puts("exit") + end + expect(out).to include("Installing foo 1.0") + expect(out).to include("hello") + expect(the_bundle).to include_gems "foo 1.0" end - expect(out).to include("Installing foo 1.0") - expect(out).to include("hello") - expect(the_bundle).to include_gems "foo 1.0" end end diff --git a/spec/bundler/commands/doctor_spec.rb b/spec/bundler/commands/doctor_spec.rb index 7c6e48ce19..d350b4b3d1 100644 --- a/spec/bundler/commands/doctor_spec.rb +++ b/spec/bundler/commands/doctor_spec.rb @@ -1,14 +1,21 @@ # frozen_string_literal: true -require "spec_helper" + +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].each do |method| + [:error, :warn, :info].each do |method| allow(Bundler.ui).to receive(method).and_wrap_original do |m, message| m.call message @stdout.puts message @@ -16,49 +23,163 @@ RSpec.describe "bundle doctor" do end end - it "exits with no message if the installed gem has no C extensions" do - gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - G + it "succeeds on a sane installation" do + bundle :doctor + end - bundle :install - Bundler::CLI::Doctor.new({}).run - expect(@stdout.string).to be_empty + 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 - it "exits with no message if the installed gem's C extension dylib breakage is fine" do - gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - G + 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 - bundle :install - doctor = Bundler::CLI::Doctor.new({}) - expect(doctor).to receive(:bundles_for_gem).exactly(2).times.and_return ["/path/to/rack/rack.bundle"] - expect(doctor).to receive(:dylibs).exactly(2).times.and_return ["/usr/lib/libSystem.dylib"] - allow(File).to receive(:exist?).and_call_original - allow(File).to receive(:exist?).with("/usr/lib/libSystem.dylib").and_return(true) - doctor.run - expect(@stdout.string).to be_empty + 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 - it "exits with a message if one of the linked libraries is missing" do - gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - G + 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 - bundle :install - doctor = Bundler::CLI::Doctor.new({}) - expect(doctor).to receive(:bundles_for_gem).exactly(2).times.and_return ["/path/to/rack/rack.bundle"] - expect(doctor).to receive(:dylibs).exactly(2).times.and_return ["/usr/local/opt/icu4c/lib/libicui18n.57.1.dylib"] - allow(File).to receive(:exist?).and_call_original - allow(File).to receive(:exist?).with("/usr/local/opt/icu4c/lib/libicui18n.57.1.dylib").and_return(false) - expect { doctor.run }.to raise_error Bundler::ProductionError, strip_whitespace(<<-E).strip - The following gems are missing OS dependencies: - * bundler: /usr/local/opt/icu4c/lib/libicui18n.57.1.dylib - * rack: /usr/local/opt/icu4c/lib/libicui18n.57.1.dylib - E + context "when home contains filenames with special characters" do + it "escape filename before command execute" do + doctor = Bundler::CLI::Doctor::Diagnose.new({}) + expect(doctor).to receive(:`).with("/usr/bin/otool -L \\$\\(date\\)\\ \\\"\\'\\\\.bundle").and_return("dummy string") + doctor.dylibs_darwin('$(date) "\'\.bundle') + expect(doctor).to receive(:`).with("/usr/bin/ldd \\$\\(date\\)\\ \\\"\\'\\\\.bundle").and_return("dummy string") + doctor.dylibs_ldd('$(date) "\'\.bundle') + end end end diff --git a/spec/bundler/commands/exec_spec.rb b/spec/bundler/commands/exec_spec.rb index 7736adefe1..aa35685be8 100644 --- a/spec/bundler/commands/exec_spec.rb +++ b/spec/bundler/commands/exec_spec.rb @@ -1,65 +1,127 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle exec" do - let(:system_gems_to_install) { %w(rack-1.0.0 rack-0.9.1) } - before :each do - system_gems(system_gems_to_install) + 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 - gem "rack", "0.9.1" + source "https://gem.repo1" + gem "myrack", "0.9.1" G - bundle "exec rackup" + 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 - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "exec rackup" + bundle "exec myrackup" expect(out).to eq("1.0.0") end - it "works when running from a random directory", :ruby_repo do + it "works when running from a random directory" do install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "exec 'cd #{tmp("gems")} && rackup'" + bundle "exec 'cd #{tmp("gems")} && myrackup'" - expect(out).to include("1.0.0") + expect(out).to eq("1.0.0") end it "works when exec'ing something else" do - install_gemfile 'gem "rack"' + install_gemfile "source \"https://gem.repo1\"; gem \"myrack\"" bundle "exec echo exec" expect(out).to eq("exec") end it "works when exec'ing to ruby" do - install_gemfile 'gem "rack"' + install_gemfile "source \"https://gem.repo1\"; gem \"myrack\"" bundle "exec ruby -e 'puts %{hi}'" expect(out).to eq("hi") end + it "works when exec'ing to rubygems" do + install_gemfile "source \"https://gem.repo1\"; gem \"myrack\"" + bundle "exec #{gem_cmd} --version" + expect(out).to eq(Gem::VERSION) + end + + it "works when exec'ing to rubygems through sh -c" do + install_gemfile "source \"https://gem.repo1\"; gem \"myrack\"" + bundle "exec sh -c '#{gem_cmd} --version'" + expect(out).to eq(Gem::VERSION) + end + + it "works when exec'ing back to bundler to run a remote resolve" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + G + + bundle "exec bundle lock", env: { "BUNDLER_VERSION" => Bundler::VERSION } + + expect(out).to include("Writing lockfile") + end + + it "respects custom process title when loading through ruby" do + skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform? + + script_that_changes_its_own_title_and_checks_if_picked_up_by_ps_unix_utility = <<~'RUBY' + Process.setproctitle("1-2-3-4-5-6-7") + 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 'gem "rack"' + install_gemfile "source \"https://gem.repo1\"; gem \"myrack\"" bundle "exec --verbose echo foobar" expect(out).to eq("foobar") end it "passes --verbose to command if it is given after the command" do - install_gemfile 'gem "rack"' + install_gemfile "source \"https://gem.repo1\"; gem \"myrack\"" bundle "exec echo --verbose" expect(out).to eq("--verbose") end it "handles --keep-file-descriptors" do + skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform? + require "tempfile" command = Tempfile.new("io-test") @@ -70,38 +132,35 @@ RSpec.describe "bundle exec" do else require 'tempfile' io = Tempfile.new("io-test-fd") - args = %W[#{Gem.ruby} -I#{lib} #{bindir.join("bundle")} exec --keep-file-descriptors #{Gem.ruby} #{command.path} \#{io.to_i}] - args << { io.to_i => io } if RUBY_VERSION >= "2.0" + 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 "" - sys_exec("#{Gem.ruby} #{command.path}") + install_gemfile "source \"https://gem.repo1\"" + in_bundled_app "#{Gem.ruby} #{command.path}" - if Bundler.current_ruby.ruby_2? - expect(out).to eq("") - else - expect(out).to eq("Ruby version #{RUBY_VERSION} defaults to keeping non-standard file descriptors on Kernel#exec.") - end - - expect(err).to lack_errors + expect(out).to be_empty + expect(err).to be_empty end it "accepts --keep-file-descriptors" do - install_gemfile "" + install_gemfile "source \"https://gem.repo1\"" bundle "exec --keep-file-descriptors echo foobar" - expect(err).to lack_errors + expect(err).to be_empty end it "can run a command named --verbose" do - install_gemfile 'gem "rack"' - File.open("--verbose", "w") do |f| + skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform? + + install_gemfile "source \"https://gem.repo1\"; gem \"myrack\"" + File.open(bundled_app("--verbose"), "w") do |f| f.puts "#!/bin/sh" f.puts "echo foobar" end - File.chmod(0o744, "--verbose") + File.chmod(0o744, bundled_app("--verbose")) with_path_as(".") do bundle "exec -- --verbose" end @@ -110,135 +169,225 @@ RSpec.describe "bundle exec" do it "handles different versions in different bundles" do build_repo2 do - build_gem "rack_two", "1.0.0" do |s| - s.executables = "rackup" + build_gem "myrack_two", "1.0.0" do |s| + s.executables = "myrackup" end end install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", "0.9.1" + source "https://gem.repo1" + gem "myrack", "0.9.1" G - Dir.chdir bundled_app2 do - install_gemfile bundled_app2("Gemfile"), <<-G - source "file://#{gem_repo2}" - gem "rack_two", "1.0.0" - G - end + install_gemfile bundled_app2("Gemfile"), <<-G, dir: bundled_app2 + source "https://gem.repo2" + gem "myrack_two", "1.0.0" + G - bundle! "exec rackup" + bundle "exec myrackup" expect(out).to eq("0.9.1") - Dir.chdir bundled_app2 do - bundle! "exec rackup" - expect(out).to eq("1.0.0") + 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 - install_gemfile <<-G, :without => :middleware - source "file://#{gem_repo1}" - gem "rack" # rack 0.9.1 and 1.0 exist + bundle_config "without middleware" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" # myrack 0.9.1 and 1.0 exist group :middleware do - gem "rack_middleware" # rack_middleware depends on rack 0.9.1 + gem "myrack_middleware" # myrack_middleware depends on myrack 0.9.1 end G - bundle "exec rackup" + bundle "exec myrackup" expect(out).to eq("0.9.1") - expect(the_bundle).not_to include_gems "rack_middleware 1.0" + expect(the_bundle).not_to include_gems "myrack_middleware 1.0" end it "does not duplicate already exec'ed RUBYOPT" do + create_file("echoopt", "#!/usr/bin/env ruby\nprint ENV['RUBYOPT']") install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G - rubyopt = ENV["RUBYOPT"] - rubyopt = "-rbundler/setup #{rubyopt}" + bundler_setup_opt = "-r#{lib_dir}/bundler/setup" + + rubyopt = opt_add(bundler_setup_opt, ENV["RUBYOPT"]) - bundle "exec 'echo $RUBYOPT'" - expect(out).to have_rubyopts(rubyopt) + bundle "exec echoopt" + expect(out.split(" ").count(bundler_setup_opt)).to eq(1) - bundle "exec 'echo $RUBYOPT'", :env => { "RUBYOPT" => rubyopt } - expect(out).to have_rubyopts(rubyopt) + bundle "exec echoopt", env: { "RUBYOPT" => rubyopt } + expect(out.split(" ").count(bundler_setup_opt)).to eq(1) end - it "does not duplicate already exec'ed RUBYLIB", :ruby_repo do + it "does not duplicate already exec'ed RUBYLIB" do + create_file("echolib", "#!/usr/bin/env ruby\nprint ENV['RUBYLIB']") install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G rubylib = ENV["RUBYLIB"] - rubylib = "#{rubylib}".split(File::PATH_SEPARATOR).unshift "#{bundler_path}" + rubylib = rubylib.to_s.split(File::PATH_SEPARATOR).unshift lib_dir.to_s rubylib = rubylib.uniq.join(File::PATH_SEPARATOR) - bundle "exec 'echo $RUBYLIB'" + bundle "exec echolib" expect(out).to include(rubylib) - bundle "exec 'echo $RUBYLIB'", :env => { "RUBYLIB" => rubylib } + bundle "exec echolib", env: { "RUBYLIB" => rubylib } expect(out).to include(rubylib) end it "errors nicely when the argument doesn't exist" do install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "exec foobarbaz" - expect(exitstatus).to eq(127) if exitstatus - expect(out).to include("bundler: command not found: foobarbaz") - expect(out).to include("Install missing gem executables with `bundle install`") + bundle "exec foobarbaz", raise_on_error: false + expect(exitstatus).to eq(127) + expect(err).to include("bundler: command not found: foobarbaz") + expect(err).to include("Install missing gem executables with `bundle install`") end it "errors nicely when the argument is not executable" do install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "exec touch foo" - bundle "exec ./foo" - expect(exitstatus).to eq(126) if exitstatus - expect(out).to include("bundler: not executable: ./foo") + bundled_app("foo").write("") + bundle "exec ./foo", raise_on_error: false + expect(exitstatus).to eq(126) + expect(err).to include("bundler: not executable: ./foo") end it "errors nicely when no arguments are passed" do install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "exec" - expect(exitstatus).to eq(128) if exitstatus - expect(out).to include("bundler: exec needs a command to run") + 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", :ruby_repo, :rubygems => ">= 2.5.2" do - install_gemfile! <<-G - source "file://#{gem_repo1}" - gem "with_license" - G - [true, false].each do |l| - bundle! "config disable_exec_load #{l}" - bundle "exec rackup" - expect(err).to include "can't find executable rackup for gem rack. rack is not currently included in the bundle, perhaps you meant to add it to your Gemfile?" - end - end + 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) - # Different error message on old RG versions (before activate_bin_path) because they - # called `Kernel#gem` directly - it "raises a helpful error when exec'ing to something outside of the bundle", :rubygems => "< 2.5.2" do - install_gemfile! <<-G - source "file://#{gem_repo1}" - gem "with_license" + 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 rackup" - expect(err).to include "rack is not part of the bundle. Add it to your Gemfile." + bundle_config "disable_exec_load #{l}" + bundle "exec myrackup", raise_on_error: false + expect(err).to include "can't find executable myrackup for gem myrack. myrack is not currently included in the bundle, perhaps you meant to add it to your Gemfile?" end end @@ -250,14 +399,14 @@ RSpec.describe "bundle exec" do describe "when #{exec} is used" do before(:each) do install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G create_file("print_args", <<-'RUBY') #!/usr/bin/env ruby puts "args: #{ARGV.inspect}" RUBY - bundled_app("print_args").chmod(0o755) end it "shows executable's man page when --help is after the executable" do @@ -278,6 +427,7 @@ RSpec.describe "bundle exec" do it "shows executable's man page when the executable has a -" do FileUtils.mv(bundled_app("print_args"), bundled_app("docker-template")) + FileUtils.mv(bundled_app("print_args.bat"), bundled_app("docker-template.bat")) if Gem.win_platform? bundle "#{exec} docker-template build discourse --help" expect(out).to eq('args: ["build", "discourse", "--help"]') end @@ -292,39 +442,39 @@ RSpec.describe "bundle exec" do expect(out).to eq('args: ["-h"]') end - it "shows bundle-exec's man page when --help is between exec and the executable", :ruby_repo do + it "shows bundle-exec's man page when --help is between exec and the executable" do with_fake_man do - bundle "#{exec} --help cat" + bundle "#{exec} --help echo" end - expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + expect(out).to include(%(["#{man_dir}/bundle-exec.1"])) end - it "shows bundle-exec's man page when --help is before exec", :ruby_repo do + it "shows bundle-exec's man page when --help is before exec" do with_fake_man do bundle "--help #{exec}" end - expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + expect(out).to include(%(["#{man_dir}/bundle-exec.1"])) end - it "shows bundle-exec's man page when -h is before exec", :ruby_repo do + it "shows bundle-exec's man page when -h is before exec" do with_fake_man do bundle "-h #{exec}" end - expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + expect(out).to include(%(["#{man_dir}/bundle-exec.1"])) end - it "shows bundle-exec's man page when --help is after exec", :ruby_repo do + it "shows bundle-exec's man page when --help is after exec" do with_fake_man do bundle "#{exec} --help" end - expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + expect(out).to include(%(["#{man_dir}/bundle-exec.1"])) end - it "shows bundle-exec's man page when -h is after exec", :ruby_repo do + it "shows bundle-exec's man page when -h is after exec" do with_fake_man do bundle "#{exec} -h" end - expect(out).to include(%(["#{root}/man/bundle-exec.1"])) + expect(out).to include(%(["#{man_dir}/bundle-exec.1"])) end end end @@ -334,30 +484,31 @@ RSpec.describe "bundle exec" do describe "run from a random directory" do before(:each) do install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G end - it "works when unlocked", :ruby_repo do - bundle "exec 'cd #{tmp("gems")} && rackup'" + it "works when unlocked" do + bundle "exec 'cd #{tmp("gems")} && myrackup'" expect(out).to eq("1.0.0") - expect(out).to include("1.0.0") end - it "works when locked", :ruby_repo do + it "works when locked" do expect(the_bundle).to be_locked - bundle "exec 'cd #{tmp("gems")} && rackup'" - expect(out).to include("1.0.0") + bundle "exec 'cd #{tmp("gems")} && myrackup'" + expect(out).to eq("1.0.0") end end describe "from gems bundled via :path" do before(:each) do - build_lib "fizz", :path => home("fizz") do |s| + build_lib "fizz", path: home("fizz") do |s| s.executables = "fizz" end install_gemfile <<-G + source "https://gem.repo1" gem "fizz", :path => "#{File.expand_path(home("fizz"))}" G end @@ -382,6 +533,7 @@ RSpec.describe "bundle exec" do end install_gemfile <<-G + source "https://gem.repo1" gem "fizz_git", :git => "#{lib_path("fizz_git-1.0")}" G end @@ -400,11 +552,12 @@ RSpec.describe "bundle exec" do describe "from gems bundled via :git with no gemspec" do before(:each) do - build_git "fizz_no_gemspec", :gemspec => false do |s| + build_git "fizz_no_gemspec", gemspec: false do |s| s.executables = "fizz_no_gemspec" end install_gemfile <<-G + source "https://gem.repo1" gem "fizz_no_gemspec", "1.0", :git => "#{lib_path("fizz_no_gemspec-1.0")}" G end @@ -424,18 +577,61 @@ RSpec.describe "bundle exec" do it "performs an automatic bundle install" do gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", "0.9.1" + source "https://gem.repo1" + gem "myrack", "0.9.1" gem "foo" G - bundle "config auto_install 1" - bundle "exec rackup" + bundle_config "auto_install 1" + bundle "exec myrackup", artifice: "compact_index" expect(out).to include("Installing foo 1.0") end + it "performs an automatic bundle install with git gems" do + build_git "foo" do |s| + s.executables = "foo" + end + gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + bundle_config "auto_install 1" + bundle "exec foo", artifice: "compact_index" + expect(out).to include("Fetching myrack 0.9.1") + expect(out).to include("Fetching #{lib_path("foo-1.0")}") + expect(out.lines).to end_with("1.0") + end + + it "loads the correct optparse when `auto_install` is set, and optparse is a dependency" do + build_repo4 do + build_gem "fastlane", "2.192.0" do |s| + s.executables = "fastlane" + s.add_dependency "optparse", "~> 999.999.999" + end + + build_gem "optparse", "999.999.998" + build_gem "optparse", "999.999.999" + end + + system_gems "optparse-999.999.998", gem_repo: gem_repo4 + + bundle_config "auto_install 1" + bundle_config "path vendor/bundle" + + gemfile <<~G + source "https://gem.repo4" + gem "fastlane" + G + + bundle "exec fastlane", artifice: "compact_index" + expect(out).to include("Installing optparse 999.999.999") + expect(out).to include("2.192.0") + end + describe "with gems bundled via :path with invalid gemspecs" do - it "outputs the gemspec validation errors", :rubygems => ">= 1.7.2" do + it "outputs the gemspec validation errors" do build_lib "foo" gemspec = lib_path("foo-1.0").join("foo.gemspec").to_s @@ -446,24 +642,28 @@ RSpec.describe "bundle exec" do s.version = '1.0' s.summary = 'TODO: Add summary' s.authors = 'Me' + s.rubygems_version = nil end G end - install_gemfile <<-G + gemfile <<-G + source "https://gem.repo1" gem "foo", :path => "#{lib_path("foo-1.0")}" G - bundle "exec irb" + bundle "exec erb", raise_on_error: false expect(err).to match("The gemspec at #{lib_path("foo-1.0").join("foo.gemspec")} is not valid") - expect(err).to match('"TODO" is not a summary') + expect(err).to match(/missing value for attribute rubygems_version|rubygems_version must not be nil/) end end describe "with gems bundled for deployment" do it "works when calling bundler from another script" do gemfile <<-G + source "https://gem.repo1" + module Monkey def bin_path(a,b,c) raise Gem::GemNotFoundException.new('Fail') @@ -471,70 +671,146 @@ RSpec.describe "bundle exec" do end Bundler.rubygems.extend(Monkey) G - bundle "install --deployment" - bundle "exec ruby -e '`#{bindir.join("bundler")} -v`; puts $?.success?'" + bundle_config "path.system true" + bundle "install" + bundle "exec ruby -e '`bundle -v`; puts $?.success?'", env: { "BUNDLER_VERSION" => Bundler::VERSION } expect(out).to match("true") end end + describe "bundle exec gem uninstall" do + before do + build_repo4 do + build_gem "foo" + end + + install_gemfile <<-G + source "https://gem.repo4" + + gem "foo" + G + end + + it "works" do + bundle "exec #{gem_cmd} uninstall foo" + expect(out).to eq("Successfully uninstalled foo-1.0") + end + end + + describe "running gem commands in presence of rubygems plugins" do + before do + build_repo4 do + build_gem "foo" do |s| + s.write "lib/rubygems_plugin.rb", "puts 'FAIL'" + end + end + + system_gems "foo-1.0", path: default_bundle_path, gem_repo: gem_repo4 + + install_gemfile <<-G + source "https://gem.repo4" + G + end + + it "does not load plugins outside of the bundle" do + bundle "exec #{gem_cmd} -v" + expect(out).not_to include("FAIL") + end + end + context "`load`ing a ruby file instead of `exec`ing" do let(:path) { bundled_app("ruby_executable") } let(:shebang) { "#!/usr/bin/env ruby" } - let(:executable) { <<-RUBY.gsub(/^ */, "").strip } + let(:executable) { <<~RUBY.strip } #{shebang} - require "rack" + require "myrack" puts "EXEC: \#{caller.grep(/load/).empty? ? 'exec' : 'load'}" puts "ARGS: \#{$0} \#{ARGV.join(' ')}" - puts "RACK: \#{RACK}" - process_title = `ps -o args -p \#{Process.pid}`.split("\n", 2).last.strip + puts "MYRACK: \#{MYRACK}" + if Gem.win_platform? + process_title = "ruby" + else + process_title = `ps -o args -p \#{Process.pid}`.split("\n", 2).last.strip + end puts "PROCESS: \#{process_title}" RUBY before do - path.open("w") {|f| f << executable } - path.chmod(0o755) + create_file(bundled_app(path), executable) install_gemfile <<-G - gem "rack" + source "https://gem.repo1" + gem "myrack" G end let(:exec) { "EXEC: load" } let(:args) { "ARGS: #{path} arg1 arg2" } - let(:rack) { "RACK: 1.0.0" } + let(:myrack) { "MYRACK: 1.0.0" } let(:process) do - title = "PROCESS: #{path}" - title += " arg1 arg2" if RUBY_VERSION >= "2.1" - title + if Gem.win_platform? + "PROCESS: ruby" + else + "PROCESS: #{path} arg1 arg2" + end end let(:exit_code) { 0 } - let(:expected) { [exec, args, rack, process].join("\n") } + let(:expected) { [exec, args, myrack, process].join("\n") } let(:expected_err) { "" } - subject { bundle "exec #{path} arg1 arg2" } + subject { bundle "exec #{path} arg1 arg2", raise_on_error: false } - shared_examples_for "it runs" do - it "like a normally executed executable" do - subject - expect(exitstatus).to eq(exit_code) if exitstatus - expect(err).to eq(expected_err) - expect(out).to eq(expected) - end + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) end - it_behaves_like "it runs" - context "the executable exits explicitly" do let(:executable) { super() << "\nexit #{exit_code}\nputs 'POST_EXIT'\n" } context "with exit 0" do - it_behaves_like "it runs" + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end end context "with exit 99" do let(:exit_code) { 99 } - it_behaves_like "it runs" + + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end + end + end + + 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 @@ -542,66 +818,148 @@ RSpec.describe "bundle exec" do let(:executable) { "" } let(:exit_code) { 0 } - let(:expected) { "#{path} is empty" } - let(:expected_err) { "" } - if LessThanProc.with(RUBY_VERSION).call("1.9") - # Kernel#exec in ruby < 1.9 will raise Errno::ENOEXEC if the command content is empty, - # even if the command is set as an executable. - pending "Kernel#exec is different" - else - it_behaves_like "it runs" + 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) { super() << "\nbundler: failed to load command: #{path} (#{path})" } let(:expected_err) do - "RuntimeError: ERROR\n #{path}:10" + - (Bundler.current_ruby.ruby_18? ? "" : ":in `<top (required)>'") + /\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 - it_behaves_like "it runs" end - context "when the file uses the current ruby shebang", :ruby_repo do + context "when the file uses the current ruby shebang" do let(:shebang) { "#!#{Gem.ruby}" } - it_behaves_like "it runs" + + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end end context "when Bundler.setup fails" do before do + system_gems(%w[myrack-1.0.0 myrack-0.9.1], path: default_bundle_path) + gemfile <<-G - gem 'rack', '2' + source "https://gem.repo1" + gem 'myrack', '2' G ENV["BUNDLER_FORCE_TTY"] = "true" end let(:exit_code) { Bundler::GemNotFound.new.status_code } - let(:expected) { <<-EOS.strip } -\e[31mCould not find gem 'rack (= 2)' in any of the gem sources listed in your Gemfile.\e[0m -\e[33mRun `bundle install` to install missing gems.\e[0m + let(:expected) { "" } + let(:expected_err) { <<~EOS.strip } + Could not find gem 'myrack (= 2)' in locally installed gems. + + The source contains the following gems matching 'myrack': + * myrack-0.9.1 + * myrack-1.0.0 + Run `bundle install` to install missing gems. EOS - it_behaves_like "it runs" + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end + end + + context "when Bundler.setup fails 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_behaves_like "it runs" + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end end context "when disable_exec_load is set" do let(:exec) { "EXEC: exec" } - let(:process) { "PROCESS: ruby #{path} arg1 arg2" } + let(:process) do + if Gem.win_platform? + "PROCESS: ruby" + else + "PROCESS: ruby #{path} arg1 arg2" + end + end before do - bundle "config disable_exec_load true" + bundle_config "disable_exec_load true" end - it_behaves_like "it runs" + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end end context "regarding $0 and __FILE__" do @@ -611,126 +969,304 @@ RSpec.describe "bundle exec" do puts "__FILE__: #{__FILE__.inspect}" RUBY - let(:expected) { super() + <<-EOS.chomp } + context "when the path is absolute" do + let(:expected) { super() + <<~EOS.chomp } -$0: #{path.to_s.inspect} -__FILE__: #{path.to_s.inspect} - EOS + $0: #{path.to_s.inspect} + __FILE__: #{path.to_s.inspect} + EOS - it_behaves_like "it runs" + it "runs" do + subject + expect(exitstatus).to eq(exit_code) + expect(err).to eq(expected_err) + expect(out).to eq(expected) + end + end context "when the path is relative" do let(:path) { super().relative_path_from(bundled_app) } + let(:expected) { super() + <<~EOS.chomp } - if LessThanProc.with(RUBY_VERSION).call("1.9") - pending "relative paths have ./ __FILE__" - else - it_behaves_like "it runs" + $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(Pathname.pwd)}") } - - if LessThanProc.with(RUBY_VERSION).call("< 1.9") - pending "relative paths with ./ have absolute __FILE__" - else - it_behaves_like "it runs" + 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 "signals being trapped by bundler" do - let(:executable) { strip_whitespace <<-RUBY } - #{shebang} - begin - Thread.new do - puts 'Started' # For process sync - STDOUT.flush - sleep 1 # ignore quality_spec - raise "Didn't receive INT at all" - end.join - rescue Interrupt - puts "foo" - end - RUBY + 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 "popen3 doesn't provide a way to get pid " unless RUBY_VERSION >= "1.9.3" + 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 - bundle("exec #{path}") do |_, o, thr| - o.gets # Consumes 'Started' and ensures that thread has started - Process.kill("INT", thr.pid) + expect(out).to eq("foo") end + end - expect(out).to eq("foo") + 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", :ruby_repo do - let(:system_gems_to_install) { super() << :bundler } - - context "with shared gems disabled" do + context "nested bundle exec" do + context "when bundle in a local path" do before do + skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform? + gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle :install, :system_bundler => true, :path => "vendor/bundler" + bundle_config "path vendor/bundler" + bundle :install end - it "overrides disable_shared_gems so bundler can be found" do + it "correctly shells out" do file = bundled_app("file_that_bundle_execs.rb") - create_file(file, <<-RB) + create_file(file, <<-RUBY) #!#{Gem.ruby} puts `bundle exec echo foo` - RB + RUBY file.chmod(0o777) - bundle! "exec #{file}", :system_bundler => true + bundle "exec #{file}", env: { "PATH" => path } expect(out).to eq("foo") end end + context "when Kernel.require uses extra monkeypatches" do + before do + skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform? + + install_gemfile "source \"https://gem.repo1\"" + end + + it "does not undo the monkeypatches" do + karafka = bundled_app("bin/karafka") + create_file(karafka, <<~RUBY) + #!#{Gem.ruby} + + module Kernel + module_function + + alias_method :require_before_extra_monkeypatches, :require + + def require(path) + puts "requiring \#{path} used the monkeypatch" + + require_before_extra_monkeypatches(path) + end + end + + Bundler.setup(:default) + + require "foo" + RUBY + karafka.chmod(0o777) + + foreman = bundled_app("bin/foreman") + create_file(foreman, <<~RUBY) + #!#{Gem.ruby} + + puts `bundle exec bin/karafka` + RUBY + foreman.chmod(0o777) + + bundle "exec #{foreman}" + expect(out).to eq("requiring foo used the monkeypatch") + end + end + + context "when gemfile and path are configured", :ruby_repo do + before do + build_repo2 do + build_gem "rails", "6.1.0" do |s| + s.executables = "rails" + end + end + + bundle_config "path vendor/bundle" + bundle_config "gemfile gemfiles/myrack_6_1.gemfile" + + gemfile(bundled_app("gemfiles/myrack_6_1.gemfile"), <<~RUBY) + source "https://gem.repo2" + + gem "rails", "6.1.0" + RUBY + + # A Gemfile needs to be in the root to trick bundler's root resolution + gemfile "source 'https://gem.repo1'" + + bundle "install", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s } + end + + it "can still find gems after a nested subprocess" do + script = bundled_app("bin/myscript") + + create_file(script, <<~RUBY) + #!#{Gem.ruby} + + puts `bundle exec rails` + RUBY + + script.chmod(0o777) + + bundle "exec #{script}" + + expect(err).to be_empty + expect(out).to eq("6.1.0") + end + + it "can still find gems after a nested subprocess when using bundler (with a final r) executable" do + script = bundled_app("bin/myscript") + + create_file(script, <<~RUBY) + #!#{Gem.ruby} + + puts `bundler exec rails` + RUBY + + script.chmod(0o777) + + bundle "exec #{script}" + + expect(err).to be_empty + expect(out).to eq("6.1.0") + end + end + context "with a system gem that shadows a default gem" do let(:openssl_version) { "99.9.9" } - let(:expected) { ruby "gem 'openssl', '< 999999'; require 'openssl'; puts OpenSSL::VERSION", :artifice => nil } it "only leaves the default gem in the stdlib available" do - skip "openssl isn't a default gem" if expected.empty? + default_openssl_version = ruby "require 'openssl'; puts OpenSSL::VERSION" - install_gemfile! "" # must happen before installing the broken system gem + skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform? + + install_gemfile "source \"https://gem.repo1\"" # must happen before installing the broken system gem build_repo4 do build_gem "openssl", openssl_version do |s| - s.write("lib/openssl.rb", <<-RB) - raise "custom openssl should not be loaded, it's not in the gemfile!" - RB + s.write("lib/openssl.rb", <<-RUBY) + raise ArgumentError, "custom openssl should not be loaded" + RUBY end end - system_gems(:bundler, "openssl-#{openssl_version}", :gem_repo => gem_repo4) + system_gems("openssl-#{openssl_version}", gem_repo: gem_repo4) file = bundled_app("require_openssl.rb") - create_file(file, <<-RB) + create_file(file, <<-RUBY) #!/usr/bin/env ruby require "openssl" puts OpenSSL::VERSION warn Gem.loaded_specs.values.map(&:full_name) - RB + RUBY file.chmod(0o777) + env = { "PATH" => path } aggregate_failures do - expect(bundle!("exec #{file}", :system_bundler => true, :artifice => nil)).to eq(expected) - expect(bundle!("exec bundle exec #{file}", :system_bundler => true, :artifice => nil)).to eq(expected) - expect(bundle!("exec ruby #{file}", :system_bundler => true, :artifice => nil)).to eq(expected) - expect(run!(file.read, :no_lib => true, :artifice => nil)).to eq(expected) + expect(bundle("exec #{file}", env: env)).to eq(default_openssl_version) + expect(bundle("exec bundle exec #{file}", env: env)).to eq(default_openssl_version) + expect(bundle("exec ruby #{file}", env: env)).to eq(default_openssl_version) + expect(run(file.read, artifice: nil, env: env)).to eq(default_openssl_version) end + skip "ruby_core has openssl and rubygems in the same folder, and this test needs rubygems require but default openssl not in a directly added entry in $LOAD_PATH" if ruby_core? # sanity check that we get the newer, custom version without bundler - sys_exec("#{Gem.ruby} #{file}") + sys_exec "#{Gem.ruby} #{file}", env: env, raise_on_error: false expect(err).to include("custom openssl should not be loaded") end end + + context "with a git gem that includes extensions", :ruby_repo do + before do + build_git "simple_git_binary", &:add_c_extension + bundle_config "path .bundle" + install_gemfile <<-G + source "https://gem.repo1" + gem "simple_git_binary", :git => '#{lib_path("simple_git_binary-1.0")}' + G + end + + it "allows calling bundle install" do + bundle "exec bundle install" + end + + it "allows calling bundle install after removing gem.build_complete" do + FileUtils.rm_r Dir[bundled_app(".bundle/**/gem.build_complete")] + bundle "exec #{Gem.ruby} -S bundle install" + end + end end end diff --git a/spec/bundler/commands/fund_spec.rb b/spec/bundler/commands/fund_spec.rb new file mode 100644 index 0000000000..5883b8a63a --- /dev/null +++ b/spec/bundler/commands/fund_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +RSpec.describe "bundle fund" do + before do + build_repo2 do + build_gem "has_funding_and_other_metadata" do |s| + s.metadata = { + "bug_tracker_uri" => "https://example.com/user/bestgemever/issues", + "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md", + "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1", + "homepage_uri" => "https://bestgemever.example.io", + "mailing_list_uri" => "https://groups.example.com/bestgemever", + "funding_uri" => "https://example.com/has_funding_and_other_metadata/funding", + "source_code_uri" => "https://example.com/user/bestgemever", + "wiki_uri" => "https://example.com/user/bestgemever/wiki", + } + end + + build_gem "has_funding", "1.2.3" do |s| + s.metadata = { + "funding_uri" => "https://example.com/has_funding/funding", + } + end + + build_gem "gem_with_dependent_funding", "1.0" do |s| + s.add_dependency "has_funding" + end + end + end + + it "prints fund information for all gems in the bundle" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'has_funding_and_other_metadata' + gem 'has_funding' + gem 'myrack-obama' + G + + bundle "fund" + + expect(out).to include("* has_funding_and_other_metadata (1.0)\n Funding: https://example.com/has_funding_and_other_metadata/funding") + expect(out).to include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding") + expect(out).to_not include("myrack-obama") + end + + it "does not consider fund information for gem dependencies" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'gem_with_dependent_funding' + G + + bundle "fund" + + expect(out).to_not include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding") + expect(out).to_not include("gem_with_dependent_funding") + end + + it "does not consider fund information for uninstalled optional dependencies" do + install_gemfile <<-G + source "https://gem.repo2" + group :whatever, optional: true do + gem 'has_funding_and_other_metadata' + end + gem 'has_funding' + gem 'myrack-obama' + G + + bundle "fund" + + expect(out).to include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding") + expect(out).to_not include("has_funding_and_other_metadata") + expect(out).to_not include("myrack-obama") + end + + it "considers fund information for installed optional dependencies" do + bundle_config "with whatever" + + install_gemfile <<-G + source "https://gem.repo2" + group :whatever, optional: true do + gem 'has_funding_and_other_metadata' + end + gem 'has_funding' + gem 'myrack-obama' + G + + bundle "fund" + + expect(out).to include("* has_funding_and_other_metadata (1.0)\n Funding: https://example.com/has_funding_and_other_metadata/funding") + expect(out).to include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding") + expect(out).to_not include("myrack-obama") + end + + it "prints message if none of the gems have fund information" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'myrack-obama' + G + + bundle "fund" + + expect(out).to include("None of the installed gems you directly depend on are looking for funding.") + end + + describe "with --group option" do + it "prints fund message for only specified group gems" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'has_funding_and_other_metadata', :group => :development + gem 'has_funding' + G + + bundle "fund --group development" + expect(out).to include("* has_funding_and_other_metadata (1.0)\n Funding: https://example.com/has_funding_and_other_metadata/funding") + expect(out).to_not include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding") + end + end +end diff --git a/spec/bundler/commands/help_spec.rb b/spec/bundler/commands/help_spec.rb index 6faeed058e..f9ad9fff14 100644 --- a/spec/bundler/commands/help_spec.rb +++ b/spec/bundler/commands/help_spec.rb @@ -1,43 +1,37 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle help" do - # Rubygems 1.4+ no longer load gem plugins so this test is no longer needed - it "complains if older versions of bundler are installed", :rubygems => "< 1.4" do - system_gems "bundler-0.8.1" - - bundle "help" - expect(err).to include("older than 0.9") - expect(err).to include("running `gem cleanup bundler`.") - end - - it "uses mann when available", :ruby_repo do + it "uses man when available" do with_fake_man do bundle "help gemfile" end - expect(out).to eq(%(["#{root}/man/gemfile.5"])) + expect(out).to eq(%(["#{man_dir}/gemfile.5"])) end - it "prefixes bundle commands with bundle- when finding the groff files", :ruby_repo do + it "prefixes bundle commands with bundle- when finding the man files" do with_fake_man do bundle "help install" end - expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + expect(out).to eq(%(["#{man_dir}/bundle-install.1"])) end - it "simply outputs the txt file when there is no man on the path", :ruby_repo do - with_path_as("") do - bundle "help install" + it "prexifes bundle commands with bundle- and resolves aliases when finding the man files" do + with_fake_man do + bundle "help package" end - expect(out).to match(/BUNDLE-INSTALL/) + expect(out).to eq(%(["#{man_dir}/bundle-cache.1"])) end - it "still outputs the old help for commands that do not have man pages yet" do - bundle "help version" - expect(out).to include("Prints the bundler's version information") + it "simply outputs the human readable file when there is no man on the path" do + with_path_as("") do + bundle "help install" + end + expect(out).to match(/bundle-install/) end it "looks for a binary and executes it with --help option if it's named bundler-<task>" do + skip "Could not find command testtasks, probably because not a windows friendly executable" if Gem.win_platform? + File.open(tmp("bundler-testtasks"), "w", 0o755) do |f| f.puts "#!/usr/bin/env ruby\nputs ARGV.join(' ')\n" end @@ -46,54 +40,53 @@ RSpec.describe "bundle help" do bundle "help testtasks" end - expect(exitstatus).to be_zero if exitstatus expect(out).to eq("--help") end - it "is called when the --help flag is used after the command", :ruby_repo do + 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(%(["#{root}/man/bundle-install.1"])) + expect(out).to eq(%(["#{man_dir}/bundle-install.1"])) end - it "is called when the --help flag is used before the command", :ruby_repo do + it "is called when the --help flag is used before the command" do with_fake_man do bundle "--help install" end - expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + expect(out).to eq(%(["#{man_dir}/bundle-install.1"])) end - it "is called when the -h flag is used before the command", :ruby_repo do + it "is called when the -h flag is used before the command" do with_fake_man do bundle "-h install" end - expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + expect(out).to eq(%(["#{man_dir}/bundle-install.1"])) end - it "is called when the -h flag is used after the command", :ruby_repo do + it "is called when the -h flag is used after the command" do with_fake_man do bundle "install -h" end - expect(out).to eq(%(["#{root}/man/bundle-install.1"])) + expect(out).to eq(%(["#{man_dir}/bundle-install.1"])) end it "has helpful output when using --help flag for a non-existent command" do with_fake_man do - bundle "instill -h" + bundle "instill -h", raise_on_error: false end - expect(out).to include('Could not find command "instill".') + expect(err).to include('Could not find command "instill".') end - it "is called when only using the --help flag", :ruby_repo do + it "is called when only using the --help flag" do with_fake_man do bundle "--help" end - expect(out).to eq(%(["#{root}/man/bundle.1"])) + expect(out).to eq(%(["#{man_dir}/bundle.1"])) with_fake_man do bundle "-h" end - expect(out).to eq(%(["#{root}/man/bundle.1"])) + expect(out).to eq(%(["#{man_dir}/bundle.1"])) end end diff --git a/spec/bundler/commands/info_spec.rb b/spec/bundler/commands/info_spec.rb index cdfea983dc..a26b1696fb 100644 --- a/spec/bundler/commands/info_spec.rb +++ b/spec/bundler/commands/info_spec.rb @@ -1,41 +1,118 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle info" do - context "info from specific gem in gemfile" do + context "with a standard Gemfile" do before do + build_repo2 do + build_gem "has_metadata" do |s| + s.metadata = { + "bug_tracker_uri" => "https://example.com/user/bestgemever/issues", + "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md", + "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1", + "homepage_uri" => "https://bestgemever.example.io", + "mailing_list_uri" => "https://groups.example.com/bestgemever", + "source_code_uri" => "https://example.com/user/bestgemever", + "wiki_uri" => "https://example.com/user/bestgemever/wiki", + } + end + end + install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo2" gem "rails" + gem "has_metadata" + gem "thin" G end - it "prints information about the current gem" do + 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" - expect(out).to match(%r{Path\: .*\/rails\-2\.3\.2}) +\tHomepage: http://example.com +\tPath: #{default_bundle_path("gems", "rails-2.3.2")}" end - context "given a gem that is not installed" do - it "prints missing gem error" do - bundle "info foo" - expect(out).to eq "Could not find gem 'foo'." - end + 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 shippped in ruby", :ruby_repo do - it "prints information about the default gem", :if => (RUBY_VERSION >= "2.0") do - bundle "info rdoc" - expect(out).to include("* rdoc") + 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_repo1 do + build_repo2 do build_gem "rails", "2.3.2" do |s| s.executables = "rails" s.summary = "Just another test gem" @@ -48,11 +125,125 @@ RSpec.describe "bundle info" do end end - context "given --path option" do - it "prints the path to the gem" do + 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).to match(%r{.*\/rails\-2\.3\.2}) + + 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 index 6ab7e25cc3..989d6fa812 100644 --- a/spec/bundler/commands/init_spec.rb +++ b/spec/bundler/commands/init_spec.rb @@ -1,10 +1,33 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle init" do it "generates a Gemfile" do bundle :init - expect(bundled_app("Gemfile")).to exist + 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 @@ -15,34 +38,65 @@ RSpec.describe "bundle init" do end it "does not change existing Gemfiles" do - expect { bundle :init }.not_to change { File.read(bundled_app("Gemfile")) } + expect { bundle :init, raise_on_error: false }.not_to change { File.read(bundled_app_gemfile) } end it "notifies the user that an existing Gemfile already exists" do + bundle :init, 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 - expect(out).to include("Gemfile already exists") + + 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.join("test.gemspec") } + let(:spec_file) { tmp("test.gemspec") } it "should generate from an existing gemspec" do File.open(spec_file, "w") do |file| file << <<-S Gem::Specification.new do |s| s.name = 'test' - s.add_dependency 'rack', '= 1.0.1' + s.add_dependency 'myrack', '= 1.0.1' s.add_development_dependency 'rspec', '1.2' end S end - bundle :init, :gemspec => spec_file + bundle :init, gemspec: spec_file - gemfile = bundled_app("Gemfile").read + gemfile = bundled_app_gemfile.read expect(gemfile).to match(%r{source 'https://rubygems.org'}) - expect(gemfile.scan(/gem "rack", "= 1.0.1"/).size).to eq(1) + expect(gemfile.scan(/gem "myrack", "= 1.0.1"/).size).to eq(1) expect(gemfile.scan(/gem "rspec", "= 1.2"/).size).to eq(1) expect(gemfile.scan(/group :development/).size).to eq(1) end @@ -58,9 +112,96 @@ RSpec.describe "bundle init" do S end - bundle :init, :gemspec => spec_file - expect(out).to include("There was an error while loading `test.gemspec`") + 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/inject_spec.rb b/spec/bundler/commands/inject_spec.rb deleted file mode 100644 index dd0f1348cc..0000000000 --- a/spec/bundler/commands/inject_spec.rb +++ /dev/null @@ -1,114 +0,0 @@ -# frozen_string_literal: true -require "spec_helper" - -RSpec.describe "bundle inject" do - before :each do - gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - G - end - - context "without a lockfile" do - it "locks with the injected gems" do - expect(bundled_app("Gemfile.lock")).not_to exist - bundle "inject 'rack-obama' '> 0'" - expect(bundled_app("Gemfile.lock").read).to match(/rack-obama/) - end - end - - context "with a lockfile" do - before do - bundle "install" - end - - it "adds the injected gems to the Gemfile" do - expect(bundled_app("Gemfile").read).not_to match(/rack-obama/) - bundle "inject 'rack-obama' '> 0'" - expect(bundled_app("Gemfile").read).to match(/rack-obama/) - end - - it "locks with the injected gems" do - expect(bundled_app("Gemfile.lock").read).not_to match(/rack-obama/) - bundle "inject 'rack-obama' '> 0'" - expect(bundled_app("Gemfile.lock").read).to match(/rack-obama/) - end - end - - context "with injected gems already in the Gemfile" do - it "doesn't add existing gems" do - bundle "inject 'rack' '> 0'" - expect(out).to match(/cannot specify the same gem twice/i) - end - end - - context "incorrect arguments" do - it "fails when more than 2 arguments are passed" do - bundle "inject gem_name 1 v" - expect(out).to eq(<<-E.strip) -ERROR: "bundle inject" was called with arguments ["gem_name", "1", "v"] -Usage: "bundle inject GEM VERSION" - E - end - end - - context "with source option" do - it "add gem with source option in gemfile" do - bundle "inject 'foo' '>0' --source file://#{gem_repo1}" - gemfile = bundled_app("Gemfile").read - str = "gem \"foo\", \"> 0\", :source => \"file://#{gem_repo1}\"" - expect(gemfile).to include str - end - end - - context "with group option" do - it "add gem with group option in gemfile" do - bundle "inject 'rack-obama' '>0' --group=development" - gemfile = bundled_app("Gemfile").read - str = "gem \"rack-obama\", \"> 0\", :group => [:development]" - expect(gemfile).to include str - end - - it "add gem with multiple groups in gemfile" do - bundle "inject 'rack-obama' '>0' --group=development,test" - gemfile = bundled_app("Gemfile").read - str = "gem \"rack-obama\", \"> 0\", :groups => [:development, :test]" - expect(gemfile).to include str - end - end - - context "when frozen" do - before do - bundle "install" - bundle "config --local frozen 1" - end - - it "injects anyway" do - bundle "inject 'rack-obama' '> 0'" - expect(bundled_app("Gemfile").read).to match(/rack-obama/) - end - - it "locks with the injected gems" do - expect(bundled_app("Gemfile.lock").read).not_to match(/rack-obama/) - bundle "inject 'rack-obama' '> 0'" - expect(bundled_app("Gemfile.lock").read).to match(/rack-obama/) - end - - it "restores frozen afterwards" do - bundle "inject 'rack-obama' '> 0'" - config = YAML.load(bundled_app(".bundle/config").read) - expect(config["BUNDLE_FROZEN"]).to eq("1") - end - - it "doesn't allow Gemfile changes" do - gemfile <<-G - source "file://#{gem_repo1}" - gem "rack-obama" - G - bundle "inject 'rack' '> 0'" - expect(out).to match(/trying to install in deployment mode after changing/) - - expect(bundled_app("Gemfile.lock").read).not_to match(/rack-obama/) - end - end -end diff --git a/spec/bundler/commands/install_spec.rb b/spec/bundler/commands/install_spec.rb index 2d67a39f1e..3b24434dc7 100644 --- a/spec/bundler/commands/install_spec.rb +++ b/spec/bundler/commands/install_spec.rb @@ -1,50 +1,112 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install with gem sources" do describe "the simple case" do it "prints output and returns if no dependencies are specified" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" G bundle :install - expect(out).to match(/no dependencies/) + expect(err).to match(/no dependencies/) end it "does not make a lockfile if the install fails" do - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false raise StandardError, "FAIL" G - expect(err).to lack_errors - expect(out).to match(/StandardError, "FAIL"/) - expect(bundled_app("Gemfile.lock")).not_to exist + expect(err).to include('StandardError, "FAIL"') + expect(bundled_app_lock).not_to exist end it "creates a Gemfile.lock" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + 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("Gemfile.lock")).to exist + expect(bundled_app_lock).not_to exist end it "does not create ./.bundle by default" do - gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" G - bundle :install # can't use install_gemfile since it sets retry expect(bundled_app(".bundle")).not_to exist end - it "creates lock files based on the Gemfile name" do + 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 "file://#{gem_repo1}" - gem "rack", "1.0" + source "https://gem.repo1" + gem "myrack", "1.0" G bundle "install --gemfile OmgFile" @@ -52,71 +114,126 @@ RSpec.describe "bundle install with gem sources" do expect(bundled_app("OmgFile.lock")).to exist end + it "doesn't create a lockfile if --no-lock option is given" do + gemfile bundled_app("OmgFile"), <<-G + source "https://gem.repo1" + gem "myrack", "1.0" + G + + bundle "install --gemfile OmgFile --no-lock" + + expect(bundled_app("OmgFile.lock")).not_to exist + end + + it "doesn't create a lockfile if --no-lock and --lockfile options are given" do + gemfile bundled_app("OmgFile"), <<-G + source "https://gem.repo1" + gem "myrack", "1.0" + G + + bundle "install --gemfile OmgFile --no-lock --lockfile ReallyOmgFile.lock" + + expect(bundled_app("OmgFile.lock")).not_to exist + expect(bundled_app("ReallyOmgFile.lock")).not_to exist + end + it "doesn't delete the lockfile if one already exists" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' G - lockfile = File.read(bundled_app("Gemfile.lock")) + lockfile = File.read(bundled_app_lock) - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false raise StandardError, "FAIL" G - expect(File.read(bundled_app("Gemfile.lock"))).to eq(lockfile) + expect(File.read(bundled_app_lock)).to eq(lockfile) end it "does not touch the lockfile if nothing changed" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - expect { run "1" }.not_to change { File.mtime(bundled_app("Gemfile.lock")) } + expect { run "1" }.not_to change { File.mtime(bundled_app_lock) } end it "fetches gems" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem 'rack' + 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 - expect(default_bundle_path("gems/rack-1.0.0")).to exist - expect(the_bundle).to include_gems("rack 1.0.0") + FileUtils.rm_r(default_bundle_path("gems/myrack-1.0.0")) + + bundle "install --verbose" + + expect(out).to include("Installing myrack 1.0.0") + expect(default_bundle_path("gems/myrack-1.0.0")).to exist + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "does not state that it's constantly reinstalling empty gems" do + build_repo4 do + build_gem "empty", "1.0.0", no_default: true + end + + install_gemfile <<~G + source "https://gem.repo4" + + gem "empty" + G + gem_dir = default_bundle_path("gems/empty-1.0.0") + expect(gem_dir).to be_empty + + bundle "install --verbose" + expect(out).not_to include("Installing empty") end it "fetches gems when multiple versions are specified" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem 'rack', "> 0.9", "< 1.0" + source "https://gem.repo1" + gem 'myrack', "> 0.9", "< 1.0" G - expect(default_bundle_path("gems/rack-0.9.1")).to exist - expect(the_bundle).to include_gems("rack 0.9.1") + expect(default_bundle_path("gems/myrack-0.9.1")).to exist + expect(the_bundle).to include_gems("myrack 0.9.1") end it "fetches gems when multiple versions are specified take 2" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem 'rack', "< 1.0", "> 0.9" + source "https://gem.repo1" + gem 'myrack', "< 1.0", "> 0.9" G - expect(default_bundle_path("gems/rack-0.9.1")).to exist - expect(the_bundle).to include_gems("rack 0.9.1") + expect(default_bundle_path("gems/myrack-0.9.1")).to exist + expect(the_bundle).to include_gems("myrack 0.9.1") end it "raises an appropriate error when gems are specified using symbols" do - install_gemfile(<<-G) - source "file://#{gem_repo1}" - gem :rack + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem :myrack G - expect(exitstatus).to eq(4) if exitstatus + expect(exitstatus).to eq(4) end it "pulls in dependencies" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "rails" G @@ -125,16 +242,22 @@ RSpec.describe "bundle install with gem sources" do it "does the right version" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", "0.9.1" + source "https://gem.repo1" + gem "myrack", "0.9.1" G - expect(the_bundle).to include_gems "rack 0.9.1" + expect(the_bundle).to include_gems "myrack 0.9.1" end it "does not install the development dependency" do + build_repo2 do + build_gem "with_development_dependency" do |s| + s.add_development_dependency "activesupport", "= 2.3.5" + end + end + install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo2" gem "with_development_dependency" G @@ -144,7 +267,7 @@ RSpec.describe "bundle install with gem sources" do it "resolves correctly" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "activemerchant" gem "rails" G @@ -154,12 +277,12 @@ RSpec.describe "bundle install with gem sources" do it "activates gem correctly according to the resolved gems" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "activesupport", "2.3.5" G install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "activemerchant" gem "rails" G @@ -168,7 +291,7 @@ RSpec.describe "bundle install with gem sources" do end it "does not reinstall any gem that is already available locally" do - system_gems "activesupport-2.3.2" + system_gems "activesupport-2.3.2", path: default_bundle_path build_repo2 do build_gem "activesupport", "2.3.2" do |s| @@ -177,7 +300,7 @@ RSpec.describe "bundle install with gem sources" do end install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "activerecord", "2.3.2" G @@ -185,170 +308,474 @@ RSpec.describe "bundle install with gem sources" do end it "works when the gemfile specifies gems that only exist in the system" do - build_gem "foo", :to_system => true + build_gem "foo", to_bundle: true install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" gem "foo" G - expect(the_bundle).to include_gems "rack 1.0.0", "foo 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0", "foo 1.0.0" end it "prioritizes local gems over remote gems" do - build_gem "rack", "1.0.0", :to_system => true do |s| - s.add_dependency "activesupport", "2.3.5" - end + build_gem "myrack", "9.0.0", to_bundle: true install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.5" + 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 - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "platform_specific" - G + simulate_platform "x86-darwin-100" do + install_gemfile <<-G + source "https://gem.repo1" + gem "platform_specific" + G - run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" - expect(out).to eq("1.0.0 #{Bundler.local_platform}") + expect(the_bundle).to include_gems("platform_specific 1.0 x86-darwin-100") + end end it "falls back on plain ruby" do - simulate_platform "foo-bar-baz" - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "platform_specific" - G + simulate_platform "foo-bar-baz" do + install_gemfile <<-G + source "https://gem.repo1" + gem "platform_specific" + G - run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" - expect(out).to eq("1.0.0 RUBY") + expect(the_bundle).to include_gems("platform_specific 1.0 ruby") + end end it "installs gems for java" do - simulate_platform "java" - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "platform_specific" - G + simulate_platform "java" do + install_gemfile <<-G + source "https://gem.repo1" + gem "platform_specific" + G - run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" - expect(out).to eq("1.0.0 JAVA") + expect(the_bundle).to include_gems("platform_specific 1.0 java") + end end it "installs gems for windows" do - simulate_platform mswin + simulate_platform "x86-mswin32" do + install_gemfile <<-G + source "https://gem.repo1" + gem "platform_specific" + G - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "platform_specific" - G + expect(the_bundle).to include_gems("platform_specific 1.0 x86-mswin32") + end + end - run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" - expect(out).to eq("1.0.0 MSWIN") + 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 - describe "doing bundle install foo" do - before do - gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - G + it "gives useful errors if no global sources are set, and gems not installed locally, with and without a lockfile" do + install_gemfile <<-G, raise_on_error: false + gem "myrack" + G + + expect(err).to eq("Could not find gem 'myrack' in locally installed gems.") + + lockfile <<~L + GEM + specs: + myrack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install", raise_on_error: false + + expect(err).to include( + "Because your Gemfile specifies no global remote source, your bundle is locked to " \ + "myrack (1.0.0) from locally installed gems. However, myrack (1.0.0) is not installed. " \ + "You'll need to either add a global remote source to your Gemfile or make sure myrack (1.0.0) " \ + "is available locally before rerunning Bundler." + ) + end + + it "creates a Gemfile.lock on a blank Gemfile" do + install_gemfile <<-G + source "https://gem.repo1" + G + + expect(File.exist?(bundled_app_lock)).to eq(true) + end + + it "throws a warning if a gem is added twice in Gemfile without version requirements" do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack" + gem "myrack" + G + + expect(err).to include("Your Gemfile lists the gem myrack (>= 0) more than once.") + expect(err).to include("Remove any duplicate entries and specify the gem only once.") + expect(err).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.") + end + + it "throws a warning if a gem is added twice in Gemfile with same versions" do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack", "1.0" + gem "myrack", "1.0" + G + + expect(err).to include("Your Gemfile lists the gem myrack (= 1.0) more than once.") + expect(err).to include("Remove any duplicate entries and specify the gem only once.") + expect(err).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.") + end + + it "throws a warning if a gem is added twice under different platforms and does not crash when using the generated lockfile" do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack", :platform => :jruby + gem "myrack" + G + + bundle "install" + + expect(err).to include("Your Gemfile lists the gem myrack (>= 0) more than once.") + expect(err).to include("Remove any duplicate entries and specify the gem only once.") + expect(err).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.") + end + + it "does not throw a warning if a gem is added once in Gemfile and also inside a gemspec as a development dependency" do + build_lib "my-gem", path: bundled_app do |s| + s.add_development_dependency "my-private-gem" end - it "works" do - bundle "install --path vendor" - expect(the_bundle).to include_gems "rack 1.0" + build_repo2 do + build_gem "my-private-gem" end - it "allows running bundle install --system without deleting foo" do - bundle "install --path vendor" - bundle "install --system" - FileUtils.rm_rf(bundled_app("vendor")) - expect(the_bundle).to include_gems "rack 1.0" + gemfile <<~G + source "https://gem.repo2" + + gemspec + + gem "my-private-gem", :group => :development + G + + bundle :install + + expect(err).to be_empty + expect(the_bundle).to include_gems("my-private-gem 1.0") + end + + it "does not warn if a gem is added once in Gemfile and also inside a gemspec as a development dependency, with compatible requirements" do + build_lib "my-gem", path: bundled_app do |s| + s.add_development_dependency "rubocop", "~> 1.36.0" end - it "allows running bundle install --system after deleting foo" do - bundle "install --path vendor" - FileUtils.rm_rf(bundled_app("vendor")) - bundle "install --system" - expect(the_bundle).to include_gems "rack 1.0" + build_repo4 do + build_gem "rubocop", "1.36.0" + build_gem "rubocop", "1.37.1" end + + gemfile <<~G + source "https://gem.repo4" + + gemspec + + gem "rubocop", group: :development + G + + bundle :install + + expect(err).to be_empty + + expect(the_bundle).to include_gems("rubocop 1.36.0") end - it "finds gems in multiple sources" do - build_repo2 - update_repo2 + it "raises an error if a gem is added once in Gemfile and also inside a gemspec as a development dependency, with incompatible requirements" do + build_lib "my-gem", path: bundled_app do |s| + s.add_development_dependency "rubocop", "~> 1.36.0" + end - install_gemfile <<-G - source "file://#{gem_repo1}" - source "file://#{gem_repo2}" + build_repo4 do + build_gem "rubocop", "1.36.0" + build_gem "rubocop", "1.37.1" + end - gem "activesupport", "1.2.3" - gem "rack", "1.2" + gemfile <<~G + source "https://gem.repo4" + + gemspec + + gem "rubocop", "~> 1.37.0", group: :development G - expect(the_bundle).to include_gems "rack 1.2", "activesupport 1.2.3" + bundle :install, raise_on_error: false + + expect(err).to include("The rubocop dependency has conflicting requirements in Gemfile (~> 1.37.0) and gemspec (~> 1.36.0)") end - it "gives a useful error if no sources are set" do - install_gemfile <<-G - gem "rack" + it "includes the gem without warning if two gemspecs add it with the same requirement" do + gem1 = tmp("my-gem-1") + gem2 = tmp("my-gem-2") + + build_lib "my-gem", path: gem1 do |s| + s.add_development_dependency "rubocop", "~> 1.36.0" + end + + build_lib "my-gem-2", path: gem2 do |s| + s.add_development_dependency "rubocop", "~> 1.36.0" + end + + build_repo4 do + build_gem "rubocop", "1.36.0" + end + + gemfile <<~G + source "https://gem.repo4" + + gemspec path: "#{gem1}" + gemspec path: "#{gem2}" G bundle :install - expect(out).to include("Your Gemfile has no gem server sources") + + expect(err).to be_empty + expect(the_bundle).to include_gems("rubocop 1.36.0") end - it "creates a Gemfile.lock on a blank Gemfile" do - install_gemfile <<-G + it "includes the gem without warning if two gemspecs add it with compatible requirements" do + gem1 = tmp("my-gem-1") + gem2 = tmp("my-gem-2") + + build_lib "my-gem", path: gem1 do |s| + s.add_development_dependency "rubocop", "~> 1.0" + end + + build_lib "my-gem-2", path: gem2 do |s| + s.add_development_dependency "rubocop", "~> 1.36.0" + end + + build_repo4 do + build_gem "rubocop", "1.36.0" + end + + gemfile <<~G + source "https://gem.repo4" + + gemspec path: "#{gem1}" + gemspec path: "#{gem2}" G - expect(File.exist?(bundled_app("Gemfile.lock"))).to eq(true) + bundle :install + + expect(err).to be_empty + expect(the_bundle).to include_gems("rubocop 1.36.0") end - it "gracefully handles error when rubygems server is unavailable" do - install_gemfile <<-G, :artifice => nil - source "file://#{gem_repo1}" - source "http://localhost:9384" + it "errors out if two gemspecs add it with incompatible requirements" do + gem1 = tmp("my-gem-1") + gem2 = tmp("my-gem-2") - gem 'foo' + 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, :artifice => nil - expect(out).to include("Could not fetch specs from http://localhost:9384/") - expect(out).not_to include("file://") + 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 "fails gracefully when downloading an invalid specification from the full index", :rubygems => "2.5" do - build_repo2 do - build_gem "ajp-rails", "0.0.0", :gemspec => false, :skip_validation => true do |s| - bad_deps = [["ruby-ajp", ">= 0.2.0"], ["rails", ">= 0.14"]] + 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, bad_deps) - - raise "failed to set bad deps" unless s.dependencies == bad_deps + instance_variable_set(:@dependencies, invalid_deps) end + build_gem "ruby-ajp", "1.0.0" end - install_gemfile <<-G, :full_index => true - source "file://#{gem_repo2}" + install_gemfile <<-G, full_index: true, raise_on_error: false + source "https://gem.repo2" gem "ajp-rails", "0.0.0" G - expect(out).not_to match(/Error Report/i) - expect(err).not_to match(/Error Report/i) - expect(out).to include("An error occurred while installing ajp-rails (0.0.0), and Bundler cannot continue."). - and include("Make sure that `gem install ajp-rails -v '0.0.0'` succeeds before bundling.") + 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 @@ -356,11 +783,10 @@ RSpec.describe "bundle install with gem sources" do FileUtils.touch(bundled_app(".bundle/config")) install_gemfile(<<-G) - source "file://#{gem_repo1}" + source "https://gem.repo1" gem 'foo' G - expect(exitstatus).to eq(0) if exitstatus end it "doesn't blow up when the global .bundle/config is empty" do @@ -368,130 +794,713 @@ RSpec.describe "bundle install with gem sources" do FileUtils.touch("#{Bundler.rubygems.user_home}/.bundle/config") install_gemfile(<<-G) - source "file://#{gem_repo1}" + source "https://gem.repo1" gem 'foo' G - expect(exitstatus).to eq(0) if exitstatus end end describe "Ruby version in Gemfile.lock" do - include Bundler::GemHelpers - context "and using an unsupported Ruby version" do it "prints an error" do - install_gemfile <<-G - ::RUBY_VERSION = '1.8.7' - ruby '~> 2.1' + install_gemfile <<-G, raise_on_error: false + ruby '~> 1.2' + source "https://gem.repo1" G - expect(out).to include("Your Ruby version is 1.8.7, but your Gemfile specified ~> 2.1") + expect(err).to include("Your Ruby version is #{Gem.ruby_version}, but your Gemfile specified ~> 1.2") end end context "and using a supported Ruby version" do before do install_gemfile <<-G - ::RUBY_VERSION = '2.1.3' - ::RUBY_PATCHLEVEL = 100 - ruby '~> 2.1.0' + ruby '~> #{Gem.ruby_version}' + source "https://gem.repo1" G end it "writes current Ruby version to Gemfile.lock" do - lockfile_should_be <<-L + checksums = checksums_section_when_enabled + expect(lockfile).to eq <<~L GEM + remote: https://gem.repo1/ specs: PLATFORMS - ruby + #{lockfile_platforms} DEPENDENCIES - + #{checksums} RUBY VERSION - ruby 2.1.3p100 + #{Bundler::RubyVersion.system} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L end - it "updates Gemfile.lock with updated incompatible ruby version" do + it "updates Gemfile.lock with updated yet still compatible ruby version" do install_gemfile <<-G - ::RUBY_VERSION = '2.2.3' - ::RUBY_PATCHLEVEL = 100 - ruby '~> 2.2.0' + ruby '~> #{current_ruby_minor}' + source "https://gem.repo1" G - lockfile_should_be <<-L + checksums = checksums_section_when_enabled + + expect(lockfile).to eq <<~L GEM + remote: https://gem.repo1/ specs: PLATFORMS - ruby + #{lockfile_platforms} DEPENDENCIES - + #{checksums} RUBY VERSION - ruby 2.2.3p100 + #{Bundler::RubyVersion.system} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L end + + it "does not crash when unlocking" do + gemfile <<-G + source "https://gem.repo1" + ruby '>= 2.1.0' + G + + bundle "update" + + expect(err).not_to include("Could not find gem 'Ruby") + end end end describe "when Bundler root contains regex chars" do - before do + it "doesn't blow up when using the `gem` DSL" do root_dir = tmp("foo[]bar") FileUtils.mkdir_p(root_dir) - in_app_root_custom(root_dir) - end - it "doesn't blow up" do build_lib "foo" gemfile = <<-G + source "https://gem.repo1" gem 'foo', :path => "#{lib_path("foo-1.0")}" G - File.open("Gemfile", "w") do |file| + File.open("#{root_dir}/Gemfile", "w") do |file| file.puts gemfile end - bundle :install + bundle :install, dir: root_dir + end + + it "doesn't blow up when using the `gemspec` DSL" do + root_dir = tmp("foo[]bar") + + 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 - expect(exitstatus).to eq(0) if exitstatus + bundle :install, dir: root_dir end end describe "when requesting a quiet install via --quiet" do - it "should be quiet" do + it "should be quiet if there are no warnings" do + bundle_config "force_ruby_platform true" + gemfile <<-G - gem 'rack' + source "https://gem.repo1" + gem 'myrack' G - bundle :install, :quiet => true - expect(out).to include("Could not find gem 'rack'") - expect(out).to_not include("Your Gemfile has no gem server sources") + 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 write access" do + describe "when bundle path does not have cd permission", :permissions do + let(:bundle_path) { bundled_app("vendor") } + before do - FileUtils.mkdir_p(bundled_app("vendor")) + FileUtils.mkdir_p(bundle_path) gemfile <<-G - source "file://#{gem_repo1}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' G end it "should display a proper message to explain the problem" do - FileUtils.chmod(0o500, bundled_app("vendor")) + FileUtils.chmod(0o500, bundle_path) - bundle :install, :path => "vendor" - expect(out).to include(bundled_app("vendor").to_s) - expect(out).to include("grant write permissions") + 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 @@ -499,15 +1508,608 @@ RSpec.describe "bundle install with gem sources" do before do gemfile <<-G source 'https://rubygems.org/' - gem 'bundler' + gem "." G end - it "should display a helpful messag explaining how to fix it" do - bundle :install, :env => { "BUNDLE_RUBYGEMS__ORG" => "user:pass{word" } - expect(exitstatus).to eq(17) if exitstatus - expect(out).to eq("Please CGI escape your usernames and passwords before " \ + 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 index 056ef0f300..346cdedc42 100644 --- a/spec/bundler/commands/issue_spec.rb +++ b/spec/bundler/commands/issue_spec.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle issue" do it "exits with a message" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "rails" G diff --git a/spec/bundler/commands/licenses_spec.rb b/spec/bundler/commands/licenses_spec.rb index 0ee1a46945..ebfad5ed4a 100644 --- a/spec/bundler/commands/licenses_spec.rb +++ b/spec/bundler/commands/licenses_spec.rb @@ -1,10 +1,15 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle licenses" do before :each do + build_repo2 do + build_gem "with_license" do |s| + s.license = "MIT" + end + end + install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo2" gem "rails" gem "with_license" G @@ -13,19 +18,19 @@ RSpec.describe "bundle licenses" do it "prints license information for all gems in the bundle" do bundle "licenses" - expect(out).to include("bundler: Unknown") + expect(out).to include("bundler: MIT") expect(out).to include("with_license: MIT") end it "performs an automatic bundle install" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo2" gem "rails" gem "with_license" gem "foo" G - bundle "config auto_install 1" + bundle_config "auto_install 1" bundle :licenses expect(out).to include("Installing foo 1.0") end diff --git a/spec/bundler/commands/list_spec.rb b/spec/bundler/commands/list_spec.rb 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 index 5c15b6a7f6..8ab3cc7e8d 100644 --- a/spec/bundler/commands/lock_spec.rb +++ b/spec/bundler/commands/lock_spec.rb @@ -1,28 +1,22 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle lock" do - def strip_lockfile(lockfile) - strip_whitespace(lockfile).sub(/\n\Z/, "") - end - - def read_lockfile(file = "Gemfile.lock") - strip_lockfile bundled_app(file).read - end - - let(:repo) { gem_repo1 } - - before :each do - gemfile <<-G - source "file://#{repo}" - gem "rails" - gem "with_license" - gem "foo" - G + let(:expected_lockfile) do + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "actionmailer", "2.3.2" + c.checksum gem_repo4, "actionpack", "2.3.2" + c.checksum gem_repo4, "activerecord", "2.3.2" + c.checksum gem_repo4, "activeresource", "2.3.2" + c.checksum gem_repo4, "activesupport", "2.3.2" + c.checksum gem_repo4, "foo", "1.0" + c.checksum gem_repo4, "rails", "2.3.2" + c.checksum gem_repo4, "rake", rake_version + c.checksum gem_repo4, "weakling", "0.0.3" + end - @lockfile = strip_lockfile <<-L + <<~L GEM - remote: file:#{repo}/ + remote: https://gem.repo4/ specs: actionmailer (2.3.2) activesupport (= 2.3.2) @@ -39,80 +33,654 @@ RSpec.describe "bundle lock" do actionpack (= 2.3.2) activerecord (= 2.3.2) activeresource (= 2.3.2) - rake (= 10.0.2) - rake (10.0.2) - with_license (1.0) + rake (= #{rake_version}) + rake (#{rake_version}) + weakling (0.0.3) PLATFORMS - #{local} + #{lockfile_platforms} DEPENDENCIES foo rails - with_license + weakling + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + let(:outdated_lockfile) do + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "actionmailer", "2.3.1" + c.checksum gem_repo4, "actionpack", "2.3.1" + c.checksum gem_repo4, "activerecord", "2.3.1" + c.checksum gem_repo4, "activeresource", "2.3.1" + c.checksum gem_repo4, "activesupport", "2.3.1" + c.checksum gem_repo4, "foo", "1.0" + c.checksum gem_repo4, "rails", "2.3.1" + c.checksum gem_repo4, "rake", rake_version + c.checksum gem_repo4, "weakling", "0.0.3" + end + + <<~L + GEM + remote: https://gem.repo4/ + specs: + actionmailer (2.3.1) + activesupport (= 2.3.1) + actionpack (2.3.1) + activesupport (= 2.3.1) + activerecord (2.3.1) + activesupport (= 2.3.1) + activeresource (2.3.1) + activesupport (= 2.3.1) + activesupport (2.3.1) + foo (1.0) + rails (2.3.1) + actionmailer (= 2.3.1) + actionpack (= 2.3.1) + activerecord (= 2.3.1) + activeresource (= 2.3.1) + rake (= #{rake_version}) + rake (#{rake_version}) + weakling (0.0.3) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo + rails + weakling + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L end + let(:gemfile_with_rails_weakling_and_foo_from_repo4) do + build_repo4 do + build_gem "rake", "10.0.1" + build_gem "rake", rake_version + + %w[2.3.1 2.3.2].each do |version| + build_gem "rails", version do |s| + s.executables = "rails" + s.add_dependency "rake", version == "2.3.1" ? "10.0.1" : rake_version + s.add_dependency "actionpack", version + s.add_dependency "activerecord", version + s.add_dependency "actionmailer", version + s.add_dependency "activeresource", version + end + build_gem "actionpack", version do |s| + s.add_dependency "activesupport", version + end + build_gem "activerecord", version do |s| + s.add_dependency "activesupport", version + end + build_gem "actionmailer", version do |s| + s.add_dependency "activesupport", version + end + build_gem "activeresource", version do |s| + s.add_dependency "activesupport", version + end + build_gem "activesupport", version + end + + build_gem "weakling", "0.0.3" + + build_gem "foo" + end + + gemfile <<-G + source "https://gem.repo4" + gem "rails" + gem "weakling" + gem "foo" + G + end + it "prints a lockfile when there is no existing lockfile with --print" do + gemfile_with_rails_weakling_and_foo_from_repo4 + bundle "lock --print" - expect(out).to include(@lockfile) + expect(out).to eq(expected_lockfile.chomp) end it "prints a lockfile when there is an existing lockfile with --print" do - lockfile @lockfile + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile expected_lockfile + + bundle "lock --print" + + expect(out).to eq(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(@lockfile) + expect(out).to eq(expected_lockfile.chomp) end it "writes a lockfile when there is no existing lockfile" do + gemfile_with_rails_weakling_and_foo_from_repo4 + bundle "lock" - expect(read_lockfile).to eq(@lockfile) + expect(read_lockfile).to eq(expected_lockfile) + end + + it "prints a lockfile without fetching new checksums if the existing lockfile had no checksums" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile expected_lockfile + + bundle "lock --print" + + expect(out).to eq(expected_lockfile.chomp) + end + + it "touches the lockfile when there is an existing lockfile that does not need changes" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile expected_lockfile + + expect do + bundle "lock" + end.to change { bundled_app_lock.mtime } + end + + it "does not touch lockfile with --print" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile expected_lockfile + + expect do + bundle "lock --print" + end.not_to change { bundled_app_lock.mtime } end it "writes a lockfile when there is an outdated lockfile using --update" do - lockfile @lockfile.gsub("2.3.2", "2.3.1") + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile outdated_lockfile + + bundle "lock --update" + + expect(read_lockfile).to eq(expected_lockfile) + end + + 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 --update" + bundle "lock --print --update" - expect(read_lockfile).to eq(@lockfile) + expect(out).to eq(expected_lockfile.rstrip) + end + + it "emits info messages to stderr when updating an outdated lockfile using --print --update" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile outdated_lockfile + + bundle "lock --print --update" + + expect(err).to eq(<<~STDERR.rstrip) + Fetching gem metadata from https://gem.repo4/... + Resolving dependencies... + STDERR + end + + it "writes a lockfile when there is an outdated lockfile and bundle is frozen" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile outdated_lockfile + + bundle "lock --update", env: { "BUNDLE_FROZEN" => "true" } + + expect(read_lockfile).to eq(expected_lockfile) end it "does not fetch remote specs when using the --local option" do - bundle "lock --update --local" + gemfile_with_rails_weakling_and_foo_from_repo4 + + bundle "lock --update --local", raise_on_error: false + + expect(err).to match(/locally installed gems/) + end + + it "does not fetch remote checksums with --local" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile expected_lockfile + + bundle "lock --print --local" + + expect(out).to eq(expected_lockfile.chomp) + end - expect(out).to include("sources listed in your Gemfile") + 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(@lockfile) + expect(read_lockfile("lock")).to eq(expected_lockfile) expect { read_lockfile }.to raise_error(Errno::ENOENT) end + it "updates a specific gem and write to a custom location" do + build_repo4 do + build_gem "foo", %w[1.0.2 1.0.3] + build_gem "warning", %w[1.4.0 1.5.0] + end + + gemfile <<~G + source "https://gem.repo4" + + gem "foo" + gem "warning" + G + + lockfile <<~L + GEM + remote: https://gem.repo4 + specs: + foo (1.0.2) + warning (1.4.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + uri + warning + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --update foo --lockfile=lock" + + lockfile_content = read_lockfile("lock") + expect(lockfile_content).to include("foo (1.0.3)") + expect(lockfile_content).to include("warning (1.4.0)") + end + + it "writes to custom location using --lockfile when a default lockfile is present" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + bundle "install" + bundle "lock --lockfile=lock" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "actionmailer", "2.3.2" + c.checksum gem_repo4, "actionpack", "2.3.2" + c.checksum gem_repo4, "activerecord", "2.3.2" + c.checksum gem_repo4, "activeresource", "2.3.2" + c.checksum gem_repo4, "activesupport", "2.3.2" + c.checksum gem_repo4, "foo", "1.0" + c.checksum gem_repo4, "rails", "2.3.2" + c.checksum gem_repo4, "rake", rake_version + c.checksum gem_repo4, "weakling", "0.0.3" + end + + lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + actionmailer (2.3.2) + activesupport (= 2.3.2) + actionpack (2.3.2) + activesupport (= 2.3.2) + activerecord (2.3.2) + activesupport (= 2.3.2) + activeresource (2.3.2) + activesupport (= 2.3.2) + activesupport (2.3.2) + foo (1.0) + rails (2.3.2) + actionmailer (= 2.3.2) + actionpack (= 2.3.2) + activerecord (= 2.3.2) + activeresource (= 2.3.2) + rake (= #{rake_version}) + rake (#{rake_version}) + weakling (0.0.3) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo + rails + weakling + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + expect(out).to match(/Writing lockfile to.+lock/) + expect(read_lockfile("lock")).to eq(lockfile) + end + it "update specific gems using --update" do - lockfile @lockfile.gsub("2.3.2", "2.3.1").gsub("10.0.2", "10.0.1") + gemfile_with_rails_weakling_and_foo_from_repo4 + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "actionmailer", "2.3.1" + c.checksum gem_repo4, "actionpack", "2.3.1" + c.checksum gem_repo4, "activerecord", "2.3.1" + c.checksum gem_repo4, "activeresource", "2.3.1" + c.checksum gem_repo4, "activesupport", "2.3.1" + c.checksum gem_repo4, "foo", "1.0" + c.checksum gem_repo4, "rails", "2.3.1" + c.checksum gem_repo4, "rake", "10.0.1" + c.checksum gem_repo4, "weakling", "0.0.3" + end + + lockfile_with_outdated_rails_and_rake = <<~L + GEM + remote: https://gem.repo4/ + specs: + actionmailer (2.3.1) + activesupport (= 2.3.1) + actionpack (2.3.1) + activesupport (= 2.3.1) + activerecord (2.3.1) + activesupport (= 2.3.1) + activeresource (2.3.1) + activesupport (= 2.3.1) + activesupport (2.3.1) + foo (1.0) + rails (2.3.1) + actionmailer (= 2.3.1) + actionpack (= 2.3.1) + activerecord (= 2.3.1) + activeresource (= 2.3.1) + rake (= 10.0.1) + rake (10.0.1) + weakling (0.0.3) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo + rails + weakling + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + lockfile lockfile_with_outdated_rails_and_rake bundle "lock --update rails rake" - expect(read_lockfile).to eq(@lockfile) + expect(read_lockfile).to eq(expected_lockfile) + end + + it "updates specific gems using --update, even if that requires unlocking other top level gems" do + build_repo4 do + build_gem "prism", "0.15.1" + build_gem "prism", "0.24.0" + + build_gem "ruby-lsp", "0.12.0" do |s| + s.add_dependency "prism", "< 0.24.0" + end + + build_gem "ruby-lsp", "0.16.1" do |s| + s.add_dependency "prism", ">= 0.24.0" + end + + build_gem "tapioca", "0.11.10" do |s| + s.add_dependency "prism", "< 0.24.0" + end + + build_gem "tapioca", "0.13.1" do |s| + s.add_dependency "prism", ">= 0.24.0" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "tapioca" + gem "ruby-lsp" + G + + lockfile <<~L + GEM + remote: https://gem.repo4 + specs: + prism (0.15.1) + ruby-lsp (0.12.0) + prism (< 0.24.0) + tapioca (0.11.10) + prism (< 0.24.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ruby-lsp + tapioca + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --update tapioca --verbose" + + expect(lockfile).to include("tapioca (0.13.1)") + end + + it "updates specific gems using --update, even if that requires unlocking other top level gems, but only as few as possible" do + build_repo4 do + build_gem "prism", "0.15.1" + build_gem "prism", "0.24.0" + + build_gem "ruby-lsp", "0.12.0" do |s| + s.add_dependency "prism", "< 0.24.0" + end + + build_gem "ruby-lsp", "0.16.1" do |s| + s.add_dependency "prism", ">= 0.24.0" + end + + build_gem "tapioca", "0.11.10" do |s| + s.add_dependency "prism", "< 0.24.0" + end + + build_gem "tapioca", "0.13.1" do |s| + s.add_dependency "prism", ">= 0.24.0" + end + + build_gem "other-prism-dependent", "1.0.0" do |s| + s.add_dependency "prism", ">= 0.15.1" + end + + build_gem "other-prism-dependent", "1.1.0" do |s| + s.add_dependency "prism", ">= 0.15.1" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "tapioca" + gem "ruby-lsp" + gem "other-prism-dependent" + G + + lockfile <<~L + GEM + remote: https://gem.repo4 + specs: + other-prism-dependent (1.0.0) + prism (>= 0.15.1) + prism (0.15.1) + ruby-lsp (0.12.0) + prism (< 0.24.0) + tapioca (0.11.10) + prism (< 0.24.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ruby-lsp + tapioca + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --update tapioca" + + expect(lockfile).to include("tapioca (0.13.1)") + expect(lockfile).to include("other-prism-dependent (1.0.0)") + end + + it "preserves unknown checksum algorithms" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile expected_lockfile.gsub(/(sha256=[a-f0-9]+)$/, "constant=true,\\1,xyz=123") + + previous_lockfile = read_lockfile + + bundle "lock" + + expect(read_lockfile).to eq(previous_lockfile) + end + + it "does not unlock git sources when only uri shape changes" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + build_git("foo") + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + # Change uri format to end with "/" and reinstall + install_gemfile <<-G, verbose: true + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}/" + G + + expect(out).to include("using resolution from the lockfile") + expect(out).not_to include("re-resolving dependencies because the list of sources changed") + end + + it "updates specific gems using --update using the locked revision of unrelated git gems for resolving" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + ref = build_git("foo").ref_for("HEAD") + + gemfile <<-G + source "https://gem.repo1" + gem "rake" + gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "deadbeef" + G + + lockfile <<~L + GIT + remote: #{lib_path("foo-1.0")} + revision: #{ref} + branch: deadbeef + specs: + foo (1.0) + + GEM + remote: https://gem.repo1/ + specs: + rake (10.0.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + rake + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --update rake --verbose" + expect(out).to match(/Writing lockfile to.+lock/) + expect(lockfile).to include("rake (#{rake_version})") end it "errors when updating a missing specific gems using --update" do - lockfile @lockfile + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile expected_lockfile - bundle "lock --update blahblah" - expect(out).to eq("Could not find gem 'blahblah'.") + bundle "lock --update blahblah", raise_on_error: false + expect(err).to eq("Could not find gem 'blahblah'.") - expect(read_lockfile).to eq(@lockfile) + expect(read_lockfile).to eq(expected_lockfile) + end + + it "can lock without downloading gems" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + gemfile <<-G + source "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 @@ -120,22 +688,25 @@ RSpec.describe "bundle lock" do context "conservative updates" do before do build_repo4 do - build_gem "foo", %w(1.4.3 1.4.4) do |s| + 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| + 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| + build_gem "foo", %w[1.5.1] do |s| s.add_dependency "bar", "~> 3.0" end - build_gem "bar", %w(2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0) - build_gem "qux", %w(1.0.0 1.0.1 1.1.0 2.0.0) + build_gem "foo", %w[2.0.0.pre] do |s| + s.add_dependency "bar" + end + build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 2.1.2.pre 3.0.0 3.1.0.pre 4.0.0.pre] + build_gem "qux", %w[1.0.0 1.0.1 1.1.0 2.0.0] end # establish a lockfile set to 1.4.3 install_gemfile <<-G - source "file://#{gem_repo4}" + source "https://gem.repo4" gem 'foo', '1.4.3' gem 'bar', '2.0.3' gem 'qux', '1.0.0' @@ -144,66 +715,340 @@ RSpec.describe "bundle lock" do # remove 1.4.3 requirement and bar altogether # to setup update specs below gemfile <<-G - source "file://#{gem_repo4}" + source "https://gem.repo4" gem 'foo' gem 'qux' G + + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) end it "single gem updates dependent gem to minor" do bundle "lock --update foo --patch" - expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w(foo-1.4.5 bar-2.1.1 qux-1.0.0).sort) + expect(the_bundle.locked_specs).to eq(%w[foo-1.4.5 bar-2.1.1 qux-1.0.0].sort) end it "minor preferred with strict" do bundle "lock --update --minor --strict" - expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w(foo-1.5.0 bar-2.1.1 qux-1.1.0).sort) + expect(the_bundle.locked_specs).to eq(%w[foo-1.5.0 bar-2.1.1 qux-1.1.0].sort) + end + + it "shows proper error when Gemfile changes forbid patch upgrades, and --patch --strict is given" do + # force next minor via Gemfile + gemfile <<-G + source "https://gem.repo4" + gem 'foo', '1.5.0' + gem 'qux' + G + + bundle "lock --update foo --patch --strict", raise_on_error: false + + expect(err).to include( + "foo is locked to 1.4.3, while Gemfile is requesting foo (= 1.5.0). " \ + "--strict --patch was specified, but there are no patch level upgrades from 1.4.3 satisfying foo (= 1.5.0), so version solving has failed" + ) + end + + context "pre" do + it "defaults to major" do + bundle "lock --update --pre" + + expect(the_bundle.locked_specs).to eq(%w[foo-2.0.0.pre bar-4.0.0.pre qux-2.0.0].sort) + end + + it "patch preferred" do + bundle "lock --update --patch --pre" + + expect(the_bundle.locked_specs).to eq(%w[foo-1.4.5 bar-2.1.2.pre qux-1.0.1].sort) + end + + it "minor preferred" do + bundle "lock --update --minor --pre" + + expect(the_bundle.locked_specs).to eq(%w[foo-1.5.1 bar-3.1.0.pre qux-1.1.0].sort) + end + + it "major preferred" do + bundle "lock --update --major --pre" + + expect(the_bundle.locked_specs).to eq(%w[foo-2.0.0.pre bar-4.0.0.pre qux-2.0.0].sort) + end + end + end + + 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" do - bundle! "lock --add-platform java x86-mingw32" + it "supports adding new platforms with force_ruby_platform = true" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + lockfile <<-L + GEM + remote: https://gem.repo1/ + specs: + platform_specific (1.0) + platform_specific (1.0-x86-64_linux) + + PLATFORMS + ruby + x86_64-linux - lockfile = Bundler::LockfileParser.new(read_lockfile) - expect(lockfile.platforms).to eq([java, local, mingw]) + DEPENDENCIES + platform_specific + L + + bundle_config "force_ruby_platform true" + bundle "lock --add-platform java x86-mingw32" + + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + expect(the_bundle.locked_platforms).to contain_exactly(Gem::Platform::RUBY, "x86_64-linux", "java", "x86-mingw32") end it "supports adding the `ruby` platform" do - bundle! "lock --add-platform ruby" - lockfile = Bundler::LockfileParser.new(read_lockfile) - expect(lockfile.platforms).to eq([local, "ruby"].uniq) + gemfile_with_rails_weakling_and_foo_from_repo4 + + bundle "lock --add-platform ruby" + + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + expect(the_bundle.locked_platforms).to match_array(default_platform_list("ruby")) end - it "warns when adding an unknown platform" do - bundle "lock --add-platform foobarbaz" - expect(out).to include("The platform `foobarbaz` is unknown to RubyGems and adding it will likely lead to resolution errors") + it "fails when adding an unknown platform" do + gemfile_with_rails_weakling_and_foo_from_repo4 + + bundle "lock --add-platform foobarbaz", raise_on_error: false + expect(err).to include("The platform `foobarbaz` is unknown to RubyGems and can't be added to the lockfile") + expect(last_command).to be_failure end it "allows removing platforms" do - bundle! "lock --add-platform java x86-mingw32" + gemfile_with_rails_weakling_and_foo_from_repo4 + + bundle "lock --add-platform java x86-mingw32" + + 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 = Bundler::LockfileParser.new(read_lockfile) - expect(lockfile.platforms).to eq([java, local, mingw]) + 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 - bundle! "lock --remove-platform java" + checksums.delete("nokogiri", Gem::Platform::RUBY) - lockfile = Bundler::LockfileParser.new(read_lockfile) - expect(lockfile.platforms).to eq([local, mingw]) + simulate_platform "x86_64-darwin-22" do + bundle "lock --remove-platform ruby" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.12.0-x86_64-darwin) + + PLATFORMS + x86_64-darwin + + DEPENDENCIES + nokogiri + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L end it "errors when removing all platforms" do - bundle "lock --remove-platform #{local}" - expect(out).to include("Removing all platforms from the bundle is not allowed") + gemfile_with_rails_weakling_and_foo_from_repo4 + + bundle "lock --remove-platform #{local_platform}", raise_on_error: false + expect(err).to include("Removing all platforms from the bundle is not allowed") end - # from https://github.com/bundler/bundler/issues/4896 + # from https://github.com/rubygems/bundler/issues/4896 it "properly adds platforms when platform requirements come from different dependencies" do build_repo4 do build_gem "ffi", "1.9.14" build_gem "ffi", "1.9.14" do |s| - s.platform = mingw + s.platform = "x86-mingw32" end build_gem "gssapi", "0.1" @@ -221,7 +1066,7 @@ RSpec.describe "bundle lock" do # 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| + %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 @@ -229,17 +1074,24 @@ RSpec.describe "bundle lock" do end gemfile <<-G - source "file:#{gem_repo4}" + source "https://gem.repo4" gem "mixlib-shellout" gem "gssapi" G - simulate_platform(mingw) { bundle! :lock } + simulate_platform("x86-mingw32") { bundle :lock } - expect(the_bundle.lockfile).to read_as(strip_whitespace(<<-G)) + 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: file:#{gem_repo4}/ + remote: https://gem.repo4/ specs: ffi (1.9.14-x86-mingw32) gssapi (1.2.0) @@ -255,16 +1107,20 @@ RSpec.describe "bundle lock" do DEPENDENCIES gssapi mixlib-shellout - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G - simulate_platform(rb) { bundle! :lock } + bundle_config "force_ruby_platform true" + bundle :lock - expect(the_bundle.lockfile).to read_as(strip_whitespace(<<-G)) + checksums.checksum gem_repo4, "ffi", "1.9.14" + checksums.checksum gem_repo4, "mixlib-shellout", "2.2.6" + + expect(lockfile).to eq <<~G GEM - remote: file:#{gem_repo4}/ + remote: https://gem.repo4/ specs: ffi (1.9.14) ffi (1.9.14-x86-mingw32) @@ -283,33 +1139,1739 @@ RSpec.describe "bundle lock" do DEPENDENCIES gssapi mixlib-shellout + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "doesn't crash when an update candidate doesn't have any matching platform" do + build_repo4 do + build_gem "libv8", "8.4.255.0" + build_gem "libv8", "8.4.255.0" do |s| + s.platform = "x86_64-darwin-19" + end + + build_gem "libv8", "15.0.71.48.1beta2" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "libv8" + G + + lockfile <<-G + GEM + remote: https://gem.repo4/ + specs: + libv8 (8.4.255.0) + libv8 (8.4.255.0-x86_64-darwin-19) + + PLATFORMS + ruby + x86_64-darwin-19 + + DEPENDENCIES + libv8 BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} G + + simulate_platform("x86_64-darwin-19") { bundle "lock --update" } + + expect(out).to match(/Writing lockfile to.+Gemfile\.lock/) end - context "when an update is available" do - let(:repo) { gem_repo2 } + it "adds all more specific candidates when they all have the same dependencies" do + build_repo4 do + build_gem "libv8", "8.4.255.0" do |s| + s.platform = "x86_64-darwin-19" + end + + build_gem "libv8", "8.4.255.0" do |s| + s.platform = "x86_64-darwin-20" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "libv8" + G + + simulate_platform("x86_64-darwin-19") { bundle "lock" } + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-19" + c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-20" + end + + expect(lockfile).to eq <<~G + GEM + remote: https://gem.repo4/ + specs: + libv8 (8.4.255.0-x86_64-darwin-19) + libv8 (8.4.255.0-x86_64-darwin-20) + + PLATFORMS + x86_64-darwin-19 + x86_64-darwin-20 + + DEPENDENCIES + libv8 + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "respects the previous lockfile if it had a matching less specific platform already locked, and installs the best variant for each platform" do + build_repo4 do + build_gem "libv8", "8.4.255.0" do |s| + s.platform = "x86_64-darwin-19" + end + + build_gem "libv8", "8.4.255.0" do |s| + s.platform = "x86_64-darwin-20" + end + end + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-19" + c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-20" + end + + gemfile <<-G + source "https://gem.repo4" + + gem "libv8" + G + + lockfile <<-G + GEM + remote: https://gem.repo4/ + specs: + libv8 (8.4.255.0-x86_64-darwin-19) + libv8 (8.4.255.0-x86_64-darwin-20) + + PLATFORMS + x86_64-darwin + + DEPENDENCIES + libv8 + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G + + previous_lockfile = lockfile + + %w[x86_64-darwin-19 x86_64-darwin-20].each do |platform| + simulate_platform(platform) do + bundle "lock" + expect(lockfile).to eq(previous_lockfile) + + bundle "install" + expect(the_bundle).to include_gem("libv8 8.4.255.0 #{platform}") + end + end + end + + it "does not conflict on ruby requirements when adding new platforms" do + build_repo4 do + build_gem "raygun-apm", "1.0.78" do |s| + s.platform = "x86_64-linux" + s.required_ruby_version = "< #{next_ruby_minor}.dev" + end + + build_gem "raygun-apm", "1.0.78" do |s| + s.platform = "universal-darwin" + s.required_ruby_version = "< #{next_ruby_minor}.dev" + end + + build_gem "raygun-apm", "1.0.78" do |s| + s.platform = "x64-mingw-ucrt" + s.required_ruby_version = "< #{next_ruby_minor}.dev" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "raygun-apm" + G + + lockfile <<-L + GEM + remote: https://gem.repo4/ + specs: + raygun-apm (1.0.78-universal-darwin) + + PLATFORMS + x86_64-darwin-19 + + DEPENDENCIES + raygun-apm + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --add-platform x86_64-linux" + end + + it "adds platform specific gems as necessary, even when adding the current platform" do + build_repo4 do + build_gem "nokogiri", "1.16.0" + + build_gem "nokogiri", "1.16.0" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "nokogiri" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.16.0) + + PLATFORMS + ruby + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle "lock --add-platform x86_64-linux" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.16.0) + nokogiri (1.16.0-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "refuses to add platforms incompatible with the lockfile" do + build_repo4 do + build_gem "sorbet-static", "0.5.11989" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet-static" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet-static (0.5.11989-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + sorbet-static + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle "lock --add-platform ruby", raise_on_error: false + end + nice_error = <<~E.strip + Could not find gems matching 'sorbet-static' valid for all resolution platforms (x86_64-linux, ruby) in rubygems repository https://gem.repo4/ or installed locally. + + The source contains the following gems matching 'sorbet-static': + * sorbet-static-0.5.11989-x86_64-linux + E + expect(err).to include(nice_error) + end + + it "respects lower bound ruby requirements" do + build_repo4 do + build_gem "our_private_gem", "0.1.0" do |s| + s.required_ruby_version = ">= #{Gem.ruby_version}" + end + end + + gemfile <<-G + source "https://localgemserver.test" + + gem "our_private_gem" + G + + lockfile <<-L + GEM + remote: https://localgemserver.test/ + specs: + our_private_gem (0.1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + our_private_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + end + + context "when an update is available" do before do - lockfile(@lockfile) - build_repo2 do + gemfile_with_rails_weakling_and_foo_from_repo4 + + build_repo4 do build_gem "foo", "2.0" end + + lockfile(expected_lockfile) end it "does not implicitly update" do - bundle! "lock" + bundle "lock" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "actionmailer", "2.3.2" + c.checksum gem_repo4, "actionpack", "2.3.2" + c.checksum gem_repo4, "activerecord", "2.3.2" + c.checksum gem_repo4, "activeresource", "2.3.2" + c.checksum gem_repo4, "activesupport", "2.3.2" + c.checksum gem_repo4, "foo", "1.0" + c.checksum gem_repo4, "rails", "2.3.2" + c.checksum gem_repo4, "rake", rake_version + c.checksum gem_repo4, "weakling", "0.0.3" + end - expect(read_lockfile).to eq(@lockfile) + expected_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + actionmailer (2.3.2) + activesupport (= 2.3.2) + actionpack (2.3.2) + activesupport (= 2.3.2) + activerecord (2.3.2) + activesupport (= 2.3.2) + activeresource (2.3.2) + activesupport (= 2.3.2) + activesupport (2.3.2) + foo (1.0) + rails (2.3.2) + actionmailer (= 2.3.2) + actionpack (= 2.3.2) + activerecord (= 2.3.2) + activeresource (= 2.3.2) + rake (= #{rake_version}) + rake (#{rake_version}) + weakling (0.0.3) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo + rails + weakling + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + expect(read_lockfile).to eq(expected_lockfile) end it "accounts for changes in the gemfile" do gemfile gemfile.gsub('"foo"', '"foo", "2.0"') - bundle! "lock" + bundle "lock" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "actionmailer", "2.3.2" + c.checksum gem_repo4, "actionpack", "2.3.2" + c.checksum gem_repo4, "activerecord", "2.3.2" + c.checksum gem_repo4, "activeresource", "2.3.2" + c.checksum gem_repo4, "activesupport", "2.3.2" + c.checksum gem_repo4, "foo", "2.0" + c.checksum gem_repo4, "rails", "2.3.2" + c.checksum gem_repo4, "rake", rake_version + c.checksum gem_repo4, "weakling", "0.0.3" + end + + expected_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + actionmailer (2.3.2) + activesupport (= 2.3.2) + actionpack (2.3.2) + activesupport (= 2.3.2) + activerecord (2.3.2) + activesupport (= 2.3.2) + activeresource (2.3.2) + activesupport (= 2.3.2) + activesupport (2.3.2) + foo (2.0) + rails (2.3.2) + actionmailer (= 2.3.2) + actionpack (= 2.3.2) + activerecord (= 2.3.2) + activeresource (= 2.3.2) + rake (= #{rake_version}) + rake (#{rake_version}) + weakling (0.0.3) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo (= 2.0) + rails + weakling + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + expect(read_lockfile).to eq(expected_lockfile) + end + end + + context "when a system gem has incorrect dependencies, different from the lockfile" do + before do + build_repo4 do + build_gem "debug", "1.6.3" do |s| + s.add_dependency "irb", ">= 1.3.6" + end + + build_gem "irb", "1.5.0" + end + + system_gems "irb-1.5.0", gem_repo: gem_repo4 + system_gems "debug-1.6.3", gem_repo: gem_repo4 + + # simulate gemspec with wrong empty dependencies + debug_gemspec_path = system_gem_path("specifications/debug-1.6.3.gemspec") + debug_gemspec = Gem::Specification.load(debug_gemspec_path.to_s) + debug_gemspec.dependencies.clear + File.write(debug_gemspec_path, debug_gemspec.to_ruby) + end + + it "respects the existing lockfile, even when reresolving" do + gemfile <<~G + source "https://gem.repo4" + + gem "debug" + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "debug", "1.6.3" + c.checksum gem_repo4, "irb", "1.5.0" + end + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + debug (1.6.3) + irb (>= 1.3.6) + irb (1.5.0) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + debug + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "arm64-darwin-22" do + bundle "lock" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + debug (1.6.3) + irb (>= 1.3.6) + irb (1.5.0) + + PLATFORMS + arm64-darwin-22 + x86_64-linux + + DEPENDENCIES + debug + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when a system gem has incorrect dependencies, different from remote gems" do + before do + build_repo4 do + build_gem "foo", "1.0.0" do |s| + s.add_dependency "bar" + end + + build_gem "bar", "1.0.0" + end + + system_gems "foo-1.0.0", gem_repo: gem_repo4, path: default_bundle_path + + # simulate gemspec with wrong empty dependencies + foo_gemspec_path = default_bundle_path("specifications/foo-1.0.0.gemspec") + foo_gemspec = Gem::Specification.load(foo_gemspec_path.to_s) + foo_gemspec.dependencies.clear + File.write(foo_gemspec_path, foo_gemspec.to_ruby) + end + + it "generates a lockfile using remote dependencies, and prints a warning" do + gemfile <<~G + source "https://gem.repo4" + + gem "foo" + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "foo", "1.0.0" + c.checksum gem_repo4, "bar", "1.0.0" + end + + simulate_platform "x86_64-linux" do + bundle "lock --verbose" + end + + expect(err).to eq("Local specification for foo-1.0.0 has different dependencies than the remote gem, ignoring it") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + bar (1.0.0) + foo (1.0.0) + bar + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + foo + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "properly shows resolution errors including OR requirements" do + build_repo4 do + build_gem "activeadmin", "2.13.1" do |s| + s.add_dependency "railties", ">= 6.1", "< 7.1" + end + build_gem "actionpack", "6.1.4" + build_gem "actionpack", "7.0.3.1" + build_gem "actionpack", "7.0.4" + build_gem "railties", "6.1.4" do |s| + s.add_dependency "actionpack", "6.1.4" + end + build_gem "rails", "7.0.3.1" do |s| + s.add_dependency "railties", "7.0.3.1" + end + build_gem "rails", "7.0.4" do |s| + s.add_dependency "railties", "7.0.4" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "rails", ">= 7.0.3.1" + gem "activeadmin", "2.13.1" + G + + bundle "lock", raise_on_error: false + + expect(err).to eq <<~ERR.strip + Could not find compatible versions + + Because rails >= 7.0.4 depends on railties = 7.0.4 + and rails < 7.0.4 depends on railties = 7.0.3.1, + railties = 7.0.3.1 OR = 7.0.4 is required. + So, because railties = 7.0.3.1 OR = 7.0.4 could not be found in rubygems repository https://gem.repo4/ or installed locally, + version solving has failed. + ERR + end + + it "is able to display some explanation on crazy irresolvable cases" do + build_repo4 do + build_gem "activeadmin", "2.13.1" do |s| + s.add_dependency "ransack", "= 3.1.0" + end + + # Activemodel is missing as a dependency in lockfile + build_gem "ransack", "3.1.0" do |s| + s.add_dependency "activemodel", ">= 6.0.4" + s.add_dependency "activesupport", ">= 6.0.4" + end + + %w[6.0.4 7.0.2.3 7.0.3.1 7.0.4].each do |version| + build_gem "activesupport", version + + # Activemodel is only available on 6.0.4 + if version == "6.0.4" + build_gem "activemodel", version do |s| + s.add_dependency "activesupport", version + end + end + + build_gem "rails", version do |s| + # Depednencies of Rails 7.0.2.3 are in reverse order + if version == "7.0.2.3" + s.add_dependency "activesupport", version + s.add_dependency "activemodel", version + else + s.add_dependency "activemodel", version + s.add_dependency "activesupport", version + end + end + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "rails", ">= 7.0.2.3" + gem "activeadmin", "= 2.13.1" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + activeadmin (2.13.1) + ransack (= 3.1.0) + ransack (3.1.0) + activemodel (>= 6.0.4) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + activeadmin (= 2.13.1) + ransack (= 3.1.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + expected_error = <<~ERR.strip + Could not find compatible versions + + Because rails >= 7.0.4 depends on activemodel = 7.0.4 + and rails >= 7.0.3.1, < 7.0.4 depends on activemodel = 7.0.3.1, + rails >= 7.0.3.1 requires activemodel = 7.0.3.1 OR = 7.0.4. + (1) So, because rails >= 7.0.2.3, < 7.0.3.1 depends on activemodel = 7.0.2.3 + and every version of activemodel depends on activesupport = 6.0.4, + rails >= 7.0.2.3 requires activesupport = 6.0.4. + + Because rails >= 7.0.2.3, < 7.0.3.1 depends on activesupport = 7.0.2.3 + and rails >= 7.0.3.1, < 7.0.4 depends on activesupport = 7.0.3.1, + rails >= 7.0.2.3, < 7.0.4 requires activesupport = 7.0.2.3 OR = 7.0.3.1. + And because rails >= 7.0.4 depends on activesupport = 7.0.4, + rails >= 7.0.2.3 requires activesupport = 7.0.2.3 OR = 7.0.3.1 OR = 7.0.4. + And because rails >= 7.0.2.3 requires activesupport = 6.0.4 (1), + rails >= 7.0.2.3 cannot be used. + So, because Gemfile depends on rails >= 7.0.2.3, + version solving has failed. + ERR + + bundle "lock", raise_on_error: false + expect(err).to eq(expected_error) + + lockfile lockfile.gsub(/PLATFORMS\n #{local_platform}/m, "PLATFORMS\n #{lockfile_platforms("ruby")}") + + bundle "lock", raise_on_error: false + expect(err).to eq(expected_error) + end + + it "does not accidentally resolves to prereleases" do + build_repo4 do + build_gem "autoproj", "2.0.3" do |s| + s.add_dependency "autobuild", ">= 1.10.0.a" + s.add_dependency "tty-prompt" + end + + build_gem "tty-prompt", "0.6.0" + build_gem "tty-prompt", "0.7.0" + + build_gem "autobuild", "1.10.0.b3" + build_gem "autobuild", "1.10.1" do |s| + s.add_dependency "tty-prompt", "~> 0.6.0" + end + end + + gemfile <<~G + source "https://gem.repo4" + gem "autoproj", ">= 2.0.0" + G + + bundle "lock" + expect(lockfile).to_not include("autobuild (1.10.0.b3)") + expect(lockfile).to include("autobuild (1.10.1)") + end + + # Newer rails depends on Bundler, while ancient Rails does not. Bundler tries + # a first resolution pass that does not consider pre-releases. However, when + # using a pre-release Bundler (like the .dev version), that results in that + # pre-release being ignored and resolving to a version that does not depend on + # Bundler at all. We should avoid that and still consider .dev Bundler. + # + it "does not ignore prereleases with there's only one candidate" do + build_repo4 do + build_gem "rails", "7.4.0.2" do |s| + s.add_dependency "bundler", ">= 1.15.0" + end + + build_gem "rails", "2.3.18" + end + + gemfile <<~G + source "https://gem.repo4" + gem "rails" + G + + bundle "lock" + expect(lockfile).to_not include("rails (2.3.18)") + expect(lockfile).to include("rails (7.4.0.2)") + end + + it "deals with platform specific incompatibilities" do + build_repo4 do + build_gem "activerecord", "6.0.6" + build_gem "activerecord-jdbc-adapter", "60.4" do |s| + s.platform = "java" + s.add_dependency "activerecord", "~> 6.0.0" + end + build_gem "activerecord-jdbc-adapter", "61.0" do |s| + s.platform = "java" + s.add_dependency "activerecord", "~> 6.1.0" + end + end + + gemfile <<~G + source "https://gem.repo4" + gem "activerecord", "6.0.6" + gem "activerecord-jdbc-adapter", "61.0" + G + + simulate_platform "universal-java-19" do + bundle "lock", raise_on_error: false + end + + expect(err).to include("Could not find compatible versions") + expect(err).not_to include("ERROR REPORT TEMPLATE") + end + + it "adds checksums to an existing lockfile, when re-resolving is necessary" do + build_repo4 do + build_gem "nokogiri", "1.14.2" + build_gem "nokogiri", "1.14.2" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "nokogiri" + G + + # lockfile has a typo (nogokiri) in the dependencies section, so Bundler + # sees dependencies have changed, and re-resolves + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nogokiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle "lock --add-checksums" + end + + checksums = checksums_section do |c| + c.checksum gem_repo4, "nokogiri", "1.14.2" + c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "adds checksums to an existing lockfile, when no re-resolve is necessary" do + build_repo4 do + build_gem "nokogiri", "1.14.2" + build_gem "nokogiri", "1.14.2" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "nokogiri" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle "lock --add-checksums" + end + + checksums = checksums_section do |c| + c.checksum gem_repo4, "nokogiri", "1.14.2" + c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "adds checksums when source is not specified" do + system_gems(%w[myrack-1.0.0], path: default_bundle_path) + + gemfile <<-G + gem "myrack" + G + + lockfile <<~L + GEM + specs: + myrack (1.0.0) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + myrack + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle "lock --add-checksums" + end + + # myrack is coming from gem_repo1 + # but it's simulated to install in the system gems path + checksums = checksums_section do |c| + c.checksum gem_repo1, "myrack", "1.0.0" + end + + expect(lockfile).to eq <<~L + GEM + specs: + myrack (1.0.0) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "adds checksums to an existing lockfile, when gems are already installed" do + build_repo4 do + build_gem "nokogiri", "1.14.2" + build_gem "nokogiri", "1.14.2" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<-G + source "https://gem.repo4" + + gem "nokogiri" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle "install" + + bundle "lock --add-checksums" + end + + checksums = checksums_section do |c| + c.checksum gem_repo4, "nokogiri", "1.14.2" + c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "generates checksums by default" do + build_repo4 do + build_gem "nokogiri", "1.14.2" + build_gem "nokogiri", "1.14.2" do |s| + s.platform = "x86_64-linux" + end + end + + simulate_platform "x86_64-linux" do + install_gemfile <<-G + source "https://gem.repo4" + + gem "nokogiri" + G + end + + checksums = checksums_section do |c| + c.checksum gem_repo4, "nokogiri", "1.14.2" + c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "disables checksums if configured to do so" do + build_repo4 do + build_gem "nokogiri", "1.14.2" + build_gem "nokogiri", "1.14.2" do |s| + s.platform = "x86_64-linux" + end + end + + bundle_config "lockfile_checksums false" + + simulate_platform "x86_64-linux" do + install_gemfile <<-G + source "https://gem.repo4" + + gem "nokogiri" + G + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "add checksums for gems installed on disk" do + build_repo4 do + build_gem "warning", "18.0.0" + end + + bundle_config "lockfile_checksums false" + + simulate_platform "x86_64-linux" do + install_gemfile(<<-G, artifice: "endpoint") + source "https://gem.repo4" + + gem "warning" + G + + bundle "config --delete lockfile_checksums" + bundle("lock --add-checksums", artifice: "endpoint") + end + + checksums = checksums_section do |c| + c.checksum gem_repo4, "warning", "18.0.0" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + warning (18.0.0) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + warning + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "doesn't add checksum for gems not installed on disk" do + lockfile(<<~L) + GEM + remote: https://gem.repo4/ + specs: + warning (18.0.0) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + warning + + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile(<<~G) + source "https://gem.repo4" + + gem "warning" + G + + build_repo4 do + build_gem "warning", "18.0.0" + end + + FileUtils.rm_rf("#{gem_repo4}/gems") + + bundle("lock --add-checksums", artifice: "endpoint") + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "warning", "18.0.0" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + warning (18.0.0) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + warning + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + context "when re-resolving to include prereleases" do + before do + build_repo4 do + build_gem "tzinfo-data", "1.2022.7" + build_gem "rails", "7.1.0.alpha" do |s| + s.add_dependency "activesupport" + end + build_gem "activesupport", "7.1.0.alpha" + end + end + + it "does not end up including gems scoped to other platforms in the lockfile" do + gemfile <<-G + source "https://gem.repo4" + gem "rails" + gem "tzinfo-data", platform: :windows + G + + simulate_platform "x86_64-darwin-22" do + bundle "lock" + end + + expect(lockfile).not_to include("tzinfo-data (1.2022.7)") + end + end + + context "when resolving platform specific gems as indirect dependencies on truffleruby", :truffleruby_only do + before do + build_lib "foo", path: bundled_app do |s| + s.add_dependency "nokogiri" + end + + build_repo4 do + build_gem "nokogiri", "1.14.2" + build_gem "nokogiri", "1.14.2" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<-G + source "https://gem.repo4" + gemspec + G + end + + it "locks both ruby and platform specific specs" do + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo4, "nokogiri", "1.14.2" + c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux" + end + + simulate_platform "x86_64-linux" do + bundle "lock" + end + + expect(lockfile).to eq <<~L + PATH + remote: . + specs: + foo (1.0) + nokogiri + + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + context "and a lockfile with platform specific gems only already exists" do + before do + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux" + end + + lockfile <<~L + PATH + remote: . + specs: + foo (1.0) + nokogiri + + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "keeps platform specific gems" do + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo4, "nokogiri", "1.14.2" + c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux" + end + + simulate_platform "x86_64-linux" do + bundle "install" + end + + expect(lockfile).to eq <<~L + PATH + remote: . + specs: + foo (1.0) + nokogiri + + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.2) + nokogiri (1.14.2-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + end + + context "when adding a new gem that requires unlocking other transitive deps" do + before do + build_repo4 do + build_gem "govuk_app_config", "0.1.0" + + build_gem "govuk_app_config", "4.13.0" do |s| + s.add_dependency "railties", ">= 5.0" + end + + %w[7.0.4.1 7.0.4.3].each do |v| + build_gem "railties", v do |s| + s.add_dependency "actionpack", v + s.add_dependency "activesupport", v + end + + build_gem "activesupport", v + build_gem "actionpack", v + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "govuk_app_config" + gem "activesupport", "7.0.4.3" + G + + # Simulate out of sync lockfile because top level dependency on + # activesuport has just been added to the Gemfile, and locked to a higher + # version + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + actionpack (7.0.4.1) + activesupport (7.0.4.1) + govuk_app_config (4.13.0) + railties (>= 5.0) + railties (7.0.4.1) + actionpack (= 7.0.4.1) + activesupport (= 7.0.4.1) + + PLATFORMS + arm64-darwin-22 + + DEPENDENCIES + govuk_app_config + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "does not downgrade top level dependencies" do + checksums = checksums_section_when_enabled do |c| + c.no_checksum "actionpack", "7.0.4.3" + c.no_checksum "activesupport", "7.0.4.3" + c.no_checksum "govuk_app_config", "4.13.0" + c.no_checksum "railties", "7.0.4.3" + end + + simulate_platform "arm64-darwin-22" do + bundle "lock" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + actionpack (7.0.4.3) + activesupport (7.0.4.3) + govuk_app_config (4.13.0) + railties (>= 5.0) + railties (7.0.4.3) + actionpack (= 7.0.4.3) + activesupport (= 7.0.4.3) + + PLATFORMS + arm64-darwin-22 + + DEPENDENCIES + activesupport (= 7.0.4.3) + govuk_app_config + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when lockfile has incorrectly indented platforms" do + before do + build_repo4 do + build_gem "ffi", "1.1.0" do |s| + s.platform = "x86_64-linux" + end + + build_gem "ffi", "1.1.0" do |s| + s.platform = "arm64-darwin" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "ffi" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + ffi (1.1.0-arm64-darwin) + + PLATFORMS + arm64-darwin + + DEPENDENCIES + ffi + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "does not remove any gems" do + simulate_platform "x86_64-linux" do + bundle "lock --update" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + ffi (1.1.0-arm64-darwin) + ffi (1.1.0-x86_64-linux) + + PLATFORMS + arm64-darwin + x86_64-linux + + DEPENDENCIES + ffi + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + describe "--normalize-platforms on linux" do + let(:normalized_lockfile) do + <<~L + GEM + remote: https://gem.repo4/ + specs: + irb (1.0.0) + irb (1.0.0-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + irb + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + before do + build_repo4 do + build_gem "irb", "1.0.0" + + build_gem "irb", "1.0.0" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "irb" + G + end + + context "when already normalized" do + before do + lockfile normalized_lockfile + end + + it "is a noop" do + simulate_platform "x86_64-linux" do + bundle "lock --normalize-platforms" + end + + expect(lockfile).to eq(normalized_lockfile) + end + end + + context "when not already normalized" do + before do + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + irb (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + irb + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "normalizes the list of platforms and native gems in the lockfile" do + simulate_platform "x86_64-linux" do + bundle "lock --normalize-platforms" + end + + expect(lockfile).to eq(normalized_lockfile) + end + end + end + + describe "--normalize-platforms on darwin" do + let(:normalized_lockfile) do + <<~L + GEM + remote: https://gem.repo4/ + specs: + irb (1.0.0) + irb (1.0.0-arm64-darwin) + + PLATFORMS + arm64-darwin + ruby + + DEPENDENCIES + irb + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + before do + build_repo4 do + build_gem "irb", "1.0.0" + + build_gem "irb", "1.0.0" do |s| + s.platform = "arm64-darwin" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "irb" + G + end + + context "when already normalized" do + before do + lockfile normalized_lockfile + end + + it "is a noop" do + simulate_platform "arm64-darwin-23" do + bundle "lock --normalize-platforms" + end + + expect(lockfile).to eq(normalized_lockfile) + end + end + + context "when having only ruby" do + before do + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + irb (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + irb + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "normalizes the list of platforms and native gems in the lockfile" do + simulate_platform "arm64-darwin-23" do + bundle "lock --normalize-platforms" + end + + expect(lockfile).to eq(normalized_lockfile) + end + end + + context "when having only the current platform with version" do + before do + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + irb (1.0.0-arm64-darwin) + + PLATFORMS + arm64-darwin-23 + + DEPENDENCIES + irb + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "normalizes the list of platforms by removing version" do + simulate_platform "arm64-darwin-23" do + bundle "lock --normalize-platforms" + end + + expect(lockfile).to eq(normalized_lockfile) + end + end + + context "when having other platforms with version" do + before do + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + irb (1.0.0-arm64-darwin) + + PLATFORMS + arm64-darwin-22 + + DEPENDENCIES + irb + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "normalizes the list of platforms by removing version" do + simulate_platform "arm64-darwin-23" do + bundle "lock --normalize-platforms" + end + + expect(lockfile).to eq(normalized_lockfile) + end + end + end + + describe "--normalize-platforms with gems without generic variant" do + let(:original_lockfile) do + <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet-static (1.0-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + sorbet-static + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + before do + build_repo4 do + build_gem "sorbet-static" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet-static" + G + + lockfile original_lockfile + end + + it "removes invalid platforms" do + simulate_platform "x86_64-linux" do + bundle "lock --normalize-platforms" + end - expect(read_lockfile).to eq(@lockfile.sub("foo (1.0)", "foo (2.0)").sub(/foo$/, "foo (= 2.0)")) + expect(lockfile).to eq(original_lockfile.gsub(/^ ruby\n/m, "")) end end end diff --git a/spec/bundler/commands/newgem_spec.rb b/spec/bundler/commands/newgem_spec.rb index e9c19005eb..65fbad05aa 100644 --- a/spec/bundler/commands/newgem_spec.rb +++ b/spec/bundler/commands/newgem_spec.rb @@ -1,180 +1,454 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle gem" do - def reset! - super - global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false" + 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 remove_push_guard(gem_name) - # Remove exception that prevents public pushes on older RubyGems versions - if Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.0") - path = "#{gem_name}/#{gem_name}.gemspec" - content = File.read(path).sub(/raise "RubyGems 2\.0 or newer.*/, "") - File.open(path, "w") {|f| f.write(content) } - end + 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 execute_bundle_gem(gem_name, flag = "", to_remove_push_guard = true) - bundle! "gem #{gem_name} #{flag}" - remove_push_guard(gem_name) if to_remove_push_guard - # reset gemspec cache for each test because of commit 3d4163a - Bundler.clear_gemspec_cache + 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 gem_skeleton_assertions(gem_name) - expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to exist - expect(bundled_app("#{gem_name}/README.md")).to exist - expect(bundled_app("#{gem_name}/Gemfile")).to exist - expect(bundled_app("#{gem_name}/Rakefile")).to exist - expect(bundled_app("#{gem_name}/lib/test/gem.rb")).to exist - expect(bundled_app("#{gem_name}/lib/test/gem/version.rb")).to exist + 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 - before do - git_config_content = <<-EOF - [user] - name = "Bundler User" - email = user@example.com - [github] - user = bundleuser - EOF - @git_config_location = ENV["GIT_CONFIG"] - path = "#{File.expand_path(tmp, File.dirname(__FILE__))}/test_git_config.txt" - File.open(path, "w") {|f| f.write(git_config_content) } - ENV["GIT_CONFIG"] = path + def installed_go? + sys_exec("go version", raise_on_error: true) + true + rescue StandardError + false end - after do - FileUtils.rm(ENV["GIT_CONFIG"]) if File.exist?(ENV["GIT_CONFIG"]) - ENV["GIT_CONFIG"] = @git_config_location + 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 - shared_examples_for "git config is present" do - context "git config user.{name,email} present" do - it "sets gemspec author to git user.name if available" do - expect(generated_gem.gemspec.authors.first).to eq("Bundler User") - end + 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 "sets gemspec email to git user.email if available" do - expect(generated_gem.gemspec.email.first).to eq("user@example.com") - 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 - end - shared_examples_for "git config is absent" do - it "sets gemspec author to default message if git user.name is not set or empty" do - expect(generated_gem.gemspec.authors.first).to eq("TODO: Write your name") + 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 - it "sets gemspec email to default message if git user.email is not set or empty" do - expect(generated_gem.gemspec.email.first).to eq("TODO: Write your email address") + 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 - execute_bundle_gem(gem_name, "--mit") + bundle "gem #{gem_name} --mit" end it "generates a gem skeleton with MIT license" do - gem_skeleton_assertions(gem_name) - expect(bundled_app("test-gem/LICENSE.txt")).to exist - skel = Bundler::GemHelper.new(bundled_app(gem_name).to_s) - expect(skel.gemspec.license).to eq("MIT") + 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 - execute_bundle_gem(gem_name, "--no-mit") + bundle "gem #{gem_name} --no-mit" end it "generates a gem skeleton without MIT license" do - gem_skeleton_assertions(gem_name) - expect(bundled_app("test-gem/LICENSE.txt")).to_not exist + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/LICENSE.txt")).to_not exist end end shared_examples_for "--coc flag" do - before do - execute_bundle_gem(gem_name, "--coc", false) - end it "generates a gem skeleton with MIT license" do - gem_skeleton_assertions(gem_name) - expect(bundled_app("test-gem/CODE_OF_CONDUCT.md")).to exist + bundle "gem #{gem_name} --coc" + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/CODE_OF_CONDUCT.md")).to exist end - describe "README additions" do - it "generates the README with a section for the Code of Conduct" do - expect(bundled_app("test-gem/README.md").read).to include("## Code of Conduct") - expect(bundled_app("test-gem/README.md").read).to include("https://github.com/bundleuser/#{gem_name}/blob/master/CODE_OF_CONDUCT.md") - end + 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 - execute_bundle_gem(gem_name, "--no-coc", false) + bundle "gem #{gem_name} --no-coc" end it "generates a gem skeleton without Code of Conduct" do - gem_skeleton_assertions(gem_name) - expect(bundled_app("test-gem/CODE_OF_CONDUCT.md")).to_not exist + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/CODE_OF_CONDUCT.md")).to_not exist end - describe "README additions" do - it "generates the README without a section for the Code of Conduct" do - expect(bundled_app("test-gem/README.md").read).not_to include("## Code of Conduct") - expect(bundled_app("test-gem/README.md").read).not_to include("https://github.com/bundleuser/#{gem_name}/blob/master/CODE_OF_CONDUCT.md") - end + 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 - context "README.md" do - let(:gem_name) { "test_gem" } - let(:generated_gem) { Bundler::GemHelper.new(bundled_app(gem_name).to_s) } + 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 - execute_bundle_gem(gem_name) + bundle "gem #{gem_name}" end it "contribute URL set to git username" do - expect(bundled_app("test_gem/README.md").read).not_to include("[USERNAME]") - expect(bundled_app("test_gem/README.md").read).to include("github.com/bundleuser") + 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 - sys_exec("git config --unset github.user") - reset! - in_app_root + git("config --global --unset github.user") bundle "gem #{gem_name}" - remove_push_guard(gem_name) end it "contribute URL set to [USERNAME]" do - expect(bundled_app("test_gem/README.md").read).to include("[USERNAME]") - expect(bundled_app("test_gem/README.md").read).not_to include("github.com/bundleuser") + 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 - in_app_root - bundle "gem test_gem" - expect(bundled_app("test_gem/.git")).to exist + bundle "gem #{gem_name}" + expect(bundled_app("#{gem_name}/.git")).to exist end - context "when git is not avaiable" do - let(:gem_name) { "test_gem" } - - # This spec cannot have `git` avaiable in the test env + context "when git is not available" do + # This spec cannot have `git` available in the test env before do - load_paths = [lib, spec] - load_path_str = "-I#{load_paths.join(File::PATH_SEPARATOR)}" - - sys_exec "PATH=\"\" #{Gem.ruby} #{load_path_str} #{bindir.join("bundle")} gem #{gem_name}" + bundle "gem #{gem_name}", env: { "PATH" => "" } end it "creates the gem without the need for git" do @@ -188,50 +462,32 @@ RSpec.describe "bundle gem" do it "doesn't create a .gitignore file" do expect(bundled_app("#{gem_name}/.gitignore")).to_not exist end + + it "does not add .gitignore into ignore list" do + expect(ignore_paths).not_to include(".gitignore") + end end it "generates a valid gemspec" do - system_gems ["rake-10.0.2"] - - in_app_root bundle "gem newgem --bin" - process_file(bundled_app("newgem", "newgem.gemspec")) do |line| - # Simulate replacing TODOs with real values - case line - when /spec\.metadata\['allowed_push_host'\]/, /spec\.homepage/ - line.gsub(/\=.*$/, "= 'http://example.org'") - when /spec\.summary/ - line.gsub(/\=.*$/, "= %q{A short summary of my new gem.}") - when /spec\.description/ - line.gsub(/\=.*$/, "= %q{A longer description of my new gem.}") - # Remove exception that prevents public pushes on older RubyGems versions - when /raise "RubyGems 2.0 or newer/ - line.gsub(/.*/, "") if Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.0") - else - line - end - end + prepare_gemspec(bundled_app("newgem", "newgem.gemspec")) - Dir.chdir(bundled_app("newgem")) do - bundle "exec rake build" + build_repo2 do + build_dummy_irb "9.9.9" end + gems = ["rake-#{rake_version}", "irb-9.9.9"] + system_gems gems, path: system_gem_path, gem_repo: gem_repo2 + bundle "exec rake build", dir: bundled_app("newgem") - expect(exitstatus).to be_zero if exitstatus - expect(out).not_to include("ERROR") - expect(err).not_to include("ERROR") + expect(stdboth).not_to include("ERROR") end context "gem naming with relative paths" do - before do - reset! - in_app_root - end - it "resolves ." do create_temporary_dir("tmp") - bundle "gem ." + bundle "gem .", dir: bundled_app("tmp") expect(bundled_app("tmp/lib/tmp.rb")).to exist end @@ -239,7 +495,7 @@ RSpec.describe "bundle gem" do it "resolves .." do create_temporary_dir("temp/empty_dir") - bundle "gem .." + bundle "gem ..", dir: bundled_app("temp/empty_dir") expect(bundled_app("temp/lib/temp.rb")).to exist end @@ -247,661 +503,1635 @@ RSpec.describe "bundle gem" do it "resolves relative directory" do create_temporary_dir("tmp/empty/tmp") - bundle "gem ../../empty" + bundle "gem ../../empty", dir: bundled_app("tmp/empty/tmp") expect(bundled_app("tmp/empty/lib/empty.rb")).to exist end def create_temporary_dir(dir) - FileUtils.mkdir_p(dir) - Dir.chdir(dir) + FileUtils.mkdir_p(bundled_app(dir)) end end - context "gem naming with underscore" do - let(:gem_name) { "test_gem" } + 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 - execute_bundle_gem(gem_name) + bundle "gem #{gem_name}" end - let(:generated_gem) { Bundler::GemHelper.new(bundled_app(gem_name).to_s) } + it "sets gemspec author to git user.name if available" do + expect(generated_gemspec.authors.first).to eq("Bundler User") + end - it "generates a gem skeleton" do - expect(bundled_app("test_gem/test_gem.gemspec")).to exist - expect(bundled_app("test_gem/Gemfile")).to exist - expect(bundled_app("test_gem/Rakefile")).to exist - expect(bundled_app("test_gem/lib/test_gem.rb")).to exist - expect(bundled_app("test_gem/lib/test_gem/version.rb")).to exist - expect(bundled_app("test_gem/.gitignore")).to exist + it "sets gemspec email to git user.email if available" do + expect(generated_gemspec.email.first).to eq("user@example.com") + end + end - expect(bundled_app("test_gem/bin/setup")).to exist - expect(bundled_app("test_gem/bin/console")).to exist - expect(bundled_app("test_gem/bin/setup")).to be_executable - expect(bundled_app("test_gem/bin/console")).to be_executable + 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 "starts with version 0.1.0" do - expect(bundled_app("test_gem/lib/test_gem/version.rb").read).to match(/VERSION = "0.1.0"/) + 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 "does not nest constants" do - expect(bundled_app("test_gem/lib/test_gem/version.rb").read).to match(/module TestGem/) - expect(bundled_app("test_gem/lib/test_gem.rb").read).to match(/module TestGem/) + 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_should_behave_like "git config is present" + it "sets gemspec metadata['allowed_push_host']" do + bundle "gem #{gem_name}" - context "git config user.{name,email} is not set" do - before do - `git config --unset user.name` - `git config --unset user.email` - reset! - in_app_root - bundle "gem #{gem_name}" - remove_push_guard(gem_name) + 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 - it_should_behave_like "git config is absent" + 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 "sets gemspec metadata['allowed_push_host']", :rubygems => "2.0" do - expect(generated_gem.gemspec.metadata["allowed_push_host"]). - to match(/mygemserver\.com/) + it "includes .rspec and spec/ into ignore list" do + expect(ignore_paths).to include(".rspec") + expect(ignore_paths).to include("spec/") end + end - it "requires the version file" do - expect(bundled_app("test_gem/lib/test_gem.rb").read).to match(%r{require "test_gem/version"}) + 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 "runs rake without problems" do - system_gems ["rake-10.0.2"] + 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 - rakefile = strip_whitespace <<-RAKEFILE - task :default do - puts 'SUCCESS' + 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 - File.open(bundled_app("test_gem/Rakefile"), "w") do |file| - file.puts rakefile - end - Dir.chdir(bundled_app(gem_name)) do - sys_exec(rake) - expect(out).to include("SUCCESS") - end + expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) end + end - context "--exe parameter set" do - before do - reset! - in_app_root - bundle "gem #{gem_name} --exe" - end + context "--test parameter set to an invalid value" do + before do + bundle "gem #{gem_name} --test=foo", raise_on_error: false + end - it "builds exe skeleton" do - expect(bundled_app("test_gem/exe/test_gem")).to exist - 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 - it "requires 'test-gem'" do - expect(bundled_app("test_gem/exe/test_gem").read).to match(/require "test_gem"/) - 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 - context "--bin parameter set" do - before do - reset! - in_app_root - bundle "gem #{gem_name} --bin" - 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 "builds exe skeleton" do - expect(bundled_app("test_gem/exe/test_gem")).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 "requires 'test-gem'" do - expect(bundled_app("test_gem/exe/test_gem").read).to match(/require "test_gem"/) - end + it "hints that --test is already configured" do + expect(out).to match("rspec is already configured, ignoring --test flag.") end + end - context "no --test parameter" do - before do - reset! - in_app_root - bundle "gem #{gem_name}" + 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 "doesn't create any spec/test file" do - expect(bundled_app("test_gem/.rspec")).to_not exist - expect(bundled_app("test_gem/spec/test_gem_spec.rb")).to_not exist - expect(bundled_app("test_gem/spec/spec_helper.rb")).to_not exist - expect(bundled_app("test_gem/test/test_test_gem.rb")).to_not exist - expect(bundled_app("test_gem/test/minitest_helper.rb")).to_not exist - end + it "asks to generate test files" do + expect(out).to match("Do you want to generate tests with your gem?") end - context "--test parameter set to rspec" do - before do - reset! - in_app_root - bundle "gem #{gem_name} --test=rspec" - 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 "builds spec skeleton" do - expect(bundled_app("test_gem/.rspec")).to exist - expect(bundled_app("test_gem/spec/test_gem_spec.rb")).to exist - expect(bundled_app("test_gem/spec/spec_helper.rb")).to exist - end + it_behaves_like "test framework is absent" + end - it "depends on a specific version of rspec", :rubygems => ">= 1.8.1" do - remove_push_guard(gem_name) - rspec_dep = generated_gem.gemspec.development_dependencies.find {|d| d.name == "rspec" } - expect(rspec_dep).to be_specific + 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 "requires 'test-gem'" do - expect(bundled_app("test_gem/spec/spec_helper.rb").read).to include(%(require "test_gem")) - end + it "asks to generate test files" do + expect(out).to match("Do you want to generate tests with your gem?") + end - it "creates a default test which fails" do - expect(bundled_app("test_gem/spec/test_gem_spec.rb").read).to include("expect(false).to eq(true)") - end + it "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 - context "gem.test setting set to rspec" do - before do - reset! - in_app_root - bundle "config gem.test rspec" - bundle "gem #{gem_name}" + 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 - it "builds spec skeleton" do - expect(bundled_app("test_gem/.rspec")).to exist - expect(bundled_app("test_gem/spec/test_gem_spec.rb")).to exist - expect(bundled_app("test_gem/spec/spec_helper.rb")).to exist + 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 - context "gem.test setting set to rspec and --test is set to minitest" do - before do - reset! - in_app_root - bundle "config gem.test rspec" - bundle "gem #{gem_name} --test=minitest" + 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 - it "builds spec skeleton" do - expect(bundled_app("test_gem/test/test_gem_test.rb")).to exist - expect(bundled_app("test_gem/test/test_helper.rb")).to exist + 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 - context "--test parameter set to minitest" do + 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 - reset! - in_app_root - bundle "gem #{gem_name} --test=minitest" + bundle "gem #{gem_name} --exe" end - it "depends on a specific version of minitest", :rubygems => ">= 1.8.1" do - remove_push_guard(gem_name) - rspec_dep = generated_gem.gemspec.development_dependencies.find {|d| d.name == "minitest" } - expect(rspec_dep).to be_specific + 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 - it "builds spec skeleton" do - expect(bundled_app("test_gem/test/test_gem_test.rb")).to exist - expect(bundled_app("test_gem/test/test_helper.rb")).to exist + context "--bin parameter set" do + before do + bundle "gem #{gem_name} --bin" end - it "requires 'test-gem'" do - expect(bundled_app("test_gem/test/test_helper.rb").read).to include(%(require "test_gem")) + 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 - it "requires 'minitest_helper'" do - expect(bundled_app("test_gem/test/test_gem_test.rb").read).to include(%(require "test_helper")) + context "--test parameter set to rspec" do + before do + bundle "gem #{gem_name} --test=rspec" end - it "creates a default test which fails" do - expect(bundled_app("test_gem/test/test_gem_test.rb").read).to include("assert false") + 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 "gem.test setting set to minitest" do + context "--test parameter set to minitest" do before do - reset! - in_app_root - bundle "config gem.test minitest" - bundle "gem #{gem_name}" + bundle "gem #{gem_name} --test=minitest" end - it "creates a default rake task to run the test suite" do - rakefile = strip_whitespace <<-RAKEFILE - require "bundler/gem_tasks" - require "rake/testtask" - - Rake::TestTask.new(:test) do |t| - t.libs << "test" - t.libs << "lib" - t.test_files = FileList["test/**/*_test.rb"] - end - - task :default => :test - RAKEFILE + 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("test_gem/Rakefile").read).to eq(rakefile) + 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 with no arguments" do + context "--test parameter set to test-unit" do before do - reset! - in_app_root - bundle "gem #{gem_name} --test" + bundle "gem #{gem_name} --test=test-unit" end - it "defaults to rspec" do - expect(bundled_app("test_gem/spec/spec_helper.rb")).to exist - expect(bundled_app("test_gem/test/minitest_helper.rb")).to_not exist + 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 - it "creates a .travis.yml file to test the library against the current Ruby version on Travis CI" do - expect(bundled_app("test_gem/.travis.yml").read).to match(/- #{RUBY_VERSION}/) - end + 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 "--edit option" do - it "opens the generated gemspec in the user's text editor" do - reset! - in_app_root - output = bundle "gem #{gem_name} --edit=echo" - gemspec_path = File.join(Dir.pwd, gem_name, "#{gem_name}.gemspec") - expect(output).to include("echo \"#{gemspec_path}\"") - end + 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 "testing --mit and --coc options against bundle config settings" do - let(:gem_name) { "test-gem" } + 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 mit option in bundle config settings set to true" do - before do - global_config "BUNDLE_GEM__MIT" => "true", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false" - end - after { reset! } - it_behaves_like "--mit flag" - it_behaves_like "--no-mit flag" + 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 mit option in bundle config settings set to false" do - it_behaves_like "--mit flag" - it_behaves_like "--no-mit flag" + 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 coc option in bundle config settings set to true" do - before do - global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "true" - end - after { reset! } - it_behaves_like "--coc flag" - it_behaves_like "--no-coc flag" + 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 coc option in bundle config settings set to false" do - it_behaves_like "--coc flag" - it_behaves_like "--no-coc flag" + 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 "gem naming with dashed" do - let(:gem_name) { "test-gem" } + 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 - execute_bundle_gem(gem_name) + 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 - let(:generated_gem) { Bundler::GemHelper.new(bundled_app(gem_name).to_s) } + 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 - it "generates a gem skeleton" do - expect(bundled_app("test-gem/test-gem.gemspec")).to exist - expect(bundled_app("test-gem/Gemfile")).to exist - expect(bundled_app("test-gem/Rakefile")).to exist - expect(bundled_app("test-gem/lib/test/gem.rb")).to exist - expect(bundled_app("test-gem/lib/test/gem/version.rb")).to exist + 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 "starts with version 0.1.0" do - expect(bundled_app("test-gem/lib/test/gem/version.rb").read).to match(/VERSION = "0.1.0"/) + it "runs bundle install" do + bundle "gem #{gem_name}" + expect(out).to include("Running bundle install in the new gem directory.") end + end - it "nests constants so they work" do - expect(bundled_app("test-gem/lib/test/gem/version.rb").read).to match(/module Test\n module Gem/) - expect(bundled_app("test-gem/lib/test/gem.rb").read).to match(/module Test\n module Gem/) + 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_should_behave_like "git config is present" + 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 "git config user.{name,email} is not set" do + context "without git config github.user set" do + before do + git("config --global --unset github.user") + end + context "with github-username option in bundle config settings set to some value" do before do - `git config --unset user.name` - `git config --unset user.email` - reset! - in_app_root - bundle "gem #{gem_name}" - remove_push_guard(gem_name) + bundle_config_global "gem.github_username different_username" end - - it_should_behave_like "git config is absent" + it_behaves_like "--github-username option", "gh_user" end - it "requires the version file" do - expect(bundled_app("test-gem/lib/test/gem.rb").read).to match(%r{require "test/gem/version"}) + it_behaves_like "github_username configuration" + + context "with github-username option in bundle config settings set to false" do + before do + bundle_config_global "gem.github_username false" + end + it_behaves_like "--github-username option", "gh_user" end - it "runs rake without problems" do - system_gems ["rake-10.0.2"] + 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" - rakefile = strip_whitespace <<-RAKEFILE - task :default do - puts 'SUCCESS' - end - RAKEFILE - File.open(bundled_app("test-gem/Rakefile"), "w") do |file| - file.puts rakefile + 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}" - Dir.chdir(bundled_app(gem_name)) do - sys_exec(rake) - expect(out).to include("SUCCESS") + 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 "--bin parameter set" do + context "with git config github.user set" do + context "with github-username option in bundle config settings set to some value" do before do - reset! - in_app_root - bundle "gem #{gem_name} --bin" + bundle_config_global "gem.github_username different_username" end + it_behaves_like "--github-username option", "gh_user" + end + + it_behaves_like "github_username configuration" - it "builds bin skeleton" do - expect(bundled_app("test-gem/exe/test-gem")).to exist + 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" - it "requires 'test/gem'" do - expect(bundled_app("test-gem/exe/test-gem").read).to match(%r{require "test/gem"}) + 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 "no --test parameter" do - before do - reset! - in_app_root + 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}" - end - it "doesn't create any spec/test file" do - expect(bundled_app("test-gem/.rspec")).to_not exist - expect(bundled_app("test-gem/spec/test/gem_spec.rb")).to_not exist - expect(bundled_app("test-gem/spec/spec_helper.rb")).to_not exist - expect(bundled_app("test-gem/test/test_test/gem.rb")).to_not exist - expect(bundled_app("test-gem/test/minitest_helper.rb")).to_not exist + 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" } - context "--test parameter set to rspec" do before do - reset! - in_app_root - bundle "gem #{gem_name} --test=rspec" + bundle ["gem", gem_name, flags].compact.join(" ") end - it "builds spec skeleton" do - expect(bundled_app("test-gem/.rspec")).to exist - expect(bundled_app("test-gem/spec/test/gem_spec.rb")).to exist - expect(bundled_app("test-gem/spec/spec_helper.rb")).to exist + 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 "requires 'test/gem'" do - expect(bundled_app("test-gem/spec/spec_helper.rb").read).to include(%(require "test/gem")) + 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 "creates a default test which fails" do - expect(bundled_app("test-gem/spec/test/gem_spec.rb").read).to include("expect(false).to eq(true)") + 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 "creates a default rake task to run the specs" do - rakefile = strip_whitespace <<-RAKEFILE + it "depends on compile task for build" do + rakefile = <<~RAKEFILE + # frozen_string_literal: true + require "bundler/gem_tasks" - require "rspec/core/rake_task" + require "rake/extensiontask" + + task build: :compile - RSpec::Core::RakeTask.new(:spec) + GEMSPEC = Gem::Specification.load("#{gem_name}.gemspec") - task :default => :spec + Rake::ExtensionTask.new("#{gem_name}", GEMSPEC) do |ext| + ext.lib_dir = "lib/#{gem_name}" + end + + task default: %i[clobber compile] RAKEFILE - expect(bundled_app("test-gem/Rakefile").read).to eq(rakefile) + expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) end end - context "--test parameter set to minitest" do + context "--ext parameter set with rust" do + let(:flags) { "--ext=rust" } + before do - reset! - in_app_root - bundle "gem #{gem_name} --test=minitest" + bundle ["gem", gem_name, flags].compact.join(" ") end - it "builds spec skeleton" do - expect(bundled_app("test-gem/test/test/gem_test.rb")).to exist - expect(bundled_app("test-gem/test/test_helper.rb")).to exist + it "is not deprecated" do + expect(err).not_to include "[DEPRECATED] Option `--ext` without explicit value is deprecated." end - it "requires 'test/gem'" do - expect(bundled_app("test-gem/test/test_helper.rb").read).to match(%r{require "test/gem"}) + 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 "requires 'test_helper'" do - expect(bundled_app("test-gem/test/test/gem_test.rb").read).to match(/require "test_helper"/) + 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 "creates a default test which fails" do - expect(bundled_app("test-gem/test/test/gem_test.rb").read).to match(/assert false/) - end + it "depends on compile task for build" do + rakefile = <<~RAKEFILE + # frozen_string_literal: true - it "creates a default rake task to run the test suite" do - rakefile = strip_whitespace <<-RAKEFILE require "bundler/gem_tasks" - require "rake/testtask" + require "rb_sys/extensiontask" + + task build: :compile - Rake::TestTask.new(:test) do |t| - t.libs << "test" - t.libs << "lib" - t.test_files = FileList["test/**/*_test.rb"] + GEMSPEC = Gem::Specification.load("#{gem_name}.gemspec") + + RbSys::ExtensionTask.new("#{gem_name}", GEMSPEC) do |ext| + ext.lib_dir = "lib/#{gem_name}" end - task :default => :test + task default: :compile RAKEFILE - expect(bundled_app("test-gem/Rakefile").read).to eq(rakefile) + expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) end - end - context "--test with no arguments" do - before do - reset! - in_app_root - bundle "gem #{gem_name} --test" + 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 - it "defaults to rspec" do - expect(bundled_app("test-gem/spec/spec_helper.rb")).to exist - expect(bundled_app("test-gem/test/minitest_helper.rb")).to_not exist + 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" do + context "--ext parameter set with go" do + let(:flags) { "--ext=go" } + before do - reset! - in_app_root - bundle "gem test_gem --ext" + 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("test_gem/ext/test_gem/extconf.rb")).to exist - expect(bundled_app("test_gem/ext/test_gem/test_gem.h")).to exist - expect(bundled_app("test_gem/ext/test_gem/test_gem.c")).to exist + 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 rake-compiler" do - expect(bundled_app("test_gem/test_gem.gemspec").read).to include('spec.add_development_dependency "rake-compiler"') + 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 "depends on compile task for build" do - rakefile = strip_whitespace <<-RAKEFILE - require "bundler/gem_tasks" - require "rake/extensiontask" + 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 - task :build => :compile + 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 - Rake::ExtensionTask.new("test_gem") do |ext| - ext.lib_dir = "lib/test_gem" - end + 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 - task :default => [:clobber, :compile, :spec] - RAKEFILE + 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" } - expect(bundled_app("test_gem/Rakefile").read).to eq(rakefile) + 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" - Bundler.clear_gemspec_cache expect(bundled_app("a--a/a--a.gemspec")).to exist end it "fails gracefully with a ." do - bundle "gem foo.gemspec" - expect(out).to end_with("Invalid gem name foo.gemspec -- `Foo.gemspec` is an invalid constant name") + 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 ^" - expect(out).to end_with("Invalid gem name ^ -- `^` is an invalid constant name") + 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'" - expect(out).to end_with("Invalid gem name foo bar -- `Foo bar` is an invalid constant name") + 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" - expect(out).to eq(<<-E.strip) + 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 GEM [OPTIONS]" +Usage: "bundle gem NAME [OPTIONS]" E end end describe "#ensure_safe_gem_name" do before do - bundle "gem #{subject}" - end - after do - Bundler.clear_gemspec_cache + bundle "gem #{subject}", raise_on_error: false end context "with an existing const name" do subject { "gem" } - it { expect(out).to include("Invalid gem name #{subject}") } + it { expect(err).to include("Invalid gem name #{subject}") } end context "with an existing hyphenated const name" do subject { "gem-specification" } - it { expect(out).to include("Invalid gem name #{subject}") } + 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(out).not_to include("Invalid gem name #{subject}") } + it { expect(err).not_to include("Invalid gem name #{subject}") } end context "ending with an existing const name" do subject { "somenewconstantname-gem" } - it { expect(out).not_to include("Invalid gem name #{subject}") } + it { expect(err).not_to include("Invalid gem name #{subject}") } end end - context "on first run" do - before do - in_app_root - end - + context "on first run", :readline do it "asks about test framework" do - global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__COC" => "false" + bundle_config_global "BUNDLE_GEM__TEST" => nil bundle "gem foobar" do |input, _, _| input.puts "rspec" end expect(bundled_app("foobar/spec/spec_helper.rb")).to exist - rakefile = strip_whitespace <<-RAKEFILE + rakefile = <<~RAKEFILE + # frozen_string_literal: true + require "bundler/gem_tasks" require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) - task :default => :spec + task default: :spec RAKEFILE expect(bundled_app("foobar/Rakefile").read).to eq(rakefile) - expect(bundled_app("foobar/foobar.gemspec").read).to include('spec.add_development_dependency "rspec"') + expect(bundled_app("foobar/Gemfile").read).to include('gem "rspec"') end - it "asks about MIT license" do - global_config "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false" + it "asks about CI service" do + bundle_config_global "BUNDLE_GEM__CI" => nil + + bundle "gem foobar" do |input, _, _| + input.puts "github" + end - bundle :config + 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" do - global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false" + it "asks about CoC just once" do + bundle_config_global "BUNDLE_GEM__COC" => nil bundle "gem foobar" do |input, _, _| input.puts "yes" end expect(bundled_app("foobar/CODE_OF_CONDUCT.md")).to exist + expect(out).to include("Codes of conduct can increase contributions to your project").once + end + + it "asks about CHANGELOG just once" do + bundle_config_global "BUNDLE_GEM__CHANGELOG" => nil + + bundle "gem foobar" do |input, _, _| + input.puts "yes" + end + + expect(bundled_app("foobar/CHANGELOG.md")).to exist + expect(out).to include("A changelog is a file which contains").once end end context "on conflicts with a previously created file" do it "should fail gracefully" do - in_app_root do - FileUtils.touch("conflict-foobar") - end - output = bundle "gem conflict-foobar" - expect(output).to include("Errno::ENOTDIR") - expect(exitstatus).to eql(32) if exitstatus + 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 - in_app_root do - FileUtils.mkdir_p("conflict-foobar/Gemfile") - end - bundle! "gem conflict-foobar" + FileUtils.mkdir_p(bundled_app("conflict-foobar/Gemfile")) + bundle "gem conflict-foobar" expect(out).to include("file_clash conflict-foobar/Gemfile"). and include "Initializing git repo in #{bundled_app("conflict-foobar")}" end diff --git a/spec/bundler/commands/open_spec.rb b/spec/bundler/commands/open_spec.rb index 6872e859d2..664dc58919 100644 --- a/spec/bundler/commands/open_spec.rb +++ b/spec/bundler/commands/open_spec.rb @@ -1,93 +1,175 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle open" do - before :each do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rails" - G - end + 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 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 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 "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 no EDITOR is set" do + bundle "open rails", env: { "EDITOR" => "", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to eq("To open a bundled gem, set $EDITOR or $BUNDLER_EDITOR") + end - it "complains if gem not in bundle" do - bundle "open missing", :env => { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } - expect(out).to match(/could not find gem 'missing'/i) - end + it "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("master", 11) + 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 "file://#{gem_repo1}" - gem 'foo', :git => "#{lib_path("foo-1.0")}" - G + 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 match("editor #{default_bundle_path.join("bundler/gems/foo-1.0-#{ref}")}") - end + bundle "open foo", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("editor #{default_bundle_path("bundler", "gems", "foo-1.0-#{ref}")}") + end - it "suggests alternatives for similar-sounding gems" do - bundle "open Rails", :env => { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } - expect(out).to match(/did you mean rails\?/i) - 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" } + 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 + expect(out).to include("bundler_editor #{default_bundle_path("gems", "activerecord-2.3.2")}") + end - it "select the gem from many match gems" do - env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" } - bundle "open active", :env => env do |input, _, _| - input.puts "2" + 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 - expect(out).to match(/bundler_editor #{default_bundle_path('gems', 'activerecord-2.3.2')}\z/) - 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 "allows selecting exit from many match gems" do - env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" } - bundle! "open active", :env => env do |input, _, _| - input.puts "0" + 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 - end - it "performs an automatic bundle install" do - gemfile <<-G - source "file://#{gem_repo1}" - gem "rails" - gem "foo" - G + 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 - bundle "config auto_install 1" - bundle "open rails", :env => { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } - expect(out).to include("Installing foo 1.0") + 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 - it "opens the editor with a clean env" do - bundle "open", :env => { "EDITOR" => "sh -c 'env'", "VISUAL" => "", "BUNDLER_EDITOR" => "" } - expect(out).not_to include("BUNDLE_GEMFILE=") + 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 index c6b6c9f59e..28ed51d61e 100644 --- a/spec/bundler/commands/outdated_spec.rb +++ b/spec/bundler/commands/outdated_spec.rb @@ -1,64 +1,85 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle outdated" do - before :each do - build_repo2 do - build_git "foo", :path => lib_path("foo") - build_git "zebra", :path => lib_path("zebra") - end - - install_gemfile <<-G - source "file://#{gem_repo2}" - gem "zebra", :git => "#{lib_path("zebra")}" - gem "foo", :git => "#{lib_path("foo")}" - gem "activesupport", "2.3.5" - gem "weakling", "~> 0.0.1" - gem "duradura", '7.0' - gem "terranova", '8' - G - end - describe "with no arguments" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + end + it "returns a sorted list of outdated gems" do update_repo2 do build_gem "activesupport", "3.0" build_gem "weakling", "0.2" - update_git "foo", :path => lib_path("foo") - update_git "zebra", :path => lib_path("zebra") + update_git "foo", path: lib_path("foo") + update_git "zebra", path: lib_path("zebra") end - bundle "outdated" + bundle "outdated", raise_on_error: false - expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5)") - expect(out).to include("weakling (newest 0.2, installed 0.0.3, requested ~> 0.0.1)") - expect(out).to include("foo (newest 1.0") + 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 - # Gem names are one per-line, between "*" and their parenthesized version. - gem_list = out.split("\n").map {|g| g[/\* (.*) \(/, 1] }.compact - expect(gem_list).to eq(gem_list.sort) + expect(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") + update_git "foo", path: lib_path("foo") end - bundle "outdated" + bundle "outdated", raise_on_error: false - expect(exitstatus).to_not be_zero if exitstatus + expect(exitstatus).to_not be_zero end it "returns success exit status if no outdated gems present" do bundle "outdated" - - expect(exitstatus).to be_zero if exitstatus end it "adds gem group to dependency output when repo is updated" do install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" + + gem "terranova", '8' group :development, :test do gem 'activesupport', '2.3.5' @@ -66,16 +87,79 @@ RSpec.describe "bundle outdated" do G update_repo2 { build_gem "activesupport", "3.0" } + update_repo2 { build_gem "terranova", "9" } + + bundle "outdated", raise_on_error: false - bundle "outdated --verbose" - expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5) in groups \"development, test\"") + 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 - def test_group_option(group = nil, gems_list_size = 1) + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "weakling", "~> 0.0.1" gem "terranova", '8' @@ -84,80 +168,112 @@ RSpec.describe "bundle outdated" do 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}" - - # Gem names are one per-line, between "*" and their parenthesized version. - gem_list = out.split("\n").map {|g| g[/\* (.*) \(/, 1] }.compact - expect(gem_list).to eq(gem_list.sort) - expect(gem_list.size).to eq gems_list_size + bundle "outdated --group #{group}", raise_on_error: false end - it "not outdated gems" do - install_gemfile <<-G - source "file://#{gem_repo2}" - - gem "weakling", "~> 0.0.1" - gem "terranova", '8' - group :development, :test do - gem 'activesupport', '2.3.5' - gem "duradura", '7.0' - end - G - + it "works when the bundle is up to date" do bundle "outdated --group" - expect(out).to include("Bundle up to date!") + expect(out).to end_with("Bundle up to date!") + end + + it "works when only out of date gems are not in given group" do + update_repo2 do + build_gem "terranova", "9" + end + bundle "outdated --group development" + expect(out).to end_with("Bundle up to date!") end it "returns a sorted list of outdated gems from one group => 'default'" do test_group_option("default") - expect(out).to include("===== Group default =====") - expect(out).to include("terranova (") + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + terranova 8 9 = 8 default + TABLE - expect(out).not_to include("===== Group development, test =====") - expect(out).not_to include("activesupport") - expect(out).not_to include("duradura") + expect(out).to end_with(expected_output) end it "returns a sorted list of outdated gems from one group => 'development'" do - test_group_option("development", 2) + test_group_option("development") + + 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") - expect(out).not_to include("===== Group default =====") - expect(out).not_to include("terranova (") + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + activesupport 2.3.5 3.0 = 2.3.5 development, test + duradura 7.0 8.0 = 7.0 development, test + TABLE - expect(out).to include("===== Group development, test =====") - expect(out).to include("activesupport") - expect(out).to include("duradura") + expect(out).to end_with(expected_output) end end - describe "with --groups option" do - it "not outdated gems" do - install_gemfile <<-G - source "file://#{gem_repo2}" + 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") - gem "weakling", "~> 0.0.1" - gem "terranova", '8' - group :development, :test do - gem 'activesupport', '2.3.5' - gem "duradura", '7.0' + 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 - bundle "outdated --groups" - expect(out).to include("Bundle up to date!") + update_repo2 do + build_gem "bar", %w[3.0.0] + end end - it "returns a sorted list of outdated gems by groups" do + it "returns a sorted list of outdated gems" do + bundle "outdated --groups", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + bar 2.0.0 3.0.0 + TABLE + + expect(out).to end_with(expected_output) + end + end + + describe "with --groups option" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "weakling", "~> 0.0.1" gem "terranova", '8' @@ -166,44 +282,76 @@ RSpec.describe "bundle outdated" do 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" - expect(out).to include("===== Group default =====") - expect(out).to include("terranova (newest 9, installed 8, requested = 8)") - expect(out).to include("===== Group development, test =====") - expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5)") - expect(out).to include("duradura (newest 8.0, installed 7.0, requested = 7.0)") + bundle "outdated --groups", raise_on_error: false - expect(out).not_to include("weakling (") + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + activesupport 2.3.5 3.0 = 2.3.5 development, test + duradura 7.0 8.0 = 7.0 development, test + terranova 8 9 = 8 default + TABLE - # TODO: check gems order inside the group + expect(out).to end_with(expected_output) end end describe "with --local option" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + + gem "weakling", "~> 0.0.1" + gem "terranova", '8' + group :development, :test do + gem 'activesupport', '2.3.5' + gem "duradura", '7.0' + end + G + end + it "uses local cache to return a list of outdated gems" do update_repo2 do build_gem "activesupport", "2.3.4" end + bundle_config "clean false" + install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "activesupport", "2.3.4" G - bundle "outdated --local" + bundle "outdated --local", raise_on_error: false - expect(out).to include("activesupport (newest 2.3.5, installed 2.3.4, requested = 2.3.4)") + 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_rf(gem_repo2) + FileUtils.rm_r(gem_repo2) bundle "outdated --local" expect(out).not_to match(/Fetching (gem|version|dependency) metadata from/) @@ -213,54 +361,163 @@ RSpec.describe "bundle outdated" do shared_examples_for "a minimal output is desired" do context "and gems are outdated" do before do - update_repo2 do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + build_gem "activesupport", "3.0" build_gem "weakling", "0.2" end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G end - it "outputs a sorted list of outdated gems with a more minimal format" do + it "outputs a sorted list of outdated gems with a more minimal format to stdout" do minimal_output = "activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5)\n" \ "weakling (newest 0.2, installed 0.0.3, requested ~> 0.0.1)" subject expect(out).to eq(minimal_output) end + + it "outputs progress to stderr" do + subject + expect(err).to include("Fetching gem metadata") + end end context "and no gems are outdated" do - it "has empty output" do + before do + build_repo2 do + build_gem "activesupport", "3.0" + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport", "3.0" + G + end + + it "does not output to stdout" do + subject + expect(out).to be_empty + end + + it "outputs progress to stderr" do subject - expect(out).to eq("") + expect(err).to include("Fetching gem metadata") end end end describe "with --parseable option" do - subject { bundle "outdated --parseable" } + subject { bundle "outdated --parseable", raise_on_error: false } it_behaves_like "a minimal output is desired" end describe "with aliased --porcelain option" do - subject { bundle "outdated --porcelain" } + subject { bundle "outdated --porcelain", raise_on_error: false } it_behaves_like "a minimal output is desired" end describe "with specified gems" do it "returns list of outdated gems" do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + update_repo2 do build_gem "activesupport", "3.0" - update_git "foo", :path => lib_path("foo") + update_git "foo", path: lib_path("foo") + end + + bundle "outdated foo", 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 - bundle "outdated foo" - expect(out).not_to include("activesupport (newest") - expect(out).to include("foo (newest 1.0") + 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 @@ -268,7 +525,8 @@ RSpec.describe "bundle outdated" do end bundle "outdated" - expect(out).not_to include("activesupport (3.0.0.beta > 2.3.5)") + + expect(out).to end_with("Bundle up to date!") end end @@ -278,8 +536,14 @@ RSpec.describe "bundle outdated" do build_gem "activesupport", "3.0.0.beta" end - bundle "outdated --pre" - expect(out).to include("activesupport (newest 3.0.0.beta, installed 2.3.5, requested = 2.3.5)") + bundle "outdated --pre", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + activesupport 2.3.5 3.0.0.beta = 2.3.5 default + TABLE + + expect(out).to end_with(expected_output) end end @@ -291,195 +555,333 @@ RSpec.describe "bundle outdated" do end install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "activesupport", "3.0.0.beta.1" G - bundle "outdated" - expect(out).to include("(newest 3.0.0.beta.2, installed 3.0.0.beta.1, requested = 3.0.0.beta.1)") + bundle "outdated", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + activesupport 3.0.0.beta.1 3.0.0.beta.2 = 3.0.0.beta.1 default + TABLE + + expect(out).to end_with(expected_output) end end end - describe "with --strict option" do + describe "with --filter-strict option" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + end + it "only reports gems that have a newer version that matches the specified dependency version requirements" do update_repo2 do build_gem "activesupport", "3.0" build_gem "weakling", "0.0.5" end - bundle "outdated --strict" + 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_not include("activesupport (newest") - expect(out).to include("(newest 0.0.5, installed 0.0.3, requested ~> 0.0.1)") + expect(out).to end_with(expected_output) + end + + it "doesn't crash when some deps unused on the current platform" do + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport", platforms: [:ruby_22] + G + + bundle :outdated, "filter-strict": true + + expect(out).to end_with("Bundle up to date!") end it "only reports gem dependencies when they can actually be updated" do install_gemfile <<-G - source "file://#{gem_repo2}" - gem "rack_middleware", "1.0" + source "https://gem.repo2" + gem "myrack_middleware", "1.0" G - bundle "outdated --strict" + bundle :outdated, "filter-strict": true - expect(out).to_not include("rack (1.2") + expect(out).to end_with("Bundle up to date!") end describe "and filter options" do it "only reports gems that match requirement and patch filter level" do install_gemfile <<-G - source "file://#{gem_repo2}" + 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 "activesupport", %w[2.4.0 3.0.0] build_gem "weakling", "0.0.5" end - bundle "outdated --strict --filter-patch" + bundle :outdated, :"filter-strict" => true, "filter-patch" => true, :raise_on_error => false - expect(out).to_not include("activesupport (newest") - expect(out).to include("(newest 0.0.5, installed 0.0.3") + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + weakling 0.0.3 0.0.5 >= 0.0.1 default + TABLE + + expect(out).to end_with(expected_output) end it "only reports gems that match requirement and minor filter level" do install_gemfile <<-G - source "file://#{gem_repo2}" + 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 "activesupport", %w[2.3.9] build_gem "weakling", "0.1.5" end - bundle "outdated --strict --filter-minor" + bundle :outdated, :"filter-strict" => true, "filter-minor" => true, :raise_on_error => false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + weakling 0.0.3 0.1.5 >= 0.0.1 default + TABLE - expect(out).to_not include("activesupport (newest") - expect(out).to include("(newest 0.1.5, installed 0.0.3") + expect(out).to end_with(expected_output) end it "only reports gems that match requirement and major filter level" do install_gemfile <<-G - source "file://#{gem_repo2}" + 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 "activesupport", %w[2.4.0 2.5.0] build_gem "weakling", "1.1.5" end - bundle "outdated --strict --filter-major" + 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_not include("activesupport (newest") - expect(out).to include("(newest 1.1.5, installed 0.0.3") + 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" - expect(out).to include("Could not find gem 'invalid_gem_name'.") + bundle "outdated invalid_gem_name", raise_on_error: false + expect(err).to include("Could not find gem 'invalid_gem_name'.") end it "returns non-zero exit code" do - bundle "outdated invalid_gem_name" - expect(exitstatus).to_not be_zero if exitstatus + bundle "outdated invalid_gem_name", raise_on_error: false + expect(exitstatus).to_not be_zero end end it "performs an automatic bundle install" do gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", "0.9.1" + source "https://gem.repo1" + gem "myrack", "0.9.1" gem "foo" G - bundle "config auto_install 1" - bundle :outdated + bundle_config "auto_install 1" + bundle :outdated, raise_on_error: false expect(out).to include("Installing foo 1.0") end - context "after bundle install --deployment" do + context "in deployment mode" do before do - install_gemfile <<-G, :deployment => true - source "file://#{gem_repo2}" + build_repo2 - gem "rack" + 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" - expect(exitstatus).to_not be_zero if exitstatus - expect(out).to include("You are trying to check outdated gems in deployment mode.") - expect(out).to include("Run `bundle outdated` elsewhere.") - expect(out).to include("If this is a development machine, remove the ") - expect(out).to include("Gemfile freeze\nby running `bundle install --no-deployment`.") + 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 "file://#{gem_repo2}" + source "https://gem.repo2" gem "laduradura", '= 5.15.2' G end it "reports that no updates are available" do bundle "outdated" - expect(out).to include("Bundle up to date!") + expect(out).to end_with("Bundle up to date!") end end context "update available for a gem on the same platform while multiple platforms used for gem" do + before do + build_repo2 + end + it "reports that updates are available if the Ruby platform is used" do install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] G bundle "outdated" - expect(out).to include("Bundle up to date!") + expect(out).to end_with("Bundle up to date!") end - it "reports that updates are available if the JRuby platform is used" do - simulate_ruby_engine "jruby", "1.6.7" do - simulate_platform "jruby" do - install_gemfile <<-G - source "file://#{gem_repo2}" - gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] - G + it "reports that updates are available if the JRuby platform is used", :jruby_only do + install_gemfile <<-G + source "https://gem.repo2" + gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] + G + + bundle "outdated", raise_on_error: false - bundle "outdated" - expect(out).to include("Outdated gems included in the bundle:") - expect(out).to include("laduradura (newest 5.15.3, installed 5.15.2, requested = 5.15.2)") - end - end + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + laduradura 5.15.2 5.15.3 = 5.15.2 default + TABLE + + expect(out).to end_with(expected_output) end end shared_examples_for "version update is detected" do it "reports that a gem has a newer version" do subject - expect(out).to include("Outdated gems included in the bundle:") - expect(out).to include("activesupport (newest") - expect(out).to_not include("ERROR REPORT TEMPLATE") + + outdated_gems = out.split("\n").drop_while {|l| !l.start_with?("Gem") }[1..-1] + + expect(outdated_gems.size).to be > 0 end end shared_examples_for "major version updates are detected" do before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + update_repo2 do build_gem "activesupport", "3.3.5" build_gem "weakling", "0.8.0" @@ -491,21 +893,51 @@ RSpec.describe "bundle outdated" do context "when on a new machine" do before do - simulate_new_machine + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + 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_git "foo", path: lib_path("foo") update_repo2 do build_gem "activesupport", "3.3.5" build_gem "weakling", "0.8.0" end end - subject { bundle "outdated" } + subject { bundle "outdated", raise_on_error: false } it_behaves_like "version update is detected" end shared_examples_for "minor version updates are detected" do before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + update_repo2 do build_gem "activesupport", "2.7.5" build_gem "weakling", "2.0.1" @@ -517,6 +949,21 @@ RSpec.describe "bundle outdated" do shared_examples_for "patch version updates are detected" do before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + update_repo2 do build_gem "activesupport", "2.3.7" build_gem "weakling", "0.3.1" @@ -529,15 +976,27 @@ RSpec.describe "bundle outdated" do shared_examples_for "no version updates are detected" do it "does not detect any version updates" do subject - expect(out).to include("updates to display.") - expect(out).to_not include("ERROR REPORT TEMPLATE") - expect(out).to_not include("activesupport (newest") - expect(out).to_not include("weakling (newest") + expect(out).to end_with("updates to display.") end end shared_examples_for "major version is ignored" do before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + update_repo2 do build_gem "activesupport", "3.3.5" build_gem "weakling", "1.0.1" @@ -549,6 +1008,21 @@ RSpec.describe "bundle outdated" do shared_examples_for "minor version is ignored" do before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + update_repo2 do build_gem "activesupport", "2.4.5" build_gem "weakling", "0.3.1" @@ -560,6 +1034,21 @@ RSpec.describe "bundle outdated" do shared_examples_for "patch version is ignored" do before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + update_repo2 do build_gem "activesupport", "2.3.6" build_gem "weakling", "0.0.4" @@ -570,7 +1059,7 @@ RSpec.describe "bundle outdated" do end describe "with --filter-major option" do - subject { bundle "outdated --filter-major" } + subject { bundle "outdated --filter-major", raise_on_error: false } it_behaves_like "major version updates are detected" it_behaves_like "minor version is ignored" @@ -578,7 +1067,7 @@ RSpec.describe "bundle outdated" do end describe "with --filter-minor option" do - subject { bundle "outdated --filter-minor" } + subject { bundle "outdated --filter-minor", raise_on_error: false } it_behaves_like "minor version updates are detected" it_behaves_like "major version is ignored" @@ -586,7 +1075,7 @@ RSpec.describe "bundle outdated" do end describe "with --filter-patch option" do - subject { bundle "outdated --filter-patch" } + subject { bundle "outdated --filter-patch", raise_on_error: false } it_behaves_like "patch version updates are detected" it_behaves_like "major version is ignored" @@ -594,7 +1083,7 @@ RSpec.describe "bundle outdated" do end describe "with --filter-minor --filter-patch options" do - subject { bundle "outdated --filter-minor --filter-patch" } + subject { bundle "outdated --filter-minor --filter-patch", raise_on_error: false } it_behaves_like "minor version updates are detected" it_behaves_like "patch version updates are detected" @@ -602,7 +1091,7 @@ RSpec.describe "bundle outdated" do end describe "with --filter-major --filter-minor options" do - subject { bundle "outdated --filter-major --filter-minor" } + subject { bundle "outdated --filter-major --filter-minor", raise_on_error: false } it_behaves_like "major version updates are detected" it_behaves_like "minor version updates are detected" @@ -610,7 +1099,7 @@ RSpec.describe "bundle outdated" do end describe "with --filter-major --filter-patch options" do - subject { bundle "outdated --filter-major --filter-patch" } + subject { bundle "outdated --filter-major --filter-patch", raise_on_error: false } it_behaves_like "major version updates are detected" it_behaves_like "patch version updates are detected" @@ -618,7 +1107,7 @@ RSpec.describe "bundle outdated" do end describe "with --filter-major --filter-minor --filter-patch options" do - subject { bundle "outdated --filter-major --filter-minor --filter-patch" } + subject { bundle "outdated --filter-major --filter-minor --filter-patch", raise_on_error: false } it_behaves_like "major version updates are detected" it_behaves_like "minor version updates are detected" @@ -626,106 +1115,255 @@ RSpec.describe "bundle outdated" do end context "conservative updates" do - context "without update-strict" do - before do - build_repo4 do - build_gem "patch", %w(1.0.0 1.0.1) - build_gem "minor", %w(1.0.0 1.0.1 1.1.0) - build_gem "major", %w(1.0.0 1.0.1 1.1.0 2.0.0) - end + before do + build_repo4 do + build_gem "patch", %w[1.0.0 1.0.1] + build_gem "minor", %w[1.0.0 1.0.1 1.1.0] + build_gem "major", %w[1.0.0 1.0.1 1.1.0 2.0.0] + end - # establish a lockfile set to 1.0.0 - install_gemfile <<-G - source "file://#{gem_repo4}" + # establish a lockfile set to 1.0.0 + install_gemfile <<-G + source "https://gem.repo4" gem 'patch', '1.0.0' gem 'minor', '1.0.0' gem 'major', '1.0.0' - G + G - # remove 1.4.3 requirement and bar altogether - # to setup update specs below - gemfile <<-G - source "file://#{gem_repo4}" + # remove all version requirements + gemfile <<-G + source "https://gem.repo4" gem 'patch' gem 'minor' gem 'major' - G - end + G + end - it "shows nothing when patching and filtering to minor" do - bundle "outdated --patch --filter-minor" + it "shows nothing when patching and filtering to minor" do + bundle "outdated --patch --filter-minor" - expect(out).to include("No minor updates to display.") - expect(out).not_to include("patch (newest") - expect(out).not_to include("minor (newest") - expect(out).not_to include("major (newest") - end + expect(out).to end_with("No minor updates to display.") + end + + it "shows all gems when patching and filtering to patch" do + bundle "outdated --patch --filter-patch", 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 - it "shows all gems when patching and filtering to patch" do - bundle "outdated --patch --filter-patch" + expect(out).not_to include("patch (newest") + expect(out).to include("minor (newest") + expect(out).not_to include("major (newest") + end + end - expect(out).to include("patch (newest 1.0.1") - expect(out).to include("minor (newest 1.0.1") - expect(out).to include("major (newest 1.0.1") + context "tricky conservative updates" do + before do + build_repo4 do + build_gem "foo", %w[1.4.3 1.4.4] do |s| + s.add_dependency "bar", "~> 2.0" + end + build_gem "foo", %w[1.4.5 1.5.0] do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "foo", %w[1.5.1] do |s| + s.add_dependency "bar", "~> 3.0" + end + build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0] + build_gem "qux", %w[1.0.0 1.1.0 2.0.0] end - it "shows minor and major when updating to minor and filtering to patch and minor" do - bundle "outdated --minor --filter-minor" + # 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" } - expect(out).not_to include("patch (newest") - expect(out).to include("minor (newest 1.1.0") - expect(out).to include("major (newest 1.1.0") + 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 - it "shows minor when updating to major and filtering to minor with parseable" do - bundle "outdated --major --filter-minor --parseable" + 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 - expect(out).not_to include("patch (newest") - expect(out).to include("minor (newest") - expect(out).not_to include("major (newest") + 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 - context "with update-strict" do - before do - build_repo4 do - build_gem "foo", %w(1.4.3 1.4.4) do |s| - s.add_dependency "bar", "~> 2.0" - end - build_gem "foo", %w(1.4.5 1.5.0) do |s| - s.add_dependency "bar", "~> 2.1" - end - build_gem "foo", %w(1.5.1) do |s| - s.add_dependency "bar", "~> 3.0" - end - build_gem "bar", %w(2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0) - build_gem "qux", %w(1.0.0 1.1.0 2.0.0) + 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 - # establish a lockfile set to 1.4.3 - install_gemfile <<-G - source "file://#{gem_repo4}" - gem 'foo', '1.4.3' - gem 'bar', '2.0.3' - gem 'qux', '1.0.0' - G + build_gem "mini_portile2", "2.5.3" - # remove 1.4.3 requirement and bar altogether - # to setup update specs below - gemfile <<-G - source "file://#{gem_repo4}" - gem 'foo' - gem 'qux' - G + build_gem "net-ftp", "0.1.2" end - it "shows gems with update-strict updating to patch and filtering to patch" do - bundle "outdated --patch --update-strict --filter-patch" + gemfile <<~G + source "https://gem.repo4" - expect(out).to include("foo (newest 1.4.4") - expect(out).to include("bar (newest 2.0.5") - expect(out).not_to include("qux (newest") - end + 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/package_spec.rb b/spec/bundler/commands/package_spec.rb deleted file mode 100644 index 86c09db3ca..0000000000 --- a/spec/bundler/commands/package_spec.rb +++ /dev/null @@ -1,306 +0,0 @@ -# frozen_string_literal: true -require "spec_helper" - -RSpec.describe "bundle package" do - context "with --gemfile" do - it "finds the gemfile" do - gemfile bundled_app("NotGemfile"), <<-G - source "file://#{gem_repo1}" - gem 'rack' - G - - bundle "package --gemfile=NotGemfile" - - ENV["BUNDLE_GEMFILE"] = "NotGemfile" - expect(the_bundle).to include_gems "rack 1.0.0" - end - end - - context "with --all" do - context "without a gemspec" do - it "caches all dependencies except bundler itself" do - gemfile <<-D - source "file://#{gem_repo1}" - gem 'rack' - gem 'bundler' - D - - bundle "package --all" - - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist - expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist - end - end - - context "with a gemspec" do - context "that has the same name as the gem" do - before do - File.open(bundled_app("mygem.gemspec"), "w") do |f| - f.write <<-G - Gem::Specification.new do |s| - s.name = "mygem" - s.version = "0.1.1" - s.summary = "" - s.authors = ["gem author"] - s.add_development_dependency "nokogiri", "=1.4.2" - end - G - end - end - - it "caches all dependencies except bundler and the gemspec specified gem" do - gemfile <<-D - source "file://#{gem_repo1}" - gem 'rack' - gemspec - D - - bundle! "package --all" - - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist - expect(bundled_app("vendor/cache/nokogiri-1.4.2.gem")).to exist - expect(bundled_app("vendor/cache/mygem-0.1.1.gem")).to_not exist - expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist - end - end - - context "that has a different name as the gem" do - before do - File.open(bundled_app("mygem_diffname.gemspec"), "w") do |f| - f.write <<-G - Gem::Specification.new do |s| - s.name = "mygem" - s.version = "0.1.1" - s.summary = "" - s.authors = ["gem author"] - s.add_development_dependency "nokogiri", "=1.4.2" - end - G - end - end - - it "caches all dependencies except bundler and the gemspec specified gem" do - gemfile <<-D - source "file://#{gem_repo1}" - gem 'rack' - gemspec - D - - bundle! "package --all" - - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist - expect(bundled_app("vendor/cache/nokogiri-1.4.2.gem")).to exist - expect(bundled_app("vendor/cache/mygem-0.1.1.gem")).to_not exist - expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist - end - end - end - - context "with multiple gemspecs" do - before do - File.open(bundled_app("mygem.gemspec"), "w") do |f| - f.write <<-G - Gem::Specification.new do |s| - s.name = "mygem" - s.version = "0.1.1" - s.summary = "" - s.authors = ["gem author"] - s.add_development_dependency "nokogiri", "=1.4.2" - end - G - end - File.open(bundled_app("mygem_client.gemspec"), "w") do |f| - f.write <<-G - Gem::Specification.new do |s| - s.name = "mygem_test" - s.version = "0.1.1" - s.summary = "" - s.authors = ["gem author"] - s.add_development_dependency "weakling", "=0.0.3" - end - G - end - end - - it "caches all dependencies except bundler and the gemspec specified gems" do - gemfile <<-D - source "file://#{gem_repo1}" - gem 'rack' - gemspec :name => 'mygem' - gemspec :name => 'mygem_test' - D - - bundle! "package --all" - - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist - expect(bundled_app("vendor/cache/nokogiri-1.4.2.gem")).to exist - expect(bundled_app("vendor/cache/weakling-0.0.3.gem")).to exist - expect(bundled_app("vendor/cache/mygem-0.1.1.gem")).to_not exist - expect(bundled_app("vendor/cache/mygem_test-0.1.1.gem")).to_not exist - expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist - end - end - end - - context "with --path" do - it "sets root directory for gems" do - gemfile <<-D - source "file://#{gem_repo1}" - gem 'rack' - D - - bundle "package --path=#{bundled_app("test")}" - - expect(the_bundle).to include_gems "rack 1.0.0" - expect(bundled_app("test/vendor/cache/")).to exist - end - end - - context "with --no-install" do - it "puts the gems in vendor/cache but does not install them" do - gemfile <<-D - source "file://#{gem_repo1}" - gem 'rack' - D - - bundle "package --no-install" - - expect(the_bundle).not_to include_gems "rack 1.0.0" - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist - end - - it "does not prevent installing gems with bundle install" do - gemfile <<-D - source "file://#{gem_repo1}" - gem 'rack' - D - - bundle "package --no-install" - bundle "install" - - expect(the_bundle).to include_gems "rack 1.0.0" - end - end - - context "with --all-platforms" do - it "puts the gems in vendor/cache even for other rubies", :ruby => "2.1" do - gemfile <<-D - source "file://#{gem_repo1}" - gem 'rack', :platforms => :ruby_19 - D - - bundle "package --all-platforms" - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist - end - end - - context "with --frozen" do - before do - gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - G - bundle "install" - end - - subject { bundle "package --frozen" } - - it "tries to install with frozen" do - gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - gem "rack-obama" - G - subject - expect(exitstatus).to eq(16) if exitstatus - expect(out).to include("deployment mode") - expect(out).to include("You have added to the Gemfile") - expect(out).to include("* rack-obama") - bundle "env" - expect(out).to include("frozen") - end - end -end - -RSpec.describe "bundle install with gem sources" do - describe "when cached and locked" do - it "does not hit the remote at all" do - build_repo2 - install_gemfile <<-G - source "file://#{gem_repo2}" - gem "rack" - G - - bundle :pack - simulate_new_machine - FileUtils.rm_rf gem_repo2 - - bundle "install --local" - expect(the_bundle).to include_gems "rack 1.0.0" - end - - it "does not hit the remote at all" do - build_repo2 - install_gemfile <<-G - source "file://#{gem_repo2}" - gem "rack" - G - - bundle :pack - simulate_new_machine - FileUtils.rm_rf gem_repo2 - - bundle "install --deployment" - expect(the_bundle).to include_gems "rack 1.0.0" - end - - it "does not reinstall already-installed gems" do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - G - bundle :pack - - build_gem "rack", "1.0.0", :path => bundled_app("vendor/cache") do |s| - s.write "lib/rack.rb", "raise 'omg'" - end - - bundle :install - expect(err).to lack_errors - expect(the_bundle).to include_gems "rack 1.0" - end - - it "ignores cached gems for the wrong platform" do - simulate_platform "java" do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "platform_specific" - G - bundle :pack - end - - simulate_new_machine - - simulate_platform "ruby" do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "platform_specific" - G - run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" - expect(out).to eq("1.0.0 RUBY") - end - end - - it "does not update the cache if --no-cache is passed" do - gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - G - bundled_app("vendor/cache").mkpath - expect(bundled_app("vendor/cache").children).to be_empty - - bundle "install --no-cache" - expect(bundled_app("vendor/cache").children).to be_empty - end - end -end diff --git a/spec/bundler/commands/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 index 3aca313e0f..5f80b9e534 100644 --- a/spec/bundler/commands/pristine_spec.rb +++ b/spec/bundler/commands/pristine_spec.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require "spec_helper" -require "fileutils" -RSpec.describe "bundle pristine", :ruby_repo do +require "bundler/vendored_fileutils" + +RSpec.describe "bundle pristine" do before :each do - build_lib "baz", :path => bundled_app do |s| + build_lib "baz", path: bundled_app do |s| s.version = "1.0.0" s.add_development_dependency "baz-dev", "=1.0.0" end @@ -13,24 +13,28 @@ RSpec.describe "bundle pristine", :ruby_repo do build_gem "weakling" build_gem "baz-dev", "1.0.0" build_gem "very_simple_binary", &:add_c_extension - build_git "foo", :path => lib_path("foo") - build_lib "bar", :path => lib_path("bar") + build_git "foo", path: lib_path("foo") + build_git "git_with_ext", path: lib_path("git_with_ext"), &:add_c_extension + build_lib "bar", path: lib_path("bar") end - install_gemfile! <<-G - source "file://#{gem_repo2}" + install_gemfile <<-G + source "https://gem.repo2" gem "weakling" gem "very_simple_binary" - gem "foo", :git => "#{lib_path("foo")}" + gem "foo", :git => "#{lib_path("foo")}", :branch => "main" + gem "git_with_ext", :git => "#{lib_path("git_with_ext")}" gem "bar", :path => "#{lib_path("bar")}" gemspec G + + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) end - context "when sourced from Rubygems" do + context "when sourced from RubyGems" do it "reverts using cached .gem file" do - spec = Bundler.definition.specs["weakling"].first + spec = find_spec("weakling") changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt") FileUtils.touch(changes_txt) @@ -40,46 +44,130 @@ RSpec.describe "bundle pristine", :ruby_repo do expect(changes_txt).to_not be_file end - it "does not delete the bundler gem", :ruby_repo do - system_gems :bundler - bundle! "install" - bundle! "pristine", :system_bundler => true - bundle! "-v", :system_bundler => true + 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 = Bundler.definition.specs["foo"].first + spec = find_spec("foo") changed_file = Pathname.new(spec.full_gem_path).join("lib/foo.rb") diff = "#Pristine spec changes" - File.open(changed_file, "a") {|f| f.puts "#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 = Bundler.definition.specs["baz"].first + spec = find_spec("baz") changed_file = Pathname.new(spec.full_gem_path).join("lib/baz.rb") diff = "#Pristine spec changes" - File.open(changed_file, "a") {|f| f.puts "#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(out).to include("Cannot pristine #{spec.name} (#{spec.version}#{spec.git_version}). Gem is sourced from local path.") + 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 = Bundler.definition.specs["baz-dev"].first - changed_file = Pathname.new(spec.full_gem_path).join("lib/baz-dev.rb") + spec = find_spec("baz-dev") + changed_file = Pathname.new(spec.full_gem_path).join("lib/baz/dev.rb") diff = "#Pristine spec changes" File.open(changed_file, "a") {|f| f.puts "#Pristine spec changes" } @@ -92,30 +180,96 @@ RSpec.describe "bundle pristine", :ruby_repo do context "when sourced from path" do it "displays warning and ignores changes when sourced from local path" do - spec = Bundler.definition.specs["bar"].first + spec = find_spec("bar") changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt") FileUtils.touch(changes_txt) expect(changes_txt).to be_file bundle "pristine" - expect(out).to include("Cannot pristine #{spec.name} (#{spec.version}#{spec.git_version}). Gem is sourced from local path.") + 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) { Bundler.definition.specs["very_simple_binary"].first } + let(:very_simple_binary) { find_spec("very_simple_binary") } let(:c_ext_dir) { Pathname.new(very_simple_binary.full_gem_path).join("ext") } let(:build_opt) { "--with-ext-lib=#{c_ext_dir}" } - before { bundle "config build.very_simple_binary -- #{build_opt}" } + before { bundle_config "build.very_simple_binary -- #{build_opt}" } + + # This just verifies that the generated Makefile from the c_ext gem makes + # use of the build_args from the bundle config + it "applies the config when installing the gem" do + bundle "pristine" + + 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" + bundle "pristine" makefile_contents = File.read(c_ext_dir.join("Makefile").to_s) - expect(makefile_contents).to match(/libpath =.*#{c_ext_dir}/) - expect(makefile_contents).to match(/LIBPATH =.*-L#{c_ext_dir}/) + expect(makefile_contents).to match(/libpath =.*#{Regexp.escape(c_ext_dir.to_s)}/) + expect(makefile_contents).to match(/LIBPATH =.*-L#{Regexp.escape(c_ext_dir.to_s)}/) + end + end + + context "when BUNDLE_GEMFILE doesn't exist" do + before do + bundle "pristine", env: { "BUNDLE_GEMFILE" => "does/not/exist" }, raise_on_error: false + end + + it "shows a meaningful error" do + expect(err).to eq("#{bundled_app("does/not/exist")} not found") + end + end + + def find_spec(name) + without_env_side_effects do + Bundler.definition.specs[name].first end end end diff --git a/spec/bundler/commands/remove_spec.rb b/spec/bundler/commands/remove_spec.rb 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 index 0391ddec52..d0d55ffbb9 100644 --- a/spec/bundler/commands/show_spec.rb +++ b/spec/bundler/commands/show_spec.rb @@ -1,29 +1,30 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle show" do context "with a standard Gemfile" do before :each do + build_repo2 + install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo2" gem "rails" G end it "creates a Gemfile.lock if one did not exist" do - FileUtils.rm("Gemfile.lock") + FileUtils.rm(bundled_app_lock) bundle "show" - expect(bundled_app("Gemfile.lock")).to exist + expect(bundled_app_lock).to exist end it "creates a Gemfile.lock when invoked with a gem name" do - FileUtils.rm("Gemfile.lock") + FileUtils.rm(bundled_app_lock) bundle "show rails" - expect(bundled_app("Gemfile.lock")).to exist + expect(bundled_app_lock).to exist end it "prints path if gem exists in bundle" do @@ -31,29 +32,34 @@ RSpec.describe "bundle show" do expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s) end - it "warns if path no longer exists on disk" do - FileUtils.rm_rf("#{system_gem_path}/gems/rails-2.3.2") + 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(out).to match(/has been deleted/i) - expect(out).to include(default_bundle_path("gems", "rails-2.3.2").to_s) + 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", :ruby_repo do + 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" - expect(out).to match(/could not find gem 'missing'/i) + 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-10.0.2").to_s) + expect(out).to include(default_bundle_path("gems", "rake-#{rake_version}").to_s) expect(out).to include(default_bundle_path("gems", "rails-2.3.2").to_s) # Gem names are the last component of their path. @@ -64,10 +70,41 @@ RSpec.describe "bundle show" do it "prints summary of gems" do bundle "show --verbose" - expect(out).to include("* actionmailer (2.3.2)") - expect(out).to include("\tSummary: This is just a fake gem for testing") - expect(out).to include("\tHomepage: No website available.") - expect(out).to include("\tStatus: Up to date") + 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 @@ -83,11 +120,11 @@ RSpec.describe "bundle show" do expect(the_bundle).to include_gems "foo 1.0" bundle :show - expect(out).to include("foo (1.0 #{@git.ref_for("master", 6)}") + expect(out).to include("foo (1.0 #{@git.ref_for("main", 6)}") end - it "prints out branch names other than master" do - update_git "foo", :branch => "omg" do |s| + it "prints out branch names other than main" do + update_git "foo", branch: "omg" do |s| s.write "lib/foo.rb", "FOO = '1.0.omg'" end @revision = revision_for(lib_path("foo-1.0"))[0...6] @@ -111,81 +148,70 @@ RSpec.describe "bundle show" do expect(out).to include("foo (1.0 #{sha[0..6]})") end - it "handles when a version is a '-' prerelease", :rubygems => "2.1" do - @git = build_git("foo", "1.0.0-beta.1", :path => lib_path("foo")) + 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 + bundle :show expect(out).to include("foo (1.0.0.pre.beta.1") end end context "in a fresh gem in a blank git repo" do before :each do - build_git "foo", :path => lib_path("foo") - in_app_root_custom lib_path("foo") - File.open("Gemfile", "w") {|f| f.puts "gemspec" } - sys_exec "rm -rf .git && git init" + build_git "foo", path: lib_path("foo") + File.open(lib_path("foo/Gemfile"), "w") {|f| f.puts "gemspec" } + sys_exec "rm -rf .git && git init", dir: lib_path("foo") end it "does not output git errors" do - bundle :show - expect(err).to lack_errors + bundle :show, dir: lib_path("foo") + expect(err_without_deprecations).to be_empty end end it "performs an automatic bundle install" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "foo" G - bundle "config auto_install 1" + bundle_config "auto_install 1" bundle :show expect(out).to include("Installing foo 1.0") end - context "with an invalid regexp for gem name" do - it "does not find the gem" do + context "with a valid regexp for gem name" do + it "presents alternatives", :readline do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rails" + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" G - invalid_regexp = "[]" - - bundle "show #{invalid_regexp}" - expect(out).to include("Could not find gem '#{invalid_regexp}'.") + bundle "show rac" + expect(out).to match(/\A1 : myrack\n2 : myrack-obama\n0 : - exit -(\n>|\z)/) end end - context "--outdated option" do - # Regression test for https://github.com/bundler/bundler/issues/5375 - before do - build_repo2 - end - - it "doesn't update gems to newer versions" do - install_gemfile! <<-G - source "file://#{gem_repo2}" + 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 - expect(the_bundle).to include_gem("rails 2.3.2") - - update_repo2 do - build_gem "rails", "3.0.0" do |s| - s.executables = "rails" - end - end - - bundle! "show --outdated" + invalid_regexp = "[]" - bundle! "install" - expect(the_bundle).to include_gem("rails 2.3.2") + 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 index 4992e428da..03a3786d80 100644 --- a/spec/bundler/commands/update_spec.rb +++ b/spec/bundler/commands/update_spec.rb @@ -1,41 +1,148 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle update" do - before :each do - build_repo2 + describe "with no arguments" do + before do + build_repo2 - install_gemfile <<-G - source "file://#{gem_repo2}" - gem "activesupport" - gem "rack-obama" - G - end + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + gem "platform_specific" + G + end - describe "with no arguments" do it "updates the entire bundle" do update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + build_gem "activesupport", "3.0" end bundle "update" expect(out).to include("Bundle updated!") - expect(the_bundle).to include_gems "rack 1.2", "rack-obama 1.0", "activesupport 3.0" + expect(the_bundle).to include_gems "myrack 1.2", "myrack-obama 1.0", "activesupport 3.0" end it "doesn't delete the Gemfile.lock file if something goes wrong" do gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "activesupport" - gem "rack-obama" + gem "myrack-obama" exit! G - bundle "update" - expect(bundled_app("Gemfile.lock")).to exist + bundle "update", raise_on_error: false + expect(bundled_app_lock).to exist + end + end + + describe "with --verbose" do + before do + build_repo2 + + install_gemfile <<~G + source "https://gem.repo2" + gem "myrack" + G + end + + it "logs the reason for re-resolving" do + bundle "update --verbose" + expect(out).not_to include("Found changes from the lockfile") + expect(out).to include("Re-resolving dependencies because bundler is unlocking") + end + end + + describe "with --all" 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!") @@ -43,100 +150,622 @@ RSpec.describe "bundle update" do end describe "with a top level dependency" do + before do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + gem "platform_specific" + G + end + it "unlocks all child dependencies that are unrelated to other locked dependencies" do update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + build_gem "activesupport", "3.0" end - bundle "update rack-obama" - expect(the_bundle).to include_gems "rack 1.2", "rack-obama 1.0", "activesupport 2.3.5" + bundle "update myrack-obama" + expect(the_bundle).to include_gems "myrack 1.2", "myrack-obama 1.0", "activesupport 2.3.5" end end describe "with an unknown dependency" do + before do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + gem "platform_specific" + G + end + it "should inform the user" do - bundle "update halting-problem-solver" - expect(out).to include "Could not find gem 'halting-problem-solver'" + bundle "update halting-problem-solver", raise_on_error: false + expect(err).to include "Could not find gem 'halting-problem-solver'" end it "should suggest alternatives" do - bundle "update active-support" - expect(out).to include "Did you mean activesupport?" + bundle "update platformspecific", raise_on_error: false + expect(err).to include "Did you mean 'platform_specific'?" end end describe "with a child dependency" do + before do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + gem "myrack-obama" + gem "platform_specific" + G + end + it "should update the child dependency" do - update_repo2 - bundle "update rack" - expect(the_bundle).to include_gems "rack 1.2" + update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + end + + bundle "update myrack" + expect(the_bundle).to include_gems "myrack 1.2" end end describe "when a possible resolve requires an older version of a locked gem" do - context "and only_update_to_newer_versions is set" do - before do - bundle! "config only_update_to_newer_versions true" + it "does not go to an older version" do + build_repo4 do + build_gem "tilt", "2.0.8" + build_gem "slim", "3.0.9" do |s| + s.add_dependency "tilt", [">= 1.3.3", "< 2.1"] + end + build_gem "slim_lint", "0.16.1" do |s| + s.add_dependency "slim", [">= 3.0", "< 5.0"] + end + build_gem "slim-rails", "0.2.1" do |s| + s.add_dependency "slim", ">= 0.9.2" + end + build_gem "slim-rails", "3.1.3" do |s| + s.add_dependency "slim", "~> 3.0" + end end - it "does not go to an older version" do - build_repo4 do - build_gem "a" do |s| - s.add_dependency "b" - s.add_dependency "c" - end - build_gem "b" - build_gem "c" - build_gem "c", "2.0" + + install_gemfile <<-G + source "https://gem.repo4" + gem "slim-rails" + gem "slim_lint" + G + + expect(the_bundle).to include_gems("slim 3.0.9", "slim-rails 3.1.3", "slim_lint 0.16.1") + + build_repo4 do + build_gem "slim", "4.0.0" do |s| + s.add_dependency "tilt", [">= 2.0.6", "< 2.1"] end + end - install_gemfile! <<-G - source "file:#{gem_repo4}" - gem "a" - G + bundle "update", all: true + + expect(the_bundle).to include_gems("slim 3.0.9", "slim-rails 3.1.3", "slim_lint 0.16.1") + end - expect(the_bundle).to include_gems("a 1.0", "b 1.0", "c 2.0") + 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) - update_repo4 do - build_gem "b", "2.0" do |s| - s.add_dependency "c", "< 2" - end + 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" + bundle "update", all: true, verbose: true + expect(the_bundle).to include_gems("a 1.0", "b 1.0", "c 2.0") + end - expect(the_bundle).to include_gems("a 1.0", "b 1.0", "c 2.0") + it "should still downgrade if forced by the Gemfile" do + build_repo4 do + build_gem "a" + build_gem "b", "1.0" + build_gem "b", "2.0" end + + install_gemfile <<-G + source "https://gem.repo4" + gem "a" + gem "b" + G + + expect(the_bundle).to include_gems("a 1.0", "b 2.0") + + gemfile <<-G + source "https://gem.repo4" + gem "a" + gem "b", "1.0" + G + + bundle "update b" + + expect(the_bundle).to include_gems("a 1.0", "b 1.0") + end + + it "should still downgrade if forced by the Gemfile, when transitive dependencies also need downgrade" do + build_repo4 do + build_gem "activesupport", "6.1.4.1" do |s| + s.add_dependency "tzinfo", "~> 2.0" + end + + build_gem "activesupport", "6.0.4.1" do |s| + s.add_dependency "tzinfo", "~> 1.1" + end + + build_gem "tzinfo", "2.0.4" + build_gem "tzinfo", "1.2.9" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "activesupport", "~> 6.1.0" + G + + expect(the_bundle).to include_gems("activesupport 6.1.4.1", "tzinfo 2.0.4") + + gemfile <<-G + source "https://gem.repo4" + gem "activesupport", "~> 6.0.0" + G + + original_lockfile = lockfile + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "activesupport", "6.0.4.1" + c.checksum gem_repo4, "tzinfo", "1.2.9" + end + + expected_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + activesupport (6.0.4.1) + tzinfo (~> 1.1) + tzinfo (1.2.9) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + activesupport (~> 6.0.0) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update activesupport" + expect(the_bundle).to include_gems("activesupport 6.0.4.1", "tzinfo 1.2.9") + expect(lockfile).to eq(expected_lockfile) + + lockfile original_lockfile + bundle "update" + expect(the_bundle).to include_gems("activesupport 6.0.4.1", "tzinfo 1.2.9") + expect(lockfile).to eq(expected_lockfile) + + lockfile original_lockfile + bundle "lock --update" + expect(the_bundle).to include_gems("activesupport 6.0.4.1", "tzinfo 1.2.9") + expect(lockfile).to eq(expected_lockfile) end end describe "with --local option" do - it "doesn't hit repo2" do - FileUtils.rm_rf(gem_repo2) + before do + build_repo2 - bundle "update --local" - expect(out).not_to match(/Fetching source index/) + 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 - it "should update only specifed group gems" do + before do + build_repo2 + end + + it "should update only specified group gems" do install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "activesupport", :group => :development - gem "rack" + gem "myrack" G update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + build_gem "activesupport", "3.0" end bundle "update --group development" expect(the_bundle).to include_gems "activesupport 3.0" - expect(the_bundle).not_to include_gems "rack 1.2" + expect(the_bundle).not_to include_gems "myrack 1.2" + end + + context "when conservatively updating a group with non-group sub-deps" do + it "should update only specified group gems" do + install_gemfile <<-G + source "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 :each do - build_git "foo", :path => lib_path("activesupport") + before do + build_git "foo", path: lib_path("activesupport") install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "activesupport", :group => :development gem "foo", :git => "#{lib_path("activesupport")}" G @@ -144,56 +773,111 @@ RSpec.describe "bundle update" do it "should not update the gems from that source" do update_repo2 { build_gem "activesupport", "3.0" } - update_git "foo", "2.0", :path => lib_path("activesupport") + update_git "foo", "2.0", path: lib_path("activesupport") bundle "update --group development" expect(the_bundle).to include_gems "activesupport 3.0" 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 "install --deployment" - bundle "update" + bundle_config "deployment true" + bundle "update", all: true, raise_on_error: false - expect(out).to match(/You are trying to install in deployment mode after changing.your Gemfile/m) - expect(out).to match(/freeze \nby running `bundle install --no-deployment`./m) - expect(exitstatus).not_to eq(0) if exitstatus + 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 suggest different command when frozen is set globally" do - bundler "config --global frozen 1" - bundle "update" - expect(out).to match(/You are trying to install in deployment mode after changing.your Gemfile/m) - expect(out).to match(/freeze \nby running `bundle config --delete frozen`./m) + 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 - it "should not update gems not included in the source that happen to have the same name" do - pending("Allowed to fail to preserve backwards-compatibility") + 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 "file://#{gem_repo2}" + 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" + expect(the_bundle).not_to include_gem "activesupport 3.0" end - it "should update gems not included in the source that happen to have the same name" do + it "should not update gems not included in the source that happen to have the same name" do install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "activesupport" G update_repo2 { build_gem "activesupport", "3.0" } bundle "update --source activesupport" - expect(the_bundle).to include_gems "activesupport 3.0" + expect(the_bundle).not_to include_gems "activesupport 3.0" end end @@ -207,7 +891,7 @@ RSpec.describe "bundle update" do end install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "harry" gem "fred" G @@ -222,8 +906,7 @@ RSpec.describe "bundle update" do end bundle "update --source harry" - expect(the_bundle).to include_gems "harry 2.0" - expect(the_bundle).to include_gems "fred 1.0" + expect(the_bundle).to include_gems "harry 1.0", "fred 1.0" end end @@ -240,7 +923,7 @@ RSpec.describe "bundle update" do end install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "harry" gem "fred" G @@ -255,41 +938,233 @@ RSpec.describe "bundle update" do end bundle "update --source harry" - expect(the_bundle).to include_gems "harry 2.0" - expect(the_bundle).to include_gems "fred 1.0" - expect(the_bundle).to include_gems "george 1.0" + expect(the_bundle).to include_gems "harry 1.0", "fred 1.0", "george 1.0" + end + end + + it "shows the previous version of the gem when updated from rubygems source" do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + G + + bundle "update", all: true, verbose: true + expect(out).to include("Using activesupport 2.3.5") + + update_repo2 do + build_gem "activesupport", "3.0" + end + + bundle "update", all: true + expect(out).to include("Installing activesupport 3.0 (was 2.3.5)") + end + + it "only prints `Using` for versions that have changed" do + build_repo4 do + build_gem "bar" + build_gem "foo" + end + + 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 :each do + before do build_repo2 end it "will eagerly unlock dependencies of a specified gem" do install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "thin" - gem "rack-obama" + gem "myrack-obama" G update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + build_gem "thin", "2.0" do |s| - s.add_dependency "rack" + s.add_dependency "myrack" end end bundle "update thin" - expect(the_bundle).to include_gems "thin 2.0", "rack 1.2", "rack-obama 1.0" + expect(the_bundle).to include_gems "thin 2.0", "myrack 1.2", "myrack-obama 1.0" + end + + it "will warn when some explicitly updated gems are not updated" do + install_gemfile <<-G + source "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 "file://#{gem_repo2}" + source "https://gem.repo2" - source "file://#{gem_repo1}" do + source "https://gem.repo1" do gem "thin" end G @@ -298,9 +1173,85 @@ RSpec.describe "bundle update in more complicated situations" do build_gem "thin", "2.0" end - bundle "update" + bundle "update", artifice: "compact_index" expect(the_bundle).to include_gems "thin 1.0" end + + context "when the lockfile is for a different platform" do + 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 @@ -308,107 +1259,69 @@ RSpec.describe "bundle update without a Gemfile.lock" do build_repo2 gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" - gem "rack", "1.0" + gem "myrack", "1.0" G - bundle "update" + bundle "update", all: true - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end end RSpec.describe "bundle update when a gem depends on a newer version of bundler" do - before(:each) do + before do build_repo2 do build_gem "rails", "3.0.1" do |s| - s.add_dependency "bundler", Bundler::VERSION.succ + s.add_dependency "bundler", "9.9.9" end + + build_gem "bundler", "9.9.9" end gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "rails", "3.0.1" G end - it "should not explode" do - bundle "update" - expect(err).to lack_errors - end - - it "should explain that bundler conflicted" do - bundle "update" - expect(out).not_to match(/in snapshot/i) - expect(out).to match(/current Bundler version/i) - expect(out).to match(/perhaps you need to update bundler/i) - end -end - -RSpec.describe "bundle update" do - it "shows the previous version of the gem when updated from rubygems source" do - build_repo2 - - install_gemfile <<-G - source "file://#{gem_repo2}" - gem "activesupport" - G - - bundle "update" - expect(out).to include("Using activesupport 2.3.5") - - update_repo2 do - build_gem "activesupport", "3.0" - end - - bundle "update" - expect(out).to include("Installing activesupport 3.0 (was 2.3.5)") - end - - it "shows error message when Gemfile.lock is not preset and gem is specified" do - install_gemfile <<-G - source "file://#{gem_repo2}" - gem "activesupport" - G - - bundle "update nonexisting" - expect(out).to include("This Bundle hasn't been installed yet. Run `bundle install` to update and install the bundled gems.") - expect(exitstatus).to eq(22) if exitstatus + 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 - before do - install_gemfile <<-G - ::RUBY_VERSION = '2.1.3' - ::RUBY_PATCHLEVEL = 100 - ruby '~> 2.1.0' - G - bundle "update --ruby" - end - context "when the Gemfile removes the ruby" do before do install_gemfile <<-G - ::RUBY_VERSION = '2.1.4' - ::RUBY_PATCHLEVEL = 222 + ruby '~> #{Gem.ruby_version}' + source "https://gem.repo1" + G + + gemfile <<-G + source "https://gem.repo1" G end + it "removes the Ruby from the Gemfile.lock" do bundle "update --ruby" - lockfile_should_be <<-L + expect(lockfile).to eq <<~L GEM + remote: https://gem.repo1/ specs: PLATFORMS - ruby + #{lockfile_platforms} DEPENDENCIES - + #{checksums_section_when_enabled} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L end end @@ -416,27 +1329,33 @@ RSpec.describe "bundle update --ruby" do context "when the Gemfile specified an updated Ruby version" do before do install_gemfile <<-G - ::RUBY_VERSION = '2.1.4' - ::RUBY_PATCHLEVEL = 222 - ruby '~> 2.1.0' + ruby '~> #{Gem.ruby_version}' + source "https://gem.repo1" + G + + gemfile <<-G + ruby '~> #{current_ruby_minor}' + source "https://gem.repo1" G end + it "updates the Gemfile.lock with the latest version" do bundle "update --ruby" - lockfile_should_be <<-L - GEM - specs: - - PLATFORMS - ruby + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: - DEPENDENCIES + PLATFORMS + #{lockfile_platforms} - RUBY VERSION - ruby 2.1.4p222 + DEPENDENCIES + #{checksums_section_when_enabled} + RUBY VERSION + #{Bundler::RubyVersion.system} - BUNDLED WITH + BUNDLED WITH #{Bundler::VERSION} L end @@ -445,45 +1364,435 @@ RSpec.describe "bundle update --ruby" do context "when a different Ruby is being used than has been versioned" do before do install_gemfile <<-G - ::RUBY_VERSION = '2.2.2' - ::RUBY_PATCHLEVEL = 505 + ruby '~> #{Gem.ruby_version}' + source "https://gem.repo1" + G + + gemfile <<-G ruby '~> 2.1.0' + source "https://gem.repo1" G end it "shows a helpful error message" do - bundle "update --ruby" + bundle "update --ruby", raise_on_error: false - expect(out).to include("Your Ruby version is 2.2.2, but your Gemfile specified ~> 2.1.0") + expect(err).to include("Your Ruby version is #{Bundler::RubyVersion.system.gem_version}, but your Gemfile specified ~> 2.1.0") end end context "when updating Ruby version and Gemfile `ruby`" do before do - install_gemfile <<-G - ::RUBY_VERSION = '1.8.3' - ::RUBY_PATCHLEVEL = 55 - ruby '~> 1.8.0' + lockfile <<~L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + + CHECKSUMS + + RUBY VERSION + ruby 2.1.4p222 + + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile <<-G + ruby '~> #{Gem.ruby_version}' + source "https://gem.repo1" G end + it "updates the Gemfile.lock with the latest version" do bundle "update --ruby" - lockfile_should_be <<-L + expect(lockfile).to eq <<~L GEM + remote: https://gem.repo1/ specs: PLATFORMS - ruby + #{lockfile_platforms} DEPENDENCIES - + #{checksums_section_when_enabled} RUBY VERSION - ruby 1.8.3p55 + #{Bundler::RubyVersion.system} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} + L + end + end +end + +RSpec.describe "bundle update --bundler" do + it "updates the bundler version in the lockfile" 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 @@ -492,22 +1801,25 @@ 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| + 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| + 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| + build_gem "foo", %w[1.5.1] do |s| s.add_dependency "bar", "~> 3.0" end - build_gem "bar", %w(2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0) - build_gem "qux", %w(1.0.0 1.0.1 1.1.0 2.0.0) + build_gem "foo", %w[2.0.0.pre] do |s| + s.add_dependency "bar" + end + build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 2.1.2.pre 3.0.0 3.1.0.pre 4.0.0.pre] + build_gem "qux", %w[1.0.0 1.0.1 1.1.0 2.0.0] end # establish a lockfile set to 1.4.3 install_gemfile <<-G - source "file://#{gem_repo4}" + source "https://gem.repo4" gem 'foo', '1.4.3' gem 'bar', '2.0.3' gem 'qux', '1.0.0' @@ -516,12 +1828,21 @@ RSpec.describe "bundle update conservative" do # remove 1.4.3 requirement and bar altogether # to setup update specs below gemfile <<-G - source "file://#{gem_repo4}" + 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" @@ -530,7 +1851,7 @@ RSpec.describe "bundle update conservative" do end it "update all" do - bundle "update --patch" + bundle "update --patch", all: true expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.1" end @@ -552,41 +1873,67 @@ RSpec.describe "bundle update conservative" do end it "minor preferred" do - bundle "update --minor --strict" + 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| + 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 "isolated_dep", %w[2.0.1 2.0.2] - build_gem "shared_owner_a", %w(3.0.1 3.0.2) do |s| + 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| + 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) + build_gem "shared_dep", %w[5.0.1 5.0.2] end gemfile <<-G - source "file://#{gem_repo4}" + source "https://gem.repo4" gem 'isolated_owner' gem 'shared_owner_a' gem 'shared_owner_b' G - lockfile <<-L + lockfile <<~L GEM - remote: file://#{gem_repo4} + remote: https://gem.repo4/ specs: isolated_dep (2.0.1) isolated_owner (1.0.1) @@ -598,15 +1945,17 @@ RSpec.describe "bundle update conservative" do shared_dep (~> 5.0) PLATFORMS - ruby + #{local_platform} DEPENDENCIES + isolated_owner shared_owner_a shared_owner_b - isolated_owner + + CHECKSUMS BUNDLED WITH - 1.13.0 + #{Bundler::VERSION} L end @@ -625,12 +1974,55 @@ RSpec.describe "bundle update conservative" do it "should not eagerly unlock with --conservative" do bundle "update --conservative shared_owner_a isolated_owner" - expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.2", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + end + + it "should only update direct dependencies when fully updating with --conservative" do + bundle "update --conservative" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.2" + end + + it "should only change direct dependencies when updating the lockfile with --conservative" do + bundle "lock --update --conservative" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "isolated_dep", "2.0.1" + c.checksum gem_repo4, "isolated_owner", "1.0.2" + c.checksum gem_repo4, "shared_dep", "5.0.1" + c.checksum gem_repo4, "shared_owner_a", "3.0.2" + c.checksum gem_repo4, "shared_owner_b", "4.0.2" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + isolated_dep (2.0.1) + isolated_owner (1.0.2) + isolated_dep (~> 2.0) + shared_dep (5.0.1) + shared_owner_a (3.0.2) + shared_dep (~> 5.0) + shared_owner_b (4.0.2) + shared_dep (~> 5.0) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + isolated_owner + shared_owner_a + shared_owner_b + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L end it "should match bundle install conservative update behavior when not eagerly unlocking" do gemfile <<-G - source "file://#{gem_repo4}" + source "https://gem.repo4" gem 'isolated_owner', '1.0.2' gem 'shared_owner_a', '3.0.2' @@ -639,19 +2031,65 @@ RSpec.describe "bundle update conservative" do bundle "install" - expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.2", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + end + end + + context "when Gemfile dependencies have changed" do + before do + build_repo4 do + build_gem "nokogiri", "1.16.4" do |s| + s.platform = "arm64-darwin" + end + + build_gem "nokogiri", "1.16.4" do |s| + s.platform = "x86_64-linux" + end + + build_gem "prism", "0.25.0" + end + + gemfile <<~G + source "https://gem.repo4" + gem "nokogiri", ">=1.16.4" + gem "prism", ">=0.25.0" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.16.4-arm64-darwin) + nokogiri (1.16.4-x86_64-linux) + + PLATFORMS + arm64-darwin + x86_64-linux + + DEPENDENCIES + nokogiri (>= 1.16.4) + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "still works" do + simulate_platform "arm64-darwin-23" do + bundle "update" + end end end context "error handling" do before do - gemfile "" + gemfile "source 'https://gem.repo1'" end it "raises if too many flags are provided" do - bundle "update --patch --minor" + bundle "update --patch --minor", all: true, raise_on_error: false - expect(out).to eq "Provide only one of the following options: minor, patch" + 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 diff --git a/spec/bundler/commands/viz_spec.rb b/spec/bundler/commands/viz_spec.rb deleted file mode 100644 index 77112aace4..0000000000 --- a/spec/bundler/commands/viz_spec.rb +++ /dev/null @@ -1,150 +0,0 @@ -# frozen_string_literal: true -require "spec_helper" - -RSpec.describe "bundle viz", :ruby => "1.9.3", :if => Bundler.which("dot") do - let(:ruby_graphviz) do - graphviz_glob = base_system_gems.join("cache/ruby-graphviz*") - Pathname.glob(graphviz_glob).first - end - - before do - system_gems ruby_graphviz - end - - it "graphs gems from the Gemfile" do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - gem "rack-obama" - G - - bundle! "viz" - expect(out).to include("gem_graph.png") - - bundle! "viz", :format => "debug" - expect(out).to eq(strip_whitespace(<<-DOT).strip) - digraph Gemfile { - concentrate = "true"; - normalize = "true"; - nodesep = "0.55"; - edge[ weight = "2"]; - node[ fontname = "Arial, Helvetica, SansSerif"]; - edge[ fontname = "Arial, Helvetica, SansSerif" , fontsize = "12"]; - default [style = "filled", fillcolor = "#B9B9D5", shape = "box3d", fontsize = "16", label = "default"]; - rack [style = "filled", fillcolor = "#B9B9D5", label = "rack"]; - default -> rack [constraint = "false"]; - "rack-obama" [style = "filled", fillcolor = "#B9B9D5", label = "rack-obama"]; - default -> "rack-obama" [constraint = "false"]; - "rack-obama" -> rack; - } - debugging bundle viz... - DOT - end - - it "graphs gems that are prereleases" do - build_repo2 do - build_gem "rack", "1.3.pre" - end - - install_gemfile <<-G - source "file://#{gem_repo2}" - gem "rack", "= 1.3.pre" - gem "rack-obama" - G - - bundle! "viz" - expect(out).to include("gem_graph.png") - - bundle! "viz", :format => :debug, :version => true - expect(out).to eq(strip_whitespace(<<-EOS).strip) - digraph Gemfile { - concentrate = "true"; - normalize = "true"; - nodesep = "0.55"; - edge[ weight = "2"]; - node[ fontname = "Arial, Helvetica, SansSerif"]; - edge[ fontname = "Arial, Helvetica, SansSerif" , fontsize = "12"]; - default [style = "filled", fillcolor = "#B9B9D5", shape = "box3d", fontsize = "16", label = "default"]; - rack [style = "filled", fillcolor = "#B9B9D5", label = "rack\\n1.3.pre"]; - default -> rack [constraint = "false"]; - "rack-obama" [style = "filled", fillcolor = "#B9B9D5", label = "rack-obama\\n1.0"]; - default -> "rack-obama" [constraint = "false"]; - "rack-obama" -> rack; - } - debugging bundle viz... - EOS - end - - context "with another gem that has a graphviz file" do - before do - build_repo4 do - build_gem "graphviz", "999" do |s| - s.write("lib/graphviz.rb", "abort 'wrong graphviz gem loaded'") - end - end - - system_gems ruby_graphviz, "graphviz-999", :gem_repo => gem_repo4 - end - - it "loads the correct ruby-graphviz gem" do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - gem "rack-obama" - G - - bundle! "viz", :format => "debug" - expect(out).to eq(strip_whitespace(<<-DOT).strip) - digraph Gemfile { - concentrate = "true"; - normalize = "true"; - nodesep = "0.55"; - edge[ weight = "2"]; - node[ fontname = "Arial, Helvetica, SansSerif"]; - edge[ fontname = "Arial, Helvetica, SansSerif" , fontsize = "12"]; - default [style = "filled", fillcolor = "#B9B9D5", shape = "box3d", fontsize = "16", label = "default"]; - rack [style = "filled", fillcolor = "#B9B9D5", label = "rack"]; - default -> rack [constraint = "false"]; - "rack-obama" [style = "filled", fillcolor = "#B9B9D5", label = "rack-obama"]; - default -> "rack-obama" [constraint = "false"]; - "rack-obama" -> rack; - } - debugging bundle viz... - DOT - end - end - - context "--without option" do - it "one group" do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "activesupport" - - group :rails do - gem "rails" - end - G - - bundle! "viz --without=rails" - expect(out).to include("gem_graph.png") - end - - it "two groups" do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "activesupport" - - group :rack do - gem "rack" - end - - group :rails do - gem "rails" - end - G - - bundle! "viz --without=rails:rack" - expect(out).to include("gem_graph.png") - end - end -end |
