summaryrefslogtreecommitdiff
path: root/spec/bundler
diff options
context:
space:
mode:
Diffstat (limited to 'spec/bundler')
-rw-r--r--spec/bundler/bundler/build_metadata_spec.rb50
-rw-r--r--spec/bundler/bundler/bundler_spec.rb344
-rw-r--r--spec/bundler/bundler/ci_detector_spec.rb21
-rw-r--r--spec/bundler/bundler/cli_common_spec.rb22
-rw-r--r--spec/bundler/bundler/cli_spec.rb298
-rw-r--r--spec/bundler/bundler/compact_index_client/parser_spec.rb249
-rw-r--r--spec/bundler/bundler/compact_index_client/updater_spec.rb243
-rw-r--r--spec/bundler/bundler/current_ruby_spec.rb157
-rw-r--r--spec/bundler/bundler/definition_spec.rb376
-rw-r--r--spec/bundler/bundler/dependency_spec.rb49
-rw-r--r--spec/bundler/bundler/digest_spec.rb24
-rw-r--r--spec/bundler/bundler/dsl_spec.rb558
-rw-r--r--spec/bundler/bundler/endpoint_specification_spec.rb123
-rw-r--r--spec/bundler/bundler/env_spec.rb237
-rw-r--r--spec/bundler/bundler/environment_preserver_spec.rb87
-rw-r--r--spec/bundler/bundler/errors_spec.rb91
-rw-r--r--spec/bundler/bundler/fetcher/base_spec.rb77
-rw-r--r--spec/bundler/bundler/fetcher/compact_index_spec.rb109
-rw-r--r--spec/bundler/bundler/fetcher/dependency_spec.rb296
-rw-r--r--spec/bundler/bundler/fetcher/downloader_spec.rb302
-rw-r--r--spec/bundler/bundler/fetcher/gem_remote_fetcher_spec.rb60
-rw-r--r--spec/bundler/bundler/fetcher/index_spec.rb75
-rw-r--r--spec/bundler/bundler/fetcher_spec.rb260
-rw-r--r--spec/bundler/bundler/friendly_errors_spec.rb231
-rw-r--r--spec/bundler/bundler/gem_helper_spec.rb456
-rw-r--r--spec/bundler/bundler/gem_version_promoter_spec.rb163
-rw-r--r--spec/bundler/bundler/index_spec.rb36
-rw-r--r--spec/bundler/bundler/installer/gem_installer_spec.rb47
-rw-r--r--spec/bundler/bundler/installer/parallel_installer_spec.rb79
-rw-r--r--spec/bundler/bundler/installer/spec_installation_spec.rb89
-rw-r--r--spec/bundler/bundler/lockfile_parser_spec.rb303
-rw-r--r--spec/bundler/bundler/mirror_spec.rb331
-rw-r--r--spec/bundler/bundler/override_spec.rb175
-rw-r--r--spec/bundler/bundler/plugin/api/source_spec.rb88
-rw-r--r--spec/bundler/bundler/plugin/api_spec.rb83
-rw-r--r--spec/bundler/bundler/plugin/dsl_spec.rb38
-rw-r--r--spec/bundler/bundler/plugin/events_spec.rb32
-rw-r--r--spec/bundler/bundler/plugin/index_spec.rb275
-rw-r--r--spec/bundler/bundler/plugin/installer_spec.rb137
-rw-r--r--spec/bundler/bundler/plugin/source_list_spec.rb25
-rw-r--r--spec/bundler/bundler/plugin_spec.rb368
-rw-r--r--spec/bundler/bundler/remote_specification_spec.rb187
-rw-r--r--spec/bundler/bundler/resolver/candidate_spec.rb20
-rw-r--r--spec/bundler/bundler/resolver/cooldown_spec.rb148
-rw-r--r--spec/bundler/bundler/retry_spec.rb190
-rw-r--r--spec/bundler/bundler/ruby_dsl_spec.rb248
-rw-r--r--spec/bundler/bundler/ruby_version_spec.rb500
-rw-r--r--spec/bundler/bundler/rubygems_ext_spec.rb39
-rw-r--r--spec/bundler/bundler/rubygems_integration_spec.rb126
-rw-r--r--spec/bundler/bundler/settings/validator_spec.rb111
-rw-r--r--spec/bundler/bundler/settings_spec.rb380
-rw-r--r--spec/bundler/bundler/shared_helpers_spec.rb518
-rw-r--r--spec/bundler/bundler/source/git/git_proxy_spec.rb354
-rw-r--r--spec/bundler/bundler/source/git_spec.rb123
-rw-r--r--spec/bundler/bundler/source/path_spec.rb31
-rw-r--r--spec/bundler/bundler/source/rubygems/remote_spec.rb207
-rw-r--r--spec/bundler/bundler/source/rubygems_spec.rb104
-rw-r--r--spec/bundler/bundler/source_list_spec.rb475
-rw-r--r--spec/bundler/bundler/source_spec.rb174
-rw-r--r--spec/bundler/bundler/spec_set_spec.rb148
-rw-r--r--spec/bundler/bundler/specifications/foo.gemspec13
-rw-r--r--spec/bundler/bundler/stub_specification_spec.rb82
-rw-r--r--spec/bundler/bundler/ui/shell_spec.rb112
-rw-r--r--spec/bundler/bundler/ui_spec.rb41
-rw-r--r--spec/bundler/bundler/uri_credentials_filter_spec.rb137
-rw-r--r--spec/bundler/bundler/uri_normalizer_spec.rb25
-rw-r--r--spec/bundler/bundler/worker_spec.rb89
-rw-r--r--spec/bundler/bundler/yaml_serializer_spec.rb235
-rw-r--r--spec/bundler/cache/cache_path_spec.rb32
-rw-r--r--spec/bundler/cache/gems_spec.rb427
-rw-r--r--spec/bundler/cache/git_spec.rb511
-rw-r--r--spec/bundler/cache/path_spec.rb121
-rw-r--r--spec/bundler/cache/platform_spec.rb49
-rw-r--r--spec/bundler/commands/add_spec.rb448
-rw-r--r--spec/bundler/commands/binstubs_spec.rb333
-rw-r--r--spec/bundler/commands/cache_spec.rb620
-rw-r--r--spec/bundler/commands/check_spec.rb601
-rw-r--r--spec/bundler/commands/clean_spec.rb936
-rw-r--r--spec/bundler/commands/config_spec.rb646
-rw-r--r--spec/bundler/commands/console_spec.rb214
-rw-r--r--spec/bundler/commands/doctor_spec.rb185
-rw-r--r--spec/bundler/commands/exec_spec.rb1272
-rw-r--r--spec/bundler/commands/fund_spec.rb118
-rw-r--r--spec/bundler/commands/help_spec.rb92
-rw-r--r--spec/bundler/commands/info_spec.rb249
-rw-r--r--spec/bundler/commands/init_spec.rb207
-rw-r--r--spec/bundler/commands/install_spec.rb2115
-rw-r--r--spec/bundler/commands/issue_spec.rb16
-rw-r--r--spec/bundler/commands/licenses_spec.rb37
-rw-r--r--spec/bundler/commands/list_spec.rb315
-rw-r--r--spec/bundler/commands/lock_spec.rb2877
-rw-r--r--spec/bundler/commands/newgem_spec.rb2139
-rw-r--r--spec/bundler/commands/open_spec.rb175
-rw-r--r--spec/bundler/commands/outdated_spec.rb1369
-rw-r--r--spec/bundler/commands/platform_spec.rb1260
-rw-r--r--spec/bundler/commands/post_bundle_message_spec.rb183
-rw-r--r--spec/bundler/commands/pristine_spec.rb275
-rw-r--r--spec/bundler/commands/remove_spec.rb736
-rw-r--r--spec/bundler/commands/show_spec.rb217
-rw-r--r--spec/bundler/commands/ssl_spec.rb373
-rw-r--r--spec/bundler/commands/update_spec.rb2095
-rw-r--r--spec/bundler/commands/version_spec.rb65
-rw-r--r--spec/bundler/install/allow_offline_install_spec.rb95
-rw-r--r--spec/bundler/install/binstubs_spec.rb49
-rw-r--r--spec/bundler/install/bundler_spec.rb268
-rw-r--r--spec/bundler/install/cooldown_spec.rb272
-rw-r--r--spec/bundler/install/deploy_spec.rb493
-rw-r--r--spec/bundler/install/failure_spec.rb85
-rw-r--r--spec/bundler/install/force_spec.rb71
-rw-r--r--spec/bundler/install/gemfile/eval_gemfile_spec.rb122
-rw-r--r--spec/bundler/install/gemfile/force_ruby_platform_spec.rb136
-rw-r--r--spec/bundler/install/gemfile/gemspec_spec.rb737
-rw-r--r--spec/bundler/install/gemfile/git_spec.rb1745
-rw-r--r--spec/bundler/install/gemfile/groups_spec.rb345
-rw-r--r--spec/bundler/install/gemfile/install_if_spec.rb51
-rw-r--r--spec/bundler/install/gemfile/lockfile_spec.rb42
-rw-r--r--spec/bundler/install/gemfile/override_spec.rb401
-rw-r--r--spec/bundler/install/gemfile/path_spec.rb1017
-rw-r--r--spec/bundler/install/gemfile/platform_spec.rb638
-rw-r--r--spec/bundler/install/gemfile/ruby_spec.rb157
-rw-r--r--spec/bundler/install/gemfile/sources_spec.rb1313
-rw-r--r--spec/bundler/install/gemfile/specific_platform_spec.rb1973
-rw-r--r--spec/bundler/install/gemfile_spec.rb192
-rw-r--r--spec/bundler/install/gems/compact_index_spec.rb1012
-rw-r--r--spec/bundler/install/gems/dependency_api_fallback_spec.rb19
-rw-r--r--spec/bundler/install/gems/dependency_api_spec.rb656
-rw-r--r--spec/bundler/install/gems/env_spec.rb107
-rw-r--r--spec/bundler/install/gems/flex_spec.rb403
-rw-r--r--spec/bundler/install/gems/fund_spec.rb164
-rw-r--r--spec/bundler/install/gems/gemfile_source_header_spec.rb24
-rw-r--r--spec/bundler/install/gems/mirror_probe_spec.rb68
-rw-r--r--spec/bundler/install/gems/mirror_spec.rb39
-rw-r--r--spec/bundler/install/gems/native_extensions_spec.rb184
-rw-r--r--spec/bundler/install/gems/no_build_extension_spec.rb54
-rw-r--r--spec/bundler/install/gems/no_install_plugin_spec.rb53
-rw-r--r--spec/bundler/install/gems/post_install_spec.rb150
-rw-r--r--spec/bundler/install/gems/resolving_spec.rb786
-rw-r--r--spec/bundler/install/gems/standalone_spec.rb524
-rw-r--r--spec/bundler/install/gems/win32_spec.rb25
-rw-r--r--spec/bundler/install/gemspecs_spec.rb179
-rw-r--r--spec/bundler/install/git_spec.rb369
-rw-r--r--spec/bundler/install/global_cache_spec.rb305
-rw-r--r--spec/bundler/install/path_spec.rb214
-rw-r--r--spec/bundler/install/prereleases_spec.rb54
-rw-r--r--spec/bundler/install/process_lock_spec.rb113
-rw-r--r--spec/bundler/install/security_policy_spec.rb72
-rw-r--r--spec/bundler/install/yanked_spec.rb254
-rw-r--r--spec/bundler/lock/git_spec.rb258
-rw-r--r--spec/bundler/lock/lockfile_spec.rb2416
-rw-r--r--spec/bundler/other/cli_dispatch_spec.rb20
-rw-r--r--spec/bundler/other/cli_man_pages_spec.rb100
-rw-r--r--spec/bundler/other/ext_spec.rb50
-rw-r--r--spec/bundler/other/major_deprecation_spec.rb798
-rw-r--r--spec/bundler/plugins/command_spec.rb112
-rw-r--r--spec/bundler/plugins/hook_spec.rb331
-rw-r--r--spec/bundler/plugins/install_spec.rb466
-rw-r--r--spec/bundler/plugins/list_spec.rb60
-rw-r--r--spec/bundler/plugins/source/example_spec.rb456
-rw-r--r--spec/bundler/plugins/source_spec.rb111
-rw-r--r--spec/bundler/plugins/uninstall_spec.rb74
-rw-r--r--spec/bundler/quality_es_spec.rb61
-rw-r--r--spec/bundler/quality_spec.rb261
-rw-r--r--spec/bundler/realworld/double_check_spec.rb40
-rw-r--r--spec/bundler/realworld/edgecases_spec.rb75
-rw-r--r--spec/bundler/realworld/ffi_spec.rb57
-rw-r--r--spec/bundler/realworld/fixtures/tapioca/Gemfile5
-rw-r--r--spec/bundler/realworld/fixtures/tapioca/Gemfile.lock49
-rw-r--r--spec/bundler/realworld/fixtures/warbler/.gitignore1
-rw-r--r--spec/bundler/realworld/fixtures/warbler/Gemfile7
-rw-r--r--spec/bundler/realworld/fixtures/warbler/Gemfile.lock35
-rw-r--r--spec/bundler/realworld/fixtures/warbler/bin/warbler-example.rb3
-rw-r--r--spec/bundler/realworld/fixtures/warbler/demo/demo.gemspec10
-rw-r--r--spec/bundler/realworld/git_spec.rb11
-rw-r--r--spec/bundler/realworld/parallel_spec.rb66
-rw-r--r--spec/bundler/realworld/slow_perf_spec.rb35
-rw-r--r--spec/bundler/resolver/basic_spec.rb422
-rw-r--r--spec/bundler/resolver/platform_spec.rb423
-rw-r--r--spec/bundler/runtime/env_helpers_spec.rb236
-rw-r--r--spec/bundler/runtime/executable_spec.rb141
-rw-r--r--spec/bundler/runtime/gem_tasks_spec.rb158
-rw-r--r--spec/bundler/runtime/inline_spec.rb751
-rw-r--r--spec/bundler/runtime/load_spec.rb113
-rw-r--r--spec/bundler/runtime/platform_spec.rb468
-rw-r--r--spec/bundler/runtime/require_spec.rb480
-rw-r--r--spec/bundler/runtime/requiring_spec.rb15
-rw-r--r--spec/bundler/runtime/self_management_spec.rb279
-rw-r--r--spec/bundler/runtime/setup_spec.rb1710
-rw-r--r--spec/bundler/spec_helper.rb192
-rw-r--r--spec/bundler/support/activate.rb9
-rw-r--r--spec/bundler/support/artifice/compact_index.rb6
-rw-r--r--spec/bundler/support/artifice/compact_index_api_missing.rb13
-rw-r--r--spec/bundler/support/artifice/compact_index_basic_authentication.rb15
-rw-r--r--spec/bundler/support/artifice/compact_index_checksum_mismatch.rb16
-rw-r--r--spec/bundler/support/artifice/compact_index_concurrent_download.rb33
-rw-r--r--spec/bundler/support/artifice/compact_index_cooldown.rb6
-rw-r--r--spec/bundler/support/artifice/compact_index_creds_diff_host.rb39
-rw-r--r--spec/bundler/support/artifice/compact_index_etag_match.rb16
-rw-r--r--spec/bundler/support/artifice/compact_index_extra.rb6
-rw-r--r--spec/bundler/support/artifice/compact_index_extra_api.rb6
-rw-r--r--spec/bundler/support/artifice/compact_index_extra_api_missing.rb17
-rw-r--r--spec/bundler/support/artifice/compact_index_extra_missing.rb17
-rw-r--r--spec/bundler/support/artifice/compact_index_forbidden.rb13
-rw-r--r--spec/bundler/support/artifice/compact_index_host_redirect.rb21
-rw-r--r--spec/bundler/support/artifice/compact_index_mirror_down.rb21
-rw-r--r--spec/bundler/support/artifice/compact_index_no_checksums.rb16
-rw-r--r--spec/bundler/support/artifice/compact_index_no_gem.rb13
-rw-r--r--spec/bundler/support/artifice/compact_index_partial_update.rb38
-rw-r--r--spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb40
-rw-r--r--spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb42
-rw-r--r--spec/bundler/support/artifice/compact_index_precompiled_before.rb25
-rw-r--r--spec/bundler/support/artifice/compact_index_range_ignored.rb40
-rw-r--r--spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb34
-rw-r--r--spec/bundler/support/artifice/compact_index_rate_limited.rb48
-rw-r--r--spec/bundler/support/artifice/compact_index_redirects.rb21
-rw-r--r--spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb20
-rw-r--r--spec/bundler/support/artifice/compact_index_wrong_dependencies.rb17
-rw-r--r--spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb21
-rw-r--r--spec/bundler/support/artifice/endpoint.rb6
-rw-r--r--spec/bundler/support/artifice/endpoint_500.rb17
-rw-r--r--spec/bundler/support/artifice/endpoint_api_forbidden.rb13
-rw-r--r--spec/bundler/support/artifice/endpoint_basic_authentication.rb15
-rw-r--r--spec/bundler/support/artifice/endpoint_creds_diff_host.rb39
-rw-r--r--spec/bundler/support/artifice/endpoint_extra.rb33
-rw-r--r--spec/bundler/support/artifice/endpoint_extra_api.rb34
-rw-r--r--spec/bundler/support/artifice/endpoint_extra_missing.rb17
-rw-r--r--spec/bundler/support/artifice/endpoint_fallback.rb19
-rw-r--r--spec/bundler/support/artifice/endpoint_host_redirect.rb17
-rw-r--r--spec/bundler/support/artifice/endpoint_marshal_fail.rb6
-rw-r--r--spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb15
-rw-r--r--spec/bundler/support/artifice/endpoint_mirror_source.rb17
-rw-r--r--spec/bundler/support/artifice/endpoint_redirect.rb17
-rw-r--r--spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb20
-rw-r--r--spec/bundler/support/artifice/endpoint_timeout.rb15
-rw-r--r--spec/bundler/support/artifice/fail.rb27
-rw-r--r--spec/bundler/support/artifice/helpers/artifice.rb30
-rw-r--r--spec/bundler/support/artifice/helpers/compact_index.rb128
-rw-r--r--spec/bundler/support/artifice/helpers/compact_index_cooldown.rb13
-rw-r--r--spec/bundler/support/artifice/helpers/compact_index_extra.rb33
-rw-r--r--spec/bundler/support/artifice/helpers/compact_index_extra_api.rb48
-rw-r--r--spec/bundler/support/artifice/helpers/endpoint.rb113
-rw-r--r--spec/bundler/support/artifice/helpers/endpoint_extra.rb29
-rw-r--r--spec/bundler/support/artifice/helpers/endpoint_fallback.rb15
-rw-r--r--spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb9
-rw-r--r--spec/bundler/support/artifice/helpers/rack_request.rb100
-rw-r--r--spec/bundler/support/artifice/vcr.rb152
-rw-r--r--spec/bundler/support/artifice/windows.rb45
-rw-r--r--spec/bundler/support/build_metadata.rb53
-rw-r--r--spec/bundler/support/builders.rb749
-rwxr-xr-xspec/bundler/support/bundle6
-rw-r--r--spec/bundler/support/bundle.rb7
-rw-r--r--spec/bundler/support/checksums.rb135
-rw-r--r--spec/bundler/support/command_execution.rb78
-rw-r--r--spec/bundler/support/env.rb13
-rw-r--r--spec/bundler/support/filters.rb42
-rw-r--r--spec/bundler/support/hax.rb74
-rw-r--r--spec/bundler/support/helpers.rb631
-rw-r--r--spec/bundler/support/indexes.rb424
-rw-r--r--spec/bundler/support/matchers.rb227
-rw-r--r--spec/bundler/support/options.rb15
-rw-r--r--spec/bundler/support/path.rb381
-rw-r--r--spec/bundler/support/permissions.rb12
-rw-r--r--spec/bundler/support/platforms.rb75
-rw-r--r--spec/bundler/support/rubygems_ext.rb222
-rw-r--r--spec/bundler/support/rubygems_version_manager.rb124
-rw-r--r--spec/bundler/support/setup.rb9
-rw-r--r--spec/bundler/support/shards.rb200
-rw-r--r--spec/bundler/support/subprocess.rb115
-rw-r--r--spec/bundler/support/switch_rubygems.rb5
-rw-r--r--spec/bundler/support/the_bundle.rb41
-rw-r--r--spec/bundler/support/vendored_net_http.rb23
-rw-r--r--spec/bundler/update/force_spec.rb30
-rw-r--r--spec/bundler/update/gemfile_spec.rb47
-rw-r--r--spec/bundler/update/gems/fund_spec.rb50
-rw-r--r--spec/bundler/update/gems/post_install_spec.rb76
-rw-r--r--spec/bundler/update/git_spec.rb341
-rw-r--r--spec/bundler/update/path_spec.rb19
276 files changed, 66470 insertions, 0 deletions
diff --git a/spec/bundler/bundler/build_metadata_spec.rb b/spec/bundler/bundler/build_metadata_spec.rb
new file mode 100644
index 0000000000..2e69821f68
--- /dev/null
+++ b/spec/bundler/bundler/build_metadata_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require "bundler"
+require "bundler/build_metadata"
+
+RSpec.describe Bundler::BuildMetadata do
+ before do
+ allow(Time).to receive(:now).and_return(Time.at(0))
+ Bundler::BuildMetadata.instance_variable_set(:@timestamp, nil)
+ end
+
+ describe "#timestamp" do
+ it "returns %Y-%m-%d formatted current time if built_at not set" do
+ Bundler::BuildMetadata.instance_variable_set(:@built_at, nil)
+ expect(Bundler::BuildMetadata.timestamp).to eq "1970-01-01"
+ end
+
+ it "returns %Y-%m-%d formatted current time if built_at not set" do
+ Bundler::BuildMetadata.instance_variable_set(:@built_at, "2025-01-01")
+ expect(Bundler::BuildMetadata.timestamp).to eq "2025-01-01"
+ ensure
+ Bundler::BuildMetadata.instance_variable_set(:@built_at, nil)
+ end
+ end
+
+ describe "#git_commit_sha" do
+ context "if instance valuable is defined" do
+ before do
+ Bundler::BuildMetadata.instance_variable_set(:@git_commit_sha, "foo")
+ end
+
+ after do
+ Bundler::BuildMetadata.remove_instance_variable(:@git_commit_sha)
+ end
+
+ it "returns set value" do
+ expect(Bundler::BuildMetadata.git_commit_sha).to eq "foo"
+ end
+ end
+ end
+
+ describe "#to_h" do
+ subject { Bundler::BuildMetadata.to_h }
+
+ it "returns a hash includes Timestamp, and Git SHA" do
+ expect(subject["Timestamp"]).to eq "1970-01-01"
+ expect(subject["Git SHA"]).to be_instance_of(String)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/bundler_spec.rb b/spec/bundler/bundler/bundler_spec.rb
new file mode 100644
index 0000000000..bddcbdaef3
--- /dev/null
+++ b/spec/bundler/bundler/bundler_spec.rb
@@ -0,0 +1,344 @@
+# frozen_string_literal: true
+
+require "bundler"
+require "tmpdir"
+
+RSpec.describe Bundler do
+ describe "#load_marshal" do
+ it "is a private method and raises an error" do
+ data = Marshal.dump(Bundler)
+ expect { Bundler.load_marshal(data) }.to raise_error(NoMethodError, /private method [`']load_marshal' called/)
+ end
+
+ it "loads any data" do
+ data = Marshal.dump(Bundler)
+ expect(Bundler.send(:load_marshal, data)).to eq(Bundler)
+ end
+ end
+
+ describe "#safe_load_marshal" do
+ it "fails on unexpected class" do
+ data = Marshal.dump(Bundler)
+ expect { Bundler.safe_load_marshal(data) }.to raise_error(Bundler::MarshalError)
+ end
+
+ it "loads simple structure" do
+ simple_structure = { "name" => [:development] }
+ data = Marshal.dump(simple_structure)
+ expect(Bundler.safe_load_marshal(data)).to eq(simple_structure)
+ end
+
+ it "loads Gem::Specification" do
+ gem_spec = Gem::Specification.new do |s|
+ s.name = "bundler"
+ s.version = Gem::Version.new("2.4.7")
+ s.installed_by_version = Gem::Version.new("0")
+ s.authors = ["André Arko",
+ "Samuel Giddins",
+ "Colby Swandale",
+ "Hiroshi Shibata",
+ "David Rodríguez",
+ "Grey Baker",
+ "Stephanie Morillo",
+ "Chris Morris",
+ "James Wen",
+ "Tim Moore",
+ "André Medeiros",
+ "Jessica Lynn Suttles",
+ "Terence Lee",
+ "Carl Lerche",
+ "Yehuda Katz"]
+ s.date = Time.utc(2023, 2, 15)
+ s.description = "Bundler manages an application's dependencies through its entire life, across many machines, systematically and repeatably"
+ s.email = ["team@bundler.io"]
+ s.homepage = "https://bundler.io"
+ s.metadata = { "bug_tracker_uri" => "https://github.com/ruby/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3ABundler",
+ "changelog_uri" => "https://github.com/ruby/rubygems/blob/master/bundler/CHANGELOG.md",
+ "homepage_uri" => "https://bundler.io/",
+ "source_code_uri" => "https://github.com/ruby/rubygems/tree/master/bundler" }
+ s.require_paths = ["lib"]
+ s.required_ruby_version = Gem::Requirement.new([">= 2.6.0"])
+ s.required_rubygems_version = Gem::Requirement.new([">= 3.0.1"])
+ s.rubygems_version = "3.4.7"
+ s.specification_version = 4
+ s.summary = "The best way to manage your application's dependencies"
+ s.license = false
+ end
+ data = Marshal.dump(gem_spec)
+ expect(Bundler.safe_load_marshal(data)).to eq(gem_spec)
+ end
+ end
+
+ describe "#load_gemspec_uncached" do
+ let(:app_gemspec_path) { tmp("test.gemspec") }
+ subject { Bundler.load_gemspec_uncached(app_gemspec_path) }
+
+ context "with incorrect YAML file" do
+ before do
+ File.open(app_gemspec_path, "wb") do |f|
+ f.write <<~GEMSPEC
+ ---
+ {:!00 ao=gu\g1= 7~f
+ GEMSPEC
+ end
+ end
+
+ it "catches YAML syntax errors" do
+ expect { subject }.to raise_error(Bundler::GemspecError, /error while loading `test.gemspec`/)
+ end
+ end
+
+ context "with correct YAML file", if: defined?(Encoding) do
+ it "can load a gemspec with unicode characters with default ruby encoding" do
+ # spec_helper forces the external encoding to UTF-8 but that's not the
+ # default until Ruby 2.0
+ verbose = $VERBOSE
+ $VERBOSE = false
+ encoding = Encoding.default_external
+ Encoding.default_external = "ASCII"
+ $VERBOSE = verbose
+
+ File.open(app_gemspec_path, "wb") do |file|
+ file.puts <<~GEMSPEC
+ # -*- encoding: utf-8 -*-
+ Gem::Specification.new do |gem|
+ gem.author = "André the Giant"
+ end
+ GEMSPEC
+ end
+
+ expect(subject.author).to eq("André the Giant")
+
+ verbose = $VERBOSE
+ $VERBOSE = false
+ Encoding.default_external = encoding
+ $VERBOSE = verbose
+ end
+ end
+
+ it "sets loaded_from" do
+ app_gemspec_path.open("w") do |f|
+ f.puts <<-GEMSPEC
+ Gem::Specification.new do |gem|
+ gem.name = "validated"
+ end
+ GEMSPEC
+ end
+
+ expect(subject.loaded_from).to eq(app_gemspec_path.expand_path.to_s)
+ end
+
+ context "validate is true" do
+ subject { Bundler.load_gemspec_uncached(app_gemspec_path, true) }
+
+ it "validates the specification" do
+ app_gemspec_path.open("w") do |f|
+ f.puts <<-GEMSPEC
+ Gem::Specification.new do |gem|
+ gem.name = "validated"
+ end
+ GEMSPEC
+ end
+ expect(Bundler.rubygems).to receive(:validate).with have_attributes(name: "validated")
+ subject
+ end
+ end
+
+ context "with gemspec containing local variables" do
+ before do
+ File.open(app_gemspec_path, "wb") do |f|
+ f.write <<~GEMSPEC
+ must_not_leak = true
+ Gem::Specification.new do |gem|
+ gem.name = "leak check"
+ end
+ GEMSPEC
+ end
+ end
+
+ it "should not pollute the TOPLEVEL_BINDING" do
+ subject
+ expect(TOPLEVEL_BINDING.eval("local_variables")).to_not include(:must_not_leak)
+ end
+ end
+ end
+
+ describe "#which" do
+ it "can detect relative path" do
+ script_path = bundled_app("tmp/test_command")
+ create_file(script_path, "#!/usr/bin/env ruby\n")
+
+ result = Dir.chdir script_path.dirname.dirname do
+ Bundler.which("test_command")
+ end
+ expect(result).to eq(nil)
+
+ result = Dir.chdir script_path.dirname do
+ Bundler.which("test_command")
+ end
+
+ expect(result).to eq("test_command") unless Gem.win_platform?
+ expect(result).to eq("test_command.bat") if Gem.win_platform?
+ end
+
+ it "can detect absolute path" do
+ create_file("test_command", "#!/usr/bin/env ruby\n")
+
+ ENV["PATH"] = bundled_app("test_command").parent.to_s
+
+ result = Bundler.which("test_command")
+ expect(result).to eq(bundled_app("test_command").to_s) unless Gem.win_platform?
+ expect(result).to eq(bundled_app("test_command.bat").to_s) if Gem.win_platform?
+ end
+
+ it "returns nil when not found" do
+ result = Bundler.which("test_command")
+ expect(result).to eq(nil)
+ end
+ end
+
+ describe "configuration" do
+ context "disable_shared_gems" do
+ it "should unset GEM_PATH with empty string" do
+ expect(Bundler).to receive(:use_system_gems?).and_return(false)
+ Bundler.send(:configure_gem_path)
+ expect(ENV["GEM_PATH"]).to eq ""
+ end
+ end
+ end
+
+ describe "#mkdir_p" do
+ it "creates a folder at the given path" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ allow(Bundler).to receive(:root).and_return(bundled_app)
+
+ Bundler.mkdir_p(bundled_app("foo", "bar"))
+ expect(bundled_app("foo", "bar")).to exist
+ end
+ end
+
+ describe "#user_home" do
+ context "home directory is set" do
+ it "should return the user home" do
+ path = "/home/oggy"
+ allow(Bundler.rubygems).to receive(:user_home).and_return(path)
+ allow(File).to receive(:directory?).with(path).and_return true
+ allow(File).to receive(:writable?).with(path).and_return true
+ expect(Bundler.user_home).to eq(Pathname(path))
+ end
+
+ context "is not a directory" do
+ it "should issue a warning and return a temporary user home" do
+ path = "/home/oggy"
+ allow(Bundler.rubygems).to receive(:user_home).and_return(path)
+ allow(File).to receive(:directory?).with(path).and_return false
+ allow(Bundler).to receive(:tmp).and_return(Pathname.new("/tmp/trulyrandom"))
+ expect(Bundler.ui).to receive(:warn).with("`/home/oggy` is not a directory.\n")
+ expect(Bundler.ui).to receive(:warn).with("Bundler will use `/tmp/trulyrandom' as your home directory temporarily.\n")
+ expect(Bundler.user_home).to eq(Pathname("/tmp/trulyrandom"))
+ end
+ end
+
+ context "is not writable" do
+ let(:path) { "/home/oggy" }
+ let(:dotbundle) { "/home/oggy/.bundle" }
+
+ it "should issue a warning and return a temporary user home" do
+ allow(Bundler.rubygems).to receive(:user_home).and_return(path)
+ allow(File).to receive(:directory?).with(path).and_return true
+ allow(File).to receive(:writable?).and_call_original
+ allow(File).to receive(:writable?).with(path).and_return false
+ allow(File).to receive(:directory?).with(dotbundle).and_return false
+ allow(Bundler).to receive(:tmp).and_return(Pathname.new("/tmp/trulyrandom"))
+ expect(Bundler.ui).to receive(:warn).with("`/home/oggy` is not writable.\n")
+ expect(Bundler.ui).to receive(:warn).with("Bundler will use `/tmp/trulyrandom' as your home directory temporarily.\n")
+ expect(Bundler.user_home).to eq(Pathname("/tmp/trulyrandom"))
+ end
+
+ context ".bundle exists and have correct permissions" do
+ it "should return the user home" do
+ allow(Bundler.rubygems).to receive(:user_home).and_return(path)
+ allow(File).to receive(:directory?).with(path).and_return true
+ allow(File).to receive(:writable?).with(path).and_return false
+ allow(File).to receive(:directory?).with(dotbundle).and_return true
+ allow(File).to receive(:writable?).with(dotbundle).and_return true
+ expect(Bundler.user_home).to eq(Pathname(path))
+ end
+ end
+ end
+ end
+
+ context "home directory is not set" do
+ it "should issue warning and return a temporary user home" do
+ allow(Bundler.rubygems).to receive(:user_home).and_return(nil)
+ allow(Bundler).to receive(:tmp).and_return(Pathname.new("/tmp/trulyrandom"))
+ expect(Bundler.ui).to receive(:warn).with("Your home directory is not set.\n")
+ expect(Bundler.ui).to receive(:warn).with("Bundler will use `/tmp/trulyrandom' as your home directory temporarily.\n")
+ expect(Bundler.user_home).to eq(Pathname("/tmp/trulyrandom"))
+ end
+ end
+ end
+
+ context "user cache dir" do
+ let(:home_path) { Pathname.new(ENV["HOME"]) }
+
+ let(:xdg_data_home) { home_path.join(".local") }
+ let(:xdg_cache_home) { home_path.join(".cache") }
+ let(:xdg_config_home) { home_path.join(".config") }
+
+ let(:bundle_user_home_default) { home_path.join(".bundle") }
+ let(:bundle_user_home_custom) { xdg_data_home.join("bundle") }
+
+ let(:bundle_user_cache_default) { bundle_user_home_default.join("cache") }
+ let(:bundle_user_cache_custom) { xdg_cache_home.join("bundle") }
+
+ let(:bundle_user_config_default) { bundle_user_home_default.join("config") }
+ let(:bundle_user_config_custom) { xdg_config_home.join("bundle") }
+
+ let(:bundle_user_plugin_default) { bundle_user_home_default.join("plugin") }
+ let(:bundle_user_plugin_custom) { xdg_data_home.join("bundle").join("plugin") }
+
+ describe "#user_bundle_path" do
+ before do
+ allow(Bundler.rubygems).to receive(:user_home).and_return(home_path)
+ end
+
+ it "should use the default home path" do
+ expect(Bundler.user_bundle_path).to eq(bundle_user_home_default)
+ expect(Bundler.user_bundle_path("home")).to eq(bundle_user_home_default)
+ expect(Bundler.user_bundle_path("cache")).to eq(bundle_user_cache_default)
+ expect(Bundler.user_cache).to eq(bundle_user_cache_default)
+ expect(Bundler.user_bundle_path("config")).to eq(bundle_user_config_default)
+ expect(Bundler.user_bundle_path("plugin")).to eq(bundle_user_plugin_default)
+ end
+
+ it "should use custom home path as root for other paths" do
+ ENV["BUNDLE_USER_HOME"] = bundle_user_home_custom.to_s
+ allow(Bundler.rubygems).to receive(:user_home).and_raise
+ expect(Bundler.user_bundle_path).to eq(bundle_user_home_custom)
+ expect(Bundler.user_bundle_path("home")).to eq(bundle_user_home_custom)
+ expect(Bundler.user_bundle_path("cache")).to eq(bundle_user_home_custom.join("cache"))
+ expect(Bundler.user_cache).to eq(bundle_user_home_custom.join("cache"))
+ expect(Bundler.user_bundle_path("config")).to eq(bundle_user_home_custom.join("config"))
+ expect(Bundler.user_bundle_path("plugin")).to eq(bundle_user_home_custom.join("plugin"))
+ end
+
+ it "should use all custom paths, except home" do
+ ENV.delete("BUNDLE_USER_HOME")
+ ENV["BUNDLE_USER_CACHE"] = bundle_user_cache_custom.to_s
+ ENV["BUNDLE_USER_CONFIG"] = bundle_user_config_custom.to_s
+ ENV["BUNDLE_USER_PLUGIN"] = bundle_user_plugin_custom.to_s
+ expect(Bundler.user_bundle_path).to eq(bundle_user_home_default)
+ expect(Bundler.user_bundle_path("home")).to eq(bundle_user_home_default)
+ expect(Bundler.user_bundle_path("cache")).to eq(bundle_user_cache_custom)
+ expect(Bundler.user_cache).to eq(bundle_user_cache_custom)
+ expect(Bundler.user_bundle_path("config")).to eq(bundle_user_config_custom)
+ expect(Bundler.user_bundle_path("plugin")).to eq(bundle_user_plugin_custom)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/ci_detector_spec.rb b/spec/bundler/bundler/ci_detector_spec.rb
new file mode 100644
index 0000000000..299d8005e8
--- /dev/null
+++ b/spec/bundler/bundler/ci_detector_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::CIDetector do
+ # This is properly tested in rubygems, under the name Gem::CIDetector
+ # But the test that confirms that our version _stays in sync_ with that version
+ # will live here.
+
+ it "stays in sync with the rubygems implementation" do
+ rubygems_implementation_path = File.join(git_root, "lib", "rubygems", "ci_detector.rb")
+ expect(File.exist?(rubygems_implementation_path)).to be_truthy
+ rubygems_code = File.read(rubygems_implementation_path)
+ denamespaced_rubygems_code = rubygems_code.sub("Gem", "NAMESPACE")
+
+ bundler_implementation_path = File.join(source_lib_dir, "bundler", "ci_detector.rb")
+ expect(File.exist?(bundler_implementation_path)).to be_truthy
+ bundler_code = File.read(bundler_implementation_path)
+ denamespaced_bundler_code = bundler_code.sub("Bundler", "NAMESPACE")
+
+ expect(denamespaced_bundler_code).to eq(denamespaced_rubygems_code)
+ end
+end
diff --git a/spec/bundler/bundler/cli_common_spec.rb b/spec/bundler/bundler/cli_common_spec.rb
new file mode 100644
index 0000000000..015894b3a1
--- /dev/null
+++ b/spec/bundler/bundler/cli_common_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "bundler/cli"
+
+RSpec.describe Bundler::CLI::Common do
+ describe "gem_not_found_message" do
+ it "should suggest alternate gem names" do
+ message = subject.gem_not_found_message("ralis", ["BOGUS"])
+ expect(message).to match("Could not find gem 'ralis'.$")
+ message = subject.gem_not_found_message("ralis", ["rails"])
+ expect(message).to match("Did you mean 'rails'?")
+ message = subject.gem_not_found_message("Rails", ["rails"])
+ expect(message).to match("Did you mean 'rails'?")
+ message = subject.gem_not_found_message("meail", %w[email fail eval])
+ expect(message).to match("Did you mean 'email'?")
+ message = subject.gem_not_found_message("nokogri", %w[nokogiri rails sidekiq dog])
+ expect(message).to match("Did you mean 'nokogiri'?")
+ message = subject.gem_not_found_message("methosd", %w[method methods bogus])
+ expect(message).to match(/Did you mean 'method(|s)' or 'method(|s)'?/)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/cli_spec.rb b/spec/bundler/bundler/cli_spec.rb
new file mode 100644
index 0000000000..56caf9937e
--- /dev/null
+++ b/spec/bundler/bundler/cli_spec.rb
@@ -0,0 +1,298 @@
+# frozen_string_literal: true
+
+require "bundler/cli"
+
+RSpec.describe "bundle executable" do
+ it "returns non-zero exit status when passed unrecognized options" do
+ bundle "--invalid_argument", raise_on_error: false
+ expect(exitstatus).to_not be_zero
+ end
+
+ it "returns non-zero exit status when passed unrecognized task" do
+ bundle "unrecognized-task", raise_on_error: false
+ expect(exitstatus).to_not be_zero
+ end
+
+ it "looks for a binary and executes it if it's named bundler-<task>" do
+ skip "Could not find command testtasks, probably because not a windows friendly executable" if Gem.win_platform?
+
+ File.open(tmp("bundler-testtasks"), "w", 0o755) do |f|
+ ruby = ENV["RUBY"] || "/usr/bin/env ruby"
+ f.puts "#!#{ruby}\nputs 'Hello, world'\n"
+ end
+
+ with_path_added(tmp) do
+ bundle "testtasks"
+ end
+
+ expect(out).to eq("Hello, world")
+ end
+
+ describe "aliases" do
+ it "aliases e to exec" do
+ bundle "e --help"
+
+ expect(out_with_macos_man_workaround).to include("bundle-exec")
+ end
+
+ it "aliases ex to exec" do
+ bundle "ex --help"
+
+ expect(out_with_macos_man_workaround).to include("bundle-exec")
+ end
+
+ it "aliases exe to exec" do
+ bundle "exe --help"
+
+ expect(out_with_macos_man_workaround).to include("bundle-exec")
+ end
+
+ it "aliases c to check" do
+ bundle "c --help"
+
+ expect(out_with_macos_man_workaround).to include("bundle-check")
+ end
+
+ it "aliases i to install" do
+ bundle "i --help"
+
+ expect(out_with_macos_man_workaround).to include("bundle-install")
+ end
+
+ it "aliases ls to list" do
+ bundle "ls --help"
+
+ expect(out_with_macos_man_workaround).to include("bundle-list")
+ end
+
+ it "aliases package to cache" do
+ bundle "package --help"
+
+ expect(out_with_macos_man_workaround).to include("bundle-cache")
+ end
+
+ it "aliases pack to cache" do
+ bundle "pack --help"
+
+ expect(out_with_macos_man_workaround).to include("bundle-cache")
+ end
+
+ private
+
+ # Some `man` (e.g., on macOS) always highlights the output even to
+ # non-tty.
+ def out_with_macos_man_workaround
+ out.gsub(/.[\b]/, "")
+ end
+ end
+
+ context "with no arguments" do
+ it "tries to installs by default but print help on missing Gemfile" do
+ bundle "", raise_on_error: false
+ expect(err).to include("Could not locate Gemfile")
+ expect(out).to include("In a future version of Bundler")
+
+ expect(out).to include("Bundler version #{Bundler::VERSION}").
+ and include("\n\nBundler commands:\n\n").
+ and include("\n\n Primary commands:\n").
+ and include("\n\n Utilities:\n").
+ and include("\n\nOptions:\n")
+ end
+
+ it "runs bundle install when default_cli_command set to install" do
+ bundle_config "default_cli_command install"
+ bundle "", raise_on_error: false
+ expect(out).to_not include("In a future version of Bundler")
+ expect(err).to include("Could not locate Gemfile")
+ expect(exitstatus).to_not be_zero
+ end
+ end
+
+ context "when ENV['BUNDLE_GEMFILE'] is set to an empty string" do
+ it "ignores it" do
+ gemfile bundled_app_gemfile, <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle :install, env: { "BUNDLE_GEMFILE" => "" }
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ context "with --verbose" do
+ before do
+ gemfile "source 'https://gem.repo1'"
+ end
+
+ it "prints the running command" do
+ bundle "info bundler", verbose: true
+ expect(out).to start_with("Running `bundle info bundler --verbose` with bundler #{Bundler::VERSION}")
+
+ bundle "install", verbose: true
+ expect(out).to start_with("Running `bundle install --verbose` with bundler #{Bundler::VERSION}")
+ end
+
+ it "prints the simulated version too when setting is enabled" do
+ bundle "config set simulate_version 4", verbose: true
+ bundle "info bundler", verbose: true
+ expect(out).to start_with("Running `bundle info bundler --verbose` with bundler #{Bundler::VERSION} (simulating Bundler 4)")
+ end
+ end
+
+ context "with verbose configuration" do
+ before do
+ bundle_config "verbose true"
+ end
+
+ it "prints the running command" do
+ gemfile "source 'https://gem.repo1'"
+ bundle "info bundler"
+ expect(out).to start_with("Running `bundle info bundler` with bundler #{Bundler::VERSION}")
+ end
+ end
+
+ describe "bundle outdated" do
+ let(:run_command) do
+ bundle "install"
+
+ bundle "outdated #{flags}", raise_on_error: false
+ end
+
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", '0.9.1'
+ G
+ end
+
+ context "with --groups flag" do
+ let(:flags) { "--groups" }
+
+ it "prints a message when there are outdated gems" do
+ run_command
+
+ expect(out).to include("Gem Current Latest Requested Groups")
+ expect(out).to include("myrack 0.9.1 1.0.0 = 0.9.1 default")
+ end
+ end
+
+ context "with --parseable" do
+ let(:flags) { "--parseable" }
+
+ it "prints a message when there are outdated gems" do
+ run_command
+
+ expect(out).to include("myrack (newest 1.0.0, installed 0.9.1, requested = 0.9.1)")
+ end
+ end
+
+ context "with --groups and --parseable" do
+ let(:flags) { "--groups --parseable" }
+
+ it "prints a simplified message when there are outdated gems" do
+ run_command
+
+ expect(out).to include("myrack (newest 1.0.0, installed 0.9.1, requested = 0.9.1)")
+ end
+ end
+ end
+
+ describe "printing the outdated warning" do
+ shared_examples_for "no warning" do
+ it "prints no warning" do
+ bundle "fail", env: { "BUNDLER_VERSION" => bundler_version }, raise_on_error: false
+ expect(stdboth).to eq("Could not find command \"fail\".")
+ end
+ end
+
+ let(:bundler_version) { "2.0" }
+ let(:latest_version) { nil }
+ before do
+ bundle_config_global "disable_version_check false"
+
+ pristine_system_gems "bundler-#{bundler_version}"
+ if latest_version
+ info_path = home(".bundle/cache/compact_index/rubygems.org.443.29b0360b937aa4d161703e6160654e47/info/bundler")
+ info_path.parent.mkpath
+ info_path.open("w") {|f| f.write "#{latest_version}\n" }
+ end
+ end
+
+ context "when there is no latest version" do
+ include_examples "no warning"
+ end
+
+ context "when the latest version is equal to the current version" do
+ let(:latest_version) { bundler_version }
+ include_examples "no warning"
+ end
+
+ context "when the latest version is less than the current version" do
+ let(:latest_version) { "0.9" }
+ include_examples "no warning"
+ end
+
+ context "when the latest version is greater than the current version" do
+ let(:latest_version) { "222.0" }
+ it "prints the version warning" do
+ bundle "fail", env: { "BUNDLER_VERSION" => bundler_version }, raise_on_error: false
+ expect(err).to start_with(<<-EOS.strip)
+The latest bundler is #{latest_version}, but you are currently running #{bundler_version}.
+To update to the most recent version, run `bundle update --bundler`
+ EOS
+ end
+
+ context "and disable_version_check is set" do
+ before { bundle "config set disable_version_check true", env: { "BUNDLER_VERSION" => bundler_version } }
+ include_examples "no warning"
+ end
+
+ context "running a parseable command" do
+ it "prints no warning" do
+ bundle "config set foo value", env: { "BUNDLER_VERSION" => bundler_version }
+ bundle "config get --parseable foo", env: { "BUNDLER_VERSION" => bundler_version }
+ expect(out).to eq "foo=value"
+ expect(err).to eq ""
+
+ bundle "platform --ruby", env: { "BUNDLER_VERSION" => bundler_version }, raise_on_error: false
+ expect(stdboth).to eq "Could not locate Gemfile"
+ end
+ end
+
+ context "and is a pre-release" do
+ let(:latest_version) { "222.0.0.pre.4" }
+ it "prints the version warning" do
+ bundle "fail", env: { "BUNDLER_VERSION" => bundler_version }, raise_on_error: false
+ expect(err).to start_with(<<-EOS.strip)
+The latest bundler is #{latest_version}, but you are currently running #{bundler_version}.
+To update to the most recent version, run `bundle update --bundler`
+ EOS
+ end
+ end
+ end
+ end
+end
+
+RSpec.describe "bundler executable" do
+ it "shows the bundler version just as the `bundle` executable does" do
+ bundler "--version"
+ expect(out).to eq(Bundler::VERSION.to_s)
+
+ bundle_config "simulate_version 5"
+ bundler "--version"
+ expect(out).to eq("#{Bundler::VERSION} (simulating Bundler 5)")
+ end
+
+ it "shows cli_help when bundler install and no Gemfile is found" do
+ bundler "install", raise_on_error: false
+ expect(err).to include("Could not locate Gemfile")
+
+ expect(out).to include("Bundler version #{Bundler::VERSION}").
+ and include("\n\nBundler commands:\n\n").
+ and include("\n\n Primary commands:\n").
+ and include("\n\n Utilities:\n").
+ and include("\n\nOptions:\n")
+ end
+end
diff --git a/spec/bundler/bundler/compact_index_client/parser_spec.rb b/spec/bundler/bundler/compact_index_client/parser_spec.rb
new file mode 100644
index 0000000000..6aa867f058
--- /dev/null
+++ b/spec/bundler/bundler/compact_index_client/parser_spec.rb
@@ -0,0 +1,249 @@
+# frozen_string_literal: true
+
+require "bundler/compact_index_client"
+require "bundler/compact_index_client/parser"
+
+TestCompactIndexClient = Struct.new(:names, :versions, :info_data) do
+ # Requiring the checksum to match the input data helps ensure
+ # that we are parsing the correct checksum from the versions file
+ def info(name, checksum)
+ info_data.dig(name, checksum)
+ end
+
+ def set_info_data(name, value)
+ info_data[name] = value
+ end
+end
+
+RSpec.describe Bundler::CompactIndexClient::Parser do
+ subject(:parser) { described_class.new(compact_index) }
+
+ let(:compact_index) { TestCompactIndexClient.new(names, versions, info_data) }
+ let(:names) { "a\nb\nc\n" }
+ let(:versions) { <<~VERSIONS.dup }
+ created_at: 2024-05-01T00:00:04Z
+ ---
+ a 1.0.0,1.0.1,1.1.0 aaa111
+ b 2.0.0,2.0.0-java bbb222
+ c 3.0.0,3.0.3,3.3.3 ccc333
+ c -3.0.3 ccc333yanked
+ VERSIONS
+ let(:info_data) do
+ {
+ "a" => { "aaa111" => a_info },
+ "b" => { "bbb222" => b_info },
+ "c" => { "ccc333yanked" => c_info },
+ }
+ end
+ let(:a_info) { <<~INFO.dup }
+ ---
+ 1.0.0 |checksum:aaa1,ruby:>= 3.0.0,rubygems:>= 3.2.3
+ 1.0.1 |checksum:aaa2,ruby:>= 3.0.0,rubygems:>= 3.2.3
+ 1.1.0 |checksum:aaa3,ruby:>= 3.0.0,rubygems:>= 3.2.3
+ INFO
+ let(:b_info) { <<~INFO }
+ 2.0.0 a:~> 1.0&<= 3.0|checksum:bbb1
+ 2.0.0-java a:~> 1.0&<= 3.0|checksum:bbb2
+ INFO
+ let(:c_info) { <<~INFO }
+ 3.0.0 a:= 1.0.0,b:~> 2.0|checksum:ccc1,ruby:>= 2.7.0,rubygems:>= 3.0.0
+ 3.3.3 a:>= 1.1.0,b:~> 2.0|checksum:ccc3,ruby:>= 3.0.0,rubygems:>= 3.2.3,created_at:2026-05-12T10:00:00Z
+ INFO
+
+ describe "#available?" do
+ it "returns true versions are available" do
+ expect(parser).to be_available
+ end
+
+ it "returns true when versions has only one gem" do
+ compact_index.versions = +"a 1.0.0 aaa1\n"
+ expect(parser).to be_available
+ end
+
+ it "returns true when versions has a gem and a header" do
+ compact_index.versions = +"---\na 1.0.0 aaa1\n"
+ expect(parser).to be_available
+ end
+
+ it "returns true when versions has a gem and a header with header data" do
+ compact_index.versions = +"created_at: 2024-05-01T00:00:04Z\n---\na 1.0.0 aaa1\n"
+ expect(parser).to be_available
+ end
+
+ it "returns false when versions has only the header" do
+ compact_index.versions = +"---\n"
+ expect(parser).not_to be_available
+ end
+
+ it "returns false when versions has only the header with header data" do
+ compact_index.versions = +"created_at: 2024-05-01T00:00:04Z\n---\n"
+ expect(parser).not_to be_available
+ end
+
+ it "returns false when versions index is not available" do
+ compact_index.versions = nil
+ expect(parser).not_to be_available
+ end
+
+ it "returns false when versions is empty" do
+ compact_index.versions = +""
+ expect(parser).not_to be_available
+ end
+ end
+
+ describe "#names" do
+ it "returns the names" do
+ expect(parser.names).to eq(%w[a b c])
+ end
+
+ it "returns an empty array when names is empty" do
+ compact_index.names = ""
+ expect(parser.names).to eq([])
+ end
+
+ it "returns an empty array when names is not readable" do
+ compact_index.names = nil
+ expect(parser.names).to eq([])
+ end
+ end
+
+ describe "#versions" do
+ it "returns the versions" do
+ expect(parser.versions).to eq(
+ "a" => [
+ ["a", "1.0.0"],
+ ["a", "1.0.1"],
+ ["a", "1.1.0"],
+ ],
+ "b" => [
+ ["b", "2.0.0"],
+ ["b", "2.0.0", "java"],
+ ],
+ "c" => [
+ ["c", "3.0.0"],
+ ["c", "3.3.3"],
+ ],
+ )
+ end
+
+ it "returns an empty hash when versions is empty" do
+ compact_index.versions = ""
+ expect(parser.versions).to eq({})
+ end
+
+ it "returns an empty hash when versions is not readable" do
+ compact_index.versions = nil
+ expect(parser.versions).to eq({})
+ end
+ end
+
+ describe "#info" do
+ let(:a_result) do
+ [
+ [
+ "a",
+ "1.0.0",
+ nil,
+ [],
+ [["checksum", ["aaa1"]], ["ruby", [">= 3.0.0"]], ["rubygems", [">= 3.2.3"]]],
+ ],
+ [
+ "a",
+ "1.0.1",
+ nil,
+ [],
+ [["checksum", ["aaa2"]], ["ruby", [">= 3.0.0"]], ["rubygems", [">= 3.2.3"]]],
+ ],
+ [
+ "a",
+ "1.1.0",
+ nil,
+ [],
+ [["checksum", ["aaa3"]], ["ruby", [">= 3.0.0"]], ["rubygems", [">= 3.2.3"]]],
+ ],
+ ]
+ end
+ let(:b_result) do
+ [
+ [
+ "b",
+ "2.0.0",
+ nil,
+ [["a", ["~> 1.0", "<= 3.0"]]],
+ [["checksum", ["bbb1"]]],
+ ],
+ [
+ "b",
+ "2.0.0",
+ "java",
+ [["a", ["~> 1.0", "<= 3.0"]]],
+ [["checksum", ["bbb2"]]],
+ ],
+ ]
+ end
+ let(:c_result) do
+ [
+ [
+ "c",
+ "3.0.0",
+ nil,
+ [["a", ["= 1.0.0"]], ["b", ["~> 2.0"]]],
+ [["checksum", ["ccc1"]], ["ruby", [">= 2.7.0"]], ["rubygems", [">= 3.0.0"]]],
+ ],
+ [
+ "c",
+ "3.3.3",
+ nil,
+ [["a", [">= 1.1.0"]], ["b", ["~> 2.0"]]],
+ [["checksum", ["ccc3"]], ["ruby", [">= 3.0.0"]], ["rubygems", [">= 3.2.3"]], ["created_at", ["2026-05-12T10:00:00Z"]]],
+ ],
+ ]
+ end
+
+ it "returns the info for example gem 'a' which has no deps" do
+ expect(parser.info("a")).to eq(a_result)
+ end
+
+ it "returns the info for example gem 'b' which has platform and compound deps" do
+ expect(parser.info("b")).to eq(b_result)
+ end
+
+ it "returns the info for example gem 'c' which has deps and yanked version (requires use of correct info checksum)" do
+ expect(parser.info("c")).to eq(c_result)
+ end
+
+ it "returns an empty array when the info is empty" do
+ compact_index.set_info_data("a", {})
+ expect(parser.info("a")).to eq([])
+ end
+
+ it "returns an empty array when the info is not readable" do
+ expect(parser.info("d")).to eq([])
+ end
+
+ it "handles empty lines in the versions file (Artifactory bug that they have yet to fix)" do
+ compact_index.versions = +<<~VERSIONS
+ created_at: 2024-05-01T00:00:04Z
+ ---
+ a 1.0.0,1.0.1,1.1.0 aaa111
+ b 2.0.0,2.0.0-java bbb222
+
+ c 3.0.0,3.0.3,3.3.3 ccc333
+ c -3.0.3 ccc333yanked
+ VERSIONS
+ expect(parser.info("a")).to eq(a_result)
+ end
+
+ it "handles lines without a checksum" do
+ compact_index.versions = <<~VERSIONS
+ created_at: 2024-05-01T00:00:04Z
+ ---
+ a 1.0.0,1.0.1,1.1.0 aaa111
+ b 2.0.0,2.0.0-java
+ c 3.0.0,3.0.3,3.3.3 ccc333
+ VERSIONS
+
+ expect(parser.info("a")).to eq(a_result)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/compact_index_client/updater_spec.rb b/spec/bundler/bundler/compact_index_client/updater_spec.rb
new file mode 100644
index 0000000000..fd63a652a4
--- /dev/null
+++ b/spec/bundler/bundler/compact_index_client/updater_spec.rb
@@ -0,0 +1,243 @@
+# frozen_string_literal: true
+
+require "bundler/vendored_net_http"
+require "bundler/compact_index_client"
+require "bundler/compact_index_client/updater"
+require "tmpdir"
+
+RSpec.describe Bundler::CompactIndexClient::Updater do
+ subject(:updater) { described_class.new(fetcher) }
+
+ let(:fetcher) { double(:fetcher) }
+ let(:local_path) { Pathname.new(Dir.mktmpdir("localpath")).join("versions") }
+ let(:etag_path) { Pathname.new(Dir.mktmpdir("localpath-etags")).join("versions.etag") }
+ let(:remote_path) { double(:remote_path) }
+
+ let(:full_body) { "abc123" }
+ let(:response) { double(:response, body: full_body, is_a?: false) }
+ let(:digest) { Digest::SHA256.base64digest(full_body) }
+
+ context "when the local path does not exist" do
+ before do
+ allow(response).to receive(:[]).with("Repr-Digest") { nil }
+ allow(response).to receive(:[]).with("Digest") { nil }
+ allow(response).to receive(:[]).with("ETag") { '"thisisanetag"' }
+ end
+
+ it "downloads the file without attempting append" do
+ expect(fetcher).to receive(:call).once.with(remote_path, {}) { response }
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq(full_body)
+ expect(etag_path.read).to eq("thisisanetag")
+ end
+
+ it "fails immediately on bad checksum" do
+ expect(fetcher).to receive(:call).once.with(remote_path, {}) { response }
+ allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:baddigest:" }
+
+ expect do
+ updater.update(remote_path, local_path, etag_path)
+ end.to raise_error(Bundler::CompactIndexClient::Updater::MismatchedChecksumError)
+ end
+ end
+
+ context "when the local path exists" do
+ let(:local_body) { "abc" }
+
+ before do
+ local_path.open("w") {|f| f.write(local_body) }
+ end
+
+ context "with an etag" do
+ before do
+ etag_path.open("w") {|f| f.write("LocalEtag") }
+ end
+
+ let(:headers) do
+ {
+ "If-None-Match" => '"LocalEtag"',
+ "Range" => "bytes=2-",
+ }
+ end
+
+ it "does nothing if etags match" do
+ expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response)
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { false }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified) { true }
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq("abc")
+ expect(etag_path.read).to eq("LocalEtag")
+ end
+
+ it "appends the file if etags do not match" do
+ expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response)
+ allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" }
+ allow(response).to receive(:[]).with("ETag") { '"NewEtag"' }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified) { false }
+ allow(response).to receive(:body) { "c123" }
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq(full_body)
+ expect(etag_path.read).to eq("NewEtag")
+ end
+
+ it "replaces the file if response ignores range" do
+ expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response)
+ allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" }
+ allow(response).to receive(:[]).with("ETag") { '"NewEtag"' }
+ allow(response).to receive(:body) { full_body }
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq(full_body)
+ expect(etag_path.read).to eq("NewEtag")
+ end
+
+ it "tries the request again if the partial response fails digest check" do
+ allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:baddigest:" }
+ allow(response).to receive(:body) { "the beginning of the file changed" }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true }
+ expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response)
+
+ full_response = double(:full_response, body: full_body, is_a?: false)
+ allow(full_response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" }
+ allow(full_response).to receive(:[]).with("ETag") { '"NewEtag"' }
+ expect(fetcher).to receive(:call).once.with(remote_path, { "If-None-Match" => '"LocalEtag"' }).and_return(full_response)
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq(full_body)
+ expect(etag_path.read).to eq("NewEtag")
+ end
+
+ it "tries the request again if the partial response is blank" do
+ allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:baddigest:" }
+ allow(response).to receive(:body) { "" }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true }
+ expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response)
+
+ full_response = double(:full_response, body: full_body, is_a?: false)
+ allow(full_response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" }
+ allow(full_response).to receive(:[]).with("ETag") { '"NewEtag"' }
+ expect(fetcher).to receive(:call).once.with(remote_path, { "If-None-Match" => '"LocalEtag"' }).and_return(full_response)
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq(full_body)
+ expect(etag_path.read).to eq("NewEtag")
+ end
+ end
+
+ context "without an etag file" do
+ let(:headers) do
+ { "Range" => "bytes=2-" }
+ end
+
+ it "appends the file" do
+ expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response)
+ allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" }
+ allow(response).to receive(:[]).with("ETag") { '"OpaqueEtag"' }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified) { false }
+ allow(response).to receive(:body) { "c123" }
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq(full_body)
+ expect(etag_path.read).to eq("OpaqueEtag")
+ end
+
+ it "replaces the file on full file response that ignores range request" do
+ expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response)
+ allow(response).to receive(:[]).with("Repr-Digest") { nil }
+ allow(response).to receive(:[]).with("Digest") { nil }
+ allow(response).to receive(:[]).with("ETag") { '"OpaqueEtag"' }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { false }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified) { false }
+ allow(response).to receive(:body) { full_body }
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq(full_body)
+ expect(etag_path.read).to eq("OpaqueEtag")
+ end
+
+ it "tries the request again if the partial response fails digest check" do
+ allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:baddigest:" }
+ allow(response).to receive(:body) { "the beginning of the file changed" }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true }
+ expect(fetcher).to receive(:call).once.with(remote_path, headers) do
+ # During the failed first request, we simulate another process writing the etag.
+ # This ensures the second request doesn't generate the md5 etag again but just uses whatever is written.
+ etag_path.open("w") {|f| f.write("LocalEtag") }
+ response
+ end
+
+ full_response = double(:full_response, body: full_body, is_a?: false)
+ allow(full_response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" }
+ allow(full_response).to receive(:[]).with("ETag") { '"NewEtag"' }
+ expect(fetcher).to receive(:call).once.with(remote_path, { "If-None-Match" => '"LocalEtag"' }).and_return(full_response)
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq(full_body)
+ expect(etag_path.read).to eq("NewEtag")
+ end
+ end
+ end
+
+ context "when the ETag header is missing" do
+ # Regression test for https://github.com/rubygems/bundler/issues/5463
+ let(:response) { double(:response, body: full_body) }
+
+ it "treats the response as an update" do
+ allow(response).to receive(:[]).with("Repr-Digest") { nil }
+ allow(response).to receive(:[]).with("Digest") { nil }
+ allow(response).to receive(:[]).with("ETag") { nil }
+ expect(fetcher).to receive(:call) { response }
+
+ updater.update(remote_path, local_path, etag_path)
+ end
+ end
+
+ context "when the download is corrupt" do
+ let(:response) { double(:response, body: "") }
+
+ it "raises HTTPError" do
+ expect(fetcher).to receive(:call).and_raise(Zlib::GzipFile::Error)
+
+ expect do
+ updater.update(remote_path, local_path, etag_path)
+ end.to raise_error(Bundler::HTTPError)
+ end
+ end
+
+ context "when receiving non UTF-8 data and default internal encoding set to ASCII" do
+ let(:response) { double(:response, body: "\x8B".b) }
+
+ it "works just fine" do
+ old_verbose = $VERBOSE
+ previous_internal_encoding = Encoding.default_internal
+
+ begin
+ $VERBOSE = false
+ Encoding.default_internal = "ASCII"
+ allow(response).to receive(:[]).with("Repr-Digest") { nil }
+ allow(response).to receive(:[]).with("Digest") { nil }
+ allow(response).to receive(:[]).with("ETag") { nil }
+ expect(fetcher).to receive(:call) { response }
+
+ updater.update(remote_path, local_path, etag_path)
+ ensure
+ Encoding.default_internal = previous_internal_encoding
+ $VERBOSE = old_verbose
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/current_ruby_spec.rb b/spec/bundler/bundler/current_ruby_spec.rb
new file mode 100644
index 0000000000..79eb802aa5
--- /dev/null
+++ b/spec/bundler/bundler/current_ruby_spec.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::CurrentRuby do
+ describe "PLATFORM_MAP" do
+ subject { described_class::PLATFORM_MAP }
+
+ # rubocop:disable Naming/VariableNumber
+ let(:platforms) do
+ { ruby: Gem::Platform::RUBY,
+ ruby_18: Gem::Platform::RUBY,
+ ruby_19: Gem::Platform::RUBY,
+ ruby_20: Gem::Platform::RUBY,
+ ruby_21: Gem::Platform::RUBY,
+ ruby_22: Gem::Platform::RUBY,
+ ruby_23: Gem::Platform::RUBY,
+ ruby_24: Gem::Platform::RUBY,
+ ruby_25: Gem::Platform::RUBY,
+ ruby_26: Gem::Platform::RUBY,
+ ruby_27: Gem::Platform::RUBY,
+ ruby_30: Gem::Platform::RUBY,
+ ruby_31: Gem::Platform::RUBY,
+ ruby_32: Gem::Platform::RUBY,
+ ruby_33: Gem::Platform::RUBY,
+ ruby_34: Gem::Platform::RUBY,
+ ruby_40: Gem::Platform::RUBY,
+ ruby_41: Gem::Platform::RUBY,
+ mri: Gem::Platform::RUBY,
+ mri_18: Gem::Platform::RUBY,
+ mri_19: Gem::Platform::RUBY,
+ mri_20: Gem::Platform::RUBY,
+ mri_21: Gem::Platform::RUBY,
+ mri_22: Gem::Platform::RUBY,
+ mri_23: Gem::Platform::RUBY,
+ mri_24: Gem::Platform::RUBY,
+ mri_25: Gem::Platform::RUBY,
+ mri_26: Gem::Platform::RUBY,
+ mri_27: Gem::Platform::RUBY,
+ mri_30: Gem::Platform::RUBY,
+ mri_31: Gem::Platform::RUBY,
+ mri_32: Gem::Platform::RUBY,
+ mri_33: Gem::Platform::RUBY,
+ mri_34: Gem::Platform::RUBY,
+ mri_40: Gem::Platform::RUBY,
+ mri_41: Gem::Platform::RUBY,
+ rbx: Gem::Platform::RUBY,
+ truffleruby: Gem::Platform::RUBY,
+ jruby: Gem::Platform::JAVA,
+ jruby_18: Gem::Platform::JAVA,
+ jruby_19: Gem::Platform::JAVA,
+ windows: Gem::Platform::WINDOWS,
+ windows_18: Gem::Platform::WINDOWS,
+ windows_19: Gem::Platform::WINDOWS,
+ windows_20: Gem::Platform::WINDOWS,
+ windows_21: Gem::Platform::WINDOWS,
+ windows_22: Gem::Platform::WINDOWS,
+ windows_23: Gem::Platform::WINDOWS,
+ windows_24: Gem::Platform::WINDOWS,
+ windows_25: Gem::Platform::WINDOWS,
+ windows_26: Gem::Platform::WINDOWS,
+ windows_27: Gem::Platform::WINDOWS,
+ windows_30: Gem::Platform::WINDOWS,
+ windows_31: Gem::Platform::WINDOWS,
+ windows_32: Gem::Platform::WINDOWS,
+ windows_33: Gem::Platform::WINDOWS,
+ windows_34: Gem::Platform::WINDOWS,
+ windows_40: Gem::Platform::WINDOWS,
+ windows_41: Gem::Platform::WINDOWS }
+ end
+
+ let(:deprecated) do
+ { mswin: Gem::Platform::MSWIN,
+ mswin_18: Gem::Platform::MSWIN,
+ mswin_19: Gem::Platform::MSWIN,
+ mswin_20: Gem::Platform::MSWIN,
+ mswin_21: Gem::Platform::MSWIN,
+ mswin_22: Gem::Platform::MSWIN,
+ mswin_23: Gem::Platform::MSWIN,
+ mswin_24: Gem::Platform::MSWIN,
+ mswin_25: Gem::Platform::MSWIN,
+ mswin_26: Gem::Platform::MSWIN,
+ mswin_27: Gem::Platform::MSWIN,
+ mswin_30: Gem::Platform::MSWIN,
+ mswin_31: Gem::Platform::MSWIN,
+ mswin_32: Gem::Platform::MSWIN,
+ mswin_33: Gem::Platform::MSWIN,
+ mswin_34: Gem::Platform::MSWIN,
+ mswin_40: Gem::Platform::MSWIN,
+ mswin_41: Gem::Platform::MSWIN,
+ mswin64: Gem::Platform::MSWIN64,
+ mswin64_19: Gem::Platform::MSWIN64,
+ mswin64_20: Gem::Platform::MSWIN64,
+ mswin64_21: Gem::Platform::MSWIN64,
+ mswin64_22: Gem::Platform::MSWIN64,
+ mswin64_23: Gem::Platform::MSWIN64,
+ mswin64_24: Gem::Platform::MSWIN64,
+ mswin64_25: Gem::Platform::MSWIN64,
+ mswin64_26: Gem::Platform::MSWIN64,
+ mswin64_27: Gem::Platform::MSWIN64,
+ mswin64_30: Gem::Platform::MSWIN64,
+ mswin64_31: Gem::Platform::MSWIN64,
+ mswin64_32: Gem::Platform::MSWIN64,
+ mswin64_33: Gem::Platform::MSWIN64,
+ mswin64_34: Gem::Platform::MSWIN64,
+ mswin64_40: Gem::Platform::MSWIN64,
+ mswin64_41: Gem::Platform::MSWIN64,
+ mingw: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_18: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_19: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_20: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_21: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_22: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_23: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_24: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_25: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_26: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_27: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_30: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_31: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_32: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_33: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_34: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_40: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_41: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_20: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_21: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_22: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_23: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_24: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_25: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_26: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_27: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_30: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_31: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_32: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_33: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_34: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_40: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_41: Gem::Platform::UNIVERSAL_MINGW }
+ end
+ # rubocop:enable Naming/VariableNumber
+
+ it "includes all platforms" do
+ expect(subject).to eq(platforms.merge(deprecated))
+ end
+ end
+
+ describe "Deprecated platform" do
+ it "outputs an error and aborts when calling maglev?" do
+ expect { Bundler.current_ruby.maglev? }.to raise_error(Bundler::RemovedError, /`CurrentRuby#maglev\?` was removed with no replacement./)
+ end
+
+ it "outputs an error and aborts when calling maglev_31?" do
+ expect { Bundler.current_ruby.maglev_31? }.to raise_error(Bundler::RemovedError, /`CurrentRuby#maglev_31\?` was removed with no replacement./)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/definition_spec.rb b/spec/bundler/bundler/definition_spec.rb
new file mode 100644
index 0000000000..8c4a5a0331
--- /dev/null
+++ b/spec/bundler/bundler/definition_spec.rb
@@ -0,0 +1,376 @@
+# frozen_string_literal: true
+
+require "bundler/definition"
+
+RSpec.describe Bundler::Definition do
+ describe "#overrides" do
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile) { bundled_app_gemfile }
+ end
+
+ subject { Bundler::Definition.new(bundled_app_lock, [], Bundler::SourceList.new, {}) }
+
+ it "defaults to an empty array" do
+ expect(subject.overrides).to eq([])
+ end
+
+ it "is writable" do
+ override = Bundler::Override.new("rails", :version, ">= 8.0")
+ subject.overrides = [override]
+ expect(subject.overrides).to eq([override])
+ end
+ end
+
+ describe "#lock" do
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile) { bundled_app_gemfile }
+ allow(Bundler).to receive(:ui) { double("UI", info: "", debug: "") }
+ end
+
+ subject { Bundler::Definition.new(bundled_app_lock, [], Bundler::SourceList.new, {}) }
+
+ context "when it's not possible to write to the file" do
+ it "raises an PermissionError with explanation" do
+ allow(File).to receive(:open).and_call_original
+ expect(File).to receive(:open).with(bundled_app_lock, "wb").
+ and_raise(Errno::EACCES.new(bundled_app_lock.to_s))
+ expect { subject.lock }.
+ to raise_error(Bundler::PermissionError, /Gemfile\.lock/)
+ end
+ end
+ context "when a temporary resource access issue occurs" do
+ it "raises a TemporaryResourceError with explanation" do
+ allow(File).to receive(:open).and_call_original
+ expect(File).to receive(:open).with(bundled_app_lock, "wb").
+ and_raise(Errno::EAGAIN)
+ expect { subject.lock }.
+ to raise_error(Bundler::TemporaryResourceError, /temporarily unavailable/)
+ end
+ end
+ context "when Bundler::Definition.no_lock is set to true" do
+ before { Bundler::Definition.no_lock = true }
+ after { Bundler::Definition.no_lock = false }
+
+ it "does not create a lockfile" do
+ subject.lock
+ expect(bundled_app_lock).not_to be_file
+ end
+ end
+ end
+
+ describe "detects changes" do
+ it "for a path gem with changes" do
+ build_lib "foo", "1.0", path: lib_path("foo")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo")}"
+ G
+
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", "1.0"
+ end
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo1, "myrack", "1.0.0"
+ end
+
+ bundle :install, env: { "DEBUG" => "1" }
+
+ expect(out).to match(/re-resolving dependencies/)
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (1.0)
+ myrack (= 1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "with an explicit update" do
+ build_repo4 do
+ build_gem("ffi", "1.9.23") {|s| s.platform = "java" }
+ build_gem("ffi", "1.9.23")
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "ffi"
+ G
+
+ bundle "lock --add-platform java"
+
+ bundle "update ffi", env: { "DEBUG" => "1" }
+
+ expect(out).to match(/because bundler is unlocking gems: \(ffi\)/)
+ end
+
+ it "for a path gem with deps and no changes" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", "1.0"
+ s.add_development_dependency "net-ssh", "1.0"
+ end
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo1, "myrack", "1.0.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo")}"
+ G
+
+ expected_lockfile = <<~G
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (1.0)
+ myrack (= 1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ expect(lockfile).to eq(expected_lockfile)
+
+ bundle :check, env: { "DEBUG" => "1" }
+
+ expect(out).to match(/using resolution from the lockfile/)
+ expect(lockfile).to eq(expected_lockfile)
+ end
+
+ it "for a locked gem for another platform" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "only_java", platform: :jruby
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo1, "only_java", "1.1", "java"
+ end
+
+ bundle "lock --add-platform java"
+ bundle :check, env: { "DEBUG" => "1" }
+
+ expect(out).to match(/using resolution from the lockfile/)
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ only_java (1.1-java)
+
+ PLATFORMS
+ #{lockfile_platforms("java")}
+
+ DEPENDENCIES
+ only_java
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "for a rubygems gem" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo1, "foo", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo"
+ G
+
+ bundle :check, env: { "DEBUG" => "1" }
+
+ expect(out).to match(/using resolution from the lockfile/)
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ foo (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+ end
+
+ describe "initialize" do
+ context "gem version promoter" do
+ context "eager unlock" do
+ let(:source_list) do
+ Bundler::SourceList.new.tap do |source_list|
+ source_list.add_global_rubygems_remote("https://gem.repo4")
+ end
+ end
+
+ before do
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'isolated_owner'
+
+ gem 'shared_owner_a'
+ gem 'shared_owner_b'
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ isolated_dep (2.0.1)
+ isolated_owner (1.0.1)
+ isolated_dep (~> 2.0)
+ shared_dep (5.0.1)
+ shared_owner_a (3.0.1)
+ shared_dep (~> 5.0)
+ shared_owner_b (4.0.1)
+ shared_dep (~> 5.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ shared_owner_a
+ shared_owner_b
+ isolated_owner
+
+ BUNDLED WITH
+ 1.13.0
+ L
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ it "should not eagerly unlock shared dependency with bundle install conservative updating behavior" do
+ updated_deps_in_gemfile = [Bundler::Dependency.new("isolated_owner", ">= 0"),
+ Bundler::Dependency.new("shared_owner_a", "3.0.2"),
+ Bundler::Dependency.new("shared_owner_b", ">= 0")]
+ unlock_hash_for_bundle_install = {}
+ definition = Bundler::Definition.new(
+ bundled_app_lock,
+ updated_deps_in_gemfile,
+ source_list,
+ unlock_hash_for_bundle_install
+ )
+ locked = definition.send(:converge_locked_specs).map(&:name)
+ expect(locked).to include "shared_dep"
+ end
+
+ it "should not eagerly unlock shared dependency with bundle update conservative updating behavior" do
+ updated_deps_in_gemfile = [Bundler::Dependency.new("isolated_owner", ">= 0"),
+ Bundler::Dependency.new("shared_owner_a", ">= 0"),
+ Bundler::Dependency.new("shared_owner_b", ">= 0")]
+ definition = Bundler::Definition.new(
+ bundled_app_lock,
+ updated_deps_in_gemfile,
+ source_list,
+ gems: ["shared_owner_a"], conservative: true
+ )
+ locked = definition.send(:converge_locked_specs).map(&:name)
+ expect(locked).to eq %w[isolated_dep isolated_owner shared_dep shared_owner_b]
+ expect(locked.include?("shared_dep")).to be_truthy
+ end
+ end
+ end
+ end
+
+ describe "#precompute_source_requirements_for_indirect_dependencies?" do
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile) { Pathname.new("Gemfile") }
+ end
+
+ let(:sources) { Bundler::SourceList.new }
+ subject { Bundler::Definition.new(nil, [], sources, []) }
+
+ before do
+ allow(sources).to receive(:non_global_rubygems_sources).and_return(non_global_rubygems_sources)
+ end
+
+ context "when all the scoped sources implement a dependency API" do
+ let(:non_global_rubygems_sources) do
+ [
+ double("non-global-source-0", "dependency_api_available?":true, to_s:"a"),
+ double("non-global-source-1", "dependency_api_available?":true, to_s:"b"),
+ ]
+ end
+
+ it "returns true without warning" do
+ expect(subject).not_to receive(:non_dependency_api_warning)
+
+ expect(subject.send(:precompute_source_requirements_for_indirect_dependencies?)).to be_truthy
+ end
+ end
+
+ context "when some scoped sources do not implement a dependency API" do
+ let(:non_global_rubygems_sources) do
+ [
+ double("non-global-source-0", "dependency_api_available?":true, to_s:"a"),
+ double("non-global-source-1", "dependency_api_available?":false, to_s:"b"),
+ double("non-global-source-2", "dependency_api_available?":false, to_s:"c"),
+ ]
+ end
+
+ it "returns false and warns about the non-API sources" do
+ expect(Bundler.ui).to receive(:warn).with(<<-W.strip)
+Your Gemfile contains scoped sources that don't implement a dependency API, namely:
+
+ * b
+ * c
+
+Using the above gem servers may result in installing unexpected gems. To resolve this warning, make sure you use gem servers that implement dependency APIs, such as gemstash or geminabox gem servers.
+ W
+
+ expect(subject.send(:precompute_source_requirements_for_indirect_dependencies?)).to be_falsy
+ end
+ end
+ end
+
+ def mock_source_list
+ Class.new do
+ def all_sources
+ []
+ end
+
+ def path_sources
+ []
+ end
+
+ def replace_sources!(arg)
+ nil
+ end
+ end.new
+ end
+end
diff --git a/spec/bundler/bundler/dependency_spec.rb b/spec/bundler/bundler/dependency_spec.rb
new file mode 100644
index 0000000000..f930459571
--- /dev/null
+++ b/spec/bundler/bundler/dependency_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Dependency do
+ let(:options) do
+ {}
+ end
+ let(:dependency) do
+ described_class.new(
+ "test_gem",
+ "1.0.0",
+ options
+ )
+ end
+
+ describe "to_lock" do
+ it "returns formatted string" do
+ expect(dependency.to_lock).to eq(" test_gem (= 1.0.0)")
+ end
+
+ it "matches format of Gem::Dependency#to_lock" do
+ gem_dependency = Gem::Dependency.new("test_gem", "1.0.0")
+ expect(dependency.to_lock).to eq(gem_dependency.to_lock)
+ end
+
+ context "when source is passed" do
+ let(:options) do
+ {
+ "source" => Bundler::Source::Git.new({}),
+ }
+ end
+
+ it "returns formatted string with exclamation mark" do
+ expect(dependency.to_lock).to eq(" test_gem (= 1.0.0)!")
+ end
+ end
+ end
+
+ it "is on the current platform" do
+ engine = Gem.win_platform? ? "windows" : RUBY_ENGINE
+
+ dep = described_class.new(
+ "test_gem",
+ "1.0.0",
+ { "platforms" => "#{engine}_#{RbConfig::CONFIG["MAJOR"]}#{RbConfig::CONFIG["MINOR"]}" },
+ )
+
+ expect(dep.current_platform?).to be_truthy
+ end
+end
diff --git a/spec/bundler/bundler/digest_spec.rb b/spec/bundler/bundler/digest_spec.rb
new file mode 100644
index 0000000000..f876827964
--- /dev/null
+++ b/spec/bundler/bundler/digest_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "openssl"
+require "bundler/digest"
+
+RSpec.describe Bundler::Digest do
+ context "SHA1" do
+ subject { Bundler::Digest }
+ let(:stdlib) { OpenSSL::Digest::SHA1 }
+
+ it "is compatible with stdlib" do
+ random_strings = ["foo", "skfjsdlkfjsdf", "3924m", "ldskfj"]
+
+ # https://www.rfc-editor.org/rfc/rfc3174#section-7.3
+ rfc3174_test_cases = ["abc", "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", "a", "01234567" * 8]
+
+ (random_strings + rfc3174_test_cases).each do |payload|
+ sha1 = subject.sha1(payload)
+ sha1_stdlib = stdlib.hexdigest(payload)
+ expect(sha1).to be == sha1_stdlib, "#{payload}'s sha1 digest (#{sha1}) did not match stlib's result (#{sha1_stdlib})"
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/dsl_spec.rb b/spec/bundler/bundler/dsl_spec.rb
new file mode 100644
index 0000000000..b6e67a312c
--- /dev/null
+++ b/spec/bundler/bundler/dsl_spec.rb
@@ -0,0 +1,558 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Dsl do
+ before do
+ @rubygems = double("rubygems")
+ allow(Bundler::Source::Rubygems).to receive(:new) { @rubygems }
+ end
+
+ describe "#git_source" do
+ it "registers custom hosts" do
+ subject.git_source(:example) {|repo_name| "git@git.example.com:#{repo_name}.git" }
+ subject.git_source(:foobar) {|repo_name| "git@foobar.com:#{repo_name}.git" }
+ subject.gem("dobry-pies", example: "strzalek/dobry-pies")
+ example_uri = "git@git.example.com:strzalek/dobry-pies.git"
+ expect(subject.dependencies.first.source.uri).to eq(example_uri)
+ end
+
+ it "raises exception on invalid hostname" do
+ expect do
+ subject.git_source(:group) {|repo_name| "git@git.example.com:#{repo_name}.git" }
+ end.to raise_error(Bundler::InvalidOption)
+ end
+
+ it "expects block passed" do
+ expect { subject.git_source(:example) }.to raise_error(Bundler::InvalidOption)
+ end
+
+ it "converts :github PR to URI using https" do
+ subject.gem("sparks", github: "https://github.com/indirect/sparks/pull/5")
+ github_uri = "https://github.com/indirect/sparks.git"
+ expect(subject.dependencies.first.source.uri).to eq(github_uri)
+ expect(subject.dependencies.first.source.ref).to eq("refs/pull/5/head")
+ end
+
+ it "converts :gitlab PR to URI using https" do
+ subject.gem("sparks", gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5")
+ gitlab_uri = "https://gitlab.com/indirect/sparks.git"
+ expect(subject.dependencies.first.source.uri).to eq(gitlab_uri)
+ expect(subject.dependencies.first.source.ref).to eq("refs/merge-requests/5/head")
+ end
+
+ it "rejects :github PR URI with a branch, ref or tag" do
+ expect do
+ subject.gem("sparks", github: "https://github.com/indirect/sparks/pull/5", branch: "foo")
+ end.to raise_error(
+ Bundler::GemfileError,
+ %(The :branch option can't be used with `github: "https://github.com/indirect/sparks/pull/5"`),
+ )
+
+ expect do
+ subject.gem("sparks", github: "https://github.com/indirect/sparks/pull/5", ref: "foo")
+ end.to raise_error(
+ Bundler::GemfileError,
+ %(The :ref option can't be used with `github: "https://github.com/indirect/sparks/pull/5"`),
+ )
+
+ expect do
+ subject.gem("sparks", github: "https://github.com/indirect/sparks/pull/5", tag: "foo")
+ end.to raise_error(
+ Bundler::GemfileError,
+ %(The :tag option can't be used with `github: "https://github.com/indirect/sparks/pull/5"`),
+ )
+ end
+
+ it "rejects :gitlab PR URI with a branch, ref or tag" do
+ expect do
+ subject.gem("sparks", gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5", branch: "foo")
+ end.to raise_error(
+ Bundler::GemfileError,
+ %(The :branch option can't be used with `gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5"`),
+ )
+
+ expect do
+ subject.gem("sparks", gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5", ref: "foo")
+ end.to raise_error(
+ Bundler::GemfileError,
+ %(The :ref option can't be used with `gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5"`),
+ )
+
+ expect do
+ subject.gem("sparks", gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5", tag: "foo")
+ end.to raise_error(
+ Bundler::GemfileError,
+ %(The :tag option can't be used with `gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5"`),
+ )
+ end
+
+ it "rejects :github with :git" do
+ expect do
+ subject.gem("sparks", github: "indirect/sparks", git: "https://github.com/indirect/sparks.git")
+ end.to raise_error(
+ Bundler::GemfileError,
+ %(The :git option can't be used with `github: "indirect/sparks"`),
+ )
+ end
+
+ it "rejects :gitlab with :git" do
+ expect do
+ subject.gem("sparks", gitlab: "indirect/sparks", git: "https://gitlab.com/indirect/sparks.git")
+ end.to raise_error(
+ Bundler::GemfileError,
+ %(The :git option can't be used with `gitlab: "indirect/sparks"`),
+ )
+ end
+
+ context "default hosts" do
+ it "converts :github to URI using https" do
+ subject.gem("sparks", github: "indirect/sparks")
+ github_uri = "https://github.com/indirect/sparks.git"
+ expect(subject.dependencies.first.source.uri).to eq(github_uri)
+ end
+
+ it "converts :github shortcut to URI using https" do
+ subject.gem("sparks", github: "rails")
+ github_uri = "https://github.com/rails/rails.git"
+ expect(subject.dependencies.first.source.uri).to eq(github_uri)
+ end
+
+ it "converts :gitlab to URI using https" do
+ subject.gem("sparks", gitlab: "indirect/sparks")
+ gitlab_uri = "https://gitlab.com/indirect/sparks.git"
+ expect(subject.dependencies.first.source.uri).to eq(gitlab_uri)
+ end
+
+ it "converts :gitlab shortcut to URI using https" do
+ subject.gem("sparks", gitlab: "rails")
+ gitlab_uri = "https://gitlab.com/rails/rails.git"
+ expect(subject.dependencies.first.source.uri).to eq(gitlab_uri)
+ end
+
+ it "converts numeric :gist to :git" do
+ subject.gem("not-really-a-gem", gist: 2_859_988)
+ github_uri = "https://gist.github.com/2859988.git"
+ expect(subject.dependencies.first.source.uri).to eq(github_uri)
+ end
+
+ it "converts :gist to :git" do
+ subject.gem("not-really-a-gem", gist: "2859988")
+ github_uri = "https://gist.github.com/2859988.git"
+ expect(subject.dependencies.first.source.uri).to eq(github_uri)
+ end
+
+ it "converts :bitbucket to :git" do
+ subject.gem("not-really-a-gem", bitbucket: "mcorp/flatlab-rails")
+ bitbucket_uri = "https://mcorp@bitbucket.org/mcorp/flatlab-rails.git"
+ expect(subject.dependencies.first.source.uri).to eq(bitbucket_uri)
+ end
+
+ it "converts 'mcorp' to 'mcorp/mcorp'" do
+ subject.gem("not-really-a-gem", bitbucket: "mcorp")
+ bitbucket_uri = "https://mcorp@bitbucket.org/mcorp/mcorp.git"
+ expect(subject.dependencies.first.source.uri).to eq(bitbucket_uri)
+ end
+ end
+
+ context "default git sources" do
+ it "has bitbucket, gist, github, and gitlab" do
+ expect(subject.instance_variable_get(:@git_sources).keys.sort).to eq(%w[bitbucket gist github gitlab])
+ end
+ end
+ end
+
+ describe "#method_missing" do
+ it "raises an error for unknown DSL methods" do
+ expect(Bundler).to receive(:read_file).with(git_root.join("Gemfile").to_s).
+ and_return("unknown")
+
+ error_msg = "There was an error parsing `Gemfile`: Undefined local variable or method `unknown' for Gemfile. Bundler cannot continue."
+ expect { subject.eval_gemfile("Gemfile") }.
+ to raise_error(Bundler::GemfileError, Regexp.new(error_msg))
+ end
+ end
+
+ describe "#eval_gemfile" do
+ it "handles syntax errors with a useful message" do
+ expect(Bundler).to receive(:read_file).with(git_root.join("Gemfile").to_s).and_return("}")
+ expect { subject.eval_gemfile("Gemfile") }.
+ to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`: (syntax error, unexpected tSTRING_DEND|(compile error - )?syntax error, unexpected '\}'|.+?unexpected '}', ignoring it\n). Bundler cannot continue./m)
+ end
+
+ it "distinguishes syntax errors from evaluation errors" do
+ expect(Bundler).to receive(:read_file).with(git_root.join("Gemfile").to_s).and_return(
+ "ruby '2.1.5', :engine => 'ruby', :engine_version => '1.2.4'"
+ )
+ expect { subject.eval_gemfile("Gemfile") }.
+ to raise_error(Bundler::GemfileError, /There was an error evaluating `Gemfile`: ruby_version must match the :engine_version for MRI/)
+ end
+
+ it "populates __dir__ and __FILE__ correctly" do
+ abs_path = git_root.join("../fragment.rb").to_s
+ expect(Bundler).to receive(:read_file).with(abs_path).and_return(<<~RUBY)
+ @fragment_dir = __dir__
+ @fragment_file = __FILE__
+ RUBY
+ subject.eval_gemfile("../fragment.rb")
+ expect(subject.instance_variable_get(:@fragment_dir)).to eq(git_root.dirname.to_s)
+ expect(subject.instance_variable_get(:@fragment_file)).to eq(abs_path)
+ end
+ end
+
+ describe "#gem" do
+ # rubocop:disable Naming/VariableNumber
+ [:ruby, :ruby_18, :ruby_19, :ruby_20, :ruby_21, :ruby_22, :ruby_23, :ruby_24, :ruby_25, :ruby_26, :ruby_27,
+ :ruby_30, :ruby_31, :ruby_32, :ruby_33, :ruby_34, :ruby_40, :mri, :mri_18, :mri_19, :mri_20, :mri_21, :mri_22, :mri_23, :mri_24,
+ :mri_25, :mri_26, :mri_27, :mri_30, :mri_31, :mri_32, :mri_33, :mri_34, :mri_40, :jruby, :rbx, :truffleruby].each do |platform|
+ it "allows #{platform} as a valid platform" do
+ subject.gem("foo", platform: platform)
+ end
+ end
+ # rubocop:enable Naming/VariableNumber
+
+ it "allows platforms matching the running Ruby version" do
+ platform = "ruby_#{RbConfig::CONFIG["MAJOR"]}#{RbConfig::CONFIG["MINOR"]}"
+
+ expect { subject.gem("foo", platform: platform) }.not_to raise_error
+ expect(Bundler.current_ruby.respond_to?("#{platform}?")).to be_truthy
+ end
+
+ it "rejects invalid platforms" do
+ expect { subject.gem("foo", platform: :bogus) }.
+ to raise_error(Bundler::GemfileError, /is not a valid platform/)
+ end
+
+ it "warn for legacy windows platforms" do
+ expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with(/\APlatform :mswin, :x64_mingw will be removed in the future./)
+ subject.gem("foo", platforms: [:mswin, :jruby, :x64_mingw])
+ end
+
+ it "rejects empty gem name" do
+ expect { subject.gem("") }.
+ to raise_error(Bundler::GemfileError, /an empty gem name is not valid/)
+ end
+
+ it "rejects with a leading space in the name" do
+ expect { subject.gem(" foo") }.
+ to raise_error(Bundler::GemfileError, /' foo' is not a valid gem name because it contains whitespace/)
+ end
+
+ it "rejects with a trailing space in the name" do
+ expect { subject.gem("foo ") }.
+ to raise_error(Bundler::GemfileError, /'foo ' is not a valid gem name because it contains whitespace/)
+ end
+
+ it "rejects with a space in the gem name" do
+ expect { subject.gem("fo o") }.
+ to raise_error(Bundler::GemfileError, /'fo o' is not a valid gem name because it contains whitespace/)
+ end
+
+ it "rejects with a tab in the gem name" do
+ expect { subject.gem("fo\to") }.
+ to raise_error(Bundler::GemfileError, /'fo\to' is not a valid gem name because it contains whitespace/)
+ end
+
+ it "rejects with a newline in the gem name" do
+ expect { subject.gem("fo\no") }.
+ to raise_error(Bundler::GemfileError, /'fo\no' is not a valid gem name because it contains whitespace/)
+ end
+
+ it "rejects with a carriage return in the gem name" do
+ expect { subject.gem("fo\ro") }.
+ to raise_error(Bundler::GemfileError, /'fo\ro' is not a valid gem name because it contains whitespace/)
+ end
+
+ it "rejects with a form feed in the gem name" do
+ expect { subject.gem("fo\fo") }.
+ to raise_error(Bundler::GemfileError, /'fo\fo' is not a valid gem name because it contains whitespace/)
+ end
+
+ it "rejects symbols as gem name" do
+ expect { subject.gem(:foo) }.
+ to raise_error(Bundler::GemfileError, /You need to specify gem names as Strings. Use 'gem "foo"' instead/)
+ end
+
+ it "rejects branch option on non-git gems" do
+ expect { subject.gem("foo", branch: "test") }.
+ to raise_error(Bundler::GemfileError, /The `branch` option for `gem 'foo'` is not allowed. Only gems with a git source can specify a branch/)
+ end
+
+ it "allows specifying a branch on git gems" do
+ subject.gem("foo", branch: "test", git: "http://mytestrepo")
+ dep = subject.dependencies.last
+ expect(dep.name).to eq "foo"
+ end
+
+ it "allows specifying a branch on git gems with a git_source" do
+ subject.git_source(:test_source) {|n| "https://github.com/#{n}" }
+ subject.gem("foo", branch: "test", test_source: "bundler/bundler")
+ dep = subject.dependencies.last
+ expect(dep.name).to eq "foo"
+ end
+ end
+
+ describe "#platforms" do
+ it "warn for legacy windows platforms" do
+ expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with(/\APlatform :mswin64, :mingw will be removed in the future./)
+ subject.platforms(:mswin64, :jruby, :mingw) do
+ subject.gem("foo")
+ end
+ end
+ end
+
+ context "can bundle groups of gems with" do
+ # git "https://github.com/rails/rails.git" do
+ # gem "railties"
+ # gem "action_pack"
+ # gem "active_model"
+ # end
+ describe "#git" do
+ it "from a single repo" do
+ rails_gems = %w[railties action_pack active_model]
+ subject.git "https://github.com/rails/rails.git" do
+ rails_gems.each {|rails_gem| subject.send :gem, rails_gem }
+ end
+ expect(subject.dependencies.map(&:name)).to match_array rails_gems
+ end
+ end
+
+ # github 'spree' do
+ # gem 'spree_core'
+ # gem 'spree_api'
+ # gem 'spree_backend'
+ # end
+ describe "#github" do
+ it "from github" do
+ spree_gems = %w[spree_core spree_api spree_backend]
+ subject.github "spree" do
+ spree_gems.each {|spree_gem| subject.send :gem, spree_gem }
+ end
+
+ subject.dependencies.each do |d|
+ expect(d.source.uri).to eq("https://github.com/spree/spree.git")
+ end
+ end
+ end
+ end
+
+ describe "syntax errors" do
+ it "will raise a Bundler::GemfileError" do
+ gemfile "gem 'foo', :path => /unquoted/string/syntax/error"
+ expect { Bundler::Dsl.evaluate(bundled_app_gemfile, nil, true) }.
+ to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`:( compile error -)?.+?unknown regexp options - trg.+ Bundler cannot continue./m)
+ end
+ end
+
+ describe "Runtime errors" do
+ it "will raise a Bundler::GemfileError" do
+ gemfile "raise RuntimeError, 'foo'"
+ expect { Bundler::Dsl.evaluate(bundled_app_gemfile, nil, true) }.
+ to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`: foo. Bundler cannot continue./i)
+ end
+ end
+
+ describe "#with_source" do
+ context "if there was a rubygem source already defined" do
+ it "restores it after it's done" do
+ other_source = double("other-source")
+ allow(Bundler::Source::Rubygems).to receive(:new).and_return(other_source)
+ allow(Bundler).to receive(:default_gemfile).and_return(Pathname.new("./Gemfile"))
+
+ subject.source("https://other-source.org") do
+ subject.gem("dobry-pies", path: "foo")
+ subject.gem("foo")
+ end
+
+ expect(subject.dependencies.last.source).to eq(other_source)
+ end
+ end
+ end
+
+ describe "#source with cooldown" do
+ before do
+ allow(@rubygems).to receive(:add_remote)
+ end
+
+ it "accepts a non-negative integer" do
+ expect do
+ subject.source("https://rubygems.org", cooldown: 7)
+ end.not_to raise_error
+ end
+
+ it "accepts 0 as an explicit disable" do
+ expect do
+ subject.source("https://rubygems.org", cooldown: 0)
+ end.not_to raise_error
+ end
+
+ it "rejects a string" do
+ expect do
+ subject.source("https://rubygems.org", cooldown: "7")
+ end.to raise_error(Bundler::InvalidOption, /non-negative integer/)
+ end
+
+ it "rejects a float" do
+ expect do
+ subject.source("https://rubygems.org", cooldown: 7.5)
+ end.to raise_error(Bundler::InvalidOption, /non-negative integer/)
+ end
+
+ it "rejects a negative integer" do
+ expect do
+ subject.source("https://rubygems.org", cooldown: -7)
+ end.to raise_error(Bundler::InvalidOption, /non-negative integer/)
+ end
+
+ it "rejects an array" do
+ expect do
+ subject.source("https://rubygems.org", cooldown: [7])
+ end.to raise_error(Bundler::InvalidOption, /non-negative integer/)
+ end
+ end
+
+ describe "#override" do
+ it "stores an Override for a gem with a version: operation" do
+ subject.override("rails", version: ">= 8.0")
+
+ expect(subject.overrides.size).to eq(1)
+ override = subject.overrides.first
+ expect(override.target).to eq("rails")
+ expect(override.field).to eq(:version)
+ expect(override.operation).to eq(">= 8.0")
+ end
+
+ it "accepts :ignore_upper as the operation" do
+ subject.override("nokogiri", version: :ignore_upper)
+ expect(subject.overrides.first.operation).to eq(:ignore_upper)
+ end
+
+ it "accepts nil as the operation" do
+ subject.override("legacy", version: nil)
+ expect(subject.overrides.first.operation).to be_nil
+ end
+
+ it "appends to overrides across multiple statements" do
+ subject.override("rails", version: ">= 8.0")
+ subject.override("nokogiri", version: :ignore_upper)
+ expect(subject.overrides.map(&:target)).to eq(["rails", "nokogiri"])
+ end
+
+ it "is empty by default" do
+ expect(subject.overrides).to eq([])
+ end
+
+ it "raises ArgumentError when target is :all and version: is given" do
+ expect do
+ subject.override(:all, version: ">= 8.0")
+ end.to raise_error(ArgumentError, /`override :all, version:` is not allowed/)
+ end
+
+ it "rejects :all + version: even when other fields are also given" do
+ expect do
+ subject.override(:all, required_ruby_version: :ignore_upper, version: ">= 8.0")
+ end.to raise_error(ArgumentError, /`override :all, version:` is not allowed/)
+ end
+
+ it "does not record any override when :all + version: is rejected" do
+ expect do
+ subject.override(:all, version: ">= 8.0")
+ end.to raise_error(ArgumentError)
+ expect(subject.overrides).to eq([])
+ end
+
+ it "raises ArgumentError when target is neither :all nor a string" do
+ expect do
+ subject.override(:rails, version: ">= 8.0")
+ end.to raise_error(ArgumentError, /target must be :all or a gem name string/)
+ end
+
+ it "raises ArgumentError for an unsupported field" do
+ expect do
+ subject.override("rails", as: "y")
+ end.to raise_error(ArgumentError, /unsupported override field `as:`/)
+ end
+
+ it "stores an Override for a gem with a required_ruby_version: operation" do
+ subject.override("rails", required_ruby_version: :ignore_upper)
+ override = subject.overrides.first
+ expect(override.target).to eq("rails")
+ expect(override.field).to eq(:required_ruby_version)
+ expect(override.operation).to eq(:ignore_upper)
+ end
+
+ it "stores an Override for a gem with a required_rubygems_version: operation" do
+ subject.override("rails", required_rubygems_version: nil)
+ override = subject.overrides.first
+ expect(override.field).to eq(:required_rubygems_version)
+ expect(override.operation).to be_nil
+ end
+
+ it "stores an Override targeting :all with a metadata field" do
+ subject.override(:all, required_ruby_version: :ignore_upper)
+ override = subject.overrides.first
+ expect(override.target).to eq(:all)
+ expect(override.field).to eq(:required_ruby_version)
+ expect(override.operation).to eq(:ignore_upper)
+ end
+
+ it "stores an Override targeting :all with required_rubygems_version" do
+ subject.override(:all, required_rubygems_version: nil)
+ override = subject.overrides.first
+ expect(override.target).to eq(:all)
+ expect(override.field).to eq(:required_rubygems_version)
+ end
+
+ it "raises ArgumentError for a non-string, non-symbol, non-nil operation" do
+ expect do
+ subject.override("rails", version: 42)
+ end.to raise_error(ArgumentError, /override operation must be a String, Symbol, or nil/)
+ end
+
+ it "raises ArgumentError for an unsupported symbol operation" do
+ expect do
+ subject.override("rails", version: :explode)
+ end.to raise_error(ArgumentError, /unsupported override operation/)
+ end
+
+ it "raises ArgumentError for an unparsable version string" do
+ expect do
+ subject.override("rails", version: "not a version")
+ end.to raise_error(ArgumentError, /invalid override version requirement/)
+ end
+
+ it "does not record an override when the version string is invalid" do
+ expect do
+ subject.override("rails", version: "not a version")
+ end.to raise_error(ArgumentError)
+ expect(subject.overrides).to eq([])
+ end
+
+ it "rejects atomically when one field in a multi-field call is invalid" do
+ expect do
+ subject.override("rails", version: ">= 8.0", as: "y")
+ end.to raise_error(ArgumentError, /unsupported override field/)
+ expect(subject.overrides).to eq([])
+ end
+
+ it "raises ArgumentError when the same target and field are overridden twice" do
+ subject.override("rails", version: ">= 8.0")
+ expect do
+ subject.override("rails", version: :ignore_upper)
+ end.to raise_error(ArgumentError, /duplicate override for "rails" `version:`/)
+ end
+
+ it "keeps the original override when a duplicate is rejected" do
+ subject.override("rails", version: ">= 8.0")
+ expect do
+ subject.override("rails", version: :ignore_upper)
+ end.to raise_error(ArgumentError)
+ expect(subject.overrides.size).to eq(1)
+ expect(subject.overrides.first.operation).to eq(">= 8.0")
+ end
+
+ it "allows different targets with the same field" do
+ subject.override("rails", version: ">= 8.0")
+ subject.override("nokogiri", version: :ignore_upper)
+ expect(subject.overrides.size).to eq(2)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/endpoint_specification_spec.rb b/spec/bundler/bundler/endpoint_specification_spec.rb
new file mode 100644
index 0000000000..4fbd59d48f
--- /dev/null
+++ b/spec/bundler/bundler/endpoint_specification_spec.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::EndpointSpecification do
+ let(:name) { "foo" }
+ let(:version) { "1.0.0" }
+ let(:platform) { Gem::Platform::RUBY }
+ let(:dependencies) { [] }
+ let(:spec_fetcher) { double(:spec_fetcher) }
+ let(:metadata) { nil }
+
+ subject(:spec) { described_class.new(name, version, platform, spec_fetcher, dependencies, metadata) }
+
+ describe "#build_dependency" do
+ let(:name) { "foo" }
+ let(:requirement1) { "~> 1.1" }
+ let(:requirement2) { ">= 1.1.7" }
+
+ it "should return a Gem::Dependency" do
+ expect(subject.send(:build_dependency, name, [requirement1, requirement2])).
+ to eq(Gem::Dependency.new(name, requirement1, requirement2))
+ end
+
+ context "when an ArgumentError occurs" do
+ before do
+ allow(Gem::Dependency).to receive(:new).with(name, [requirement1, requirement2]) {
+ raise ArgumentError.new("Some error occurred")
+ }
+ end
+
+ it "should raise the original error" do
+ expect { subject.send(:build_dependency, name, [requirement1, requirement2]) }.to raise_error(
+ ArgumentError, "Some error occurred"
+ )
+ end
+ end
+ end
+
+ describe "#parse_metadata" do
+ context "when the metadata has malformed requirements" do
+ let(:metadata) { { "rubygems" => ">\n" } }
+ it "raises a helpful error message" do
+ expect { subject }.to raise_error(
+ Bundler::GemspecError,
+ a_string_including("There was an error parsing the metadata for the gem foo (1.0.0)").
+ and(a_string_including("The metadata was #{{ "rubygems" => ">\n" }.inspect}"))
+ )
+ end
+ end
+
+ context "when the metadata has created_at" do
+ let(:metadata) { { "created_at" => ["2026-05-12T10:00:00Z"] } }
+
+ it "parses created_at as a Time" do
+ expect(subject.created_at).to eq(Time.utc(2026, 5, 12, 10, 0, 0))
+ end
+ end
+
+ context "when the metadata has a string created_at (older rubygems shape)" do
+ let(:metadata) { { "created_at" => "2026-05-12T10:00:00Z" } }
+
+ it "still parses created_at" do
+ expect(subject.created_at).to eq(Time.utc(2026, 5, 12, 10, 0, 0))
+ end
+ end
+
+ context "when created_at is truncated (older rubygems splits on colons)" do
+ let(:metadata) { { "created_at" => "2026-05-12T10" } }
+
+ it "leaves created_at as nil instead of raising" do
+ expect(subject.created_at).to be_nil
+ end
+ end
+
+ context "when the metadata has no created_at" do
+ let(:metadata) { { "checksum" => ["abc"] } }
+ let(:spec_fetcher) { double(:spec_fetcher, uri: "https://rubygems.org") }
+
+ it "leaves created_at as nil" do
+ allow(Bundler::Checksum).to receive(:from_api).and_return(nil)
+ expect(subject.created_at).to be_nil
+ end
+ end
+
+ context "when the metadata is nil" do
+ it "leaves created_at as nil" do
+ expect(subject.created_at).to be_nil
+ end
+ end
+ end
+
+ describe "#required_ruby_version" do
+ context "required_ruby_version is already set on endpoint specification" do
+ existing_value = "already set value"
+ let(:required_ruby_version) { existing_value }
+
+ it "should return the current value when already set on endpoint specification" do
+ expect(spec.required_ruby_version). eql?(existing_value)
+ end
+ end
+
+ it "should return the remote spec value when not set on endpoint specification and remote spec has one" do
+ remote_value = "remote_value"
+ remote_spec = double(:remote_spec, required_ruby_version: remote_value, required_rubygems_version: nil)
+ allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec)
+
+ expect(spec.required_ruby_version). eql?(remote_value)
+ end
+
+ it "should use the default Gem Requirement value when not set on endpoint specification and not set on remote spec" do
+ remote_spec = double(:remote_spec, required_ruby_version: nil, required_rubygems_version: nil)
+ allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec)
+ expect(spec.required_ruby_version). eql?(Gem::Requirement.default)
+ end
+ end
+
+ it "supports equality comparison" do
+ remote_spec = double(:remote_spec, required_ruby_version: nil, required_rubygems_version: nil)
+ allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec)
+ other_spec = described_class.new("bar", version, platform, spec_fetcher, dependencies, metadata)
+ expect(spec).to eql(spec)
+ expect(spec).to_not eql(other_spec)
+ end
+end
diff --git a/spec/bundler/bundler/env_spec.rb b/spec/bundler/bundler/env_spec.rb
new file mode 100644
index 0000000000..2b7dbde217
--- /dev/null
+++ b/spec/bundler/bundler/env_spec.rb
@@ -0,0 +1,237 @@
+# frozen_string_literal: true
+
+require "bundler/settings"
+require "openssl"
+
+RSpec.describe Bundler::Env do
+ let(:git_proxy_stub) { Bundler::Source::Git::GitProxy.new(nil, nil) }
+
+ describe "#report" do
+ it "prints the environment" do
+ out = described_class.report
+
+ expect(out).to include("Environment")
+ expect(out).to include(Bundler::VERSION)
+ expect(out).to include(Gem::VERSION)
+ expect(out).to include(described_class.send(:ruby_version))
+ expect(out).to include(described_class.send(:git_version))
+ expect(out).to include(OpenSSL::OPENSSL_VERSION)
+ end
+
+ describe "rubygems paths" do
+ it "prints gem home" do
+ with_clear_paths("GEM_HOME", "/a/b/c") do
+ out = described_class.report
+ expect(out).to include("Gem Home /a/b/c")
+ end
+ end
+
+ it "prints gem path" do
+ with_clear_paths("GEM_PATH", "/a/b/c#{File::PATH_SEPARATOR}d/e/f") do
+ out = described_class.report
+ expect(out).to include("Gem Path /a/b/c#{File::PATH_SEPARATOR}d/e/f")
+ end
+ end
+
+ it "prints user home" do
+ with_clear_paths("HOME", "/a/b/c") do
+ out = described_class.report
+ expect(out).to include("User Home /a/b/c")
+ end
+ end
+
+ it "prints user path" do
+ with_clear_paths("HOME", "/a/b/c") do
+ allow(File).to receive(:exist?)
+ allow(File).to receive(:exist?).with("/a/b/c/.gem").and_return(true)
+ out = described_class.report
+ expect(out).to include("User Path /a/b/c/.gem")
+ end
+ end
+
+ it "prints bin dir" do
+ with_clear_paths("GEM_HOME", "/a/b/c") do
+ out = described_class.report
+ expect(out).to include("Bin Dir /a/b/c/bin")
+ end
+ end
+
+ private
+
+ def with_clear_paths(env_var, env_value)
+ old_env_var = ENV[env_var]
+ ENV[env_var] = env_value
+ Gem.clear_paths
+ yield
+ ensure
+ ENV[env_var] = old_env_var
+ end
+ end
+
+ context "when there is a Gemfile and a lockfile and print_gemfile is true" do
+ before do
+ gemfile "source 'https://gem.repo1'; gem 'myrack', '1.0.0'"
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ 1.10.0
+ L
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ let(:output) { described_class.report(print_gemfile: true) }
+
+ it "prints the Gemfile" do
+ expect(output).to include("Gemfile")
+ expect(output).to include("'myrack', '1.0.0'")
+ end
+
+ it "prints the lockfile" do
+ expect(output).to include("Gemfile.lock")
+ expect(output).to include("myrack (1.0.0)")
+ end
+ end
+
+ context "when there no Gemfile and print_gemfile is true" do
+ let(:output) { described_class.report(print_gemfile: true) }
+
+ it "prints the environment" do
+ expect(output).to start_with("## Environment")
+ end
+ end
+
+ context "when there's bundler config with credentials" do
+ before do
+ bundle "config set https://localgemserver.test/ user:pass"
+ end
+
+ let(:output) { described_class.report(print_gemfile: true) }
+
+ it "prints the config with redacted values" do
+ expect(output).to include("https://localgemserver.test")
+ expect(output).to include("user:[REDACTED]")
+ expect(output).to_not include("user:pass")
+ end
+ end
+
+ context "when there's bundler config with OAuth token credentials" do
+ before do
+ bundle "config set https://localgemserver.test/ api_token:x-oauth-basic"
+ end
+
+ let(:output) { described_class.report(print_gemfile: true) }
+
+ it "prints the config with redacted values" do
+ expect(output).to include("https://localgemserver.test")
+ expect(output).to include("[REDACTED]:x-oauth-basic")
+ expect(output).to_not include("api_token:x-oauth-basic")
+ end
+ end
+
+ context "when Gemfile contains a gemspec and print_gemspecs is true" do
+ let(:gemspec) do
+ <<~GEMSPEC
+ Gem::Specification.new do |gem|
+ gem.name = "foo"
+ gem.author = "Fumofu"
+ end
+ GEMSPEC
+ end
+
+ before do
+ gemfile("source 'https://gem.repo1'; gemspec")
+
+ File.open(bundled_app("foo.gemspec"), "wb") do |f|
+ f.write(gemspec)
+ end
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ it "prints the gemspec" do
+ output = described_class.report(print_gemspecs: true)
+
+ expect(output).to include("foo.gemspec")
+ expect(output).to include(gemspec)
+ end
+ end
+
+ context "when eval_gemfile is used" do
+ it "prints all gemfiles" do
+ gemfile bundled_app("other/Gemfile-other"), "gem 'myrack'"
+ gemfile bundled_app("other/Gemfile"), "eval_gemfile 'Gemfile-other'"
+ gemfile bundled_app("Gemfile-alt"), <<-G
+ source "https://gem.repo1"
+ eval_gemfile "other/Gemfile"
+ G
+ gemfile "eval_gemfile #{bundled_app("Gemfile-alt").to_s.dump}"
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ allow(Bundler::SharedHelpers).to receive(:pwd).and_return(bundled_app)
+
+ output = described_class.report(print_gemspecs: true)
+ expect(output).to include(<<~ENV)
+ ## Gemfile
+
+ ### Gemfile
+
+ ```ruby
+ eval_gemfile #{bundled_app("Gemfile-alt").to_s.dump}
+ ```
+
+ ### Gemfile-alt
+
+ ```ruby
+ source "https://gem.repo1"
+ eval_gemfile "other/Gemfile"
+ ```
+
+ ### other/Gemfile
+
+ ```ruby
+ eval_gemfile 'Gemfile-other'
+ ```
+
+ ### other/Gemfile-other
+
+ ```ruby
+ gem 'myrack'
+ ```
+
+ ### Gemfile.lock
+
+ ```
+ <No #{bundled_app_lock} found>
+ ```
+ ENV
+ end
+ end
+
+ context "when the git version is OS specific" do
+ it "includes OS specific information with the version number" do
+ status = double("success?" => true)
+ expect(Open3).to receive(:capture3).with("git", "--version").
+ and_return(["git version 1.2.3 (Apple Git-BS)", "", status])
+ expect(Bundler::Source::Git::GitProxy).to receive(:new).and_return(git_proxy_stub)
+
+ expect(described_class.report).to include("Git 1.2.3 (Apple Git-BS)")
+ end
+ end
+
+ it "no longer reports the Tools section or external tool versions" do
+ report = described_class.report
+ expect(report).not_to include("Tools")
+ ["rbenv", "RVM", "chruby"].each do |tool|
+ expect(report).not_to include(tool)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/environment_preserver_spec.rb b/spec/bundler/bundler/environment_preserver_spec.rb
new file mode 100644
index 0000000000..6c7066d0c6
--- /dev/null
+++ b/spec/bundler/bundler/environment_preserver_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::EnvironmentPreserver do
+ let(:preserver) { described_class.new(env, ["foo"]) }
+
+ describe "#backup" do
+ let(:env) { { "foo" => "my-foo", "bar" => "my-bar" } }
+ subject { preserver.backup }
+
+ it "should create backup entries" do
+ expect(subject["BUNDLER_ORIG_foo"]).to eq("my-foo")
+ end
+
+ it "should keep the original entry" do
+ expect(subject["foo"]).to eq("my-foo")
+ end
+
+ it "should not create backup entries for unspecified keys" do
+ expect(subject.key?("BUNDLER_ORIG_bar")).to eq(false)
+ end
+
+ it "should not affect the original env" do
+ subject
+ expect(env.keys.sort).to eq(%w[bar foo])
+ end
+
+ context "when a key is empty" do
+ let(:env) { { "foo" => "" } }
+
+ it "should keep the original entry" do
+ expect(subject["foo"]).to be_empty
+ end
+
+ it "should still create backup entries" do
+ expect(subject).to have_key "BUNDLER_ORIG_foo"
+ end
+ end
+
+ context "when an original key is set" do
+ let(:env) { { "foo" => "my-foo", "BUNDLER_ORIG_foo" => "orig-foo" } }
+
+ it "should keep the original value in the BUNDLER_ORIG_ variable" do
+ expect(subject["BUNDLER_ORIG_foo"]).to eq("orig-foo")
+ end
+
+ it "should keep the variable" do
+ expect(subject["foo"]).to eq("my-foo")
+ end
+ end
+ end
+
+ describe "#restore" do
+ subject { preserver.restore }
+
+ context "when an original key is set" do
+ let(:env) { { "foo" => "my-foo", "BUNDLER_ORIG_foo" => "orig-foo" } }
+
+ it "should restore the original value" do
+ expect(subject["foo"]).to eq("orig-foo")
+ end
+
+ it "should delete the backup value" do
+ expect(subject.key?("BUNDLER_ORIG_foo")).to eq(false)
+ end
+ end
+
+ context "when no original key is set" do
+ let(:env) { { "foo" => "my-foo" } }
+
+ it "should keep the current value" do
+ expect(subject["foo"]).to eq("my-foo")
+ end
+ end
+
+ context "when the original key is empty" do
+ let(:env) { { "foo" => "my-foo", "BUNDLER_ORIG_foo" => "" } }
+
+ it "should restore the original value" do
+ expect(subject["foo"]).to be_empty
+ end
+
+ it "should delete the backup value" do
+ expect(subject.key?("BUNDLER_ORIG_foo")).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/errors_spec.rb b/spec/bundler/bundler/errors_spec.rb
new file mode 100644
index 0000000000..b62d85d32b
--- /dev/null
+++ b/spec/bundler/bundler/errors_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::IncorrectLockfileDependencies do
+ describe "#message" do
+ let(:spec) do
+ double("LazySpecification", full_name: "rubocop-1.82.0")
+ end
+
+ context "without dependency details" do
+ subject { described_class.new(spec) }
+
+ it "provides a basic error message" do
+ expect(subject.message).to include("Bundler found incorrect dependencies in the lockfile for rubocop-1.82.0")
+ expect(subject.message).to include("Please run `bundle install` to regenerate the lockfile.")
+ end
+ end
+
+ context "with dependency details" do
+ let(:actual_dependencies) do
+ [
+ Gem::Dependency.new("json", [">= 2.3", "< 4.0"]),
+ Gem::Dependency.new("parallel", ["~> 1.10"]),
+ Gem::Dependency.new("parser", [">= 3.3.0.2"]),
+ ]
+ end
+
+ let(:lockfile_dependencies) do
+ [
+ Gem::Dependency.new("json", [">= 2.3", "< 3.0"]),
+ Gem::Dependency.new("parallel", ["~> 1.10"]),
+ Gem::Dependency.new("parser", [">= 3.2.0.0"]),
+ ]
+ end
+
+ subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) }
+
+ it "shows only mismatched dependencies" do
+ message = subject.message
+
+ expect(message).to include("json: gemspec specifies")
+ expect(message).to include("parser: gemspec specifies")
+ expect(message).not_to include("parallel")
+ end
+ end
+
+ context "when gemspec has dependencies but lockfile has none" do
+ let(:actual_dependencies) do
+ [
+ Gem::Dependency.new("myrack-test", ["~> 1.0"]),
+ ]
+ end
+
+ let(:lockfile_dependencies) { [] }
+
+ subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) }
+
+ it "shows the dependency as not in lockfile" do
+ message = subject.message
+
+ expect(message).to include("myrack-test: gemspec specifies ~> 1.0, not in lockfile")
+ end
+ end
+
+ context "when gemspec has no dependencies but lockfile has some" do
+ let(:actual_dependencies) { [] }
+
+ let(:lockfile_dependencies) do
+ [
+ Gem::Dependency.new("unexpected", ["~> 1.0"]),
+ ]
+ end
+
+ subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) }
+
+ it "shows the dependency as not in gemspec" do
+ message = subject.message
+
+ expect(message).to include("unexpected: not in gemspec, lockfile has ~> 1.0")
+ end
+ end
+ end
+
+ describe "#status_code" do
+ let(:spec) { double("LazySpecification", full_name: "test-1.0.0") }
+ subject { described_class.new(spec) }
+
+ it "returns 41" do
+ expect(subject.status_code).to eq(41)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/fetcher/base_spec.rb b/spec/bundler/bundler/fetcher/base_spec.rb
new file mode 100644
index 0000000000..b8c6b57b10
--- /dev/null
+++ b/spec/bundler/bundler/fetcher/base_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Fetcher::Base do
+ let(:downloader) { double(:downloader) }
+ let(:remote) { double(:remote) }
+ let(:display_uri) { "http://sample_uri.com" }
+ let(:gem_remote_fetcher) { nil }
+
+ class TestClass < described_class; end
+
+ subject { TestClass.new(downloader, remote, display_uri, gem_remote_fetcher) }
+
+ describe "#initialize" do
+ context "with the abstract Base class" do
+ it "should raise an error" do
+ expect { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) }.to raise_error(RuntimeError, "Abstract class")
+ end
+ end
+
+ context "with a class that inherits the Base class" do
+ it "should set the passed attributes" do
+ expect(subject.downloader).to eq(downloader)
+ expect(subject.remote).to eq(remote)
+ expect(subject.display_uri).to eq("http://sample_uri.com")
+ end
+ end
+ end
+
+ describe "#remote_uri" do
+ let(:remote_uri_obj) { double(:remote_uri_obj) }
+
+ before { allow(remote).to receive(:uri).and_return(remote_uri_obj) }
+
+ it "should return the remote's uri" do
+ expect(subject.remote_uri).to eq(remote_uri_obj)
+ end
+ end
+
+ describe "#fetch_uri" do
+ let(:remote_uri_obj) { Gem::URI("http://rubygems.org") }
+
+ before { allow(subject).to receive(:remote_uri).and_return(remote_uri_obj) }
+
+ context "when the remote uri's host is rubygems.org" do
+ it "should create a copy of the remote uri with index.rubygems.org as the host" do
+ fetched_uri = subject.fetch_uri
+ expect(fetched_uri.host).to eq("index.rubygems.org")
+ expect(fetched_uri).to_not be(remote_uri_obj)
+ end
+ end
+
+ context "when the remote uri's host is not rubygems.org" do
+ let(:remote_uri_obj) { Gem::URI("http://otherhost.org") }
+
+ it "should return the remote uri" do
+ expect(subject.fetch_uri).to eq(Gem::URI("http://otherhost.org"))
+ end
+ end
+
+ it "memoizes the fetched uri" do
+ expect(remote_uri_obj).to receive(:host).once
+ 2.times { subject.fetch_uri }
+ end
+ end
+
+ describe "#available?" do
+ it "should return whether the api is available" do
+ expect(subject.available?).to be_truthy
+ end
+ end
+
+ describe "#api_fetcher?" do
+ it "should return false" do
+ expect(subject.api_fetcher?).to be_falsey
+ end
+ end
+end
diff --git a/spec/bundler/bundler/fetcher/compact_index_spec.rb b/spec/bundler/bundler/fetcher/compact_index_spec.rb
new file mode 100644
index 0000000000..aa536673d9
--- /dev/null
+++ b/spec/bundler/bundler/fetcher/compact_index_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+# load CompactIndexClient upfront to prevent thread safety issues during parallel specs
+require "bundler/compact_index_client"
+
+RSpec.describe Bundler::Fetcher::CompactIndex do
+ let(:response) { double(:response) }
+ let(:downloader) { double(:downloader, fetch: response) }
+ let(:display_uri) { Gem::URI("http://sampleuri.com") }
+ let(:remote) { double(:remote, cache_slug: "lsjdf", uri: display_uri) }
+ let(:gem_remote_fetcher) { nil }
+ let(:compact_index) { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) }
+ let(:compact_index_client) { double(:compact_index_client, available?: true, info: [["lskdjf", "1", nil, [], []]]) }
+
+ before do
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified).and_return(true)
+ allow(compact_index).to receive(:log_specs) {}
+ allow(compact_index).to receive(:compact_index_client).and_return(compact_index_client)
+ end
+
+ describe "#specs_for_names" do
+ let(:thread_list) { Thread.list.select {|thread| thread.status == "run" } }
+ let(:thread_inspection) { thread_list.map {|th| " * #{th}:\n #{th.backtrace_locations.join("\n ")}" }.join("\n") }
+
+ it "has only one thread open at the end of the run" do
+ compact_index.specs_for_names(["lskdjf"])
+
+ thread_count = thread_list.count
+ expect(thread_count).to eq(1), "Expected 1 active thread after `#specs_for_names`, but found #{thread_count}. In particular, found:\n#{thread_inspection}"
+ end
+
+ it "calls worker#stop during the run" do
+ expect_any_instance_of(Bundler::Worker).to receive(:stop).at_least(:once).and_call_original
+
+ compact_index.specs_for_names(["lskdjf"])
+ end
+
+ describe "#available?" do
+ it "returns true" do
+ expect(compact_index).to be_available
+ end
+
+ context "when OpenSSL is not available" do
+ before do
+ allow(compact_index).to receive(:require).with("openssl").and_raise(LoadError)
+ end
+
+ it "returns true" do
+ expect(compact_index).to be_available
+ end
+ end
+
+ context "when OpenSSL is FIPS-enabled" do
+ def remove_cached_md5_availability
+ return unless Bundler::SharedHelpers.instance_variable_defined?(:@md5_available)
+ Bundler::SharedHelpers.remove_instance_variable(:@md5_available)
+ end
+
+ before do
+ remove_cached_md5_availability
+ stub_const("OpenSSL::OPENSSL_FIPS", true)
+ end
+
+ after { remove_cached_md5_availability }
+
+ context "when FIPS-mode is active" do
+ before do
+ allow(OpenSSL::Digest).to receive(:digest).with("MD5", "").
+ and_raise(OpenSSL::Digest::DigestError)
+ end
+
+ it "returns false" do
+ expect(compact_index).to_not be_available
+ end
+ end
+
+ it "returns true" do
+ expect(compact_index).to be_available
+ end
+ end
+ end
+
+ context "logging" do
+ before { allow(compact_index).to receive(:log_specs).and_call_original }
+
+ context "with debug on" do
+ before do
+ allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(true)
+ end
+
+ it "should log at info level" do
+ expect(Bundler).to receive_message_chain(:ui, :debug).with('Looking up gems ["lskdjf"]')
+ compact_index.specs_for_names(["lskdjf"])
+ end
+ end
+
+ context "with debug off" do
+ before do
+ allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(false)
+ end
+
+ it "should log at info level" do
+ expect(Bundler).to receive_message_chain(:ui, :info).with(".", false)
+ compact_index.specs_for_names(["lskdjf"])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/fetcher/dependency_spec.rb b/spec/bundler/bundler/fetcher/dependency_spec.rb
new file mode 100644
index 0000000000..501bc269a5
--- /dev/null
+++ b/spec/bundler/bundler/fetcher/dependency_spec.rb
@@ -0,0 +1,296 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Fetcher::Dependency do
+ let(:downloader) { double(:downloader) }
+ let(:remote) { double(:remote, uri: Gem::URI("http://localhost:5000")) }
+ let(:display_uri) { "http://sample_uri.com" }
+ let(:gem_remote_fetcher) { nil }
+
+ subject { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) }
+
+ describe "#available?" do
+ let(:dependency_api_uri) { double(:dependency_api_uri) }
+ let(:fetched_spec) { double(:fetched_spec) }
+
+ before do
+ allow(subject).to receive(:dependency_api_uri).and_return(dependency_api_uri)
+ allow(downloader).to receive(:fetch).with(dependency_api_uri).and_return(fetched_spec)
+ end
+
+ it "should be truthy" do
+ expect(subject.available?).to be_truthy
+ end
+
+ context "when there is no network access" do
+ before do
+ allow(downloader).to receive(:fetch).with(dependency_api_uri) {
+ raise Bundler::Fetcher::NetworkDownError.new("Network Down Message")
+ }
+ end
+
+ it "should raise an HTTPError with the original message" do
+ expect { subject.available? }.to raise_error(Bundler::HTTPError, "Network Down Message")
+ end
+ end
+
+ context "when authentication is required" do
+ let(:remote_uri) { "http://remote_uri.org" }
+
+ before do
+ allow(downloader).to receive(:fetch).with(dependency_api_uri) {
+ raise Bundler::Fetcher::AuthenticationRequiredError.new(remote_uri)
+ }
+ end
+
+ it "should raise the original error" do
+ expect { subject.available? }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError,
+ %r{Authentication is required for http://remote_uri.org})
+ end
+ end
+
+ context "when there is an http error" do
+ before { allow(downloader).to receive(:fetch).with(dependency_api_uri) { raise Bundler::HTTPError.new } }
+
+ it "should be falsey" do
+ expect(subject.available?).to be_falsey
+ end
+ end
+ end
+
+ describe "#api_fetcher?" do
+ it "should return true" do
+ expect(subject.api_fetcher?).to be_truthy
+ end
+ end
+
+ describe "#specs" do
+ let(:gem_names) { %w[foo bar] }
+ let(:full_dependency_list) { ["bar"] }
+ let(:last_spec_list) { [["boulder", gem_version1, "ruby", resque]] }
+ let(:fail_errors) { double(:fail_errors) }
+ let(:bundler_retry) { double(:bundler_retry) }
+ let(:gem_version1) { double(:gem_version1) }
+ let(:resque) { double(:resque) }
+ let(:remote_uri) { "http://remote-uri.org" }
+
+ before do
+ stub_const("Bundler::Fetcher::FAIL_ERRORS", fail_errors)
+ allow(Bundler::Retry).to receive(:new).with("dependency api", fail_errors).and_return(bundler_retry)
+ allow(bundler_retry).to receive(:attempts) {|&block| block.call }
+ allow(subject).to receive(:log_specs) {}
+ allow(subject).to receive(:remote_uri).and_return(remote_uri)
+ allow(Bundler).to receive_message_chain(:ui, :debug?)
+ allow(Bundler).to receive_message_chain(:ui, :info)
+ allow(Bundler).to receive_message_chain(:ui, :debug)
+ end
+
+ context "when there are given gem names that are not in the full dependency list" do
+ let(:spec_list) { [["top", gem_version2, "ruby", faraday]] }
+ let(:deps_list) { [] }
+ let(:dependency_specs) { [spec_list, deps_list] }
+ let(:gem_version2) { double(:gem_version2) }
+ let(:faraday) { double(:faraday) }
+
+ before { allow(subject).to receive(:dependency_specs).with(["foo"]).and_return(dependency_specs) }
+
+ it "should return a hash with the remote_uri and the list of specs" do
+ expect(subject.specs(gem_names, full_dependency_list, last_spec_list)).to eq([
+ ["top", gem_version2, "ruby", faraday],
+ ["boulder", gem_version1, "ruby", resque],
+ ])
+ end
+ end
+
+ context "when all given gem names are in the full dependency list" do
+ let(:gem_names) { ["foo"] }
+ let(:full_dependency_list) { %w[foo bar] }
+ let(:last_spec_list) { ["boulder"] }
+
+ it "should return a hash with the remote_uri and the last spec list" do
+ expect(subject.specs(gem_names, full_dependency_list, last_spec_list)).to eq(["boulder"])
+ end
+ end
+
+ context "logging" do
+ before { allow(subject).to receive(:log_specs).and_call_original }
+
+ context "with debug on" do
+ before do
+ allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(true)
+ allow(subject).to receive(:dependency_specs).with(["foo"]).and_return([[], []])
+ end
+
+ it "should log the query list at debug level" do
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("Query List: [\"foo\"]")
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("Query List: []")
+ subject.specs(gem_names, full_dependency_list, last_spec_list)
+ end
+ end
+
+ context "with debug off" do
+ before do
+ allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(false)
+ allow(subject).to receive(:dependency_specs).with(["foo"]).and_return([[], []])
+ end
+
+ it "should log at info level" do
+ expect(Bundler).to receive_message_chain(:ui, :info).with(".", false)
+ expect(Bundler).to receive_message_chain(:ui, :info).with(".", false)
+ subject.specs(gem_names, full_dependency_list, last_spec_list)
+ end
+ end
+ end
+
+ shared_examples_for "the error is properly handled" do
+ it "should return nil" do
+ expect(subject.specs(gem_names, full_dependency_list, last_spec_list)).to be_nil
+ end
+
+ context "debug logging is not on" do
+ before { allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(false) }
+
+ it "should log a new line to info" do
+ expect(Bundler).to receive_message_chain(:ui, :info).with("")
+ subject.specs(gem_names, full_dependency_list, last_spec_list)
+ end
+ end
+ end
+
+ shared_examples_for "the error is logged" do
+ it "should log the inability to fetch from API at debug level, and mention retrying" do
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("could not fetch from the dependency API, trying the full index")
+ subject.specs(gem_names, full_dependency_list, last_spec_list)
+ end
+ end
+
+ context "when an HTTPError occurs" do
+ before { allow(subject).to receive(:dependency_specs) { raise Bundler::HTTPError.new } }
+
+ it_behaves_like "the error is properly handled"
+ it_behaves_like "the error is logged"
+ end
+
+ context "when a GemspecError occurs" do
+ before { allow(subject).to receive(:dependency_specs) { raise Bundler::GemspecError.new } }
+
+ it_behaves_like "the error is properly handled"
+ it_behaves_like "the error is logged"
+ end
+
+ context "when a MarshalError occurs" do
+ before { allow(subject).to receive(:dependency_specs) { raise Bundler::MarshalError.new } }
+
+ it_behaves_like "the error is properly handled"
+ it_behaves_like "the error is logged"
+ end
+ end
+
+ describe "#dependency_specs" do
+ let(:gem_names) { [%w[foo bar], %w[bundler rubocop]] }
+ let(:gem_list) { double(:gem_list) }
+ let(:formatted_specs_and_deps) { double(:formatted_specs_and_deps) }
+
+ before do
+ allow(subject).to receive(:unmarshalled_dep_gems).with(gem_names).and_return(gem_list)
+ allow(subject).to receive(:get_formatted_specs_and_deps).with(gem_list).and_return(formatted_specs_and_deps)
+ end
+
+ it "should log the query list at debug level" do
+ expect(Bundler).to receive_message_chain(:ui, :debug).with(
+ "Query Gemcutter Dependency Endpoint API: foo,bar,bundler,rubocop"
+ )
+ subject.dependency_specs(gem_names)
+ end
+
+ it "should return formatted specs and a unique list of dependencies" do
+ expect(subject.dependency_specs(gem_names)).to eq(formatted_specs_and_deps)
+ end
+ end
+
+ describe "#unmarshalled_dep_gems" do
+ let(:gem_names) { [%w[foo bar], %w[bundler rubocop]] }
+ let(:dep_api_uri) { double(:dep_api_uri) }
+ let(:unmarshalled_gems) { double(:unmarshalled_gems) }
+ let(:fetch_response) { double(:fetch_response, body: double(:body)) }
+ let(:rubygems_limit) { 100 }
+
+ before { allow(subject).to receive(:dependency_api_uri).with(gem_names).and_return(dep_api_uri) }
+
+ it "should fetch dependencies from RubyGems and unmarshal them" do
+ expect(gem_names).to receive(:each_slice).with(rubygems_limit).and_call_original
+ expect(downloader).to receive(:fetch).with(dep_api_uri).and_return(fetch_response)
+ expect(Bundler).to receive(:safe_load_marshal).with(fetch_response.body).and_return([unmarshalled_gems])
+ expect(subject.unmarshalled_dep_gems(gem_names)).to eq([unmarshalled_gems])
+ end
+
+ it "should fetch as many dependencies as specified" do
+ allow(subject).to receive(:dependency_api_uri).with([%w[foo bar]]).and_return(dep_api_uri)
+ allow(subject).to receive(:dependency_api_uri).with([%w[bundler rubocop]]).and_return(dep_api_uri)
+
+ expect(downloader).to receive(:fetch).twice.with(dep_api_uri).and_return(fetch_response)
+ expect(Bundler).to receive(:safe_load_marshal).twice.with(fetch_response.body).and_return([unmarshalled_gems])
+
+ Bundler.settings.temporary(api_request_size: 1) do
+ expect(subject.unmarshalled_dep_gems(gem_names)).to eq([unmarshalled_gems, unmarshalled_gems])
+ end
+ end
+ end
+
+ describe "#get_formatted_specs_and_deps" do
+ let(:gem_list) do
+ [
+ {
+ dependencies: {
+ "resque" => "req3,req4",
+ },
+ name: "typhoeus",
+ number: "1.0.1",
+ platform: "ruby",
+ },
+ {
+ dependencies: {
+ "faraday" => "req1,req2",
+ },
+ name: "grape",
+ number: "2.0.2",
+ platform: "jruby",
+ },
+ ]
+ end
+
+ it "should return formatted specs and a unique list of dependencies" do
+ spec_list, deps_list = subject.get_formatted_specs_and_deps(gem_list)
+ expect(spec_list).to eq([["typhoeus", "1.0.1", "ruby", [["resque", ["req3,req4"]]]],
+ ["grape", "2.0.2", "jruby", [["faraday", ["req1,req2"]]]]])
+ expect(deps_list).to eq(%w[resque faraday])
+ end
+ end
+
+ describe "#dependency_api_uri" do
+ let(:uri) { Gem::URI("http://gem-api.com") }
+
+ context "with gem names" do
+ let(:gem_names) { %w[foo bar bundler rubocop] }
+
+ before { allow(subject).to receive(:fetch_uri).and_return(uri) }
+
+ it "should return an api calling uri with the gems in the query" do
+ expect(subject.dependency_api_uri(gem_names).to_s).to eq(
+ "http://gem-api.com/api/v1/dependencies?gems=bar%2Cbundler%2Cfoo%2Crubocop"
+ )
+ end
+ end
+
+ context "with no gem names" do
+ let(:gem_names) { [] }
+
+ before { allow(subject).to receive(:fetch_uri).and_return(uri) }
+
+ it "should return an api calling uri with no query" do
+ expect(subject.dependency_api_uri(gem_names).to_s).to eq(
+ "http://gem-api.com/api/v1/dependencies"
+ )
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/fetcher/downloader_spec.rb b/spec/bundler/bundler/fetcher/downloader_spec.rb
new file mode 100644
index 0000000000..edf426328a
--- /dev/null
+++ b/spec/bundler/bundler/fetcher/downloader_spec.rb
@@ -0,0 +1,302 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Fetcher::Downloader do
+ let(:connection) { double(:connection) }
+ let(:redirect_limit) { 5 }
+ let(:uri) { Gem::URI("http://www.uri-to-fetch.com/api/v2/endpoint") }
+ let(:options) { double(:options) }
+
+ subject { described_class.new(connection, redirect_limit) }
+
+ describe "fetch" do
+ let(:counter) { 0 }
+ let(:httpv) { "1.1" }
+ let(:http_response) { double(:response) }
+
+ before do
+ allow(subject).to receive(:request).with(uri, options).and_return(http_response)
+ allow(http_response).to receive(:body).and_return("Body with info")
+ end
+
+ context "when the # requests counter is greater than the redirect limit" do
+ let(:counter) { redirect_limit + 1 }
+
+ it "should raise a Bundler::HTTPError specifying too many redirects" do
+ expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::HTTPError, "Too many redirects")
+ end
+ end
+
+ context "logging" do
+ let(:http_response) { Gem::Net::HTTPSuccess.new("1.1", 200, "Success") }
+
+ it "should log the HTTP response code and message to debug" do
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP 200 Success #{uri}")
+ subject.fetch(uri, options, counter)
+ end
+ end
+
+ context "when the request response is a Gem::Net::HTTPRedirection" do
+ let(:http_response) { Gem::Net::HTTPRedirection.new(httpv, 308, "Moved") }
+
+ before { http_response["location"] = "http://www.redirect-uri.com/api/v2/endpoint" }
+
+ it "should try to fetch the redirect uri and iterate the # requests counter" do
+ expect(subject).to receive(:fetch).with(Gem::URI("http://www.uri-to-fetch.com/api/v2/endpoint"), options, 0).and_call_original
+ expect(subject).to receive(:fetch).with(Gem::URI("http://www.redirect-uri.com/api/v2/endpoint"), options, 1)
+ subject.fetch(uri, options, counter)
+ end
+
+ context "when the redirect uri and original uri are the same" do
+ let(:uri) { Gem::URI("ssh://username:password@www.uri-to-fetch.com/api/v2/endpoint") }
+
+ before { http_response["location"] = "ssh://www.uri-to-fetch.com/api/v1/endpoint" }
+
+ it "should set the same user and password for the redirect uri" do
+ expect(subject).to receive(:fetch).with(Gem::URI("ssh://username:password@www.uri-to-fetch.com/api/v2/endpoint"), options, 0).and_call_original
+ expect(subject).to receive(:fetch).with(Gem::URI("ssh://username:password@www.uri-to-fetch.com/api/v1/endpoint"), options, 1)
+ subject.fetch(uri, options, counter)
+ end
+ end
+ end
+
+ context "when the request response is a Gem::Net::HTTPSuccess" do
+ let(:http_response) { Gem::Net::HTTPSuccess.new("1.1", 200, "Success") }
+
+ it "should return the response body" do
+ expect(subject.fetch(uri, options, counter)).to eq(http_response)
+ end
+ end
+
+ context "when the request response is a Gem::Net::HTTPRequestEntityTooLarge" do
+ let(:http_response) { Gem::Net::HTTPRequestEntityTooLarge.new("1.1", 413, "Too Big") }
+
+ it "should raise a Bundler::Fetcher::FallbackError with the response body" do
+ expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::FallbackError, "Body with info")
+ end
+ end
+
+ context "when the request response is a Gem::Net::HTTPUnauthorized" do
+ let(:http_response) { Gem::Net::HTTPUnauthorized.new("1.1", 401, "Unauthorized") }
+
+ it "should raise a Bundler::Fetcher::AuthenticationRequiredError with the uri host" do
+ expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError,
+ /Authentication is required for www.uri-to-fetch.com/)
+ end
+
+ it "should raise a Bundler::Fetcher::AuthenticationRequiredError with advice" do
+ expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError,
+ /`bundle config set --global www\.uri-to-fetch\.com username:password`.*`BUNDLE_WWW__URI___TO___FETCH__COM`/m)
+ end
+
+ context "when there are credentials provided in the request" do
+ let(:uri) { Gem::URI("http://user:password@www.uri-to-fetch.com") }
+
+ it "should raise a Bundler::Fetcher::BadAuthenticationError that doesn't contain the password" do
+ expect { subject.fetch(uri, options, counter) }.
+ to raise_error(Bundler::Fetcher::BadAuthenticationError, /Bad username or password for www.uri-to-fetch.com/)
+ end
+ end
+ end
+
+ context "when the request response is a Gem::Net::HTTPForbidden" do
+ let(:http_response) { Gem::Net::HTTPForbidden.new("1.1", 403, "Forbidden") }
+ let(:uri) { Gem::URI("http://user:password@www.uri-to-fetch.com") }
+
+ it "should raise a Bundler::Fetcher::AuthenticationForbiddenError with the uri host" do
+ expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::AuthenticationForbiddenError,
+ /Access token could not be authenticated for www.uri-to-fetch.com/)
+ end
+ end
+
+ context "when the request response is a Gem::Net::HTTPNotFound" do
+ let(:http_response) { Gem::Net::HTTPNotFound.new("1.1", 404, "Not Found") }
+
+ it "should raise a Bundler::Fetcher::FallbackError with Gem::Net::HTTPNotFound" do
+ expect { subject.fetch(uri, options, counter) }.
+ to raise_error(Bundler::Fetcher::FallbackError, "Gem::Net::HTTPNotFound: http://www.uri-to-fetch.com/api/v2/endpoint")
+ end
+
+ context "when there are credentials provided in the request" do
+ let(:uri) { Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") }
+
+ it "should raise a Bundler::Fetcher::FallbackError that doesn't contain the password" do
+ expect { subject.fetch(uri, options, counter) }.
+ to raise_error(Bundler::Fetcher::FallbackError, "Gem::Net::HTTPNotFound: http://username@www.uri-to-fetch.com/api/v2/endpoint")
+ end
+ end
+ end
+
+ context "when the request response is a Gem::Net::HTTPRequestedRangeNotSatisfiable" do
+ let(:http_response) { Gem::Net::HTTPRequestedRangeNotSatisfiable.new("1.1", 416, "Range Not Satisfiable") }
+ let(:success_response) { Gem::Net::HTTPSuccess.new("1.1", 200, "Success") }
+ let(:options) { { "Range" => "bytes=1000-", "If-None-Match" => "some-etag" } }
+
+ before do
+ # First request returns 416, retry request returns success
+ allow(subject).to receive(:request).with(uri, options).and_return(http_response)
+ allow(subject).to receive(:request).with(uri, { "If-None-Match" => "some-etag" }).and_return(success_response)
+ end
+
+ # The 416 handler removes the Range header and retries without incrementing the counter.
+ # Importantly, it does NOT add Accept-Encoding header, which would break Ruby's
+ # automatic gzip decompression (see issue #9271 for details on that bug).
+ it "should retry the request without the Range header" do
+ expect(subject).to receive(:request).with(uri, options).ordered
+ expect(subject).to receive(:request).with(uri, hash_excluding("Range", "Accept-Encoding")).ordered
+ subject.fetch(uri, options, counter)
+ end
+
+ it "should preserve other headers on retry" do
+ expect(subject).to receive(:request).with(uri, options).ordered
+ expect(subject).to receive(:request).with(uri, hash_including("If-None-Match" => "some-etag")).ordered
+ subject.fetch(uri, options, counter)
+ end
+
+ it "should return the successful response" do
+ result = subject.fetch(uri, options, counter)
+ expect(result).to eq(success_response)
+ end
+ end
+
+ context "when the request response is some other type" do
+ let(:http_response) { Gem::Net::HTTPBadGateway.new("1.1", 500, "Fatal Error") }
+
+ it "should raise a Bundler::HTTPError with the response class and body" do
+ expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::HTTPError, "Gem::Net::HTTPBadGateway: Body with info")
+ end
+ end
+ end
+
+ describe "request" do
+ let(:net_http_get) { double(:net_http_get) }
+ let(:response) { double(:response) }
+
+ before do
+ allow(Gem::Net::HTTP::Get).to receive(:new).with("/api/v2/endpoint", options).and_return(net_http_get)
+ allow(connection).to receive(:request).with(uri, net_http_get).and_return(response)
+ end
+
+ it "should log the HTTP GET request to debug" do
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://www.uri-to-fetch.com/api/v2/endpoint")
+ subject.request(uri, options)
+ end
+
+ context "when there is a user provided in the request" do
+ context "and there is also a password provided" do
+ context "that contains cgi escaped characters" do
+ let(:uri) { Gem::URI("http://username:password%24@www.uri-to-fetch.com/api/v2/endpoint") }
+
+ it "should request basic authentication with the username and password, and log the HTTP GET request to debug, without the password" do
+ expect(net_http_get).to receive(:basic_auth).with("username", "password$")
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://username@www.uri-to-fetch.com/api/v2/endpoint")
+ subject.request(uri, options)
+ end
+ end
+
+ context "that is all unescaped characters" do
+ let(:uri) { Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") }
+ it "should request basic authentication with the username and proper cgi compliant password, and log the HTTP GET request to debug, without the password" do
+ expect(net_http_get).to receive(:basic_auth).with("username", "password")
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://username@www.uri-to-fetch.com/api/v2/endpoint")
+ subject.request(uri, options)
+ end
+ end
+ end
+
+ context "and it's used as the authentication token" do
+ let(:uri) { Gem::URI("http://username@www.uri-to-fetch.com/api/v2/endpoint") }
+
+ it "should request basic authentication with just the user, and log the HTTP GET request to debug, without the token" do
+ expect(net_http_get).to receive(:basic_auth).with("username", nil)
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://www.uri-to-fetch.com/api/v2/endpoint")
+ subject.request(uri, options)
+ end
+ end
+
+ context "and it's used as the authentication token, and contains cgi escaped characters" do
+ let(:uri) { Gem::URI("http://username%24@www.uri-to-fetch.com/api/v2/endpoint") }
+
+ it "should request basic authentication with the proper cgi compliant password user, and log the HTTP GET request to debug, without the token" do
+ expect(net_http_get).to receive(:basic_auth).with("username$", nil)
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://www.uri-to-fetch.com/api/v2/endpoint")
+ subject.request(uri, options)
+ end
+ end
+ end
+
+ context "when the request response causes a OpenSSL::SSL::SSLError" do
+ before { allow(connection).to receive(:request).with(uri, net_http_get) { raise OpenSSL::SSL::SSLError.new } }
+
+ it "should raise a LoadError about openssl" do
+ expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::CertificateFailureError,
+ %r{Could not verify the SSL certificate for http://www.uri-to-fetch.com/api/v2/endpoint})
+ end
+ end
+
+ context "when the request response causes an HTTP error" do
+ let(:message) { "error about network" }
+ let(:error) { error_class.new(message) }
+
+ before do
+ allow(connection).to receive(:request).with(uri, net_http_get) { raise error }
+ end
+
+ context "that it's retryable" do
+ let(:error_class) { Gem::Timeout::Error }
+
+ it "should trace log the error" do
+ allow(Bundler).to receive_message_chain(:ui, :debug)
+ expect(Bundler).to receive_message_chain(:ui, :trace).with(error)
+ expect { subject.request(uri, options) }.to raise_error(Bundler::HTTPError)
+ end
+
+ it "should raise a Bundler::HTTPError" do
+ expect { subject.request(uri, options) }.to raise_error(Bundler::HTTPError,
+ "Network error while fetching http://www.uri-to-fetch.com/api/v2/endpoint (error about network)")
+ end
+
+ context "when there are credentials provided in the request" do
+ let(:uri) { Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") }
+ before do
+ allow(net_http_get).to receive(:basic_auth).with("username", "password")
+ end
+
+ it "should raise a Bundler::HTTPError that doesn't contain the password" do
+ expect { subject.request(uri, options) }.to raise_error(Bundler::HTTPError,
+ "Network error while fetching http://username@www.uri-to-fetch.com/api/v2/endpoint (error about network)")
+ end
+ end
+ end
+
+ context "when error is about the host being down" do
+ let(:error_class) { Gem::Net::HTTP::Persistent::Error }
+ let(:message) { "host down: http://www.uri-to-fetch.com" }
+
+ it "should raise a Bundler::Fetcher::NetworkDownError" do
+ expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::NetworkDownError,
+ /Could not reach host www.uri-to-fetch.com/)
+ end
+ end
+
+ context "when error is about connection refused" do
+ let(:error_class) { Gem::Net::HTTP::Persistent::Error }
+ let(:message) { "connection refused down: http://www.uri-to-fetch.com" }
+
+ it "should raise a Bundler::Fetcher::NetworkDownError" do
+ expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::NetworkDownError,
+ /Could not reach host www.uri-to-fetch.com/)
+ end
+ end
+
+ context "when error is about no route to host" do
+ let(:error_class) { SocketError }
+ let(:message) { "Failed to open TCP connection to www.uri-to-fetch.com:443 " }
+
+ it "should raise a Bundler::Fetcher::NetworkDownError" do
+ expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::NetworkDownError,
+ /Could not reach host www.uri-to-fetch.com/)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/fetcher/gem_remote_fetcher_spec.rb b/spec/bundler/bundler/fetcher/gem_remote_fetcher_spec.rb
new file mode 100644
index 0000000000..df1a58d843
--- /dev/null
+++ b/spec/bundler/bundler/fetcher/gem_remote_fetcher_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require "rubygems/remote_fetcher"
+require "bundler/fetcher/gem_remote_fetcher"
+require_relative "../../support/artifice/helpers/artifice"
+require "bundler/vendored_persistent.rb"
+
+RSpec.describe Bundler::Fetcher::GemRemoteFetcher do
+ describe "Parallel download" do
+ it "download using multiple connections from the pool" do
+ unless Bundler.rubygems.provides?(">= 4.0.0.dev")
+ skip "This example can only run when RubyGems supports multiple http connection pool"
+ end
+
+ require_relative "../../support/artifice/helpers/endpoint"
+ concurrent_ruby_path = Dir[scoped_base_system_gem_path.join("gems/concurrent-ruby-*/lib/concurrent-ruby")].first
+ $LOAD_PATH.unshift(concurrent_ruby_path)
+ require "concurrent-ruby"
+
+ require_rack_test
+ responses = []
+
+ latch1 = Concurrent::CountDownLatch.new
+ latch2 = Concurrent::CountDownLatch.new
+ previous_client = Gem::Request::ConnectionPools.client
+ dummy_endpoint = Class.new(Endpoint) do
+ get "/foo" do
+ latch2.count_down
+ latch1.wait
+
+ responses << "foo"
+ end
+
+ get "/bar" do
+ responses << "bar"
+
+ latch1.count_down
+ end
+ end
+
+ Artifice.activate_with(dummy_endpoint)
+ Gem::Request::ConnectionPools.client = Gem::Net::HTTP
+
+ first_request = Thread.new do
+ subject.fetch_path("https://example.org/foo")
+ end
+ second_request = Thread.new do
+ latch2.wait
+ subject.fetch_path("https://example.org/bar")
+ end
+
+ [first_request, second_request].each(&:join)
+
+ expect(responses).to eq(["bar", "foo"])
+ ensure
+ Artifice.deactivate
+ Gem::Request::ConnectionPools.client = previous_client
+ end
+ end
+end
diff --git a/spec/bundler/bundler/fetcher/index_spec.rb b/spec/bundler/bundler/fetcher/index_spec.rb
new file mode 100644
index 0000000000..a6a18efd98
--- /dev/null
+++ b/spec/bundler/bundler/fetcher/index_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require "rubygems/remote_fetcher"
+
+RSpec.describe Bundler::Fetcher::Index do
+ let(:downloader) { nil }
+ let(:remote) { double(:remote, uri: remote_uri) }
+ let(:remote_uri) { Gem::URI("http://#{userinfo}remote-uri.org") }
+ let(:userinfo) { "" }
+ let(:display_uri) { "http://sample_uri.com" }
+ let(:rubygems) { double(:rubygems) }
+ let(:gem_names) { %w[foo bar] }
+ let(:gem_remote_fetcher) { nil }
+
+ subject { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) }
+
+ before { allow(Bundler).to receive(:rubygems).and_return(rubygems) }
+
+ it "fetches and returns the list of remote specs" do
+ expect(rubygems).to receive(:fetch_all_remote_specs) { nil }
+ subject.specs(gem_names)
+ end
+
+ context "error handling" do
+ before do
+ allow(rubygems).to receive(:fetch_all_remote_specs) { raise Gem::RemoteFetcher::FetchError.new(error_message, display_uri) }
+ end
+
+ context "when certificate verify failed" do
+ let(:error_message) { "certificate verify failed" }
+
+ it "should raise a Bundler::Fetcher::CertificateFailureError" do
+ expect { subject.specs(gem_names) }.to raise_error(Bundler::Fetcher::CertificateFailureError,
+ %r{Could not verify the SSL certificate for http://sample_uri.com})
+ end
+ end
+
+ context "when a 401 response occurs" do
+ let(:error_message) { "401" }
+
+ it "should raise a Bundler::Fetcher::AuthenticationRequiredError" do
+ expect { subject.specs(gem_names) }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError,
+ %r{Authentication is required for http://remote-uri.org})
+ end
+
+ context "and there was userinfo" do
+ let(:userinfo) { "user:pass@" }
+
+ it "should raise a Bundler::Fetcher::BadAuthenticationError" do
+ expect { subject.specs(gem_names) }.to raise_error(Bundler::Fetcher::BadAuthenticationError,
+ %r{Bad username or password for http://user@remote-uri.org})
+ end
+ end
+ end
+
+ context "when a 403 response occurs" do
+ let(:error_message) { "403" }
+
+ it "should raise a Bundler::Fetcher::AuthenticationForbiddenError" do
+ expect { subject.specs(gem_names) }.to raise_error(Bundler::Fetcher::AuthenticationForbiddenError,
+ %r{Access token could not be authenticated for http://remote-uri.org})
+ end
+ end
+
+ context "any other message is returned" do
+ let(:error_message) { "You get an error, you get an error!" }
+
+ before { allow(Bundler).to receive(:ui).and_return(double(trace: nil)) }
+
+ it "should raise a Bundler::HTTPError" do
+ expect { subject.specs(gem_names) }.to raise_error(Bundler::HTTPError, "Could not fetch specs from http://sample_uri.com due to underlying error <You get an error, you get an error! (http://sample_uri.com)>")
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/fetcher_spec.rb b/spec/bundler/bundler/fetcher_spec.rb
new file mode 100644
index 0000000000..e20f7e7c48
--- /dev/null
+++ b/spec/bundler/bundler/fetcher_spec.rb
@@ -0,0 +1,260 @@
+# frozen_string_literal: true
+
+require "bundler/fetcher"
+
+RSpec.describe Bundler::Fetcher do
+ let(:uri) { Gem::URI("https://example.com") }
+ let(:remote) { double("remote", uri: uri, original_uri: nil) }
+
+ subject(:fetcher) { Bundler::Fetcher.new(remote) }
+
+ before do
+ allow(Bundler).to receive(:root) { Pathname.new("root") }
+ end
+
+ describe "#connection" do
+ context "when Gem.configuration doesn't specify http_proxy" do
+ it "specify no http_proxy" do
+ expect(fetcher.http_proxy).to be_nil
+ end
+ it "consider environment vars when determine proxy" do
+ with_env_vars("HTTP_PROXY" => "http://proxy-example.com") do
+ expect(fetcher.http_proxy).to match("http://proxy-example.com")
+ end
+ end
+ end
+ context "when Gem.configuration specifies http_proxy " do
+ let(:proxy) { "http://proxy-example2.com" }
+ before do
+ allow(Gem.configuration).to receive(:[]).with(:http_proxy).and_return(proxy)
+ end
+ it "consider Gem.configuration when determine proxy" do
+ expect(fetcher.http_proxy).to match("http://proxy-example2.com")
+ end
+ it "consider Gem.configuration when determine proxy" do
+ with_env_vars("HTTP_PROXY" => "http://proxy-example.com") do
+ expect(fetcher.http_proxy).to match("http://proxy-example2.com")
+ end
+ end
+ context "when the proxy is :no_proxy" do
+ let(:proxy) { :no_proxy }
+ it "does not set a proxy" do
+ expect(fetcher.http_proxy).to be_nil
+ end
+ end
+ end
+
+ context "when a rubygems source mirror is set" do
+ let(:orig_uri) { Gem::URI("http://zombo.com") }
+ let(:remote_with_mirror) do
+ double("remote", uri: uri, original_uri: orig_uri, anonymized_uri: uri)
+ end
+
+ let(:fetcher) { Bundler::Fetcher.new(remote_with_mirror) }
+
+ it "sets the 'X-Gemfile-Source' header containing the original source" do
+ expect(
+ fetcher.send(:connection).override_headers["X-Gemfile-Source"]
+ ).to eq("http://zombo.com")
+ end
+ end
+
+ context "when there is no rubygems source mirror set" do
+ let(:remote_no_mirror) do
+ double("remote", uri: uri, original_uri: nil, anonymized_uri: uri)
+ end
+
+ let(:fetcher) { Bundler::Fetcher.new(remote_no_mirror) }
+
+ it "does not set the 'X-Gemfile-Source' header" do
+ expect(fetcher.send(:connection).override_headers["X-Gemfile-Source"]).to be_nil
+ end
+ end
+
+ context "when there are proxy environment variable(s) set" do
+ it "consider http_proxy" do
+ with_env_vars("HTTP_PROXY" => "http://proxy-example3.com") do
+ expect(fetcher.http_proxy).to match("http://proxy-example3.com")
+ end
+ end
+ it "consider no_proxy" do
+ with_env_vars("HTTP_PROXY" => "http://proxy-example4.com", "NO_PROXY" => ".example.com,.example.net") do
+ expect(
+ fetcher.send(:connection).no_proxy
+ ).to eq([".example.com", ".example.net"])
+ end
+ end
+ end
+
+ context "when no ssl configuration is set" do
+ it "no cert" do
+ expect(fetcher.send(:connection).cert).to be_nil
+ expect(fetcher.send(:connection).key).to be_nil
+ end
+ end
+
+ context "when bunder ssl ssl configuration is set" do
+ before do
+ cert = File.join(Spec::Path.tmpdir, "cert")
+ File.open(cert, "w") {|f| f.write "PEM" }
+ allow(Bundler.settings).to receive(:[]).and_return(nil)
+ allow(Bundler.settings).to receive(:[]).with(:ssl_client_cert).and_return(cert)
+ expect(OpenSSL::X509::Certificate).to receive(:new).with("PEM").and_return("cert")
+ expect(OpenSSL::PKey::RSA).to receive(:new).with("PEM").and_return("key")
+ end
+ after do
+ FileUtils.rm File.join(Spec::Path.tmpdir, "cert")
+ end
+ it "use bundler configuration" do
+ expect(fetcher.send(:connection).cert).to eq("cert")
+ expect(fetcher.send(:connection).key).to eq("key")
+ end
+ end
+
+ context "when gem ssl configuration is set" do
+ before do
+ allow(Gem.configuration).to receive_messages(
+ http_proxy: nil,
+ ssl_client_cert: "cert",
+ ssl_ca_cert: "ca"
+ )
+ expect(File).to receive(:read).and_return("")
+ expect(OpenSSL::X509::Certificate).to receive(:new).and_return("cert")
+ expect(OpenSSL::PKey::RSA).to receive(:new).and_return("key")
+ store = double("ca store")
+ expect(store).to receive(:add_file)
+ expect(OpenSSL::X509::Store).to receive(:new).and_return(store)
+ end
+ it "use gem configuration" do
+ expect(fetcher.send(:connection).cert).to eq("cert")
+ expect(fetcher.send(:connection).key).to eq("key")
+ end
+ end
+ end
+
+ describe "#user_agent" do
+ it "builds user_agent with current ruby version and Bundler settings" do
+ allow(Bundler.settings).to receive(:all).and_return(%w[foo bar])
+ expect(fetcher.user_agent).to match(%r{bundler/(\d.)})
+ expect(fetcher.user_agent).to match(%r{rubygems/(\d.)})
+ expect(fetcher.user_agent).to match(%r{ruby/(\d.)})
+ expect(fetcher.user_agent).to match(%r{options/foo,bar})
+ end
+
+ describe "include CI information" do
+ it "from one CI" do
+ with_env_vars("CI" => nil, "JENKINS_URL" => "foo") do
+ ci_part = fetcher.user_agent.split(" ").find {|x| x.start_with?("ci/") }
+ cis = ci_part.split("/").last.split(",")
+ expect(cis).to include("jenkins")
+ expect(cis).not_to include("ci")
+ end
+ end
+
+ it "from many CI" do
+ with_env_vars("CI" => "true", "SEMAPHORE" => nil, "TRAVIS" => "foo", "GITLAB_CI" => "gitlab", "CI_NAME" => "MY_ci") do
+ ci_part = fetcher.user_agent.split(" ").find {|x| x.start_with?("ci/") }
+ cis = ci_part.split("/").last.split(",")
+ expect(cis).to include("ci", "gitlab", "my_ci", "travis")
+ expect(cis).not_to include("semaphore")
+ end
+ end
+ end
+ end
+
+ describe "#fetch_spec" do
+ let(:name) { "name" }
+ let(:version) { "1.3.17" }
+ let(:platform) { "platform" }
+ let(:downloader) { double("downloader") }
+ let(:body) { double(Gem::Net::HTTP::Get, body: downloaded_data) }
+
+ context "when attempting to load a Gem::Specification" do
+ let(:spec) { Gem::Specification.new(name, version) }
+ let(:downloaded_data) { Zlib::Deflate.deflate(Marshal.dump(spec)) }
+
+ it "returns the spec" do
+ expect(Bundler::Fetcher::Downloader).to receive(:new).and_return(downloader)
+ expect(downloader).to receive(:fetch).once.and_return(body)
+ result = fetcher.fetch_spec([name, version, platform])
+ expect(result).to eq(spec)
+ end
+ end
+
+ context "when attempting to load an unexpected class" do
+ let(:downloaded_data) { Zlib::Deflate.deflate(Marshal.dump(3)) }
+
+ it "raises a HTTPError error" do
+ expect(Bundler::Fetcher::Downloader).to receive(:new).and_return(downloader)
+ expect(downloader).to receive(:fetch).once.and_return(body)
+ expect { fetcher.fetch_spec([name, version, platform]) }.to raise_error(Bundler::HTTPError, /Gemspec .* contained invalid data/i)
+ end
+ end
+ end
+
+ describe "#specs_with_retry" do
+ let(:downloader) { double(:downloader) }
+ let(:remote) { double(:remote, cache_slug: "slug", uri: uri, original_uri: nil, anonymized_uri: uri) }
+ let(:compact_index) { double(Bundler::Fetcher::CompactIndex, available?: true, api_fetcher?: true) }
+ let(:dependency) { double(Bundler::Fetcher::Dependency, available?: true, api_fetcher?: true) }
+ let(:index) { double(Bundler::Fetcher::Index, available?: true, api_fetcher?: false) }
+
+ before do
+ allow(Bundler::Fetcher::CompactIndex).to receive(:new).and_return(compact_index)
+ allow(Bundler::Fetcher::Dependency).to receive(:new).and_return(dependency)
+ allow(Bundler::Fetcher::Index).to receive(:new).and_return(index)
+ end
+
+ it "picks the first fetcher that works" do
+ expect(compact_index).to receive(:specs).with("name").and_return([["name", "1.2.3", "ruby"]])
+ expect(dependency).not_to receive(:specs)
+ expect(index).not_to receive(:specs)
+ fetcher.specs_with_retry("name", double(Bundler::Source::Rubygems))
+ end
+
+ context "when APIs are not available" do
+ before do
+ allow(compact_index).to receive(:available?).and_return(false)
+ allow(dependency).to receive(:available?).and_return(false)
+ end
+
+ it "uses the index" do
+ expect(compact_index).not_to receive(:specs)
+ expect(dependency).not_to receive(:specs)
+ expect(index).to receive(:specs).with("name").and_return([["name", "1.2.3", "ruby"]])
+
+ fetcher.specs_with_retry("name", double(Bundler::Source::Rubygems))
+ end
+ end
+ end
+
+ describe "#api_fetcher?" do
+ let(:downloader) { double(:downloader) }
+ let(:remote) { double(:remote, cache_slug: "slug", uri: uri, original_uri: nil, anonymized_uri: uri) }
+ let(:compact_index) { double(Bundler::Fetcher::CompactIndex, available?: false, api_fetcher?: true) }
+ let(:dependency) { double(Bundler::Fetcher::Dependency, available?: false, api_fetcher?: true) }
+ let(:index) { double(Bundler::Fetcher::Index, available?: true, api_fetcher?: false) }
+
+ before do
+ allow(Bundler::Fetcher::CompactIndex).to receive(:new).and_return(compact_index)
+ allow(Bundler::Fetcher::Dependency).to receive(:new).and_return(dependency)
+ allow(Bundler::Fetcher::Index).to receive(:new).and_return(index)
+ end
+
+ context "when an api fetcher is available" do
+ before do
+ allow(compact_index).to receive(:available?).and_return(true)
+ end
+
+ it "is truthy" do
+ expect(fetcher).to be_api_fetcher
+ end
+ end
+
+ context "when only the index fetcher is available" do
+ it "is falsey" do
+ expect(fetcher).not_to be_api_fetcher
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/friendly_errors_spec.rb b/spec/bundler/bundler/friendly_errors_spec.rb
new file mode 100644
index 0000000000..426e3c856d
--- /dev/null
+++ b/spec/bundler/bundler/friendly_errors_spec.rb
@@ -0,0 +1,231 @@
+# frozen_string_literal: true
+
+require "bundler"
+require "bundler/friendly_errors"
+require "cgi/escape"
+require "cgi/util" unless defined?(CGI::EscapeExt)
+
+RSpec.describe Bundler, "friendly errors" do
+ context "with invalid YAML in .gemrc" do
+ before do
+ File.open(home(".gemrc"), "w") do |f|
+ f.write "invalid: yaml: hah"
+ end
+ end
+
+ after do
+ FileUtils.rm(home(".gemrc"))
+ end
+
+ it "reports a relevant friendly error message" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle :install, env: { "DEBUG" => "true" }
+
+ expect(err).to include("Failed to load #{home(".gemrc")}")
+ end
+ end
+
+ it "calls log_error in case of exception" do
+ exception = Exception.new
+ expect(Bundler::FriendlyErrors).to receive(:exit_status).with(exception).and_return(1)
+ expect do
+ Bundler.with_friendly_errors do
+ raise exception
+ end
+ end.to raise_error(SystemExit)
+ end
+
+ it "calls exit_status on exception" do
+ exception = Exception.new
+ expect(Bundler::FriendlyErrors).to receive(:log_error).with(exception)
+ expect do
+ Bundler.with_friendly_errors do
+ raise exception
+ end
+ end.to raise_error(SystemExit)
+ end
+
+ describe "#log_error" do
+ shared_examples "Bundler.ui receive error" do |error, message|
+ it "" do
+ expect(Bundler.ui).to receive(:error).with(message || error.message)
+ Bundler::FriendlyErrors.log_error(error)
+ end
+ end
+
+ shared_examples "Bundler.ui receive trace" do |error|
+ it "" do
+ expect(Bundler.ui).to receive(:trace).with(error)
+ Bundler::FriendlyErrors.log_error(error)
+ end
+ end
+
+ context "YamlSyntaxError" do
+ it_behaves_like "Bundler.ui receive error", Bundler::YamlSyntaxError.new(StandardError.new, "sample_message")
+
+ it "Bundler.ui receive trace" do
+ std_error = StandardError.new
+ exception = Bundler::YamlSyntaxError.new(std_error, "sample_message")
+ expect(Bundler.ui).to receive(:trace).with(std_error)
+ Bundler::FriendlyErrors.log_error(exception)
+ end
+ end
+
+ context "Dsl::DSLError, GemspecError" do
+ it_behaves_like "Bundler.ui receive error", Bundler::Dsl::DSLError.new("description", "dsl_path", "backtrace")
+ it_behaves_like "Bundler.ui receive error", Bundler::GemspecError.new
+ end
+
+ context "GemRequireError" do
+ let(:orig_error) { StandardError.new }
+ let(:error) { Bundler::GemRequireError.new(orig_error, "sample_message") }
+
+ before do
+ allow(orig_error).to receive(:backtrace).and_return([])
+ end
+
+ it "Bundler.ui receive error" do
+ expect(Bundler.ui).to receive(:error).with(error.message)
+ Bundler::FriendlyErrors.log_error(error)
+ end
+
+ it "writes to Bundler.ui.trace" do
+ expect(Bundler.ui).to receive(:trace).with(orig_error)
+ Bundler::FriendlyErrors.log_error(error)
+ end
+ end
+
+ context "BundlerError" do
+ it "Bundler.ui receive error" do
+ error = Bundler::BundlerError.new
+ expect(Bundler.ui).to receive(:error).with(error.message, wrap: true)
+ Bundler::FriendlyErrors.log_error(error)
+ end
+ end
+
+ context "Thor::Error" do
+ it_behaves_like "Bundler.ui receive error", Bundler::Thor::Error.new
+ end
+
+ context "Interrupt" do
+ it "Bundler.ui receive error" do
+ expect(Bundler.ui).to receive(:error).with("\nQuitting...")
+ Bundler::FriendlyErrors.log_error(Interrupt.new)
+ end
+ it_behaves_like "Bundler.ui receive trace", Interrupt.new
+ end
+
+ context "Gem::InvalidSpecificationException" do
+ it "Bundler.ui receive error" do
+ error = Gem::InvalidSpecificationException.new
+ expect(Bundler.ui).to receive(:error).with(error.message, wrap: true)
+ Bundler::FriendlyErrors.log_error(error)
+ end
+ end
+
+ context "SystemExit" do
+ # Does nothing
+ end
+
+ context "Java::JavaLang::OutOfMemoryError", :jruby_only do
+ it "Bundler.ui receive error" do
+ install_gemfile <<-G, raise_on_error: false, env: { "JRUBY_OPTS" => "-J-Xmx32M" }, artifice: nil
+ source "https://gem.repo1"
+ G
+
+ expect(err).to include("JVM has run out of memory")
+ end
+ end
+
+ context "unexpected error" do
+ it "calls request_issue_report_for with error" do
+ error = StandardError.new
+ expect(Bundler::FriendlyErrors).to receive(:request_issue_report_for).with(error)
+ Bundler::FriendlyErrors.log_error(error)
+ end
+ end
+ end
+
+ describe "#exit_status" do
+ it "calls status_code for BundlerError" do
+ error = Bundler::BundlerError.new
+ expect(error).to receive(:status_code).and_return("sample_status_code")
+ expect(Bundler::FriendlyErrors.exit_status(error)).to eq("sample_status_code")
+ end
+
+ it "returns 15 for Thor::Error" do
+ error = Bundler::Thor::Error.new
+ expect(Bundler::FriendlyErrors.exit_status(error)).to eq(15)
+ end
+
+ it "calls status for SystemExit" do
+ error = SystemExit.new
+ expect(error).to receive(:status).and_return("sample_status")
+ expect(Bundler::FriendlyErrors.exit_status(error)).to eq("sample_status")
+ end
+
+ it "returns 1 in other cases" do
+ error = StandardError.new
+ expect(Bundler::FriendlyErrors.exit_status(error)).to eq(1)
+ end
+ end
+
+ describe "#request_issue_report_for" do
+ it "calls relevant methods for Bundler.ui" do
+ expect(Bundler.ui).not_to receive(:info)
+ expect(Bundler.ui).to receive(:error).exactly(3).times
+ expect(Bundler.ui).not_to receive(:warn)
+ Bundler::FriendlyErrors.request_issue_report_for(StandardError.new)
+ end
+
+ it "includes error class, message and backlog" do
+ error = StandardError.new
+ allow(Bundler::FriendlyErrors).to receive(:issues_url).and_return("")
+
+ expect(error).to receive(:class).at_least(:once)
+ expect(error).to receive(:message).at_least(:once)
+ expect(error).to receive(:backtrace).at_least(:once)
+ Bundler::FriendlyErrors.request_issue_report_for(error)
+ end
+ end
+
+ describe "#issues_url" do
+ it "generates a search URL for the exception message" do
+ exception = Exception.new("Exception message")
+
+ expect(Bundler::FriendlyErrors.issues_url(exception)).to eq("https://github.com/ruby/rubygems/search?q=Exception+message&type=Issues")
+ end
+
+ it "generates a search URL for only the first line of a multi-line exception message" do
+ exception = Exception.new(<<END)
+First line of the exception message
+Second line of the exception message
+END
+
+ expect(Bundler::FriendlyErrors.issues_url(exception)).to eq("https://github.com/ruby/rubygems/search?q=First+line+of+the+exception+message&type=Issues")
+ end
+
+ it "generates the url without colons" do
+ exception = Exception.new(<<END)
+Exception ::: with ::: colons :::
+END
+ issues_url = Bundler::FriendlyErrors.issues_url(exception)
+ expect(issues_url).not_to include("%3A")
+ expect(issues_url).to eq("https://github.com/ruby/rubygems/search?q=#{CGI.escape("Exception with colons ")}&type=Issues")
+ end
+
+ it "removes information after - for Errono::EACCES" do
+ exception = Exception.new(<<END)
+Errno::EACCES: Permission denied @ dir_s_mkdir - /Users/foo/bar/
+END
+ allow(exception).to receive(:is_a?).with(Errno).and_return(true)
+ issues_url = Bundler::FriendlyErrors.issues_url(exception)
+ expect(issues_url).not_to include("/Users/foo/bar")
+ expect(issues_url).to eq("https://github.com/ruby/rubygems/search?q=#{CGI.escape("Errno EACCES Permission denied @ dir_s_mkdir ")}&type=Issues")
+ end
+ end
+end
diff --git a/spec/bundler/bundler/gem_helper_spec.rb b/spec/bundler/bundler/gem_helper_spec.rb
new file mode 100644
index 0000000000..b4ae2abdc5
--- /dev/null
+++ b/spec/bundler/bundler/gem_helper_spec.rb
@@ -0,0 +1,456 @@
+# frozen_string_literal: true
+
+require "rake"
+require "bundler/gem_helper"
+
+RSpec.describe Bundler::GemHelper do
+ let(:app_name) { "lorem__ipsum" }
+ let(:app_path) { bundled_app app_name }
+ let(:app_gemspec_path) { app_path.join("#{app_name}.gemspec") }
+
+ before(:each) do
+ bundle_config_global "gem.mit false"
+ bundle_config_global "gem.test false"
+ bundle_config_global "gem.coc false"
+ bundle_config_global "gem.linter false"
+ bundle_config_global "gem.ci false"
+ bundle_config_global "gem.changelog false"
+ git("config --global init.defaultBranch main")
+ bundle "gem #{app_name}"
+ prepare_gemspec(app_gemspec_path)
+ end
+
+ context "determining gemspec" do
+ subject { Bundler::GemHelper.new(app_path) }
+
+ context "fails" do
+ it "when there is no gemspec" do
+ FileUtils.rm app_gemspec_path
+ expect { subject }.to raise_error(/Unable to determine name/)
+ end
+
+ it "when there are two gemspecs and the name isn't specified" do
+ FileUtils.touch app_path.join("#{app_name}-2.gemspec")
+ expect { subject }.to raise_error(/Unable to determine name/)
+ end
+ end
+
+ context "interpolates the name" do
+ it "when there is only one gemspec" do
+ expect(subject.gemspec.name).to eq(app_name)
+ end
+
+ it "for a hidden gemspec" do
+ FileUtils.mv app_gemspec_path, app_path.join(".gemspec")
+ expect(subject.gemspec.name).to eq(app_name)
+ end
+ end
+
+ it "handles namespaces and converts them to CamelCase" do
+ bundle "gem #{app_name}-foo_bar"
+ underscore_path = bundled_app "#{app_name}-foo_bar"
+
+ lib = underscore_path.join("lib/#{app_name}/foo_bar.rb").read
+ expect(lib).to include("module LoremIpsum")
+ expect(lib).to include("module FooBar")
+ end
+ end
+
+ context "gem management" do
+ def mock_confirm_message(message)
+ expect(Bundler.ui).to receive(:confirm).with(message)
+ end
+
+ def mock_build_message(name, version)
+ message = "#{name} #{version} built to pkg/#{name}-#{version}.gem."
+ mock_confirm_message message
+ end
+
+ def mock_checksum_message(name, version)
+ message = "#{name} #{version} checksum written to checksums/#{name}-#{version}.gem.sha512."
+ mock_confirm_message message
+ end
+
+ def sha512_hexdigest(path)
+ Digest::SHA512.file(path).hexdigest
+ end
+
+ subject! { Bundler::GemHelper.new(app_path) }
+ let(:app_version) { "0.1.0" }
+ let(:app_gem_dir) { app_path.join("pkg") }
+ let(:app_gem_path) { app_gem_dir.join("#{app_name}-#{app_version}.gem") }
+ let(:app_sha_path) { app_path.join("checksums", "#{app_name}-#{app_version}.gem.sha512") }
+ let(:app_gemspec_content) { File.read(app_gemspec_path) }
+
+ before(:each) do
+ content = app_gemspec_content.gsub("TODO: ", "")
+ content.sub!(/homepage\s+= ".*"/, 'homepage = ""')
+ content.gsub!(/spec\.metadata.+\n/, "")
+ File.open(app_gemspec_path, "w") {|file| file << content }
+ end
+
+ it "uses a shell UI for output" do
+ expect(Bundler.ui).to be_a(Bundler::UI::Shell)
+ end
+
+ describe "#install" do
+ let!(:rake_application) { Rake.application }
+
+ before(:each) do
+ Rake.application = Rake::Application.new
+ end
+
+ after(:each) do
+ Rake.application = rake_application
+ end
+
+ context "defines Rake tasks" do
+ let(:task_names) do
+ %w[build install release release:guard_clean
+ release:source_control_push release:rubygem_push]
+ end
+
+ context "before installation" do
+ it "raises an error with appropriate message" do
+ task_names.each do |name|
+ skip "Rake::FileTask '#{name}' exists" if File.exist?(name)
+ expect { Rake.application[name] }.
+ to raise_error(/^Don't know how to build task '#{name}'/)
+ end
+ end
+ end
+
+ context "after installation" do
+ before do
+ subject.install
+ end
+
+ it "adds Rake tasks successfully" do
+ task_names.each do |name|
+ expect { Rake.application[name] }.not_to raise_error
+ expect(Rake.application[name]).to be_instance_of Rake::Task
+ end
+ end
+
+ it "provides a way to access the gemspec object" do
+ expect(subject.gemspec.name).to eq(app_name)
+ end
+ end
+ end
+ end
+
+ describe "#build_gem" do
+ context "when build failed" do
+ it "raises an error with appropriate message" do
+ # break the gemspec by adding back the TODOs
+ File.open(app_gemspec_path, "w") {|file| file << app_gemspec_content }
+ expect { subject.build_gem }.to raise_error(/TODO/)
+ end
+ end
+
+ context "when build was successful" do
+ it "creates .gem file" do
+ mock_build_message app_name, app_version
+ subject.build_gem
+ expect(app_gem_path).to exist
+ end
+ end
+
+ context "when building in the current working directory" do
+ it "creates .gem file" do
+ mock_build_message app_name, app_version
+ Dir.chdir app_path do
+ Bundler::GemHelper.new.build_gem
+ end
+ expect(app_gem_path).to exist
+ end
+ end
+
+ context "when building in a location relative to the current working directory" do
+ it "creates .gem file" do
+ mock_build_message app_name, app_version
+ Dir.chdir File.dirname(app_path) do
+ Bundler::GemHelper.new(File.basename(app_path)).build_gem
+ end
+ expect(app_gem_path).to exist
+ end
+ end
+ end
+
+ describe "#build_checksum" do
+ it "calculates SHA512 of the content" do
+ FileUtils.mkdir_p(app_gem_dir)
+ File.write(app_gem_path, "")
+ mock_checksum_message app_name, app_version
+ subject.build_checksum(app_gem_path)
+ expect(File.read(app_sha_path).chomp).to eql(Digest::SHA512.hexdigest(""))
+ end
+
+ context "when build was successful" do
+ it "creates .sha512 file" do
+ mock_build_message app_name, app_version
+ mock_checksum_message app_name, app_version
+ subject.build_checksum
+ expect(app_sha_path).to exist
+ expect(File.read(app_sha_path).chomp).to eql(sha512_hexdigest(app_gem_path))
+ end
+ end
+ context "when building in the current working directory" do
+ it "creates a .sha512 file" do
+ mock_build_message app_name, app_version
+ mock_checksum_message app_name, app_version
+ Dir.chdir app_path do
+ Bundler::GemHelper.new.build_checksum
+ end
+ expect(app_sha_path).to exist
+ expect(File.read(app_sha_path).chomp).to eql(sha512_hexdigest(app_gem_path))
+ end
+ end
+ context "when building in a location relative to the current working directory" do
+ it "creates a .sha512 file" do
+ mock_build_message app_name, app_version
+ mock_checksum_message app_name, app_version
+ Dir.chdir File.dirname(app_path) do
+ Bundler::GemHelper.new(File.basename(app_path)).build_checksum
+ end
+ expect(app_sha_path).to exist
+ expect(File.read(app_sha_path).chomp).to eql(sha512_hexdigest(app_gem_path))
+ end
+ end
+ end
+
+ describe "#install_gem" do
+ context "when installation was successful" do
+ it "gem is installed" do
+ mock_build_message app_name, app_version
+ mock_confirm_message "#{app_name} (#{app_version}) installed."
+ subject.install_gem(nil, :local)
+ expect(app_gem_path).to exist
+ installed_gems_list
+ expect(out).to include("#{app_name} (#{app_version})")
+ end
+ end
+
+ context "when installation fails" do
+ it "raises an error with appropriate message" do
+ # create empty gem file in order to simulate install failure
+ allow(subject).to receive(:build_gem) do
+ FileUtils.mkdir_p(app_gem_dir)
+ FileUtils.touch app_gem_path
+ app_gem_path
+ end
+ expect { subject.install_gem }.to raise_error(/Running `#{gem_bin} install #{app_gem_path}` failed/)
+ end
+ end
+ end
+
+ describe "rake release" do
+ let!(:rake_application) { Rake.application }
+
+ before(:each) do
+ Rake.application = Rake::Application.new
+ subject.install
+ end
+
+ after(:each) do
+ Rake.application = rake_application
+ end
+
+ before do
+ git("init", app_path)
+ git("config user.email \"you@example.com\"", app_path)
+ git("config user.name \"name\"", app_path)
+ git("config commit.gpgsign false", app_path)
+ git("config push.default simple", app_path)
+
+ # silence messages
+ allow(Bundler.ui).to receive(:confirm)
+ allow(Bundler.ui).to receive(:error)
+ end
+
+ context "fails" do
+ it "when there are unstaged files" do
+ expect { Rake.application["release"].invoke }.
+ to raise_error("There are files that need to be committed first.")
+ end
+
+ it "when there are uncommitted files" do
+ git("add .", app_path)
+ expect { Rake.application["release"].invoke }.
+ to raise_error("There are files that need to be committed first.")
+ end
+
+ it "when there is no git remote" do
+ git("commit -a -m \"initial commit\"", app_path)
+ expect { Rake.application["release"].invoke }.to raise_error(RuntimeError)
+ end
+ end
+
+ context "succeeds" do
+ let(:repo) { build_git("foo", bare: true) }
+
+ before do
+ git("remote add origin #{repo.path}", app_path)
+ git('commit -a -m "initial commit"', app_path)
+ end
+
+ context "on releasing" do
+ before do
+ mock_build_message app_name, app_version
+ mock_confirm_message "Tagged v#{app_version}."
+ mock_confirm_message "Pushed git commits and release tag."
+
+ git("push -u origin main", app_path)
+ end
+
+ it "calls rubygem_push with proper arguments" do
+ expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s)
+
+ Rake.application["release"].invoke
+ end
+
+ it "uses Kernel.system" do
+ cmd = gem_bin.shellsplit
+ expect(Kernel).to receive(:system).with(*cmd, "push", app_gem_path.to_s, "--host", "http://example.org").and_return(true)
+
+ Rake.application["release"].invoke
+ end
+
+ it "also works when releasing from an ambiguous reference" do
+ # Create a branch with the same name as the tag
+ git("checkout -b v#{app_version}", app_path)
+ git("push -u origin v#{app_version}", app_path)
+
+ expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s)
+
+ Rake.application["release"].invoke
+ end
+
+ it "also works with releasing from a branch not yet pushed" do
+ git("checkout -b module_function", app_path)
+
+ expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s)
+
+ Rake.application["release"].invoke
+ end
+ end
+
+ context "on releasing with a custom tag prefix" do
+ before do
+ Bundler::GemHelper.tag_prefix = "foo-"
+ mock_build_message app_name, app_version
+ mock_confirm_message "Pushed git commits and release tag."
+
+ git("push -u origin main", app_path)
+ expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s)
+ end
+
+ it "prepends the custom prefix to the tag" do
+ mock_confirm_message "Tagged foo-v#{app_version}."
+
+ Rake.application["release"].invoke
+ end
+ end
+
+ it "even if tag already exists" do
+ mock_build_message app_name, app_version
+ mock_confirm_message "Tag v#{app_version} has already been created."
+ expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s)
+
+ git("tag -a -m \"Version #{app_version}\" v#{app_version}", app_path)
+
+ Rake.application["release"].invoke
+ end
+ end
+ end
+
+ describe "release:rubygem_push" do
+ let!(:rake_application) { Rake.application }
+
+ before(:each) do
+ Rake.application = Rake::Application.new
+ subject.install
+ allow(subject).to receive(:sh_with_input)
+ end
+
+ after(:each) do
+ Rake.application = rake_application
+ end
+
+ before do
+ git("init", app_path)
+ git("config user.email \"you@example.com\"", app_path)
+ git("config user.name \"name\"", app_path)
+ git("config push.gpgsign simple", app_path)
+
+ # silence messages
+ allow(Bundler.ui).to receive(:confirm)
+ allow(Bundler.ui).to receive(:error)
+
+ credentials = double("credentials", "file?" => true)
+ allow(Bundler.user_home).to receive(:join).
+ with(".gem/credentials").and_return(credentials)
+ allow(Bundler.user_home).to receive(:join).and_call_original
+ end
+
+ describe "success messaging" do
+ context "No allowed_push_host set" do
+ before do
+ allow(subject).to receive(:allowed_push_host).and_return(nil)
+ end
+
+ around do |example|
+ orig_host = ENV["RUBYGEMS_HOST"]
+ ENV["RUBYGEMS_HOST"] = rubygems_host_env
+
+ example.run
+
+ ENV["RUBYGEMS_HOST"] = orig_host
+ end
+
+ context "RUBYGEMS_HOST env var is set" do
+ let(:rubygems_host_env) { "https://custom.env.gemhost.com" }
+
+ it "should report successful push to the host from the environment" do
+ mock_confirm_message "Pushed #{app_name} #{app_version} to #{rubygems_host_env}"
+
+ Rake.application["release:rubygem_push"].invoke
+ end
+ end
+
+ context "RUBYGEMS_HOST env var is not set" do
+ let(:rubygems_host_env) { nil }
+
+ it "should report successful push to rubygems.org" do
+ mock_confirm_message "Pushed #{app_name} #{app_version} to rubygems.org"
+
+ Rake.application["release:rubygem_push"].invoke
+ end
+ end
+
+ context "RUBYGEMS_HOST env var is an empty string" do
+ let(:rubygems_host_env) { "" }
+
+ it "should report successful push to rubygems.org" do
+ mock_confirm_message "Pushed #{app_name} #{app_version} to rubygems.org"
+
+ Rake.application["release:rubygem_push"].invoke
+ end
+ end
+ end
+
+ context "allowed_push_host set in gemspec" do
+ before do
+ allow(subject).to receive(:allowed_push_host).and_return("https://my.gemhost.com")
+ end
+
+ it "should report successful push to the allowed gem host" do
+ mock_confirm_message "Pushed #{app_name} #{app_version} to https://my.gemhost.com"
+
+ Rake.application["release:rubygem_push"].invoke
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/gem_version_promoter_spec.rb b/spec/bundler/bundler/gem_version_promoter_spec.rb
new file mode 100644
index 0000000000..0e1b7c9cc8
--- /dev/null
+++ b/spec/bundler/bundler/gem_version_promoter_spec.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::GemVersionPromoter do
+ let(:gvp) { described_class.new }
+
+ # Rightmost (highest array index) in result is most preferred.
+ # Leftmost (lowest array index) in result is least preferred.
+ # `build_candidates` has all versions of gem in index.
+ # `build_spec` is the version currently in the .lock file.
+ #
+ # In default (not strict) mode, all versions in the index will
+ # be returned, allowing Bundler the best chance to resolve all
+ # dependencies, but sometimes resulting in upgrades that some
+ # would not consider conservative.
+
+ describe "#sort_versions" do
+ def build_candidates(versions)
+ versions.map do |v|
+ Bundler::Resolver::Candidate.new(v)
+ end
+ end
+
+ def build_package(name, version, unlock)
+ Bundler::Resolver::Package.new(name, [], locked_specs: Bundler::SpecSet.new(build_spec(name, version)), unlock: unlock)
+ end
+
+ def sorted_versions(candidates:, current:, unlock: true)
+ gvp.sort_versions(
+ build_package("foo", current, unlock),
+ build_candidates(candidates)
+ ).flatten.map(&:version).map(&:to_s)
+ end
+
+ it "numerically sorts versions" do
+ versions = sorted_versions(candidates: %w[1.7.7 1.7.8 1.7.9 1.7.15 1.8.0], current: "1.7.8")
+ expect(versions).to eq %w[1.8.0 1.7.15 1.7.9 1.7.8 1.7.7]
+ end
+
+ context "with no options" do
+ it "defaults to level=:major, strict=false, pre=false" do
+ versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
+ expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0]
+ end
+ end
+
+ context "when strict" do
+ before { gvp.strict = true }
+
+ context "when level is major" do
+ before { gvp.level = :major }
+
+ it "keeps downgrades" do
+ versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
+ expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0]
+ end
+ end
+
+ context "when level is minor" do
+ before { gvp.level = :minor }
+
+ it "sorts highest minor within same major in first position" do
+ versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
+ expect(versions).to eq %w[0.9.0 0.3.1 0.3.0 1.0.0 2.1.0 2.0.1 0.2.0]
+ end
+ end
+
+ context "when level is patch" do
+ before { gvp.level = :patch }
+
+ it "sorts highest patch within same minor in first position" do
+ versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
+ expect(versions).to eq %w[0.3.1 0.3.0 0.9.0 1.0.0 2.0.1 2.1.0 0.2.0]
+ end
+ end
+ end
+
+ context "when not strict" do
+ before { gvp.strict = false }
+
+ context "when level is major" do
+ before { gvp.level = :major }
+
+ it "orders by version" do
+ versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
+ expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0]
+ end
+ end
+
+ context "when level is minor" do
+ before { gvp.level = :minor }
+
+ it "favors minor upgrades, then patch upgrades, then major upgrades, then downgrades" do
+ versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
+ expect(versions).to eq %w[0.9.0 0.3.1 0.3.0 1.0.0 2.1.0 2.0.1 0.2.0]
+ end
+ end
+
+ context "when level is patch" do
+ before { gvp.level = :patch }
+
+ it "favors patch upgrades, then minor upgrades, then major upgrades, then downgrades" do
+ versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
+ expect(versions).to eq %w[0.3.1 0.3.0 0.9.0 1.0.0 2.0.1 2.1.0 0.2.0]
+ end
+ end
+ end
+
+ context "when pre" do
+ before { gvp.pre = true }
+
+ it "sorts regardless of prerelease status" do
+ versions = sorted_versions(candidates: %w[1.7.7.pre 1.8.0 1.8.1.pre 1.8.1 2.0.0.pre 2.0.0], current: "1.8.0")
+ expect(versions).to eq %w[2.0.0 2.0.0.pre 1.8.1 1.8.1.pre 1.8.0 1.7.7.pre]
+ end
+ end
+
+ context "when not pre" do
+ before { gvp.pre = false }
+
+ it "deprioritizes prerelease gems" do
+ versions = sorted_versions(candidates: %w[1.7.7.pre 1.8.0 1.8.1.pre 1.8.1 2.0.0.pre 2.0.0], current: "1.8.0")
+ expect(versions).to eq %w[2.0.0 1.8.1 1.8.0 2.0.0.pre 1.8.1.pre 1.7.7.pre]
+ end
+ end
+
+ context "when locking and not major" do
+ before { gvp.level = :minor }
+
+ it "keeps the current version first" do
+ versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.1.0 2.0.1], current: "0.3.0", unlock: [])
+ expect(versions.first).to eq("0.3.0")
+ end
+ end
+ end
+
+ describe "#level=" do
+ subject { described_class.new }
+
+ it "should raise if not major, minor or patch is passed" do
+ expect { subject.level = :minjor }.to raise_error ArgumentError
+ end
+
+ it "should raise if invalid classes passed" do
+ [123, nil].each do |value|
+ expect { subject.level = value }.to raise_error ArgumentError
+ end
+ end
+
+ it "should accept major, minor patch symbols" do
+ [:major, :minor, :patch].each do |value|
+ subject.level = value
+ expect(subject.level).to eq value
+ end
+ end
+
+ it "should accept major, minor patch strings" do
+ %w[major minor patch].each do |value|
+ subject.level = value
+ expect(subject.level).to eq value.to_sym
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/index_spec.rb b/spec/bundler/bundler/index_spec.rb
new file mode 100644
index 0000000000..0f3f6e4944
--- /dev/null
+++ b/spec/bundler/bundler/index_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Index do
+ let(:specs) { [] }
+ subject { described_class.build {|i| i.use(specs) } }
+
+ context "specs with a nil platform" do
+ let(:spec) do
+ Gem::Specification.new do |s|
+ s.name = "json"
+ s.version = "1.8.3"
+ allow(s).to receive(:platform).and_return(nil)
+ end
+ end
+ let(:specs) { [spec] }
+
+ describe "#search_by_spec" do
+ it "finds the spec when a nil platform is specified" do
+ expect(subject.search(spec)).to eq([spec])
+ end
+
+ it "finds the spec when a ruby platform is specified" do
+ query = spec.dup.tap {|s| s.platform = "ruby" }
+ expect(subject.search(query)).to eq([spec])
+ end
+ end
+ end
+
+ context "with specs that include development dependencies" do
+ let(:specs) { [*build_spec("a", "1.0.0") {|s| s.development("b", "~> 1.0") }] }
+
+ it "does not include b in #dependency_names" do
+ expect(subject.dependency_names).not_to include("b")
+ end
+ end
+end
diff --git a/spec/bundler/bundler/installer/gem_installer_spec.rb b/spec/bundler/bundler/installer/gem_installer_spec.rb
new file mode 100644
index 0000000000..dbd4e1d2c8
--- /dev/null
+++ b/spec/bundler/bundler/installer/gem_installer_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require "bundler/installer/gem_installer"
+
+RSpec.describe Bundler::GemInstaller do
+ let(:definition) { instance_double("Definition", locked_gems: nil) }
+ let(:installer) { instance_double("Installer", definition: definition) }
+ let(:spec_source) { instance_double("SpecSource") }
+ let(:spec) { instance_double("Specification", name: "dummy", version: "0.0.1", loaded_from: "dummy", source: spec_source) }
+ let(:base_options) { { force: false, local: false, previous_spec: nil } }
+
+ subject { described_class.new(spec, installer) }
+
+ context "spec_settings is nil" do
+ it "invokes install method with empty build_args" do
+ allow(spec_source).to receive(:install).with(
+ spec,
+ base_options.merge(build_args: [])
+ )
+ subject.install_from_spec
+ end
+ end
+
+ context "spec_settings is build option" do
+ it "invokes install method with build_args" do
+ allow(Bundler.settings).to receive(:[])
+ allow(Bundler.settings).to receive(:[]).with("build.dummy").and_return("--with-dummy-config=dummy")
+ expect(spec_source).to receive(:install).with(
+ spec,
+ base_options.merge(build_args: ["--with-dummy-config=dummy"])
+ )
+ subject.install_from_spec
+ end
+ end
+
+ context "spec_settings is build option with spaces" do
+ it "invokes install method with build_args" do
+ allow(Bundler.settings).to receive(:[])
+ allow(Bundler.settings).to receive(:[]).with("build.dummy").and_return("--with-dummy-config=dummy --with-another-dummy-config")
+ expect(spec_source).to receive(:install).with(
+ spec,
+ base_options.merge(build_args: ["--with-dummy-config=dummy", "--with-another-dummy-config"])
+ )
+ subject.install_from_spec
+ end
+ end
+end
diff --git a/spec/bundler/bundler/installer/parallel_installer_spec.rb b/spec/bundler/bundler/installer/parallel_installer_spec.rb
new file mode 100644
index 0000000000..49bcb5310b
--- /dev/null
+++ b/spec/bundler/bundler/installer/parallel_installer_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require "bundler/installer/parallel_installer"
+require "bundler/rubygems_gem_installer"
+require "rubygems/remote_fetcher"
+require "bundler"
+
+RSpec.describe Bundler::ParallelInstaller do
+ describe "priority queue" do
+ before do
+ require "support/artifice/compact_index"
+
+ @previous_client = Gem::Request::ConnectionPools.client
+ Gem::Request::ConnectionPools.client = Gem::Net::HTTP
+ Gem::RemoteFetcher.fetcher.close_all
+
+ build_repo2 do
+ build_gem "gem_with_extension", &:add_c_extension
+ build_gem "gem_without_extension"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ gem "gem_with_extension"
+ gem "gem_without_extension"
+ G
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ gem_with_extension (1.0)
+ gem_without_extension (1.0)
+
+ DEPENDENCIES
+ gem_with_extension
+ gem_without_extension
+ L
+
+ @old_ui = Bundler.ui
+ Bundler.ui = Bundler::UI::Silent.new
+ end
+
+ after do
+ Bundler.ui = @old_ui
+ Gem::Request::ConnectionPools.client = @previous_client
+ Artifice.deactivate
+ end
+
+ let(:definition) do
+ allow(Bundler).to receive(:root) { bundled_app }
+
+ definition = Bundler::Definition.build(bundled_app.join("Gemfile"), bundled_app.join("Gemfile.lock"), false)
+ definition.tap(&:setup_domain!)
+ end
+ let(:installer) { Bundler::Installer.new(bundled_app, definition) }
+
+ it "queues native extensions in priority" do
+ parallel_installer = Bundler::ParallelInstaller.new(installer, definition.specs, 2, false, true)
+ worker_pool = parallel_installer.send(:worker_pool)
+ expected = 6 # Enqueue to download bundler and the 2 gems. Enqueue to install Bundler and the 2 gems.
+
+ expect(worker_pool).to receive(:enq).exactly(expected).times.and_wrap_original do |original_enq, spec, opts|
+ unless opts.nil? # Enqueued for download, no priority
+ if spec.name == "gem_with_extension"
+ expect(opts).to eq({ priority: true })
+ else
+ expect(opts).to eq({ priority: false })
+ end
+ end
+
+ opts ||= {}
+ original_enq.call(spec, **opts)
+ end
+
+ parallel_installer.call
+ end
+ end
+end
diff --git a/spec/bundler/bundler/installer/spec_installation_spec.rb b/spec/bundler/bundler/installer/spec_installation_spec.rb
new file mode 100644
index 0000000000..57868766d9
--- /dev/null
+++ b/spec/bundler/bundler/installer/spec_installation_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require "bundler/installer/parallel_installer"
+
+RSpec.describe Bundler::ParallelInstaller::SpecInstallation do
+ def build_spec(name, extensions: [])
+ spec = Object.new
+ spec.define_singleton_method(:name) { name }
+ spec.define_singleton_method(:full_name) { "#{name}-1.0" }
+ spec.define_singleton_method(:extensions) { extensions }
+ spec.define_singleton_method(:dependencies) { [] }
+ spec
+ end
+
+ let!(:dep) { build_spec("I like tests") }
+
+ describe "#ready_to_enqueue?" do
+ context "when in enqueued state" do
+ it "is falsey" do
+ spec = described_class.new(dep)
+ spec.state = :enqueued
+ expect(spec.ready_to_enqueue?).to be_falsey
+ end
+ end
+
+ context "when in installed state" do
+ it "returns falsey" do
+ spec = described_class.new(dep)
+ spec.state = :installed
+ expect(spec.ready_to_enqueue?).to be_falsey
+ end
+ end
+
+ it "returns truthy" do
+ spec = described_class.new(dep)
+ expect(spec.ready_to_enqueue?).to be_truthy
+ end
+ end
+
+ describe "#dependencies_installed?" do
+ it "returns true when all dependencies are installed" do
+ alpha = described_class.new(build_spec("alpha"))
+ alpha.dependencies = []
+
+ beta = described_class.new(build_spec("beta"))
+ beta.dependencies = [alpha]
+
+ gamma = described_class.new(build_spec("gamma"))
+ gamma.dependencies = [beta]
+
+ expect(gamma.dependencies_installed?({})).to be_falsey
+ expect(gamma.dependencies_installed?({ "beta" => true })).to be_falsey
+ expect(gamma.dependencies_installed?({ "alpha" => true, "beta" => true })).to be_truthy
+ end
+ end
+
+ describe "#ready_to_install?" do
+ context "when spec has no extensions" do
+ it "returns true regardless of dependencies" do
+ beta = described_class.new(build_spec("beta"))
+ beta.dependencies = []
+
+ spec = described_class.new(dep)
+ spec.state = :downloaded
+ spec.dependencies = [beta]
+
+ expect(spec.ready_to_install?({})).to be_truthy
+ end
+ end
+
+ context "when spec has extensions" do
+ it "returns true when all dependencies are installed" do
+ alpha = described_class.new(build_spec("alpha"))
+ alpha.dependencies = []
+
+ beta = described_class.new(build_spec("beta"))
+ beta.dependencies = [alpha]
+
+ gamma = described_class.new(build_spec("gamma", extensions: ["ext/Rakefile"]))
+ gamma.state = :downloaded
+ gamma.dependencies = [beta]
+
+ expect(gamma.ready_to_install?({})).to be_falsey
+ expect(gamma.ready_to_install?({ "beta" => true })).to be_falsey
+ expect(gamma.ready_to_install?({ "alpha" => true, "beta" => true })).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/lockfile_parser_spec.rb b/spec/bundler/bundler/lockfile_parser_spec.rb
new file mode 100644
index 0000000000..7364ab98e5
--- /dev/null
+++ b/spec/bundler/bundler/lockfile_parser_spec.rb
@@ -0,0 +1,303 @@
+# frozen_string_literal: true
+
+require "bundler/lockfile_parser"
+
+RSpec.describe Bundler::LockfileParser do
+ let(:lockfile_contents) { <<~L }
+ GIT
+ remote: https://github.com/alloy/peiji-san.git
+ revision: eca485d8dc95f12aaec1a434b49d295c7e91844b
+ specs:
+ peiji-san (1.2.0)
+
+ GEM
+ remote: https://rubygems.org/
+ specs:
+ rake (10.3.2)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ peiji-san!
+ rake
+
+ CHECKSUMS
+ rake (10.3.2) sha256=814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8
+
+ RUBY VERSION
+ ruby 2.1.3p242
+
+ BUNDLED WITH
+ 1.12.0.rc.2
+ L
+
+ describe ".sections_in_lockfile" do
+ it "returns the attributes" do
+ attributes = described_class.sections_in_lockfile(lockfile_contents)
+ expect(attributes).to contain_exactly(
+ "BUNDLED WITH", "CHECKSUMS", "DEPENDENCIES", "GEM", "GIT", "PLATFORMS", "RUBY VERSION"
+ )
+ end
+ end
+
+ describe ".unknown_sections_in_lockfile" do
+ let(:lockfile_contents) { <<~L }
+ UNKNOWN ATTR
+
+ UNKNOWN ATTR 2
+ random contents
+ L
+
+ it "returns the unknown attributes" do
+ attributes = described_class.unknown_sections_in_lockfile(lockfile_contents)
+ expect(attributes).to contain_exactly("UNKNOWN ATTR", "UNKNOWN ATTR 2")
+ end
+ end
+
+ describe ".sections_to_ignore" do
+ subject { described_class.sections_to_ignore(base_version) }
+
+ context "with a nil base version" do
+ let(:base_version) { nil }
+
+ it "returns the same as > 1.0" do
+ expect(subject).to contain_exactly(
+ described_class::BUNDLED, described_class::CHECKSUMS, described_class::RUBY, described_class::PLUGIN
+ )
+ end
+ end
+
+ context "with a prerelease base version" do
+ let(:base_version) { Gem::Version.create("1.11.0.rc.1") }
+
+ it "returns the same as for the release version" do
+ expect(subject).to contain_exactly(
+ described_class::CHECKSUMS, described_class::RUBY, described_class::PLUGIN
+ )
+ end
+ end
+
+ context "with a current version" do
+ let(:base_version) { Gem::Version.create(Bundler::VERSION) }
+
+ it "returns an empty array" do
+ expect(subject).to eq([])
+ end
+ end
+
+ context "with a future version" do
+ let(:base_version) { Gem::Version.create("5.5.5") }
+
+ it "returns an empty array" do
+ expect(subject).to eq([])
+ end
+ end
+ end
+
+ describe "#initialize" do
+ before { allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app("gems.rb")) }
+ subject { described_class.new(lockfile_contents) }
+
+ let(:sources) do
+ [Bundler::Source::Git.new("uri" => "https://github.com/alloy/peiji-san.git", "revision" => "eca485d8dc95f12aaec1a434b49d295c7e91844b"),
+ Bundler::Source::Rubygems.new("remotes" => ["https://rubygems.org"])]
+ end
+ let(:dependencies) do
+ {
+ "peiji-san" => Bundler::Dependency.new("peiji-san", ">= 0"),
+ "rake" => Bundler::Dependency.new("rake", ">= 0"),
+ }
+ end
+ let(:specs) do
+ [
+ Bundler::LazySpecification.new("peiji-san", v("1.2.0"), Gem::Platform::RUBY),
+ Bundler::LazySpecification.new("rake", v("10.3.2"), Gem::Platform::RUBY),
+ ]
+ end
+ let(:platforms) { [Gem::Platform::RUBY] }
+ let(:bundler_version) { Gem::Version.new("1.12.0.rc.2") }
+ let(:ruby_version) { "ruby 2.1.3p242" }
+ let(:lockfile_path) { Bundler.default_lockfile.relative_path_from(Dir.pwd) }
+ let(:rake_sha256_checksum) do
+ Bundler::Checksum.from_lock(
+ "sha256=814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8",
+ "#{lockfile_path}:20:17"
+ )
+ end
+ let(:rake_checksums) { [rake_sha256_checksum] }
+
+ shared_examples_for "parsing" do
+ it "parses correctly" do
+ expect(subject.valid?).to be(true)
+ expect(subject.sources).to eq sources
+ expect(subject.dependencies).to eq dependencies
+ expect(subject.specs).to eq specs
+ expect(Hash[subject.specs.map {|s| [s, s.dependencies] }]).to eq Hash[subject.specs.map {|s| [s, s.dependencies] }]
+ expect(subject.platforms).to eq platforms
+ expect(subject.bundler_version).to eq bundler_version
+ expect(subject.ruby_version).to eq ruby_version
+ rake_spec = specs.last
+ checksums = subject.sources.last.checksum_store.to_lock(specs.last)
+ expect(checksums).to eq("#{rake_spec.lock_name} #{rake_checksums.map(&:to_lock).sort.join(",")}")
+ end
+ end
+
+ include_examples "parsing"
+
+ context "when an extra section is at the end" do
+ let(:lockfile_contents) { super() + "\n\nFOO BAR\n baz\n baa\n qux\n" }
+ include_examples "parsing"
+ end
+
+ context "when an extra section is at the start" do
+ let(:lockfile_contents) { "FOO BAR\n baz\n baa\n qux\n\n" + super() }
+ include_examples "parsing"
+ end
+
+ context "when an extra section is in the middle" do
+ let(:lockfile_contents) { super().split(/(?=GEM)/).insert(1, "FOO BAR\n baz\n baa\n qux\n\n").join }
+ include_examples "parsing"
+ end
+
+ context "when a dependency has options" do
+ let(:lockfile_contents) { super().sub("peiji-san!", "peiji-san!\n foo: bar") }
+ include_examples "parsing"
+ end
+
+ context "when the checksum is urlsafe base64 encoded" do
+ let(:lockfile_contents) do
+ super().sub(
+ "sha256=814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8",
+ "sha256=gUgow08TFdfnt-gpUYRXfMTpabrWFWrAadAtY_WNgug="
+ )
+ end
+ include_examples "parsing"
+ end
+
+ context "when the checksum is of an unknown algorithm" do
+ let(:rake_sha512_checksum) do
+ Bundler::Checksum.from_lock(
+ "sha512=pVDn9GLmcFkz8vj1ueiVxj5uGKkAyaqYjEX8zG6L5O4BeVg3wANaKbQdpj/B82Nd/MHVszy6polHcyotUdwilQ==",
+ "#{lockfile_path}:20:17"
+ )
+ end
+ let(:lockfile_contents) do
+ super().sub(
+ "sha256=",
+ "sha512=pVDn9GLmcFkz8vj1ueiVxj5uGKkAyaqYjEX8zG6L5O4BeVg3wANaKbQdpj/B82Nd/MHVszy6polHcyotUdwilQ==,sha256="
+ )
+ end
+ let(:rake_checksums) { [rake_sha256_checksum, rake_sha512_checksum] }
+ include_examples "parsing"
+ end
+
+ context "when the content does not contain any recognized lockfile sections" do
+ let(:lockfile_contents) { "hello world\nlorem ipsum\n" }
+
+ it "does not raise, is not valid, and deprecates" do
+ expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with(
+ /does not appear to be a valid lockfile.*future version of Bundler/m
+ )
+ parser = described_class.new(lockfile_contents)
+ expect(parser.valid?).to be(false)
+ expect(parser.specs).to eq([])
+ expect(parser.dependencies).to eq({})
+ end
+
+ it "does not raise when strict: true, and still deprecates" do
+ expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with(
+ /does not appear to be a valid lockfile.*future version of Bundler/m
+ )
+ parser = described_class.new(lockfile_contents, strict: true)
+ expect(parser.valid?).to be(false)
+ expect(parser.specs).to eq([])
+ expect(parser.dependencies).to eq({})
+ end
+ end
+
+ context "when the content looks like a Gemfile DSL" do
+ let(:lockfile_contents) { <<~G }
+ source "https://rubygems.org"
+ gem "rake"
+ G
+
+ it "does not raise, is not valid, and deprecates" do
+ expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with(
+ /does not appear to be a valid lockfile.*future version of Bundler/m
+ )
+ parser = described_class.new(lockfile_contents)
+ expect(parser.valid?).to be(false)
+ expect(parser.specs).to eq([])
+ expect(parser.dependencies).to eq({})
+ end
+
+ it "does not raise when strict: true, and still deprecates" do
+ expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with(
+ /does not appear to be a valid lockfile.*future version of Bundler/m
+ )
+ parser = described_class.new(lockfile_contents, strict: true)
+ expect(parser.valid?).to be(false)
+ expect(parser.specs).to eq([])
+ expect(parser.dependencies).to eq({})
+ end
+ end
+
+ context "when the content is empty" do
+ let(:lockfile_contents) { "" }
+
+ it "does not raise and is valid" do
+ expect { subject }.not_to raise_error
+ expect(subject.valid?).to be(true)
+ end
+ end
+
+ context "when lockfile_path is given" do
+ it "uses the provided path in error messages instead of looking up Bundler.default_lockfile" do
+ expect(Bundler::SharedHelpers).not_to receive(:relative_lockfile_path)
+ parser = described_class.new(lockfile_contents, lockfile_path: "custom/path.lock")
+ expect(parser.valid?).to be(true)
+ rake_spec = parser.specs.last
+ checksums = parser.sources.last.checksum_store.to_lock(rake_spec)
+ expected_checksum = Bundler::Checksum.from_lock(
+ "sha256=814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8",
+ "custom/path.lock:20:17"
+ )
+ expect(checksums).to eq("#{rake_spec.lock_name} #{expected_checksum.to_lock}")
+ end
+
+ it "raises with the provided path when the lockfile contains merge conflicts" do
+ expect do
+ described_class.new("<<<<<<<\n", lockfile_path: "custom/path.lock")
+ end.to raise_error(Bundler::LockfileError, %r{custom/path\.lock contains merge conflicts})
+ end
+ end
+
+ context "when CHECKSUMS has duplicate checksums in the lockfile that don't match" do
+ let(:bad_checksum) { "sha256=c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11" }
+ let(:lockfile_contents) { super().split(/(?<=CHECKSUMS\n)/m).insert(1, " rake (10.3.2) #{bad_checksum}\n").join }
+
+ it "raises a security error" do
+ expect { subject }.to raise_error(Bundler::SecurityError) do |e|
+ expect(e.message).to match <<~MESSAGE
+ Bundler found mismatched checksums. This is a potential security risk.
+ rake (10.3.2) #{bad_checksum}
+ from the lockfile CHECKSUMS at #{lockfile_path}:20:17
+ rake (10.3.2) #{rake_sha256_checksum.to_lock}
+ from the lockfile CHECKSUMS at #{lockfile_path}:21:17
+
+ To resolve this issue you can either:
+ 1. remove the matching checksum in #{lockfile_path}:21:17
+ 2. run `bundle install`
+ or if you are sure that the new checksum from the lockfile CHECKSUMS at #{lockfile_path}:21:17 is correct:
+ 1. remove the matching checksum in #{lockfile_path}:20:17
+ 2. run `bundle install`
+
+ To ignore checksum security warnings, disable checksum validation with
+ `bundle config set --local disable_checksum_validation true`
+ MESSAGE
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/mirror_spec.rb b/spec/bundler/bundler/mirror_spec.rb
new file mode 100644
index 0000000000..ba1c6ed413
--- /dev/null
+++ b/spec/bundler/bundler/mirror_spec.rb
@@ -0,0 +1,331 @@
+# frozen_string_literal: true
+
+require "bundler/mirror"
+
+RSpec.describe Bundler::Settings::Mirror do
+ let(:mirror) { Bundler::Settings::Mirror.new }
+
+ it "returns zero when fallback_timeout is not set" do
+ expect(mirror.fallback_timeout).to eq(0)
+ end
+
+ it "takes a number as a fallback_timeout" do
+ mirror.fallback_timeout = 1
+ expect(mirror.fallback_timeout).to eq(1)
+ end
+
+ it "takes truthy as a default fallback timeout" do
+ mirror.fallback_timeout = true
+ expect(mirror.fallback_timeout).to eq(0.1)
+ end
+
+ it "takes falsey as a zero fallback timeout" do
+ mirror.fallback_timeout = false
+ expect(mirror.fallback_timeout).to eq(0)
+ end
+
+ it "takes a string with 'true' as a default fallback timeout" do
+ mirror.fallback_timeout = "true"
+ expect(mirror.fallback_timeout).to eq(0.1)
+ end
+
+ it "takes a string with 'false' as a zero fallback timeout" do
+ mirror.fallback_timeout = "false"
+ expect(mirror.fallback_timeout).to eq(0)
+ end
+
+ it "takes a string for the uri but returns an uri object" do
+ mirror.uri = "http://localhost:9292"
+ expect(mirror.uri).to eq(Gem::URI("http://localhost:9292"))
+ end
+
+ it "takes an uri object for the uri" do
+ mirror.uri = Gem::URI("http://localhost:9293")
+ expect(mirror.uri).to eq(Gem::URI("http://localhost:9293"))
+ end
+
+ context "without a uri" do
+ it "invalidates the mirror" do
+ mirror.validate!
+ expect(mirror.valid?).to be_falsey
+ end
+ end
+
+ context "with an uri" do
+ before { mirror.uri = "http://localhost:9292" }
+
+ context "without a fallback timeout" do
+ it "is not valid by default" do
+ expect(mirror.valid?).to be_falsey
+ end
+
+ context "when probed" do
+ let(:probe) { double }
+
+ context "with a replying mirror" do
+ before do
+ allow(probe).to receive(:replies?).and_return(true)
+ mirror.validate!(probe)
+ end
+
+ it "is valid" do
+ expect(mirror.valid?).to be_truthy
+ end
+ end
+
+ context "with a non replying mirror" do
+ before do
+ allow(probe).to receive(:replies?).and_return(false)
+ mirror.validate!(probe)
+ end
+
+ it "is still valid" do
+ expect(mirror.valid?).to be_truthy
+ end
+ end
+ end
+ end
+
+ context "with a fallback timeout" do
+ before { mirror.fallback_timeout = 1 }
+
+ it "is not valid by default" do
+ expect(mirror.valid?).to be_falsey
+ end
+
+ context "when probed" do
+ let(:probe) { double }
+
+ context "with a replying mirror" do
+ before do
+ allow(probe).to receive(:replies?).and_return(true)
+ mirror.validate!(probe)
+ end
+
+ it "is valid" do
+ expect(mirror.valid?).to be_truthy
+ end
+
+ it "is validated only once" do
+ allow(probe).to receive(:replies?).and_raise("Only once!")
+ mirror.validate!(probe)
+ expect(mirror.valid?).to be_truthy
+ end
+ end
+
+ context "with a non replying mirror" do
+ before do
+ allow(probe).to receive(:replies?).and_return(false)
+ mirror.validate!(probe)
+ end
+
+ it "is not valid" do
+ expect(mirror.valid?).to be_falsey
+ end
+
+ it "is validated only once" do
+ allow(probe).to receive(:replies?).and_raise("Only once!")
+ mirror.validate!(probe)
+ expect(mirror.valid?).to be_falsey
+ end
+ end
+ end
+ end
+
+ describe "#==" do
+ it "returns true if uri and fallback timeout are the same" do
+ uri = "https://ruby.taobao.org"
+ mirror = Bundler::Settings::Mirror.new(uri, 1)
+ another_mirror = Bundler::Settings::Mirror.new(uri, 1)
+
+ expect(mirror == another_mirror).to be true
+ end
+ end
+ end
+end
+
+RSpec.describe Bundler::Settings::Mirrors do
+ let(:localhost_uri) { Gem::URI("http://localhost:9292") }
+
+ context "with a just created mirror" do
+ let(:mirrors) do
+ probe = double
+ allow(probe).to receive(:replies?).and_return(true)
+ Bundler::Settings::Mirrors.new(probe)
+ end
+
+ it "returns a mirror that contains the source uri for an unknown uri" do
+ mirror = mirrors.for("http://rubygems.org/")
+ expect(mirror).to eq(Bundler::Settings::Mirror.new("http://rubygems.org/"))
+ end
+
+ it "parses a mirror key and returns a mirror for the parsed uri" do
+ mirrors.parse("mirror.http://rubygems.org/", localhost_uri)
+ expect(mirrors.for("http://rubygems.org/").uri).to eq(localhost_uri)
+ end
+
+ it "parses a relative mirror key and returns a mirror for the parsed http uri" do
+ mirrors.parse("mirror.rubygems.org", localhost_uri)
+ expect(mirrors.for("http://rubygems.org/").uri).to eq(localhost_uri)
+ end
+
+ it "parses a relative mirror key and returns a mirror for the parsed https uri" do
+ mirrors.parse("mirror.rubygems.org", localhost_uri)
+ expect(mirrors.for("https://rubygems.org/").uri).to eq(localhost_uri)
+ end
+
+ context "with a uri parsed already" do
+ before { mirrors.parse("mirror.http://rubygems.org/", localhost_uri) }
+
+ it "takes a mirror fallback_timeout and assigns the timeout" do
+ mirrors.parse("mirror.http://rubygems.org.fallback_timeout", "2")
+ expect(mirrors.for("http://rubygems.org/").fallback_timeout).to eq(2)
+ end
+
+ it "parses a 'true' fallback timeout and sets the default timeout" do
+ mirrors.parse("mirror.http://rubygems.org.fallback_timeout", "true")
+ expect(mirrors.for("http://rubygems.org/").fallback_timeout).to eq(0.1)
+ end
+
+ it "parses a 'false' fallback timeout and sets it to zero" do
+ mirrors.parse("mirror.http://rubygems.org.fallback_timeout", "false")
+ expect(mirrors.for("http://rubygems.org/").fallback_timeout).to eq(0)
+ end
+ end
+ end
+
+ context "with a mirror prober that replies on time" do
+ let(:mirrors) do
+ probe = double
+ allow(probe).to receive(:replies?).and_return(true)
+ Bundler::Settings::Mirrors.new(probe)
+ end
+
+ context "with a default fallback_timeout for rubygems.org" do
+ before do
+ mirrors.parse("mirror.http://rubygems.org/", localhost_uri)
+ mirrors.parse("mirror.http://rubygems.org.fallback_timeout", "true")
+ end
+
+ it "returns localhost" do
+ expect(mirrors.for("http://rubygems.org").uri).to eq(localhost_uri)
+ end
+ end
+
+ context "with a mirror for all" do
+ before do
+ mirrors.parse("mirror.all", localhost_uri)
+ end
+
+ context "without a fallback timeout" do
+ it "returns localhost uri for rubygems" do
+ expect(mirrors.for("http://rubygems.org").uri).to eq(localhost_uri)
+ end
+
+ it "returns localhost for any other url" do
+ expect(mirrors.for("http://whatever.com/").uri).to eq(localhost_uri)
+ end
+ end
+ context "with a fallback timeout" do
+ before { mirrors.parse("mirror.all.fallback_timeout", "1") }
+
+ it "returns localhost uri for rubygems" do
+ expect(mirrors.for("http://rubygems.org").uri).to eq(localhost_uri)
+ end
+
+ it "returns localhost for any other url" do
+ expect(mirrors.for("http://whatever.com/").uri).to eq(localhost_uri)
+ end
+ end
+ end
+ end
+
+ context "with a mirror prober that does not reply on time" do
+ let(:mirrors) do
+ probe = double
+ allow(probe).to receive(:replies?).and_return(false)
+ Bundler::Settings::Mirrors.new(probe)
+ end
+
+ context "with a localhost mirror for all" do
+ before { mirrors.parse("mirror.all", localhost_uri) }
+
+ context "without a fallback timeout" do
+ it "returns localhost" do
+ expect(mirrors.for("http://whatever.com").uri).to eq(localhost_uri)
+ end
+ end
+
+ context "with a fallback timeout" do
+ before { mirrors.parse("mirror.all.fallback_timeout", "true") }
+
+ it "returns the source uri, not localhost" do
+ expect(mirrors.for("http://whatever.com").uri).to eq(Gem::URI("http://whatever.com/"))
+ end
+ end
+ end
+
+ context "with localhost as a mirror for rubygems.org" do
+ before { mirrors.parse("mirror.http://rubygems.org/", localhost_uri) }
+
+ context "without a fallback timeout" do
+ it "returns the uri that is not mirrored" do
+ expect(mirrors.for("http://whatever.com").uri).to eq(Gem::URI("http://whatever.com/"))
+ end
+
+ it "returns localhost for rubygems.org" do
+ expect(mirrors.for("http://rubygems.org/").uri).to eq(localhost_uri)
+ end
+ end
+
+ context "with a fallback timeout" do
+ before { mirrors.parse("mirror.http://rubygems.org/.fallback_timeout", "true") }
+
+ it "returns the uri that is not mirrored" do
+ expect(mirrors.for("http://whatever.com").uri).to eq(Gem::URI("http://whatever.com/"))
+ end
+
+ it "returns rubygems.org for rubygems.org" do
+ expect(mirrors.for("http://rubygems.org/").uri).to eq(Gem::URI("http://rubygems.org/"))
+ end
+ end
+ end
+ end
+end
+
+RSpec.describe Bundler::Settings::TCPSocketProbe do
+ let(:probe) { Bundler::Settings::TCPSocketProbe.new }
+
+ context "with a listening TCP Server" do
+ def with_server_and_mirror
+ server = TCPServer.new("0.0.0.0", 0)
+ mirror = Bundler::Settings::Mirror.new("http://0.0.0.0:#{server.addr[1]}", 1)
+ yield server, mirror
+ server.close unless server.closed?
+ end
+
+ it "probes the server correctly" do
+ skip "obscure error" if Gem.win_platform?
+
+ with_server_and_mirror do |server, mirror|
+ expect(server.closed?).to be_falsey
+ expect(probe.replies?(mirror)).to be_truthy
+ end
+ end
+
+ it "probes falsey when the server is down" do
+ with_server_and_mirror do |server, mirror|
+ server.close
+ expect(probe.replies?(mirror)).to be_falsey
+ end
+ end
+ end
+
+ context "with an invalid mirror" do
+ let(:mirror) { Bundler::Settings::Mirror.new("http://127.0.0.127:9292", true) }
+
+ it "fails with a timeout when there is nothing to tcp handshake" do
+ expect(probe.replies?(mirror)).to be_falsey
+ end
+ end
+end
diff --git a/spec/bundler/bundler/override_spec.rb b/spec/bundler/bundler/override_spec.rb
new file mode 100644
index 0000000000..ad8be75520
--- /dev/null
+++ b/spec/bundler/bundler/override_spec.rb
@@ -0,0 +1,175 @@
+# frozen_string_literal: true
+
+RSpec.describe "MatchMetadata override-aware checks" do
+ let(:spec_class) do
+ Class.new do
+ include Bundler::MatchMetadata
+ attr_accessor :name
+ def initialize(name, ruby_req, rubygems_req)
+ @name = name
+ @required_ruby_version = ruby_req
+ @required_rubygems_version = rubygems_req
+ end
+ end
+ end
+
+ it "matches_current_metadata? ignores overrides (strict path)" do
+ spec = spec_class.new("rails", Gem::Requirement.new("< #{Gem.ruby_version}"), Gem::Requirement.default)
+ overrides = [Bundler::Override.new("rails", :required_ruby_version, :ignore_upper)]
+ # Strict method MUST NOT apply overrides; guards SelfManager and other generic callers.
+ expect(spec.matches_current_metadata?).to be(false)
+ expect(spec.matches_current_metadata_with_overrides?(overrides)).to be(true)
+ end
+
+ it "matches_current_ruby_with_overrides? returns the strict result for an empty override list" do
+ spec = spec_class.new("rails", Gem::Requirement.new(">= #{Gem.ruby_version}"), Gem::Requirement.default)
+ expect(spec.matches_current_ruby_with_overrides?([])).to be(true)
+ expect(spec.matches_current_ruby_with_overrides?(nil)).to be(true)
+ end
+
+ it "matches_current_rubygems_with_overrides? honors :all override" do
+ spec = spec_class.new("rails", Gem::Requirement.default, Gem::Requirement.new("< #{Gem.rubygems_version}"))
+ overrides = [Bundler::Override.new(:all, :required_rubygems_version, :ignore_upper)]
+ expect(spec.matches_current_rubygems_with_overrides?(overrides)).to be(true)
+ end
+end
+
+RSpec.describe "LazySpecification override propagation" do
+ let(:overrides) { [Bundler::Override.new("rails", :required_ruby_version, :ignore_upper)] }
+
+ it "carries overrides forward from a source LazySpec via from_spec" do
+ src = Bundler::LazySpecification.new("rails", "8.0", Gem::Platform::RUBY)
+ src.overrides = overrides
+ derived = Bundler::LazySpecification.from_spec(src)
+ expect(derived.overrides).to eq(overrides)
+ end
+
+ it "does not call respond_to? on the source spec, avoiding gemspec lazy load" do
+ # If from_spec used respond_to?(:overrides), a RemoteSpec source would
+ # force-load the backing gemspec. Use a stand-in object whose
+ # respond_to? raises to prove it is never asked.
+ src = Object.new
+ src.define_singleton_method(:name) { "rails" }
+ src.define_singleton_method(:version) { Gem::Version.new("8.0") }
+ src.define_singleton_method(:platform) { Gem::Platform::RUBY }
+ src.define_singleton_method(:source) { nil }
+ src.define_singleton_method(:runtime_dependencies) { [] }
+ src.define_singleton_method(:required_ruby_version) { Gem::Requirement.default }
+ src.define_singleton_method(:required_rubygems_version) { Gem::Requirement.default }
+ src.define_singleton_method(:respond_to?) {|*| raise "from_spec must not call respond_to?" }
+ expect { Bundler::LazySpecification.from_spec(src) }.not_to raise_error
+ end
+end
+
+RSpec.describe Bundler::Override do
+ describe ".find_for" do
+ it "returns the matching override by target and field" do
+ a = described_class.new("rails", :version, ">= 8.0")
+ b = described_class.new("nokogiri", :version, :ignore_upper)
+ expect(described_class.find_for([a, b], "rails", :version)).to be(a)
+ end
+
+ it "returns nil when no override matches the target" do
+ a = described_class.new("rails", :version, ">= 8.0")
+ expect(described_class.find_for([a], "sinatra", :version)).to be_nil
+ end
+
+ it "returns nil when no override matches the field" do
+ a = described_class.new("rails", :version, ">= 8.0")
+ expect(described_class.find_for([a], "rails", :required_ruby_version)).to be_nil
+ end
+
+ it "returns nil for an empty overrides list" do
+ expect(described_class.find_for([], "rails", :version)).to be_nil
+ end
+
+ it "falls back to an :all override on the same field" do
+ a = described_class.new(:all, :required_ruby_version, :ignore_upper)
+ expect(described_class.find_for([a], "rails", :required_ruby_version)).to be(a)
+ end
+
+ it "prefers a per-gem override over a matching :all override" do
+ per_gem = described_class.new("rails", :required_ruby_version, ">= 3.4")
+ all_target = described_class.new(:all, :required_ruby_version, :ignore_upper)
+ expect(described_class.find_for([all_target, per_gem], "rails", :required_ruby_version)).to be(per_gem)
+ end
+
+ it "does not fall back to :all when the field differs" do
+ a = described_class.new(:all, :required_ruby_version, :ignore_upper)
+ expect(described_class.find_for([a], "rails", :required_rubygems_version)).to be_nil
+ end
+ end
+
+ describe "#apply_to" do
+ context "when operation is a version spec string" do
+ it "replaces the existing requirement entirely" do
+ override = described_class.new("rails", :version, ">= 8.0")
+ result = override.apply_to(Gem::Requirement.new(">= 1.0", "< 2.0"))
+ expect(result).to eq(Gem::Requirement.new(">= 8.0"))
+ end
+
+ it "ignores the existing requirement regardless of its content" do
+ override = described_class.new("rails", :version, "= 1.0")
+ result = override.apply_to(Gem::Requirement.new(">= 99.0"))
+ expect(result).to eq(Gem::Requirement.new("= 1.0"))
+ end
+ end
+
+ context "when operation is :ignore_upper" do
+ it "removes < and <= operators" do
+ override = described_class.new("rails", :version, :ignore_upper)
+ result = override.apply_to(Gem::Requirement.new(">= 1.0", "< 2.0"))
+ expect(result).to eq(Gem::Requirement.new(">= 1.0"))
+ end
+
+ it "keeps >, >=, = operators" do
+ override = described_class.new("rails", :version, :ignore_upper)
+ result = override.apply_to(Gem::Requirement.new("> 1.0", "<= 2.0"))
+ expect(result).to eq(Gem::Requirement.new("> 1.0"))
+ end
+
+ it "converts ~> to >= preserving the lower bound" do
+ override = described_class.new("rails", :version, :ignore_upper)
+ result = override.apply_to(Gem::Requirement.new("~> 1.5"))
+ expect(result).to eq(Gem::Requirement.new(">= 1.5"))
+ end
+
+ it "preserves != exclusion constraints" do
+ override = described_class.new("rails", :version, :ignore_upper)
+ result = override.apply_to(Gem::Requirement.new(">= 1.0", "!= 1.5.0", "< 2.0"))
+ expect(result).to eq(Gem::Requirement.new(">= 1.0", "!= 1.5.0"))
+ end
+
+ it "returns the default requirement when only upper bounds remain" do
+ override = described_class.new("rails", :version, :ignore_upper)
+ result = override.apply_to(Gem::Requirement.new("< 2.0"))
+ expect(result).to eq(Gem::Requirement.default)
+ end
+
+ it "returns the default requirement when the input is nil" do
+ override = described_class.new("rails", :version, :ignore_upper)
+ expect(override.apply_to(nil)).to eq(Gem::Requirement.default)
+ end
+
+ it "returns the default requirement when the input is already the default" do
+ override = described_class.new("rails", :version, :ignore_upper)
+ expect(override.apply_to(Gem::Requirement.default)).to eq(Gem::Requirement.default)
+ end
+ end
+
+ context "when operation is nil" do
+ it "returns the default requirement" do
+ override = described_class.new("rails", :version, nil)
+ result = override.apply_to(Gem::Requirement.new(">= 1.0", "< 2.0"))
+ expect(result).to eq(Gem::Requirement.default)
+ end
+ end
+
+ context "when operation is unsupported" do
+ it "raises ArgumentError" do
+ override = described_class.new("rails", :version, 42)
+ expect { override.apply_to(Gem::Requirement.default) }.to raise_error(ArgumentError, /unsupported override operation/)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/plugin/api/source_spec.rb b/spec/bundler/bundler/plugin/api/source_spec.rb
new file mode 100644
index 0000000000..ae02e08bea
--- /dev/null
+++ b/spec/bundler/bundler/plugin/api/source_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Plugin::API::Source do
+ let(:uri) { "uri://to/test" }
+ let(:type) { "spec_type" }
+
+ subject(:source) do
+ klass = Class.new
+ klass.send :include, Bundler::Plugin::API::Source
+ klass.new("uri" => uri, "type" => type)
+ end
+
+ describe "attributes" do
+ it "allows access to uri" do
+ expect(source.uri).to eq("uri://to/test")
+ end
+
+ it "allows access to name" do
+ expect(source.name).to eq("spec_type at uri://to/test")
+ end
+ end
+
+ context "post_install" do
+ let(:installer) { double(:installer) }
+
+ before do
+ allow(Bundler::Source::Path::Installer).to receive(:new) { installer }
+ end
+
+ it "calls Path::Installer's post_install" do
+ expect(installer).to receive(:post_install).once
+
+ source.post_install(double(:spec))
+ end
+ end
+
+ context "install_path" do
+ let(:uri) { "uri://to/a/repository-name" }
+ let(:hash) { Digest(:SHA1).hexdigest(uri) }
+ let(:install_path) { Pathname.new "/bundler/install/path" }
+
+ before do
+ allow(Bundler).to receive(:install_path) { install_path }
+ end
+
+ it "returns basename with uri_hash" do
+ expected = Pathname.new "#{install_path}/repository-name-#{hash[0..11]}"
+ expect(source.install_path).to eq(expected)
+ end
+ end
+
+ context "to_lock" do
+ it "returns the string with remote and type" do
+ expected = <<~L
+ PLUGIN SOURCE
+ remote: #{uri}
+ type: #{type}
+ specs:
+ L
+
+ expect(source.to_lock).to eq(expected)
+ end
+
+ context "with additional options to lock" do
+ before do
+ allow(source).to receive(:options_to_lock) { { "first" => "option" } }
+ end
+
+ it "includes them" do
+ expected = <<~L
+ PLUGIN SOURCE
+ remote: #{uri}
+ type: #{type}
+ first: option
+ specs:
+ L
+
+ expect(source.to_lock).to eq(expected)
+ end
+ end
+ end
+
+ describe "to_s" do
+ it "returns the string with type and uri" do
+ expect(source.to_s).to eq("plugin source for spec_type with uri uri://to/test")
+ end
+ end
+end
diff --git a/spec/bundler/bundler/plugin/api_spec.rb b/spec/bundler/bundler/plugin/api_spec.rb
new file mode 100644
index 0000000000..58fb908572
--- /dev/null
+++ b/spec/bundler/bundler/plugin/api_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Plugin::API do
+ context "plugin declarations" do
+ before do
+ stub_const "UserPluginClass", Class.new(Bundler::Plugin::API)
+ end
+
+ describe "#command" do
+ it "declares a command plugin with same class as handler" do
+ expect(Bundler::Plugin).
+ to receive(:add_command).with("meh", UserPluginClass).once
+
+ UserPluginClass.command "meh"
+ end
+
+ it "accepts another class as argument that handles the command" do
+ stub_const "NewClass", Class.new
+ expect(Bundler::Plugin).to receive(:add_command).with("meh", NewClass).once
+
+ UserPluginClass.command "meh", NewClass
+ end
+ end
+
+ describe "#source" do
+ it "declares a source plugin with same class as handler" do
+ expect(Bundler::Plugin).
+ to receive(:add_source).with("a_source", UserPluginClass).once
+
+ UserPluginClass.source "a_source"
+ end
+
+ it "accepts another class as argument that handles the command" do
+ stub_const "NewClass", Class.new
+ expect(Bundler::Plugin).to receive(:add_source).with("a_source", NewClass).once
+
+ UserPluginClass.source "a_source", NewClass
+ end
+ end
+
+ describe "#hook" do
+ it "accepts a block and passes it to Plugin module" do
+ foo = double("tester")
+ expect(foo).to receive(:called)
+
+ expect(Bundler::Plugin).to receive(:add_hook).with("post-foo").and_yield
+
+ Bundler::Plugin::API.hook("post-foo") { foo.called }
+ end
+ end
+ end
+
+ context "bundler interfaces provided" do
+ before do
+ stub_const "UserPluginClass", Class.new(Bundler::Plugin::API)
+ end
+
+ subject(:api) { UserPluginClass.new }
+
+ # A test of delegation
+ it "provides the Bundler's functions" do
+ expect(Bundler).to receive(:an_unknown_function).once
+
+ api.an_unknown_function
+ end
+
+ it "includes Bundler::SharedHelpers' functions" do
+ expect(Bundler::SharedHelpers).to receive(:an_unknown_helper).once
+
+ api.an_unknown_helper
+ end
+
+ context "#tmp" do
+ it "provides a tmp dir" do
+ expect(api.tmp("mytmp")).to be_directory
+ end
+
+ it "accepts multiple names for suffix" do
+ expect(api.tmp("myplugin", "download")).to be_directory
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/plugin/dsl_spec.rb b/spec/bundler/bundler/plugin/dsl_spec.rb
new file mode 100644
index 0000000000..235a549735
--- /dev/null
+++ b/spec/bundler/bundler/plugin/dsl_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Plugin::DSL do
+ DSL = Bundler::Plugin::DSL
+
+ subject(:dsl) { Bundler::Plugin::DSL.new }
+
+ before do
+ allow(Bundler).to receive(:root) { Pathname.new "/" }
+ end
+
+ describe "it ignores only the methods defined in Bundler::Dsl" do
+ it "doesn't raises error for Dsl methods" do
+ expect { dsl.install_if }.not_to raise_error
+ end
+
+ it "raises error for other methods" do
+ expect { dsl.no_method }.to raise_error(DSL::PluginGemfileError)
+ end
+ end
+
+ describe "source block" do
+ it "adds #source with :type to list and also inferred_plugins list" do
+ expect(dsl).to receive(:plugin).with("bundler-source-news").once
+
+ dsl.source("some_random_url", type: "news") {}
+
+ expect(dsl.inferred_plugins).to eq(["bundler-source-news"])
+ end
+
+ it "registers a source type plugin only once for multiple declarations" do
+ expect(dsl).to receive(:plugin).with("bundler-source-news").and_call_original.once
+
+ dsl.source("some_random_url", type: "news") {}
+ dsl.source("another_random_url", type: "news") {}
+ end
+ end
+end
diff --git a/spec/bundler/bundler/plugin/events_spec.rb b/spec/bundler/bundler/plugin/events_spec.rb
new file mode 100644
index 0000000000..77e5fdb74c
--- /dev/null
+++ b/spec/bundler/bundler/plugin/events_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Plugin::Events do
+ context "plugin events" do
+ before do
+ @old_constants = Bundler::Plugin::Events.constants.map {|name| [name, Bundler::Plugin::Events.const_get(name)] }
+ Bundler::Plugin::Events.send :reset
+ end
+
+ after do
+ Bundler::Plugin::Events.send(:reset)
+ Hash[@old_constants].each do |name, value|
+ Bundler::Plugin::Events.send(:define, name, value)
+ end
+ end
+
+ describe "#define" do
+ it "raises when redefining a constant" do
+ Bundler::Plugin::Events.send(:define, :TEST_EVENT, "foo")
+
+ expect do
+ Bundler::Plugin::Events.send(:define, :TEST_EVENT, "bar")
+ end.to raise_error(ArgumentError)
+ end
+
+ it "can define a new constant" do
+ Bundler::Plugin::Events.send(:define, :NEW_CONSTANT, "value")
+ expect(Bundler::Plugin::Events::NEW_CONSTANT).to eq("value")
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/plugin/index_spec.rb b/spec/bundler/bundler/plugin/index_spec.rb
new file mode 100644
index 0000000000..a28934269b
--- /dev/null
+++ b/spec/bundler/bundler/plugin/index_spec.rb
@@ -0,0 +1,275 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Plugin::Index do
+ Index = Bundler::Plugin::Index
+
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ gemfile "source 'https://gem.repo1'"
+ path = lib_path(plugin_name)
+ index.register_plugin("new-plugin", path.to_s, [path.join("lib").to_s], commands, sources, hooks)
+ end
+
+ let(:plugin_name) { "new-plugin" }
+ let(:commands) { [] }
+ let(:sources) { [] }
+ let(:hooks) { [] }
+
+ subject(:index) { Index.new }
+
+ describe "#register plugin" do
+ it "is available for retrieval" do
+ expect(index.plugin_path(plugin_name)).to eq(lib_path(plugin_name))
+ end
+
+ it "load_paths is available for retrieval" do
+ expect(index.load_paths(plugin_name)).to eq([lib_path(plugin_name).join("lib").to_s])
+ end
+
+ it "is persistent" do
+ new_index = Index.new
+ expect(new_index.plugin_path(plugin_name)).to eq(lib_path(plugin_name))
+ end
+
+ it "load_paths are persistent" do
+ new_index = Index.new
+ expect(new_index.load_paths(plugin_name)).to eq([lib_path(plugin_name).join("lib").to_s])
+ end
+ end
+
+ describe "commands" do
+ let(:commands) { ["newco"] }
+
+ it "returns the plugins name on query" do
+ expect(index.command_plugin("newco")).to eq(plugin_name)
+ end
+
+ it "raises error on conflict" do
+ expect do
+ index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, ["newco"], [], [])
+ end.to raise_error(Index::CommandConflict)
+ end
+
+ it "is persistent" do
+ new_index = Index.new
+ expect(new_index.command_plugin("newco")).to eq(plugin_name)
+ end
+ end
+
+ describe "source" do
+ let(:sources) { ["new_source"] }
+
+ it "returns the plugins name on query" do
+ expect(index.source_plugin("new_source")).to eq(plugin_name)
+ end
+
+ it "raises error on conflict" do
+ expect do
+ index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, [], ["new_source"], [])
+ end.to raise_error(Index::SourceConflict)
+ end
+
+ it "is persistent" do
+ new_index = Index.new
+ expect(new_index.source_plugin("new_source")).to eq(plugin_name)
+ end
+ end
+
+ describe "hook" do
+ let(:hooks) { ["after-bar"] }
+
+ it "returns the plugins name on query" do
+ expect(index.hook_plugins("after-bar")).to include(plugin_name)
+ end
+
+ it "is persistent" do
+ new_index = Index.new
+ expect(new_index.hook_plugins("after-bar")).to eq([plugin_name])
+ end
+
+ it "only registers a gem once for an event" do
+ path = lib_path(plugin_name)
+ index.register_plugin(plugin_name,
+ path.to_s,
+ [path.join("lib").to_s],
+ commands,
+ sources,
+ hooks + hooks)
+ expect(index.hook_plugins("after-bar")).to eq([plugin_name])
+ end
+
+ it "is gone after unregistration" do
+ expect(index.index_file.read).to include("after-bar:\n - \"new-plugin\"\n")
+ index.unregister_plugin(plugin_name)
+ expect(index.index_file.read).to_not include("after-bar:\n - \n")
+ end
+
+ context "that are not registered" do
+ let(:file) { double("index-file") }
+
+ before do
+ index.hook_plugins("not-there")
+ allow(File).to receive(:open).and_yield(file)
+ end
+
+ it "should not save it with next registered hook" do
+ expect(file).to receive(:puts) do |content|
+ expect(content).not_to include("not-there")
+ end
+
+ index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, [], [], [])
+ end
+ end
+ end
+
+ describe "global index" do
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(nil)
+
+ Bundler::Plugin.reset!
+ path = lib_path("gplugin")
+ index.register_plugin("gplugin", path.to_s, [path.join("lib").to_s], [], ["glb_source"], [])
+ end
+
+ it "skips sources" do
+ new_index = Index.new
+ expect(new_index.source_plugin("glb_source")).to be_falsy
+ end
+ end
+
+ describe "after conflict" do
+ let(:commands) { ["foo"] }
+ let(:sources) { ["bar"] }
+ let(:hooks) { ["thehook"] }
+
+ shared_examples "it cleans up" do
+ it "the path" do
+ expect(index.installed?("cplugin")).to be_falsy
+ end
+
+ it "the command" do
+ expect(index.command_plugin("xfoo")).to be_falsy
+ end
+
+ it "the source" do
+ expect(index.source_plugin("xbar")).to be_falsy
+ end
+
+ it "the hook" do
+ expect(index.hook_plugins("xthehook")).to be_empty
+ end
+ end
+
+ context "on command conflict it cleans up" do
+ before do
+ expect do
+ path = lib_path("cplugin")
+ index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["xbar"], ["xthehook"])
+ end.to raise_error(Index::CommandConflict)
+ end
+
+ include_examples "it cleans up"
+ end
+
+ context "on source conflict it cleans up" do
+ before do
+ expect do
+ path = lib_path("cplugin")
+ index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["xfoo"], ["bar"], ["xthehook"])
+ end.to raise_error(Index::SourceConflict)
+ end
+
+ include_examples "it cleans up"
+ end
+
+ context "on command and source conflict it cleans up" do
+ before do
+ expect do
+ path = lib_path("cplugin")
+ index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["bar"], ["xthehook"])
+ end.to raise_error(Index::CommandConflict)
+ end
+
+ include_examples "it cleans up"
+ end
+ end
+
+ describe "relative plugin paths" do
+ let(:plugin_name) { "relative-plugin" }
+
+ before do
+ Bundler::Plugin.reset!
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+
+ plugin_root = Bundler::Plugin.root
+ FileUtils.mkdir_p(plugin_root)
+
+ path = plugin_root.join(plugin_name)
+ FileUtils.mkdir_p(path.join("lib"))
+
+ index.register_plugin(plugin_name, path.to_s, [path.join("lib").to_s], [], [], [])
+ end
+
+ it "stores plugin paths relative to the plugin root" do
+ require "yaml"
+ data = YAML.load_file(index.index_file)
+
+ expect(data["plugin_paths"][plugin_name]).to eq(plugin_name)
+ expect(data["load_paths"][plugin_name]).to eq([File.join(plugin_name, "lib")])
+ end
+
+ it "expands relative paths to absolute on load" do
+ require "bundler/yaml_serializer"
+
+ plugin_root = Bundler::Plugin.root
+
+ relative_index = {
+ "commands" => {},
+ "hooks" => {},
+ "load_paths" => { plugin_name => [File.join(plugin_name, "lib")] },
+ "plugin_paths" => { plugin_name => plugin_name },
+ "sources" => {},
+ }
+
+ File.open(index.index_file, "w") {|f| f.puts Bundler::YAMLSerializer.dump(relative_index) }
+
+ new_index = Index.new
+ expect(new_index.plugin_path(plugin_name)).to eq(plugin_root.join(plugin_name))
+ expect(new_index.load_paths(plugin_name)).to eq([plugin_root.join(plugin_name, "lib").to_s])
+ end
+
+ it "keeps paths outside the plugin root as absolute" do
+ outside_path = tmp.join("outside", "external-plugin")
+ FileUtils.mkdir_p(outside_path.join("lib"))
+
+ index.register_plugin("external-plugin", outside_path.to_s, [outside_path.join("lib").to_s], [], [], [])
+
+ require "yaml"
+ data = YAML.load_file(index.index_file)
+
+ expect(data["plugin_paths"]["external-plugin"]).to eq(outside_path.to_s)
+ expect(data["load_paths"]["external-plugin"]).to eq([outside_path.join("lib").to_s])
+ end
+
+ it "reads legacy index files with absolute paths" do
+ require "bundler/yaml_serializer"
+
+ plugin_root = Bundler::Plugin.root
+ absolute_path = plugin_root.join(plugin_name).to_s
+
+ legacy_index = {
+ "commands" => {},
+ "hooks" => {},
+ "load_paths" => { plugin_name => [File.join(absolute_path, "lib")] },
+ "plugin_paths" => { plugin_name => absolute_path },
+ "sources" => {},
+ }
+
+ File.open(index.index_file, "w") {|f| f.puts Bundler::YAMLSerializer.dump(legacy_index) }
+
+ new_index = Index.new
+ expect(new_index.plugin_path(plugin_name)).to eq(Pathname.new(absolute_path))
+ expect(new_index.load_paths(plugin_name)).to eq([File.join(absolute_path, "lib")])
+ end
+ end
+end
diff --git a/spec/bundler/bundler/plugin/installer_spec.rb b/spec/bundler/bundler/plugin/installer_spec.rb
new file mode 100644
index 0000000000..c200a98afa
--- /dev/null
+++ b/spec/bundler/bundler/plugin/installer_spec.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Plugin::Installer do
+ subject(:installer) { Bundler::Plugin::Installer.new }
+
+ describe "cli install" do
+ it "uses Gem.sources when non of the source is provided" do
+ sources = double(:sources)
+ allow(Gem).to receive(:sources) { sources }
+
+ allow(installer).to receive(:install_rubygems).
+ with("new-plugin", [">= 0"], sources).once
+
+ installer.install("new-plugin", {})
+ end
+
+ describe "with mocked installers" do
+ let(:spec) { double(:spec) }
+ it "returns the installed spec after installing git plugins" do
+ allow(installer).to receive(:install_git).
+ and_return("new-plugin" => spec)
+
+ expect(installer.install(["new-plugin"], git: "https://some.ran/dom")).
+ to eq("new-plugin" => spec)
+ end
+
+ it "returns the installed spec after installing local git plugins" do
+ allow(installer).to receive(:install_git).
+ and_return("new-plugin" => spec)
+
+ expect(installer.install(["new-plugin"], git: "/phony/path/repo")).
+ to eq("new-plugin" => spec)
+ end
+
+ it "returns the installed spec after installing rubygems plugins" do
+ allow(installer).to receive(:install_rubygems).
+ and_return("new-plugin" => spec)
+
+ expect(installer.install(["new-plugin"], source: "https://some.ran/dom")).
+ to eq("new-plugin" => spec)
+ end
+ end
+
+ describe "with actual installers" do
+ before do
+ build_repo2 do
+ build_plugin "re-plugin"
+ build_plugin "ma-plugin"
+ end
+
+ @previous_ui = Bundler.ui
+ Bundler.ui = Bundler::UI::Silent.new
+ end
+
+ after do
+ Bundler.ui = @previous_ui
+ end
+
+ context "git plugins" do
+ before do
+ build_git "ga-plugin", path: lib_path("ga-plugin") do |s|
+ s.write "plugins.rb"
+ end
+ end
+
+ let(:result) do
+ installer.install(["ga-plugin"], git: lib_path("ga-plugin").to_s)
+ end
+
+ it "returns the installed spec after installing" do
+ spec = result["ga-plugin"]
+ expect(spec.full_name).to eq "ga-plugin-1.0"
+ end
+
+ it "has expected full_gem_path" do
+ rev = revision_for(lib_path("ga-plugin"))
+ expect(result["ga-plugin"].full_gem_path).
+ to eq(Bundler::Plugin.root.join("bundler", "gems", "ga-plugin-#{rev[0..11]}").to_s)
+ end
+ end
+
+ context "local git plugins" do
+ before do
+ build_git "ga-plugin", path: lib_path("ga-plugin") do |s|
+ s.write "plugins.rb"
+ end
+ end
+
+ let(:result) do
+ installer.install(["ga-plugin"], git: lib_path("ga-plugin").to_s)
+ end
+
+ it "returns the installed spec after installing" do
+ spec = result["ga-plugin"]
+ expect(spec.full_name).to eq "ga-plugin-1.0"
+ end
+
+ it "has expected full_gem_path" do
+ rev = revision_for(lib_path("ga-plugin"))
+ expect(result["ga-plugin"].full_gem_path).
+ to eq(Bundler::Plugin.root.join("bundler", "gems", "ga-plugin-#{rev[0..11]}").to_s)
+ end
+ end
+
+ context "rubygems plugins" do
+ let(:result) do
+ installer.install(["re-plugin"], source: file_uri_for(gem_repo2))
+ end
+
+ it "returns the installed spec after installing " do
+ expect(result["re-plugin"]).to be_kind_of(Bundler::RemoteSpecification)
+ end
+
+ it "has expected full_gem_path" do
+ expect(result["re-plugin"].full_gem_path).
+ to eq(global_plugin_gem("re-plugin-1.0").to_s)
+ end
+ end
+
+ context "multiple plugins" do
+ let(:result) do
+ installer.install(["re-plugin", "ma-plugin"], source: file_uri_for(gem_repo2))
+ end
+
+ it "returns the installed spec after installing " do
+ expect(result["re-plugin"]).to be_kind_of(Bundler::RemoteSpecification)
+ expect(result["ma-plugin"]).to be_kind_of(Bundler::RemoteSpecification)
+ end
+
+ it "has expected full_gem_path" do
+ expect(result["re-plugin"].full_gem_path).to eq(global_plugin_gem("re-plugin-1.0").to_s)
+ expect(result["ma-plugin"].full_gem_path).to eq(global_plugin_gem("ma-plugin-1.0").to_s)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/plugin/source_list_spec.rb b/spec/bundler/bundler/plugin/source_list_spec.rb
new file mode 100644
index 0000000000..64a1233dd1
--- /dev/null
+++ b/spec/bundler/bundler/plugin/source_list_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Plugin::SourceList do
+ SourceList = Bundler::Plugin::SourceList
+
+ before do
+ allow(Bundler).to receive(:root) { Pathname.new "/" }
+ end
+
+ subject(:source_list) { SourceList.new }
+
+ describe "adding sources uses classes for plugin" do
+ it "uses Plugin::Installer::Rubygems for rubygems sources" do
+ source = source_list.
+ add_rubygems_source("remotes" => ["https://existing-rubygems.org"])
+ expect(source).to be_instance_of(Bundler::Plugin::Installer::Rubygems)
+ end
+
+ it "uses Plugin::Installer::Git for git sources" do
+ source = source_list.
+ add_git_source("uri" => "git://existing-git.org/path.git")
+ expect(source).to be_instance_of(Bundler::Plugin::Installer::Git)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/plugin_spec.rb b/spec/bundler/bundler/plugin_spec.rb
new file mode 100644
index 0000000000..b379594c6f
--- /dev/null
+++ b/spec/bundler/bundler/plugin_spec.rb
@@ -0,0 +1,368 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Plugin do
+ Plugin = Bundler::Plugin
+
+ let(:installer) { double(:installer) }
+ let(:index) { double(:index) }
+ let(:spec) { double(:spec) }
+ let(:spec2) { double(:spec2) }
+
+ before do
+ build_lib "new-plugin", path: lib_path("new-plugin") do |s|
+ s.write "plugins.rb"
+ end
+
+ build_lib "another-plugin", path: lib_path("another-plugin") do |s|
+ s.write "plugins.rb"
+ end
+
+ allow(spec).to receive(:full_gem_path).
+ and_return(lib_path("new-plugin").to_s)
+ allow(spec).to receive(:load_paths).
+ and_return([lib_path("new-plugin").join("lib").to_s])
+
+ allow(spec2).to receive(:full_gem_path).
+ and_return(lib_path("another-plugin").to_s)
+ allow(spec2).to receive(:load_paths).
+ and_return([lib_path("another-plugin").join("lib").to_s])
+
+ allow(Plugin::Installer).to receive(:new) { installer }
+ allow(Plugin).to receive(:index) { index }
+ allow(index).to receive(:register_plugin)
+ end
+
+ describe "list command" do
+ context "when no plugins are installed" do
+ before { allow(index).to receive(:installed_plugins) { [] } }
+ it "outputs no plugins installed" do
+ expect(Bundler.ui).to receive(:info).with("No plugins installed")
+ subject.list
+ end
+ end
+
+ context "with installed plugins" do
+ before do
+ allow(index).to receive(:installed_plugins) { %w[plug1 plug2] }
+ allow(index).to receive(:plugin_commands).with("plug1") { %w[c11 c12] }
+ allow(index).to receive(:plugin_commands).with("plug2") { %w[c21 c22] }
+ end
+ it "list plugins followed by commands" do
+ expected_output = "plug1\n-----\n c11\n c12\n\nplug2\n-----\n c21\n c22\n\n"
+ expect(Bundler.ui).to receive(:info).with(expected_output)
+ subject.list
+ end
+ end
+ end
+
+ describe "install command" do
+ let(:opts) { { "version" => "~> 1.0", "source" => "foo" } }
+
+ before do
+ allow(installer).to receive(:install).with(["new-plugin"], opts) do
+ { "new-plugin" => spec }
+ end
+ end
+
+ it "passes the name and options to installer" do
+ allow(index).to receive(:up_to_date?).
+ with(spec)
+ allow(installer).to receive(:install).with(["new-plugin"], opts) do
+ { "new-plugin" => spec }
+ end.once
+
+ subject.install ["new-plugin"], opts
+ end
+
+ it "validates the installed plugin" do
+ allow(index).to receive(:up_to_date?).
+ with(spec)
+ allow(subject).
+ to receive(:validate_plugin!).with(lib_path("new-plugin")).once
+
+ subject.install ["new-plugin"], opts
+ end
+
+ it "registers the plugin with index" do
+ allow(index).to receive(:up_to_date?).
+ with(spec)
+ allow(index).to receive(:register_plugin).
+ with("new-plugin", lib_path("new-plugin").to_s, [lib_path("new-plugin").join("lib").to_s], []).once
+ subject.install ["new-plugin"], opts
+ end
+
+ context "multiple plugins" do
+ it do
+ allow(installer).to receive(:install).
+ with(["new-plugin", "another-plugin"], opts) do
+ {
+ "new-plugin" => spec,
+ "another-plugin" => spec2,
+ }
+ end.once
+
+ allow(subject).to receive(:validate_plugin!).twice
+ allow(index).to receive(:up_to_date?).twice
+ allow(index).to receive(:register_plugin).twice
+ subject.install ["new-plugin", "another-plugin"], opts
+ end
+ end
+ end
+
+ describe "evaluate gemfile for plugins" do
+ let(:definition) { double("definition") }
+ let(:builder) { double("builder") }
+ let(:gemfile) { bundled_app_gemfile }
+
+ before do
+ allow(Plugin::DSL).to receive(:new) { builder }
+ allow(builder).to receive(:eval_gemfile).with(gemfile)
+ allow(builder).to receive(:check_primary_source_safety)
+ allow(builder).to receive(:to_definition) { definition }
+ allow(builder).to receive(:inferred_plugins) { [] }
+ end
+
+ it "doesn't calls installer without any plugins" do
+ allow(definition).to receive(:dependencies) { [] }
+ allow(installer).to receive(:install_definition).never
+
+ subject.gemfile_install(gemfile)
+ end
+
+ context "with dependencies" do
+ let(:plugin_specs) do
+ {
+ "new-plugin" => spec,
+ "another-plugin" => spec2,
+ }
+ end
+
+ before do
+ allow(index).to receive(:up_to_date?) { nil }
+ allow(definition).to receive(:dependencies) { [Bundler::Dependency.new("new-plugin", ">=0"), Bundler::Dependency.new("another-plugin", ">=0")] }
+ allow(installer).to receive(:install_definition) { plugin_specs }
+ end
+
+ it "should validate and register the plugins" do
+ expect(subject).to receive(:validate_plugin!).twice
+ expect(subject).to receive(:register_plugin).twice
+
+ subject.gemfile_install(gemfile)
+ end
+
+ it "should pass the optional plugins to #register_plugin" do
+ allow(builder).to receive(:inferred_plugins) { ["another-plugin"] }
+
+ expect(subject).to receive(:register_plugin).
+ with("new-plugin", spec, false).once
+
+ expect(subject).to receive(:register_plugin).
+ with("another-plugin", spec2, true).once
+
+ subject.gemfile_install(gemfile)
+ end
+ end
+ end
+
+ describe "#command?" do
+ it "returns true value for commands in index" do
+ allow(index).
+ to receive(:command_plugin).with("newcommand") { "my-plugin" }
+ result = subject.command? "newcommand"
+ expect(result).to be_truthy
+ end
+
+ it "returns false value for commands not in index" do
+ allow(index).to receive(:command_plugin).with("newcommand") { nil }
+ result = subject.command? "newcommand"
+ expect(result).to be_falsy
+ end
+ end
+
+ describe "#exec_command" do
+ it "raises UndefinedCommandError when command is not found" do
+ allow(index).to receive(:command_plugin).with("newcommand") { nil }
+ expect { subject.exec_command("newcommand", []) }.
+ to raise_error(Plugin::UndefinedCommandError)
+ end
+ end
+
+ describe "#source?" do
+ it "returns true value for sources in index" do
+ allow(index).
+ to receive(:command_plugin).with("foo-source") { "my-plugin" }
+ result = subject.command? "foo-source"
+ expect(result).to be_truthy
+ end
+
+ it "returns false value for source not in index" do
+ allow(index).to receive(:command_plugin).with("foo-source") { nil }
+ result = subject.command? "foo-source"
+ expect(result).to be_falsy
+ end
+ end
+
+ describe "#source" do
+ it "raises UnknownSourceError when source is not found" do
+ allow(index).to receive(:source_plugin).with("bar") { nil }
+ expect { subject.source("bar") }.
+ to raise_error(Plugin::UnknownSourceError)
+ end
+
+ it "loads the plugin, if not loaded" do
+ allow(index).to receive(:source_plugin).with("foo-bar") { "plugin_name" }
+
+ expect(subject).to receive(:load_plugin).with("plugin_name")
+ subject.source("foo-bar")
+ end
+
+ it "returns the class registered with #add_source" do
+ allow(index).to receive(:source_plugin).with("foo") { "plugin_name" }
+ stub_const "NewClass", Class.new
+
+ subject.add_source("foo", NewClass)
+ expect(subject.source("foo")).to be(NewClass)
+ end
+ end
+
+ describe "#from_lock" do
+ it "returns instance of registered class initialized with locked opts" do
+ opts = { "type" => "l_source", "remote" => "xyz", "other" => "random" }
+ allow(index).to receive(:source_plugin).with("l_source") { "plugin_name" }
+
+ stub_const "SClass", Class.new
+ s_instance = double(:s_instance)
+ subject.add_source("l_source", SClass)
+
+ expect(SClass).to receive(:new).
+ with(hash_including("type" => "l_source", "uri" => "xyz", "other" => "random")) { s_instance }
+ expect(subject.from_lock(opts)).to be(s_instance)
+ end
+ end
+
+ describe "#root" do
+ context "in app dir" do
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ it "returns plugin dir in app .bundle path" do
+ expect(subject.root).to eq(bundled_app(".bundle/plugin"))
+ end
+ end
+
+ context "outside app dir" do
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(nil)
+ end
+
+ it "returns plugin dir in global bundle path" do
+ expect(subject.root).to eq(home(".bundle/plugin"))
+ end
+ end
+ end
+
+ describe "#add_hook" do
+ it "raises an ArgumentError on an unregistered event" do
+ ran = false
+ expect do
+ Plugin.add_hook("unregistered-hook") { ran = true }
+ end.to raise_error(ArgumentError)
+ expect(ran).to be(false)
+ end
+ end
+
+ describe "#hook" do
+ before do
+ path = lib_path("foo-plugin")
+ build_lib "foo-plugin", path: path do |s|
+ s.write "plugins.rb", code
+ end
+
+ @old_constants = Bundler::Plugin::Events.constants.map {|name| [name, Bundler::Plugin::Events.const_get(name)] }
+ Bundler::Plugin::Events.send(:reset)
+ Bundler::Plugin::Events.send(:define, :EVENT1, "event-1")
+ Bundler::Plugin::Events.send(:define, :EVENT2, "event-2")
+
+ allow(index).to receive(:hook_plugins).with(Bundler::Plugin::Events::EVENT1).
+ and_return(["foo-plugin", "", nil])
+ allow(index).to receive(:hook_plugins).with(Bundler::Plugin::Events::EVENT2).
+ and_return(["foo-plugin"])
+ allow(index).to receive(:plugin_path).with("foo-plugin").and_return(path)
+ allow(index).to receive(:load_paths).with("foo-plugin").and_return([])
+ end
+
+ after do
+ Bundler::Plugin::Events.send(:reset)
+ Hash[@old_constants].each do |name, value|
+ Bundler::Plugin::Events.send(:define, name, value)
+ end
+ end
+
+ let(:code) { <<-RUBY }
+ Bundler::Plugin::API.hook("event-1") { puts "hook for event 1" }
+ RUBY
+
+ it "raises an ArgumentError on an unregistered event" do
+ expect do
+ Plugin.hook("unregistered-hook")
+ end.to raise_error(ArgumentError)
+ end
+
+ it "executes the hook" do
+ expect do
+ Plugin.hook(Bundler::Plugin::Events::EVENT1)
+ end.to output("hook for event 1\n").to_stdout
+ end
+
+ context "single plugin declaring more than one hook" do
+ let(:code) { <<-RUBY }
+ Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT1) {}
+ Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT2) {}
+ puts "loaded"
+ RUBY
+
+ it "evals plugins.rb once" do
+ expect do
+ Plugin.hook(Bundler::Plugin::Events::EVENT1)
+ Plugin.hook(Bundler::Plugin::Events::EVENT2)
+ end.to output("loaded\n").to_stdout
+ end
+ end
+
+ context "a block is passed" do
+ let(:code) { <<-RUBY }
+ Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT1) { |&blk| blk.call }
+ RUBY
+
+ it "is passed to the hook" do
+ expect do
+ Plugin.hook(Bundler::Plugin::Events::EVENT1) { puts "win" }
+ end.to output("win\n").to_stdout
+ end
+ end
+
+ context "the plugin load_path is invalid" do
+ before do
+ allow(index).to receive(:load_paths).with("foo-plugin").
+ and_return(["invalid-file-name1", "invalid-file-name2"])
+ end
+
+ it "outputs a useful warning" do
+ msg =
+ "The following plugin paths don't exist: invalid-file-name1, invalid-file-name2.\n\n" \
+ "This can happen if the plugin was " \
+ "installed with a different version of Ruby that has since been uninstalled.\n\n" \
+ "If you would like to reinstall the plugin, run:\n\n" \
+ "bundler plugin uninstall foo-plugin && bundler plugin install foo-plugin\n\n" \
+ "Continuing without installing plugin foo-plugin.\n"
+
+ expect(Bundler.ui).to receive(:warn).with(msg)
+
+ Plugin.hook(Bundler::Plugin::Events::EVENT1)
+
+ expect(subject.loaded?("foo-plugin")).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/remote_specification_spec.rb b/spec/bundler/bundler/remote_specification_spec.rb
new file mode 100644
index 0000000000..f35b231d58
--- /dev/null
+++ b/spec/bundler/bundler/remote_specification_spec.rb
@@ -0,0 +1,187 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::RemoteSpecification do
+ let(:name) { "foo" }
+ let(:version) { Gem::Version.new("1.0.0") }
+ let(:platform) { Gem::Platform::RUBY }
+ let(:spec_fetcher) { double(:spec_fetcher) }
+
+ subject { described_class.new(name, version, platform, spec_fetcher) }
+
+ it "is Comparable" do
+ expect(described_class.ancestors).to include(Comparable)
+ end
+
+ it "can match platforms" do
+ expect(described_class.ancestors).to include(Bundler::MatchPlatform)
+ end
+
+ describe "#fetch_platform" do
+ let(:remote_spec) { double(:remote_spec, platform: "jruby") }
+
+ before { allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec) }
+
+ it "should return the spec platform" do
+ expect(subject.fetch_platform).to eq("jruby")
+ end
+ end
+
+ describe "#full_name" do
+ context "when platform is ruby" do
+ it "should return the spec name and version" do
+ expect(subject.full_name).to eq("foo-1.0.0")
+ end
+ end
+
+ context "when platform is nil" do
+ let(:platform) { nil }
+
+ it "should return the spec name and version" do
+ expect(subject.full_name).to eq("foo-1.0.0")
+ end
+ end
+
+ context "when platform is a non-ruby platform" do
+ let(:platform) { "jruby" }
+
+ it "should return the spec name, version, and platform" do
+ expect(subject.full_name).to eq("foo-1.0.0-java")
+ end
+ end
+ end
+
+ describe "#<=>" do
+ let(:other_name) { name }
+ let(:other_version) { version }
+ let(:other_platform) { platform }
+ let(:other_spec_fetcher) { spec_fetcher }
+
+ shared_examples_for "a comparison" do
+ context "which exactly matches" do
+ it "returns 0" do
+ expect(subject <=> other).to eq(0)
+ end
+ end
+
+ context "which is different by name" do
+ let(:other_name) { "a" }
+ it "returns 1" do
+ expect(subject <=> other).to eq(1)
+ end
+ end
+
+ context "which has a lower version" do
+ let(:other_version) { Gem::Version.new("0.9.0") }
+ it "returns 1" do
+ expect(subject <=> other).to eq(1)
+ end
+ end
+
+ context "which has a higher version" do
+ let(:other_version) { Gem::Version.new("1.1.0") }
+ it "returns -1" do
+ expect(subject <=> other).to eq(-1)
+ end
+ end
+
+ context "which has a different platform" do
+ let(:other_platform) { Gem::Platform.new("x86-mswin32") }
+ it "returns -1" do
+ expect(subject <=> other).to eq(-1)
+ end
+ end
+ end
+
+ context "comparing another Bundler::RemoteSpecification" do
+ let(:other) do
+ Bundler::RemoteSpecification.new(other_name, other_version,
+ other_platform, nil)
+ end
+
+ it_should_behave_like "a comparison"
+ end
+
+ context "comparing a Gem::Specification" do
+ let(:other) do
+ Gem::Specification.new(other_name, other_version).tap do |s|
+ s.platform = other_platform
+ end
+ end
+
+ it_should_behave_like "a comparison"
+ end
+
+ context "comparing a non sortable object" do
+ let(:other) { Object.new }
+ let(:remote_spec) { double(:remote_spec, platform: "jruby") }
+
+ before do
+ allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec)
+ allow(remote_spec).to receive(:<=>).and_return(nil)
+ end
+
+ it "should use default object comparison" do
+ expect(subject <=> other).to eq(nil)
+ end
+ end
+ end
+
+ describe "#__swap__" do
+ let(:spec) { double(:spec, dependencies: []) }
+ let(:new_spec) { double(:new_spec, dependencies: [], runtime_dependencies: []) }
+
+ before { subject.instance_variable_set(:@_remote_specification, spec) }
+
+ it "should replace remote specification with the passed spec" do
+ expect(subject.instance_variable_get(:@_remote_specification)).to be(spec)
+ subject.__swap__(new_spec)
+ expect(subject.instance_variable_get(:@_remote_specification)).to be(new_spec)
+ end
+ end
+
+ describe "#sort_obj" do
+ context "when platform is ruby" do
+ it "should return a sorting delegate array with name, version, and -1" do
+ expect(subject.sort_obj).to match_array(["foo", version, -1])
+ end
+ end
+
+ context "when platform is not ruby" do
+ let(:platform) { "jruby" }
+
+ it "should return a sorting delegate array with name, version, and 1" do
+ expect(subject.sort_obj).to match_array(["foo", version, 1])
+ end
+ end
+ end
+
+ describe "method missing" do
+ context "and is present in Gem::Specification" do
+ let(:remote_spec) { double(:remote_spec, authors: "abcd") }
+
+ before do
+ allow(subject).to receive(:_remote_specification).and_return(remote_spec)
+ expect(subject.methods.map(&:to_sym)).not_to include(:authors)
+ end
+
+ it "should send through to Gem::Specification" do
+ expect(subject.authors).to eq("abcd")
+ end
+ end
+ end
+
+ describe "respond to missing?" do
+ context "and is present in Gem::Specification" do
+ let(:remote_spec) { double(:remote_spec, authors: "abcd") }
+
+ before do
+ allow(subject).to receive(:_remote_specification).and_return(remote_spec)
+ expect(subject.methods.map(&:to_sym)).not_to include(:authors)
+ end
+
+ it "should send through to Gem::Specification" do
+ expect(subject.respond_to?(:authors)).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/resolver/candidate_spec.rb b/spec/bundler/bundler/resolver/candidate_spec.rb
new file mode 100644
index 0000000000..aefad3316e
--- /dev/null
+++ b/spec/bundler/bundler/resolver/candidate_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Resolver::Candidate do
+ it "compares fine" do
+ version1 = described_class.new("1.12.5", priority: -1)
+ version2 = described_class.new("1.12.5", priority: 1)
+
+ expect(version2 > version1).to be true
+
+ version1 = described_class.new("1.12.5")
+ version2 = described_class.new("1.12.5")
+
+ expect(version2 == version1).to be true
+
+ version1 = described_class.new("1.12.5", priority: 1)
+ version2 = described_class.new("1.12.5", priority: -1)
+
+ expect(version2 < version1).to be true
+ end
+end
diff --git a/spec/bundler/bundler/resolver/cooldown_spec.rb b/spec/bundler/bundler/resolver/cooldown_spec.rb
new file mode 100644
index 0000000000..37ec158cba
--- /dev/null
+++ b/spec/bundler/bundler/resolver/cooldown_spec.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Resolver do
+ let(:resolver) { described_class.allocate }
+
+ def remote(cooldown:)
+ instance_double(Bundler::Source::Rubygems::Remote, effective_cooldown: cooldown)
+ end
+
+ def spec(created_at:, remote:, name: "myrack", version: "1.0.0")
+ Struct.new(:name, :version, :created_at, :remote).new(name, Gem::Version.new(version), created_at, remote)
+ end
+
+ describe "#filter_cooldown" do
+ let(:now) { Time.now }
+
+ context "with a 7-day cooldown" do
+ let(:r) { remote(cooldown: 7) }
+
+ it "rejects versions published within the window" do
+ recent = spec(version: "1.1.0", created_at: now - (2 * 86_400), remote: r)
+ old = spec(version: "1.0.0", created_at: now - (30 * 86_400), remote: r)
+
+ expect(resolver.send(:filter_cooldown, [recent, old])).to eq([old])
+ end
+
+ it "keeps versions published exactly at the threshold" do
+ boundary = spec(created_at: now - (7 * 86_400), remote: r)
+
+ expect(resolver.send(:filter_cooldown, [boundary])).to eq([boundary])
+ end
+
+ it "leaves rolling-delay history intact" do
+ # 7-day cooldown with frequent releases must still expose an older candidate.
+ in_cooldown = spec(version: "1.2.0", created_at: now - 86_400, remote: r)
+ also_in_cooldown = spec(version: "1.1.0", created_at: now - (3 * 86_400), remote: r)
+ eligible = spec(version: "1.0.0", created_at: now - (10 * 86_400), remote: r)
+
+ result = resolver.send(:filter_cooldown, [in_cooldown, also_in_cooldown, eligible])
+
+ expect(result).to eq([eligible])
+ end
+
+ it "drops every spec sharing an excluded [name, version] tuple" do
+ # The cooldown check is by version, not per-spec: a StubSpecification for an
+ # in-cooldown release would otherwise slip through on local install paths.
+ endpoint = spec(version: "2.0.0", created_at: now - 86_400, remote: r)
+ local_stub = Struct.new(:name, :version).new("myrack", Gem::Version.new("2.0.0"))
+ eligible = spec(version: "1.0.0", created_at: now - (30 * 86_400), remote: r)
+
+ result = resolver.send(:filter_cooldown, [endpoint, local_stub, eligible])
+
+ expect(result).to eq([eligible])
+ end
+
+ it "keeps stub-only versions that no endpoint marks as in cooldown" do
+ # If no remote spec carries created_at for a version, cooldown cannot judge it;
+ # the stub stays in.
+ local_only = Struct.new(:name, :version).new("myrack", Gem::Version.new("2.0.0"))
+ eligible = spec(version: "1.0.0", created_at: now - (30 * 86_400), remote: r)
+
+ result = resolver.send(:filter_cooldown, [local_only, eligible])
+
+ expect(result).to eq([local_only, eligible])
+ end
+ end
+
+ context "when created_at is missing (blank metadata)" do
+ it "keeps the spec regardless of cooldown" do
+ s = spec(created_at: nil, remote: remote(cooldown: 7))
+
+ expect(resolver.send(:filter_cooldown, [s])).to eq([s])
+ end
+ end
+
+ context "when the remote has no cooldown" do
+ it "keeps every spec" do
+ s = spec(created_at: now - 3600, remote: remote(cooldown: nil))
+
+ expect(resolver.send(:filter_cooldown, [s])).to eq([s])
+ end
+ end
+
+ context "when cooldown is 0" do
+ it "keeps every spec (escape hatch)" do
+ s = spec(created_at: now - 3600, remote: remote(cooldown: 0))
+
+ expect(resolver.send(:filter_cooldown, [s])).to eq([s])
+ end
+ end
+
+ context "when the spec does not respond to created_at" do
+ it "keeps the spec" do
+ bare = Struct.new(:version).new("1.0.0")
+
+ expect(resolver.send(:filter_cooldown, [bare])).to eq([bare])
+ end
+ end
+
+ context "when the spec has no remote" do
+ it "keeps the spec" do
+ s = spec(created_at: now - 86_400, remote: nil)
+
+ expect(resolver.send(:filter_cooldown, [s])).to eq([s])
+ end
+ end
+
+ it "returns the same array when input is empty" do
+ expect(resolver.send(:filter_cooldown, [])).to eq([])
+ end
+ end
+
+ describe "#cooldown_hint" do
+ let(:now) { Time.now }
+ let(:r) { remote(cooldown: 7) }
+
+ it "returns nil when no spec is excluded" do
+ expect(resolver.send(:cooldown_hint, [])).to be_nil
+ end
+
+ it "returns nil when every spec is outside the cooldown window" do
+ eligible = [spec(created_at: now - (30 * 86_400), remote: r)]
+
+ expect(resolver.send(:cooldown_hint, eligible)).to be_nil
+ end
+
+ it "mentions the count and the bypass flag for one excluded version" do
+ excluded = [spec(created_at: now - 86_400, remote: r)]
+
+ hint = resolver.send(:cooldown_hint, excluded)
+
+ expect(hint).to match(/1 version excluded by the cooldown setting/)
+ expect(hint).to match(/--cooldown 0/)
+ end
+
+ it "uses plural wording when multiple versions are excluded" do
+ excluded = %w[1.0.0 1.1.0 1.2.0].map {|v| spec(version: v, created_at: now - 86_400, remote: r) }
+
+ expect(resolver.send(:cooldown_hint, excluded)).to match(/3 versions excluded/)
+ end
+
+ it "counts each unique version once even when multiple spec instances share it" do
+ duplicates = Array.new(3) { spec(created_at: now - 86_400, remote: r) }
+
+ expect(resolver.send(:cooldown_hint, duplicates)).to match(/1 version excluded/)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/retry_spec.rb b/spec/bundler/bundler/retry_spec.rb
new file mode 100644
index 0000000000..5c84d0bea5
--- /dev/null
+++ b/spec/bundler/bundler/retry_spec.rb
@@ -0,0 +1,190 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Retry do
+ it "return successful result if no errors" do
+ attempts = 0
+ result = Bundler::Retry.new(nil, nil, 3).attempt do
+ attempts += 1
+ :success
+ end
+ expect(result).to eq(:success)
+ expect(attempts).to eq(1)
+ end
+
+ it "returns the first valid result" do
+ jobs = [proc { raise "job 1 failed" }, proc { :bar }, proc { raise "job 2 failed" }]
+ attempts = 0
+ result = Bundler::Retry.new(nil, nil, 3).attempt do
+ attempts += 1
+ jobs.shift.call
+ end
+ expect(result).to eq(:bar)
+ expect(attempts).to eq(2)
+ end
+
+ it "raises the last error" do
+ errors = [StandardError, StandardError, StandardError, Bundler::GemfileNotFound]
+ attempts = 0
+ expect do
+ Bundler::Retry.new(nil, nil, 3).attempt do
+ attempts += 1
+ raise errors.shift
+ end
+ end.to raise_error(Bundler::GemfileNotFound)
+ expect(attempts).to eq(4)
+ end
+
+ it "raises exceptions" do
+ error = Bundler::GemfileNotFound
+ attempts = 0
+ expect do
+ Bundler::Retry.new(nil, error).attempt do
+ attempts += 1
+ raise error
+ end
+ end.to raise_error(error)
+ expect(attempts).to eq(1)
+ end
+
+ context "logging" do
+ let(:error) { Bundler::GemfileNotFound }
+ let(:failure_message) { "Retrying test due to error (2/2): #{error} #{error}" }
+
+ context "with debugging on" do
+ it "print error message with newline" do
+ allow(Bundler.ui).to receive(:debug?).and_return(true)
+ expect(Bundler.ui).to_not receive(:info)
+ expect(Bundler.ui).to receive(:warn).with(failure_message, true)
+
+ expect do
+ Bundler::Retry.new("test", [], 1).attempt do
+ raise error
+ end
+ end.to raise_error(error)
+ end
+ end
+
+ context "with debugging off" do
+ it "print error message with newlines" do
+ allow(Bundler.ui).to receive(:debug?).and_return(false)
+ expect(Bundler.ui).to receive(:info).with("").twice
+ expect(Bundler.ui).to receive(:warn).with(failure_message, true)
+
+ expect do
+ Bundler::Retry.new("test", [], 1).attempt do
+ raise error
+ end
+ end.to raise_error(error)
+ end
+ end
+ end
+
+ context "exponential backoff" do
+ it "can be disabled by setting base_delay to 0" do
+ attempts = 0
+ expect do
+ Bundler::Retry.new("test", [], 2, base_delay: 0).attempt do
+ attempts += 1
+ raise "error"
+ end
+ end.to raise_error(StandardError)
+
+ # Verify no sleep was called (implicitly - if sleep was called, timing would be different)
+ expect(attempts).to eq(3)
+ end
+
+ it "is enabled by default with 1 second base delay" do
+ original_base_delay = Bundler::Retry.default_base_delay
+ Bundler::Retry.default_base_delay = 1.0
+
+ attempts = 0
+ sleep_times = []
+
+ allow_any_instance_of(Bundler::Retry).to receive(:sleep) do |_instance, delay|
+ sleep_times << delay
+ end
+
+ expect do
+ Bundler::Retry.new("test", [], 2, jitter: 0).attempt do
+ attempts += 1
+ raise "error"
+ end
+ end.to raise_error(StandardError)
+
+ expect(attempts).to eq(3)
+ expect(sleep_times.length).to eq(2)
+ # First retry: 1.0 * 2^0 = 1.0
+ expect(sleep_times[0]).to eq(1.0)
+ # Second retry: 1.0 * 2^1 = 2.0
+ expect(sleep_times[1]).to eq(2.0)
+ ensure
+ Bundler::Retry.default_base_delay = original_base_delay
+ end
+
+ it "sleeps with exponential backoff when base_delay is set" do
+ attempts = 0
+ sleep_times = []
+
+ allow_any_instance_of(Bundler::Retry).to receive(:sleep) do |_instance, delay|
+ sleep_times << delay
+ end
+
+ expect do
+ Bundler::Retry.new("test", [], 2, base_delay: 1.0, jitter: 0).attempt do
+ attempts += 1
+ raise "error"
+ end
+ end.to raise_error(StandardError)
+
+ expect(attempts).to eq(3)
+ expect(sleep_times.length).to eq(2)
+ # First retry: 1.0 * 2^0 = 1.0
+ expect(sleep_times[0]).to eq(1.0)
+ # Second retry: 1.0 * 2^1 = 2.0
+ expect(sleep_times[1]).to eq(2.0)
+ end
+
+ it "respects max_delay" do
+ sleep_times = []
+
+ allow_any_instance_of(Bundler::Retry).to receive(:sleep) do |_instance, delay|
+ sleep_times << delay
+ end
+
+ expect do
+ Bundler::Retry.new("test", [], 3, base_delay: 10.0, max_delay: 15.0, jitter: 0).attempt do
+ raise "error"
+ end
+ end.to raise_error(StandardError)
+
+ # First retry: 10.0 * 2^0 = 10.0
+ expect(sleep_times[0]).to eq(10.0)
+ # Second retry: 10.0 * 2^1 = 20.0, capped at 15.0
+ expect(sleep_times[1]).to eq(15.0)
+ # Third retry: 10.0 * 2^2 = 40.0, capped at 15.0
+ expect(sleep_times[2]).to eq(15.0)
+ end
+
+ it "adds jitter to delay" do
+ sleep_times = []
+
+ allow_any_instance_of(Bundler::Retry).to receive(:sleep) do |_instance, delay|
+ sleep_times << delay
+ end
+
+ expect do
+ Bundler::Retry.new("test", [], 2, base_delay: 1.0, jitter: 0.5).attempt do
+ raise "error"
+ end
+ end.to raise_error(StandardError)
+
+ expect(sleep_times.length).to eq(2)
+ # First retry should be between 1.0 and 1.5 (base + jitter)
+ expect(sleep_times[0]).to be >= 1.0
+ expect(sleep_times[0]).to be <= 1.5
+ # Second retry should be between 2.0 and 2.5
+ expect(sleep_times[1]).to be >= 2.0
+ expect(sleep_times[1]).to be <= 2.5
+ end
+ end
+end
diff --git a/spec/bundler/bundler/ruby_dsl_spec.rb b/spec/bundler/bundler/ruby_dsl_spec.rb
new file mode 100644
index 0000000000..45a37c5795
--- /dev/null
+++ b/spec/bundler/bundler/ruby_dsl_spec.rb
@@ -0,0 +1,248 @@
+# frozen_string_literal: true
+
+require "bundler/ruby_dsl"
+
+RSpec.describe Bundler::RubyDsl do
+ class MockDSL
+ include Bundler::RubyDsl
+
+ attr_reader :ruby_version
+ attr_accessor :gemfile
+ end
+
+ let(:dsl) { MockDSL.new }
+ let(:ruby_version) { "2.0.0" }
+ let(:ruby_version_arg) { ruby_version }
+ let(:version) { "2.0.0" }
+ let(:engine) { "jruby" }
+ let(:engine_version) { "9000" }
+ let(:patchlevel) { "100" }
+ let(:options) do
+ { patchlevel: patchlevel,
+ engine: engine,
+ engine_version: engine_version }
+ end
+ let(:project_root) { Pathname.new("/path/to/project") }
+ let(:gemfile) { project_root.join("Gemfile") }
+ before { allow(Bundler).to receive(:root).and_return(project_root) }
+
+ let(:invoke) do
+ proc do
+ args = []
+ args << ruby_version_arg if ruby_version_arg
+ args << options
+
+ dsl.ruby(*args)
+ end
+ end
+
+ subject do
+ dsl.gemfile = gemfile
+ invoke.call
+ dsl.ruby_version
+ end
+
+ describe "#ruby_version" do
+ shared_examples_for "it stores the ruby version" do
+ it "stores the version" do
+ expect(subject.versions).to eq(Array(ruby_version))
+ expect(subject.gem_version.version).to eq(version)
+ end
+
+ it "stores the engine details" do
+ expect(subject.engine).to eq(engine)
+ expect(subject.engine_versions).to eq(Array(engine_version))
+ end
+
+ it "stores the patchlevel" do
+ expect(subject.patchlevel).to eq(patchlevel)
+ end
+ end
+
+ context "with a plain version" do
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "with a single requirement" do
+ let(:ruby_version) { ">= 2.0.0" }
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "with a preview version" do
+ let(:ruby_version) { "3.3.0-preview2" }
+
+ it "stores the version" do
+ expect(subject.versions).to eq(Array("3.3.0.preview2"))
+ expect(subject.gem_version.version).to eq("3.3.0.preview2")
+ end
+ end
+
+ context "with two requirements in the same string" do
+ let(:ruby_version) { ">= 2.0.0, < 3.0" }
+ it "raises an error" do
+ expect { subject }.to raise_error(Bundler::InvalidArgumentError)
+ end
+ end
+
+ context "with two requirements" do
+ let(:ruby_version) { ["~> 2.0.0", "> 2.0.1"] }
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "with multiple engine versions" do
+ let(:engine_version) { ["> 200", "< 300"] }
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "with no options hash" do
+ let(:invoke) { proc { dsl.ruby(ruby_version) } }
+
+ let(:patchlevel) { nil }
+ let(:engine) { "ruby" }
+ let(:engine_version) { version }
+
+ it_behaves_like "it stores the ruby version"
+
+ context "and with multiple requirements" do
+ let(:ruby_version) { ["~> 2.0.0", "> 2.0.1"] }
+ let(:engine_version) { ruby_version }
+ it_behaves_like "it stores the ruby version"
+ end
+ end
+
+ context "with a file option" do
+ let(:file) { ".ruby-version" }
+ let(:ruby_version_file_path) { gemfile.dirname.join(file) }
+ let(:options) do
+ { file: file,
+ patchlevel: patchlevel,
+ engine: engine,
+ engine_version: engine_version }
+ end
+ let(:ruby_version_arg) { nil }
+ let(:file_content) { "#{version}\n" }
+
+ before do
+ allow(Bundler).to receive(:read_file) do |path|
+ raise Errno::ENOENT, <<~ERROR unless path == ruby_version_file_path
+ #{file} not found in specs:
+ expected: #{ruby_version_file_path}
+ received: #{path}
+ ERROR
+ file_content
+ end
+ end
+
+ it_behaves_like "it stores the ruby version"
+
+ context "with the Gemfile ruby file: path is relative to the Gemfile in a subdir" do
+ let(:gemfile) { project_root.join("subdir", "Gemfile") }
+ let(:file) { "../.ruby-version" }
+ let(:ruby_version_file_path) { gemfile.dirname.join(file) }
+
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "with bundler root in a subdir of the project" do
+ let(:project_root) { Pathname.new("/path/to/project/subdir") }
+ let(:gemfile) { project_root.parent.join("Gemfile") }
+
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "with the ruby- prefix in the file" do
+ let(:file_content) { "ruby-#{version}\n" }
+
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "and a version" do
+ let(:ruby_version_arg) { version }
+
+ it "raises an error" do
+ expect { subject }.to raise_error(Bundler::GemfileError, "Do not pass version argument when using :file option")
+ end
+ end
+
+ context "with a @gemset" do
+ let(:file_content) { "ruby-#{version}@gemset\n" }
+
+ it "raises an error" do
+ expect { subject }.to raise_error(Bundler::InvalidArgumentError, "2.0.0@gemset is not a valid requirement on the Ruby version")
+ end
+ end
+
+ context "with a mise.toml file format" do
+ let(:file) { "mise.toml" }
+ let(:ruby_version_arg) { nil }
+ let(:file_content) do
+ <<~TOML
+ [tools]
+ ruby = #{quote}#{version}#{quote}
+ TOML
+ end
+
+ context "with double quotes" do
+ let(:quote) { '"' }
+
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "with single quotes" do
+ let(:quote) { "'" }
+
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "with mismatched quotes" do
+ let(:file_content) do
+ <<~TOML
+ [tools]
+ ruby = "#{version}'
+ TOML
+ end
+
+ it "raises an error" do
+ expect { subject }.to raise_error(Bundler::InvalidArgumentError, "= is not a valid requirement on the Ruby version")
+ end
+ end
+ end
+
+ context "with a .tool-versions file format" do
+ let(:file) { ".tool-versions" }
+ let(:ruby_version_arg) { nil }
+ let(:file_content) do
+ <<~TOOLS
+ nodejs 18.16.0
+ ruby #{version} # This is a comment
+ pnpm 8.6.12
+ TOOLS
+ end
+
+ it_behaves_like "it stores the ruby version"
+
+ context "with extra spaces and a very cozy comment" do
+ let(:file_content) do
+ <<~TOOLS
+ nodejs 18.16.0
+ ruby #{version}# This is a cozy comment
+ pnpm 8.6.12
+ TOOLS
+ end
+
+ it_behaves_like "it stores the ruby version"
+ end
+ end
+
+ context "when the file does not exist" do
+ let(:ruby_version_file_path) { nil }
+ let(:ruby_version_arg) { nil }
+ let(:file) { "nonexistent.txt" }
+
+ it "raises an error" do
+ expect { subject }.to raise_error(Bundler::GemfileError, /Could not find version file nonexistent.txt/)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/ruby_version_spec.rb b/spec/bundler/bundler/ruby_version_spec.rb
new file mode 100644
index 0000000000..0d41ec9901
--- /dev/null
+++ b/spec/bundler/bundler/ruby_version_spec.rb
@@ -0,0 +1,500 @@
+# frozen_string_literal: true
+
+require "bundler/ruby_version"
+
+RSpec.describe "Bundler::RubyVersion and its subclasses" do
+ let(:version) { "2.0.0" }
+ let(:patchlevel) { "645" }
+ let(:engine) { "jruby" }
+ let(:engine_version) { "2.0.1" }
+
+ describe Bundler::RubyVersion do
+ subject { Bundler::RubyVersion.new(version, patchlevel, engine, engine_version) }
+
+ let(:ruby_version) { subject }
+ let(:other_version) { version }
+ let(:other_patchlevel) { patchlevel }
+ let(:other_engine) { engine }
+ let(:other_engine_version) { engine_version }
+ let(:other_ruby_version) { Bundler::RubyVersion.new(other_version, other_patchlevel, other_engine, other_engine_version) }
+
+ describe "#initialize" do
+ context "no engine is passed" do
+ let(:engine) { nil }
+
+ it "should set ruby as the engine" do
+ expect(subject.engine).to eq("ruby")
+ end
+ end
+
+ context "no engine_version is passed" do
+ let(:engine_version) { nil }
+
+ it "should set engine version as the passed version" do
+ expect(subject.engine_versions).to eq(["2.0.0"])
+ end
+ end
+
+ context "with engine in symbol" do
+ let(:engine) { :jruby }
+
+ it "should coerce engine to string" do
+ expect(subject.engine).to eq("jruby")
+ end
+ end
+
+ context "is called with multiple requirements" do
+ let(:version) { ["<= 2.0.0", "> 1.9.3"] }
+ let(:engine_version) { nil }
+
+ it "sets the versions" do
+ expect(subject.versions).to eq(version)
+ end
+
+ it "sets the engine versions" do
+ expect(subject.engine_versions).to eq(version)
+ end
+ end
+
+ context "is called with multiple engine requirements" do
+ let(:engine_version) { [">= 2.0", "< 2.3"] }
+
+ it "sets the engine versions" do
+ expect(subject.engine_versions).to eq(engine_version)
+ end
+ end
+ end
+
+ describe ".from_string" do
+ shared_examples_for "returning" do
+ it "returns the original RubyVersion" do
+ expect(described_class.from_string(subject.to_s)).to eq(subject)
+ end
+ end
+
+ include_examples "returning"
+
+ context "no patchlevel" do
+ let(:patchlevel) { nil }
+
+ include_examples "returning"
+ end
+
+ context "engine is ruby" do
+ let(:engine) { "ruby" }
+ let(:engine_version) { version }
+
+ include_examples "returning"
+ end
+
+ context "with multiple requirements" do
+ let(:engine_version) { ["> 9", "< 11"] }
+ let(:version) { ["> 8", "< 10"] }
+ let(:patchlevel) { nil }
+
+ it "returns nil" do
+ expect(described_class.from_string(subject.to_s)).to be_nil
+ end
+ end
+ end
+
+ describe "#to_s" do
+ it "should return info string with the ruby version, patchlevel, engine, and engine version" do
+ expect(subject.to_s).to eq("ruby 2.0.0 (jruby 2.0.1)")
+ end
+
+ context "no patchlevel" do
+ let(:patchlevel) { nil }
+
+ it "should return info string with the version, engine, and engine version" do
+ expect(subject.to_s).to eq("ruby 2.0.0 (jruby 2.0.1)")
+ end
+ end
+
+ context "engine is ruby" do
+ let(:engine) { "ruby" }
+
+ it "should return info string with the ruby version and patchlevel" do
+ expect(subject.to_s).to eq("ruby 2.0.0")
+ end
+ end
+
+ context "with multiple requirements" do
+ let(:engine_version) { ["> 9", "< 11"] }
+ let(:version) { ["> 8", "< 10"] }
+ let(:patchlevel) { nil }
+
+ it "should return info string with all requirements" do
+ expect(subject.to_s).to eq("ruby > 8, < 10 (jruby > 9, < 11)")
+ end
+ end
+ end
+
+ describe "#==" do
+ shared_examples_for "two ruby versions are not equal" do
+ it "should return false" do
+ expect(subject).to_not eq(other_ruby_version)
+ end
+ end
+
+ shared_examples_for "the versions, engines, and engine_versions match" do
+ it "should return true" do
+ expect(subject).to eq(other_ruby_version)
+ end
+ end
+
+ context "the versions do not match" do
+ let(:other_version) { "1.21.6" }
+
+ it_behaves_like "two ruby versions are not equal"
+ end
+
+ context "the patchlevels do not match" do
+ let(:other_patchlevel) { "21" }
+
+ it_behaves_like "the versions, engines, and engine_versions match"
+ end
+
+ context "the engines do not match" do
+ let(:other_engine) { "ruby" }
+
+ it_behaves_like "two ruby versions are not equal"
+ end
+
+ context "the engine versions do not match" do
+ let(:other_engine_version) { "1.11.2" }
+
+ it_behaves_like "two ruby versions are not equal"
+ end
+ end
+
+ describe "#host" do
+ before do
+ allow(RbConfig::CONFIG).to receive(:[]).with("host_cpu").and_return("x86_64")
+ allow(RbConfig::CONFIG).to receive(:[]).with("host_vendor").and_return("apple")
+ allow(RbConfig::CONFIG).to receive(:[]).with("host_os").and_return("darwin14.5.0")
+ end
+
+ it "should return an info string with the host cpu, vendor, and os" do
+ expect(subject.host).to eq("x86_64-apple-darwin14.5.0")
+ end
+
+ it "memoizes the info string with the host cpu, vendor, and os" do
+ expect(RbConfig::CONFIG).to receive(:[]).with("host_cpu").once.and_call_original
+ expect(RbConfig::CONFIG).to receive(:[]).with("host_vendor").once.and_call_original
+ expect(RbConfig::CONFIG).to receive(:[]).with("host_os").once.and_call_original
+ 2.times { ruby_version.host }
+ end
+ end
+
+ describe "#gem_version" do
+ let(:gem_version) { "2.0.0" }
+ let(:gem_version_obj) { Gem::Version.new(gem_version) }
+
+ shared_examples_for "it parses the version from the requirement string" do |version|
+ let(:version) { version }
+ it "should return the underlying version" do
+ expect(ruby_version.gem_version).to eq(gem_version_obj)
+ expect(ruby_version.gem_version.version).to eq(gem_version)
+ end
+ end
+
+ it_behaves_like "it parses the version from the requirement string", "2.0.0"
+ it_behaves_like "it parses the version from the requirement string", ">= 2.0.0"
+ it_behaves_like "it parses the version from the requirement string", "~> 2.0.0"
+ it_behaves_like "it parses the version from the requirement string", "< 2.0.0"
+ it_behaves_like "it parses the version from the requirement string", "= 2.0.0"
+ it_behaves_like "it parses the version from the requirement string", ["> 2.0.0", "< 2.4.5"]
+ end
+
+ describe "#diff" do
+ let(:engine) { "ruby" }
+
+ shared_examples_for "there is a difference in the engines" do
+ it "should return a tuple with :engine and the two different engines" do
+ expect(ruby_version.diff(other_ruby_version)).to eq([:engine, engine, other_engine])
+ end
+ end
+
+ shared_examples_for "there is a difference in the versions" do
+ it "should return a tuple with :version and the two different versions" do
+ expect(ruby_version.diff(other_ruby_version)).to eq([:version, Array(version).join(", "), Array(other_version).join(", ")])
+ end
+ end
+
+ shared_examples_for "there is a difference in the engine versions" do
+ it "should return a tuple with :engine_version and the two different engine versions" do
+ expect(ruby_version.diff(other_ruby_version)).to eq([:engine_version, Array(engine_version).join(", "), Array(other_engine_version).join(", ")])
+ end
+ end
+
+ shared_examples_for "even there is a difference in the patchlevels" do
+ it "should return nil" do
+ expect(ruby_version.diff(other_ruby_version)).to be_nil
+ end
+ end
+
+ shared_examples_for "there are no differences" do
+ it "should return nil" do
+ expect(ruby_version.diff(other_ruby_version)).to be_nil
+ end
+ end
+
+ context "all things match exactly" do
+ it_behaves_like "there are no differences"
+ end
+
+ context "detects engine discrepancies first" do
+ let(:other_version) { "2.0.1" }
+ let(:other_patchlevel) { "643" }
+ let(:other_engine) { "rbx" }
+ let(:other_engine_version) { "2.0.0" }
+
+ it_behaves_like "there is a difference in the engines"
+ end
+
+ context "detects version discrepancies second" do
+ let(:other_version) { "2.0.1" }
+ let(:other_patchlevel) { "643" }
+ let(:other_engine_version) { "2.0.0" }
+
+ it_behaves_like "there is a difference in the versions"
+ end
+
+ context "detects version discrepancies with multiple requirements second" do
+ let(:other_version) { "2.0.1" }
+ let(:other_patchlevel) { "643" }
+ let(:other_engine_version) { "2.0.0" }
+
+ let(:version) { ["> 2.0.0", "< 1.0.0"] }
+
+ it_behaves_like "there is a difference in the versions"
+ end
+
+ context "detects engine version discrepancies third" do
+ let(:other_patchlevel) { "643" }
+ let(:other_engine_version) { "2.0.0" }
+
+ it_behaves_like "there is a difference in the engine versions"
+ end
+
+ context "detects engine version discrepancies with multiple requirements third" do
+ let(:other_patchlevel) { "643" }
+ let(:other_engine_version) { "2.0.0" }
+
+ let(:engine_version) { ["> 2.0.0", "< 1.0.0"] }
+
+ it_behaves_like "there is a difference in the engine versions"
+ end
+
+ context "ignores patchlevel discrepancies last" do
+ let(:other_patchlevel) { "643" }
+
+ it_behaves_like "even there is a difference in the patchlevels"
+ end
+
+ context "successfully matches gem requirements" do
+ let(:version) { ">= 2.0.0" }
+ let(:patchlevel) { "< 643" }
+ let(:engine) { "ruby" }
+ let(:engine_version) { "~> 2.0.1" }
+ let(:other_version) { "2.0.0" }
+ let(:other_patchlevel) { "642" }
+ let(:other_engine) { "ruby" }
+ let(:other_engine_version) { "2.0.5" }
+
+ it_behaves_like "there are no differences"
+ end
+
+ context "successfully matches multiple gem requirements" do
+ let(:version) { [">= 2.0.0", "< 2.4.5"] }
+ let(:patchlevel) { "< 643" }
+ let(:engine) { "ruby" }
+ let(:engine_version) { ["~> 2.0.1", "< 2.4.5"] }
+ let(:other_version) { "2.0.0" }
+ let(:other_patchlevel) { "642" }
+ let(:other_engine) { "ruby" }
+ let(:other_engine_version) { "2.0.5" }
+
+ it_behaves_like "there are no differences"
+ end
+
+ context "successfully detects bad gem requirements with versions with multiple requirements" do
+ let(:version) { ["~> 2.0.0", "< 2.0.5"] }
+ let(:patchlevel) { "< 643" }
+ let(:engine) { "ruby" }
+ let(:engine_version) { "~> 2.0.1" }
+ let(:other_version) { "2.0.5" }
+ let(:other_patchlevel) { "642" }
+ let(:other_engine) { "ruby" }
+ let(:other_engine_version) { "2.0.5" }
+
+ it_behaves_like "there is a difference in the versions"
+ end
+
+ context "successfully detects bad gem requirements with versions" do
+ let(:version) { "~> 2.0.0" }
+ let(:patchlevel) { "< 643" }
+ let(:engine) { "ruby" }
+ let(:engine_version) { "~> 2.0.1" }
+ let(:other_version) { "2.1.0" }
+ let(:other_patchlevel) { "642" }
+ let(:other_engine) { "ruby" }
+ let(:other_engine_version) { "2.0.5" }
+
+ it_behaves_like "there is a difference in the versions"
+ end
+
+ context "successfully detects bad gem requirements with patchlevels" do
+ let(:version) { ">= 2.0.0" }
+ let(:patchlevel) { "< 643" }
+ let(:engine) { "ruby" }
+ let(:engine_version) { "~> 2.0.1" }
+ let(:other_version) { "2.0.0" }
+ let(:other_patchlevel) { "645" }
+ let(:other_engine) { "ruby" }
+ let(:other_engine_version) { "2.0.5" }
+
+ it_behaves_like "even there is a difference in the patchlevels"
+ end
+
+ context "successfully detects bad gem requirements with engine versions" do
+ let(:version) { ">= 2.0.0" }
+ let(:patchlevel) { "< 643" }
+ let(:engine) { "ruby" }
+ let(:engine_version) { "~> 2.0.1" }
+ let(:other_version) { "2.0.0" }
+ let(:other_patchlevel) { "642" }
+ let(:other_engine) { "ruby" }
+ let(:other_engine_version) { "2.1.0" }
+
+ it_behaves_like "there is a difference in the engine versions"
+ end
+
+ context "with a patchlevel of -1" do
+ let(:version) { ">= 2.0.0" }
+ let(:patchlevel) { "-1" }
+ let(:engine) { "ruby" }
+ let(:engine_version) { "~> 2.0.1" }
+ let(:other_version) { version }
+ let(:other_engine) { engine }
+ let(:other_engine_version) { engine_version }
+
+ context "and comparing with another patchlevel of -1" do
+ let(:other_patchlevel) { patchlevel }
+
+ it_behaves_like "there are no differences"
+ end
+
+ context "and comparing with a patchlevel that is not -1" do
+ let(:other_patchlevel) { "642" }
+
+ it_behaves_like "even there is a difference in the patchlevels"
+ end
+ end
+ end
+
+ describe "#system" do
+ subject { Bundler::RubyVersion.system }
+
+ let(:bundler_system_ruby_version) { subject }
+
+ around do |example|
+ if Bundler::RubyVersion.instance_variable_defined?("@system")
+ begin
+ old_ruby_version = Bundler::RubyVersion.instance_variable_get("@system")
+ Bundler::RubyVersion.remove_instance_variable("@system")
+ example.run
+ ensure
+ Bundler::RubyVersion.instance_variable_set("@system", old_ruby_version)
+ end
+ else
+ begin
+ example.run
+ ensure
+ Bundler::RubyVersion.remove_instance_variable("@system")
+ end
+ end
+ end
+
+ it "should return an instance of Bundler::RubyVersion" do
+ expect(subject).to be_kind_of(Bundler::RubyVersion)
+ end
+
+ it "memoizes the instance of Bundler::RubyVersion" do
+ expect(Bundler::RubyVersion).to receive(:new).once.and_call_original
+ 2.times { subject }
+ end
+
+ describe "#version" do
+ it "should return the value of Gem.ruby_version as a string" do
+ expect(subject.versions).to eq([Gem.ruby_version.to_s])
+ end
+ end
+
+ describe "#engine" do
+ before { stub_const("RUBY_ENGINE", "jruby") }
+ before { stub_const("RUBY_ENGINE_VERSION", "2.1.1") }
+
+ it "should return a copy of the value of RUBY_ENGINE" do
+ expect(subject.engine).to eq("jruby")
+ expect(subject.engine).to_not be(RUBY_ENGINE)
+ end
+ end
+
+ describe "#engine_version" do
+ context "engine is ruby" do
+ before do
+ allow(Gem).to receive(:ruby_version).and_return(Gem::Version.new("2.2.4"))
+ stub_const("RUBY_ENGINE", "ruby")
+ end
+
+ it "should return the value of Gem.ruby_version as a string" do
+ expect(bundler_system_ruby_version.engine_versions).to eq(["2.2.4"])
+ end
+ end
+
+ context "engine is rbx" do
+ before do
+ stub_const("RUBY_ENGINE", "rbx")
+ stub_const("RUBY_ENGINE_VERSION", "2.0.0")
+ end
+
+ it "should return a copy of the value of RUBY_ENGINE_VERSION" do
+ expect(bundler_system_ruby_version.engine_versions).to eq(["2.0.0"])
+ expect(bundler_system_ruby_version.engine_versions.first).to_not be(RUBY_ENGINE_VERSION)
+ end
+ end
+
+ context "engine is jruby" do
+ before do
+ stub_const("RUBY_ENGINE", "jruby")
+ stub_const("RUBY_ENGINE_VERSION", "2.1.1")
+ end
+
+ it "should return a copy of the value of RUBY_ENGINE_VERSION" do
+ expect(subject.engine_versions).to eq(["2.1.1"])
+ expect(bundler_system_ruby_version.engine_versions.first).to_not be(RUBY_ENGINE_VERSION)
+ end
+ end
+
+ context "engine is some other ruby engine" do
+ before do
+ stub_const("RUBY_ENGINE", "not_supported_ruby_engine")
+ stub_const("RUBY_ENGINE_VERSION", "1.2.3")
+ end
+
+ it "returns RUBY_ENGINE_VERSION" do
+ expect(bundler_system_ruby_version.engine_versions).to eq(["1.2.3"])
+ end
+ end
+ end
+
+ describe "#patchlevel" do
+ it "should return a string with the value of RUBY_PATCHLEVEL" do
+ expect(subject.patchlevel).to eq(RUBY_PATCHLEVEL.to_s)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/rubygems_ext_spec.rb b/spec/bundler/bundler/rubygems_ext_spec.rb
new file mode 100644
index 0000000000..0fc528f78c
--- /dev/null
+++ b/spec/bundler/bundler/rubygems_ext_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require "bundler/rubygems_ext"
+
+RSpec.describe Gem::SplitCompactIndexEntryOnFirstColon do
+ # Reproduces the RubyGems < 4.0.13 `Gem::Resolver::APISet::GemParser` that
+ # split each compact index entry on every colon, corrupting metadata values
+ # that themselves contain colons.
+ let(:legacy_parser_class) do
+ Class.new do
+ def parse_dependency(string)
+ dependency = string.split(":")
+ dependency[-1] = dependency[-1].split("&") if dependency.size > 1
+ dependency[0] = -dependency[0]
+ dependency
+ end
+ end
+ end
+
+ before { legacy_parser_class.prepend(described_class) }
+
+ it "preserves colon-bearing metadata values such as created_at timestamps" do
+ parser = legacy_parser_class.new
+
+ expect(parser.send(:parse_dependency, "created_at:2026-05-12T10:00:00Z")).to eq(["created_at", ["2026-05-12T10:00:00Z"]])
+ end
+
+ it "still parses ordinary name:requirement entries" do
+ parser = legacy_parser_class.new
+
+ expect(parser.send(:parse_dependency, "myrack:>= 1.0")).to eq(["myrack", [">= 1.0"]])
+ end
+
+ it "keeps parse_dependency private" do
+ parser = legacy_parser_class.new
+
+ expect { parser.parse_dependency("created_at:x") }.to raise_error(NoMethodError, /private method/)
+ end
+end
diff --git a/spec/bundler/bundler/rubygems_integration_spec.rb b/spec/bundler/bundler/rubygems_integration_spec.rb
new file mode 100644
index 0000000000..a2c63a7ca0
--- /dev/null
+++ b/spec/bundler/bundler/rubygems_integration_spec.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::RubygemsIntegration do
+ context "#validate" do
+ let(:spec) do
+ Gem::Specification.new do |s|
+ s.name = "to-validate"
+ s.version = "1.0.0"
+ s.loaded_from = __FILE__
+ end
+ end
+ subject { Bundler.rubygems.validate(spec) }
+
+ it "validates for resolution" do
+ expect(spec).to receive(:validate_for_resolution)
+ subject
+ end
+
+ context "with an invalid spec" do
+ before do
+ expect(spec).to receive(:validate_for_resolution).
+ and_raise(Gem::InvalidSpecificationException.new("TODO is not an author"))
+ end
+
+ it "should raise a Gem::InvalidSpecificationException and produce a helpful warning message" do
+ expect { subject }.to raise_error(Gem::InvalidSpecificationException,
+ "The gemspec at #{__FILE__} is not valid. "\
+ "Please fix this gemspec.\nThe validation error was 'TODO is not an author'\n")
+ end
+ end
+ end
+
+ describe "#download_gem" do
+ let(:bundler_retry) { double(Bundler::Retry) }
+ let(:cache_dir) { "#{Gem.path.first}/cache" }
+ let(:spec) do
+ spec = Gem::Specification.new("Foo", Gem::Version.new("2.5.2"))
+ spec.remote = Bundler::Source::Rubygems::Remote.new(uri.to_s)
+ spec
+ end
+ let(:fetcher) { double("gem_remote_fetcher") }
+
+ context "when uri is public" do
+ let(:uri) { Gem::URI.parse("https://foo.bar") }
+
+ it "successfully downloads gem with retries" do
+ expect(Bundler::Retry).to receive(:new).with("download gem from #{uri}/").
+ and_return(bundler_retry)
+ expect(bundler_retry).to receive(:attempts).and_yield
+ expect(fetcher).to receive(:cache_update_path)
+
+ Bundler.rubygems.download_gem(spec, uri, cache_dir, fetcher)
+ end
+ end
+
+ context "when uri contains userinfo part" do
+ let(:uri) { Gem::URI.parse("https://#{userinfo}@foo.bar") }
+
+ context "with user and password" do
+ let(:userinfo) { "user:password" }
+
+ it "successfully downloads gem with retries with filtered log" do
+ expect(Bundler::Retry).to receive(:new).with("download gem from https://user:REDACTED@foo.bar/").
+ and_return(bundler_retry)
+ expect(bundler_retry).to receive(:attempts).and_yield
+ expect(fetcher).to receive(:cache_update_path)
+
+ Bundler.rubygems.download_gem(spec, uri, cache_dir, fetcher)
+ end
+ end
+
+ context "with token [as user]" do
+ let(:userinfo) { "token" }
+
+ it "successfully downloads gem with retries with filtered log" do
+ expect(Bundler::Retry).to receive(:new).with("download gem from https://REDACTED@foo.bar/").
+ and_return(bundler_retry)
+ expect(bundler_retry).to receive(:attempts).and_yield
+ expect(fetcher).to receive(:cache_update_path)
+
+ Bundler.rubygems.download_gem(spec, uri, cache_dir, fetcher)
+ end
+ end
+ end
+ end
+
+ describe "#fetch_all_remote_specs" do
+ let(:uri) { "https://example.com" }
+ let(:fetcher) { double("gem_remote_fetcher") }
+ let(:specs_response) { Marshal.dump(["specs"]) }
+ let(:prerelease_specs_response) { Marshal.dump(["prerelease_specs"]) }
+
+ context "when a rubygems source mirror is set" do
+ let(:orig_uri) { Gem::URI("http://zombo.com") }
+ let(:remote_with_mirror) { double("remote", uri: uri, original_uri: orig_uri) }
+
+ it "sets the 'X-Gemfile-Source' header containing the original source" do
+ expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(specs_response)
+ expect(fetcher).to receive(:fetch_path).with(uri + "prerelease_specs.4.8.gz").and_return(prerelease_specs_response)
+ result = Bundler.rubygems.fetch_all_remote_specs(remote_with_mirror, fetcher)
+ expect(result).to eq(%w[specs prerelease_specs])
+ end
+ end
+
+ context "when there is no rubygems source mirror set" do
+ let(:remote_no_mirror) { double("remote", uri: uri, original_uri: nil) }
+
+ it "does not set the 'X-Gemfile-Source' header" do
+ expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(specs_response)
+ expect(fetcher).to receive(:fetch_path).with(uri + "prerelease_specs.4.8.gz").and_return(prerelease_specs_response)
+ result = Bundler.rubygems.fetch_all_remote_specs(remote_no_mirror, fetcher)
+ expect(result).to eq(%w[specs prerelease_specs])
+ end
+ end
+
+ context "when loading an unexpected class" do
+ let(:remote_no_mirror) { double("remote", uri: uri, original_uri: nil) }
+ let(:unexpected_specs_response) { Marshal.dump(3) }
+
+ it "raises a MarshalError error" do
+ expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(unexpected_specs_response)
+ expect { Bundler.rubygems.fetch_all_remote_specs(remote_no_mirror, fetcher) }.to raise_error(Bundler::MarshalError, /unexpected class/i)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/settings/validator_spec.rb b/spec/bundler/bundler/settings/validator_spec.rb
new file mode 100644
index 0000000000..b252ba59a0
--- /dev/null
+++ b/spec/bundler/bundler/settings/validator_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Settings::Validator do
+ describe ".validate!" do
+ def validate!(key, value, settings)
+ transformed_key = Bundler.settings.key_for(key)
+ if value.nil?
+ settings.delete(transformed_key)
+ else
+ settings[transformed_key] = value
+ end
+ described_class.validate!(key, value, settings)
+ settings
+ end
+
+ it "path and path.system are mutually exclusive" do
+ expect(validate!("path", "bundle", {})).to eq("BUNDLE_PATH" => "bundle")
+ expect(validate!("path", "bundle", "BUNDLE_PATH__SYSTEM" => false)).to eq("BUNDLE_PATH" => "bundle")
+ expect(validate!("path", "bundle", "BUNDLE_PATH__SYSTEM" => true)).to eq("BUNDLE_PATH" => "bundle")
+ expect(validate!("path", nil, "BUNDLE_PATH__SYSTEM" => true)).to eq("BUNDLE_PATH__SYSTEM" => true)
+ expect(validate!("path", nil, "BUNDLE_PATH__SYSTEM" => false)).to eq("BUNDLE_PATH__SYSTEM" => false)
+ expect(validate!("path", nil, {})).to eq({})
+
+ expect(validate!("path.system", true, "BUNDLE_PATH" => "bundle")).to eq("BUNDLE_PATH__SYSTEM" => true)
+ expect(validate!("path.system", false, "BUNDLE_PATH" => "bundle")).to eq("BUNDLE_PATH" => "bundle", "BUNDLE_PATH__SYSTEM" => false)
+ expect(validate!("path.system", nil, "BUNDLE_PATH" => "bundle")).to eq("BUNDLE_PATH" => "bundle")
+ expect(validate!("path.system", true, {})).to eq("BUNDLE_PATH__SYSTEM" => true)
+ expect(validate!("path.system", false, {})).to eq("BUNDLE_PATH__SYSTEM" => false)
+ expect(validate!("path.system", nil, {})).to eq({})
+ end
+
+ it "a group cannot be in both `with` & `without` simultaneously" do
+ expect do
+ validate!("with", "", {})
+ validate!("with", nil, {})
+ validate!("with", "", "BUNDLE_WITHOUT" => "a")
+ validate!("with", nil, "BUNDLE_WITHOUT" => "a")
+ validate!("with", "b:c", "BUNDLE_WITHOUT" => "a")
+
+ validate!("without", "", {})
+ validate!("without", nil, {})
+ validate!("without", "", "BUNDLE_WITH" => "a")
+ validate!("without", nil, "BUNDLE_WITH" => "a")
+ validate!("without", "b:c", "BUNDLE_WITH" => "a")
+ end.not_to raise_error
+
+ expect { validate!("with", "b:c", "BUNDLE_WITHOUT" => "c:d") }.to raise_error Bundler::InvalidOption, <<~EOS.strip
+ Setting `with` to "b:c" failed:
+ - a group cannot be in both `with` & `without` simultaneously
+ - `without` is current set to [:c, :d]
+ - the `c` groups conflict
+ EOS
+
+ expect { validate!("without", "b:c", "BUNDLE_WITH" => "c:d") }.to raise_error Bundler::InvalidOption, <<~EOS.strip
+ Setting `without` to "b:c" failed:
+ - a group cannot be in both `with` & `without` simultaneously
+ - `with` is current set to [:c, :d]
+ - the `c` groups conflict
+ EOS
+ end
+ end
+
+ describe described_class::Rule do
+ let(:keys) { %w[key] }
+ let(:description) { "rule description" }
+ let(:validate) { proc { raise "validate called!" } }
+ subject(:rule) { described_class.new(keys, description, &validate) }
+
+ describe "#validate!" do
+ it "calls the block" do
+ expect { rule.validate!("key", nil, {}) }.to raise_error(RuntimeError, /validate called!/)
+ end
+ end
+
+ describe "#fail!" do
+ it "raises with a helpful message" do
+ expect { subject.fail!("key", "value", "reason1", "reason2") }.to raise_error Bundler::InvalidOption, <<~EOS.strip
+ Setting `key` to "value" failed:
+ - rule description
+ - reason1
+ - reason2
+ EOS
+ end
+ end
+
+ describe "#set" do
+ it "works when the value has not changed" do
+ allow(Bundler.ui).to receive(:info).never
+
+ subject.set({}, "key", nil)
+ subject.set({ "BUNDLE_KEY" => "value" }, "key", "value")
+ end
+
+ it "prints out when the value is changing" do
+ settings = {}
+
+ expect(Bundler.ui).to receive(:info).with("Setting `key` to \"value\", since rule description, reason1")
+ subject.set(settings, "key", "value", "reason1")
+ expect(settings).to eq("BUNDLE_KEY" => "value")
+
+ expect(Bundler.ui).to receive(:info).with("Setting `key` to \"value2\", since rule description, reason2")
+ subject.set(settings, "key", "value2", "reason2")
+ expect(settings).to eq("BUNDLE_KEY" => "value2")
+
+ expect(Bundler.ui).to receive(:info).with("Setting `key` to nil, since rule description, reason3")
+ subject.set(settings, "key", nil, "reason3")
+ expect(settings).to eq({})
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/settings_spec.rb b/spec/bundler/bundler/settings_spec.rb
new file mode 100644
index 0000000000..5e1aaaa555
--- /dev/null
+++ b/spec/bundler/bundler/settings_spec.rb
@@ -0,0 +1,380 @@
+# frozen_string_literal: true
+
+require "bundler/settings"
+
+RSpec.describe Bundler::Settings do
+ subject(:settings) { described_class.new(bundled_app) }
+
+ describe "#set_local" do
+ context "root is nil" do
+ subject(:settings) { described_class.new(nil) }
+
+ before do
+ allow(Pathname).to receive(:new).and_call_original
+ allow(Pathname).to receive(:new).with(".bundle").and_return home(".bundle")
+ end
+
+ it "works" do
+ subject.set_local("foo", "bar")
+
+ expect(subject["foo"]).to eq("bar")
+ end
+ end
+ end
+
+ describe "load_config" do
+ let(:hash) do
+ {
+ "build.thrift" => "--with-cppflags=-D_FORTIFY_SOURCE=0",
+ "build.libv8" => "--with-system-v8",
+ "build.therubyracer" => "--with-v8-dir",
+ "build.pg" => "--with-pg-config=/usr/local/Cellar/postgresql92/9.2.8_1/bin/pg_config",
+ "gem.coc" => "false",
+ "gem.mit" => "false",
+ "gem.test" => "minitest",
+ "thingy" => <<-EOS.tr("\n", " "),
+--asdf --fdsa --ty=oh man i hope this doesn't break bundler because
+that would suck --ehhh=oh geez it looks like i might have broken bundler somehow
+--very-important-option=DontDeleteRoo
+--very-important-option=DontDeleteRoo
+--very-important-option=DontDeleteRoo
+--very-important-option=DontDeleteRoo
+ EOS
+ "xyz" => "zyx",
+ }
+ end
+
+ before do
+ hash.each do |key, value|
+ settings.set_local key, value
+ end
+ end
+
+ it "can load the config" do
+ loaded = settings.send(:load_config, bundled_app("config"))
+ expected = Hash[hash.map do |k, v|
+ [settings.send(:key_for, k), v.to_s]
+ end]
+ expect(loaded).to eq(expected)
+ end
+
+ context "when BUNDLE_IGNORE_CONFIG is set" do
+ before { ENV["BUNDLE_IGNORE_CONFIG"] = "TRUE" }
+
+ it "ignores the config" do
+ loaded = settings.send(:load_config, bundled_app("config"))
+ expect(loaded).to eq({})
+ end
+ end
+ end
+
+ describe "#global_config_file" do
+ context "when $HOME is not accessible" do
+ it "does not raise" do
+ expect(Bundler.rubygems).to receive(:user_home).twice.and_return(nil)
+
+ expect(subject.send(:global_config_file)).to be_nil
+ end
+ end
+ end
+
+ describe "#[]" do
+ context "when the local config file is not found" do
+ subject(:settings) { described_class.new }
+
+ it "does not raise" do
+ expect do
+ subject["foo"]
+ end.not_to raise_error
+ end
+ end
+
+ context "when not set" do
+ context "when default value present" do
+ it "retrieves value" do
+ expect(settings[:retry]).to be 3
+ end
+ end
+
+ it "returns nil" do
+ expect(settings[:buttermilk]).to be nil
+ end
+ end
+
+ context "when is boolean" do
+ it "returns a boolean" do
+ settings.set_local :frozen, "true"
+ expect(settings[:frozen]).to be true
+ end
+ context "when specific gem is configured" do
+ it "returns a boolean" do
+ settings.set_local "ignore_messages.foobar", "true"
+ expect(settings["ignore_messages.foobar"]).to be true
+ end
+ end
+ end
+
+ context "when is number" do
+ it "returns a number" do
+ settings.set_local :ssl_verify_mode, "1"
+ expect(settings[:ssl_verify_mode]).to be 1
+ end
+
+ it "coerces cooldown to integer" do
+ settings.set_local :cooldown, "7"
+ expect(settings[:cooldown]).to be 7
+ end
+ end
+
+ context "when it's not possible to create the settings directory" do
+ it "raises an PermissionError with explanation" do
+ settings_dir = settings.send(:local_config_file).dirname
+ expect(::Bundler::FileUtils).to receive(:mkdir_p).with(settings_dir).
+ and_raise(Errno::EACCES.new(settings_dir.to_s))
+ expect { settings.set_local :frozen, "1" }.
+ to raise_error(Bundler::PermissionError, /#{settings_dir}/)
+ end
+ end
+ end
+
+ describe "#temporary" do
+ it "reset after used" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+
+ Bundler.settings.set_command_option :no_install, true
+
+ Bundler.settings.temporary(no_install: false) do
+ expect(Bundler.settings[:no_install]).to eq false
+ end
+
+ expect(Bundler.settings[:no_install]).to eq true
+
+ Bundler.settings.set_command_option :no_install, nil
+ end
+
+ it "returns the return value of the block" do
+ ret = Bundler.settings.temporary({}) { :ret }
+ expect(ret).to eq :ret
+ end
+
+ context "when called without a block" do
+ it "leaves the setting changed" do
+ Bundler.settings.temporary(foo: :random)
+ expect(Bundler.settings[:foo]).to eq "random"
+ end
+
+ it "returns nil" do
+ expect(Bundler.settings.temporary(foo: :bar)).to be_nil
+ end
+ end
+ end
+
+ describe "#set_global" do
+ context "when it's not possible to write to create the settings directory" do
+ it "raises an PermissionError with explanation" do
+ settings_dir = settings.send(:global_config_file).dirname
+ expect(::Bundler::FileUtils).to receive(:mkdir_p).with(settings_dir).
+ and_raise(Errno::EACCES.new(settings_dir.to_s))
+ expect { settings.set_global(:frozen, "1") }.
+ to raise_error(Bundler::PermissionError, /#{settings_dir}/)
+ end
+ end
+ end
+
+ describe "#pretty_values_for" do
+ it "prints the converted value rather than the raw string" do
+ bool_key = described_class::BOOL_KEYS.first
+ settings.set_local(bool_key, "false")
+ expect(subject.pretty_values_for(bool_key)).to eq [
+ "Set for your local app (#{bundled_app("config")}): false",
+ ]
+ end
+ end
+
+ describe "#mirror_for" do
+ let(:uri) { Gem::URI("https://rubygems.org/") }
+
+ context "with no configured mirror" do
+ it "returns the original URI" do
+ expect(settings.mirror_for(uri)).to eq(uri)
+ end
+
+ it "converts a string parameter to a URI" do
+ expect(settings.mirror_for("https://rubygems.org/")).to eq(uri)
+ end
+ end
+
+ context "with a configured mirror" do
+ let(:mirror_uri) { Gem::URI("https://example-mirror.rubygems.org/") }
+
+ before { settings.set_local "mirror.https://rubygems.org/", mirror_uri.to_s }
+
+ it "returns the mirror URI" do
+ expect(settings.mirror_for(uri)).to eq(mirror_uri)
+ end
+
+ it "converts a string parameter to a URI" do
+ expect(settings.mirror_for("https://rubygems.org/")).to eq(mirror_uri)
+ end
+
+ it "normalizes the URI" do
+ expect(settings.mirror_for("https://rubygems.org")).to eq(mirror_uri)
+ end
+
+ it "is case insensitive" do
+ expect(settings.mirror_for("HTTPS://RUBYGEMS.ORG/")).to eq(mirror_uri)
+ end
+
+ context "with a file URI" do
+ let(:mirror_uri) { Gem::URI("file:/foo/BAR/baz/qUx/") }
+
+ it "returns the mirror URI" do
+ expect(settings.mirror_for(uri)).to eq(mirror_uri)
+ end
+
+ it "converts a string parameter to a URI" do
+ expect(settings.mirror_for("file:/foo/BAR/baz/qUx/")).to eq(mirror_uri)
+ end
+
+ it "normalizes the URI" do
+ expect(settings.mirror_for("file:/foo/BAR/baz/qUx")).to eq(mirror_uri)
+ end
+ end
+ end
+ end
+
+ describe "#credentials_for" do
+ let(:uri) { Gem::URI("https://gemserver.example.org/") }
+ let(:credentials) { "username:password" }
+
+ context "with no configured credentials" do
+ it "returns nil" do
+ expect(settings.credentials_for(uri)).to be_nil
+ end
+ end
+
+ context "with credentials configured by URL" do
+ before { settings.set_local "https://gemserver.example.org/", credentials }
+
+ it "returns the configured credentials" do
+ expect(settings.credentials_for(uri)).to eq(credentials)
+ end
+ end
+
+ context "with credentials configured by hostname" do
+ before { settings.set_local "gemserver.example.org", credentials }
+
+ it "returns the configured credentials" do
+ expect(settings.credentials_for(uri)).to eq(credentials)
+ end
+ end
+ end
+
+ describe "URI normalization" do
+ it "normalizes HTTP URIs in credentials configuration" do
+ settings.set_local "http://gemserver.example.org", "username:password"
+ expect(settings.all).to include("http://gemserver.example.org/")
+ end
+
+ it "normalizes HTTPS URIs in credentials configuration" do
+ settings.set_local "https://gemserver.example.org", "username:password"
+ expect(settings.all).to include("https://gemserver.example.org/")
+ end
+
+ it "normalizes HTTP URIs in mirror configuration" do
+ settings.set_local "mirror.http://rubygems.org", "http://example-mirror.rubygems.org"
+ expect(settings.all).to include("mirror.http://rubygems.org/")
+ end
+
+ it "normalizes HTTPS URIs in mirror configuration" do
+ settings.set_local "mirror.https://rubygems.org", "http://example-mirror.rubygems.org"
+ expect(settings.all).to include("mirror.https://rubygems.org/")
+ end
+
+ it "does not normalize other config keys that happen to contain 'http'" do
+ settings.set_local "local.httparty", home("httparty")
+ expect(settings.all).to include("local.httparty")
+ end
+
+ it "does not normalize other config keys that happen to contain 'https'" do
+ settings.set_local "local.httpsmarty", home("httpsmarty")
+ expect(settings.all).to include("local.httpsmarty")
+ end
+
+ it "reads older keys without trailing slashes" do
+ settings.set_local "mirror.https://rubygems.org", "http://example-mirror.rubygems.org"
+ expect(settings.mirror_for("https://rubygems.org/")).to eq(
+ Gem::URI("http://example-mirror.rubygems.org/")
+ )
+ end
+
+ it "normalizes URIs with a fallback_timeout option" do
+ settings.set_local "mirror.https://rubygems.org/.fallback_timeout", "true"
+ expect(settings.all).to include("mirror.https://rubygems.org/.fallback_timeout")
+ end
+
+ it "normalizes URIs with a fallback_timeout option without a trailing slash" do
+ settings.set_local "mirror.https://rubygems.org.fallback_timeout", "true"
+ expect(settings.all).to include("mirror.https://rubygems.org/.fallback_timeout")
+ end
+ end
+
+ describe "BUNDLE_ keys format" do
+ let(:settings) { described_class.new(bundled_app(".bundle")) }
+
+ it "converts older keys without double underscore" do
+ bundle_config("BUNDLE_MY__PERSONAL.MYRACK" => "~/Work/git/myrack")
+ expect(settings["my.personal.myrack"]).to eq("~/Work/git/myrack")
+ end
+
+ it "converts older keys without trailing slashes and double underscore" do
+ bundle_config("BUNDLE_MIRROR__HTTPS://RUBYGEMS.ORG" => "http://example-mirror.rubygems.org")
+ expect(settings["mirror.https://rubygems.org/"]).to eq("http://example-mirror.rubygems.org")
+ end
+
+ it "ignores commented out keys" do
+ create_file bundled_app(".bundle/config"), <<~C
+ # BUNDLE_MY-PERSONAL-SERVER__ORG: my-personal-server.org
+ C
+
+ expect(Bundler.ui).not_to receive(:warn)
+ expect(settings.all).to be_empty
+ end
+
+ it "converts older keys with dashes" do
+ bundle_config("BUNDLE_MY-PERSONAL-SERVER__ORG" => "my-personal-server.org")
+ expect(Bundler.ui).to receive(:warn).with(
+ "Your #{bundled_app(".bundle/config")} config includes `BUNDLE_MY-PERSONAL-SERVER__ORG`, which contains the dash character (`-`).\n" \
+ "This is deprecated, because configuration through `ENV` should be possible, but `ENV` keys cannot include dashes.\n" \
+ "Please edit #{bundled_app(".bundle/config")} and replace any dashes in configuration keys with a triple underscore (`___`)."
+ )
+ expect(settings["my-personal-server.org"]).to eq("my-personal-server.org")
+ end
+
+ it "reads newer keys format properly" do
+ bundle_config("BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/" => "http://example-mirror.rubygems.org")
+ expect(settings["mirror.https://rubygems.org/"]).to eq("http://example-mirror.rubygems.org")
+ end
+ end
+
+ describe "default_cli_command validation" do
+ it "accepts 'install' as a valid value" do
+ expect { settings.set_local("default_cli_command", "install") }.not_to raise_error
+ end
+
+ it "accepts 'cli_help' as a valid value" do
+ expect { settings.set_local("default_cli_command", "cli_help") }.not_to raise_error
+ end
+
+ it "rejects invalid values" do
+ expect { settings.set_local("default_cli_command", "invalid") }.to raise_error(
+ Bundler::InvalidOption,
+ /Setting `default_cli_command` to "invalid" failed:\n - default_cli_command must be either 'install' or 'cli_help'\n - must be one of: install, cli_help/
+ )
+ end
+
+ it "accepts nil values" do
+ expect { settings.set_local("default_cli_command", nil) }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/bundler/bundler/shared_helpers_spec.rb b/spec/bundler/bundler/shared_helpers_spec.rb
new file mode 100644
index 0000000000..41115aa667
--- /dev/null
+++ b/spec/bundler/bundler/shared_helpers_spec.rb
@@ -0,0 +1,518 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::SharedHelpers do
+ before do
+ pwd_stub
+ end
+
+ let(:pwd_stub) { allow(subject).to receive(:pwd).and_return(bundled_app) }
+
+ subject { Bundler::SharedHelpers }
+
+ describe "#default_gemfile" do
+ before { ENV["BUNDLE_GEMFILE"] = "/path/Gemfile" }
+
+ context "Gemfile is present" do
+ let(:expected_gemfile_path) { Pathname.new("/path/Gemfile").expand_path }
+
+ it "returns the Gemfile path" do
+ expect(subject.default_gemfile).to eq(expected_gemfile_path)
+ end
+ end
+
+ context "Gemfile is not present" do
+ before { ENV["BUNDLE_GEMFILE"] = nil }
+
+ it "raises a GemfileNotFound error" do
+ expect { subject.default_gemfile }.to raise_error(
+ Bundler::GemfileNotFound, "Could not locate Gemfile"
+ )
+ end
+ end
+
+ context "Gemfile is not an absolute path" do
+ before { ENV["BUNDLE_GEMFILE"] = "Gemfile" }
+
+ let(:expected_gemfile_path) { Pathname.new("Gemfile").expand_path }
+
+ it "returns the Gemfile path" do
+ expect(subject.default_gemfile).to eq(expected_gemfile_path)
+ end
+ end
+ end
+
+ describe "#default_lockfile" do
+ context "gemfile is gems.rb" do
+ let(:gemfile_path) { Pathname.new("/path/gems.rb") }
+ let(:expected_lockfile_path) { Pathname.new("/path/gems.locked") }
+
+ before { allow(subject).to receive(:default_gemfile).and_return(gemfile_path) }
+
+ it "returns the gems.locked path" do
+ expect(subject.default_lockfile).to eq(expected_lockfile_path)
+ end
+ end
+
+ context "is a regular Gemfile" do
+ let(:gemfile_path) { Pathname.new("/path/Gemfile") }
+ let(:expected_lockfile_path) { Pathname.new("/path/Gemfile.lock") }
+
+ before { allow(subject).to receive(:default_gemfile).and_return(gemfile_path) }
+
+ it "returns the lockfile path" do
+ expect(subject.default_lockfile).to eq(expected_lockfile_path)
+ end
+ end
+ end
+
+ describe "#default_bundle_dir" do
+ context ".bundle does not exist" do
+ it "returns nil" do
+ expect(subject.default_bundle_dir).to be_nil
+ end
+ end
+
+ context ".bundle is global .bundle" do
+ let(:global_rubygems_dir) { Pathname.new(bundled_app) }
+
+ before do
+ Dir.mkdir bundled_app(".bundle")
+ allow(Bundler.rubygems).to receive(:user_home).and_return(global_rubygems_dir)
+ end
+
+ it "returns nil" do
+ expect(subject.default_bundle_dir).to be_nil
+ end
+ end
+
+ context ".bundle is not global .bundle" do
+ let(:global_rubygems_dir) { Pathname.new("/path/rubygems") }
+ let(:expected_bundle_dir_path) { Pathname.new("#{bundled_app}/.bundle") }
+
+ before do
+ Dir.mkdir bundled_app(".bundle")
+ allow(Bundler.rubygems).to receive(:user_home).and_return(global_rubygems_dir)
+ end
+
+ it "returns the .bundle path" do
+ expect(subject.default_bundle_dir).to eq(expected_bundle_dir_path)
+ end
+ end
+ end
+
+ describe "#in_bundle?" do
+ it "calls the find_gemfile method" do
+ expect(subject).to receive(:find_gemfile)
+ subject.in_bundle?
+ end
+
+ shared_examples_for "correctly determines whether to return a Gemfile path" do
+ context "currently in directory with a Gemfile" do
+ before { FileUtils.touch(bundled_app_gemfile) }
+ after { FileUtils.rm(bundled_app_gemfile) }
+
+ it "returns path of the bundle Gemfile" do
+ expect(subject.in_bundle?).to eq("#{bundled_app}/Gemfile")
+ end
+ end
+
+ context "currently in directory without a Gemfile" do
+ it "returns nil" do
+ expect(subject.in_bundle?).to be_nil
+ end
+ end
+ end
+
+ context "ENV['BUNDLE_GEMFILE'] set" do
+ before { ENV["BUNDLE_GEMFILE"] = "/path/Gemfile" }
+
+ it "returns ENV['BUNDLE_GEMFILE']" do
+ expect(subject.in_bundle?).to eq("/path/Gemfile")
+ end
+ end
+
+ context "ENV['BUNDLE_GEMFILE'] not set" do
+ before { ENV["BUNDLE_GEMFILE"] = nil }
+
+ it_behaves_like "correctly determines whether to return a Gemfile path"
+ end
+
+ context "ENV['BUNDLE_GEMFILE'] is blank" do
+ before { ENV["BUNDLE_GEMFILE"] = "" }
+
+ it_behaves_like "correctly determines whether to return a Gemfile path"
+ end
+ end
+
+ describe "#chdir" do
+ let(:op_block) { proc { Dir.mkdir "nested_dir" } }
+
+ before { Dir.mkdir bundled_app("chdir_test_dir") }
+
+ it "executes the passed block while in the specified directory" do
+ subject.chdir(bundled_app("chdir_test_dir"), &op_block)
+ expect(bundled_app("chdir_test_dir/nested_dir")).to exist
+ end
+ end
+
+ describe "#pwd" do
+ let(:pwd_stub) { nil }
+
+ it "returns the current absolute path" do
+ expect(subject.pwd).to eq(git_root.to_s)
+ end
+ end
+
+ describe "#with_clean_git_env" do
+ let(:with_clean_git_env_block) { proc { Dir.mkdir bundled_app("with_clean_git_env_test_dir") } }
+
+ before do
+ ENV["GIT_DIR"] = "ORIGINAL_ENV_GIT_DIR"
+ ENV["GIT_WORK_TREE"] = "ORIGINAL_ENV_GIT_WORK_TREE"
+ end
+
+ it "executes the passed block" do
+ subject.with_clean_git_env(&with_clean_git_env_block)
+ expect(bundled_app("with_clean_git_env_test_dir")).to exist
+ end
+
+ context "when a block is passed" do
+ let(:with_clean_git_env_block) do
+ proc do
+ Dir.mkdir bundled_app("git_dir_test_dir") unless ENV["GIT_DIR"].nil?
+ Dir.mkdir bundled_app("git_work_tree_test_dir") unless ENV["GIT_WORK_TREE"].nil?
+ end end
+
+ it "uses a fresh git env for execution" do
+ subject.with_clean_git_env(&with_clean_git_env_block)
+ expect(bundled_app("git_dir_test_dir")).to_not exist
+ expect(bundled_app("git_work_tree_test_dir")).to_not exist
+ end
+ end
+
+ context "passed block does not throw errors" do
+ let(:with_clean_git_env_block) do
+ proc do
+ ENV["GIT_DIR"] = "NEW_ENV_GIT_DIR"
+ ENV["GIT_WORK_TREE"] = "NEW_ENV_GIT_WORK_TREE"
+ end end
+
+ it "restores the git env after" do
+ subject.with_clean_git_env(&with_clean_git_env_block)
+ expect(ENV["GIT_DIR"]).to eq("ORIGINAL_ENV_GIT_DIR")
+ expect(ENV["GIT_WORK_TREE"]).to eq("ORIGINAL_ENV_GIT_WORK_TREE")
+ end
+ end
+
+ context "passed block throws errors" do
+ let(:with_clean_git_env_block) do
+ proc do
+ ENV["GIT_DIR"] = "NEW_ENV_GIT_DIR"
+ ENV["GIT_WORK_TREE"] = "NEW_ENV_GIT_WORK_TREE"
+ raise RuntimeError.new
+ end end
+
+ it "restores the git env after" do
+ expect { subject.with_clean_git_env(&with_clean_git_env_block) }.to raise_error(RuntimeError)
+ expect(ENV["GIT_DIR"]).to eq("ORIGINAL_ENV_GIT_DIR")
+ expect(ENV["GIT_WORK_TREE"]).to eq("ORIGINAL_ENV_GIT_WORK_TREE")
+ end
+ end
+ end
+
+ describe "#set_bundle_environment" do
+ before do
+ ENV["BUNDLE_GEMFILE"] = "Gemfile"
+ end
+
+ shared_examples_for "ENV['PATH'] gets set correctly" do
+ before { Dir.mkdir bundled_app(".bundle") }
+
+ it "ensures bundle bin path is in ENV['PATH']" do
+ subject.set_bundle_environment
+ paths = ENV["PATH"].split(File::PATH_SEPARATOR)
+ expect(paths).to include("#{Bundler.bundle_path}/bin")
+ end
+ end
+
+ shared_examples_for "ENV['RUBYOPT'] gets set correctly" do
+ it "ensures -rbundler/setup is at the beginning of ENV['RUBYOPT']" do
+ subject.set_bundle_environment
+ expect(ENV["RUBYOPT"].split(" ")).to start_with("-r#{install_path}/bundler/setup")
+ end
+ end
+
+ shared_examples_for "ENV['BUNDLER_SETUP'] gets set correctly" do
+ it "ensures bundler/setup is set in ENV['BUNDLER_SETUP']" do
+ subject.set_bundle_environment
+ expect(ENV["BUNDLER_SETUP"]).to eq("#{source_lib_dir}/bundler/setup")
+ end
+ end
+
+ shared_examples_for "ENV['RUBYLIB'] gets set correctly" do
+ let(:ruby_lib_path) { "stubbed_ruby_lib_dir" }
+
+ before do
+ allow(subject).to receive(:bundler_ruby_lib).and_return(ruby_lib_path)
+ end
+
+ it "ensures bundler's ruby version lib path is in ENV['RUBYLIB']" do
+ subject.set_bundle_environment
+ expect(rubylib).to include(ruby_lib_path)
+ end
+ end
+
+ it "calls the appropriate set methods" do
+ expect(subject).to receive(:set_bundle_variables)
+ expect(subject).to receive(:set_path)
+ expect(subject).to receive(:set_rubyopt)
+ expect(subject).to receive(:set_rubylib)
+ subject.set_bundle_environment
+ end
+
+ it "ignores if bundler_ruby_lib is same as rubylibdir" do
+ allow(subject).to receive(:bundler_ruby_lib).and_return(RbConfig::CONFIG["rubylibdir"])
+
+ subject.set_bundle_environment
+
+ expect(rubylib.count(RbConfig::CONFIG["rubylibdir"])).to eq(0)
+ end
+
+ it "exits if bundle path contains the unix-like path separator" do
+ if Gem.respond_to?(:path_separator)
+ allow(Gem).to receive(:path_separator).and_return(":")
+ else
+ stub_const("File::PATH_SEPARATOR", ":")
+ end
+ allow(Bundler).to receive(:bundle_path) { Pathname.new("so:me/dir/bin") }
+ expect { subject.send(:validate_bundle_path) }.to raise_error(
+ Bundler::PathError,
+ "Your bundle path contains text matching \":\", which is the " \
+ "path separator for your system. Bundler cannot " \
+ "function correctly when the Bundle path contains the " \
+ "system's PATH separator. Please change your " \
+ "bundle path to not match \":\".\nYour current bundle " \
+ "path is '#{Bundler.bundle_path}'."
+ )
+ end
+
+ context "with a jruby path_separator regex" do
+ # In versions of jruby that supported ruby 1.8, the path separator was the standard File::PATH_SEPARATOR
+ let(:regex) { Regexp.new("(?<!jar:file|jar|file|classpath|uri:classloader|uri|http|https):") }
+ it "does not exit if bundle path is the standard uri path" do
+ allow(Bundler.rubygems).to receive(:path_separator).and_return(regex)
+ allow(Bundler).to receive(:bundle_path) { Pathname.new("uri:classloader:/WEB-INF/gems") }
+ expect { subject.send(:validate_bundle_path) }.not_to raise_error
+ end
+
+ it "exits if bundle path contains another directory" do
+ allow(Bundler.rubygems).to receive(:path_separator).and_return(regex)
+ allow(Bundler).to receive(:bundle_path) {
+ Pathname.new("uri:classloader:/WEB-INF/gems:other/dir")
+ }
+
+ expect { subject.send(:validate_bundle_path) }.to raise_error(
+ Bundler::PathError,
+ "Your bundle path contains text matching " \
+ "/(?<!jar:file|jar|file|classpath|uri:classloader|uri|http|https):/, which is the " \
+ "path separator for your system. Bundler cannot " \
+ "function correctly when the Bundle path contains the " \
+ "system's PATH separator. Please change your " \
+ "bundle path to not match " \
+ "/(?<!jar:file|jar|file|classpath|uri:classloader|uri|http|https):/." \
+ "\nYour current bundle path is '#{Bundler.bundle_path}'."
+ )
+ end
+ end
+
+ context "ENV['PATH'] does not exist" do
+ before { ENV.delete("PATH") }
+
+ it_behaves_like "ENV['PATH'] gets set correctly"
+ end
+
+ context "ENV['PATH'] is empty" do
+ before { ENV["PATH"] = "" }
+
+ it_behaves_like "ENV['PATH'] gets set correctly"
+ end
+
+ context "ENV['PATH'] exists" do
+ before { ENV["PATH"] = "/some_path/bin" }
+
+ it_behaves_like "ENV['PATH'] gets set correctly"
+ end
+
+ context "ENV['PATH'] already contains the bundle bin path" do
+ let(:bundle_path) { "#{Bundler.bundle_path}/bin" }
+
+ before do
+ ENV["PATH"] = bundle_path
+ end
+
+ it_behaves_like "ENV['PATH'] gets set correctly"
+
+ it "ENV['PATH'] should only contain one instance of bundle bin path" do
+ subject.set_bundle_environment
+ paths = ENV["PATH"].split(File::PATH_SEPARATOR)
+ expect(paths.count(bundle_path)).to eq(1)
+ end
+ end
+
+ context "when bundler install path is standard" do
+ let(:install_path) { source_lib_dir }
+
+ context "ENV['RUBYOPT'] does not exist" do
+ before { ENV.delete("RUBYOPT") }
+
+ it_behaves_like "ENV['RUBYOPT'] gets set correctly"
+ end
+
+ context "ENV['RUBYOPT'] exists without -rbundler/setup" do
+ before { ENV["RUBYOPT"] = "-I/some_app_path/lib" }
+
+ it_behaves_like "ENV['RUBYOPT'] gets set correctly"
+ end
+
+ context "ENV['RUBYOPT'] exists and contains -rbundler/setup" do
+ before { ENV["RUBYOPT"] = "-rbundler/setup" }
+
+ it_behaves_like "ENV['RUBYOPT'] gets set correctly"
+ end
+ end
+
+ context "when bundler install path contains special characters" do
+ let(:install_path) { "/opt/ruby3.3.0-preview2/lib/ruby/3.3.0+0" }
+
+ before do
+ ENV["RUBYOPT"] = "-r#{install_path}/bundler/setup"
+ allow(File).to receive(:expand_path).and_return("#{install_path}/bundler/setup")
+ allow(Gem).to receive(:bin_path).and_return("#{install_path}/bundler/setup")
+ end
+
+ it "ensures -rbundler/setup is not duplicated" do
+ subject.set_bundle_environment
+ expect(ENV["RUBYOPT"].split(" ").grep(%r{-r.*/bundler/setup}).length).to eq(1)
+ end
+
+ it_behaves_like "ENV['RUBYOPT'] gets set correctly"
+ end
+
+ context "ENV['RUBYLIB'] does not exist" do
+ before { ENV.delete("RUBYLIB") }
+
+ it_behaves_like "ENV['RUBYLIB'] gets set correctly"
+ end
+
+ context "ENV['RUBYLIB'] is empty" do
+ before { ENV["PATH"] = "" }
+
+ it_behaves_like "ENV['RUBYLIB'] gets set correctly"
+ end
+
+ context "ENV['RUBYLIB'] exists" do
+ before { ENV["PATH"] = "/some_path/bin" }
+
+ it_behaves_like "ENV['RUBYLIB'] gets set correctly"
+ end
+
+ context "bundle executable in ENV['BUNDLE_BIN_PATH'] does not exist" do
+ before { ENV["BUNDLE_BIN_PATH"] = "/does/not/exist" }
+ before { Bundler.rubygems.replace_bin_path [] }
+
+ it "sets BUNDLE_BIN_PATH to the bundle executable file" do
+ subject.set_bundle_environment
+ bin_path = ENV["BUNDLE_BIN_PATH"]
+ expect(bin_path).to eq(exedir.join("bundle").to_s)
+ expect(File.exist?(bin_path)).to be true
+ end
+ end
+
+ context "ENV['RUBYLIB'] already contains the bundler's ruby version lib path" do
+ let(:ruby_lib_path) { "stubbed_ruby_lib_dir" }
+
+ before do
+ ENV["RUBYLIB"] = ruby_lib_path
+ end
+
+ it_behaves_like "ENV['RUBYLIB'] gets set correctly"
+
+ it "ENV['RUBYLIB'] should only contain one instance of bundler's ruby version lib path" do
+ subject.set_bundle_environment
+ expect(rubylib.count(ruby_lib_path)).to eq(1)
+ end
+ end
+ end
+
+ describe "#filesystem_access" do
+ context "system has proper permission access" do
+ let(:file_op_block) { proc {|path| FileUtils.mkdir_p(path) } }
+
+ it "performs the operation in the passed block" do
+ subject.filesystem_access(bundled_app("test_dir"), &file_op_block)
+ expect(bundled_app("test_dir")).to exist
+ end
+ end
+
+ context "system throws Errno::EACESS" do
+ let(:file_op_block) { proc {|_path| raise Errno::EACCES.new("/path") } }
+
+ it "raises a PermissionError" do
+ expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error(
+ Bundler::PermissionError
+ )
+ end
+ end
+
+ context "system throws Errno::EAGAIN" do
+ let(:file_op_block) { proc {|_path| raise Errno::EAGAIN } }
+
+ it "raises a TemporaryResourceError" do
+ expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error(
+ Bundler::TemporaryResourceError
+ )
+ end
+ end
+
+ context "system throws Errno::EPROTO" do
+ let(:file_op_block) { proc {|_path| raise Errno::EPROTO } }
+
+ it "raises a VirtualProtocolError" do
+ expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error(
+ Bundler::VirtualProtocolError
+ )
+ end
+ end
+
+ context "system throws Errno::ENOTSUP" do
+ let(:file_op_block) { proc {|_path| raise Errno::ENOTSUP } }
+
+ it "raises a OperationNotSupportedError" do
+ expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error(
+ Bundler::OperationNotSupportedError
+ )
+ end
+ end
+
+ context "system throws Errno::ENOSPC" do
+ let(:file_op_block) { proc {|_path| raise Errno::ENOSPC } }
+
+ it "raises a NoSpaceOnDeviceError" do
+ expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error(
+ Bundler::NoSpaceOnDeviceError
+ )
+ end
+ end
+
+ context "system throws an unhandled SystemCallError" do
+ let(:error) { SystemCallError.new("Shields down", 1337) }
+ let(:file_op_block) { proc {|_path| raise error } }
+
+ it "raises a GenericSystemCallError" do
+ expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error(
+ Bundler::GenericSystemCallError, /error creating.+underlying.+Shields down/m
+ )
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/source/git/git_proxy_spec.rb b/spec/bundler/bundler/source/git/git_proxy_spec.rb
new file mode 100644
index 0000000000..1f10ca4b07
--- /dev/null
+++ b/spec/bundler/bundler/source/git/git_proxy_spec.rb
@@ -0,0 +1,354 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Source::Git::GitProxy do
+ let(:path) { Pathname("path") }
+ let(:uri) { "https://github.com/ruby/rubygems.git" }
+ let(:ref) { nil }
+ let(:branch) { nil }
+ let(:tag) { nil }
+ let(:options) { { "ref" => ref, "branch" => branch, "tag" => tag }.compact }
+ let(:revision) { nil }
+ let(:git_source) { nil }
+ let(:clone_result) { double(Process::Status, success?: true) }
+ let(:fail_result) { double(Process::Status, success?: false) }
+ let(:base_clone_args) { ["clone", "--bare", "--no-hardlinks", "--quiet", "--no-tags", "--depth", "1", "--single-branch"] }
+ let(:base_fetch_args) { ["fetch", "--force", "--quiet", "--no-tags", "--depth", "1"] }
+ subject(:git_proxy) { described_class.new(path, uri, options, revision, git_source) }
+
+ context "with explicit ref" do
+ context "with branch only" do
+ let(:branch) { "main" }
+ it "sets explicit ref to branch" do
+ expect(git_proxy.explicit_ref).to eq(branch)
+ end
+ end
+
+ context "with ref only" do
+ let(:ref) { "HEAD" }
+ it "sets explicit ref to ref" do
+ expect(git_proxy.explicit_ref).to eq(ref)
+ end
+ end
+
+ context "with tag only" do
+ let(:tag) { "v1.0" }
+ it "sets explicit ref to ref" do
+ expect(git_proxy.explicit_ref).to eq(tag)
+ end
+ end
+
+ context "with tag and branch" do
+ let(:tag) { "v1.0" }
+ let(:branch) { "main" }
+ it "raises error" do
+ expect { git_proxy }.to raise_error(Bundler::Source::Git::AmbiguousGitReference)
+ end
+ end
+
+ context "with tag and ref" do
+ let(:tag) { "v1.0" }
+ let(:ref) { "HEAD" }
+ it "raises error" do
+ expect { git_proxy }.to raise_error(Bundler::Source::Git::AmbiguousGitReference)
+ end
+ end
+
+ context "with branch and ref" do
+ let(:branch) { "main" }
+ let(:ref) { "HEAD" }
+ it "honors ref over branch" do
+ expect(git_proxy.explicit_ref).to eq(ref)
+ end
+ end
+ end
+
+ context "with configured credentials" do
+ it "adds username and password to URI" do
+ Bundler.settings.temporary(uri => "u:p") do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", "https://u:p@github.com/ruby/rubygems.git", path.to_s], nil).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+
+ it "adds username and password to URI for host" do
+ Bundler.settings.temporary("github.com" => "u:p") do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", "https://u:p@github.com/ruby/rubygems.git", path.to_s], nil).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+
+ it "does not add username and password to mismatched URI" do
+ Bundler.settings.temporary("https://u:p@github.com/ruby/rubygems-mismatch.git" => "u:p") do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", uri, path.to_s], nil).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+
+ it "keeps original userinfo" do
+ Bundler.settings.temporary("github.com" => "u:p") do
+ original = "https://orig:info@github.com/ruby/rubygems.git"
+ git_proxy = described_class.new(Pathname("path"), original, options)
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", original, path.to_s], nil).and_return(["", "", clone_result])
+ git_proxy.checkout
+ end
+ end
+ end
+
+ describe "#version" do
+ context "with a normal version number" do
+ before do
+ expect(described_class).to receive(:full_version).
+ and_return("git version 1.2.3")
+ end
+
+ it "returns the git version number" do
+ expect(git_proxy.version).to eq("1.2.3")
+ end
+
+ it "does not raise an error when passed into Gem::Version.create" do
+ expect { Gem::Version.create git_proxy.version }.not_to raise_error
+ end
+ end
+
+ context "with a OSX version number" do
+ before do
+ expect(described_class).to receive(:full_version).
+ and_return("git version 1.2.3 (Apple Git-BS)")
+ end
+
+ it "strips out OSX specific additions in the version string" do
+ expect(git_proxy.version).to eq("1.2.3")
+ end
+
+ it "does not raise an error when passed into Gem::Version.create" do
+ expect { Gem::Version.create git_proxy.version }.not_to raise_error
+ end
+ end
+
+ context "with a msysgit version number" do
+ before do
+ expect(described_class).to receive(:full_version).
+ and_return("git version 1.2.3.msysgit.0")
+ end
+
+ it "strips out msysgit specific additions in the version string" do
+ expect(git_proxy.version).to eq("1.2.3")
+ end
+
+ it "does not raise an error when passed into Gem::Version.create" do
+ expect { Gem::Version.create git_proxy.version }.not_to raise_error
+ end
+ end
+ end
+
+ describe "#full_version" do
+ context "with a normal version number" do
+ before do
+ status = double("success?" => true)
+ expect(Open3).to receive(:capture3).with("git", "--version").
+ and_return(["git version 1.2.3", "", status])
+ end
+
+ it "returns the git version number" do
+ expect(git_proxy.full_version).to eq("1.2.3")
+ end
+ end
+
+ context "with a OSX version number" do
+ before do
+ status = double("success?" => true)
+ expect(Open3).to receive(:capture3).with("git", "--version").
+ and_return(["git version 1.2.3 (Apple Git-BS)", "", status])
+ end
+
+ it "does not strip out OSX specific additions in the version string" do
+ expect(git_proxy.full_version).to eq("1.2.3 (Apple Git-BS)")
+ end
+ end
+
+ context "with a msysgit version number" do
+ before do
+ status = double("success?" => true)
+ expect(Open3).to receive(:capture3).with("git", "--version").
+ and_return(["git version 1.2.3.msysgit.0", "", status])
+ end
+
+ it "does not strip out msysgit specific additions in the version string" do
+ expect(git_proxy.full_version).to eq("1.2.3.msysgit.0")
+ end
+ end
+ end
+
+ it "doesn't allow arbitrary code execution through Gemfile uris with a leading dash" do
+ gemfile <<~G
+ gem "poc", git: "-u./pay:load.sh"
+ G
+
+ file = bundled_app("pay:load.sh")
+
+ create_file file, <<~RUBY
+ #!/bin/sh
+
+ touch #{bundled_app("canary")}
+ RUBY
+
+ FileUtils.chmod("+x", file)
+
+ bundle :lock, raise_on_error: false
+
+ expect(Pathname.new(bundled_app("canary"))).not_to exist
+ end
+
+ context "URI is HTTP" do
+ let(:uri) { "http://github.com/ruby/rubygems.git" }
+ let(:clone_args_without_depth) { ["clone", "--bare", "--no-hardlinks", "--quiet", "--no-tags", "--single-branch"] }
+
+ it "retries clone without --depth when dumb http transport fails" do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", uri, path.to_s], nil).and_return(["", "dumb http transport does not support shallow capabilities", fail_result])
+ expect(git_proxy).to receive(:capture).with([*clone_args_without_depth, "--", uri, path.to_s], nil).and_return(["", "", clone_result])
+
+ subject.checkout
+ end
+ end
+
+ describe "#installed_to?" do
+ let(:destination) { "install/dir" }
+ let(:destination_dir_exists) { true }
+ let(:children) { ["gem.gemspec", "README.me", ".git", "Rakefile"] }
+
+ before do
+ allow(Dir).to receive(:exist?).with(destination).and_return(destination_dir_exists)
+ allow(Dir).to receive(:children).with(destination).and_return(children)
+ end
+
+ context "when destination dir exists with children other than just .git" do
+ it "returns true" do
+ expect(git_proxy.installed_to?(destination)).to be true
+ end
+ end
+
+ context "when destination dir does not exist" do
+ let(:destination_dir_exists) { false }
+
+ it "returns false" do
+ expect(git_proxy.installed_to?(destination)).to be false
+ end
+ end
+
+ context "when destination dir is empty" do
+ let(:children) { [] }
+
+ it "returns false" do
+ expect(git_proxy.installed_to?(destination)).to be false
+ end
+ end
+
+ context "when destination dir has only .git directory" do
+ let(:children) { [".git"] }
+
+ it "returns false" do
+ expect(git_proxy.installed_to?(destination)).to be false
+ end
+ end
+ end
+
+ describe "#checkout" do
+ context "when the repository isn't cloned" do
+ before do
+ allow(path).to receive(:exist?).and_return(false)
+ end
+
+ it "clones the repository" do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", uri, path.to_s], nil).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+
+ context "when the repository is cloned" do
+ before do
+ allow(path).to receive(:exist?).and_return(true)
+ end
+
+ context "with a locked revision" do
+ let(:revision) { Digest::SHA1.hexdigest("ruby") }
+
+ context "when the revision exists locally" do
+ it "uses the cached revision" do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:git).with("cat-file", "-e", revision, dir: path).and_return(true)
+ subject.checkout
+ end
+ end
+
+ context "when the revision doesn't exist locally" do
+ it "fetches the specific revision" do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:git).with("cat-file", "-e", revision, dir: path).and_raise(Bundler::GitError)
+ expect(git_proxy).to receive(:capture).with(["fetch", "--force", "--quiet", "--no-tags", "--depth", "1", "--", uri, "#{revision}:refs/#{revision}-sha"], path).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+ end
+
+ context "with no explicit ref" do
+ it "fetches the HEAD revision" do
+ parsed_revision = Digest::SHA1.hexdigest("ruby")
+ allow(git_proxy).to receive(:git_local).with("rev-parse", "--abbrev-ref", "HEAD", dir: path).and_return(parsed_revision)
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with(["fetch", "--force", "--quiet", "--no-tags", "--depth", "1", "--", uri, "refs/heads/#{parsed_revision}:refs/heads/#{parsed_revision}"], path).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+
+ context "with a commit ref" do
+ let(:ref) { Digest::SHA1.hexdigest("ruby") }
+
+ context "when the revision exists locally" do
+ it "uses the cached revision" do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:git).with("cat-file", "-e", ref, dir: path).and_return(true)
+ subject.checkout
+ end
+ end
+
+ context "when the revision doesn't exist locally" do
+ it "fetches the specific revision" do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:git).with("cat-file", "-e", ref, dir: path).and_raise(Bundler::GitError)
+ expect(git_proxy).to receive(:capture).with(["fetch", "--force", "--quiet", "--no-tags", "--depth", "1", "--", uri, "#{ref}:refs/#{ref}-sha"], path).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+ end
+
+ context "with a non-commit ref" do
+ let(:ref) { "HEAD" }
+
+ it "fetches all revisions" do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with(["fetch", "--force", "--quiet", "--no-tags", "--", uri, "refs/*:refs/*"], path).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+
+ context "URI is HTTP" do
+ let(:uri) { "http://github.com/ruby/rubygems.git" }
+
+ it "retries fetch without --depth when dumb http transport fails" do
+ parsed_revision = Digest::SHA1.hexdigest("ruby")
+ allow(git_proxy).to receive(:git_local).with("rev-parse", "--abbrev-ref", "HEAD", dir: path).and_return(parsed_revision)
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with([*base_fetch_args, "--", uri, "refs/heads/#{parsed_revision}:refs/heads/#{parsed_revision}"], path).and_return(["", "dumb http transport does not support shallow capabilities", fail_result])
+ expect(git_proxy).to receive(:capture).with(["fetch", "--force", "--quiet", "--no-tags", "--", uri, "refs/heads/#{parsed_revision}:refs/heads/#{parsed_revision}"], path).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/source/git_spec.rb b/spec/bundler/bundler/source/git_spec.rb
new file mode 100644
index 0000000000..14e91c6bdc
--- /dev/null
+++ b/spec/bundler/bundler/source/git_spec.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Source::Git do
+ before do
+ allow(Bundler).to receive(:root) { Pathname.new("root") }
+ end
+
+ let(:uri) { "https://github.com/foo/bar.git" }
+ let(:options) do
+ { "uri" => uri }
+ end
+
+ subject { described_class.new(options) }
+
+ describe "#to_s" do
+ it "returns a description" do
+ expect(subject.to_s).to eq "https://github.com/foo/bar.git"
+ end
+
+ context "when the URI contains credentials" do
+ let(:uri) { "https://my-secret-token:x-oauth-basic@github.com/foo/bar.git" }
+
+ it "filters credentials" do
+ expect(subject.to_s).to eq "https://x-oauth-basic@github.com/foo/bar.git"
+ end
+ end
+
+ context "when the source has a glob specifier" do
+ let(:glob) { "bar/baz/*.gemspec" }
+ let(:options) do
+ { "uri" => uri, "glob" => glob }
+ end
+
+ it "includes it" do
+ expect(subject.to_s).to eq "https://github.com/foo/bar.git (glob: bar/baz/*.gemspec)"
+ end
+ end
+
+ context "when the source has a reference" do
+ let(:git_proxy_stub) do
+ instance_double(Bundler::Source::Git::GitProxy, revision: "123abc", branch: "v1.0.0")
+ end
+ let(:options) do
+ { "uri" => uri, "ref" => "v1.0.0" }
+ end
+
+ before do
+ allow(Bundler::Source::Git::GitProxy).to receive(:new).and_return(git_proxy_stub)
+ end
+
+ it "includes it" do
+ expect(subject.to_s).to eq "https://github.com/foo/bar.git (at v1.0.0@123abc)"
+ end
+ end
+
+ context "when the source has both reference and glob specifiers" do
+ let(:git_proxy_stub) do
+ instance_double(Bundler::Source::Git::GitProxy, revision: "123abc", branch: "v1.0.0")
+ end
+ let(:options) do
+ { "uri" => uri, "ref" => "v1.0.0", "glob" => "gems/foo/*.gemspec" }
+ end
+
+ before do
+ allow(Bundler::Source::Git::GitProxy).to receive(:new).and_return(git_proxy_stub)
+ end
+
+ it "includes both" do
+ expect(subject.to_s).to eq "https://github.com/foo/bar.git (at v1.0.0@123abc, glob: gems/foo/*.gemspec)"
+ end
+ end
+ end
+
+ describe "#locked_revision_checked_out?" do
+ let(:revision) { "abc" }
+ let(:git_proxy_revision) { revision }
+ let(:git_proxy_installed) { true }
+ let(:git_proxy) { subject.send(:git_proxy) }
+ let(:options) do
+ {
+ "uri" => uri,
+ "revision" => revision,
+ }
+ end
+
+ before do
+ allow(git_proxy).to receive(:revision).and_return(git_proxy_revision)
+ allow(git_proxy).to receive(:installed_to?).with(subject.install_path).and_return(git_proxy_installed)
+ end
+
+ context "when the locked revision is checked out" do
+ it "returns true" do
+ expect(subject.send(:locked_revision_checked_out?)).to be true
+ end
+ end
+
+ context "when no revision is provided" do
+ let(:options) do
+ { "uri" => uri }
+ end
+
+ it "returns falsey value" do
+ expect(subject.send(:locked_revision_checked_out?)).to be_falsey
+ end
+ end
+
+ context "when the git proxy revision is different than the git revision" do
+ let(:git_proxy_revision) { revision.next }
+
+ it "returns falsey value" do
+ expect(subject.send(:locked_revision_checked_out?)).to be_falsey
+ end
+ end
+
+ context "when the gem hasn't been installed" do
+ let(:git_proxy_installed) { false }
+
+ it "returns falsey value" do
+ expect(subject.send(:locked_revision_checked_out?)).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/source/path_spec.rb b/spec/bundler/bundler/source/path_spec.rb
new file mode 100644
index 0000000000..1d13e03ec1
--- /dev/null
+++ b/spec/bundler/bundler/source/path_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Source::Path do
+ before do
+ allow(Bundler).to receive(:root) { Pathname.new("root") }
+ end
+
+ describe "#eql?" do
+ subject { described_class.new("path" => "gems/a") }
+
+ context "with two equivalent relative paths from different roots" do
+ let(:a_gem_opts) { { "path" => "../gems/a", "root_path" => Bundler.root.join("nested") } }
+ let(:a_gem) { described_class.new a_gem_opts }
+
+ it "returns true" do
+ expect(subject).to eq a_gem
+ end
+ end
+
+ context "with the same (but not equivalent) relative path from different roots" do
+ subject { described_class.new("path" => "gems/a") }
+
+ let(:a_gem_opts) { { "path" => "gems/a", "root_path" => Bundler.root.join("nested") } }
+ let(:a_gem) { described_class.new a_gem_opts }
+
+ it "returns false" do
+ expect(subject).to_not eq a_gem
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/source/rubygems/remote_spec.rb b/spec/bundler/bundler/source/rubygems/remote_spec.rb
new file mode 100644
index 0000000000..27430d4a3b
--- /dev/null
+++ b/spec/bundler/bundler/source/rubygems/remote_spec.rb
@@ -0,0 +1,207 @@
+# frozen_string_literal: true
+
+require "bundler/source/rubygems/remote"
+
+RSpec.describe Bundler::Source::Rubygems::Remote do
+ def remote(uri)
+ Bundler::Source::Rubygems::Remote.new(uri)
+ end
+
+ before do
+ allow(Digest(:MD5)).to receive(:hexdigest).with(duck_type(:to_s)) {|string| "MD5HEX(#{string})" }
+ end
+
+ let(:uri_no_auth) { Gem::URI("https://gems.example.com") }
+ let(:uri_with_auth) { Gem::URI("https://#{credentials}@gems.example.com") }
+ let(:credentials) { "username:password" }
+
+ context "when the original URI has no credentials" do
+ describe "#uri" do
+ it "returns the original URI" do
+ expect(remote(uri_no_auth).uri).to eq(uri_no_auth)
+ end
+
+ it "applies configured credentials" do
+ Bundler.settings.temporary(uri_no_auth.to_s => credentials) do
+ expect(remote(uri_no_auth).uri).to eq(uri_with_auth)
+ end
+ end
+ end
+
+ describe "#anonymized_uri" do
+ it "returns the original URI" do
+ expect(remote(uri_no_auth).anonymized_uri).to eq(uri_no_auth)
+ end
+
+ it "does not apply given credentials" do
+ Bundler.settings.temporary(uri_no_auth.to_s => credentials) do
+ expect(remote(uri_no_auth).anonymized_uri).to eq(uri_no_auth)
+ end
+ end
+ end
+
+ describe "#cache_slug" do
+ it "returns the correct slug" do
+ expect(remote(uri_no_auth).cache_slug).to eq("gems.example.com.443.MD5HEX(gems.example.com.443./)")
+ end
+
+ it "only applies the given user" do
+ Bundler.settings.temporary(uri_no_auth.to_s => credentials) do
+ expect(remote(uri_no_auth).cache_slug).to eq("gems.example.com.username.443.MD5HEX(gems.example.com.username.443./)")
+ end
+ end
+ end
+ end
+
+ context "when the original URI has a username and password" do
+ describe "#uri" do
+ it "returns the original URI" do
+ expect(remote(uri_with_auth).uri).to eq(uri_with_auth)
+ end
+
+ it "does not apply configured credentials" do
+ Bundler.settings.temporary(uri_no_auth.to_s => "other:stuff")
+ expect(remote(uri_with_auth).uri).to eq(uri_with_auth)
+ end
+ end
+
+ describe "#anonymized_uri" do
+ it "returns the URI without username and password" do
+ expect(remote(uri_with_auth).anonymized_uri).to eq(uri_no_auth)
+ end
+
+ it "does not apply given credentials" do
+ Bundler.settings.temporary(uri_no_auth.to_s => "other:stuff")
+ expect(remote(uri_with_auth).anonymized_uri).to eq(uri_no_auth)
+ end
+ end
+
+ describe "#cache_slug" do
+ it "returns the correct slug" do
+ expect(remote(uri_with_auth).cache_slug).to eq("gems.example.com.username.443.MD5HEX(gems.example.com.username.443./)")
+ end
+
+ it "does not apply given credentials" do
+ Bundler.settings.temporary(uri_with_auth.to_s => credentials)
+ expect(remote(uri_with_auth).cache_slug).to eq("gems.example.com.username.443.MD5HEX(gems.example.com.username.443./)")
+ end
+ end
+ end
+
+ context "when the original URI has only a username" do
+ let(:uri) { Gem::URI("https://SeCrEt-ToKeN@gem.fury.io/me/") }
+
+ describe "#anonymized_uri" do
+ it "returns the URI without username and password" do
+ expect(remote(uri).anonymized_uri).to eq(Gem::URI("https://gem.fury.io/me/"))
+ end
+ end
+
+ describe "#cache_slug" do
+ it "returns the correct slug" do
+ expect(remote(uri).cache_slug).to eq("gem.fury.io.SeCrEt-ToKeN.443.MD5HEX(gem.fury.io.SeCrEt-ToKeN.443./me/)")
+ end
+ end
+ end
+
+ context "when a mirror with inline credentials is configured for the URI" do
+ let(:uri) { Gem::URI("https://rubygems.org/") }
+ let(:mirror_uri_with_auth) { Gem::URI("https://username:password@example-mirror.rubygems.org/") }
+ let(:mirror_uri_no_auth) { Gem::URI("https://example-mirror.rubygems.org/") }
+
+ before { Bundler.settings.temporary("mirror.https://rubygems.org/" => mirror_uri_with_auth.to_s) }
+
+ after { Bundler.settings.temporary("mirror.https://rubygems.org/" => nil) }
+
+ specify "#uri returns the mirror URI with credentials" do
+ expect(remote(uri).uri).to eq(mirror_uri_with_auth)
+ end
+
+ specify "#anonymized_uri returns the mirror URI without credentials" do
+ expect(remote(uri).anonymized_uri).to eq(mirror_uri_no_auth)
+ end
+
+ specify "#original_uri returns the original source" do
+ expect(remote(uri).original_uri).to eq(uri)
+ end
+
+ specify "#cache_slug returns the correct slug" do
+ expect(remote(uri).cache_slug).to eq("rubygems.org.443.MD5HEX(rubygems.org.443./)")
+ end
+ end
+
+ context "when a mirror with configured credentials is configured for the URI" do
+ let(:uri) { Gem::URI("https://rubygems.org/") }
+ let(:mirror_uri_with_auth) { Gem::URI("https://#{credentials}@example-mirror.rubygems.org/") }
+ let(:mirror_uri_no_auth) { Gem::URI("https://example-mirror.rubygems.org/") }
+
+ before do
+ Bundler.settings.temporary("mirror.https://rubygems.org/" => mirror_uri_no_auth.to_s)
+ Bundler.settings.temporary(mirror_uri_no_auth.to_s => credentials)
+ end
+
+ after do
+ Bundler.settings.temporary("mirror.https://rubygems.org/" => nil)
+ Bundler.settings.temporary(mirror_uri_no_auth.to_s => nil)
+ end
+
+ specify "#uri returns the mirror URI with credentials" do
+ expect(remote(uri).uri).to eq(mirror_uri_with_auth)
+ end
+
+ specify "#anonymized_uri returns the mirror URI without credentials" do
+ expect(remote(uri).anonymized_uri).to eq(mirror_uri_no_auth)
+ end
+
+ specify "#original_uri returns the original source" do
+ expect(remote(uri).original_uri).to eq(uri)
+ end
+
+ specify "#cache_slug returns the original source" do
+ expect(remote(uri).cache_slug).to eq("rubygems.org.443.MD5HEX(rubygems.org.443./)")
+ end
+ end
+
+ context "when there is no mirror set" do
+ describe "#original_uri" do
+ it "is not set" do
+ expect(remote(uri_no_auth).original_uri).to be_nil
+ end
+ end
+ end
+
+ describe "#cooldown" do
+ it "is nil by default" do
+ expect(remote(uri_no_auth).cooldown).to be_nil
+ end
+
+ it "returns the value passed to the constructor" do
+ r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7)
+ expect(r.cooldown).to eq(7)
+ end
+ end
+
+ describe "#effective_cooldown" do
+ it "returns the per-remote value when no override is set" do
+ r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7)
+ expect(r.effective_cooldown).to eq(7)
+ end
+
+ it "returns nil when neither override nor per-remote value is set" do
+ expect(remote(uri_no_auth).effective_cooldown).to be_nil
+ end
+
+ it "settings override per-remote value" do
+ r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7)
+ Bundler.settings.temporary(cooldown: 14) do
+ expect(r.effective_cooldown).to eq(14)
+ end
+ end
+
+ it "settings override even when per-remote value is absent" do
+ Bundler.settings.temporary(cooldown: 14) do
+ expect(remote(uri_no_auth).effective_cooldown).to eq(14)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/source/rubygems_spec.rb b/spec/bundler/bundler/source/rubygems_spec.rb
new file mode 100644
index 0000000000..feb787498e
--- /dev/null
+++ b/spec/bundler/bundler/source/rubygems_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Source::Rubygems do
+ before do
+ allow(Bundler).to receive(:root) { Pathname.new("root") }
+ end
+
+ describe "caches" do
+ it "includes Bundler.app_cache" do
+ expect(subject.caches).to include(Bundler.app_cache)
+ end
+
+ it "includes GEM_PATH entries" do
+ Gem.path.each do |path|
+ expect(subject.caches).to include(File.expand_path("#{path}/cache"))
+ end
+ end
+
+ it "is an array of strings or pathnames" do
+ subject.caches.each do |cache|
+ expect([String, Pathname]).to include(cache.class)
+ end
+ end
+ end
+
+ describe "#add_remote" do
+ context "when the source is an HTTP(s) URI with no host" do
+ it "raises error" do
+ expect { subject.add_remote("https:rubygems.org") }.to raise_error(ArgumentError)
+ end
+ end
+ end
+
+ describe "#no_remotes?" do
+ context "when no remote provided" do
+ it "returns a truthy value" do
+ expect(described_class.new("remotes" => []).no_remotes?).to be_truthy
+ end
+ end
+
+ context "when a remote provided" do
+ it "returns a falsey value" do
+ expect(described_class.new("remotes" => ["https://rubygems.org"]).no_remotes?).to be_falsey
+ end
+ end
+ end
+
+ describe "#clear_cache" do
+ it "invalidates memoized indexes so subsequent reads rebuild them" do
+ source = described_class.new
+
+ first_specs = source.specs
+ first_installed = source.send(:installed_specs)
+ first_default = source.send(:default_specs)
+ first_cached = source.send(:cached_specs)
+
+ expect(source.specs).to equal(first_specs)
+ expect(source.send(:installed_specs)).to equal(first_installed)
+ expect(source.send(:default_specs)).to equal(first_default)
+ expect(source.send(:cached_specs)).to equal(first_cached)
+
+ source.clear_cache
+
+ expect(source.specs).not_to equal(first_specs)
+ expect(source.send(:installed_specs)).not_to equal(first_installed)
+ expect(source.send(:default_specs)).not_to equal(first_default)
+ expect(source.send(:cached_specs)).not_to equal(first_cached)
+ end
+
+ it "reflects newly-discovered installed gems after clear_cache" do
+ source = described_class.new
+ foo = Gem::Specification.new("foo", "1.0.0")
+ bar = Gem::Specification.new("bar", "1.0.0")
+
+ allow(Bundler.rubygems).to receive(:installed_specs).and_return([foo])
+ expect(source.send(:installed_specs).search("bar")).to be_empty
+
+ allow(Bundler.rubygems).to receive(:installed_specs).and_return([foo, bar])
+ expect(source.send(:installed_specs).search("bar")).to be_empty
+
+ source.clear_cache
+
+ expect(source.send(:installed_specs).search("bar")).not_to be_empty
+ end
+ end
+
+ describe "log debug information" do
+ it "log the time spent downloading and installing a gem" do
+ build_repo2 do
+ build_gem "warning"
+ end
+
+ gemfile_content = <<~G
+ source "https://gem.repo2"
+ gem "warning"
+ G
+
+ stdout = install_gemfile(gemfile_content, env: { "DEBUG" => "1" })
+
+ expect(stdout).to match(/Downloaded warning in: \d+\.\d+s/)
+ expect(stdout).to match(/Installed warning in: \d+\.\d+s/)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/source_list_spec.rb b/spec/bundler/bundler/source_list_spec.rb
new file mode 100644
index 0000000000..61bd99b063
--- /dev/null
+++ b/spec/bundler/bundler/source_list_spec.rb
@@ -0,0 +1,475 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::SourceList do
+ before do
+ allow(Bundler).to receive(:root) { Pathname.new "./tmp/bundled_app" }
+
+ stub_const "ASourcePlugin", Class.new(Bundler::Plugin::API)
+ ASourcePlugin.source "new_source"
+ allow(Bundler::Plugin).to receive(:source?).with("new_source").and_return(true)
+ end
+
+ subject(:source_list) { Bundler::SourceList.new }
+
+ let(:global_rubygems_source) { Bundler::Source::Rubygems.new }
+ let(:metadata_source) { Bundler::Source::Metadata.new }
+
+ describe "adding sources" do
+ before do
+ source_list.add_path_source("path" => "/existing/path/to/gem")
+ source_list.add_git_source("uri" => "git://existing-git.org/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://existing-rubygems.org"])
+ source_list.add_plugin_source("new_source", "uri" => "https://some.url/a")
+ end
+
+ describe "#add_path_source" do
+ before do
+ @duplicate = source_list.add_path_source("path" => "/path/to/gem")
+ @new_source = source_list.add_path_source("path" => "/path/to/gem")
+ end
+
+ it "returns the new path source" do
+ expect(@new_source).to be_instance_of(Bundler::Source::Path)
+ end
+
+ it "passes the provided options to the new source" do
+ expect(@new_source.options).to eq("path" => "/path/to/gem")
+ end
+
+ it "adds the source to the beginning of path_sources" do
+ expect(source_list.path_sources.first).to equal(@new_source)
+ end
+
+ it "removes existing duplicates" do
+ expect(source_list.path_sources).not_to include equal(@duplicate)
+ end
+ end
+
+ describe "#add_git_source" do
+ before do
+ @duplicate = source_list.add_git_source("uri" => "git://host/path.git")
+ @new_source = source_list.add_git_source("uri" => "git://host/path.git")
+ end
+
+ it "returns the new git source" do
+ expect(@new_source).to be_instance_of(Bundler::Source::Git)
+ end
+
+ it "passes the provided options to the new source" do
+ @new_source = source_list.add_git_source("uri" => "git://host/path.git")
+ expect(@new_source.options).to eq("uri" => "git://host/path.git")
+ end
+
+ it "adds the source to the beginning of git_sources" do
+ @new_source = source_list.add_git_source("uri" => "git://host/path.git")
+ expect(source_list.git_sources.first).to equal(@new_source)
+ end
+
+ it "removes existing duplicates" do
+ @duplicate = source_list.add_git_source("uri" => "git://host/path.git")
+ @new_source = source_list.add_git_source("uri" => "git://host/path.git")
+ expect(source_list.git_sources).not_to include equal(@duplicate)
+ end
+
+ context "with the git: protocol" do
+ let(:msg) do
+ "The git source `git://existing-git.org/path.git` " \
+ "uses the `git` protocol, which transmits data without encryption. " \
+ "Disable this warning with `bundle config set --local git.allow_insecure true`, " \
+ "or switch to the `https` protocol to keep your data secure."
+ end
+
+ it "warns about git protocols" do
+ expect(Bundler.ui).to receive(:warn).with(msg)
+ source_list.add_git_source("uri" => "git://existing-git.org/path.git")
+ end
+
+ it "ignores git protocols on request" do
+ Bundler.settings.temporary("git.allow_insecure": true)
+ expect(Bundler.ui).to_not receive(:warn).with(msg)
+ source_list.add_git_source("uri" => "git://existing-git.org/path.git")
+ end
+ end
+ end
+
+ describe "#add_rubygems_source" do
+ before do
+ @duplicate = source_list.add_rubygems_source("remotes" => ["https://rubygems.org/"])
+ @new_source = source_list.add_rubygems_source("remotes" => ["https://rubygems.org/"])
+ end
+
+ it "returns the new rubygems source" do
+ expect(@new_source).to be_instance_of(Bundler::Source::Rubygems)
+ end
+
+ it "passes the provided options to the new source" do
+ expect(@new_source.options).to eq("remotes" => ["https://rubygems.org/"])
+ end
+
+ it "adds the source to the beginning of rubygems_sources" do
+ expect(source_list.rubygems_sources.first).to equal(@new_source)
+ end
+
+ it "removes duplicates" do
+ expect(source_list.rubygems_sources).not_to include equal(@duplicate)
+ end
+ end
+
+ describe "#add_global_rubygems_remote" do
+ let!(:returned_source) { source_list.add_global_rubygems_remote("https://rubygems.org/") }
+
+ it "returns the global rubygems source" do
+ expect(returned_source).to be_instance_of(Bundler::Source::Rubygems)
+ end
+
+ it "adds the provided remote to the beginning of the global source" do
+ source_list.add_global_rubygems_remote("https://othersource.org")
+ expect(returned_source.remotes).to eq [
+ Gem::URI("https://othersource.org/"),
+ Gem::URI("https://rubygems.org/"),
+ ]
+ end
+
+ it "records the per-remote cooldown when supplied" do
+ source_list.add_global_rubygems_remote("https://othersource.org", cooldown: 7)
+ expect(returned_source.cooldown_for(Gem::URI("https://othersource.org/"))).to eq(7)
+ expect(returned_source.cooldown_for(Gem::URI("https://rubygems.org/"))).to be_nil
+ end
+ end
+
+ describe "#add_plugin_source" do
+ before do
+ @duplicate = source_list.add_plugin_source("new_source", "uri" => "http://host/path.")
+ @new_source = source_list.add_plugin_source("new_source", "uri" => "http://host/path.")
+ end
+
+ it "returns the new plugin source" do
+ expect(@new_source).to be_a(Bundler::Plugin::API::Source)
+ end
+
+ it "passes the provided options to the new source" do
+ expect(@new_source.options).to eq("uri" => "http://host/path.")
+ end
+
+ it "adds the source to the beginning of git_sources" do
+ expect(source_list.plugin_sources.first).to equal(@new_source)
+ end
+
+ it "removes existing duplicates" do
+ expect(source_list.plugin_sources).not_to include equal(@duplicate)
+ end
+ end
+ end
+
+ describe "#all_sources" do
+ it "includes the global rubygems source when rubygems sources have been added" do
+ source_list.add_git_source("uri" => "git://host/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://rubygems.org"])
+ source_list.add_path_source("path" => "/path/to/gem")
+ source_list.add_plugin_source("new_source", "uri" => "https://some.url/a")
+
+ expect(source_list.all_sources).to include global_rubygems_source
+ end
+
+ it "includes the global rubygems source when no rubygems sources have been added" do
+ source_list.add_git_source("uri" => "git://host/path.git")
+ source_list.add_path_source("path" => "/path/to/gem")
+ source_list.add_plugin_source("new_source", "uri" => "https://some.url/a")
+
+ expect(source_list.all_sources).to include global_rubygems_source
+ end
+
+ it "returns sources of the same type in the reverse order that they were added" do
+ source_list.add_git_source("uri" => "git://third-git.org/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://fifth-rubygems.org"])
+ source_list.add_path_source("path" => "/third/path/to/gem")
+ source_list.add_plugin_source("new_source", "uri" => "https://some.url/b")
+ source_list.add_rubygems_source("remotes" => ["https://fourth-rubygems.org"])
+ source_list.add_path_source("path" => "/second/path/to/gem")
+ source_list.add_rubygems_source("remotes" => ["https://third-rubygems.org"])
+ source_list.add_plugin_source("new_source", "uri" => "https://some.o.url/")
+ source_list.add_git_source("uri" => "git://second-git.org/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://second-rubygems.org"])
+ source_list.add_path_source("path" => "/first/path/to/gem")
+ source_list.add_plugin_source("new_source", "uri" => "https://some.url/c")
+ source_list.add_rubygems_source("remotes" => ["https://first-rubygems.org"])
+ source_list.add_git_source("uri" => "git://first-git.org/path.git")
+
+ expect(source_list.all_sources).to eq [
+ Bundler::Source::Path.new("path" => "/first/path/to/gem"),
+ Bundler::Source::Path.new("path" => "/second/path/to/gem"),
+ Bundler::Source::Path.new("path" => "/third/path/to/gem"),
+ Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"),
+ Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"),
+ Bundler::Source::Git.new("uri" => "git://third-git.org/path.git"),
+ ASourcePlugin.new("uri" => "https://some.url/c"),
+ ASourcePlugin.new("uri" => "https://some.o.url/"),
+ ASourcePlugin.new("uri" => "https://some.url/b"),
+ Bundler::Source::Rubygems.new("remotes" => ["https://first-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://second-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://third-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://fourth-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://fifth-rubygems.org"]),
+ global_rubygems_source,
+ metadata_source,
+ ]
+ end
+ end
+
+ describe "#path_sources" do
+ it "returns an empty array when no path sources have been added" do
+ source_list.add_global_rubygems_remote("https://rubygems.org")
+ source_list.add_git_source("uri" => "git://host/path.git")
+ expect(source_list.path_sources).to be_empty
+ end
+
+ it "returns path sources in the reverse order that they were added" do
+ source_list.add_git_source("uri" => "git://third-git.org/path.git")
+ source_list.add_global_rubygems_remote("https://fifth-rubygems.org")
+ source_list.add_path_source("path" => "/third/path/to/gem")
+ source_list.add_global_rubygems_remote("https://fourth-rubygems.org")
+ source_list.add_path_source("path" => "/second/path/to/gem")
+ source_list.add_global_rubygems_remote("https://third-rubygems.org")
+ source_list.add_git_source("uri" => "git://second-git.org/path.git")
+ source_list.add_global_rubygems_remote("https://second-rubygems.org")
+ source_list.add_path_source("path" => "/first/path/to/gem")
+ source_list.add_global_rubygems_remote("https://first-rubygems.org")
+ source_list.add_git_source("uri" => "git://first-git.org/path.git")
+
+ expect(source_list.path_sources).to eq [
+ Bundler::Source::Path.new("path" => "/first/path/to/gem"),
+ Bundler::Source::Path.new("path" => "/second/path/to/gem"),
+ Bundler::Source::Path.new("path" => "/third/path/to/gem"),
+ ]
+ end
+ end
+
+ describe "#git_sources" do
+ it "returns an empty array when no git sources have been added" do
+ source_list.add_global_rubygems_remote("https://rubygems.org")
+ source_list.add_path_source("path" => "/path/to/gem")
+
+ expect(source_list.git_sources).to be_empty
+ end
+
+ it "returns git sources in the reverse order that they were added" do
+ source_list.add_git_source("uri" => "git://third-git.org/path.git")
+ source_list.add_global_rubygems_remote("https://fifth-rubygems.org")
+ source_list.add_path_source("path" => "/third/path/to/gem")
+ source_list.add_global_rubygems_remote("https://fourth-rubygems.org")
+ source_list.add_path_source("path" => "/second/path/to/gem")
+ source_list.add_global_rubygems_remote("https://third-rubygems.org")
+ source_list.add_git_source("uri" => "git://second-git.org/path.git")
+ source_list.add_global_rubygems_remote("https://second-rubygems.org")
+ source_list.add_path_source("path" => "/first/path/to/gem")
+ source_list.add_global_rubygems_remote("https://first-rubygems.org")
+ source_list.add_git_source("uri" => "git://first-git.org/path.git")
+
+ expect(source_list.git_sources).to eq [
+ Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"),
+ Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"),
+ Bundler::Source::Git.new("uri" => "git://third-git.org/path.git"),
+ ]
+ end
+ end
+
+ describe "#plugin_sources" do
+ it "returns an empty array when no plugin sources have been added" do
+ source_list.add_global_rubygems_remote("https://rubygems.org")
+ source_list.add_path_source("path" => "/path/to/gem")
+
+ expect(source_list.plugin_sources).to be_empty
+ end
+
+ it "returns plugin sources in the reverse order that they were added" do
+ source_list.add_plugin_source("new_source", "uri" => "https://third-git.org/path.git")
+ source_list.add_git_source("https://new-git.org")
+ source_list.add_path_source("path" => "/third/path/to/gem")
+ source_list.add_global_rubygems_remote("https://fourth-rubygems.org")
+ source_list.add_path_source("path" => "/second/path/to/gem")
+ source_list.add_global_rubygems_remote("https://third-rubygems.org")
+ source_list.add_plugin_source("new_source", "uri" => "git://second-git.org/path.git")
+ source_list.add_global_rubygems_remote("https://second-rubygems.org")
+ source_list.add_path_source("path" => "/first/path/to/gem")
+ source_list.add_global_rubygems_remote("https://first-rubygems.org")
+ source_list.add_plugin_source("new_source", "uri" => "git://first-git.org/path.git")
+
+ expect(source_list.plugin_sources).to eq [
+ ASourcePlugin.new("uri" => "git://first-git.org/path.git"),
+ ASourcePlugin.new("uri" => "git://second-git.org/path.git"),
+ ASourcePlugin.new("uri" => "https://third-git.org/path.git"),
+ ]
+ end
+ end
+
+ describe "#rubygems_sources" do
+ it "includes the global rubygems source when rubygems sources have been added" do
+ source_list.add_git_source("uri" => "git://host/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://rubygems.org"])
+ source_list.add_path_source("path" => "/path/to/gem")
+
+ expect(source_list.rubygems_sources).to include global_rubygems_source
+ end
+
+ it "returns only the global rubygems source when no rubygems sources have been added" do
+ source_list.add_git_source("uri" => "git://host/path.git")
+ source_list.add_path_source("path" => "/path/to/gem")
+
+ expect(source_list.rubygems_sources).to eq [global_rubygems_source]
+ end
+
+ it "returns rubygems sources in the reverse order that they were added" do
+ source_list.add_git_source("uri" => "git://third-git.org/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://fifth-rubygems.org"])
+ source_list.add_path_source("path" => "/third/path/to/gem")
+ source_list.add_rubygems_source("remotes" => ["https://fourth-rubygems.org"])
+ source_list.add_path_source("path" => "/second/path/to/gem")
+ source_list.add_rubygems_source("remotes" => ["https://third-rubygems.org"])
+ source_list.add_git_source("uri" => "git://second-git.org/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://second-rubygems.org"])
+ source_list.add_path_source("path" => "/first/path/to/gem")
+ source_list.add_rubygems_source("remotes" => ["https://first-rubygems.org"])
+ source_list.add_git_source("uri" => "git://first-git.org/path.git")
+
+ expect(source_list.rubygems_sources).to eq [
+ Bundler::Source::Rubygems.new("remotes" => ["https://first-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://second-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://third-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://fourth-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://fifth-rubygems.org"]),
+ global_rubygems_source,
+ ]
+ end
+ end
+
+ describe "#get" do
+ context "when it includes an equal source" do
+ let(:rubygems_source) { Bundler::Source::Rubygems.new("remotes" => ["https://rubygems.org"]) }
+ before { @equal_source = source_list.add_global_rubygems_remote("https://rubygems.org") }
+
+ it "returns the equal source" do
+ expect(source_list.get(rubygems_source)).to be @equal_source
+ end
+ end
+
+ context "when it does not include an equal source" do
+ let(:path_source) { Bundler::Source::Path.new("path" => "/path/to/gem") }
+
+ it "returns nil" do
+ expect(source_list.get(path_source)).to be_nil
+ end
+ end
+ end
+
+ describe "#lock_sources" do
+ before do
+ source_list.add_git_source("uri" => "git://third-git.org/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://duplicate-rubygems.org"])
+ source_list.add_plugin_source("new_source", "uri" => "https://third-bar.org/foo")
+ source_list.add_path_source("path" => "/third/path/to/gem")
+ source_list.add_rubygems_source("remotes" => ["https://third-rubygems.org"])
+ source_list.add_path_source("path" => "/second/path/to/gem")
+ source_list.add_rubygems_source("remotes" => ["https://second-rubygems.org"])
+ source_list.add_git_source("uri" => "git://second-git.org/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://first-rubygems.org"])
+ source_list.add_plugin_source("new_source", "uri" => "https://second-plugin.org/random")
+ source_list.add_path_source("path" => "/first/path/to/gem")
+ source_list.add_rubygems_source("remotes" => ["https://duplicate-rubygems.org"])
+ source_list.add_git_source("uri" => "git://first-git.org/path.git")
+ end
+
+ it "returns all sources, without combining rubygems sources" do
+ expect(source_list.lock_sources).to eq [
+ Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"),
+ Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"),
+ Bundler::Source::Git.new("uri" => "git://third-git.org/path.git"),
+ ASourcePlugin.new("uri" => "https://second-plugin.org/random"),
+ ASourcePlugin.new("uri" => "https://third-bar.org/foo"),
+ Bundler::Source::Path.new("path" => "/first/path/to/gem"),
+ Bundler::Source::Path.new("path" => "/second/path/to/gem"),
+ Bundler::Source::Path.new("path" => "/third/path/to/gem"),
+ Bundler::Source::Rubygems.new,
+ Bundler::Source::Rubygems.new("remotes" => ["https://duplicate-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://first-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://second-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://third-rubygems.org"]),
+ ]
+ end
+ end
+
+ describe "replace_sources!" do
+ let(:existing_locked_source) { Bundler::Source::Path.new("path" => "/existing/path") }
+ let(:removed_locked_source) { Bundler::Source::Path.new("path" => "/removed/path") }
+
+ let(:locked_sources) { [existing_locked_source, removed_locked_source] }
+
+ before do
+ @existing_source = source_list.add_path_source("path" => "/existing/path")
+ @new_source = source_list.add_path_source("path" => "/new/path")
+ source_list.replace_sources!(locked_sources)
+ end
+
+ it "maintains the order and number of sources" do
+ expect(source_list.path_sources).to eq [@new_source, @existing_source]
+ end
+
+ it "retains the same instance of the new source" do
+ expect(source_list.path_sources[0]).to be @new_source
+ end
+
+ it "replaces the instance of the existing source" do
+ expect(source_list.path_sources[1]).to be existing_locked_source
+ end
+ end
+
+ describe "#cached!" do
+ let(:rubygems_source) { source_list.add_rubygems_source("remotes" => ["https://rubygems.org"]) }
+ let(:git_source) { source_list.add_git_source("uri" => "git://host/path.git") }
+ let(:path_source) { source_list.add_path_source("path" => "/path/to/gem") }
+
+ it "calls #cached! on all the sources" do
+ expect(rubygems_source).to receive(:cached!)
+ expect(git_source).to receive(:cached!)
+ expect(path_source).to receive(:cached!)
+ source_list.cached!
+ end
+ end
+
+ describe "#remote!" do
+ let(:rubygems_source) { source_list.add_rubygems_source("remotes" => ["https://rubygems.org"]) }
+ let(:git_source) { source_list.add_git_source("uri" => "git://host/path.git") }
+ let(:path_source) { source_list.add_path_source("path" => "/path/to/gem") }
+
+ it "calls #remote! on all the sources" do
+ expect(rubygems_source).to receive(:remote!)
+ expect(git_source).to receive(:remote!)
+ expect(path_source).to receive(:remote!)
+ source_list.remote!
+ end
+ end
+
+ describe "#clear_cache" do
+ let(:rubygems_source) { source_list.add_rubygems_source("remotes" => ["https://rubygems.org"]) }
+
+ it "calls #clear_cache on all rubygems sources" do
+ expect(rubygems_source).to receive(:clear_cache)
+ expect(source_list.global_rubygems_source).to receive(:clear_cache)
+ source_list.clear_cache
+ end
+ end
+
+ describe "implicit_global_source?" do
+ context "when a global rubygem source provided" do
+ it "returns a falsy value" do
+ source_list.add_global_rubygems_remote("https://rubygems.org")
+
+ expect(source_list.implicit_global_source?).to be_falsey
+ end
+ end
+ context "when no global rubygem source provided" do
+ it "returns a truthy value" do
+ expect(source_list.implicit_global_source?).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/source_spec.rb b/spec/bundler/bundler/source_spec.rb
new file mode 100644
index 0000000000..01b57ce9e8
--- /dev/null
+++ b/spec/bundler/bundler/source_spec.rb
@@ -0,0 +1,174 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Source do
+ class ExampleSource < Bundler::Source
+ end
+
+ subject { ExampleSource.new }
+
+ describe "#unmet_deps" do
+ let(:specs) { double(:specs) }
+ let(:unmet_dependency_names) { double(:unmet_dependency_names) }
+
+ before do
+ allow(subject).to receive(:specs).and_return(specs)
+ allow(specs).to receive(:unmet_dependency_names).and_return(unmet_dependency_names)
+ end
+
+ it "should return the names of unmet dependencies" do
+ expect(subject.unmet_deps).to eq(unmet_dependency_names)
+ end
+ end
+
+ describe "#version_message" do
+ let(:spec) { double(:spec, name: "nokogiri", version: ">= 1.6", platform: Gem::Platform::RUBY) }
+
+ shared_examples_for "the lockfile specs are not relevant" do
+ it "should return a string with the spec name and version" do
+ expect(subject.version_message(spec)).to eq("nokogiri >= 1.6")
+ end
+ end
+
+ context "when there are locked gems" do
+ context "that contain the relevant gem spec" do
+ context "without a version" do
+ let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: nil) }
+
+ it_behaves_like "the lockfile specs are not relevant"
+ end
+
+ context "with the same version" do
+ let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: ">= 1.6") }
+
+ it_behaves_like "the lockfile specs are not relevant"
+ end
+
+ context "with a different version" do
+ let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: "< 1.5") }
+
+ context "with color", :no_color_tty do
+ before do
+ allow($stdout).to receive(:tty?).and_return(true)
+ end
+
+ it "should return a string with the spec name and version and locked spec version" do
+ expect(subject.version_message(spec, locked_gem)).to eq("nokogiri >= 1.6\e[32m (was < 1.5)\e[0m")
+ end
+ end
+
+ context "without color" do
+ around do |example|
+ with_ui(Bundler::UI::Shell.new("no-color" => true)) do
+ example.run
+ end
+ end
+
+ it "should return a string with the spec name and version and locked spec version" do
+ expect(subject.version_message(spec, locked_gem)).to eq("nokogiri >= 1.6 (was < 1.5)")
+ end
+ end
+ end
+
+ context "with a more recent version" do
+ let(:spec) { double(:spec, name: "nokogiri", version: "1.6.1", platform: Gem::Platform::RUBY) }
+ let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: "1.7.0") }
+
+ context "with color", :no_color_tty do
+ before do
+ allow($stdout).to receive(:tty?).and_return(true)
+ end
+
+ it "should return a string with the locked spec version in yellow" do
+ expect(subject.version_message(spec, locked_gem)).to eq("nokogiri 1.6.1\e[33m (was 1.7.0)\e[0m")
+ end
+ end
+
+ context "without color" do
+ around do |example|
+ with_ui(Bundler::UI::Shell.new("no-color" => true)) do
+ example.run
+ end
+ end
+
+ it "should return a string with the locked spec version in yellow" do
+ expect(subject.version_message(spec, locked_gem)).to eq("nokogiri 1.6.1 (was 1.7.0)")
+ end
+ end
+ end
+
+ context "with an older version" do
+ let(:spec) { double(:spec, name: "nokogiri", version: "1.7.1", platform: Gem::Platform::RUBY) }
+ let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: "1.7.0") }
+
+ context "with color", :no_color_tty do
+ before do
+ allow($stdout).to receive(:tty?).and_return(true)
+ end
+
+ it "should return a string with the locked spec version in green" do
+ expect(subject.version_message(spec, locked_gem)).to eq("nokogiri 1.7.1\e[32m (was 1.7.0)\e[0m")
+ end
+ end
+
+ context "without color" do
+ around do |example|
+ with_ui(Bundler::UI::Shell.new("no-color" => true)) do
+ example.run
+ end
+ end
+
+ it "should return a string with the locked spec version in yellow" do
+ expect(subject.version_message(spec, locked_gem)).to eq("nokogiri 1.7.1 (was 1.7.0)")
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe "#can_lock?" do
+ context "when the passed spec's source is equivalent" do
+ let(:spec) { double(:spec, source: subject) }
+
+ it "should return true" do
+ expect(subject.can_lock?(spec)).to be_truthy
+ end
+ end
+
+ context "when the passed spec's source is not equivalent" do
+ let(:spec) { double(:spec, source: double(:other_source)) }
+
+ it "should return false" do
+ expect(subject.can_lock?(spec)).to be_falsey
+ end
+ end
+ end
+
+ describe "#include?" do
+ context "when the passed source is equivalent" do
+ let(:source) { subject }
+
+ it "should return true" do
+ expect(subject).to include(source)
+ end
+ end
+
+ context "when the passed source is not equivalent" do
+ let(:source) { double(:source) }
+
+ it "should return false" do
+ expect(subject).to_not include(source)
+ end
+ end
+ end
+
+ private
+
+ def with_ui(ui)
+ old_ui = Bundler.ui
+ Bundler.ui = ui
+ yield
+ ensure
+ Bundler.ui = old_ui
+ end
+end
diff --git a/spec/bundler/bundler/spec_set_spec.rb b/spec/bundler/bundler/spec_set_spec.rb
new file mode 100644
index 0000000000..1e1ceadf26
--- /dev/null
+++ b/spec/bundler/bundler/spec_set_spec.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::SpecSet do
+ let(:specs) do
+ [
+ build_spec("a", "1.0"),
+ build_spec("b", "1.0"),
+ build_spec("c", "1.1") do |s|
+ s.dep "a", "< 2.0"
+ s.dep "e", "> 0"
+ end,
+ build_spec("d", "2.0") do |s|
+ s.dep "a", "1.0"
+ s.dep "c", "~> 1.0"
+ end,
+ build_spec("e", "1.0.0.pre.1"),
+ ].flatten
+ end
+
+ subject { described_class.new(specs) }
+
+ context "enumerable methods" do
+ it "has a length" do
+ expect(subject.length).to eq(5)
+ end
+
+ it "has a size" do
+ expect(subject.size).to eq(5)
+ end
+ end
+
+ describe "#find_by_name_and_platform" do
+ let(:platform) { Gem::Platform.new("universal-darwin-64") }
+ let(:platform_spec) { build_spec("b", "2.0", platform).first }
+ let(:specs) do
+ [
+ build_spec("a", "1.0"),
+ platform_spec,
+ ].flatten
+ end
+
+ it "finds spec with given name and platform" do
+ spec = described_class.new(specs).find_by_name_and_platform("b", platform)
+ expect(spec).to eq platform_spec
+ end
+
+ it "returns nil when the name is not present" do
+ spec = described_class.new(specs).find_by_name_and_platform("missing", platform)
+ expect(spec).to be_nil
+ end
+
+ it "returns nil when the name exists but no spec is installable on the requested platform" do
+ incompatible_platform = Gem::Platform.new("java")
+ incompatible_spec = build_spec("a", "1.0", incompatible_platform).first
+
+ spec = described_class.new([incompatible_spec]).find_by_name_and_platform("a", platform)
+ expect(spec).to be_nil
+ end
+
+ it "returns the first installable spec for the given name in insertion order" do
+ later_platform_spec = build_spec("b", "3.0", platform).first
+ specs = [
+ platform_spec,
+ later_platform_spec,
+ ]
+
+ spec = described_class.new(specs).find_by_name_and_platform("b", platform)
+ expect(spec).to eq platform_spec
+ end
+ end
+
+ describe "#to_a" do
+ it "returns the specs in order" do
+ expect(subject.to_a.map(&:full_name)).to eq %w[
+ a-1.0
+ b-1.0
+ e-1.0.0.pre.1
+ c-1.1
+ d-2.0
+ ]
+ end
+
+ it "puts rake first when present" do
+ specs = [
+ build_spec("a", "1.0") {|s| s.dep "rake", ">= 0" },
+ build_spec("rake", "13.0"),
+ ].flatten
+
+ expect(described_class.new(specs).to_a.map(&:full_name)).to eq %w[
+ rake-13.0
+ a-1.0
+ ]
+ end
+ end
+
+ describe "#complete_platform" do
+ let(:platform) { Gem::Platform.new("x86_64-linux") }
+
+ let(:platform_variant) do
+ build_spec("needs_old_ruby", "1.0", platform).first.tap do |s|
+ s.required_ruby_version = Gem::Requirement.new("< #{Gem.ruby_version}")
+ end
+ end
+
+ let(:lazy_spec) do
+ lazy = Bundler::LazySpecification.new("needs_old_ruby", Gem::Version.new("1.0"), Gem::Platform::RUBY)
+ lazy.required_ruby_version = Gem::Requirement.new("< #{Gem.ruby_version}")
+ source = double("source")
+ source_specs = double("source_specs")
+ allow(source).to receive(:specs).and_return(source_specs)
+ allow(source_specs).to receive(:search).
+ with(["needs_old_ruby", Gem::Version.new("1.0")]).and_return([platform_variant])
+ lazy.source = source
+ lazy
+ end
+
+ it "rejects a platform variant whose strict metadata is incompatible when no override is attached" do
+ set = described_class.new([lazy_spec])
+ expect(set.send(:complete_platform, platform)).to be(false)
+ end
+
+ it "accepts a platform variant when the LazySpec carries an override that allows it" do
+ lazy_spec.overrides = [Bundler::Override.new("needs_old_ruby", :required_ruby_version, :ignore_upper)]
+ set = described_class.new([lazy_spec])
+ expect(set.send(:complete_platform, platform)).to be(true)
+ end
+
+ it "carries overrides onto a synthesized LazySpec so a follow-up complete_platform still honors them" do
+ override = Bundler::Override.new("needs_old_ruby", :required_ruby_version, :ignore_upper)
+ lazy_spec.overrides = [override]
+ second_platform = Gem::Platform.new("aarch64-linux")
+ second_variant = build_spec("needs_old_ruby", "1.0", second_platform).first.tap do |s|
+ s.required_ruby_version = Gem::Requirement.new("< #{Gem.ruby_version}")
+ end
+ allow(lazy_spec.source.specs).to receive(:search).
+ with(["needs_old_ruby", Gem::Version.new("1.0")]).and_return([platform_variant, second_variant])
+
+ set = described_class.new([lazy_spec])
+ expect(set.send(:complete_platform, platform)).to be(true)
+ # The synthesized x86_64-linux variant is now in the set. If lookup
+ # picks it as exemplar for the next platform check, the override list
+ # must still be reachable via its overrides accessor.
+ synthesized = set.to_a.find {|s| s.platform == platform }
+ expect(synthesized.overrides).to eq([override])
+ expect(set.send(:complete_platform, second_platform)).to be(true)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/specifications/foo.gemspec b/spec/bundler/bundler/specifications/foo.gemspec
new file mode 100644
index 0000000000..19b7724e81
--- /dev/null
+++ b/spec/bundler/bundler/specifications/foo.gemspec
@@ -0,0 +1,13 @@
+# rubocop:disable Style/FrozenStringLiteralComment
+# stub: foo 1.0.0 ruby lib
+
+# The first line would be '# -*- encoding: utf-8 -*-' in a real stub gemspec
+
+Gem::Specification.new do |s|
+ s.name = "foo"
+ s.version = "1.0.0"
+ s.loaded_from = __FILE__
+ s.extensions = "ext/foo"
+ s.required_ruby_version = ">= 3.2.0"
+end
+# rubocop:enable Style/FrozenStringLiteralComment
diff --git a/spec/bundler/bundler/stub_specification_spec.rb b/spec/bundler/bundler/stub_specification_spec.rb
new file mode 100644
index 0000000000..f2faa2ea64
--- /dev/null
+++ b/spec/bundler/bundler/stub_specification_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::StubSpecification do
+ let(:with_bundler_stub_spec) do
+ gemspec = Gem::Specification.new do |s|
+ s.name = "gemname"
+ s.version = "1.0.0"
+ s.loaded_from = __FILE__
+ s.extensions = "ext/gemname"
+ end
+
+ described_class.from_stub(gemspec)
+ end
+
+ describe "#from_stub" do
+ it "returns the same stub if already a Bundler::StubSpecification" do
+ stub = described_class.from_stub(with_bundler_stub_spec)
+ expect(stub).to be(with_bundler_stub_spec)
+ end
+ end
+
+ describe "#gem_build_complete_path" do
+ it "StubSpecification should have equal gem_build_complete_path as Specification" do
+ spec_path = File.join(File.dirname(__FILE__), "specifications", "foo.gemspec")
+ spec = Gem::Specification.load(spec_path)
+ gem_stub = Gem::StubSpecification.new(spec_path, File.dirname(__FILE__),"","")
+
+ stub = described_class.from_stub(gem_stub)
+ expect(stub.gem_build_complete_path).to eq spec.gem_build_complete_path
+ end
+ end
+
+ describe "#manually_installed?" do
+ it "returns true if installed_by_version is nil or 0" do
+ stub = described_class.from_stub(with_bundler_stub_spec)
+ expect(stub.manually_installed?).to be true
+ end
+
+ it "returns false if installed_by_version is greater than 0" do
+ stub = described_class.from_stub(with_bundler_stub_spec)
+ stub.installed_by_version = Gem::Version.new(1)
+ expect(stub.manually_installed?).to be false
+ end
+ end
+
+ describe "#missing_extensions?" do
+ it "returns false if manually_installed?" do
+ stub = described_class.from_stub(with_bundler_stub_spec)
+ expect(stub.missing_extensions?).to be false
+ end
+
+ it "returns #{RUBY_ENGINE == "jruby" ? "false" : "true"} if not manually_installed?" do
+ stub = described_class.from_stub(with_bundler_stub_spec)
+ stub.installed_by_version = Gem::Version.new(1)
+ if RUBY_ENGINE == "jruby"
+ expect(stub.missing_extensions?).to be false
+ else
+ expect(stub.missing_extensions?).to be true
+ end
+ end
+ end
+
+ describe "#activated?" do
+ it "returns true after activation" do
+ stub = described_class.from_stub(with_bundler_stub_spec)
+
+ expect(stub.activated?).to be_falsey
+ stub.activated = true
+ expect(stub.activated?).to be true
+ end
+
+ it "returns true after activation if the underlying stub is a `Gem::StubSpecification`" do
+ spec_path = File.join(File.dirname(__FILE__), "specifications", "foo.gemspec")
+ gem_stub = Gem::StubSpecification.new(spec_path, File.dirname(__FILE__),"","")
+ stub = described_class.from_stub(gem_stub)
+
+ expect(stub.activated?).to be_falsey
+ stub.activated = true
+ expect(stub.activated?).to be true
+ end
+ end
+end
diff --git a/spec/bundler/bundler/ui/shell_spec.rb b/spec/bundler/bundler/ui/shell_spec.rb
new file mode 100644
index 0000000000..83f147191e
--- /dev/null
+++ b/spec/bundler/bundler/ui/shell_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::UI::Shell do
+ subject { described_class.new }
+
+ before { subject.level = "debug" }
+
+ describe "#info" do
+ before { subject.level = "info" }
+ it "prints to stdout" do
+ expect { subject.info("info") }.to output("info\n").to_stdout
+ end
+
+ context "when output_stream is :stderr" do
+ before { subject.output_stream = :stderr }
+ it "prints to stderr" do
+ expect { subject.info("info") }.to output("info\n").to_stderr
+ end
+ end
+ end
+
+ describe "#confirm" do
+ before { subject.level = "confirm" }
+ it "prints to stdout" do
+ expect { subject.confirm("confirm") }.to output("confirm\n").to_stdout
+ end
+
+ context "when output_stream is :stderr" do
+ before { subject.output_stream = :stderr }
+ it "prints to stderr" do
+ expect { subject.confirm("confirm") }.to output("confirm\n").to_stderr
+ end
+ end
+ end
+
+ describe "#warn" do
+ before { subject.level = "warn" }
+ it "prints to stderr, implicitly adding a newline" do
+ expect { subject.warn("warning") }.to output("warning\n").to_stderr
+ end
+ it "can be told not to emit a newline" do
+ expect { subject.warn("warning", false) }.to output("warning").to_stderr
+ end
+ end
+
+ describe "#debug" do
+ it "prints to stdout" do
+ expect { subject.debug("debug") }.to output("debug\n").to_stdout
+ end
+
+ context "when output_stream is :stderr" do
+ before { subject.output_stream = :stderr }
+ it "prints to stderr" do
+ expect { subject.debug("debug") }.to output("debug\n").to_stderr
+ end
+ end
+ end
+
+ describe "#error" do
+ before { subject.level = "error" }
+
+ it "prints to stderr" do
+ expect { subject.error("error!!!") }.to output("error!!!\n").to_stderr
+ end
+
+ context "when stderr is closed" do
+ it "doesn't report anything" do
+ output = begin
+ result = StringIO.new
+ result.close
+
+ $stderr = result
+
+ subject.error("Something went wrong")
+
+ result.string
+ ensure
+ $stderr = STDERR
+ end
+ expect(output).to_not eq("Something went wrong")
+ end
+ end
+ end
+
+ describe "threads" do
+ it "is thread safe when using with_level" do
+ stop_thr1 = false
+ stop_thr2 = false
+
+ expect(subject.level).to eq("debug")
+
+ thr1 = Thread.new do
+ subject.silence do
+ sleep(0.1) until stop_thr1
+ end
+
+ stop_thr2 = true
+ end
+
+ thr2 = Thread.new do
+ subject.silence do
+ stop_thr1 = true
+ sleep(0.1) until stop_thr2
+ end
+ end
+
+ [thr1, thr2].each(&:join)
+
+ expect(subject.level).to eq("debug")
+ end
+ end
+end
diff --git a/spec/bundler/bundler/ui_spec.rb b/spec/bundler/bundler/ui_spec.rb
new file mode 100644
index 0000000000..6df0d2e290
--- /dev/null
+++ b/spec/bundler/bundler/ui_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::UI do
+ describe Bundler::UI::Silent do
+ it "has the same instance methods as Shell" do
+ shell = Bundler::UI::Shell
+ methods = proc do |cls|
+ cls.instance_methods.map do |i|
+ m = shell.instance_method(i)
+ [i, m.parameters]
+ end.sort_by(&:first)
+ end
+ expect(methods.call(described_class)).to eq(methods.call(shell))
+ end
+
+ it "has the same instance class as Shell" do
+ shell = Bundler::UI::Shell
+ methods = proc do |cls|
+ cls.methods.map do |i|
+ m = shell.method(i)
+ [i, m.parameters]
+ end.sort_by(&:first)
+ end
+ expect(methods.call(described_class)).to eq(methods.call(shell))
+ end
+ end
+
+ describe Bundler::UI::Shell do
+ let(:options) { {} }
+ subject { described_class.new(options) }
+ describe "debug?" do
+ it "returns a boolean" do
+ subject.level = :debug
+ expect(subject.debug?).to eq(true)
+
+ subject.level = :error
+ expect(subject.debug?).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/uri_credentials_filter_spec.rb b/spec/bundler/bundler/uri_credentials_filter_spec.rb
new file mode 100644
index 0000000000..641f0addb4
--- /dev/null
+++ b/spec/bundler/bundler/uri_credentials_filter_spec.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::URICredentialsFilter do
+ subject { described_class }
+
+ describe "#credential_filtered_uri" do
+ shared_examples_for "original type of uri is maintained" do
+ it "maintains same type for return value as uri input type" do
+ expect(subject.credential_filtered_uri(uri)).to be_kind_of(uri.class)
+ end
+ end
+
+ shared_examples_for "sensitive credentials in uri are filtered out" do
+ context "authentication using oauth credentials" do
+ context "specified via 'x-oauth-basic'" do
+ let(:credentials) { "oauth_token:x-oauth-basic@" }
+
+ it "returns the uri without the oauth token" do
+ expect(subject.credential_filtered_uri(uri).to_s).to eq(Gem::URI("https://x-oauth-basic@github.com/company/private-repo").to_s)
+ end
+
+ it_behaves_like "original type of uri is maintained"
+ end
+
+ context "specified via 'x'" do
+ let(:credentials) { "oauth_token:x@" }
+
+ it "returns the uri without the oauth token" do
+ expect(subject.credential_filtered_uri(uri).to_s).to eq(Gem::URI("https://x@github.com/company/private-repo").to_s)
+ end
+
+ it_behaves_like "original type of uri is maintained"
+ end
+
+ context "specified without empty username" do
+ let(:credentials) { "oauth_token@" }
+
+ it "returns the uri without the oauth token" do
+ expect(subject.credential_filtered_uri(uri).to_s).to eq(Gem::URI("https://github.com/company/private-repo").to_s)
+ end
+
+ it_behaves_like "original type of uri is maintained"
+ end
+ end
+
+ context "authentication using login credentials" do
+ let(:credentials) { "username1:hunter3@" }
+
+ it "returns the uri without the password" do
+ expect(subject.credential_filtered_uri(uri).to_s).to eq(Gem::URI("https://username1@github.com/company/private-repo").to_s)
+ end
+
+ it_behaves_like "original type of uri is maintained"
+ end
+
+ context "authentication without credentials" do
+ let(:credentials) { "" }
+
+ it "returns the same uri" do
+ expect(subject.credential_filtered_uri(uri).to_s).to eq(uri.to_s)
+ end
+
+ it_behaves_like "original type of uri is maintained"
+ end
+ end
+
+ context "uri is a uri object" do
+ let(:uri) { Gem::URI("https://#{credentials}github.com/company/private-repo") }
+
+ it_behaves_like "sensitive credentials in uri are filtered out"
+ end
+
+ context "uri is a uri string" do
+ let(:uri) { "https://#{credentials}github.com/company/private-repo" }
+
+ it_behaves_like "sensitive credentials in uri are filtered out"
+ end
+
+ context "uri is a non-uri format string (ex. path)" do
+ let(:uri) { "/path/to/repo" }
+
+ it "returns the same uri" do
+ expect(subject.credential_filtered_uri(uri).to_s).to eq(uri.to_s)
+ end
+
+ it_behaves_like "original type of uri is maintained"
+ end
+
+ context "uri is nil" do
+ let(:uri) { nil }
+
+ it "returns nil" do
+ expect(subject.credential_filtered_uri(uri)).to be_nil
+ end
+
+ it_behaves_like "original type of uri is maintained"
+ end
+ end
+
+ describe "#credential_filtered_string" do
+ let(:str_to_filter) { "This is a git message containing a uri #{uri}!" }
+ let(:credentials) { "" }
+ let(:uri) { Gem::URI("https://#{credentials}github.com/company/private-repo") }
+
+ context "with a uri that contains credentials" do
+ let(:credentials) { "oauth_token:x-oauth-basic@" }
+
+ it "returns the string without the sensitive credentials" do
+ expect(subject.credential_filtered_string(str_to_filter, uri)).to eq(
+ "This is a git message containing a uri https://x-oauth-basic@github.com/company/private-repo!"
+ )
+ end
+ end
+
+ context "that does not contains credentials" do
+ it "returns the same string" do
+ expect(subject.credential_filtered_string(str_to_filter, uri)).to eq(str_to_filter)
+ end
+ end
+
+ context "string to filter is nil" do
+ let(:str_to_filter) { nil }
+
+ it "returns nil" do
+ expect(subject.credential_filtered_string(str_to_filter, uri)).to be_nil
+ end
+ end
+
+ context "uri to filter out is nil" do
+ let(:uri) { nil }
+
+ it "returns the same string" do
+ expect(subject.credential_filtered_string(str_to_filter, uri)).to eq(str_to_filter)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/uri_normalizer_spec.rb b/spec/bundler/bundler/uri_normalizer_spec.rb
new file mode 100644
index 0000000000..1308e86014
--- /dev/null
+++ b/spec/bundler/bundler/uri_normalizer_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::URINormalizer do
+ describe ".normalize_suffix" do
+ context "when trailing_slash is true" do
+ it "adds a trailing slash when missing" do
+ expect(described_class.normalize_suffix("https://example.com", trailing_slash: true)).to eq("https://example.com/")
+ end
+
+ it "keeps the trailing slash when present" do
+ expect(described_class.normalize_suffix("https://example.com/", trailing_slash: true)).to eq("https://example.com/")
+ end
+ end
+
+ context "when trailing_slash is false" do
+ it "removes a trailing slash when present" do
+ expect(described_class.normalize_suffix("https://example.com/", trailing_slash: false)).to eq("https://example.com")
+ end
+
+ it "keeps the value unchanged when no trailing slash exists" do
+ expect(described_class.normalize_suffix("https://example.com", trailing_slash: false)).to eq("https://example.com")
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/worker_spec.rb b/spec/bundler/bundler/worker_spec.rb
new file mode 100644
index 0000000000..2ad2845e37
--- /dev/null
+++ b/spec/bundler/bundler/worker_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require "bundler/worker"
+
+RSpec.describe Bundler::Worker do
+ let(:size) { 5 }
+ let(:name) { "Spec Worker" }
+ let(:function) { proc {|object, worker_number| [object, worker_number] } }
+ subject { described_class.new(size, name, function) }
+
+ after { subject.stop }
+
+ describe "#initialize" do
+ context "when Thread.start raises ThreadError" do
+ it "raises when no threads can be created" do
+ allow(Thread).to receive(:start).and_raise(ThreadError, "error creating thread")
+
+ expect { subject.enq "a" }.to raise_error(Bundler::ThreadCreationError, "Failed to create threads for the Spec Worker worker: error creating thread")
+ end
+ end
+ end
+
+ describe "priority queue" do
+ it "process elements from the priority queue first" do
+ processed_elements = []
+
+ function = proc do |element, _|
+ processed_elements << element
+ end
+
+ worker = described_class.new(1, "Spec Worker", function)
+ worker.instance_variable_set(:@threads, []) # Prevent the enqueueing from starting work.
+ worker.enq("Normal element")
+ worker.enq("Priority element", priority: true)
+ worker.send(:create_threads)
+
+ worker.stop
+
+ expect(processed_elements).to eq(["Priority element", "Normal element"])
+ end
+ end
+
+ describe "handling interrupts" do
+ let(:status) do
+ pid = Process.fork do
+ $stderr.reopen File.new("/dev/null", "w")
+ Signal.trap "INT", previous_interrupt_handler
+ subject.enq "a"
+ subject.stop unless interrupt_before_stopping
+ Process.kill "INT", Process.pid
+ end
+
+ Process.wait2(pid).last
+ end
+
+ before do
+ skip "requires Process.fork" unless Process.respond_to?(:fork)
+ end
+
+ context "when interrupted before stopping" do
+ let(:interrupt_before_stopping) { true }
+ let(:previous_interrupt_handler) { ->(*) { exit 0 } }
+
+ it "aborts" do
+ expect(status.exitstatus).to eq(1)
+ end
+ end
+
+ context "when interrupted after stopping" do
+ let(:interrupt_before_stopping) { false }
+
+ context "when the previous interrupt handler was the default" do
+ let(:previous_interrupt_handler) { "DEFAULT" }
+
+ it "uses the default interrupt handler" do
+ expect(status).to be_signaled
+ end
+ end
+
+ context "when the previous interrupt handler was customized" do
+ let(:previous_interrupt_handler) { ->(*) { exit 42 } }
+
+ it "restores the custom interrupt handler after stopping" do
+ expect(status.exitstatus).to eq(42)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/yaml_serializer_spec.rb b/spec/bundler/bundler/yaml_serializer_spec.rb
new file mode 100644
index 0000000000..9ff1579b76
--- /dev/null
+++ b/spec/bundler/bundler/yaml_serializer_spec.rb
@@ -0,0 +1,235 @@
+# frozen_string_literal: true
+
+require "bundler/yaml_serializer"
+
+RSpec.describe Bundler::YAMLSerializer do
+ subject(:serializer) { Bundler::YAMLSerializer }
+
+ describe "#dump" do
+ it "works for simple hash" do
+ hash = { "Q" => "Where does Thursday come before Wednesday? In the dictionary. :P" }
+
+ expected = <<~YAML
+ ---
+ Q: "Where does Thursday come before Wednesday? In the dictionary. :P"
+ YAML
+
+ expect(serializer.dump(hash)).to eq(expected)
+ end
+
+ it "handles nested hash" do
+ hash = {
+ "nice-one" => {
+ "read_ahead" => "All generalizations are false, including this one",
+ },
+ }
+
+ expected = <<~YAML
+ ---
+ nice-one:
+ read_ahead: "All generalizations are false, including this one"
+ YAML
+
+ expect(serializer.dump(hash)).to eq(expected)
+ end
+
+ it "array inside an hash" do
+ hash = {
+ "nested_hash" => {
+ "contains_array" => [
+ "Jack and Jill went up the hill",
+ "To fetch a pail of water.",
+ "Jack fell down and broke his crown,",
+ "And Jill came tumbling after.",
+ ],
+ },
+ }
+
+ expected = <<~YAML
+ ---
+ nested_hash:
+ contains_array:
+ - "Jack and Jill went up the hill"
+ - "To fetch a pail of water."
+ - "Jack fell down and broke his crown,"
+ - "And Jill came tumbling after."
+ YAML
+
+ expect(serializer.dump(hash)).to eq(expected)
+ end
+
+ it "handles empty array" do
+ hash = {
+ "empty_array" => [],
+ }
+
+ expected = <<~YAML
+ ---
+ empty_array: []
+ YAML
+
+ expect(serializer.dump(hash)).to eq(expected)
+ end
+ end
+
+ describe "#load" do
+ it "works for simple hash" do
+ yaml = <<~YAML
+ ---
+ Jon: "Air is free dude!"
+ Jack: "Yes.. until you buy a bag of chips!"
+ YAML
+
+ hash = {
+ "Jon" => "Air is free dude!",
+ "Jack" => "Yes.. until you buy a bag of chips!",
+ }
+
+ expect(serializer.load(yaml)).to eq(hash)
+ end
+
+ it "works for nested hash" do
+ yaml = <<~YAML
+ ---
+ baa:
+ baa: "black sheep"
+ have: "you any wool?"
+ yes: "merry have I"
+ three: "bags full"
+ YAML
+
+ hash = {
+ "baa" => {
+ "baa" => "black sheep",
+ "have" => "you any wool?",
+ "yes" => "merry have I",
+ },
+ "three" => "bags full",
+ }
+
+ expect(serializer.load(yaml)).to eq(hash)
+ end
+
+ it "handles colon in key/value" do
+ yaml = <<~YAML
+ BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/: http://example-mirror.rubygems.org
+ YAML
+
+ expect(serializer.load(yaml)).to eq("BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/" => "http://example-mirror.rubygems.org")
+ end
+
+ it "handles arrays inside hashes" do
+ yaml = <<~YAML
+ ---
+ nested_hash:
+ contains_array:
+ - "Why shouldn't you write with a broken pencil?"
+ - "Because it's pointless!"
+ YAML
+
+ hash = {
+ "nested_hash" => {
+ "contains_array" => [
+ "Why shouldn't you write with a broken pencil?",
+ "Because it's pointless!",
+ ],
+ },
+ }
+
+ expect(serializer.load(yaml)).to eq(hash)
+ end
+
+ it "handles windows-style CRLF line endings" do
+ yaml = <<~YAML.gsub("\n", "\r\n")
+ ---
+ nested_hash:
+ contains_array:
+ - "Why shouldn't you write with a broken pencil?"
+ - "Because it's pointless!"
+ - oh so silly
+ YAML
+
+ hash = {
+ "nested_hash" => {
+ "contains_array" => [
+ "Why shouldn't you write with a broken pencil?",
+ "Because it's pointless!",
+ "oh so silly",
+ ],
+ },
+ }
+
+ expect(serializer.load(yaml)).to eq(hash)
+ end
+
+ it "handles empty array" do
+ yaml = <<~YAML
+ ---
+ empty_array: []
+ YAML
+
+ hash = {
+ "empty_array" => [],
+ }
+
+ expect(serializer.load(yaml)).to eq(hash)
+ end
+
+ it "skip commented out words" do
+ yaml = <<~YAML
+ ---
+ foo: bar
+ buzz: foo # bar
+ YAML
+
+ hash = {
+ "foo" => "bar",
+ "buzz" => "foo",
+ }
+
+ expect(serializer.load(yaml)).to eq(hash)
+ end
+ end
+
+ describe "against yaml lib" do
+ let(:hash) do
+ {
+ "a_joke" => {
+ "my-stand" => "I can totally keep secrets",
+ "but" => "The people I tell them to can't :P",
+ "wouldn't it be funny if this string were empty?" => "",
+ },
+ "more" => {
+ "first" => [
+ "Can a kangaroo jump higher than a house?",
+ "Of course, a house doesn't jump at all.",
+ ],
+ "second" => [
+ "What did the sea say to the sand?",
+ "Nothing, it simply waved.",
+ ],
+ "array with empty string" => [""],
+ },
+ "sales" => {
+ "item" => "A Parachute",
+ "description" => "Only used once, never opened.",
+ },
+ "one-more" => "I'd tell you a chemistry joke but I know I wouldn't get a reaction.",
+ }
+ end
+
+ context "#load" do
+ it "retrieves the original hash" do
+ require "yaml"
+ expect(serializer.load(YAML.dump(hash))).to eq(hash)
+ end
+ end
+
+ context "#dump" do
+ it "retrieves the original hash" do
+ require "yaml"
+ expect(YAML.load(serializer.dump(hash))).to eq(hash)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/cache/cache_path_spec.rb b/spec/bundler/cache/cache_path_spec.rb
new file mode 100644
index 0000000000..2a280ea858
--- /dev/null
+++ b/spec/bundler/cache/cache_path_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle package" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ context "with --cache-path" do
+ it "caches gems at given path" do
+ bundle :cache, "cache-path" => "vendor/cache-foo"
+ expect(bundled_app("vendor/cache-foo/myrack-1.0.0.gem")).to exist
+ end
+ end
+
+ context "with config cache_path" do
+ it "caches gems at given path" do
+ bundle_config "cache_path vendor/cache-foo"
+ bundle :cache
+ expect(bundled_app("vendor/cache-foo/myrack-1.0.0.gem")).to exist
+ end
+ end
+
+ context "with absolute --cache-path" do
+ it "caches gems at given path" do
+ bundle :cache, "cache-path" => bundled_app("vendor/cache-foo")
+ expect(bundled_app("vendor/cache-foo/myrack-1.0.0.gem")).to exist
+ end
+ end
+end
diff --git a/spec/bundler/cache/gems_spec.rb b/spec/bundler/cache/gems_spec.rb
new file mode 100644
index 0000000000..198279d84c
--- /dev/null
+++ b/spec/bundler/cache/gems_spec.rb
@@ -0,0 +1,427 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle cache" do
+ shared_examples_for "when there are only gemsources" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ system_gems "myrack-1.0.0", path: path
+ bundle :cache
+ end
+
+ it "copies the .gem file to vendor/cache" do
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+
+ it "uses the cache as a source when installing gems" do
+ build_gem "omg", path: bundled_app("vendor/cache")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "omg"
+ G
+
+ expect(the_bundle).to include_gems "omg 1.0.0"
+ end
+
+ it "uses the cache as a source when installing gems with --local" do
+ system_gems [], path: default_bundle_path
+ bundle "install --local"
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "does not reinstall gems from the cache if they exist on the system" do
+ build_gem "myrack", "1.0.0", path: bundled_app("vendor/cache") do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "does not reinstall gems from the cache if they exist in the bundle" do
+ system_gems "myrack-1.0.0", path: default_bundle_path
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ build_gem "myrack", "1.0.0", path: bundled_app("vendor/cache") do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+
+ bundle :install, local: true
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "creates a lockfile" do
+ cache_gems "myrack-1.0.0"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "cache"
+
+ expect(bundled_app_lock).to exist
+ end
+ end
+
+ context "using system gems" do
+ before { bundle_config "path.system true" }
+ let(:path) { system_gem_path }
+ it_behaves_like "when there are only gemsources"
+ end
+
+ context "installing into a local path" do
+ before { bundle_config "path ./.bundle" }
+ let(:path) { local_gem_path }
+ it_behaves_like "when there are only gemsources"
+ end
+
+ describe "when there is a built-in gem", :ruby_repo do
+ let(:default_json_version) { ruby "gem 'json'; require 'json'; puts JSON::VERSION" }
+
+ before :each do
+ build_gem "json", default_json_version, to_system: true, default: true
+ end
+
+ context "when a remote gem is available for caching" do
+ before do
+ build_repo2 do
+ build_gem "json", default_json_version
+ end
+ end
+
+ it "uses remote gems when installing" do
+ install_gemfile %(source "https://gem.repo2"; gem 'json', '#{default_json_version}'), verbose: true
+ expect(out).to include("Installing json #{default_json_version}")
+ end
+
+ it "does not use remote gems when installing with --local flag" do
+ install_gemfile %(source "https://gem.repo2"; gem 'json', '#{default_json_version}'), verbose: true, local: true
+ expect(out).to include("Using json #{default_json_version}")
+ end
+
+ it "does not use remote gems when installing with --prefer-local flag" do
+ install_gemfile %(source "https://gem.repo2"; gem 'json', '#{default_json_version}'), verbose: true, "prefer-local": true
+ expect(out).to include("Using json #{default_json_version}")
+ end
+
+ it "caches remote and builtin gems" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'json', '#{default_json_version}'
+ gem 'myrack', '1.0.0'
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ expect(bundled_app("vendor/cache/json-#{default_json_version}.gem")).to exist
+ end
+
+ it "caches builtin gems when cache_all_platforms is set" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "json"
+ G
+
+ bundle_config "cache_all_platforms true"
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/json-#{default_json_version}.gem")).to exist
+ end
+
+ it "doesn't make remote request after caching the gem" do
+ build_gem "builtin_gem_2", "1.0.2", path: bundled_app("vendor/cache"), default: true
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'builtin_gem_2', '1.0.2'
+ G
+
+ bundle "install --local"
+ expect(the_bundle).to include_gems("builtin_gem_2 1.0.2")
+ end
+ end
+
+ context "when a remote gem is not available for caching" do
+ it "warns, but uses builtin gems when installing to system gems" do
+ bundle_config "path.system true"
+ install_gemfile %(source "https://gem.repo1"; gem 'json', '#{default_json_version}'), verbose: true
+ expect(err).to include("json-#{default_json_version} is built in to Ruby, and can't be cached")
+ expect(out).to include("Using json #{default_json_version}")
+ end
+
+ it "errors when explicitly caching" do
+ bundle_config "path.system true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'json', '#{default_json_version}'
+ G
+
+ bundle :cache, raise_on_error: false
+ expect(last_command).to be_failure
+ expect(err).to include("json-#{default_json_version} is built in to Ruby, and can't be cached")
+ end
+ end
+ end
+
+ describe "when there are also git sources" do
+ before do
+ build_git "foo"
+ system_gems "myrack-1.0.0"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ gem 'myrack'
+ G
+ end
+
+ it "still works" do
+ bundle :cache
+
+ system_gems []
+ bundle "install --local"
+
+ expect(the_bundle).to include_gems("myrack 1.0.0", "foo 1.0")
+ end
+
+ it "should not explode if the lockfile is not present" do
+ FileUtils.rm(bundled_app_lock)
+
+ bundle :cache
+
+ expect(bundled_app_lock).to exist
+ end
+ end
+
+ describe "when previously cached" do
+ let :setup_main_repo do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ gem "actionpack"
+ G
+ bundle :cache
+ expect(cached_gem("myrack-1.0.0")).to exist
+ expect(cached_gem("actionpack-2.3.2")).to exist
+ expect(cached_gem("activesupport-2.3.2")).to exist
+ end
+
+ it "re-caches during install" do
+ setup_main_repo
+ FileUtils.rm_rf cached_gem("myrack-1.0.0")
+ bundle :install
+ expect(out).to include("Updating files in vendor/cache")
+ expect(cached_gem("myrack-1.0.0")).to exist
+ end
+
+ it "adds and removes when gems are updated" do
+ setup_main_repo
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ bundle "update", all: true
+ expect(cached_gem("myrack-1.2")).to exist
+ expect(cached_gem("myrack-1.0.0")).not_to exist
+ end
+
+ it "adds new gems and dependencies" do
+ setup_main_repo
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails"
+ G
+ expect(cached_gem("rails-2.3.2")).to exist
+ expect(cached_gem("activerecord-2.3.2")).to exist
+ end
+
+ it "removes .gems for removed gems and dependencies" do
+ setup_main_repo
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+ expect(cached_gem("myrack-1.0.0")).to exist
+ expect(cached_gem("actionpack-2.3.2")).not_to exist
+ expect(cached_gem("activesupport-2.3.2")).not_to exist
+ end
+
+ it "removes .gems when gem changes to git source" do
+ setup_main_repo
+ build_git "myrack"
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack", :git => "#{lib_path("myrack-1.0")}"
+ gem "actionpack"
+ G
+ expect(cached_gem("myrack-1.0.0")).not_to exist
+ expect(cached_gem("actionpack-2.3.2")).to exist
+ expect(cached_gem("activesupport-2.3.2")).to exist
+ end
+
+ it "doesn't remove gems that are for another platform" do
+ simulate_platform "java" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ bundle :cache
+ expect(cached_gem("platform_specific-1.0-java")).to exist
+ end
+
+ pristine_system_gems
+
+ simulate_platform "x86-darwin-100" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ expect(cached_gem("platform_specific-1.0-x86-darwin-100")).to exist
+ expect(cached_gem("platform_specific-1.0-java")).to exist
+ end
+ end
+
+ it "doesn't remove gems cached gems that don't match their remote counterparts, but also refuses to install and prints an error" do
+ setup_main_repo
+ cached_myrack = cached_gem("myrack-1.0.0")
+ FileUtils.rm_rf cached_myrack
+ build_gem "myrack", "1.0.0",
+ path: cached_myrack.parent,
+ rubygems_version: "1.3.2"
+
+ FileUtils.rm_r default_bundle_path
+ default_system_gems
+
+ FileUtils.rm bundled_app_lock
+ bundle :install, raise_on_error: false
+
+ expect(err).to eq <<~E.strip
+ Bundler found mismatched checksums. This is a potential security risk.
+ #{checksum_to_lock(gem_repo2, "myrack", "1.0.0")}
+ from the API at https://gem.repo2/
+ #{checksum_from_package(cached_myrack, "myrack", "1.0.0")}
+ from the gem at #{cached_myrack}
+
+ If you trust the API at https://gem.repo2/, to resolve this issue you can:
+ 1. remove the gem at #{cached_myrack}
+ 2. run `bundle install`
+
+ To ignore checksum security warnings, disable checksum validation with
+ `bundle config set --local disable_checksum_validation true`
+ E
+
+ expect(cached_gem("myrack-1.0.0")).to exist
+ end
+
+ it "raises an error when a cached gem is altered and produces a different checksum than the remote gem" do
+ setup_main_repo
+ FileUtils.rm_rf cached_gem("myrack-1.0.0")
+ build_gem "myrack", "1.0.0", path: bundled_app("vendor/cache")
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo1, "myrack", "1.0.0"
+ end
+
+ FileUtils.rm_r default_bundle_path
+ default_system_gems
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+ #{checksums}
+ L
+
+ bundle :install, raise_on_error: false
+ expect(exitstatus).to eq(37)
+ expect(err).to include("Bundler found mismatched checksums.")
+ expect(err).to include("1. remove the gem at #{cached_gem("myrack-1.0.0")}")
+
+ expect(cached_gem("myrack-1.0.0")).to exist
+ FileUtils.rm_rf cached_gem("myrack-1.0.0")
+ bundle :install
+ expect(cached_gem("myrack-1.0.0")).to exist
+ end
+
+ it "installs a modified gem with a non-matching checksum when the API implementation does not provide checksums" do
+ setup_main_repo
+ FileUtils.rm_rf cached_gem("myrack-1.0.0")
+ build_gem "myrack", "1.0.0", path: bundled_app("vendor/cache")
+ pristine_system_gems
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+ L
+
+ bundle :install, artifice: "endpoint", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
+ expect(cached_gem("myrack-1.0.0")).to exist
+ end
+
+ it "handles directories and non .gem files in the cache" do
+ setup_main_repo
+ bundled_app("vendor/cache/foo").mkdir
+ File.open(bundled_app("vendor/cache/bar"), "w") {|f| f.write("not a gem") }
+ bundle :cache
+ end
+
+ it "does not say that it is removing gems when it isn't actually doing so" do
+ setup_main_repo
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ bundle "cache"
+ bundle "install"
+ expect(out).not_to match(/removing/i)
+ end
+
+ it "does not warn about all if it doesn't have any git/path dependency" do
+ setup_main_repo
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ bundle "cache"
+ expect(out).not_to match(/\-\-all/)
+ end
+
+ it "should install gems with the name bundler in them (that aren't bundler)" do
+ build_gem "foo-bundler", "1.0",
+ path: bundled_app("vendor/cache")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo-bundler"
+ G
+
+ expect(the_bundle).to include_gems "foo-bundler 1.0"
+ end
+ end
+end
diff --git a/spec/bundler/cache/git_spec.rb b/spec/bundler/cache/git_spec.rb
new file mode 100644
index 0000000000..f0976ecac7
--- /dev/null
+++ b/spec/bundler/cache/git_spec.rb
@@ -0,0 +1,511 @@
+# frozen_string_literal: true
+
+RSpec.describe "git base name" do
+ it "base_name should strip private repo uris" do
+ source = Bundler::Source::Git.new("uri" => "git@github.com:bundler.git")
+ expect(source.send(:base_name)).to eq("bundler")
+ end
+
+ it "base_name should strip network share paths" do
+ source = Bundler::Source::Git.new("uri" => "//MachineName/ShareFolder")
+ expect(source.send(:base_name)).to eq("ShareFolder")
+ end
+end
+
+RSpec.describe "bundle cache with git" do
+ it "does not copy repository to vendor cache when cache_all set to false" do
+ git = build_git "foo"
+ ref = git.ref_for("main", 11)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle_config "cache_all false"
+ bundle :cache
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).not_to exist
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "copies repository to vendor cache and uses it" do
+ git = build_git "foo"
+ ref = git.ref_for("main", 11)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.git")).not_to exist
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.bundlecache")).to be_file
+
+ FileUtils.rm_r lib_path("foo-1.0")
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "copies repository to vendor cache and uses it even when configured with `path`" do
+ git = build_git "foo"
+ ref = git.ref_for("main", 11)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.git")).not_to exist
+
+ FileUtils.rm_r lib_path("foo-1.0")
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "runs twice without exploding" do
+ build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :cache
+ bundle :cache
+
+ expect(out).to include "Updating files in vendor/cache"
+ FileUtils.rm_r lib_path("foo-1.0")
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "tracks updates" do
+ git = build_git "foo"
+ old_ref = git.ref_for("main", 11)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :cache
+
+ update_git "foo" do |s|
+ s.write "lib/foo.rb", "puts :CACHE"
+ end
+
+ ref = git.ref_for("main", 11)
+ expect(ref).not_to eq(old_ref)
+
+ bundle "update", all: true
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist
+ expect(bundled_app("vendor/cache/foo-1.0-#{old_ref}")).not_to exist
+
+ FileUtils.rm_r lib_path("foo-1.0")
+ run "require 'foo'"
+ expect(out).to eq("CACHE")
+ end
+
+ it "tracks updates when specifying the gem" do
+ git = build_git "foo"
+ old_ref = git.ref_for("main", 11)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :cache
+
+ update_git "foo" do |s|
+ s.write "lib/foo.rb", "puts :CACHE"
+ end
+
+ ref = git.ref_for("main", 11)
+ expect(ref).not_to eq(old_ref)
+
+ bundle "update foo"
+
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist
+ expect(bundled_app("vendor/cache/foo-1.0-#{old_ref}")).not_to exist
+
+ FileUtils.rm_r lib_path("foo-1.0")
+ run "require 'foo'"
+ expect(out).to eq("CACHE")
+ end
+
+ it "uses the local repository to generate the cache" do
+ git = build_git "foo"
+ ref = git.ref_for("main", 11)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-invalid")}', :branch => :main
+ G
+
+ bundle %(config set local.foo #{lib_path("foo-1.0")})
+ bundle "install"
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/foo-invalid-#{ref}")).to exist
+
+ # Updating the local still uses the local.
+ update_git "foo" do |s|
+ s.write "lib/foo.rb", "puts :LOCAL"
+ end
+
+ run "require 'foo'"
+ expect(out).to eq("LOCAL")
+ end
+
+ it "can use gems after copying install folder to a different machine with git not installed" do
+ build_git "foo"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle_config "path vendor/bundle"
+ bundle :install
+
+ pristine_system_gems
+ with_path_as "" do
+ bundle_config "deployment true"
+ bundle "install --local"
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+ end
+
+ it "can install after bundle cache without cloning remote repositories" do
+ build_git "foo"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle :cache, "all-platforms" => true
+
+ pristine_system_gems
+ bundle_config "frozen true"
+ bundle "install --local --verbose"
+ expect(out).to_not include("Fetching")
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+
+ it "can install after bundle cache without cloning remote repositories even without the original cache" do
+ build_git "foo"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle :cache, "all-platforms" => true
+
+ pristine_system_gems
+ bundle_config "frozen true"
+ bundle "install --local --verbose"
+ expect(out).to_not include("Fetching")
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+
+ it "can install after bundle cache without cloning remote repositories with only git tracked files" do
+ build_git "foo"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle :cache, "all-platforms" => true
+
+ pristine_system_gems
+ bundle_config "frozen true"
+
+ # Remove untracked files (including the empty refs dir in the cache)
+ Dir.chdir(bundled_app) do
+ system(*%W[git init --quiet])
+ system(*%W[git add --all])
+ system(*%W[git clean -d --force --quiet])
+ end
+
+ bundle "install --local --verbose"
+ expect(out).to_not include("Fetching")
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+
+ it "installs properly a bundler 2.5.17-2.5.23 cache as a bare repository without cloning remote repositories" do
+ git = build_git "foo"
+
+ short_ref = git.ref_for("main", 11)
+ cache_dir = bundled_app("vendor/cache/foo-1.0-#{short_ref}")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle_config "global_gem_cache false"
+ bundle_config "path vendor/bundle"
+ bundle :install
+
+ # Simulate old cache by copying the real cache folder to vendor/cache
+ FileUtils.mkdir_p bundled_app("vendor/cache")
+ FileUtils.cp_r "#{Dir.glob(vendored_gems("cache/bundler/git/foo-1.0-*")).first}/.", cache_dir
+ FileUtils.rm_r bundled_app("vendor/bundle")
+
+ bundle "install --local --verbose"
+ expect(err).to include("Installing from cache in old \"bare repository\" format for compatibility")
+
+ expect(out).to_not include("Fetching")
+
+ # leaves old cache alone
+ expect(cache_dir.join("lib/foo.rb")).not_to exist
+ expect(cache_dir.join("HEAD")).to exist
+
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+
+ it "migrates a bundler 2.5.17-2.5.23 cache as a bare repository when not running with --local" do
+ git = build_git "foo"
+
+ short_ref = git.ref_for("main", 11)
+ cache_dir = bundled_app("vendor/cache/foo-1.0-#{short_ref}")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle_config "global_gem_cache false"
+ bundle_config "path vendor/bundle"
+ bundle :install
+
+ # Simulate old cache by copying the real cache folder to vendor/cache
+ FileUtils.mkdir_p bundled_app("vendor/cache")
+ FileUtils.cp_r "#{Dir.glob(vendored_gems("cache/bundler/git/foo-1.0-*")).first}/.", cache_dir
+ FileUtils.rm_r bundled_app("vendor/bundle")
+
+ bundle "install --verbose"
+ expect(out).to include("Fetching")
+
+ # migrates old cache alone
+ expect(cache_dir.join("lib/foo.rb")).to exist
+ expect(cache_dir.join("HEAD")).not_to exist
+
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+
+ it "migrates a bundler 2.5.17-2.5.23 cache as a bare repository when running `bundle cache`, even if gems already installed" do
+ git = build_git "foo"
+
+ short_ref = git.ref_for("main", 11)
+ cache_dir = bundled_app("vendor/cache/foo-1.0-#{short_ref}")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle_config "global_gem_cache false"
+ bundle_config "path vendor/bundle"
+ bundle :install
+
+ # Simulate old cache by copying the real cache folder to vendor/cache
+ FileUtils.mkdir_p bundled_app("vendor/cache")
+ FileUtils.cp_r "#{Dir.glob(vendored_gems("cache/bundler/git/foo-1.0-*")).first}/.", cache_dir
+
+ bundle "cache"
+
+ # migrates old cache alone
+ expect(cache_dir.join("lib/foo.rb")).to exist
+ expect(cache_dir.join("HEAD")).not_to exist
+
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+
+ it "copies repository to vendor cache, including submodules" do
+ # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/
+ system(*%W[git config --global protocol.file.allow always])
+
+ build_git "submodule", "1.0"
+
+ git = build_git "has_submodule", "1.0" do |s|
+ s.add_dependency "submodule"
+ end
+
+ git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0")
+ git "commit -m \"submodulator\"", lib_path("has_submodule-1.0")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("has_submodule-1.0")}", :submodules => true do
+ gem "has_submodule"
+ end
+ G
+
+ ref = git.ref_for("main", 11)
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/has_submodule-1.0-#{ref}")).to exist
+ expect(bundled_app("vendor/cache/has_submodule-1.0-#{ref}/submodule-1.0")).to exist
+ expect(the_bundle).to include_gems "has_submodule 1.0"
+ end
+
+ it "caches pre-evaluated gemspecs" do
+ git = build_git "foo"
+
+ # Insert a gemspec method that shells out
+ spec_lines = lib_path("foo-1.0/foo.gemspec").read.split("\n")
+ spec_lines.insert(-2, "s.description = `echo bob`")
+ update_git("foo") {|s| s.write "foo.gemspec", spec_lines.join("\n") }
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle :cache
+
+ ref = git.ref_for("main", 11)
+ gemspec = bundled_app("vendor/cache/foo-1.0-#{ref}/foo.gemspec").read
+ expect(gemspec).to_not match("`echo bob`")
+ end
+
+ it "can install after bundle cache with git not installed" do
+ build_git "foo"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle :cache, "all-platforms" => true, :install => false
+
+ pristine_system_gems
+ with_path_as "" do
+ bundle_config "deployment true"
+ bundle :install, local: true
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+ end
+
+ it "can install after bundle cache generated with an older Bundler that kept checkouts in the cache" do
+ git = build_git("foo")
+ locked_revision = git.ref_for("main")
+ path_revision = git.ref_for("main", 11)
+
+ git_path = lib_path("foo-1.0")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{git_path}'
+ G
+ lockfile <<~L
+ GIT
+ remote: #{git_path}/
+ revision: #{locked_revision}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ # Simulate an old incorrect situation where vendor/cache would be the install location of git gems
+ FileUtils.mkdir_p bundled_app("vendor/cache")
+ FileUtils.cp_r git_path, bundled_app("vendor/cache/foo-1.0-#{path_revision}")
+ FileUtils.rm_r bundled_app("vendor/cache/foo-1.0-#{path_revision}/.git")
+
+ bundle :install, env: { "BUNDLE_DEPLOYMENT" => "true", "BUNDLE_CACHE_ALL" => "true" }
+ end
+
+ it "respects the --no-install flag" do
+ git = build_git "foo", &:add_c_extension
+ ref = git.ref_for("main", 11)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+
+ # The algorithm for the cache location for a git checkout is
+ # in Bundle::Source::Git#cache_path
+ cache_path_name = "foo-1.0-#{Digest(:SHA1).hexdigest(lib_path("foo-1.0").to_s)}"
+
+ # Run this test twice. This is because materially different codepaths
+ # will get hit the second time around.
+ # The first time, Bundler::Sources::Git#install_path is set to the system
+ # wide cache directory bundler/gems; the second time, it's set to the
+ # vendor/cache directory. We don't want the native extension to appear in
+ # either of these places, so run the `bundle cache` command twice.
+ 2.times do
+ bundle :cache, "all-platforms" => true, :install => false
+
+ # it did _NOT_ actually install the gem - neither in $GEM_HOME (bundler 2 mode),
+ # nor in .bundle (bundler 4 mode)
+ expect(Pathname.new(File.join(default_bundle_path, "gems/foo-1.0-#{ref}"))).to_not exist
+ # it _did_ cache the gem in vendor/
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist
+ # it did _NOT_ build the gems extensions in the vendor/ dir
+ expect(Dir[bundled_app("vendor/cache/foo-1.0-#{ref}/lib/foo_c*")]).to be_empty
+ # it _did_ cache the git checkout
+ expect(default_cache_path("git", cache_path_name)).to exist
+ # And the checkout is a bare checkout
+ expect(default_cache_path("git", cache_path_name, "HEAD")).to exist
+ end
+
+ # Subsequently installing the gem should compile it.
+ # _currently_, the gem gets compiled in vendor/cache, and vendor/cache is added
+ # to the $LOAD_PATH for git extensions, so it all kind of "works". However, in the
+ # future we would like to stop adding vendor/cache to the $LOAD_PATH for git extensions
+ # and instead treat them identically to normal gems (where the gem install location,
+ # not the cache location, is added to $LOAD_PATH).
+ # Verify that the compilation worked and the result is in $LOAD_PATH by simply attempting
+ # to require it; that should make sure this spec does not break if the load path behaviour
+ # is changed.
+ bundle :install, local: true
+ ruby <<~R, raise_on_error: false
+ require 'bundler/setup'
+ require 'foo_c'
+ R
+ expect(last_command).to_not be_failure
+ end
+
+ it "doesn't fail when git gem has extensions and an empty cache folder is present before bundle install" do
+ build_git "puma" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.executables = "puma"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("../lib", __FILE__)
+ FileUtils.mkdir_p(path)
+ File.open("\#{path}/puma.rb", "w") do |f|
+ f.puts "PUMA = 'YES'"
+ end
+ end
+ RUBY
+ end
+
+ FileUtils.mkdir_p(bundled_app("vendor/cache"))
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "puma", :git => "#{lib_path("puma-1.0")}"
+ G
+
+ bundle "exec puma"
+
+ expect(out).to eq("YES")
+ end
+end
diff --git a/spec/bundler/cache/path_spec.rb b/spec/bundler/cache/path_spec.rb
new file mode 100644
index 0000000000..42648aea1f
--- /dev/null
+++ b/spec/bundler/cache/path_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle cache with path" do
+ it "is no-op when the path is within the bundle" do
+ build_lib "foo", path: bundled_app("lib/foo")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => '#{bundled_app("lib/foo")}'
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/foo-1.0")).not_to exist
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "copies when the path is outside the bundle " do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/foo-1.0")).to exist
+ expect(bundled_app("vendor/cache/foo-1.0/.bundlecache")).to be_file
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "copies when the path is outside the bundle and the paths intersect" do
+ libname = File.basename(bundled_app) + "_gem"
+ libpath = File.join(File.dirname(bundled_app), libname)
+
+ build_lib libname, path: libpath
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "#{libname}", :path => '#{libpath}'
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/#{libname}")).to exist
+ expect(bundled_app("vendor/cache/#{libname}/.bundlecache")).to be_file
+
+ expect(the_bundle).to include_gems "#{libname} 1.0"
+ end
+
+ it "updates the path on each cache" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :cache
+
+ build_lib "foo" do |s|
+ s.write "lib/foo.rb", "puts :CACHE"
+ end
+
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/foo-1.0")).to exist
+
+ run "require 'foo'"
+ expect(out).to eq("CACHE")
+ end
+
+ it "removes stale entries cache" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/foo-1.0")).to exist
+
+ build_lib "bar"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "bar", :path => '#{lib_path("bar-1.0")}'
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/foo-1.0")).not_to exist
+ end
+
+ it "does not cache path gems if cache_all is set to false" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => '#{lib_path("foo-1.0")}'
+ G
+ bundle_config "cache_all false"
+
+ bundle :cache
+ expect(err).to be_empty
+ expect(bundled_app("vendor/cache/foo-1.0")).not_to exist
+ end
+
+ it "caches path gems by default" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :cache
+ expect(err).to be_empty
+ expect(bundled_app("vendor/cache/foo-1.0")).to exist
+ end
+end
diff --git a/spec/bundler/cache/platform_spec.rb b/spec/bundler/cache/platform_spec.rb
new file mode 100644
index 0000000000..71c0eaee8e
--- /dev/null
+++ b/spec/bundler/cache/platform_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle cache with multiple platforms" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ platforms :mri, :rbx do
+ gem "myrack", "1.0.0"
+ end
+
+ platforms :jruby do
+ gem "activesupport", "2.3.5"
+ end
+ G
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+ activesupport (2.3.5)
+
+ PLATFORMS
+ ruby
+ java
+
+ DEPENDENCIES
+ myrack (1.0.0)
+ activesupport (2.3.5)
+ G
+
+ cache_gems "myrack-1.0.0", "activesupport-2.3.5"
+ end
+
+ it "ensures that a successful bundle install does not delete gems for other platforms" do
+ bundle "install"
+
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ expect(bundled_app("vendor/cache/activesupport-2.3.5.gem")).to exist
+ end
+
+ it "ensures that a successful bundle update does not delete gems for other platforms" do
+ bundle "update", all: true
+
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ expect(bundled_app("vendor/cache/activesupport-2.3.5.gem")).to exist
+ end
+end
diff --git a/spec/bundler/commands/add_spec.rb b/spec/bundler/commands/add_spec.rb
new file mode 100644
index 0000000000..162650f2e5
--- /dev/null
+++ b/spec/bundler/commands/add_spec.rb
@@ -0,0 +1,448 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle add" do
+ before :each do
+ build_repo2 do
+ build_gem "foo", "1.1"
+ build_gem "foo", "2.0"
+ build_gem "baz", "1.2.3"
+ build_gem "bar", "0.12.3"
+ build_gem "cat", "0.12.3.pre"
+ build_gem "dog", "1.1.3.pre"
+ build_gem "lemur", "3.1.1.pre.2023.1.1"
+ end
+
+ build_git "foo", "2.0"
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "weakling", "~> 0.0.1"
+ G
+ end
+
+ context "when no gems are specified" do
+ it "shows error" do
+ bundle "add", raise_on_error: false
+
+ expect(err).to include("Please specify gems to add")
+ end
+ end
+
+ context "when Gemfile is empty, and frozen mode is set" do
+ it "shows error" do
+ gemfile 'source "https://gem.repo2"'
+ bundle "add bar", raise_on_error: false, env: { "BUNDLE_FROZEN" => "true" }
+
+ expect(err).to include("Frozen mode is set, but there's no lockfile")
+ end
+ end
+
+ describe "without version specified" do
+ it "version requirement becomes >= major.minor.patch when resolved version is < 1.0" do
+ bundle "add 'bar'"
+ expect(bundled_app_gemfile.read).to match(/gem "bar", ">= 0.12.3"/)
+ expect(the_bundle).to include_gems "bar 0.12.3"
+ end
+
+ it "version requirement becomes >= major.minor when resolved version is > 1.0" do
+ bundle "add 'baz'"
+ expect(bundled_app_gemfile.read).to match(/gem "baz", ">= 1.2"/)
+ expect(the_bundle).to include_gems "baz 1.2.3"
+ end
+
+ it "version requirement becomes >= major.minor.patch.pre when resolved version is < 1.0" do
+ bundle "add 'cat'"
+ expect(bundled_app_gemfile.read).to match(/gem "cat", ">= 0.12.3.pre"/)
+ expect(the_bundle).to include_gems "cat 0.12.3.pre"
+ end
+
+ it "version requirement becomes >= major.minor.pre when resolved version is >= 1.0.pre" do
+ bundle "add 'dog'"
+ expect(bundled_app_gemfile.read).to match(/gem "dog", ">= 1.1.pre"/)
+ expect(the_bundle).to include_gems "dog 1.1.3.pre"
+ end
+
+ it "version requirement becomes >= major.minor.pre.tail when resolved version has a very long tail pre version" do
+ bundle "add 'lemur'"
+ # the trailing pre purposely matches the release version to ensure that subbing the release doesn't change the pre.version"
+ expect(bundled_app_gemfile.read).to match(/gem "lemur", ">= 3.1.pre.2023.1.1"/)
+ expect(the_bundle).to include_gems "lemur 3.1.1.pre.2023.1.1"
+ end
+ end
+
+ describe "with --version" do
+ it "adds dependency of specified version and runs install" do
+ bundle "add 'foo' --version='~> 1.0'"
+ expect(bundled_app_gemfile.read).to match(/gem "foo", "~> 1.0"/)
+ expect(the_bundle).to include_gems "foo 1.1"
+ end
+
+ it "adds multiple version constraints when specified" do
+ requirements = ["< 3.0", "> 1.0"]
+ bundle "add 'foo' --version='#{requirements.join(", ")}'"
+ expect(bundled_app_gemfile.read).to match(/gem "foo", #{Gem::Requirement.new(requirements).as_list.map(&:dump).join(", ")}/)
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --require" do
+ it "adds the require param for the gem" do
+ bundle "add 'foo' --require=foo/engine"
+ expect(bundled_app_gemfile.read).to match(%r{gem "foo",(?: .*,) require: "foo\/engine"})
+ end
+
+ it "converts false to a boolean" do
+ bundle "add 'foo' --require=false"
+ expect(bundled_app_gemfile.read).to match(/gem "foo",(?: .*,) require: false/)
+ end
+ end
+
+ describe "with --group" do
+ it "adds dependency for the specified group" do
+ bundle "add 'foo' --group='development'"
+ expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2.0", group: :development/)
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+
+ it "adds dependency to more than one group" do
+ bundle "add 'foo' --group='development, test'"
+ expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2.0", groups: \[:development, :test\]/)
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --source" do
+ it "adds dependency with specified source" do
+ bundle "add 'foo' --source='https://gem.repo2'"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "foo", ">= 2.0", source: "https://gem.repo2"})
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --path" do
+ it "adds dependency with specified path" do
+ bundle "add 'foo' --path='#{lib_path("foo-2.0")}'"
+
+ expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2.0", path: "#{lib_path("foo-2.0")}"/)
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --git" do
+ it "adds dependency with specified git source" do
+ bundle "add foo --git=#{lib_path("foo-2.0")}"
+
+ expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2.0", git: "#{lib_path("foo-2.0")}"/)
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --git and --branch" do
+ before do
+ update_git "foo", "2.0", branch: "test"
+ end
+
+ it "adds dependency with specified git source and branch" do
+ bundle "add foo --git=#{lib_path("foo-2.0")} --branch=test"
+
+ expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2.0", git: "#{lib_path("foo-2.0")}", branch: "test"/)
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --git and --ref" do
+ it "adds dependency with specified git source and branch" do
+ bundle "add foo --git=#{lib_path("foo-2.0")} --ref=#{revision_for(lib_path("foo-2.0"))}"
+
+ expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2\.0", git: "#{lib_path("foo-2.0")}", ref: "#{revision_for(lib_path("foo-2.0"))}"/)
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --github" do
+ before do
+ build_git "rake", "13.0"
+ git("config --global url.file://#{lib_path("rake-13.0")}.insteadOf https://github.com/ruby/rake.git")
+ end
+
+ it "adds dependency with specified github source" do
+ bundle "add rake --github=ruby/rake"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake"})
+ end
+
+ it "adds dependency with specified github source and branch" do
+ bundle "add rake --github=ruby/rake --branch=main"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake", branch: "main"})
+ end
+
+ it "adds dependency with specified github source and ref" do
+ ref = revision_for(lib_path("rake-13.0"))
+ bundle "add rake --github=ruby/rake --ref=#{ref}"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake", ref: "#{ref}"})
+ end
+
+ it "adds dependency with specified github source and glob" do
+ bundle "add rake --github=ruby/rake --glob='./*.gemspec'"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake", glob: "\.\/\*\.gemspec"})
+ end
+
+ it "adds dependency with specified github source, branch and glob" do
+ bundle "add rake --github=ruby/rake --branch=main --glob='./*.gemspec'"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake", branch: "main", glob: "\.\/\*\.gemspec"})
+ end
+
+ it "adds dependency with specified github source, ref and glob" do
+ ref = revision_for(lib_path("rake-13.0"))
+ bundle "add rake --github=ruby/rake --ref=#{ref} --glob='./*.gemspec'"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake", ref: "#{ref}", glob: "\.\/\*\.gemspec"})
+ end
+ end
+
+ describe "with --git and --glob" do
+ it "adds dependency with specified git source" do
+ bundle "add foo --git=#{lib_path("foo-2.0")} --glob='./*.gemspec'"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "foo", ">= 2.0", git: "#{lib_path("foo-2.0")}", glob: "\./\*\.gemspec"})
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --git and --branch and --glob" do
+ before do
+ update_git "foo", "2.0", branch: "test"
+ end
+
+ it "adds dependency with specified git source and branch" do
+ bundle "add foo --git=#{lib_path("foo-2.0")} --branch=test --glob='./*.gemspec'"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "foo", ">= 2.0", git: "#{lib_path("foo-2.0")}", branch: "test", glob: "\./\*\.gemspec"})
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --git and --ref and --glob" do
+ it "adds dependency with specified git source and branch" do
+ bundle "add foo --git=#{lib_path("foo-2.0")} --ref=#{revision_for(lib_path("foo-2.0"))} --glob='./*.gemspec'"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "foo", ">= 2\.0", git: "#{lib_path("foo-2.0")}", ref: "#{revision_for(lib_path("foo-2.0"))}", glob: "\./\*\.gemspec"})
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with mismatched pair in --git/--github, --branch/--ref" do
+ describe "with --git and --github" do
+ it "throws error" do
+ bundle "add 'foo' --git x --github y", raise_on_error: false
+
+ expect(err).to include("You cannot specify `--git` and `--github` at the same time.")
+ end
+ end
+
+ describe "with --branch and --ref with --git" do
+ it "throws error" do
+ bundle "add 'foo' --branch x --ref y --git file://git", raise_on_error: false
+
+ expect(err).to include("You cannot specify `--branch` and `--ref` at the same time.")
+ end
+ end
+
+ describe "with --branch but without --git or --github" do
+ it "throws error" do
+ bundle "add 'foo' --branch x", raise_on_error: false
+
+ expect(err).to include("You cannot specify `--branch` unless `--git` or `--github` is specified.")
+ end
+ end
+
+ describe "with --ref but without --git or --github" do
+ it "throws error" do
+ bundle "add 'foo' --ref y", raise_on_error: false
+
+ expect(err).to include("You cannot specify `--ref` unless `--git` or `--github` is specified.")
+ end
+ end
+ end
+
+ describe "with --skip-install" do
+ it "adds gem to Gemfile but is not installed" do
+ bundle "add foo --skip-install --version=2.0"
+
+ expect(bundled_app_gemfile.read).to match(/gem "foo", "= 2.0"/)
+ expect(the_bundle).to_not include_gems "foo 2.0"
+ end
+ end
+
+ it "using combination of short form options works like long form" do
+ bundle "add 'foo' -s='https://gem.repo2' -g='development' -v='~>1.0'"
+ expect(bundled_app_gemfile.read).to include %(gem "foo", "~> 1.0", group: :development, source: "https://gem.repo2")
+ expect(the_bundle).to include_gems "foo 1.1"
+ end
+
+ it "shows error message when version is not formatted correctly" do
+ bundle "add 'foo' -v='~>1 . 0'", raise_on_error: false
+ expect(err).to match("Invalid gem requirement pattern '~>1 . 0'")
+ end
+
+ it "shows error message when gem cannot be found" do
+ bundle_config "force_ruby_platform true"
+ bundle "add 'werk_it'", raise_on_error: false
+ expect(err).to match("Could not find gem 'werk_it' in")
+
+ bundle "add 'werk_it' -s='https://gem.repo2'", raise_on_error: false
+ expect(err).to match("Could not find gem 'werk_it' in rubygems repository")
+ end
+
+ it "shows error message when source cannot be reached" do
+ bundle "add 'baz' --source='http://badhostasdf'", raise_on_error: false, artifice: "fail"
+ expect(err).to include("Could not reach host badhostasdf. Check your network connection and try again.")
+
+ bundle "add 'baz' --source='file://does/not/exist'", raise_on_error: false
+ expect(err).to include("Could not fetch specs from file://does/not/exist/")
+ end
+
+ describe "with --optimistic" do
+ it "ignores option" do
+ bundle "add 'foo' --optimistic"
+ expect(bundled_app_gemfile.read).to include %(gem "foo", ">= 2.0")
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --pessimistic" do
+ it "adds pessimistic version" do
+ bundle "add 'foo' --pessimistic"
+ expect(bundled_app_gemfile.read).to include %(gem "foo", "~> 2.0")
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --quiet option" do
+ it "is quiet when there are no warnings" do
+ bundle "add 'foo' --quiet"
+ expect(out).to be_empty
+ expect(err).to be_empty
+ end
+
+ it "still displays warning and errors" do
+ create_file("add_with_warning.rb", <<~RUBY)
+ require "#{lib_dir}/bundler"
+ require "#{lib_dir}/bundler/cli"
+ require "#{lib_dir}/bundler/cli/add"
+
+ module RunWithWarning
+ def run
+ super
+ rescue
+ Bundler.ui.warn "This is a warning"
+ raise
+ end
+ end
+
+ Bundler::CLI::Add.prepend(RunWithWarning)
+ RUBY
+
+ bundle "add 'non-existing-gem' --quiet", raise_on_error: false, env: { "RUBYOPT" => "-r#{bundled_app("add_with_warning.rb")}" }
+ expect(out).to be_empty
+ expect(err).to include("Could not find gem 'non-existing-gem'")
+ expect(err).to include("This is a warning")
+ end
+ end
+
+ describe "with --strict option" do
+ it "adds strict version" do
+ bundle "add 'foo' --strict"
+ expect(bundled_app_gemfile.read).to include %(gem "foo", "= 2.0")
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with no option" do
+ it "adds optimistic version" do
+ bundle "add 'foo'"
+ expect(bundled_app_gemfile.read).to include %(gem "foo", ">= 2.0")
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --pessimistic and --strict" do
+ it "throws error" do
+ bundle "add 'foo' --strict --pessimistic", raise_on_error: false
+
+ expect(err).to include("You cannot specify `--strict` and `--pessimistic` at the same time")
+ end
+ end
+
+ context "multiple gems" do
+ it "adds multiple gems to gemfile" do
+ bundle "add bar baz"
+
+ expect(bundled_app_gemfile.read).to match(/gem "bar", ">= 0.12.3"/)
+ expect(bundled_app_gemfile.read).to match(/gem "baz", ">= 1.2"/)
+ end
+
+ it "throws error if any of the specified gems are present in the gemfile with different version" do
+ bundle "add weakling bar", raise_on_error: false
+
+ expect(err).to include("You cannot specify the same gem twice with different version requirements")
+ expect(err).to include("You specified: weakling (~> 0.0.1) and weakling (>= 0).")
+ end
+ end
+
+ describe "when a gem is added which is already specified in Gemfile with version" do
+ it "shows an error when added with different version requirement" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack", "1.0"
+ G
+
+ bundle "add 'myrack' --version=1.1", raise_on_error: false
+
+ expect(err).to include("You cannot specify the same gem twice with different version requirements")
+ expect(err).to include("If you want to update the gem version, run `bundle update myrack`. You may also need to change the version requirement specified in the Gemfile if it's too restrictive")
+ end
+
+ it "shows error when added without version requirements" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack", "1.0"
+ G
+
+ bundle "add 'myrack'", raise_on_error: false
+
+ expect(err).to include("Gem already added.")
+ expect(err).to include("You cannot specify the same gem twice with different version requirements")
+ expect(err).not_to include("If you want to update the gem version, run `bundle update myrack`. You may also need to change the version requirement specified in the Gemfile if it's too restrictive")
+ end
+ end
+
+ describe "when a gem is added which is already specified in Gemfile without version" do
+ it "shows an error when added with different version requirement" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ bundle "add 'myrack' --version=1.1", raise_on_error: false
+
+ expect(err).to include("You cannot specify the same gem twice with different version requirements")
+ expect(err).to include("If you want to update the gem version, run `bundle update myrack`.")
+ expect(err).not_to include("You may also need to change the version requirement specified in the Gemfile if it's too restrictive")
+ end
+ end
+
+ describe "when a gem is added and cache exists" do
+ it "caches all new dependencies added for the specified gem" do
+ bundle :cache
+
+ bundle "add 'myrack' --version=1.0.0"
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+ end
+end
diff --git a/spec/bundler/commands/binstubs_spec.rb b/spec/bundler/commands/binstubs_spec.rb
new file mode 100644
index 0000000000..af4d24a9e8
--- /dev/null
+++ b/spec/bundler/commands/binstubs_spec.rb
@@ -0,0 +1,333 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle binstubs <gem>" do
+ context "when the gem exists in the lockfile" do
+ it "sets up the binstub" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack"
+
+ expect(bundled_app("bin/myrackup")).to exist
+ end
+
+ it "does not install other binstubs" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "rails"
+ G
+
+ bundle "binstubs rails"
+
+ expect(bundled_app("bin/myrackup")).not_to exist
+ expect(bundled_app("bin/rails")).to exist
+ end
+
+ it "does install multiple binstubs" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "rails"
+ G
+
+ bundle "binstubs rails myrack"
+
+ expect(bundled_app("bin/myrackup")).to exist
+ expect(bundled_app("bin/rails")).to exist
+ end
+
+ it "allows installing all binstubs" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ bundle :binstubs, all: true
+
+ expect(bundled_app("bin/rails")).to exist
+ expect(bundled_app("bin/rake")).to exist
+ end
+
+ it "allows installing binstubs for all platforms" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack --all-platforms"
+
+ expect(bundled_app("bin/myrackup")).to exist
+ expect(bundled_app("bin/myrackup.cmd")).to exist
+ end
+
+ it "displays an error when used without any gem" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs", raise_on_error: false
+ expect(exitstatus).to eq(1)
+ expect(err).to include("`bundle binstubs` needs at least one gem to run.")
+ end
+
+ it "displays an error when used with --all and gems" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack", all: true, raise_on_error: false
+ expect(last_command).to be_failure
+ expect(err).to include("Cannot specify --all with specific gems")
+ end
+
+ it "installs binstubs from git gems" do
+ FileUtils.mkdir_p(lib_path("foo/bin"))
+ FileUtils.touch(lib_path("foo/bin/foo"))
+ build_git "foo", "1.0", path: lib_path("foo") do |s|
+ s.executables = %w[foo]
+ end
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}"
+ G
+
+ bundle "binstubs foo"
+
+ expect(bundled_app("bin/foo")).to exist
+ end
+
+ it "installs binstubs from path gems" do
+ FileUtils.mkdir_p(lib_path("foo/bin"))
+ FileUtils.touch(lib_path("foo/bin/foo"))
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.executables = %w[foo]
+ end
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo")}"
+ G
+
+ bundle "binstubs foo"
+
+ expect(bundled_app("bin/foo")).to exist
+ end
+
+ it "sets correct permissions for binstubs" do
+ with_umask(0o002) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack"
+ binary = bundled_app("bin/myrackup")
+ expect(File.stat(binary).mode.to_s(8)).to eq(Gem.win_platform? ? "100644" : "100775")
+ end
+ end
+
+ context "when using --shebang" do
+ it "sets the specified shebang for the binstub" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack --shebang jruby"
+ expect(File.readlines(bundled_app("bin/myrackup")).first).to eq("#!/usr/bin/env jruby\n")
+ end
+ end
+ end
+
+ context "when the gem doesn't exist" do
+ it "displays an error with correct status" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ bundle "binstubs doesnt_exist", raise_on_error: false
+
+ expect(exitstatus).to eq(7)
+ expect(err).to include("Could not find gem 'doesnt_exist'.")
+ end
+ end
+
+ context "with the binstubs dir configured" do
+ before do
+ bundle_config "bin exec"
+ end
+
+ it "creates the binstubs in the configured dir" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack"
+
+ expect(bundled_app("exec/myrackup")).to exist
+ end
+ end
+
+ context "with --standalone option" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "rails"
+ G
+ end
+
+ it "generates a standalone binstub" do
+ bundle "binstubs myrack --standalone"
+ expect(bundled_app("bin/myrackup")).to exist
+ end
+
+ it "generates a binstub that does not depend on rubygems or bundler" do
+ bundle "binstubs myrack --standalone"
+ expect(File.read(bundled_app("bin/myrackup"))).to_not include("Gem.bin_path")
+ end
+
+ it "generates a standalone binstub at the given path when configured" do
+ bundle_config "bin foo"
+ bundle "binstubs myrack --standalone"
+ expect(bundled_app("foo/myrackup")).to exist
+ end
+
+ context "when specified --all-platforms option" do
+ it "generates standalone binstubs for all platforms" do
+ bundle "binstubs myrack --standalone --all-platforms"
+ expect(bundled_app("bin/myrackup")).to exist
+ expect(bundled_app("bin/myrackup.cmd")).to exist
+ end
+ end
+
+ context "when the gem is bundler" do
+ it "warns without generating a standalone binstub" do
+ bundle "binstubs bundler --standalone"
+ expect(bundled_app("bin/bundle")).not_to exist
+ expect(bundled_app("bin/bundler")).not_to exist
+ expect(err).to include("Sorry, Bundler can only be run via RubyGems.")
+ end
+ end
+
+ context "when specified --all option" do
+ it "generates standalone binstubs for all gems except bundler" do
+ bundle "binstubs --standalone --all"
+ expect(bundled_app("bin/myrackup")).to exist
+ expect(bundled_app("bin/rails")).to exist
+ expect(bundled_app("bin/bundle")).not_to exist
+ expect(bundled_app("bin/bundler")).not_to exist
+ expect(err).not_to include("Sorry, Bundler can only be run via RubyGems.")
+ end
+ end
+ end
+
+ context "when the bin already exists" do
+ it "doesn't overwrite and warns" do
+ FileUtils.mkdir_p(bundled_app("bin"))
+ File.open(bundled_app("bin/myrackup"), "wb") do |file|
+ file.print "OMG"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack"
+
+ expect(bundled_app("bin/myrackup")).to exist
+ expect(File.read(bundled_app("bin/myrackup"))).to eq("OMG")
+ expect(err).to include("Skipped myrackup")
+ expect(err).to include("overwrite skipped stubs, use --force")
+ end
+
+ context "when using --force" do
+ it "overwrites the binstub" do
+ FileUtils.mkdir_p(bundled_app("bin"))
+ File.open(bundled_app("bin/myrackup"), "wb") do |file|
+ file.print "OMG"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack --force"
+
+ expect(bundled_app("bin/myrackup")).to exist
+ expect(File.read(bundled_app("bin/myrackup"))).not_to eq("OMG")
+ end
+ end
+ end
+
+ context "when the gem has no bins" do
+ it "suggests child gems if they have bins" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack-obama"
+ G
+
+ bundle "binstubs myrack-obama"
+ expect(err).to include("myrack-obama has no executables")
+ expect(err).to include("myrack has: myrackup")
+ end
+
+ it "works if child gems don't have bins" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "actionpack"
+ G
+
+ bundle "binstubs actionpack"
+ expect(err).to include("no executables for the gem actionpack")
+ end
+
+ it "works if the gem has development dependencies" do
+ build_repo2 do
+ build_gem "with_development_dependency" do |s|
+ s.add_development_dependency "activesupport", "= 2.3.5"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_development_dependency"
+ G
+
+ bundle "binstubs with_development_dependency"
+ expect(err).to include("no executables for the gem with_development_dependency")
+ end
+ end
+
+ context "when BUNDLE_INSTALL is specified" do
+ it "performs an automatic bundle install" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle_config "auto_install 1"
+ bundle "binstubs myrack"
+ expect(out).to include("Installing myrack 1.0.0")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "does nothing when already up to date" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle_config "auto_install 1"
+ bundle "binstubs myrack", env: { "BUNDLE_INSTALL" => "1" }
+ expect(out).not_to include("Installing myrack 1.0.0")
+ end
+ end
+end
diff --git a/spec/bundler/commands/cache_spec.rb b/spec/bundler/commands/cache_spec.rb
new file mode 100644
index 0000000000..e223d07f7f
--- /dev/null
+++ b/spec/bundler/commands/cache_spec.rb
@@ -0,0 +1,620 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle cache" do
+ it "doesn't update the cache multiple times, even if it already exists" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle :cache
+ expect(out).to include("Updating files in vendor/cache").once
+
+ bundle :cache
+ expect(out).to include("Updating files in vendor/cache").once
+ end
+
+ context "with --gemfile" do
+ it "finds the gemfile" do
+ gemfile bundled_app("NotGemfile"), <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle "cache --gemfile=NotGemfile"
+
+ ENV["BUNDLE_GEMFILE"] = "NotGemfile"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ context "with cache_all configured" do
+ context "without a gemspec" do
+ it "caches all dependencies except bundler itself" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem 'myrack'
+ gem 'bundler'
+ D
+
+ bundle_config "cache_all true"
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist
+ end
+ end
+
+ context "with a gemspec" do
+ context "that has the same name as the gem" do
+ before do
+ File.open(bundled_app("mygem.gemspec"), "w") do |f|
+ f.write <<-G
+ Gem::Specification.new do |s|
+ s.name = "mygem"
+ s.version = "0.1.1"
+ s.summary = ""
+ s.authors = ["gem author"]
+ s.add_development_dependency "nokogiri", "=1.4.2"
+ end
+ G
+ end
+ end
+
+ it "caches all dependencies except bundler and the gemspec specified gem" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem 'myrack'
+ gemspec
+ D
+
+ bundle_config "cache_all true"
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ expect(bundled_app("vendor/cache/nokogiri-1.4.2.gem")).to exist
+ expect(bundled_app("vendor/cache/mygem-0.1.1.gem")).to_not exist
+ expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist
+ end
+ end
+
+ context "that has a different name as the gem" do
+ before do
+ File.open(bundled_app("mygem_diffname.gemspec"), "w") do |f|
+ f.write <<-G
+ Gem::Specification.new do |s|
+ s.name = "mygem"
+ s.version = "0.1.1"
+ s.summary = ""
+ s.authors = ["gem author"]
+ s.add_development_dependency "nokogiri", "=1.4.2"
+ end
+ G
+ end
+ end
+
+ it "caches all dependencies except bundler and the gemspec specified gem" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem 'myrack'
+ gemspec
+ D
+
+ bundle_config "cache_all true"
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ expect(bundled_app("vendor/cache/nokogiri-1.4.2.gem")).to exist
+ expect(bundled_app("vendor/cache/mygem-0.1.1.gem")).to_not exist
+ expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist
+ end
+ end
+ end
+
+ context "with multiple gemspecs" do
+ before do
+ File.open(bundled_app("mygem.gemspec"), "w") do |f|
+ f.write <<-G
+ Gem::Specification.new do |s|
+ s.name = "mygem"
+ s.version = "0.1.1"
+ s.summary = ""
+ s.authors = ["gem author"]
+ s.add_development_dependency "nokogiri", "=1.4.2"
+ end
+ G
+ end
+ File.open(bundled_app("mygem_client.gemspec"), "w") do |f|
+ f.write <<-G
+ Gem::Specification.new do |s|
+ s.name = "mygem_test"
+ s.version = "0.1.1"
+ s.summary = ""
+ s.authors = ["gem author"]
+ s.add_development_dependency "weakling", "=0.0.3"
+ end
+ G
+ end
+ end
+
+ it "caches all dependencies except bundler and the gemspec specified gems" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem 'myrack'
+ gemspec :name => 'mygem'
+ gemspec :name => 'mygem_test'
+ D
+
+ bundle_config "cache_all true"
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ expect(bundled_app("vendor/cache/nokogiri-1.4.2.gem")).to exist
+ expect(bundled_app("vendor/cache/weakling-0.0.3.gem")).to exist
+ expect(bundled_app("vendor/cache/mygem-0.1.1.gem")).to_not exist
+ expect(bundled_app("vendor/cache/mygem_test-0.1.1.gem")).to_not exist
+ expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist
+ end
+ end
+ end
+
+ context "with --no-install" do
+ it "puts the gems in vendor/cache but does not install them" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem 'myrack'
+ D
+
+ bundle "cache --no-install"
+
+ expect(the_bundle).not_to include_gems "myrack 1.0.0"
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+
+ it "does not prevent installing gems with bundle install" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem 'myrack'
+ D
+
+ bundle "cache --no-install"
+ bundle "install"
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "does not prevent installing gems with bundle update" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ D
+
+ bundle "cache --no-install"
+ bundle "update --all"
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ context "with --all-platforms" do
+ it "puts the gems in vendor/cache even for other rubies" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem 'myrack', :platforms => [:ruby_20, :windows_20]
+ D
+
+ bundle "cache --all-platforms"
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+
+ it "prints a warn when using legacy windows rubies" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem 'myrack', :platforms => [:ruby_20, :x64_mingw_20]
+ D
+
+ bundle "cache --all-platforms", raise_on_error: false
+ expect(err).to include("will be removed in the future")
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+
+ it "does not attempt to install gems in without groups" do
+ build_repo4 do
+ build_gem "uninstallable", "2.0" do |s|
+ s.add_development_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", "task(:default) { raise 'CANNOT INSTALL' }"
+ end
+ end
+
+ bundle_config "without wo"
+ install_gemfile <<-G, artifice: "compact_index_extra_api"
+ source "https://main.repo"
+ gem "myrack"
+ group :wo do
+ gem "weakling"
+ gem "uninstallable", :source => "https://main.repo/extra"
+ end
+ G
+
+ bundle :cache, "all-platforms" => true, artifice: "compact_index_extra_api"
+ expect(bundled_app("vendor/cache/weakling-0.0.3.gem")).to exist
+ expect(bundled_app("vendor/cache/uninstallable-2.0.gem")).to exist
+ expect(the_bundle).to include_gem "myrack 1.0"
+ expect(the_bundle).not_to include_gems "weakling", "uninstallable"
+
+ bundle_config "without wo"
+ bundle :install, artifice: "compact_index_extra_api"
+ expect(the_bundle).to include_gem "myrack 1.0"
+ expect(the_bundle).not_to include_gems "weakling"
+ end
+
+ it "does not fail to cache gems in excluded groups when there's a lockfile but gems not previously installed" do
+ bundle_config "without wo"
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ group :wo do
+ gem "weakling"
+ end
+ G
+
+ bundle :lock
+ bundle :cache, "all-platforms" => true
+ expect(bundled_app("vendor/cache/weakling-0.0.3.gem")).to exist
+ end
+ end
+
+ context "with frozen configured" do
+ let(:app_cache) { bundled_app("vendor/cache") }
+
+ before do
+ bundle_config "frozen true"
+ end
+
+ it "tries to install but fails when the lockfile is out of sync" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ myrack-obama
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ bundle :cache, raise_on_error: false
+ expect(exitstatus).to eq(16)
+ expect(err).to include("frozen mode")
+ expect(err).to include("You have deleted from the Gemfile")
+ expect(err).to include("* myrack-obama")
+ bundle "env"
+ expect(out).to include("frozen")
+ end
+
+ it "caches gems without installing when lockfile is in sync, and --no-install is passed, even if vendor/cache directory is initially empty" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ FileUtils.mkdir_p app_cache
+
+ bundle "cache --no-install"
+ expect(out).not_to include("Installing myrack 1.0.0")
+ expect(out).to include("Fetching myrack 1.0.0")
+ expect(app_cache.join("myrack-1.0.0.gem")).to exist
+ end
+
+ it "completes a partial cache when lockfile is in sync, even if the already cached gem is no longer available remotely" do
+ build_repo4 do
+ build_gem "foo", "1.0.0"
+ end
+
+ build_gem "bar", "1.0.0", path: bundled_app("vendor/cache")
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "foo"
+ gem "bar"
+ G
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ foo (1.0.0)
+ bar (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ bar
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "cache --no-install"
+ expect(out).to include("Fetching foo 1.0.0")
+ expect(out).not_to include("Fetching bar 1.0.0")
+ expect(app_cache.join("foo-1.0.0.gem")).to exist
+ expect(app_cache.join("bar-1.0.0.gem")).to exist
+ end
+ end
+
+ context "with gems with extensions" do
+ before do
+ build_repo2 do
+ build_gem "racc", "2.0" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", "task(:default) { puts 'INSTALLING myrack' }"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ gem "racc"
+ G
+ end
+
+ it "installs them properly from cache to a different path" do
+ bundle "cache"
+ bundle_config "path vendor/bundle"
+ bundle "install --local"
+ end
+ end
+end
+
+RSpec.describe "bundle install with gem sources" do
+ describe "when cached and locked" do
+ it "does not hit the remote at all" do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ bundle :cache
+ pristine_system_gems
+ FileUtils.rm_r gem_repo2
+
+ bundle "install --local"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "does not hit the remote at all in frozen mode" do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ bundle :cache
+ pristine_system_gems
+ FileUtils.rm_r gem_repo2
+
+ bundle_config "deployment true"
+ bundle_config "path vendor/bundle"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "does not hit the remote at all in non frozen mode either" do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ bundle :cache
+ pristine_system_gems
+ FileUtils.rm_r gem_repo2
+
+ bundle_config "path vendor/bundle"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "does not hit the remote at all when cache_all_platforms configured" do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ bundle :cache
+ pristine_system_gems
+ FileUtils.rm_r gem_repo2
+
+ bundle_config "cache_all_platforms true"
+ bundle_config "path vendor/bundle"
+ bundle "install --local"
+ expect(out).not_to include("Fetching gem metadata")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "uses cached gems for secondary sources when cache_all_platforms configured" do
+ build_repo4 do
+ build_gem "foo", "1.0.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+
+ build_gem "foo", "1.0.0" do |s|
+ s.platform = "arm64-darwin"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ source "https://gem.repo4" do
+ gem "foo"
+ end
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ foo (1.0.0-x86_64-linux)
+ foo (1.0.0-arm64-darwin)
+
+ PLATFORMS
+ arm64-darwin
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ foo
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle_config "cache_all_platforms true"
+ bundle_config "path vendor/bundle"
+ bundle :cache, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+
+ # simulate removal of all remote gems
+ empty_repo4
+
+ # delete compact index cache
+ FileUtils.rm_r home(".bundle/cache/compact_index")
+
+ bundle "install", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+
+ expect(the_bundle).to include_gems "foo 1.0.0 x86_64-linux"
+ end
+ end
+
+ it "does not reinstall already-installed gems" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ bundle :cache
+
+ build_gem "myrack", "1.0.0", path: bundled_app("vendor/cache") do |s|
+ s.write "lib/myrack.rb", "raise 'omg'"
+ end
+
+ bundle :install
+ expect(err).to be_empty
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+
+ it "ignores cached gems for the wrong platform" do
+ simulate_platform "java" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+ bundle :cache
+ end
+
+ pristine_system_gems
+
+ bundle_config "force_ruby_platform true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+ expect(the_bundle).to include_gems("platform_specific 1.0 ruby")
+ end
+
+ it "keeps gems that are locked and cached for the current platform, even if incompatible with the current ruby" do
+ build_repo4 do
+ build_gem "bcrypt_pbkdf", "1.1.1"
+ build_gem "bcrypt_pbkdf", "1.1.1" do |s|
+ s.platform = "arm64-darwin"
+ s.required_ruby_version = "< #{current_ruby_minor}"
+ end
+ end
+
+ app_cache = bundled_app("vendor/cache")
+ FileUtils.mkdir_p app_cache
+ FileUtils.cp gem_repo4("gems/bcrypt_pbkdf-1.1.1-arm64-darwin.gem"), app_cache
+ FileUtils.cp gem_repo4("gems/bcrypt_pbkdf-1.1.1.gem"), app_cache
+
+ bundle_config "cache_all_platforms true"
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ bcrypt_pbkdf (1.1.1)
+ bcrypt_pbkdf (1.1.1-arm64-darwin)
+
+ PLATFORMS
+ arm64-darwin
+ ruby
+
+ DEPENDENCIES
+ bcrypt_pbkdf
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "arm64-darwin-23" do
+ install_gemfile <<~G, verbose: true
+ source "https://gem.repo4"
+ gem "bcrypt_pbkdf"
+ G
+
+ expect(out).to include("Updating files in vendor/cache")
+ expect(err).to be_empty
+ expect(app_cache.join("bcrypt_pbkdf-1.1.1-arm64-darwin.gem")).to exist
+ expect(app_cache.join("bcrypt_pbkdf-1.1.1.gem")).to exist
+ end
+ end
+
+ it "does not update the cache if --no-cache is passed" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ bundled_app("vendor/cache").mkpath
+ expect(bundled_app("vendor/cache").children).to be_empty
+
+ bundle "install --no-cache"
+ expect(bundled_app("vendor/cache").children).to be_empty
+ end
+ end
+end
diff --git a/spec/bundler/commands/check_spec.rb b/spec/bundler/commands/check_spec.rb
new file mode 100644
index 0000000000..7fe6897ae3
--- /dev/null
+++ b/spec/bundler/commands/check_spec.rb
@@ -0,0 +1,601 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle check" do
+ it "returns success when the Gemfile is satisfied" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ bundle :check
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+
+ it "works with the --gemfile flag when not in the directory" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ bundle "check --gemfile bundled_app/Gemfile", dir: tmp
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+
+ it "creates a Gemfile.lock by default if one does not exist" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ FileUtils.rm(bundled_app_lock)
+
+ bundle "check"
+
+ expect(bundled_app_lock).to exist
+ end
+
+ it "does not create a Gemfile.lock if --dry-run was passed" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ FileUtils.rm(bundled_app_lock)
+
+ bundle "check --dry-run"
+
+ expect(bundled_app_lock).not_to exist
+ end
+
+ it "prints an error that shows missing gems" do
+ system_gems ["rails-2.3.2"], path: default_bundle_path
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ bundle :check, raise_on_error: false
+ expect(err).to include("The following gems are missing")
+ expect(err).to include(" * rake (#{rake_version})")
+ expect(err).to include(" * actionpack (2.3.2)")
+ expect(err).to include(" * activerecord (2.3.2)")
+ expect(err).to include(" * actionmailer (2.3.2)")
+ expect(err).to include(" * activeresource (2.3.2)")
+ expect(err).to include(" * activesupport (2.3.2)")
+ expect(err).to include("Install missing gems with `bundle install`")
+ end
+
+ it "prints an error that shows missing gems if a Gemfile.lock does not exist and a toplevel dependency is missing" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ bundle :check, raise_on_error: false
+ expect(exitstatus).to be > 0
+ expect(err).to include("The following gems are missing")
+ expect(err).to include(" * rails (2.3.2)")
+ expect(err).to include(" * rake (#{rake_version})")
+ expect(err).to include(" * actionpack (2.3.2)")
+ expect(err).to include(" * activerecord (2.3.2)")
+ expect(err).to include(" * actionmailer (2.3.2)")
+ expect(err).to include(" * activeresource (2.3.2)")
+ expect(err).to include(" * activesupport (2.3.2)")
+ expect(err).to include("Install missing gems with `bundle install`")
+ end
+
+ it "prints a generic error if gem git source is not checked out" do
+ build_git "foo", path: lib_path("foo")
+
+ bundle_config "path vendor/bundle"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", git: "#{lib_path("foo")}"
+ G
+
+ FileUtils.rm_r bundled_app("vendor/bundle")
+ bundle :check, raise_on_error: false
+ expect(exitstatus).to eq 1
+ expect(err).to include("Bundler can't satisfy your Gemfile's dependencies.")
+ end
+
+ it "prints a generic message if you changed your lockfile" do
+ build_repo2 do
+ build_gem "rails_pinned_to_old_activesupport" do |s|
+ s.add_dependency "activesupport", "= 1.2.3"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'rails'
+ G
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails"
+ gem "rails_pinned_to_old_activesupport"
+ G
+
+ bundle :check, raise_on_error: false
+ expect(err).to include("Bundler can't satisfy your Gemfile's dependencies.")
+ end
+
+ it "uses the without setting" do
+ bundle_config "without foo"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ group :foo do
+ gem "myrack"
+ end
+ G
+
+ bundle "check"
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+
+ it "ensures that gems are actually installed and not just cached" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :group => :foo
+ G
+
+ bundle_config "without foo"
+ bundle :install
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "check", raise_on_error: false
+ expect(err).to include("* myrack (1.0.0)")
+ expect(exitstatus).to eq(1)
+ end
+
+ it "ensures that gems are actually installed and not just cached in applications' cache" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle :cache
+
+ uninstall_gem("myrack", env: { "GEM_HOME" => vendored_gems.to_s })
+
+ bundle "check", raise_on_error: false
+ expect(err).to include("* myrack (1.0.0)")
+ expect(exitstatus).to eq(1)
+ end
+
+ it "ignores missing gems restricted to other platforms" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ platforms :#{not_local_tag} do
+ gem "activesupport"
+ end
+ G
+
+ system_gems "myrack-1.0.0", path: default_bundle_path
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ activesupport (2.3.5)
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{generic_local_platform}
+ #{not_local}
+
+ DEPENDENCIES
+ myrack
+ activesupport
+ G
+
+ bundle :check
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+
+ it "works with env conditionals" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ env :NOT_GOING_TO_BE_SET do
+ gem "activesupport"
+ end
+ G
+
+ system_gems "myrack-1.0.0", path: default_bundle_path
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ activesupport (2.3.5)
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{generic_local_platform}
+ #{not_local}
+
+ DEPENDENCIES
+ myrack
+ activesupport
+ G
+
+ bundle :check
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+
+ it "outputs an error when the default Gemfile is not found" do
+ bundle :check, raise_on_error: false
+ expect(exitstatus).to eq(10)
+ expect(err).to include("Could not locate Gemfile")
+ end
+
+ it "does not output fatal error message" do
+ bundle :check, raise_on_error: false
+ expect(exitstatus).to eq(10)
+ expect(err).not_to include("Unfortunately, a fatal error has occurred. ")
+ end
+
+ it "fails when there's no lockfile and frozen is set" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo"
+ G
+
+ bundle_config "deployment true"
+ bundle "install"
+ FileUtils.rm(bundled_app_lock)
+
+ bundle :check, raise_on_error: false
+ expect(last_command).to be_failure
+ end
+
+ describe "when locked" do
+ before :each do
+ system_gems "myrack-1.0.0"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0"
+ G
+ end
+
+ it "returns success when the Gemfile is satisfied" do
+ bundle :install
+ bundle :check
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+
+ it "shows what is missing with the current Gemfile if it is not satisfied" do
+ FileUtils.rm_r default_bundle_path
+ default_system_gems
+ bundle :check, raise_on_error: false
+ expect(err).to match(/The following gems are missing/)
+ expect(err).to include("* myrack (1.0")
+ end
+ end
+
+ describe "when locked with multiple dependents with different requirements" do
+ before :each do
+ build_repo4 do
+ build_gem "depends_on_myrack" do |s|
+ s.add_dependency "myrack", ">= 1.0"
+ end
+ build_gem "also_depends_on_myrack" do |s|
+ s.add_dependency "myrack", "~> 1.0"
+ end
+ build_gem "myrack"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "depends_on_myrack"
+ gem "also_depends_on_myrack"
+ G
+
+ bundle "lock"
+ end
+
+ it "shows what is missing with the current Gemfile without duplications" do
+ bundle :check, raise_on_error: false
+ expect(err).to match(/The following gems are missing/)
+ expect(err).to include("* myrack (1.0").once
+ end
+ end
+
+ describe "when locked under multiple platforms" do
+ before :each do
+ build_repo4 do
+ build_gem "myrack"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ ruby
+ #{local_platform}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "shows what is missing with the current Gemfile without duplications" do
+ bundle :check, raise_on_error: false
+ expect(err).to match(/The following gems are missing/)
+ expect(err).to include("* myrack (1.0").once
+ end
+ end
+
+ describe "when using only scoped rubygems sources" do
+ before do
+ gemfile <<~G
+ source "https://gem.repo2"
+ source "https://gem.repo1" do
+ gem "myrack"
+ end
+ G
+ end
+
+ it "returns success when the Gemfile is satisfied" do
+ system_gems "myrack-1.0.0", path: default_bundle_path
+ bundle :check, artifice: "compact_index"
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+ end
+
+ describe "when using only scoped rubygems sources with indirect dependencies" do
+ before do
+ build_repo4 do
+ build_gem "depends_on_myrack" do |s|
+ s.add_dependency "myrack"
+ end
+
+ build_gem "myrack"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo1"
+ source "https://gem.repo4" do
+ gem "depends_on_myrack"
+ end
+ G
+ end
+
+ it "returns success when the Gemfile is satisfied and generates a correct lockfile" do
+ system_gems "depends_on_myrack-1.0", "myrack-1.0", gem_repo: gem_repo4, path: default_bundle_path
+ bundle :check, artifice: "compact_index"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "depends_on_myrack", "1.0"
+ c.checksum gem_repo4, "myrack", "1.0"
+ end
+
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ depends_on_myrack (1.0)
+ myrack
+ myrack (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ depends_on_myrack!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "with gemspec directive and scoped sources" do
+ before do
+ build_repo4 do
+ build_gem "awesome_print"
+ end
+
+ build_repo2 do
+ build_gem "dex-dispatch-engine"
+ end
+
+ build_lib("bundle-check-issue", path: tmp("bundle-check-issue")) do |s|
+ s.write "Gemfile", <<-G
+ source "https://localgemserver.test"
+
+ gemspec
+
+ source "https://localgemserver.test/extra" do
+ gem "dex-dispatch-engine"
+ end
+ G
+
+ s.add_dependency "awesome_print"
+ end
+
+ bundle "install", artifice: "compact_index_extra", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }, dir: tmp("bundle-check-issue")
+ end
+
+ it "does not corrupt lockfile when changing version" do
+ version_file = tmp("bundle-check-issue/bundle-check-issue.gemspec")
+ File.write(version_file, File.read(version_file).gsub(/s\.version = .+/, "s.version = '9999'"))
+
+ bundle "check --verbose", dir: tmp("bundle-check-issue")
+
+ lockfile = File.read(tmp("bundle-check-issue/Gemfile.lock"))
+
+ checksums = checksums_section_when_enabled(lockfile) do |c|
+ c.checksum gem_repo4, "awesome_print", "1.0"
+ c.no_checksum "bundle-check-issue", "9999"
+ c.checksum gem_repo2, "dex-dispatch-engine", "1.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ PATH
+ remote: .
+ specs:
+ bundle-check-issue (9999)
+ awesome_print
+
+ GEM
+ remote: https://localgemserver.test/
+ specs:
+ awesome_print (1.0)
+
+ GEM
+ remote: https://localgemserver.test/extra/
+ specs:
+ dex-dispatch-engine (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ bundle-check-issue!
+ dex-dispatch-engine!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "with scoped and unscoped sources" do
+ it "does not corrupt lockfile" do
+ build_repo2 do
+ build_gem "foo"
+ build_gem "wadus"
+ build_gem("baz") {|s| s.add_dependency "wadus" }
+ end
+
+ build_repo4 do
+ build_gem "bar"
+ end
+
+ bundle_config "path.system true"
+
+ # Add all gems to ensure all gems are installed so that a bundle check
+ # would be successful
+ install_gemfile(<<-G, artifice: "compact_index_extra")
+ source "https://gem.repo2"
+
+ source "https://gem.repo4" do
+ gem "bar"
+ end
+
+ gem "foo"
+ gem "baz"
+ G
+
+ original_lockfile = lockfile
+
+ # Remove "baz" gem from the Gemfile, and bundle install again to generate
+ # a functional lockfile with no "baz" dependency or "wadus" transitive
+ # dependency
+ install_gemfile(<<-G, artifice: "compact_index_extra")
+ source "https://gem.repo2"
+
+ source "https://gem.repo4" do
+ gem "bar"
+ end
+
+ gem "foo"
+ G
+
+ # Add back "baz" gem back to the Gemfile, but _crucially_ we do not perform a
+ # bundle install
+ gemfile(gemfile + 'gem "baz"')
+
+ bundle :check, verbose: true
+
+ # Bundle check should succeed and restore the lockfile to its original
+ # state
+ expect(lockfile).to eq(original_lockfile)
+ end
+ end
+
+ describe "BUNDLED WITH" do
+ def lock_with(bundler_version = nil)
+ lock = <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ L
+
+ if bundler_version
+ lock += "\nBUNDLED WITH\n #{bundler_version}\n"
+ end
+
+ lock
+ end
+
+ before do
+ bundle_config "path vendor/bundle"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ context "is not present" do
+ it "does not change the lock" do
+ lockfile lock_with(nil)
+ bundle :check
+ expect(lockfile).to eq lock_with(nil)
+ end
+ end
+
+ context "is newer" do
+ it "does not change the lock and does not warn" do
+ lockfile lock_with(Bundler::VERSION.succ)
+ bundle :check
+ expect(err).to be_empty
+ expect(lockfile).to eq lock_with(Bundler::VERSION.succ)
+ end
+ end
+
+ context "is older" do
+ it "does not change the lock" do
+ system_gems "bundler-1.18.0"
+ lockfile lock_with("1.18.0")
+ bundle :check
+ expect(lockfile).to eq lock_with("1.18.0")
+ end
+ end
+ end
+end
diff --git a/spec/bundler/commands/clean_spec.rb b/spec/bundler/commands/clean_spec.rb
new file mode 100644
index 0000000000..c77859d378
--- /dev/null
+++ b/spec/bundler/commands/clean_spec.rb
@@ -0,0 +1,936 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle clean" do
+ def should_have_gems(*gems)
+ gems.each do |g|
+ expect(vendored_gems("gems/#{g}")).to exist
+ expect(vendored_gems("specifications/#{g}.gemspec")).to exist
+ expect(vendored_gems("cache/#{g}.gem")).to exist
+ end
+ end
+
+ def should_not_have_gems(*gems)
+ gems.each do |g|
+ expect(vendored_gems("gems/#{g}")).not_to exist
+ expect(vendored_gems("specifications/#{g}.gemspec")).not_to exist
+ expect(vendored_gems("cache/#{g}.gem")).not_to exist
+ end
+ end
+
+ it "removes unused gems that are different" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "foo"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle_config "clean false"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ G
+ bundle "install"
+
+ bundle :clean
+
+ expect(out).to include("Removing foo (1.0)")
+
+ should_have_gems "thin-1.0", "myrack-1.0.0"
+ should_not_have_gems "foo-1.0"
+
+ expect(vendored_gems("bin/myrackup")).to exist
+ end
+
+ it "removes old version of gem if unused" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "0.9.1"
+ gem "foo"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle_config "clean false"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ gem "foo"
+ G
+ bundle "install"
+
+ bundle :clean
+
+ expect(out).to include("Removing myrack (0.9.1)")
+
+ should_have_gems "foo-1.0", "myrack-1.0.0"
+ should_not_have_gems "myrack-0.9.1"
+
+ expect(vendored_gems("bin/myrackup")).to exist
+ end
+
+ it "removes new version of gem if unused" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ gem "foo"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle_config "clean false"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "0.9.1"
+ gem "foo"
+ G
+ bundle "update myrack"
+
+ bundle :clean
+
+ expect(out).to include("Removing myrack (1.0.0)")
+
+ should_have_gems "foo-1.0", "myrack-0.9.1"
+ should_not_have_gems "myrack-1.0.0"
+
+ expect(vendored_gems("bin/myrackup")).to exist
+ end
+
+ it "removes gems in bundle without groups" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "foo"
+
+ group :test_group do
+ gem "myrack", "1.0.0"
+ end
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+ bundle_config "without test_group"
+ bundle "install"
+ bundle :clean
+
+ expect(out).to include("Removing myrack (1.0.0)")
+
+ should_have_gems "foo-1.0"
+ should_not_have_gems "myrack-1.0.0"
+
+ expect(vendored_gems("bin/myrackup")).to_not exist
+ end
+
+ it "does not remove cached git dir if it's being used" do
+ build_git "foo"
+ revision = revision_for(lib_path("foo-1.0"))
+ git_path = lib_path("foo-1.0")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ git "#{git_path}", :ref => "#{revision}" do
+ gem "foo"
+ end
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ bundle :clean
+
+ digest = Digest(:SHA1).hexdigest(git_path.to_s)
+ cache_path = vendored_gems("cache/bundler/git/foo-1.0-#{digest}")
+ expect(cache_path).to exist
+ end
+
+ it "removes unused git gems" do
+ build_git "foo", path: lib_path("foo")
+ git_path = lib_path("foo")
+ revision = revision_for(git_path)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ git "#{git_path}", :ref => "#{revision}" do
+ gem "foo"
+ end
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ G
+ bundle "install"
+
+ bundle :clean
+
+ expect(out).to include("Removing foo (#{revision[0..11]})")
+
+ expect(vendored_gems("gems/myrack-1.0.0")).to exist
+ expect(vendored_gems("bundler/gems/foo-#{revision[0..11]}")).not_to exist
+ digest = Digest(:SHA1).hexdigest(git_path.to_s)
+ expect(vendored_gems("cache/bundler/git/foo-#{digest}")).not_to exist
+
+ expect(vendored_gems("specifications/myrack-1.0.0.gemspec")).to exist
+
+ expect(vendored_gems("bin/myrackup")).to exist
+ end
+
+ it "keeps used git gems even if installed to a symlinked location" do
+ build_git "foo", path: lib_path("foo")
+ git_path = lib_path("foo")
+ revision = revision_for(git_path)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ git "#{git_path}", :ref => "#{revision}" do
+ gem "foo"
+ end
+ G
+
+ FileUtils.mkdir_p(bundled_app("real-path"))
+ File.symlink(bundled_app("real-path"), bundled_app("symlink-path"))
+
+ bundle_config "path #{bundled_app("symlink-path")}"
+ bundle "install"
+
+ bundle :clean
+
+ expect(out).not_to include("Removing foo (#{revision[0..11]})")
+
+ expect(bundled_app("symlink-path/#{Bundler.ruby_scope}/bundler/gems/foo-#{revision[0..11]}")).to exist
+ end
+
+ it "removes old git gems on bundle update" do
+ build_git "foo-bar", path: lib_path("foo-bar")
+ revision = revision_for(lib_path("foo-bar"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ git "#{lib_path("foo-bar")}" do
+ gem "foo-bar"
+ end
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ update_git "foo-bar", path: lib_path("foo-bar")
+ revision2 = revision_for(lib_path("foo-bar"))
+
+ bundle "update", all: true
+ bundle :clean
+
+ expect(out).to include("Removing foo-bar (#{revision[0..11]})")
+
+ expect(vendored_gems("gems/myrack-1.0.0")).to exist
+ expect(vendored_gems("bundler/gems/foo-bar-#{revision[0..11]}")).not_to exist
+ expect(vendored_gems("bundler/gems/foo-bar-#{revision2[0..11]}")).to exist
+
+ expect(vendored_gems("specifications/myrack-1.0.0.gemspec")).to exist
+
+ expect(vendored_gems("bin/myrackup")).to exist
+ end
+
+ it "does not remove nested gems in a git repo" do
+ build_lib "activesupport", "3.0", path: lib_path("rails/activesupport")
+ build_git "rails", "3.0", path: lib_path("rails") do |s|
+ s.add_dependency "activesupport", "= 3.0"
+ end
+ revision = revision_for(lib_path("rails"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport", :git => "#{lib_path("rails")}", :ref => '#{revision}'
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+ bundle :clean
+ expect(out).to include("")
+
+ expect(vendored_gems("bundler/gems/rails-#{revision[0..11]}")).to exist
+ end
+
+ it "does not remove git sources that are in without groups" do
+ build_git "foo", path: lib_path("foo")
+ git_path = lib_path("foo")
+ revision = revision_for(git_path)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ group :test do
+ git "#{git_path}", :ref => "#{revision}" do
+ gem "foo"
+ end
+ end
+ G
+ bundle_config "path vendor/bundle"
+ bundle_config "without test"
+ bundle "install"
+
+ bundle :clean
+
+ expect(out).to include("")
+ expect(vendored_gems("bundler/gems/foo-#{revision[0..11]}")).to exist
+ digest = Digest(:SHA1).hexdigest(git_path.to_s)
+ expect(vendored_gems("cache/bundler/git/foo-#{digest}")).to_not exist
+ end
+
+ it "does not blow up when using without groups" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+
+ group :development do
+ gem "foo"
+ end
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle_config "without development"
+ bundle "install"
+
+ bundle :clean
+ end
+
+ it "displays an error when used without --path" do
+ bundle_config "path.system true"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ G
+
+ bundle :clean, raise_on_error: false
+
+ expect(exitstatus).to eq(15)
+ expect(err).to include("--force")
+ end
+
+ # handling bundle clean upgrade path from the pre's
+ it "removes .gem/.gemspec file even if there's no corresponding gem dir" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "foo"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "foo"
+ G
+ bundle "install"
+
+ FileUtils.rm(vendored_gems("bin/myrackup"))
+ FileUtils.rm_r(vendored_gems("gems/thin-1.0"))
+ FileUtils.rm_r(vendored_gems("gems/myrack-1.0.0"))
+
+ bundle :clean
+
+ should_not_have_gems "thin-1.0", "myrack-1.0"
+ should_have_gems "foo-1.0"
+
+ expect(vendored_gems("bin/myrackup")).not_to exist
+ end
+
+ it "does not call clean automatically when using system gems" do
+ bundle_config "path.system true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+
+ installed_gems_list
+ expect(out).to include("myrack (1.0.0)").and include("thin (1.0)")
+ end
+
+ it "does not clean on bundle update when path has not been set" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "foo"
+ G
+
+ update_repo2 do
+ build_gem "foo", "1.0.1"
+ end
+
+ bundle "update", all: true
+
+ files = Pathname.glob(default_bundle_path("*", "*"))
+ files.map! {|f| f.to_s.sub(default_bundle_path.to_s, "") }
+ expected_files = %W[
+ /bin/bundle
+ /bin/bundler
+ /cache/bundler-#{Bundler::VERSION}.gem
+ /cache/foo-1.0.1.gem
+ /cache/foo-1.0.gem
+ /gems/bundler-#{Bundler::VERSION}
+ /gems/foo-1.0
+ /gems/foo-1.0.1
+ /specifications/bundler-#{Bundler::VERSION}.gemspec
+ /specifications/foo-1.0.1.gemspec
+ /specifications/foo-1.0.gemspec
+ ]
+ expected_files += ["/bin/bundle.bat", "/bin/bundler.bat"] if Gem.win_platform?
+
+ expect(files.sort).to eq(expected_files.sort)
+ end
+
+ it "will automatically clean on bundle update when path has not been set", bundler: "5" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "foo"
+ G
+
+ update_repo2 do
+ build_gem "foo", "1.0.1"
+ end
+
+ bundle "update", all: true
+
+ files = Pathname.glob(local_gem_path("*", "*"))
+ files.map! {|f| f.to_s.sub(local_gem_path.to_s, "") }
+ expect(files.sort).to eq %w[
+ /cache/foo-1.0.1.gem
+ /gems/foo-1.0.1
+ /specifications/foo-1.0.1.gemspec
+ ]
+ end
+
+ it "does not clean automatically when path configured" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "myrack"
+ G
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+ bundle "install"
+
+ should_have_gems "myrack-1.0.0", "thin-1.0"
+ end
+
+ it "does not clean on bundle update when path configured" do
+ build_repo2
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "foo"
+ G
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ update_repo2 do
+ build_gem "foo", "1.0.1"
+ end
+
+ bundle :update, all: true
+ should_have_gems "foo-1.0", "foo-1.0.1"
+ end
+
+ it "does not clean on bundle update when installing to system gems" do
+ bundle_config "path.system true"
+
+ build_repo2
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "foo"
+ G
+ bundle "install"
+
+ update_repo2 do
+ build_gem "foo", "1.0.1"
+ end
+ bundle :update, all: true
+
+ installed_gems_list
+ expect(out).to include("foo (1.0.1, 1.0)")
+ end
+
+ it "cleans system gems when --force is used" do
+ bundle_config "path.system true"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "foo"
+ gem "myrack"
+ G
+ bundle :install
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+ bundle :install
+ bundle "clean --force"
+
+ expect(out).to include("Removing foo (1.0)")
+ installed_gems_list
+ expect(out).not_to include("foo (1.0)")
+ expect(out).to include("myrack (1.0.0)")
+ end
+
+ describe "when missing permissions", :permissions do
+ before { ENV["BUNDLE_PATH__SYSTEM"] = "true" }
+ let(:system_cache_path) { system_gem_path("cache") }
+ after do
+ FileUtils.chmod(0o755, system_cache_path)
+ end
+ it "returns a helpful error message" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "foo"
+ gem "myrack"
+ G
+ bundle :install
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+ bundle :install
+
+ FileUtils.chmod(0o500, system_cache_path)
+
+ bundle :clean, force: true, raise_on_error: false
+
+ expect(err).to include(system_gem_path.to_s)
+ expect(err).to include("grant write permissions")
+
+ installed_gems_list
+ expect(out).to include("foo (1.0)")
+ expect(out).to include("myrack (1.0.0)")
+ end
+ end
+
+ it "cleans git gems with a 7 length git revision" do
+ build_git "foo"
+ revision = revision_for(lib_path("foo-1.0"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ # mimic 7 length git revisions in Gemfile.lock
+ gemfile_lock = File.read(bundled_app_lock).split("\n")
+ gemfile_lock.each_with_index do |line, index|
+ gemfile_lock[index] = line[0..(11 + 7)] if line.include?(" revision:")
+ end
+ lockfile(bundled_app_lock, gemfile_lock.join("\n"))
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ bundle :clean
+
+ expect(out).not_to include("Removing foo (1.0 #{revision[0..6]})")
+
+ expect(vendored_gems("bundler/gems/foo-1.0-#{revision[0..6]}")).to exist
+ end
+
+ it "when using --force on system gems, it doesn't remove binaries" do
+ bundle_config "path.system true"
+
+ build_repo2 do
+ build_gem "bindir" do |s|
+ s.bindir = "exe"
+ s.executables = "foo"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "bindir"
+ G
+ bundle :install
+
+ bundle "clean --force"
+
+ sys_exec "foo"
+
+ expect(out).to eq("1.0")
+ end
+
+ it "when using --force, it doesn't remove default gem binaries" do
+ default_irb_version = ruby "gem 'irb', '< 999999'; require 'irb'; puts IRB::VERSION", raise_on_error: false
+ skip "irb isn't a default gem" if default_irb_version.empty?
+
+ # simulate executable for default gem
+ build_gem "irb", default_irb_version, to_system: true, default: true do |s|
+ s.executables = "irb"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ G
+
+ bundle "clean --force", env: { "BUNDLER_GEM_DEFAULT_DIR" => system_gem_path.to_s }
+
+ expect(out).not_to include("Removing irb")
+ end
+
+ it "doesn't blow up on path gems without a .gemspec" do
+ relative_path = "vendor/private_gems/bar-1.0"
+ absolute_path = bundled_app(relative_path)
+ FileUtils.mkdir_p("#{absolute_path}/lib/bar")
+ File.open("#{absolute_path}/lib/bar/bar.rb", "wb") do |file|
+ file.puts "module Bar; end"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "foo"
+ gem "bar", "1.0", :path => "#{relative_path}"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+ bundle :clean
+ end
+
+ it "doesn't remove gems in dry-run mode with path set" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "foo"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle_config "clean false"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ G
+
+ bundle :install
+
+ bundle "clean --dry-run"
+
+ expect(out).not_to include("Removing foo (1.0)")
+ expect(out).to include("Would have removed foo (1.0)")
+
+ should_have_gems "thin-1.0", "myrack-1.0.0", "foo-1.0"
+
+ expect(vendored_gems("bin/myrackup")).to exist
+ end
+
+ it "doesn't remove gems in dry-run mode with no path set" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "foo"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle_config "clean false"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ G
+
+ bundle :install
+
+ bundle "clean --dry-run"
+
+ expect(out).not_to include("Removing foo (1.0)")
+ expect(out).to include("Would have removed foo (1.0)")
+
+ should_have_gems "thin-1.0", "myrack-1.0.0", "foo-1.0"
+
+ expect(vendored_gems("bin/myrackup")).to exist
+ end
+
+ it "doesn't store dry run as a config setting" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "foo"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle_config "clean false"
+ bundle "install"
+ bundle_config "dry_run false"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ G
+
+ bundle :install
+
+ bundle "clean"
+
+ expect(out).to include("Removing foo (1.0)")
+ expect(out).not_to include("Would have removed foo (1.0)")
+
+ should_have_gems "thin-1.0", "myrack-1.0.0"
+ should_not_have_gems "foo-1.0"
+
+ expect(vendored_gems("bin/myrackup")).to exist
+ end
+
+ it "performs an automatic bundle install" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "foo"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle_config "clean false"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "weakling"
+ G
+
+ bundle_config "auto_install 1"
+ bundle :clean
+ expect(out).to include("Installing weakling 0.0.3")
+ should_have_gems "thin-1.0", "myrack-1.0.0", "weakling-0.0.3"
+ should_not_have_gems "foo-1.0"
+ end
+
+ it "doesn't remove extensions artifacts from bundled git gems after clean" do
+ build_git "very_simple_git_binary", &:add_c_extension
+
+ revision = revision_for(lib_path("very_simple_git_binary-1.0"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+ expect(vendored_gems("bundler/gems/extensions")).to exist
+ expect(vendored_gems("bundler/gems/very_simple_git_binary-1.0-#{revision[0..11]}")).to exist
+
+ bundle :clean
+ expect(out).to be_empty
+
+ expect(vendored_gems("bundler/gems/extensions")).to exist
+ expect(vendored_gems("bundler/gems/very_simple_git_binary-1.0-#{revision[0..11]}")).to exist
+ end
+
+ it "removes extension directories" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "very_simple_binary"
+ gem "simple_binary"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ very_simple_binary_extensions_dir =
+ Pathname.glob("#{vendored_gems}/extensions/*/*/very_simple_binary-1.0").first
+
+ simple_binary_extensions_dir =
+ Pathname.glob("#{vendored_gems}/extensions/*/*/simple_binary-1.0").first
+
+ expect(very_simple_binary_extensions_dir).to exist
+ expect(simple_binary_extensions_dir).to exist
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "simple_binary"
+ G
+
+ bundle "install"
+ bundle :clean
+ expect(out).to eq("Removing very_simple_binary (1.0)")
+
+ expect(very_simple_binary_extensions_dir).not_to exist
+ expect(simple_binary_extensions_dir).to exist
+ end
+
+ it "removes git extension directories" do
+ build_git "very_simple_git_binary", &:add_c_extension
+
+ revision = revision_for(lib_path("very_simple_git_binary-1.0"))
+ short_revision = revision[0..11]
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ very_simple_binary_extensions_dir =
+ Pathname.glob("#{vendored_gems}/bundler/gems/extensions/*/*/very_simple_git_binary-1.0-#{short_revision}").first
+
+ expect(very_simple_binary_extensions_dir).to exist
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}"
+ G
+
+ bundle "install"
+ bundle :clean
+ expect(out).to include("Removing thin (1.0)")
+ expect(very_simple_binary_extensions_dir).to exist
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ bundle "install"
+ bundle :clean
+ expect(out).to eq("Removing very_simple_git_binary-1.0 (#{short_revision})")
+
+ expect(very_simple_binary_extensions_dir).not_to exist
+ end
+
+ it "keeps git extension directories when excluded by group" do
+ build_git "very_simple_git_binary", &:add_c_extension
+
+ revision = revision_for(lib_path("very_simple_git_binary-1.0"))
+ short_revision = revision[0..11]
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ group :development do
+ gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}"
+ end
+ G
+
+ bundle :lock
+ bundle_config "without development"
+ bundle_config "path vendor/bundle"
+ bundle "install", verbose: true
+ bundle :clean
+
+ very_simple_binary_extensions_dir =
+ Pathname.glob("#{vendored_gems}/bundler/gems/extensions/*/*/very_simple_git_binary-1.0-#{short_revision}").first
+
+ expect(very_simple_binary_extensions_dir).to be_nil
+ end
+
+ it "does not remove the bundler version currently running" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ version = Bundler.gem_version.to_s
+ # Simulate that the locked bundler version is installed in the bundle path
+ # by creating the gem directory and gemspec (as would happen after bundle install with that version)
+ Pathname(vendored_gems("cache/bundler-#{version}.gem")).tap do |path|
+ FileUtils.touch(path)
+ end
+ FileUtils.touch(vendored_gems("gems/bundler-#{version}"))
+ Pathname(vendored_gems("specifications/bundler-#{version}.gemspec")).tap do |path|
+ path.write(<<~GEMSPEC)
+ Gem::Specification.new do |s|
+ s.name = "bundler"
+ s.version = "#{version}"
+ s.authors = ["bundler team"]
+ s.summary = "The best way to manage your application's dependencies"
+ end
+ GEMSPEC
+ end
+
+ should_have_gems "bundler-#{version}"
+
+ bundle :clean
+
+ should_have_gems "bundler-#{version}"
+ end
+end
diff --git a/spec/bundler/commands/config_spec.rb b/spec/bundler/commands/config_spec.rb
new file mode 100644
index 0000000000..e8ab32ca5d
--- /dev/null
+++ b/spec/bundler/commands/config_spec.rb
@@ -0,0 +1,646 @@
+# frozen_string_literal: true
+
+RSpec.describe ".bundle/config" do
+ describe "config" do
+ before { bundle "config set foo bar" }
+
+ it "prints a detailed report of local and user configuration" do
+ bundle "config list"
+
+ expect(out).to include("Settings are listed in order of priority. The top value will be used")
+ expect(out).to include("foo\nSet for the current user")
+ expect(out).to include(": \"bar\"")
+ end
+
+ context "given --parseable flag" do
+ it "prints a minimal report of local and user configuration" do
+ bundle "config list --parseable"
+ expect(out).to include("foo=bar")
+ end
+
+ context "with global config" do
+ it "prints config assigned to local scope" do
+ bundle "config set --local foo bar2"
+ bundle "config list --parseable"
+ expect(out).to include("foo=bar2")
+ end
+ end
+
+ context "with env overwrite" do
+ it "prints config with env" do
+ bundle "config list --parseable", env: { "BUNDLE_FOO" => "bar3" }
+ expect(out).to include("foo=bar3")
+ end
+ end
+ end
+ end
+
+ describe "location with a gemfile" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ G
+ end
+
+ it "is local by default" do
+ bundle "config set foo bar"
+ expect(bundled_app(".bundle/config")).to exist
+ expect(home(".bundle/config")).not_to exist
+ end
+
+ it "can be moved with an environment variable" do
+ ENV["BUNDLE_APP_CONFIG"] = tmp("foo/bar").to_s
+ bundle "config set --local path vendor/bundle"
+ bundle "install"
+
+ expect(bundled_app(".bundle")).not_to exist
+ expect(tmp("foo/bar/config")).to exist
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "can provide a relative path with the environment variable" do
+ FileUtils.mkdir_p bundled_app("omg")
+
+ ENV["BUNDLE_APP_CONFIG"] = "../foo"
+ bundle "config set --local path vendor/bundle"
+ bundle "install", dir: bundled_app("omg")
+
+ expect(bundled_app(".bundle")).not_to exist
+ expect(bundled_app("../foo/config")).to exist
+ expect(the_bundle).to include_gems "myrack 1.0.0", dir: bundled_app("omg")
+ end
+ end
+
+ describe "location without a gemfile" do
+ it "is global by default" do
+ bundle "config set foo bar"
+ expect(bundled_app(".bundle/config")).not_to exist
+ expect(home(".bundle/config")).to exist
+ end
+
+ it "does not list global settings as local" do
+ bundle "config set --global foo bar"
+ bundle "config list", dir: home
+
+ expect(out).to include("for the current user")
+ expect(out).not_to include("for your local app")
+ end
+
+ it "works with an absolute path" do
+ ENV["BUNDLE_APP_CONFIG"] = tmp("foo/bar").to_s
+ bundle "config set --local path vendor/bundle"
+
+ expect(bundled_app(".bundle")).not_to exist
+ expect(tmp("foo/bar/config")).to exist
+ end
+ end
+
+ describe "config location" do
+ let(:bundle_user_config) { File.join(Dir.home, ".config/bundler") }
+
+ before do
+ Dir.mkdir File.dirname(bundle_user_config)
+ end
+
+ it "can be configured through BUNDLE_USER_CONFIG" do
+ bundle "config set path vendor", env: { "BUNDLE_USER_CONFIG" => bundle_user_config }
+ bundle "config get path", env: { "BUNDLE_USER_CONFIG" => bundle_user_config }
+ expect(out).to include("Set for the current user (#{bundle_user_config}): \"vendor\"")
+ end
+
+ context "when not explicitly configured, but BUNDLE_USER_HOME set" do
+ let(:bundle_user_home) { bundled_app(".bundle").to_s }
+
+ it "uses the right location" do
+ bundle "config set path vendor", env: { "BUNDLE_USER_HOME" => bundle_user_home }
+ bundle "config get path", env: { "BUNDLE_USER_HOME" => bundle_user_home }
+ expect(out).to include("Set for the current user (#{bundle_user_home}/config): \"vendor\"")
+ end
+ end
+ end
+
+ describe "global" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ G
+ end
+
+ it "is the default" do
+ bundle "config set foo global"
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("global")
+ end
+
+ it "can also be set explicitly" do
+ bundle "config set --global foo global"
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("global")
+ end
+
+ it "has lower precedence than local" do
+ bundle "config set --local foo local"
+
+ bundle "config set --global foo global"
+ expect(out).to match(/Your application has set foo to "local"/)
+
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("local")
+ end
+
+ it "has lower precedence than env" do
+ ENV["BUNDLE_FOO"] = "env"
+
+ bundle "config set --global foo global"
+ expect(out).to match(/You have a bundler environment variable for foo set to "env"/)
+
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("env")
+ ensure
+ ENV.delete("BUNDLE_FOO")
+ end
+
+ it "can be deleted" do
+ bundle "config set --global foo global"
+ bundle "config unset foo"
+
+ run "puts Bundler.settings[:foo] == nil"
+ expect(out).to eq("true")
+ end
+
+ it "warns when overriding" do
+ bundle "config set --global foo previous"
+ bundle "config set --global foo global"
+ expect(out).to match(/You are replacing the current global value of foo/)
+
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("global")
+ end
+
+ it "does not warn when using the same value twice" do
+ bundle "config set --global foo value"
+ bundle "config set --global foo value"
+ expect(out).not_to match(/You are replacing the current global value of foo/)
+
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("value")
+ end
+
+ it "expands the path at time of setting" do
+ bundle "config set --global local.foo .."
+ run "puts Bundler.settings['local.foo']"
+ expect(out).to eq(File.expand_path(bundled_app.to_s + "/.."))
+ end
+
+ it "saves with parseable option" do
+ bundle "config set --global --parseable foo value"
+ expect(out).to eq("foo=value")
+ run "puts Bundler.settings['foo']"
+ expect(out).to eq("value")
+ end
+
+ context "when replacing a current value with the parseable flag" do
+ before { bundle "config set --global foo value" }
+ it "prints the current value in a parseable format" do
+ bundle "config set --global --parseable foo value2"
+ expect(out).to eq "foo=value2"
+ run "puts Bundler.settings['foo']"
+ expect(out).to eq("value2")
+ end
+ end
+ end
+
+ describe "local" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ G
+ end
+
+ it "can also be set explicitly" do
+ bundle "config set --local foo local"
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("local")
+ end
+
+ it "has higher precedence than env" do
+ ENV["BUNDLE_FOO"] = "env"
+ bundle "config set --local foo local"
+
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("local")
+ ensure
+ ENV.delete("BUNDLE_FOO")
+ end
+
+ it "can be deleted" do
+ bundle "config set --local foo local"
+ bundle "config unset foo"
+
+ run "puts Bundler.settings[:foo] == nil"
+ expect(out).to eq("true")
+ end
+
+ it "warns when overriding" do
+ bundle "config set --local foo previous"
+ bundle "config set --local foo local"
+ expect(out).to match(/You are replacing the current local value of foo/)
+
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("local")
+ end
+
+ it "expands the path at time of setting" do
+ bundle "config set --local local.foo .."
+ run "puts Bundler.settings['local.foo']"
+ expect(out).to eq(File.expand_path(bundled_app.to_s + "/.."))
+ end
+
+ it "can be deleted with parseable option" do
+ bundle "config set --local foo value"
+ bundle "config unset --parseable foo"
+ expect(out).to eq ""
+ run "puts Bundler.settings['foo'] == nil"
+ expect(out).to eq("true")
+ end
+ end
+
+ describe "env" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ G
+ end
+
+ it "can set boolean properties via the environment" do
+ ENV["BUNDLE_FROZEN"] = "true"
+
+ run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end"
+ expect(out).to eq("true")
+ end
+
+ it "can set negative boolean properties via the environment" do
+ run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end"
+ expect(out).to eq("false")
+
+ ENV["BUNDLE_FROZEN"] = "false"
+
+ run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end"
+ expect(out).to eq("false")
+
+ ENV["BUNDLE_FROZEN"] = "0"
+
+ run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end"
+ expect(out).to eq("false")
+
+ ENV["BUNDLE_FROZEN"] = ""
+
+ run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end"
+ expect(out).to eq("false")
+ end
+
+ it "can set properties with periods via the environment" do
+ ENV["BUNDLE_FOO__BAR"] = "baz"
+
+ run "puts Bundler.settings['foo.bar']"
+ expect(out).to eq("baz")
+ end
+ end
+
+ describe "parseable option" do
+ it "prints an empty string" do
+ bundle "config get foo --parseable", raise_on_error: false
+
+ expect(out).to eq ""
+ expect(last_command).to be_failure
+ end
+
+ it "only prints the value of the config" do
+ bundle "config set foo local"
+ bundle "config get foo --parseable"
+
+ expect(out).to eq "foo=local"
+ end
+
+ it "can print global config" do
+ bundle "config set --global bar value"
+ bundle "config get bar --parseable"
+
+ expect(out).to eq "bar=value"
+ end
+
+ it "prefers local config over global" do
+ bundle "config set --local bar value2"
+ bundle "config set --global bar value"
+ bundle "config get bar --parseable"
+
+ expect(out).to eq "bar=value2"
+ end
+ end
+
+ describe "gem mirrors" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ G
+ end
+
+ it "configures mirrors using keys with `mirror.`" do
+ bundle "config set --local mirror.http://gems.example.org http://gem-mirror.example.org"
+ run(<<-E)
+Bundler.settings.gem_mirrors.each do |k, v|
+ puts "\#{k} => \#{v}"
+end
+E
+ expect(out).to eq("http://gems.example.org/ => http://gem-mirror.example.org/")
+ end
+
+ it "allows configuring fallback timeout for each mirror, and does not duplicate configs", rubygems: ">= 3.5.12" do
+ bundle "config set --global mirror.https://rubygems.org.fallback_timeout 1"
+ bundle "config set --global mirror.https://rubygems.org.fallback_timeout 2"
+ expect(File.read(home(".bundle/config"))).to include("BUNDLE_MIRROR").once
+ end
+ end
+
+ describe "quoting" do
+ before(:each) { gemfile "source 'https://gem.repo1'" }
+ let(:long_string) do
+ "--with-xml2-include=/usr/pkg/include/libxml2 --with-xml2-lib=/usr/pkg/lib " \
+ "--with-xslt-dir=/usr/pkg"
+ end
+
+ it "saves quotes" do
+ bundle "config set foo something\\'"
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("something'")
+ end
+
+ it "doesn't return quotes around values" do
+ bundle "config set foo '1'"
+ run "puts Bundler.settings.send(:local_config_file).read"
+ expect(out).to include('"1"')
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("1")
+ end
+
+ it "doesn't duplicate quotes around values" do
+ bundled_app(".bundle").mkpath
+ File.open(bundled_app(".bundle/config"), "w") do |f|
+ f.write 'BUNDLE_FOO: "$BUILD_DIR"'
+ end
+
+ bundle "config set bar baz"
+ run "puts Bundler.settings.send(:local_config_file).read"
+
+ # Starting in Ruby 2.1, YAML automatically adds double quotes
+ # around some values, including $ and newlines.
+ expect(out).to include('BUNDLE_FOO: "$BUILD_DIR"')
+ end
+
+ it "doesn't duplicate quotes around long wrapped values" do
+ bundle "config set foo #{long_string}"
+
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq(long_string)
+
+ bundle "config set bar baz"
+
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq(long_string)
+ end
+ end
+
+ describe "very long lines" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ G
+ end
+
+ let(:long_string) do
+ "--with-xml2-include=/usr/pkg/include/libxml2 --with-xml2-lib=/usr/pkg/lib " \
+ "--with-xslt-dir=/usr/pkg"
+ end
+
+ let(:long_string_without_special_characters) do
+ "here is quite a long string that will wrap to a second line but will not be " \
+ "surrounded by quotes"
+ end
+
+ it "doesn't wrap values" do
+ bundle "config set foo #{long_string}"
+ run "puts Bundler.settings[:foo]"
+ expect(out).to match(long_string)
+ end
+
+ it "can read wrapped unquoted values" do
+ bundle "config set foo #{long_string_without_special_characters}"
+ run "puts Bundler.settings[:foo]"
+ expect(out).to match(long_string_without_special_characters)
+ end
+ end
+
+ describe "commented out settings with urls" do
+ before do
+ bundle "config set #mirror.https://rails-assets.org http://localhost:9292"
+ end
+
+ it "does not make bundler crash and ignores the configuration" do
+ bundle "config list --parseable"
+
+ expect(out).to be_empty
+ expect(err).to be_empty
+
+ ruby(<<~RUBY)
+ require "bundler"
+ print Bundler.settings.mirror_for("https://rails-assets.org")
+ RUBY
+ expect(out).to eq("https://rails-assets.org/")
+ expect(err).to be_empty
+
+ bundle "config set mirror.all http://localhost:9293"
+ ruby(<<~RUBY)
+ require "bundler"
+ print Bundler.settings.mirror_for("https://rails-assets.org")
+ RUBY
+ expect(out).to eq("http://localhost:9293/")
+ expect(err).to be_empty
+ end
+ end
+
+ describe "subcommands" do
+ it "list" do
+ bundle "config list", env: { "BUNDLE_FOO" => "bar" }
+ expect(out).to eq "Settings are listed in order of priority. The top value will be used.\nfoo\nSet via BUNDLE_FOO: \"bar\""
+
+ bundle "config list", env: { "BUNDLE_FOO" => "bar" }, parseable: true
+ expect(out).to eq "foo=bar"
+ end
+
+ it "list with credentials" do
+ bundle "config list", env: { "BUNDLE_GEMS__MYSERVER__COM" => "user:password" }
+ expect(out).to eq "Settings are listed in order of priority. The top value will be used.\ngems.myserver.com\nSet via BUNDLE_GEMS__MYSERVER__COM: \"user:[REDACTED]\""
+
+ bundle "config list", parseable: true, env: { "BUNDLE_GEMS__MYSERVER__COM" => "user:password" }
+ expect(out).to eq "gems.myserver.com=user:password"
+ end
+
+ it "list with API token credentials" do
+ bundle "config list", env: { "BUNDLE_GEMS__MYSERVER__COM" => "api_token:x-oauth-basic" }
+ expect(out).to eq "Settings are listed in order of priority. The top value will be used.\ngems.myserver.com\nSet via BUNDLE_GEMS__MYSERVER__COM: \"[REDACTED]:x-oauth-basic\""
+
+ bundle "config list", parseable: true, env: { "BUNDLE_GEMS__MYSERVER__COM" => "api_token:x-oauth-basic" }
+ expect(out).to eq "gems.myserver.com=api_token:x-oauth-basic"
+ end
+
+ it "get" do
+ ENV["BUNDLE_BAR"] = "bar_val"
+
+ bundle "config get foo", raise_on_error: false
+ expect(out).to eq "Settings for `foo` in order of priority. The top value will be used\nYou have not configured a value for `foo`"
+ expect(last_command).to be_failure
+
+ ENV["BUNDLE_FOO"] = "foo_val"
+
+ bundle "config get foo --parseable"
+ expect(out).to eq "foo=foo_val"
+
+ bundle "config get foo"
+ expect(out).to eq "Settings for `foo` in order of priority. The top value will be used\nSet via BUNDLE_FOO: \"foo_val\""
+ end
+
+ it "set" do
+ bundle "config set foo 1"
+ expect(out).to eq ""
+
+ bundle "config set --local foo 2"
+ expect(out).to eq ""
+
+ bundle "config set --global foo 3"
+ expect(out).to eq "Your application has set foo to \"2\". This will override the global value you are currently setting"
+
+ bundle "config set --parseable --local foo 4"
+ expect(out).to eq "foo=4"
+
+ bundle "config set --local foo 4.1"
+ expect(out).to eq "You are replacing the current local value of foo, which is currently \"4\""
+
+ bundle "config set --global --local foo 5", raise_on_error: false
+ expect(last_command).to be_failure
+ expect(err).to eq "The options global and local were specified. Please only use one of the switches at a time."
+ end
+
+ it "unset" do
+ bundle "config unset foo"
+ expect(out).to eq ""
+
+ bundle "config set foo 1"
+ bundle "config unset foo --parseable"
+ expect(out).to eq ""
+
+ bundle "config set --local foo 1"
+ bundle "config set --global foo 2"
+
+ bundle "config unset foo"
+ expect(out).to eq ""
+ expect(bundle("config get foo", raise_on_error: false)).to eq "Settings for `foo` in order of priority. The top value will be used\nYou have not configured a value for `foo`"
+ expect(last_command).to be_failure
+
+ bundle "config set --local foo 1"
+ bundle "config set --global foo 2"
+
+ bundle "config unset foo --local"
+ expect(out).to eq ""
+ expect(bundle("config get foo")).to eq "Settings for `foo` in order of priority. The top value will be used\nSet for the current user (#{home(".bundle/config")}): \"2\""
+ bundle "config unset foo --global"
+ expect(out).to eq ""
+ expect(bundle("config get foo", raise_on_error: false)).to eq "Settings for `foo` in order of priority. The top value will be used\nYou have not configured a value for `foo`"
+ expect(last_command).to be_failure
+
+ bundle "config set --local foo 1"
+ bundle "config set --global foo 2"
+
+ bundle "config unset foo --global"
+ expect(out).to eq ""
+ expect(bundle("config get foo")).to eq "Settings for `foo` in order of priority. The top value will be used\nSet for your local app (#{bundled_app(".bundle/config")}): \"1\""
+ bundle "config unset foo --local"
+ expect(out).to eq ""
+ expect(bundle("config get foo", raise_on_error: false)).to eq "Settings for `foo` in order of priority. The top value will be used\nYou have not configured a value for `foo`"
+ expect(last_command).to be_failure
+
+ bundle "config unset foo --local --global", raise_on_error: false
+ expect(last_command).to be_failure
+ expect(err).to eq "The options global and local were specified. Please only use one of the switches at a time."
+ end
+ end
+end
+
+RSpec.describe "setting gemfile via config" do
+ context "when a default Gemfile exists" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ gemfile bundled_app("foo/bar_gemfile"), <<-G
+ source "https://gem.repo1"
+ G
+ end
+
+ it "reports the local gemfile setting without promoting it to the environment" do
+ bundle "config set gemfile foo/bar_gemfile"
+
+ bundle "config list"
+ expect(out).to include("Set for your local app (#{bundled_app(".bundle/config")}): \"foo/bar_gemfile\"")
+ expect(out).not_to include("Set via BUNDLE_GEMFILE")
+ end
+
+ it "unsets the local gemfile setting from the app config" do
+ bundle "config set gemfile foo/bar_gemfile"
+
+ bundle "config unset gemfile"
+ bundle "config get gemfile", raise_on_error: false
+
+ expect(out).to include("You have not configured a value for `gemfile`")
+ expect(File.read(bundled_app(".bundle/config"))).not_to include("BUNDLE_GEMFILE")
+ end
+ end
+
+ context "when only the non-default Gemfile exists" do
+ it "persists the gemfile location to .bundle/config" do
+ gemfile bundled_app("NotGemfile"), <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle "config set --local gemfile #{bundled_app("NotGemfile")}"
+ expect(File.exist?(bundled_app(".bundle/config"))).to eq(true)
+
+ bundle "config list"
+ expect(out).to include("NotGemfile")
+ end
+ end
+end
+
+RSpec.describe "setting lockfile via config" do
+ it "persists the lockfile location to .bundle/config" do
+ gemfile bundled_app("NotGemfile"), <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle "config set --local gemfile #{bundled_app("NotGemfile")}"
+ bundle "config set --local lockfile #{bundled_app("ReallyNotGemfile.lock")}"
+ expect(File.exist?(bundled_app(".bundle/config"))).to eq(true)
+
+ bundle "config list"
+ expect(out).to include("NotGemfile")
+ expect(out).to include("ReallyNotGemfile.lock")
+ end
+end
diff --git a/spec/bundler/commands/console_spec.rb b/spec/bundler/commands/console_spec.rb
new file mode 100644
index 0000000000..a44f607546
--- /dev/null
+++ b/spec/bundler/commands/console_spec.rb
@@ -0,0 +1,214 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle console", readline: true do
+ before :each do
+ build_repo2 do
+ # A minimal fake pry console
+ build_gem "pry" do |s|
+ s.write "lib/pry.rb", <<-RUBY
+ class Pry
+ class << self
+ def toplevel_binding
+ unless defined?(@toplevel_binding) && @toplevel_binding
+ TOPLEVEL_BINDING.eval %{
+ def self.__pry__; binding; end
+ Pry.instance_variable_set(:@toplevel_binding, __pry__)
+ class << self; undef __pry__; end
+ }
+ end
+ @toplevel_binding.eval('private')
+ @toplevel_binding
+ end
+
+ def __pry__
+ while line = gets
+ begin
+ puts eval(line, toplevel_binding).inspect.sub(/^"(.*)"$/, '=> \\1')
+ rescue Exception => e
+ puts "\#{e.class}: \#{e.message}"
+ puts e.backtrace.first
+ end
+ end
+ end
+ alias start __pry__
+ end
+ end
+ RUBY
+ end
+
+ build_dummy_irb
+ end
+ end
+
+ context "when the library requires a non-existent file" do
+ before do
+ build_lib "loadfuuu", "1.0.0" do |s|
+ s.write "lib/loadfuuu.rb", "require_relative 'loadfuuu/bar'"
+ s.write "lib/loadfuuu/bar.rb", "require 'not-in-bundle'"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "irb"
+ path "#{lib_path}" do
+ gem "loadfuuu", require: true
+ end
+ G
+ end
+
+ it "does not show the bug report template" do
+ bundle("console", raise_on_error: false) do |input, _, _|
+ input.puts("exit")
+ end
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+ end
+ end
+
+ context "when the library references a non-existent constant" do
+ before do
+ build_lib "loadfuuu", "1.0.0" do |s|
+ s.write "lib/loadfuuu.rb", "Some::NonExistent::Constant"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "irb"
+ path "#{lib_path}" do
+ gem "loadfuuu", require: true
+ end
+ G
+ end
+
+ it "does not show the bug report template" do
+ bundle("console", raise_on_error: false) do |input, _, _|
+ input.puts("exit")
+ end
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+ end
+ end
+
+ context "when the library does not have any errors" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "irb"
+ gem "myrack"
+ gem "activesupport", :group => :test
+ gem "myrack_middleware", :group => :development
+ G
+ end
+
+ it "starts IRB with the default group loaded" do
+ bundle "console" do |input, _, _|
+ input.puts("puts MYRACK")
+ input.puts("exit")
+ end
+ expect(out).to include("0.9.1")
+ end
+
+ it "uses IRB as default console" do
+ skip "Does not work in a ruby-core context if irb is in the default $LOAD_PATH because it enables the real IRB, not our dummy one" if ruby_core? && Gem.ruby_version < Gem::Version.new("3.5.0.a")
+
+ bundle "console" do |input, _, _|
+ input.puts("__method__")
+ input.puts("exit")
+ end
+ expect(out).to include("__irb__")
+ end
+
+ it "starts another REPL if configured as such" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "irb"
+ gem "pry"
+ G
+ bundle_config "console pry"
+
+ bundle "console" do |input, _, _|
+ input.puts("__method__")
+ input.puts("exit")
+ end
+ expect(out).to include(":__pry__")
+ end
+
+ it "falls back to IRB if the other REPL isn't available" do
+ skip "Does not work in a ruby-core context if irb is in the default $LOAD_PATH because it enables the real IRB, not our dummy one" if ruby_core? && Gem.ruby_version < Gem::Version.new("3.5.0.a")
+
+ bundle_config "console pry"
+ # make sure pry isn't there
+
+ bundle "console" do |input, _, _|
+ input.puts("__method__")
+ input.puts("exit")
+ end
+ expect(out).to include("__irb__")
+ end
+
+ it "does not try IRB twice if no console is configured and IRB is not available" do
+ create_file("irb.rb", "raise LoadError, 'irb is not available'")
+
+ bundle("console", env: { "RUBYOPT" => "-I#{bundled_app} #{ENV["RUBYOPT"]}" }, raise_on_error: false) do |input, _, _|
+ input.puts("puts ACTIVESUPPORT")
+ input.puts("exit")
+ end
+ expect(err).not_to include("falling back to irb")
+ expect(err).to include("irb is not available")
+ end
+
+ it "doesn't load any other groups" do
+ bundle "console" do |input, _, _|
+ input.puts("puts ACTIVESUPPORT")
+ input.puts("exit")
+ end
+ expect(out).to include("NameError")
+ end
+
+ describe "when given a group" do
+ it "loads the given group" do
+ bundle "console test" do |input, _, _|
+ input.puts("puts ACTIVESUPPORT")
+ input.puts("exit")
+ end
+ expect(out).to include("2.3.5")
+ end
+
+ it "loads the default group" do
+ bundle "console test" do |input, _, _|
+ input.puts("puts MYRACK")
+ input.puts("exit")
+ end
+ expect(out).to include("0.9.1")
+ end
+
+ it "doesn't load other groups" do
+ bundle "console test" do |input, _, _|
+ input.puts("puts MYRACK_MIDDLEWARE")
+ input.puts("exit")
+ end
+ expect(out).to include("NameError")
+ end
+ end
+
+ it "performs an automatic bundle install" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "irb"
+ gem "myrack"
+ gem "activesupport", :group => :test
+ gem "myrack_middleware", :group => :development
+ gem "foo"
+ G
+
+ bundle_config "auto_install 1"
+ bundle :console do |input, _, _|
+ input.puts("puts 'hello'")
+ input.puts("exit")
+ end
+ expect(out).to include("Installing foo 1.0")
+ expect(out).to include("hello")
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+ end
+end
diff --git a/spec/bundler/commands/doctor_spec.rb b/spec/bundler/commands/doctor_spec.rb
new file mode 100644
index 0000000000..d350b4b3d1
--- /dev/null
+++ b/spec/bundler/commands/doctor_spec.rb
@@ -0,0 +1,185 @@
+# frozen_string_literal: true
+
+require "find"
+require "stringio"
+require "bundler/cli"
+require "bundler/cli/doctor"
+require "bundler/cli/doctor/diagnose"
+
+RSpec.describe "bundle doctor" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ @stdout = StringIO.new
+
+ [:error, :warn, :info].each do |method|
+ allow(Bundler.ui).to receive(method).and_wrap_original do |m, message|
+ m.call message
+ @stdout.puts message
+ end
+ end
+ end
+
+ it "succeeds on a sane installation" do
+ bundle :doctor
+ end
+
+ context "when all files in home are readable/writable" do
+ before(:each) do
+ stat = double("stat")
+ unwritable_file = double("file")
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ allow(Find).to receive(:find).with(Bundler.bundle_path.to_s) { [unwritable_file] }
+ allow(File).to receive(:exist?).and_call_original
+ allow(File).to receive(:writable?).and_call_original
+ allow(File).to receive(:readable?).and_call_original
+ allow(File).to receive(:exist?).with(unwritable_file).and_return(true)
+ allow(File).to receive(:stat).with(unwritable_file) { stat }
+ allow(stat).to receive(:uid) { Process.uid }
+ allow(File).to receive(:writable?).with(unwritable_file) { true }
+ allow(File).to receive(:readable?).with(unwritable_file) { true }
+
+ # The following lines are for `Gem::PathSupport#initialize`.
+ allow(File).to receive(:exist?).with(Gem.default_dir)
+ allow(File).to receive(:writable?).with(Gem.default_dir)
+ allow(File).to receive(:writable?).with(File.expand_path("..", Gem.default_dir))
+ end
+
+ it "exits with a success message if the installed gem has no C extensions" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ allow(doctor).to receive(:lookup_with_fiddle).and_return(false)
+ expect { doctor.run }.not_to raise_error
+ expect(@stdout.string).to include("No issues")
+ end
+
+ it "exits with a success message if the installed gem's C extension dylib breakage is fine" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ expect(doctor).to receive(:bundles_for_gem).exactly(2).times.and_return ["/path/to/myrack/myrack.bundle"]
+ expect(doctor).to receive(:dylibs).exactly(2).times.and_return ["/usr/lib/libSystem.dylib"]
+ allow(doctor).to receive(:lookup_with_fiddle).with("/usr/lib/libSystem.dylib").and_return(false)
+ expect { doctor.run }.not_to raise_error
+ expect(@stdout.string).to include("No issues")
+ end
+
+ it "parses otool output correctly" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ expect(doctor).to receive(:`).with("/usr/bin/otool -L fake").and_return("/home/gem/ruby/3.4.3/gems/blake3-rb-1.5.4.4/lib/digest/blake3/blake3_ext.bundle:\n\t (compatibility version 0.0.0, current version 0.0.0)\n\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)")
+ expect(doctor.dylibs_darwin("fake")).to eq(["/usr/lib/libSystem.B.dylib"])
+ end
+
+ it "exits with a message if one of the linked libraries is missing" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ expect(doctor).to receive(:bundles_for_gem).exactly(2).times.and_return ["/path/to/myrack/myrack.bundle"]
+ expect(doctor).to receive(:dylibs).exactly(2).times.and_return ["/usr/local/opt/icu4c/lib/libicui18n.57.1.dylib"]
+ allow(doctor).to receive(:lookup_with_fiddle).with("/usr/local/opt/icu4c/lib/libicui18n.57.1.dylib").and_return(true)
+ expect { doctor.run }.to raise_error(Bundler::ProductionError, <<~E.strip), @stdout.string
+ The following gems are missing OS dependencies:
+ * bundler: /usr/local/opt/icu4c/lib/libicui18n.57.1.dylib
+ * myrack: /usr/local/opt/icu4c/lib/libicui18n.57.1.dylib
+ E
+ end
+ end
+
+ context "when home contains broken symlinks" do
+ before(:each) do
+ @broken_symlink = double("file")
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ allow(Find).to receive(:find).with(Bundler.bundle_path.to_s) { [@broken_symlink] }
+ allow(File).to receive(:exist?).and_call_original
+ allow(File).to receive(:exist?).with(@broken_symlink) { false }
+ end
+
+ it "exits with an error if home contains files that are not readable/writable" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ allow(doctor).to receive(:lookup_with_fiddle).and_return(false)
+ expect { doctor.run }.not_to raise_error
+ expect(@stdout.string).to include(
+ "Broken links exist in the Bundler home. Please report them to the offending gem's upstream repo. These files are:\n - #{@broken_symlink}"
+ )
+ expect(@stdout.string).not_to include("No issues")
+ end
+ end
+
+ context "when home contains files that are not readable/writable" do
+ before(:each) do
+ @stat = double("stat")
+ @unwritable_file = double("file")
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ allow(Find).to receive(:find).with(Bundler.bundle_path.to_s) { [@unwritable_file] }
+ allow(File).to receive(:exist?).and_call_original
+ allow(File).to receive(:writable?).and_call_original
+ allow(File).to receive(:readable?).and_call_original
+ allow(File).to receive(:exist?).with(@unwritable_file) { true }
+ allow(File).to receive(:stat).with(@unwritable_file) { @stat }
+ end
+
+ it "exits with an error if home contains files that are not readable" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ allow(doctor).to receive(:lookup_with_fiddle).and_return(false)
+ allow(@stat).to receive(:uid) { Process.uid }
+ allow(File).to receive(:writable?).with(@unwritable_file) { false }
+ allow(File).to receive(:readable?).with(@unwritable_file) { false }
+ expect { doctor.run }.not_to raise_error
+ expect(@stdout.string).to include(
+ "Files exist in the Bundler home that are not readable by the current user. These files are:\n - #{@unwritable_file}"
+ )
+ expect(@stdout.string).not_to include("No issues")
+ end
+
+ it "exits without an error if home contains files that are not writable" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ allow(doctor).to receive(:lookup_with_fiddle).and_return(false)
+ allow(@stat).to receive(:uid) { Process.uid }
+ allow(File).to receive(:writable?).with(@unwritable_file) { false }
+ allow(File).to receive(:readable?).with(@unwritable_file) { true }
+ expect { doctor.run }.not_to raise_error
+ expect(@stdout.string).not_to include(
+ "Files exist in the Bundler home that are not readable by the current user. These files are:\n - #{@unwritable_file}"
+ )
+ expect(@stdout.string).to include("No issues")
+ end
+
+ context "when home contains files that are not owned by the current process", :permissions do
+ before(:each) do
+ allow(@stat).to receive(:uid) { 0o0000 }
+ end
+
+ it "exits with an error if home contains files that are not readable/writable and are not owned by the current user" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ allow(doctor).to receive(:lookup_with_fiddle).and_return(false)
+ allow(File).to receive(:writable?).with(@unwritable_file) { false }
+ allow(File).to receive(:readable?).with(@unwritable_file) { false }
+ expect { doctor.run }.not_to raise_error
+ expect(@stdout.string).to include(
+ "Files exist in the Bundler home that are owned by another user, and are not readable. These files are:\n - #{@unwritable_file}"
+ )
+ expect(@stdout.string).not_to include("No issues")
+ end
+
+ it "exits with a warning if home contains files that are read/write but not owned by current user" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ allow(doctor).to receive(:lookup_with_fiddle).and_return(false)
+ allow(File).to receive(:writable?).with(@unwritable_file) { true }
+ allow(File).to receive(:readable?).with(@unwritable_file) { true }
+ expect { doctor.run }.not_to raise_error
+ expect(@stdout.string).to include(
+ "Files exist in the Bundler home that are owned by another user, but are still readable. These files are:\n - #{@unwritable_file}"
+ )
+ expect(@stdout.string).not_to include("No issues")
+ end
+ end
+ end
+
+ context "when home contains filenames with special characters" do
+ it "escape filename before command execute" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ expect(doctor).to receive(:`).with("/usr/bin/otool -L \\$\\(date\\)\\ \\\"\\'\\\\.bundle").and_return("dummy string")
+ doctor.dylibs_darwin('$(date) "\'\.bundle')
+ expect(doctor).to receive(:`).with("/usr/bin/ldd \\$\\(date\\)\\ \\\"\\'\\\\.bundle").and_return("dummy string")
+ doctor.dylibs_ldd('$(date) "\'\.bundle')
+ end
+ end
+end
diff --git a/spec/bundler/commands/exec_spec.rb b/spec/bundler/commands/exec_spec.rb
new file mode 100644
index 0000000000..aa35685be8
--- /dev/null
+++ b/spec/bundler/commands/exec_spec.rb
@@ -0,0 +1,1272 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle exec" do
+ it "works with --gemfile flag" do
+ system_gems(%w[myrack-1.0.0 myrack-0.9.1], path: default_bundle_path)
+
+ gemfile "CustomGemfile", <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ G
+
+ bundle "exec --gemfile CustomGemfile myrackup"
+ expect(out).to eq("1.0.0")
+ end
+
+ it "activates the correct gem" do
+ system_gems(%w[myrack-1.0.0 myrack-0.9.1], path: default_bundle_path)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+
+ bundle "exec myrackup"
+ expect(out).to eq("0.9.1")
+ end
+
+ it "works and prints no warnings when HOME is not writable" do
+ system_gems(%w[myrack-1.0.0 myrack-0.9.1], path: default_bundle_path)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+
+ bundle "exec myrackup", env: { "HOME" => "/" }
+ expect(out).to eq("0.9.1")
+ expect(err).to be_empty
+ end
+
+ it "works when the bins are in ~/.bundle" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "exec myrackup"
+ expect(out).to eq("1.0.0")
+ end
+
+ it "works when running from a random directory" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "exec 'cd #{tmp("gems")} && myrackup'"
+
+ expect(out).to eq("1.0.0")
+ end
+
+ it "works when exec'ing something else" do
+ install_gemfile "source \"https://gem.repo1\"; gem \"myrack\""
+ bundle "exec echo exec"
+ expect(out).to eq("exec")
+ end
+
+ it "works when exec'ing to ruby" do
+ install_gemfile "source \"https://gem.repo1\"; gem \"myrack\""
+ bundle "exec ruby -e 'puts %{hi}'"
+ expect(out).to eq("hi")
+ end
+
+ it "works when exec'ing to rubygems" do
+ install_gemfile "source \"https://gem.repo1\"; gem \"myrack\""
+ bundle "exec #{gem_cmd} --version"
+ expect(out).to eq(Gem::VERSION)
+ end
+
+ it "works when exec'ing to rubygems through sh -c" do
+ install_gemfile "source \"https://gem.repo1\"; gem \"myrack\""
+ bundle "exec sh -c '#{gem_cmd} --version'"
+ expect(out).to eq(Gem::VERSION)
+ end
+
+ it "works when exec'ing back to bundler to run a remote resolve" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+
+ bundle "exec bundle lock", env: { "BUNDLER_VERSION" => Bundler::VERSION }
+
+ expect(out).to include("Writing lockfile")
+ end
+
+ it "respects custom process title when loading through ruby" do
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ script_that_changes_its_own_title_and_checks_if_picked_up_by_ps_unix_utility = <<~'RUBY'
+ Process.setproctitle("1-2-3-4-5-6-7")
+ puts `ps -ocommand= -p#{$$}`
+ RUBY
+ gemfile "Gemfile", "source \"https://gem.repo1\""
+ create_file "a.rb", script_that_changes_its_own_title_and_checks_if_picked_up_by_ps_unix_utility
+ bundle "exec ruby a.rb"
+ expect(out).to eq("1-2-3-4-5-6-7")
+ end
+
+ it "accepts --verbose" do
+ install_gemfile "source \"https://gem.repo1\"; gem \"myrack\""
+ bundle "exec --verbose echo foobar"
+ expect(out).to eq("foobar")
+ end
+
+ it "passes --verbose to command if it is given after the command" do
+ install_gemfile "source \"https://gem.repo1\"; gem \"myrack\""
+ bundle "exec echo --verbose"
+ expect(out).to eq("--verbose")
+ end
+
+ it "handles --keep-file-descriptors" do
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ require "tempfile"
+
+ command = Tempfile.new("io-test")
+ command.sync = true
+ command.write <<-G
+ if ARGV[0]
+ IO.for_fd(ARGV[0].to_i)
+ else
+ require 'tempfile'
+ io = Tempfile.new("io-test-fd")
+ args = %W[#{Gem.ruby} -I#{lib_dir} #{bindir.join("bundle")} exec --keep-file-descriptors #{Gem.ruby} #{command.path} \#{io.to_i}]
+ args << { io.to_i => io }
+ exec(*args)
+ end
+ G
+
+ install_gemfile "source \"https://gem.repo1\""
+ in_bundled_app "#{Gem.ruby} #{command.path}"
+
+ expect(out).to be_empty
+ expect(err).to be_empty
+ end
+
+ it "accepts --keep-file-descriptors" do
+ install_gemfile "source \"https://gem.repo1\""
+ bundle "exec --keep-file-descriptors echo foobar"
+
+ expect(err).to be_empty
+ end
+
+ it "can run a command named --verbose" do
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ install_gemfile "source \"https://gem.repo1\"; gem \"myrack\""
+ File.open(bundled_app("--verbose"), "w") do |f|
+ f.puts "#!/bin/sh"
+ f.puts "echo foobar"
+ end
+ File.chmod(0o744, bundled_app("--verbose"))
+ with_path_as(".") do
+ bundle "exec -- --verbose"
+ end
+ expect(out).to eq("foobar")
+ end
+
+ it "handles different versions in different bundles" do
+ build_repo2 do
+ build_gem "myrack_two", "1.0.0" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+
+ install_gemfile bundled_app2("Gemfile"), <<-G, dir: bundled_app2
+ source "https://gem.repo2"
+ gem "myrack_two", "1.0.0"
+ G
+
+ bundle "exec myrackup"
+
+ expect(out).to eq("0.9.1")
+
+ bundle "exec myrackup", dir: bundled_app2
+ expect(out).to eq("1.0.0")
+ end
+
+ context "with default gems" do
+ # TODO: Switch to ERB::VERSION once Ruby 3.4 support is dropped, so all
+ # supported rubies include an `erb` gem version where `ERB::VERSION` is
+ # public
+ let(:default_erb_version) { ruby "require 'erb/version'; puts ERB.const_get(:VERSION)" }
+
+ context "when not specified in Gemfile" do
+ before do
+ install_gemfile "source \"https://gem.repo1\""
+ end
+
+ it "uses version provided by ruby" do
+ bundle "exec erb --version"
+
+ expect(stdboth).to eq(default_erb_version)
+ end
+ end
+
+ context "when specified in Gemfile directly" do
+ let(:specified_erb_version) { "2.0.0" }
+
+ before do
+ build_repo2 do
+ build_gem "erb", specified_erb_version do |s|
+ s.executables = "erb"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "erb", "#{specified_erb_version}"
+ G
+ end
+
+ it "uses version specified" do
+ bundle "exec erb --version"
+
+ expect(stdboth).to eq(specified_erb_version)
+ end
+ end
+
+ context "when specified in Gemfile indirectly" do
+ let(:indirect_erb_version) { "2.0.0" }
+
+ before do
+ build_repo2 do
+ build_gem "erb", indirect_erb_version do |s|
+ s.executables = "erb"
+ end
+
+ build_gem "gem_depending_on_old_erb" do |s|
+ s.add_dependency "erb", indirect_erb_version
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "gem_depending_on_old_erb"
+ G
+ end
+
+ it "uses resolved version" do
+ bundle "exec erb --version"
+
+ expect(stdboth).to eq(indirect_erb_version)
+ end
+ end
+ end
+
+ it "warns about executable conflicts" do
+ build_repo2 do
+ build_gem "myrack_two", "1.0.0" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ bundle_config_global "path.system true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+
+ install_gemfile bundled_app2("Gemfile"), <<-G, dir: bundled_app2
+ source "https://gem.repo2"
+ gem "myrack_two", "1.0.0"
+ G
+
+ bundle "exec myrackup"
+
+ expect(last_command.stderr).to eq(
+ "Bundler is using a binstub that was created for a different gem (myrack).\n" \
+ "You should run `bundle binstub myrack_two` to work around a system/bundle conflict."
+ )
+ end
+
+ it "handles gems installed with --without" do
+ bundle_config "without middleware"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack" # myrack 0.9.1 and 1.0 exist
+
+ group :middleware do
+ gem "myrack_middleware" # myrack_middleware depends on myrack 0.9.1
+ end
+ G
+
+ bundle "exec myrackup"
+
+ expect(out).to eq("0.9.1")
+ expect(the_bundle).not_to include_gems "myrack_middleware 1.0"
+ end
+
+ it "does not duplicate already exec'ed RUBYOPT" do
+ create_file("echoopt", "#!/usr/bin/env ruby\nprint ENV['RUBYOPT']")
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundler_setup_opt = "-r#{lib_dir}/bundler/setup"
+
+ rubyopt = opt_add(bundler_setup_opt, ENV["RUBYOPT"])
+
+ bundle "exec echoopt"
+ expect(out.split(" ").count(bundler_setup_opt)).to eq(1)
+
+ bundle "exec echoopt", env: { "RUBYOPT" => rubyopt }
+ expect(out.split(" ").count(bundler_setup_opt)).to eq(1)
+ end
+
+ it "does not duplicate already exec'ed RUBYLIB" do
+ create_file("echolib", "#!/usr/bin/env ruby\nprint ENV['RUBYLIB']")
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ rubylib = ENV["RUBYLIB"]
+ rubylib = rubylib.to_s.split(File::PATH_SEPARATOR).unshift lib_dir.to_s
+ rubylib = rubylib.uniq.join(File::PATH_SEPARATOR)
+
+ bundle "exec echolib"
+ expect(out).to include(rubylib)
+
+ bundle "exec echolib", env: { "RUBYLIB" => rubylib }
+ expect(out).to include(rubylib)
+ end
+
+ it "errors nicely when the argument doesn't exist" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "exec foobarbaz", raise_on_error: false
+ expect(exitstatus).to eq(127)
+ expect(err).to include("bundler: command not found: foobarbaz")
+ expect(err).to include("Install missing gem executables with `bundle install`")
+ end
+
+ it "errors nicely when the argument is not executable" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundled_app("foo").write("")
+ bundle "exec ./foo", raise_on_error: false
+ expect(exitstatus).to eq(126)
+ expect(err).to include("bundler: not executable: ./foo")
+ end
+
+ it "errors nicely when no arguments are passed" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "exec", raise_on_error: false
+ expect(exitstatus).to eq(128)
+ expect(err).to include("bundler: exec needs a command to run")
+ end
+
+ it "raises a helpful error when exec'ing to something outside of the bundle" do
+ system_gems(%w[myrack-1.0.0 myrack-0.9.1], path: default_bundle_path)
+
+ bundle_config "clean false" # want to keep the myrackup binstub
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo"
+ G
+ [true, false].each do |l|
+ bundle_config "disable_exec_load #{l}"
+ bundle "exec myrackup", raise_on_error: false
+ expect(err).to include "can't find executable myrackup for gem myrack. myrack is not currently included in the bundle, perhaps you meant to add it to your Gemfile?"
+ end
+ end
+
+ describe "with help flags" do
+ each_prefix = proc do |string, &blk|
+ 1.upto(string.length) {|l| blk.call(string[0, l]) }
+ end
+ each_prefix.call("exec") do |exec|
+ describe "when #{exec} is used" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ create_file("print_args", <<-'RUBY')
+ #!/usr/bin/env ruby
+ puts "args: #{ARGV.inspect}"
+ RUBY
+ end
+
+ it "shows executable's man page when --help is after the executable" do
+ bundle "#{exec} print_args --help"
+ expect(out).to eq('args: ["--help"]')
+ end
+
+ it "shows executable's man page when --help is after the executable and an argument" do
+ bundle "#{exec} print_args foo --help"
+ expect(out).to eq('args: ["foo", "--help"]')
+
+ bundle "#{exec} print_args foo bar --help"
+ expect(out).to eq('args: ["foo", "bar", "--help"]')
+
+ bundle "#{exec} print_args foo --help bar"
+ expect(out).to eq('args: ["foo", "--help", "bar"]')
+ end
+
+ it "shows executable's man page when the executable has a -" do
+ FileUtils.mv(bundled_app("print_args"), bundled_app("docker-template"))
+ FileUtils.mv(bundled_app("print_args.bat"), bundled_app("docker-template.bat")) if Gem.win_platform?
+ bundle "#{exec} docker-template build discourse --help"
+ expect(out).to eq('args: ["build", "discourse", "--help"]')
+ end
+
+ it "shows executable's man page when --help is after another flag" do
+ bundle "#{exec} print_args --bar --help"
+ expect(out).to eq('args: ["--bar", "--help"]')
+ end
+
+ it "uses executable's original behavior for -h" do
+ bundle "#{exec} print_args -h"
+ expect(out).to eq('args: ["-h"]')
+ end
+
+ it "shows bundle-exec's man page when --help is between exec and the executable" do
+ with_fake_man do
+ bundle "#{exec} --help echo"
+ end
+ expect(out).to include(%(["#{man_dir}/bundle-exec.1"]))
+ end
+
+ it "shows bundle-exec's man page when --help is before exec" do
+ with_fake_man do
+ bundle "--help #{exec}"
+ end
+ expect(out).to include(%(["#{man_dir}/bundle-exec.1"]))
+ end
+
+ it "shows bundle-exec's man page when -h is before exec" do
+ with_fake_man do
+ bundle "-h #{exec}"
+ end
+ expect(out).to include(%(["#{man_dir}/bundle-exec.1"]))
+ end
+
+ it "shows bundle-exec's man page when --help is after exec" do
+ with_fake_man do
+ bundle "#{exec} --help"
+ end
+ expect(out).to include(%(["#{man_dir}/bundle-exec.1"]))
+ end
+
+ it "shows bundle-exec's man page when -h is after exec" do
+ with_fake_man do
+ bundle "#{exec} -h"
+ end
+ expect(out).to include(%(["#{man_dir}/bundle-exec.1"]))
+ end
+ end
+ end
+ end
+
+ describe "with gem executables" do
+ describe "run from a random directory" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "works when unlocked" do
+ bundle "exec 'cd #{tmp("gems")} && myrackup'"
+ expect(out).to eq("1.0.0")
+ end
+
+ it "works when locked" do
+ expect(the_bundle).to be_locked
+ bundle "exec 'cd #{tmp("gems")} && myrackup'"
+ expect(out).to eq("1.0.0")
+ end
+ end
+
+ describe "from gems bundled via :path" do
+ before(:each) do
+ build_lib "fizz", path: home("fizz") do |s|
+ s.executables = "fizz"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "fizz", :path => "#{File.expand_path(home("fizz"))}"
+ G
+ end
+
+ it "works when unlocked" do
+ bundle "exec fizz"
+ expect(out).to eq("1.0")
+ end
+
+ it "works when locked" do
+ expect(the_bundle).to be_locked
+
+ bundle "exec fizz"
+ expect(out).to eq("1.0")
+ end
+ end
+
+ describe "from gems bundled via :git" do
+ before(:each) do
+ build_git "fizz_git" do |s|
+ s.executables = "fizz_git"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "fizz_git", :git => "#{lib_path("fizz_git-1.0")}"
+ G
+ end
+
+ it "works when unlocked" do
+ bundle "exec fizz_git"
+ expect(out).to eq("1.0")
+ end
+
+ it "works when locked" do
+ expect(the_bundle).to be_locked
+ bundle "exec fizz_git"
+ expect(out).to eq("1.0")
+ end
+ end
+
+ describe "from gems bundled via :git with no gemspec" do
+ before(:each) do
+ build_git "fizz_no_gemspec", gemspec: false do |s|
+ s.executables = "fizz_no_gemspec"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "fizz_no_gemspec", "1.0", :git => "#{lib_path("fizz_no_gemspec-1.0")}"
+ G
+ end
+
+ it "works when unlocked" do
+ bundle "exec fizz_no_gemspec"
+ expect(out).to eq("1.0")
+ end
+
+ it "works when locked" do
+ expect(the_bundle).to be_locked
+ bundle "exec fizz_no_gemspec"
+ expect(out).to eq("1.0")
+ end
+ end
+ end
+
+ it "performs an automatic bundle install" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ gem "foo"
+ G
+
+ bundle_config "auto_install 1"
+ bundle "exec myrackup", artifice: "compact_index"
+ expect(out).to include("Installing foo 1.0")
+ end
+
+ it "performs an automatic bundle install with git gems" do
+ build_git "foo" do |s|
+ s.executables = "foo"
+ end
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle_config "auto_install 1"
+ bundle "exec foo", artifice: "compact_index"
+ expect(out).to include("Fetching myrack 0.9.1")
+ expect(out).to include("Fetching #{lib_path("foo-1.0")}")
+ expect(out.lines).to end_with("1.0")
+ end
+
+ it "loads the correct optparse when `auto_install` is set, and optparse is a dependency" do
+ build_repo4 do
+ build_gem "fastlane", "2.192.0" do |s|
+ s.executables = "fastlane"
+ s.add_dependency "optparse", "~> 999.999.999"
+ end
+
+ build_gem "optparse", "999.999.998"
+ build_gem "optparse", "999.999.999"
+ end
+
+ system_gems "optparse-999.999.998", gem_repo: gem_repo4
+
+ bundle_config "auto_install 1"
+ bundle_config "path vendor/bundle"
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "fastlane"
+ G
+
+ bundle "exec fastlane", artifice: "compact_index"
+ expect(out).to include("Installing optparse 999.999.999")
+ expect(out).to include("2.192.0")
+ end
+
+ describe "with gems bundled via :path with invalid gemspecs" do
+ it "outputs the gemspec validation errors" do
+ build_lib "foo"
+
+ gemspec = lib_path("foo-1.0").join("foo.gemspec").to_s
+ File.open(gemspec, "w") do |f|
+ f.write <<-G
+ Gem::Specification.new do |s|
+ s.name = 'foo'
+ s.version = '1.0'
+ s.summary = 'TODO: Add summary'
+ s.authors = 'Me'
+ s.rubygems_version = nil
+ end
+ G
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle "exec erb", raise_on_error: false
+
+ expect(err).to match("The gemspec at #{lib_path("foo-1.0").join("foo.gemspec")} is not valid")
+ expect(err).to match(/missing value for attribute rubygems_version|rubygems_version must not be nil/)
+ end
+ end
+
+ describe "with gems bundled for deployment" do
+ it "works when calling bundler from another script" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ module Monkey
+ def bin_path(a,b,c)
+ raise Gem::GemNotFoundException.new('Fail')
+ end
+ end
+ Bundler.rubygems.extend(Monkey)
+ G
+ bundle_config "path.system true"
+ bundle "install"
+ bundle "exec ruby -e '`bundle -v`; puts $?.success?'", env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ expect(out).to match("true")
+ end
+ end
+
+ describe "bundle exec gem uninstall" do
+ before do
+ build_repo4 do
+ build_gem "foo"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "foo"
+ G
+ end
+
+ it "works" do
+ bundle "exec #{gem_cmd} uninstall foo"
+ expect(out).to eq("Successfully uninstalled foo-1.0")
+ end
+ end
+
+ describe "running gem commands in presence of rubygems plugins" do
+ before do
+ build_repo4 do
+ build_gem "foo" do |s|
+ s.write "lib/rubygems_plugin.rb", "puts 'FAIL'"
+ end
+ end
+
+ system_gems "foo-1.0", path: default_bundle_path, gem_repo: gem_repo4
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ G
+ end
+
+ it "does not load plugins outside of the bundle" do
+ bundle "exec #{gem_cmd} -v"
+ expect(out).not_to include("FAIL")
+ end
+ end
+
+ context "`load`ing a ruby file instead of `exec`ing" do
+ let(:path) { bundled_app("ruby_executable") }
+ let(:shebang) { "#!/usr/bin/env ruby" }
+ let(:executable) { <<~RUBY.strip }
+ #{shebang}
+
+ require "myrack"
+ puts "EXEC: \#{caller.grep(/load/).empty? ? 'exec' : 'load'}"
+ puts "ARGS: \#{$0} \#{ARGV.join(' ')}"
+ puts "MYRACK: \#{MYRACK}"
+ if Gem.win_platform?
+ process_title = "ruby"
+ else
+ process_title = `ps -o args -p \#{Process.pid}`.split("\n", 2).last.strip
+ end
+ puts "PROCESS: \#{process_title}"
+ RUBY
+
+ before do
+ create_file(bundled_app(path), executable)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ let(:exec) { "EXEC: load" }
+ let(:args) { "ARGS: #{path} arg1 arg2" }
+ let(:myrack) { "MYRACK: 1.0.0" }
+ let(:process) do
+ if Gem.win_platform?
+ "PROCESS: ruby"
+ else
+ "PROCESS: #{path} arg1 arg2"
+ end
+ end
+ let(:exit_code) { 0 }
+ let(:expected) { [exec, args, myrack, process].join("\n") }
+ let(:expected_err) { "" }
+
+ subject { bundle "exec #{path} arg1 arg2", raise_on_error: false }
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+
+ context "the executable exits explicitly" do
+ let(:executable) { super() << "\nexit #{exit_code}\nputs 'POST_EXIT'\n" }
+
+ context "with exit 0" do
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "with exit 99" do
+ let(:exit_code) { 99 }
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+ end
+
+ context "the executable exits by SignalException" do
+ let(:executable) do
+ ex = super()
+ ex << "\n"
+ ex << "raise SignalException, 'SIGTERM'\n"
+ ex
+ end
+ let(:expected_err) { "" }
+ let(:exit_code) do
+ exit_status_for_signal(Signal.list["TERM"])
+ end
+
+ it "runs" do
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "the executable is empty" do
+ let(:executable) { "" }
+
+ let(:exit_code) { 0 }
+ let(:expected_err) { "#{path} is empty" }
+ let(:expected) { "" }
+
+ it "runs" do
+ # it's empty, so `create_file` won't add executable permission and bat scripts on Windows
+ bundled_app(path).chmod(0o755)
+ path.sub_ext(".bat").write <<~SCRIPT if Gem.win_platform?
+ @ECHO OFF
+ @"ruby.exe" "%~dpn0" %*
+ SCRIPT
+
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "the executable raises" do
+ let(:executable) { super() << "\nraise 'ERROR'" }
+ let(:exit_code) { 1 }
+ let(:expected_err) do
+ /\Abundler: failed to load command: #{Regexp.quote(path.to_s)} \(#{Regexp.quote(path.to_s)}\)\n#{Regexp.quote(path.to_s)}:[0-9]+:in [`']<top \(required\)>': ERROR \(RuntimeError\)/
+ end
+
+ it "runs like a normally executed executable" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to match(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "the executable raises an error without a backtrace" do
+ let(:executable) { super() << "\nclass Err < Exception\ndef backtrace; end;\nend\nraise Err" }
+ let(:exit_code) { 1 }
+ let(:expected_err) { "bundler: failed to load command: #{path} (#{path})\n#{system_gem_path("bin/bundle")}: Err (Err)" }
+ let(:expected) { super() }
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "when the file uses the current ruby shebang" do
+ let(:shebang) { "#!#{Gem.ruby}" }
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "when Bundler.setup fails" do
+ before do
+ system_gems(%w[myrack-1.0.0 myrack-0.9.1], path: default_bundle_path)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack', '2'
+ G
+ ENV["BUNDLER_FORCE_TTY"] = "true"
+ end
+
+ let(:exit_code) { Bundler::GemNotFound.new.status_code }
+ let(:expected) { "" }
+ let(:expected_err) { <<~EOS.strip }
+ Could not find gem 'myrack (= 2)' in locally installed gems.
+
+ The source contains the following gems matching 'myrack':
+ * myrack-0.9.1
+ * myrack-1.0.0
+ Run `bundle install` to install missing gems.
+ EOS
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "when Bundler.setup fails and Gemfile is not the default" do
+ before do
+ gemfile "CustomGemfile", <<-G
+ source "https://gem.repo1"
+ gem 'myrack', '2'
+ G
+ ENV["BUNDLER_FORCE_TTY"] = "true"
+ ENV["BUNDLE_GEMFILE"] = "CustomGemfile"
+ ENV["BUNDLER_ORIG_BUNDLE_GEMFILE"] = nil
+ end
+
+ let(:exit_code) { Bundler::GemNotFound.new.status_code }
+ let(:expected) { "" }
+
+ it "prints proper suggestion" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to include("Run `bundle install --gemfile CustomGemfile` to install missing gems.")
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "when the executable exits non-zero via at_exit" do
+ let(:executable) { super() + "\n\nat_exit { $! ? raise($!) : exit(1) }" }
+ let(:exit_code) { 1 }
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "when disable_exec_load is set" do
+ let(:exec) { "EXEC: exec" }
+ let(:process) do
+ if Gem.win_platform?
+ "PROCESS: ruby"
+ else
+ "PROCESS: ruby #{path} arg1 arg2"
+ end
+ end
+
+ before do
+ bundle_config "disable_exec_load true"
+ end
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "regarding $0 and __FILE__" do
+ let(:executable) { super() + <<-'RUBY' }
+
+ puts "$0: #{$0.inspect}"
+ puts "__FILE__: #{__FILE__.inspect}"
+ RUBY
+
+ context "when the path is absolute" do
+ let(:expected) { super() + <<~EOS.chomp }
+
+ $0: #{path.to_s.inspect}
+ __FILE__: #{path.to_s.inspect}
+ EOS
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "when the path is relative" do
+ let(:path) { super().relative_path_from(bundled_app) }
+ let(:expected) { super() + <<~EOS.chomp }
+
+ $0: #{path.to_s.inspect}
+ __FILE__: #{path.to_s.inspect}
+ EOS
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "when the path is relative with a leading ./" do
+ let(:path) { Pathname.new("./#{super().relative_path_from(bundled_app)}") }
+ let(:expected) { super() + <<~EOS.chomp }
+
+ $0: #{path.to_s.inspect}
+ __FILE__: #{File.expand_path(path, bundled_app).inspect}
+ EOS
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+ end
+
+ context "signal handling" do
+ let(:test_signals) do
+ open3_reserved_signals = %w[CHLD CLD PIPE]
+ reserved_signals = %w[SEGV BUS ILL FPE ABRT IOT VTALRM KILL STOP EXIT]
+ bundler_signals = %w[INT]
+
+ Signal.list.keys - (bundler_signals + reserved_signals + open3_reserved_signals)
+ end
+
+ context "signals being trapped by bundler" do
+ let(:executable) { <<~RUBY }
+ #{shebang}
+ begin
+ Thread.new do
+ puts 'Started' # For process sync
+ STDOUT.flush
+ sleep 1 # ignore quality_spec
+ raise RuntimeError, "Didn't receive expected INT"
+ end.join
+ rescue Interrupt
+ puts "foo"
+ end
+ RUBY
+
+ it "receives the signal" do
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ bundle("exec #{path}") do |_, o, thr|
+ o.gets # Consumes 'Started' and ensures that thread has started
+ Process.kill("INT", thr.pid)
+ end
+
+ expect(out).to eq("foo")
+ end
+ end
+
+ context "signals not being trapped by bunder" do
+ let(:executable) { <<~RUBY }
+ #{shebang}
+
+ signals = #{test_signals.inspect}
+ result = signals.map do |sig|
+ Signal.trap(sig, "IGNORE")
+ end
+ puts result.select { |ret| ret == "IGNORE" }.count
+ RUBY
+
+ it "makes sure no unexpected signals are restored to DEFAULT" do
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ test_signals.each do |n|
+ Signal.trap(n, "IGNORE")
+ end
+
+ bundle("exec #{path}")
+
+ expect(out).to eq(test_signals.count.to_s)
+ end
+ end
+ end
+ end
+
+ context "nested bundle exec" do
+ context "when bundle in a local path" do
+ before do
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ bundle_config "path vendor/bundler"
+ bundle :install
+ end
+
+ it "correctly shells out" do
+ file = bundled_app("file_that_bundle_execs.rb")
+ create_file(file, <<-RUBY)
+ #!#{Gem.ruby}
+ puts `bundle exec echo foo`
+ RUBY
+ file.chmod(0o777)
+ bundle "exec #{file}", env: { "PATH" => path }
+ expect(out).to eq("foo")
+ end
+ end
+
+ context "when Kernel.require uses extra monkeypatches" do
+ before do
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ install_gemfile "source \"https://gem.repo1\""
+ end
+
+ it "does not undo the monkeypatches" do
+ karafka = bundled_app("bin/karafka")
+ create_file(karafka, <<~RUBY)
+ #!#{Gem.ruby}
+
+ module Kernel
+ module_function
+
+ alias_method :require_before_extra_monkeypatches, :require
+
+ def require(path)
+ puts "requiring \#{path} used the monkeypatch"
+
+ require_before_extra_monkeypatches(path)
+ end
+ end
+
+ Bundler.setup(:default)
+
+ require "foo"
+ RUBY
+ karafka.chmod(0o777)
+
+ foreman = bundled_app("bin/foreman")
+ create_file(foreman, <<~RUBY)
+ #!#{Gem.ruby}
+
+ puts `bundle exec bin/karafka`
+ RUBY
+ foreman.chmod(0o777)
+
+ bundle "exec #{foreman}"
+ expect(out).to eq("requiring foo used the monkeypatch")
+ end
+ end
+
+ context "when gemfile and path are configured", :ruby_repo do
+ before do
+ build_repo2 do
+ build_gem "rails", "6.1.0" do |s|
+ s.executables = "rails"
+ end
+ end
+
+ bundle_config "path vendor/bundle"
+ bundle_config "gemfile gemfiles/myrack_6_1.gemfile"
+
+ gemfile(bundled_app("gemfiles/myrack_6_1.gemfile"), <<~RUBY)
+ source "https://gem.repo2"
+
+ gem "rails", "6.1.0"
+ RUBY
+
+ # A Gemfile needs to be in the root to trick bundler's root resolution
+ gemfile "source 'https://gem.repo1'"
+
+ bundle "install", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
+ end
+
+ it "can still find gems after a nested subprocess" do
+ script = bundled_app("bin/myscript")
+
+ create_file(script, <<~RUBY)
+ #!#{Gem.ruby}
+
+ puts `bundle exec rails`
+ RUBY
+
+ script.chmod(0o777)
+
+ bundle "exec #{script}"
+
+ expect(err).to be_empty
+ expect(out).to eq("6.1.0")
+ end
+
+ it "can still find gems after a nested subprocess when using bundler (with a final r) executable" do
+ script = bundled_app("bin/myscript")
+
+ create_file(script, <<~RUBY)
+ #!#{Gem.ruby}
+
+ puts `bundler exec rails`
+ RUBY
+
+ script.chmod(0o777)
+
+ bundle "exec #{script}"
+
+ expect(err).to be_empty
+ expect(out).to eq("6.1.0")
+ end
+ end
+
+ context "with a system gem that shadows a default gem" do
+ let(:openssl_version) { "99.9.9" }
+
+ it "only leaves the default gem in the stdlib available" do
+ default_openssl_version = ruby "require 'openssl'; puts OpenSSL::VERSION"
+
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ install_gemfile "source \"https://gem.repo1\"" # must happen before installing the broken system gem
+
+ build_repo4 do
+ build_gem "openssl", openssl_version do |s|
+ s.write("lib/openssl.rb", <<-RUBY)
+ raise ArgumentError, "custom openssl should not be loaded"
+ RUBY
+ end
+ end
+
+ system_gems("openssl-#{openssl_version}", gem_repo: gem_repo4)
+
+ file = bundled_app("require_openssl.rb")
+ create_file(file, <<-RUBY)
+ #!/usr/bin/env ruby
+ require "openssl"
+ puts OpenSSL::VERSION
+ warn Gem.loaded_specs.values.map(&:full_name)
+ RUBY
+ file.chmod(0o777)
+
+ env = { "PATH" => path }
+ aggregate_failures do
+ expect(bundle("exec #{file}", env: env)).to eq(default_openssl_version)
+ expect(bundle("exec bundle exec #{file}", env: env)).to eq(default_openssl_version)
+ expect(bundle("exec ruby #{file}", env: env)).to eq(default_openssl_version)
+ expect(run(file.read, artifice: nil, env: env)).to eq(default_openssl_version)
+ end
+
+ skip "ruby_core has openssl and rubygems in the same folder, and this test needs rubygems require but default openssl not in a directly added entry in $LOAD_PATH" if ruby_core?
+ # sanity check that we get the newer, custom version without bundler
+ sys_exec "#{Gem.ruby} #{file}", env: env, raise_on_error: false
+ expect(err).to include("custom openssl should not be loaded")
+ end
+ end
+
+ context "with a git gem that includes extensions", :ruby_repo do
+ before do
+ build_git "simple_git_binary", &:add_c_extension
+ bundle_config "path .bundle"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "simple_git_binary", :git => '#{lib_path("simple_git_binary-1.0")}'
+ G
+ end
+
+ it "allows calling bundle install" do
+ bundle "exec bundle install"
+ end
+
+ it "allows calling bundle install after removing gem.build_complete" do
+ FileUtils.rm_r Dir[bundled_app(".bundle/**/gem.build_complete")]
+ bundle "exec #{Gem.ruby} -S bundle install"
+ end
+ end
+ end
+end
diff --git a/spec/bundler/commands/fund_spec.rb b/spec/bundler/commands/fund_spec.rb
new file mode 100644
index 0000000000..5883b8a63a
--- /dev/null
+++ b/spec/bundler/commands/fund_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle fund" do
+ before do
+ build_repo2 do
+ build_gem "has_funding_and_other_metadata" do |s|
+ s.metadata = {
+ "bug_tracker_uri" => "https://example.com/user/bestgemever/issues",
+ "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md",
+ "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1",
+ "homepage_uri" => "https://bestgemever.example.io",
+ "mailing_list_uri" => "https://groups.example.com/bestgemever",
+ "funding_uri" => "https://example.com/has_funding_and_other_metadata/funding",
+ "source_code_uri" => "https://example.com/user/bestgemever",
+ "wiki_uri" => "https://example.com/user/bestgemever/wiki",
+ }
+ end
+
+ build_gem "has_funding", "1.2.3" do |s|
+ s.metadata = {
+ "funding_uri" => "https://example.com/has_funding/funding",
+ }
+ end
+
+ build_gem "gem_with_dependent_funding", "1.0" do |s|
+ s.add_dependency "has_funding"
+ end
+ end
+ end
+
+ it "prints fund information for all gems in the bundle" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'has_funding_and_other_metadata'
+ gem 'has_funding'
+ gem 'myrack-obama'
+ G
+
+ bundle "fund"
+
+ expect(out).to include("* has_funding_and_other_metadata (1.0)\n Funding: https://example.com/has_funding_and_other_metadata/funding")
+ expect(out).to include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding")
+ expect(out).to_not include("myrack-obama")
+ end
+
+ it "does not consider fund information for gem dependencies" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'gem_with_dependent_funding'
+ G
+
+ bundle "fund"
+
+ expect(out).to_not include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding")
+ expect(out).to_not include("gem_with_dependent_funding")
+ end
+
+ it "does not consider fund information for uninstalled optional dependencies" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ group :whatever, optional: true do
+ gem 'has_funding_and_other_metadata'
+ end
+ gem 'has_funding'
+ gem 'myrack-obama'
+ G
+
+ bundle "fund"
+
+ expect(out).to include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding")
+ expect(out).to_not include("has_funding_and_other_metadata")
+ expect(out).to_not include("myrack-obama")
+ end
+
+ it "considers fund information for installed optional dependencies" do
+ bundle_config "with whatever"
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ group :whatever, optional: true do
+ gem 'has_funding_and_other_metadata'
+ end
+ gem 'has_funding'
+ gem 'myrack-obama'
+ G
+
+ bundle "fund"
+
+ expect(out).to include("* has_funding_and_other_metadata (1.0)\n Funding: https://example.com/has_funding_and_other_metadata/funding")
+ expect(out).to include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding")
+ expect(out).to_not include("myrack-obama")
+ end
+
+ it "prints message if none of the gems have fund information" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack-obama'
+ G
+
+ bundle "fund"
+
+ expect(out).to include("None of the installed gems you directly depend on are looking for funding.")
+ end
+
+ describe "with --group option" do
+ it "prints fund message for only specified group gems" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'has_funding_and_other_metadata', :group => :development
+ gem 'has_funding'
+ G
+
+ bundle "fund --group development"
+ expect(out).to include("* has_funding_and_other_metadata (1.0)\n Funding: https://example.com/has_funding_and_other_metadata/funding")
+ expect(out).to_not include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding")
+ end
+ end
+end
diff --git a/spec/bundler/commands/help_spec.rb b/spec/bundler/commands/help_spec.rb
new file mode 100644
index 0000000000..f9ad9fff14
--- /dev/null
+++ b/spec/bundler/commands/help_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle help" do
+ it "uses man when available" do
+ with_fake_man do
+ bundle "help gemfile"
+ end
+ expect(out).to eq(%(["#{man_dir}/gemfile.5"]))
+ end
+
+ it "prefixes bundle commands with bundle- when finding the man files" do
+ with_fake_man do
+ bundle "help install"
+ end
+ expect(out).to eq(%(["#{man_dir}/bundle-install.1"]))
+ end
+
+ it "prexifes bundle commands with bundle- and resolves aliases when finding the man files" do
+ with_fake_man do
+ bundle "help package"
+ end
+ expect(out).to eq(%(["#{man_dir}/bundle-cache.1"]))
+ end
+
+ it "simply outputs the human readable file when there is no man on the path" do
+ with_path_as("") do
+ bundle "help install"
+ end
+ expect(out).to match(/bundle-install/)
+ end
+
+ it "looks for a binary and executes it with --help option if it's named bundler-<task>" do
+ skip "Could not find command testtasks, probably because not a windows friendly executable" if Gem.win_platform?
+
+ File.open(tmp("bundler-testtasks"), "w", 0o755) do |f|
+ f.puts "#!/usr/bin/env ruby\nputs ARGV.join(' ')\n"
+ end
+
+ with_path_added(tmp) do
+ bundle "help testtasks"
+ end
+
+ expect(out).to eq("--help")
+ end
+
+ it "is called when the --help flag is used after the command" do
+ with_fake_man do
+ bundle "install --help"
+ end
+ expect(out).to eq(%(["#{man_dir}/bundle-install.1"]))
+ end
+
+ it "is called when the --help flag is used before the command" do
+ with_fake_man do
+ bundle "--help install"
+ end
+ expect(out).to eq(%(["#{man_dir}/bundle-install.1"]))
+ end
+
+ it "is called when the -h flag is used before the command" do
+ with_fake_man do
+ bundle "-h install"
+ end
+ expect(out).to eq(%(["#{man_dir}/bundle-install.1"]))
+ end
+
+ it "is called when the -h flag is used after the command" do
+ with_fake_man do
+ bundle "install -h"
+ end
+ expect(out).to eq(%(["#{man_dir}/bundle-install.1"]))
+ end
+
+ it "has helpful output when using --help flag for a non-existent command" do
+ with_fake_man do
+ bundle "instill -h", raise_on_error: false
+ end
+ expect(err).to include('Could not find command "instill".')
+ end
+
+ it "is called when only using the --help flag" do
+ with_fake_man do
+ bundle "--help"
+ end
+ expect(out).to eq(%(["#{man_dir}/bundle.1"]))
+
+ with_fake_man do
+ bundle "-h"
+ end
+ expect(out).to eq(%(["#{man_dir}/bundle.1"]))
+ end
+end
diff --git a/spec/bundler/commands/info_spec.rb b/spec/bundler/commands/info_spec.rb
new file mode 100644
index 0000000000..a26b1696fb
--- /dev/null
+++ b/spec/bundler/commands/info_spec.rb
@@ -0,0 +1,249 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle info" do
+ context "with a standard Gemfile" do
+ before do
+ build_repo2 do
+ build_gem "has_metadata" do |s|
+ s.metadata = {
+ "bug_tracker_uri" => "https://example.com/user/bestgemever/issues",
+ "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md",
+ "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1",
+ "homepage_uri" => "https://bestgemever.example.io",
+ "mailing_list_uri" => "https://groups.example.com/bestgemever",
+ "source_code_uri" => "https://example.com/user/bestgemever",
+ "wiki_uri" => "https://example.com/user/bestgemever/wiki",
+ }
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails"
+ gem "has_metadata"
+ gem "thin"
+ G
+ end
+
+ it "creates a Gemfile.lock when invoked with a gem name" do
+ FileUtils.rm(bundled_app_lock)
+
+ bundle "info rails"
+
+ expect(bundled_app_lock).to exist
+ end
+
+ it "prints information if gem exists in bundle" do
+ bundle "info rails"
+ expect(out).to include "* rails (2.3.2)
+\tSummary: This is just a fake gem for testing
+\tHomepage: http://example.com
+\tPath: #{default_bundle_path("gems", "rails-2.3.2")}"
+ end
+
+ it "prints path if gem exists in bundle" do
+ bundle "info rails --path"
+ expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s)
+ end
+
+ it "prints the path to the running bundler" do
+ bundle "info bundler --path"
+ expect(out).to eq(root.to_s)
+ end
+
+ it "prints gem version if exists in bundle" do
+ bundle "info rails --version"
+ expect(out).to eq("2.3.2")
+ end
+
+ it "doesn't claim that bundler is missing, even if using a custom path without bundler there" do
+ bundle_config "path vendor/bundle"
+ bundle "install"
+ bundle "info bundler"
+ expect(out).to include("\tPath: #{root}")
+ expect(err).not_to match(/The gem bundler is missing/i)
+ end
+
+ it "complains if gem not in bundle" do
+ bundle "info missing", raise_on_error: false
+ expect(err).to eq("Could not find gem 'missing'.")
+ end
+
+ it "warns if path does not exist on disk, but specification is there" do
+ FileUtils.rm_r(default_bundle_path("gems", "rails-2.3.2"))
+
+ bundle "info rails --path"
+
+ expect(err).to include("The gem rails is missing.")
+ expect(err).to include(default_bundle_path("gems", "rails-2.3.2").to_s)
+
+ bundle "info rail --path"
+ expect(err).to include("The gem rails is missing.")
+ expect(err).to include(default_bundle_path("gems", "rails-2.3.2").to_s)
+
+ bundle "info rails"
+ expect(err).to include("The gem rails is missing.")
+ expect(err).to include(default_bundle_path("gems", "rails-2.3.2").to_s)
+ end
+
+ context "given a default gem shipped in ruby", :ruby_repo do
+ it "prints information about the default gem" do
+ bundle "info json"
+ expect(out).to include("* json")
+ expect(out).to include("Default Gem: yes")
+ end
+ end
+
+ context "given a gem with metadata" do
+ it "prints the gem metadata" do
+ bundle "info has_metadata"
+ expect(out).to include "* has_metadata (1.0)
+\tSummary: This is just a fake gem for testing
+\tHomepage: http://example.com
+\tDocumentation: https://www.example.info/gems/bestgemever/0.0.1
+\tSource Code: https://example.com/user/bestgemever
+\tWiki: https://example.com/user/bestgemever/wiki
+\tChangelog: https://example.com/user/bestgemever/CHANGELOG.md
+\tBug Tracker: https://example.com/user/bestgemever/issues
+\tMailing List: https://groups.example.com/bestgemever
+\tPath: #{default_bundle_path("gems", "has_metadata-1.0")}"
+ end
+ end
+
+ context "when gem does not have homepage" do
+ before do
+ build_repo2 do
+ build_gem "rails", "2.3.2" do |s|
+ s.executables = "rails"
+ s.summary = "Just another test gem"
+ end
+ end
+ end
+
+ it "excludes the homepage field from the output" do
+ expect(out).to_not include("Homepage:")
+ end
+ end
+
+ context "when gem has a reverse dependency on any version" do
+ it "prints the details" do
+ bundle "info myrack"
+
+ expect(out).to include("Reverse Dependencies: \n\t\tthin (1.0) depends on myrack (>= 0)")
+ end
+ end
+
+ context "when gem has a reverse dependency on a specific version" do
+ it "prints the details" do
+ bundle "info actionpack"
+
+ expect(out).to include("Reverse Dependencies: \n\t\trails (2.3.2) depends on actionpack (= 2.3.2)")
+ end
+ end
+
+ context "when gem has no reverse dependencies" do
+ it "excludes the reverse dependencies field from the output" do
+ bundle "info rails"
+
+ expect(out).not_to include("Reverse Dependencies:")
+ end
+ end
+ end
+
+ context "with a git repo in the Gemfile" do
+ before :each do
+ @git = build_git "foo", "1.0"
+ end
+
+ it "prints out git info" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+ expect(the_bundle).to include_gems "foo 1.0"
+
+ bundle "info foo"
+ expect(out).to include("foo (1.0 #{@git.ref_for("main", 6)}")
+ end
+
+ it "prints out branch names other than main" do
+ update_git "foo", branch: "omg" do |s|
+ s.write "lib/foo.rb", "FOO = '1.0.omg'"
+ end
+ @revision = revision_for(lib_path("foo-1.0"))[0...6]
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "omg"
+ G
+ expect(the_bundle).to include_gems "foo 1.0.omg"
+
+ bundle "info foo"
+ expect(out).to include("foo (1.0 #{@git.ref_for("omg", 6)}")
+ end
+
+ it "doesn't print the branch when tied to a ref" do
+ sha = revision_for(lib_path("foo-1.0"))
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{sha}"
+ G
+
+ bundle "info foo"
+ expect(out).to include("foo (1.0 #{sha[0..6]})")
+ end
+
+ it "handles when a version is a '-' prerelease" do
+ @git = build_git("foo", "1.0.0-beta.1", path: lib_path("foo"))
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", "1.0.0-beta.1", :git => "#{lib_path("foo")}"
+ G
+ expect(the_bundle).to include_gems "foo 1.0.0.pre.beta.1"
+
+ bundle "info foo"
+ expect(out).to include("foo (1.0.0.pre.beta.1")
+ end
+ end
+
+ context "with a valid regexp for gem name" do
+ it "presents alternatives", :readline do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "myrack-obama"
+ G
+
+ bundle "info rac"
+ expect(out).to match(/\A1 : myrack\n2 : myrack-obama\n0 : - exit -(\n>|\z)/)
+ end
+ end
+
+ context "with an invalid regexp for gem name" do
+ it "does not find the gem" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ invalid_regexp = "[]"
+
+ bundle "info #{invalid_regexp}", raise_on_error: false
+ expect(err).to include("Could not find gem '#{invalid_regexp}'.")
+ end
+ end
+
+ context "with without configured" do
+ it "does not find the gem, but gives a helpful error" do
+ bundle_config "without test"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails", group: :test
+ G
+
+ bundle "info rails", raise_on_error: false
+ expect(err).to include("Could not find gem 'rails', because it's in the group 'test', configured to be ignored.")
+ end
+ end
+end
diff --git a/spec/bundler/commands/init_spec.rb b/spec/bundler/commands/init_spec.rb
new file mode 100644
index 0000000000..989d6fa812
--- /dev/null
+++ b/spec/bundler/commands/init_spec.rb
@@ -0,0 +1,207 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle init" do
+ it "generates a Gemfile" do
+ bundle :init
+ expect(out).to include("Writing new Gemfile")
+ expect(bundled_app_gemfile).to be_file
+ end
+
+ context "with a template with permission flags not matching current process umask" do
+ let(:template_file) do
+ gemfile = Bundler.preferred_gemfile_name
+ templates_dir.join(gemfile)
+ end
+
+ let(:target_dir) { bundled_app("init_permissions_test") }
+
+ around do |example|
+ old_chmod = File.stat(template_file).mode
+ FileUtils.chmod(old_chmod | 0o111, template_file) # chmod +x
+ example.run
+ FileUtils.chmod(old_chmod, template_file)
+ end
+
+ it "honours the current process umask when generating from a template" do
+ FileUtils.mkdir(target_dir)
+ bundle :init, dir: target_dir
+ generated_mode = File.stat(File.join(target_dir, "Gemfile")).mode & 0o111
+ expect(generated_mode).to be_zero
+ end
+ end
+
+ context "when a Gemfile already exists" do
+ before do
+ gemfile <<-G
+ gem "rails"
+ G
+ end
+
+ it "does not change existing Gemfiles" do
+ expect { bundle :init, raise_on_error: false }.not_to change { File.read(bundled_app_gemfile) }
+ end
+
+ it "notifies the user that an existing Gemfile already exists" do
+ bundle :init, raise_on_error: false
+ expect(err).to include("Gemfile already exists")
+ end
+ end
+
+ context "when a Gemfile exists in a parent directory" do
+ let(:subdir) { "child_dir" }
+
+ it "lets users generate a Gemfile in a child directory" do
+ bundle :init
+
+ FileUtils.mkdir bundled_app(subdir)
+
+ bundle :init, dir: bundled_app(subdir)
+
+ expect(out).to include("Writing new Gemfile")
+ expect(bundled_app("#{subdir}/Gemfile")).to be_file
+ end
+ end
+
+ context "when the dir is not writable by the current user" do
+ let(:subdir) { "child_dir" }
+
+ it "notifies the user that it cannot write to it" do
+ FileUtils.mkdir bundled_app(subdir)
+ # chmod a-w it
+ mode = File.stat(bundled_app(subdir)).mode ^ 0o222
+ FileUtils.chmod mode, bundled_app(subdir)
+
+ bundle :init, dir: bundled_app(subdir), raise_on_error: false
+
+ expect(err).to include("directory is not writable")
+ expect(Dir[bundled_app("#{subdir}/*")]).to be_empty
+ end
+ end
+
+ context "given --gemspec option" do
+ let(:spec_file) { tmp("test.gemspec") }
+
+ it "should generate from an existing gemspec" do
+ File.open(spec_file, "w") do |file|
+ file << <<-S
+ Gem::Specification.new do |s|
+ s.name = 'test'
+ s.add_dependency 'myrack', '= 1.0.1'
+ s.add_development_dependency 'rspec', '1.2'
+ end
+ S
+ end
+
+ bundle :init, gemspec: spec_file
+
+ gemfile = bundled_app_gemfile.read
+ expect(gemfile).to match(%r{source 'https://rubygems.org'})
+ expect(gemfile.scan(/gem "myrack", "= 1.0.1"/).size).to eq(1)
+ expect(gemfile.scan(/gem "rspec", "= 1.2"/).size).to eq(1)
+ expect(gemfile.scan(/group :development/).size).to eq(1)
+ end
+
+ context "when gemspec file is invalid" do
+ it "notifies the user that specification is invalid" do
+ File.open(spec_file, "w") do |file|
+ file << <<-S
+ Gem::Specification.new do |s|
+ s.name = 'test'
+ s.invalid_method_name
+ end
+ S
+ end
+
+ bundle :init, gemspec: spec_file, raise_on_error: false
+ expect(err).to include("There was an error while loading `test.gemspec`")
+ end
+ end
+ end
+
+ context "when init_gems_rb setting is enabled" do
+ before { bundle_config "init_gems_rb true" }
+
+ it "generates a gems.rb" do
+ bundle :init
+ expect(out).to include("Writing new gems.rb")
+ expect(bundled_app("gems.rb")).to be_file
+ end
+
+ context "when gems.rb already exists" do
+ before do
+ gemfile("gems.rb", <<-G)
+ gem "rails"
+ G
+ end
+
+ it "does not change existing Gemfiles" do
+ expect { bundle :init, raise_on_error: false }.not_to change { File.read(bundled_app("gems.rb")) }
+ end
+
+ it "notifies the user that an existing gems.rb already exists" do
+ bundle :init, raise_on_error: false
+ expect(err).to include("gems.rb already exists")
+ end
+ end
+
+ context "when a gems.rb file exists in a parent directory" do
+ let(:subdir) { "child_dir" }
+
+ it "lets users generate a Gemfile in a child directory" do
+ bundle :init
+
+ FileUtils.mkdir bundled_app(subdir)
+
+ bundle :init, dir: bundled_app(subdir)
+
+ expect(out).to include("Writing new gems.rb")
+ expect(bundled_app("#{subdir}/gems.rb")).to be_file
+ end
+ end
+
+ context "given --gemspec option" do
+ let(:spec_file) { tmp("test.gemspec") }
+
+ before do
+ File.open(spec_file, "w") do |file|
+ file << <<-S
+ Gem::Specification.new do |s|
+ s.name = 'test'
+ s.add_dependency 'myrack', '= 1.0.1'
+ s.add_development_dependency 'rspec', '1.2'
+ end
+ S
+ end
+ end
+
+ it "should generate from an existing gemspec" do
+ bundle :init, gemspec: spec_file
+
+ gemfile = bundled_app("gems.rb").read
+ expect(gemfile).to match(%r{source 'https://rubygems.org'})
+ expect(gemfile.scan(/gem "myrack", "= 1.0.1"/).size).to eq(1)
+ expect(gemfile.scan(/gem "rspec", "= 1.2"/).size).to eq(1)
+ expect(gemfile.scan(/group :development/).size).to eq(1)
+ end
+
+ it "prints message to user" do
+ bundle :init, gemspec: spec_file
+
+ expect(out).to include("Writing new gems.rb")
+ end
+ end
+ end
+
+ describe "using the --gemfile" do
+ it "should use the --gemfile value to name the gemfile" do
+ custom_gemfile_name = "NiceGemfileName"
+
+ bundle :init, gemfile: custom_gemfile_name
+
+ expect(out).to include("Writing new #{custom_gemfile_name}")
+ used_template = File.read("#{source_root}/lib/bundler/templates/Gemfile")
+ generated_gemfile = bundled_app(custom_gemfile_name).read
+ expect(generated_gemfile).to eq(used_template)
+ end
+ end
+end
diff --git a/spec/bundler/commands/install_spec.rb b/spec/bundler/commands/install_spec.rb
new file mode 100644
index 0000000000..3b24434dc7
--- /dev/null
+++ b/spec/bundler/commands/install_spec.rb
@@ -0,0 +1,2115 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with gem sources" do
+ describe "the simple case" do
+ it "prints output and returns if no dependencies are specified" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ bundle :install
+ expect(err).to match(/no dependencies/)
+ end
+
+ it "does not make a lockfile if the install fails" do
+ install_gemfile <<-G, raise_on_error: false
+ raise StandardError, "FAIL"
+ G
+
+ expect(err).to include('StandardError, "FAIL"')
+ expect(bundled_app_lock).not_to exist
+ end
+
+ it "creates a Gemfile.lock" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(bundled_app_lock).to exist
+ end
+
+ it "creates lockfile based on the lockfile method in Gemfile" do
+ install_gemfile <<-G
+ lockfile "OmgFile.lock"
+ source "https://gem.repo1"
+ gem "myrack", "1.0"
+ G
+
+ bundle "install"
+
+ expect(bundled_app("OmgFile.lock")).to exist
+ end
+
+ it "creates lockfile using BUNDLE_LOCKFILE instead of lockfile method" do
+ ENV["BUNDLE_LOCKFILE"] = "ReallyOmgFile.lock"
+ install_gemfile <<-G
+ lockfile "OmgFile.lock"
+ source "https://gem.repo1"
+ gem "myrack", "1.0"
+ G
+
+ expect(bundled_app("ReallyOmgFile.lock")).to exist
+ expect(bundled_app("OmgFile.lock")).not_to exist
+ ensure
+ ENV.delete("BUNDLE_LOCKFILE")
+ end
+
+ it "creates lockfile based on --lockfile option is given" do
+ gemfile bundled_app("OmgFile"), <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0"
+ G
+
+ bundle "install --gemfile OmgFile --lockfile ReallyOmgFile.lock"
+
+ expect(bundled_app("ReallyOmgFile.lock")).to exist
+ end
+
+ it "does not make a lockfile if lockfile false is used in Gemfile" do
+ install_gemfile <<-G
+ lockfile false
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ expect(bundled_app_lock).not_to exist
+ end
+
+ it "does not create ./.bundle by default" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(bundled_app(".bundle")).not_to exist
+ end
+
+ it "will create a ./.bundle by default", bundler: "5" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(bundled_app(".bundle")).to exist
+ end
+
+ it "does not create ./.bundle by default when installing to system gems" do
+ install_gemfile <<-G, env: { "BUNDLE_PATH__SYSTEM" => "true" }
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(bundled_app(".bundle")).not_to exist
+ end
+
+ it "creates lockfiles based on the Gemfile name" do
+ gemfile bundled_app("OmgFile"), <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0"
+ G
+
+ bundle "install --gemfile OmgFile"
+
+ expect(bundled_app("OmgFile.lock")).to exist
+ end
+
+ it "doesn't create a lockfile if --no-lock option is given" do
+ gemfile bundled_app("OmgFile"), <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0"
+ G
+
+ bundle "install --gemfile OmgFile --no-lock"
+
+ expect(bundled_app("OmgFile.lock")).not_to exist
+ end
+
+ it "doesn't create a lockfile if --no-lock and --lockfile options are given" do
+ gemfile bundled_app("OmgFile"), <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0"
+ G
+
+ bundle "install --gemfile OmgFile --no-lock --lockfile ReallyOmgFile.lock"
+
+ expect(bundled_app("OmgFile.lock")).not_to exist
+ expect(bundled_app("ReallyOmgFile.lock")).not_to exist
+ end
+
+ it "doesn't delete the lockfile if one already exists" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ lockfile = File.read(bundled_app_lock)
+
+ install_gemfile <<-G, raise_on_error: false
+ raise StandardError, "FAIL"
+ G
+
+ expect(File.read(bundled_app_lock)).to eq(lockfile)
+ end
+
+ it "does not touch the lockfile if nothing changed" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect { run "1" }.not_to change { File.mtime(bundled_app_lock) }
+ end
+
+ it "fetches gems" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ expect(default_bundle_path("gems/myrack-1.0.0")).to exist
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "auto-heals missing gems" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ FileUtils.rm_r(default_bundle_path("gems/myrack-1.0.0"))
+
+ bundle "install --verbose"
+
+ expect(out).to include("Installing myrack 1.0.0")
+ expect(default_bundle_path("gems/myrack-1.0.0")).to exist
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "does not state that it's constantly reinstalling empty gems" do
+ build_repo4 do
+ build_gem "empty", "1.0.0", no_default: true
+ end
+
+ install_gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "empty"
+ G
+ gem_dir = default_bundle_path("gems/empty-1.0.0")
+ expect(gem_dir).to be_empty
+
+ bundle "install --verbose"
+ expect(out).not_to include("Installing empty")
+ end
+
+ it "fetches gems when multiple versions are specified" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack', "> 0.9", "< 1.0"
+ G
+
+ expect(default_bundle_path("gems/myrack-0.9.1")).to exist
+ expect(the_bundle).to include_gems("myrack 0.9.1")
+ end
+
+ it "fetches gems when multiple versions are specified take 2" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack', "< 1.0", "> 0.9"
+ G
+
+ expect(default_bundle_path("gems/myrack-0.9.1")).to exist
+ expect(the_bundle).to include_gems("myrack 0.9.1")
+ end
+
+ it "raises an appropriate error when gems are specified using symbols" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem :myrack
+ G
+ expect(exitstatus).to eq(4)
+ end
+
+ it "pulls in dependencies" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ expect(the_bundle).to include_gems "actionpack 2.3.2", "rails 2.3.2"
+ end
+
+ it "does the right version" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ end
+
+ it "does not install the development dependency" do
+ build_repo2 do
+ build_gem "with_development_dependency" do |s|
+ s.add_development_dependency "activesupport", "= 2.3.5"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_development_dependency"
+ G
+
+ expect(the_bundle).to include_gems("with_development_dependency 1.0.0").
+ and not_include_gems("activesupport 2.3.5")
+ end
+
+ it "resolves correctly" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activemerchant"
+ gem "rails"
+ G
+
+ expect(the_bundle).to include_gems "activemerchant 1.0", "activesupport 2.3.2", "actionpack 2.3.2"
+ end
+
+ it "activates gem correctly according to the resolved gems" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport", "2.3.5"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activemerchant"
+ gem "rails"
+ G
+
+ expect(the_bundle).to include_gems "activemerchant 1.0", "activesupport 2.3.2", "actionpack 2.3.2"
+ end
+
+ it "does not reinstall any gem that is already available locally" do
+ system_gems "activesupport-2.3.2", path: default_bundle_path
+
+ build_repo2 do
+ build_gem "activesupport", "2.3.2" do |s|
+ s.write "lib/activesupport.rb", "ACTIVESUPPORT = 'fail'"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activerecord", "2.3.2"
+ G
+
+ expect(the_bundle).to include_gems "activesupport 2.3.2"
+ end
+
+ it "works when the gemfile specifies gems that only exist in the system" do
+ build_gem "foo", to_bundle: true
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "foo"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "foo 1.0.0"
+ end
+
+ it "prioritizes local gems over remote gems" do
+ build_gem "myrack", "9.0.0", to_bundle: true
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 9.0.0"
+ end
+
+ it "loads env plugins" do
+ plugin_msg = "hello from an env plugin!"
+ create_file "plugins/rubygems_plugin.rb", "puts '#{plugin_msg}'"
+ install_gemfile <<-G, env: { "RUBYLIB" => rubylib.unshift(bundled_app("plugins").to_s).join(File::PATH_SEPARATOR) }
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(stdboth).to include(plugin_msg)
+ end
+
+ describe "with a gem that installs multiple platforms" do
+ it "installs gems for the local platform as first choice" do
+ simulate_platform "x86-darwin-100" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems("platform_specific 1.0 x86-darwin-100")
+ end
+ end
+
+ it "falls back on plain ruby" do
+ simulate_platform "foo-bar-baz" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems("platform_specific 1.0 ruby")
+ end
+ end
+
+ it "installs gems for java" do
+ simulate_platform "java" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems("platform_specific 1.0 java")
+ end
+ end
+
+ it "installs gems for windows" do
+ simulate_platform "x86-mswin32" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems("platform_specific 1.0 x86-mswin32")
+ end
+ end
+
+ it "installs gems for aarch64-mingw-ucrt" do
+ simulate_platform "aarch64-mingw-ucrt" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+ end
+
+ expect(out).to include("Installing platform_specific 1.0 (aarch64-mingw-ucrt)")
+ end
+ end
+
+ it "gives useful errors if no global sources are set, and gems not installed locally, with and without a lockfile" do
+ install_gemfile <<-G, raise_on_error: false
+ gem "myrack"
+ G
+
+ expect(err).to eq("Could not find gem 'myrack' in locally installed gems.")
+
+ lockfile <<~L
+ GEM
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install", raise_on_error: false
+
+ expect(err).to include(
+ "Because your Gemfile specifies no global remote source, your bundle is locked to " \
+ "myrack (1.0.0) from locally installed gems. However, myrack (1.0.0) is not installed. " \
+ "You'll need to either add a global remote source to your Gemfile or make sure myrack (1.0.0) " \
+ "is available locally before rerunning Bundler."
+ )
+ end
+
+ it "creates a Gemfile.lock on a blank Gemfile" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ expect(File.exist?(bundled_app_lock)).to eq(true)
+ end
+
+ it "throws a warning if a gem is added twice in Gemfile without version requirements" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ gem "myrack"
+ G
+
+ expect(err).to include("Your Gemfile lists the gem myrack (>= 0) more than once.")
+ expect(err).to include("Remove any duplicate entries and specify the gem only once.")
+ expect(err).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.")
+ end
+
+ it "throws a warning if a gem is added twice in Gemfile with same versions" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack", "1.0"
+ gem "myrack", "1.0"
+ G
+
+ expect(err).to include("Your Gemfile lists the gem myrack (= 1.0) more than once.")
+ expect(err).to include("Remove any duplicate entries and specify the gem only once.")
+ expect(err).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.")
+ end
+
+ it "throws a warning if a gem is added twice under different platforms and does not crash when using the generated lockfile" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack", :platform => :jruby
+ gem "myrack"
+ G
+
+ bundle "install"
+
+ expect(err).to include("Your Gemfile lists the gem myrack (>= 0) more than once.")
+ expect(err).to include("Remove any duplicate entries and specify the gem only once.")
+ expect(err).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.")
+ end
+
+ it "does not throw a warning if a gem is added once in Gemfile and also inside a gemspec as a development dependency" do
+ build_lib "my-gem", path: bundled_app do |s|
+ s.add_development_dependency "my-private-gem"
+ end
+
+ build_repo2 do
+ build_gem "my-private-gem"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ gemspec
+
+ gem "my-private-gem", :group => :development
+ G
+
+ bundle :install
+
+ expect(err).to be_empty
+ expect(the_bundle).to include_gems("my-private-gem 1.0")
+ end
+
+ it "does not warn if a gem is added once in Gemfile and also inside a gemspec as a development dependency, with compatible requirements" do
+ build_lib "my-gem", path: bundled_app do |s|
+ s.add_development_dependency "rubocop", "~> 1.36.0"
+ end
+
+ build_repo4 do
+ build_gem "rubocop", "1.36.0"
+ build_gem "rubocop", "1.37.1"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gemspec
+
+ gem "rubocop", group: :development
+ G
+
+ bundle :install
+
+ expect(err).to be_empty
+
+ expect(the_bundle).to include_gems("rubocop 1.36.0")
+ end
+
+ it "raises an error if a gem is added once in Gemfile and also inside a gemspec as a development dependency, with incompatible requirements" do
+ build_lib "my-gem", path: bundled_app do |s|
+ s.add_development_dependency "rubocop", "~> 1.36.0"
+ end
+
+ build_repo4 do
+ build_gem "rubocop", "1.36.0"
+ build_gem "rubocop", "1.37.1"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gemspec
+
+ gem "rubocop", "~> 1.37.0", group: :development
+ G
+
+ bundle :install, raise_on_error: false
+
+ expect(err).to include("The rubocop dependency has conflicting requirements in Gemfile (~> 1.37.0) and gemspec (~> 1.36.0)")
+ end
+
+ it "includes the gem without warning if two gemspecs add it with the same requirement" do
+ gem1 = tmp("my-gem-1")
+ gem2 = tmp("my-gem-2")
+
+ build_lib "my-gem", path: gem1 do |s|
+ s.add_development_dependency "rubocop", "~> 1.36.0"
+ end
+
+ build_lib "my-gem-2", path: gem2 do |s|
+ s.add_development_dependency "rubocop", "~> 1.36.0"
+ end
+
+ build_repo4 do
+ build_gem "rubocop", "1.36.0"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gemspec path: "#{gem1}"
+ gemspec path: "#{gem2}"
+ G
+
+ bundle :install
+
+ expect(err).to be_empty
+ expect(the_bundle).to include_gems("rubocop 1.36.0")
+ end
+
+ it "includes the gem without warning if two gemspecs add it with compatible requirements" do
+ gem1 = tmp("my-gem-1")
+ gem2 = tmp("my-gem-2")
+
+ build_lib "my-gem", path: gem1 do |s|
+ s.add_development_dependency "rubocop", "~> 1.0"
+ end
+
+ build_lib "my-gem-2", path: gem2 do |s|
+ s.add_development_dependency "rubocop", "~> 1.36.0"
+ end
+
+ build_repo4 do
+ build_gem "rubocop", "1.36.0"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gemspec path: "#{gem1}"
+ gemspec path: "#{gem2}"
+ G
+
+ bundle :install
+
+ expect(err).to be_empty
+ expect(the_bundle).to include_gems("rubocop 1.36.0")
+ end
+
+ it "errors out if two gemspecs add it with incompatible requirements" do
+ gem1 = tmp("my-gem-1")
+ gem2 = tmp("my-gem-2")
+
+ build_lib "my-gem", path: gem1 do |s|
+ s.add_development_dependency "rubocop", "~> 2.0"
+ end
+
+ build_lib "my-gem-2", path: gem2 do |s|
+ s.add_development_dependency "rubocop", "~> 1.36.0"
+ end
+
+ build_repo4 do
+ build_gem "rubocop", "1.36.0"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gemspec path: "#{gem1}"
+ gemspec path: "#{gem2}"
+ G
+
+ bundle :install, raise_on_error: false
+
+ expect(err).to include("Two gemspec development dependencies have conflicting requirements on the same gem: rubocop (~> 1.36.0) and rubocop (~> 2.0). Bundler cannot continue.")
+ end
+
+ it "errors out if a gem is specified in a gemspec and in the Gemfile" do
+ gem = tmp("my-gem-1")
+
+ build_lib "rubocop", path: gem do |s|
+ s.add_development_dependency "rubocop", "~> 1.0"
+ end
+
+ build_repo4 do
+ build_gem "rubocop"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "rubocop", :path => "#{gem}"
+ gemspec path: "#{gem}"
+ G
+
+ bundle :install, raise_on_error: false
+
+ expect(err).to include("There was an error parsing `Gemfile`: You cannot specify the same gem twice coming from different sources.")
+ expect(err).to include("You specified that rubocop (>= 0) should come from source at `#{gem}` and gemspec at `#{gem}`")
+ end
+
+ it "does not warn if a gem is added once in Gemfile and also inside a gemspec as a development dependency, with same requirements, and different sources" do
+ build_lib "my-gem", path: bundled_app do |s|
+ s.add_development_dependency "activesupport"
+ end
+
+ build_repo4 do
+ build_gem "activesupport"
+ end
+
+ build_git "activesupport", "1.0", path: lib_path("activesupport")
+
+ install_gemfile <<~G
+ source "https://gem.repo4"
+
+ gemspec
+
+ gem "activesupport", :git => "#{lib_path("activesupport")}"
+ G
+
+ expect(err).to be_empty
+ expect(the_bundle).to include_gems "activesupport 1.0", source: "git@#{lib_path("activesupport")}"
+
+ # if the Gemfile dependency is specified first
+ install_gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "activesupport", :git => "#{lib_path("activesupport")}"
+
+ gemspec
+ G
+
+ expect(err).to be_empty
+ expect(the_bundle).to include_gems "activesupport 1.0", source: "git@#{lib_path("activesupport")}"
+ end
+
+ it "considers both dependencies for resolution if a gem is added once in Gemfile and also inside a local gemspec as a runtime dependency, with different requirements" do
+ build_lib "my-gem", path: bundled_app do |s|
+ s.add_dependency "rubocop", "~> 1.36.0"
+ end
+
+ build_repo4 do
+ build_gem "rubocop", "1.36.0"
+ build_gem "rubocop", "1.37.1"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gemspec
+
+ gem "rubocop"
+ G
+
+ bundle :install
+
+ expect(err).to be_empty
+ expect(the_bundle).to include_gems("rubocop 1.36.0")
+ end
+
+ it "throws an error if a gem is added twice in Gemfile when version of one dependency is not specified" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "myrack"
+ gem "myrack", "1.0"
+ G
+
+ expect(err).to include("You cannot specify the same gem twice with different version requirements")
+ expect(err).to include("You specified: myrack (>= 0) and myrack (= 1.0).")
+ end
+
+ it "throws an error if a gem is added twice in Gemfile when different versions of both dependencies are specified" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "myrack", "1.0"
+ gem "myrack", "1.1"
+ G
+
+ expect(err).to include("You cannot specify the same gem twice with different version requirements")
+ expect(err).to include("You specified: myrack (= 1.0) and myrack (= 1.1).")
+ end
+
+ it "gracefully handles error when rubygems server is unavailable" do
+ install_gemfile <<-G, artifice: nil, raise_on_error: false
+ source "https://gem.repo1"
+ source "http://0.0.0.0:9384" do
+ gem 'foo'
+ end
+ G
+
+ expect(err).to eq("Could not reach host 0.0.0.0:9384. Check your network connection and try again.")
+ expect(err).not_to include("file://")
+ end
+
+ it "fails gracefully when downloading an invalid specification from the full index" do
+ build_repo2(build_compact_index: false) do
+ build_gem "ajp-rails", "0.0.0", gemspec: false, skip_validation: true do |s|
+ invalid_deps = [["ruby-ajp", ">= 0.2.0"], ["rails", ">= 0.14"]]
+ s.
+ instance_variable_get(:@spec).
+ instance_variable_set(:@dependencies, invalid_deps)
+ end
+
+ build_gem "ruby-ajp", "1.0.0"
+ end
+
+ install_gemfile <<-G, full_index: true, raise_on_error: false
+ source "https://gem.repo2"
+
+ gem "ajp-rails", "0.0.0"
+ G
+
+ expect(stdboth).not_to match(/Error Report/i)
+ expect(err).to include("An error occurred while installing ajp-rails (0.0.0), and Bundler cannot continue.").
+ and include("Bundler::APIResponseInvalidDependenciesError")
+ end
+
+ it "doesn't blow up when the local .bundle/config is empty" do
+ FileUtils.mkdir_p(bundled_app(".bundle"))
+ FileUtils.touch(bundled_app(".bundle/config"))
+
+ install_gemfile(<<-G)
+ source "https://gem.repo1"
+
+ gem 'foo'
+ G
+ end
+
+ it "doesn't blow up when the global .bundle/config is empty" do
+ FileUtils.mkdir_p("#{Bundler.rubygems.user_home}/.bundle")
+ FileUtils.touch("#{Bundler.rubygems.user_home}/.bundle/config")
+
+ install_gemfile(<<-G)
+ source "https://gem.repo1"
+
+ gem 'foo'
+ G
+ end
+ end
+
+ describe "Ruby version in Gemfile.lock" do
+ context "and using an unsupported Ruby version" do
+ it "prints an error" do
+ install_gemfile <<-G, raise_on_error: false
+ ruby '~> 1.2'
+ source "https://gem.repo1"
+ G
+ expect(err).to include("Your Ruby version is #{Gem.ruby_version}, but your Gemfile specified ~> 1.2")
+ end
+ end
+
+ context "and using a supported Ruby version" do
+ before do
+ install_gemfile <<-G
+ ruby '~> #{Gem.ruby_version}'
+ source "https://gem.repo1"
+ G
+ end
+
+ it "writes current Ruby version to Gemfile.lock" do
+ checksums = checksums_section_when_enabled
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ #{checksums}
+ RUBY VERSION
+ #{Bundler::RubyVersion.system}
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "updates Gemfile.lock with updated yet still compatible ruby version" do
+ install_gemfile <<-G
+ ruby '~> #{current_ruby_minor}'
+ source "https://gem.repo1"
+ G
+
+ checksums = checksums_section_when_enabled
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ #{checksums}
+ RUBY VERSION
+ #{Bundler::RubyVersion.system}
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "does not crash when unlocking" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby '>= 2.1.0'
+ G
+
+ bundle "update"
+
+ expect(err).not_to include("Could not find gem 'Ruby")
+ end
+ end
+ end
+
+ describe "when Bundler root contains regex chars" do
+ it "doesn't blow up when using the `gem` DSL" do
+ root_dir = tmp("foo[]bar")
+
+ FileUtils.mkdir_p(root_dir)
+
+ build_lib "foo"
+ gemfile = <<-G
+ source "https://gem.repo1"
+ gem 'foo', :path => "#{lib_path("foo-1.0")}"
+ G
+ File.open("#{root_dir}/Gemfile", "w") do |file|
+ file.puts gemfile
+ end
+
+ bundle :install, dir: root_dir
+ end
+
+ it "doesn't blow up when using the `gemspec` DSL" do
+ root_dir = tmp("foo[]bar")
+
+ FileUtils.mkdir_p(root_dir)
+
+ build_lib "foo", path: root_dir
+ gemfile = <<-G
+ source "https://gem.repo1"
+ gemspec
+ G
+ File.open("#{root_dir}/Gemfile", "w") do |file|
+ file.puts gemfile
+ end
+
+ bundle :install, dir: root_dir
+ end
+ end
+
+ describe "when requesting a quiet install via --quiet" do
+ it "should be quiet if there are no warnings" do
+ bundle_config "force_ruby_platform true"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle :install, quiet: true
+ expect(out).to be_empty
+ expect(err).to be_empty
+ end
+
+ it "should still display warnings and errors" do
+ bundle_config "force_ruby_platform true"
+
+ create_file("install_with_warning.rb", <<~RUBY)
+ require "#{lib_dir}/bundler"
+ require "#{lib_dir}/bundler/cli"
+ require "#{lib_dir}/bundler/cli/install"
+
+ module RunWithWarning
+ def run
+ super
+ rescue
+ Bundler.ui.warn "BOOOOO"
+ raise
+ end
+ end
+
+ Bundler::CLI::Install.prepend(RunWithWarning)
+ RUBY
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'non-existing-gem'
+ G
+
+ bundle :install, quiet: true, raise_on_error: false, env: { "RUBYOPT" => "-r#{bundled_app("install_with_warning.rb")}" }
+ expect(out).to be_empty
+ expect(err).to include("Could not find gem 'non-existing-gem'")
+ expect(err).to include("BOOOOO")
+ end
+ end
+
+ describe "when bundle path does not have cd permission", :permissions do
+ let(:bundle_path) { bundled_app("vendor") }
+
+ before do
+ FileUtils.mkdir_p(bundle_path)
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ FileUtils.chmod(0o500, bundle_path)
+
+ bundle_config "path vendor"
+ bundle :install, raise_on_error: false
+ expect(err).to include(bundle_path.to_s)
+ expect(err).to include("grant executable permissions")
+ end
+ end
+
+ describe "when bundle gems path does not have cd permission", :permissions do
+ let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") }
+
+ before do
+ FileUtils.mkdir_p(gems_path)
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ FileUtils.chmod("-x", gems_path)
+ bundle_config "path vendor"
+
+ begin
+ bundle :install, raise_on_error: false
+ ensure
+ FileUtils.chmod("+x", gems_path)
+ end
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+
+ expect(err).to include(
+ "There was an error while trying to create `#{gems_path.join("myrack-1.0.0")}`. " \
+ "It is likely that you need to grant executable permissions for all parent directories and write permissions for `#{gems_path}`."
+ )
+ end
+ end
+
+ describe "when there's an empty install folder (like with default gems) without cd permissions", :permissions do
+ let(:full_gem_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems/myrack-1.0.0") }
+
+ before do
+ FileUtils.mkdir_p(full_gem_path)
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ FileUtils.chmod("-x", full_gem_path)
+ bundle_config "path vendor"
+
+ begin
+ bundle :install, raise_on_error: false
+ ensure
+ FileUtils.chmod("+x", full_gem_path)
+ end
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+
+ expect(err).to include(
+ "There was an error while trying to write to `#{full_gem_path}`. " \
+ "It is likely that you need to grant write permissions for that path."
+ )
+ end
+ end
+
+ describe "when bundle bin dir does not have cd permission", :permissions do
+ let(:bin_dir) { bundled_app("vendor/#{Bundler.ruby_scope}/bin") }
+
+ before do
+ FileUtils.mkdir_p(bin_dir)
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ FileUtils.chmod("-x", bin_dir)
+ bundle_config "path vendor"
+
+ begin
+ bundle :install, raise_on_error: false
+ ensure
+ FileUtils.chmod("+x", bin_dir)
+ end
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+
+ expect(err).to include(
+ "There was an error while trying to write to `#{bin_dir}`. " \
+ "It is likely that you need to grant write permissions for that path."
+ )
+ end
+ end
+
+ describe "when bundle bin dir does not have write access", :permissions do
+ let(:bin_dir) { bundled_app("vendor/#{Bundler.ruby_scope}/bin") }
+
+ before do
+ FileUtils.mkdir_p(bin_dir)
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ FileUtils.chmod("-w", bin_dir)
+ bundle_config "path vendor"
+
+ begin
+ bundle :install, raise_on_error: false
+ ensure
+ FileUtils.chmod("+w", bin_dir)
+ end
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+
+ expect(err).to include(
+ "There was an error while trying to write to `#{bin_dir}`. " \
+ "It is likely that you need to grant write permissions for that path."
+ )
+ end
+ end
+
+ describe "when bundle extensions path does not have write access", :permissions do
+ let(:extensions_path) { bundled_app("vendor/#{Bundler.ruby_scope}/extensions/#{Gem::Platform.local}/#{Gem.extension_api_version}") }
+
+ before do
+ FileUtils.mkdir_p(extensions_path)
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'simple_binary'
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ FileUtils.chmod("-x", extensions_path)
+ bundle_config "path vendor"
+
+ begin
+ bundle :install, raise_on_error: false
+ ensure
+ FileUtils.chmod("+x", extensions_path)
+ end
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+
+ expect(err).to include(
+ "There was an error while trying to create `#{extensions_path.join("simple_binary-1.0")}`. " \
+ "It is likely that you need to grant executable permissions for all parent directories and write permissions for `#{extensions_path}`."
+ )
+ end
+ end
+
+ describe "when the path of a specific gem does not have cd permission", :permissions do
+ let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") }
+ let(:foo_path) { gems_path.join("foo-1.0.0") }
+
+ before do
+ build_repo4 do
+ build_gem "foo", "1.0.0" do |s|
+ s.write "CHANGELOG.md", "foo"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo'
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ bundle_config "path vendor"
+ bundle :install
+ expect(out).to include("Bundle complete!")
+ expect(err).to be_empty
+
+ FileUtils.chmod("-x", foo_path)
+
+ begin
+ bundle "install --force", raise_on_error: false
+ ensure
+ FileUtils.chmod("+x", foo_path)
+ end
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+ expect(err).to include("Could not delete previous installation of `#{foo_path}`.")
+ expect(err).to include("The underlying error was Errno::EACCES")
+ end
+ end
+
+ describe "when gem home does not have the writable bit set, yet it's still writable", :permissions do
+ let(:gem_home) { bundled_app("vendor/#{Bundler.ruby_scope}") }
+
+ before do
+ build_repo4 do
+ build_gem "foo", "1.0.0" do |s|
+ s.write "CHANGELOG.md", "foo"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo'
+ G
+ end
+
+ it "should still work" do
+ bundle_config "path vendor"
+ bundle :install
+ expect(out).to include("Bundle complete!")
+ expect(err).to be_empty
+
+ FileUtils.chmod("-w", gem_home)
+
+ begin
+ bundle "install --force"
+ ensure
+ FileUtils.chmod("+w", gem_home)
+ end
+
+ expect(out).to include("Bundle complete!")
+ expect(err).to be_empty
+ end
+ end
+
+ describe "when gems path is world writable (no sticky bit set)", :permissions do
+ let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") }
+
+ before do
+ build_repo4 do
+ build_gem "foo", "1.0.0" do |s|
+ s.write "CHANGELOG.md", "foo"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo'
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ bundle_config "path vendor"
+ bundle :install
+ expect(out).to include("Bundle complete!")
+ expect(err).to be_empty
+
+ FileUtils.chmod(0o777, gems_path)
+
+ bundle "install --force", raise_on_error: false
+
+ expect(err).to include("Bundler cannot reinstall foo-1.0.0 because there's a previous installation of it at #{gems_path}/foo-1.0.0 that is unsafe to remove")
+ end
+ end
+
+ describe "when gems path is world writable (no sticky bit set), but previous install is just an empty dir (like it happens with default gems)", :permissions do
+ let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") }
+ let(:full_path) { gems_path.join("foo-1.0.0") }
+
+ before do
+ build_repo4 do
+ build_gem "foo", "1.0.0" do |s|
+ s.write "CHANGELOG.md", "foo"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo'
+ G
+ end
+
+ it "does not try to remove the directory and thus don't abort with an error about unsafe directory removal" do
+ bundle_config "path vendor"
+
+ FileUtils.mkdir_p(gems_path)
+ FileUtils.chmod(0o777, gems_path)
+ Dir.mkdir(full_path)
+
+ bundle "install"
+ end
+ end
+
+ describe "when bundle cache path does not have write access", :permissions do
+ let(:cache_path) { bundled_app("vendor/#{Bundler.ruby_scope}/cache") }
+
+ before do
+ FileUtils.mkdir_p(cache_path)
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ FileUtils.chmod(0o500, cache_path)
+
+ bundle_config "path vendor"
+ bundle :install, raise_on_error: false
+ expect(err).to include(cache_path.to_s)
+ expect(err).to include("grant write permissions")
+ end
+ end
+
+ describe "when gemspecs are unreadable", :permissions do
+ let(:gemspec_path) { vendored_gems("specifications/myrack-1.0.0.gemspec") }
+
+ before do
+ gemfile <<~G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+ bundle_config "path vendor/bundle"
+ bundle :install
+ expect(out).to include("Bundle complete!")
+ expect(err).to be_empty
+
+ FileUtils.chmod("-r", gemspec_path)
+ end
+
+ it "shows a good error" do
+ bundle :install, raise_on_error: false
+ expect(err).to include(gemspec_path.to_s)
+ expect(err).to include("grant read permissions")
+ end
+ end
+
+ describe "when using umask 002 and setgid bit", :permissions do
+ let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") }
+ let(:foo_path) { gems_path.join("foo-1.0.0") }
+
+ before do
+ build_repo4 do
+ build_gem "foo", "1.0.0" do |s|
+ s.write "CHANGELOG.md", "foo"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo'
+ G
+
+ FileUtils.mkdir_p(gems_path)
+ FileUtils.chmod("g+s", gems_path)
+ end
+
+ it "should create the gem directory with proper permissions" do
+ with_umask(0o002) do
+ bundle_config "path vendor"
+ bundle :install
+ expect(out).to include("Bundle complete!")
+ expect(err).to be_empty
+ # Linux's SysV-derived mkdir(2) propagates the set-group-ID bit
+ # from the parent directory to newly created subdirectories. BSD
+ # (including macOS) inherits the parent's group via mkdir(2) but
+ # does not copy the set-group-ID bit itself, so the expected
+ # mode differs by platform.
+ expected = RUBY_PLATFORM.include?("darwin") ? 0o0775 : 0o2775
+ expect(File.stat(foo_path).mode & 0o7777).to eq(expected)
+ end
+ end
+ end
+
+ describe "parallel make" do
+ before do
+ unless Gem::Installer.private_method_defined?(:build_jobs)
+ skip "This example is runnable when RubyGems::Installer implements `build_jobs`"
+ end
+
+ @old_makeflags = ENV["MAKEFLAGS"]
+ @gemspec = nil
+
+ extconf_code = <<~CODE
+ require "mkmf"
+ create_makefile("foo")
+ CODE
+
+ build_repo4 do
+ build_gem "mypsych", "4.0.6" do |s|
+ @gemspec = s
+ extension = "ext/mypsych/extconf.rb"
+ s.extensions = extension
+
+ s.write(extension, extconf_code)
+ end
+ end
+ end
+
+ after do
+ if @old_makeflags
+ ENV["MAKEFLAGS"] = @old_makeflags
+ else
+ ENV.delete("MAKEFLAGS")
+ end
+ end
+
+ it "doesn't pass down -j to make when MAKEFLAGS is set" do
+ ENV["MAKEFLAGS"] = "-j1"
+
+ install_gemfile(<<~G, env: { "BUNDLE_JOBS" => "8" })
+ source "https://gem.repo4"
+ gem "mypsych"
+ G
+
+ gem_make_out = File.read(File.join(@gemspec.extension_dir, "gem_make.out"))
+
+ expect(gem_make_out).not_to include("make -j8")
+ end
+
+ it "pass down the BUNDLE_JOBS to RubyGems when running the compilation of an extension" do
+ ENV.delete("MAKEFLAGS")
+
+ install_gemfile(<<~G, env: { "BUNDLE_JOBS" => "8" })
+ source "https://gem.repo4"
+ gem "mypsych"
+ G
+
+ gem_make_out = File.read(File.join(@gemspec.extension_dir, "gem_make.out"))
+
+ expect(gem_make_out).to include("make -j8")
+ end
+
+ it "uses nprocessors by default" do
+ ENV.delete("MAKEFLAGS")
+
+ install_gemfile(<<~G)
+ source "https://gem.repo4"
+ gem "mypsych"
+ G
+
+ gem_make_out = File.read(File.join(@gemspec.extension_dir, "gem_make.out"))
+
+ expect(gem_make_out).to include("make -j#{Etc.nprocessors + 1}")
+ end
+ end
+
+ describe "when a native extension requires a transitive dependency at build time" do
+ before do
+ build_repo4 do
+ build_gem "alpha", "1.0.0" do |s|
+ extension = "ext/alpha/extconf.rb"
+ s.extensions = extension
+ s.write(extension, <<~CODE)
+ require "mkmf"
+ sleep 1
+ create_makefile("alpha")
+ CODE
+ s.write "lib/alpha.rb", "ALPHA = '1.0.0'"
+ end
+
+ build_gem "beta", "1.0.0" do |s|
+ s.add_dependency "alpha"
+ s.write "lib/beta.rb", "require 'alpha'\nBETA = '1.0.0'"
+ end
+
+ build_gem "gamma", "1.0.0" do |s|
+ s.add_dependency "beta"
+ extension = "ext/gamma/extconf.rb"
+ s.extensions = extension
+ s.write(extension, <<~EXTCONF)
+ require "beta"
+ require "mkmf"
+ create_makefile("gamma")
+ EXTCONF
+ end
+ end
+ end
+
+ it "installs successfully" do
+ install_gemfile <<~G
+ source "https://gem.repo4"
+ gem "gamma"
+ G
+
+ expect(the_bundle).to include_gems "alpha 1.0.0", "beta 1.0.0", "gamma 1.0.0"
+ end
+ end
+
+ describe "when configured path is UTF-8 and a file inside a gem package too" do
+ let(:app_path) do
+ path = tmp("♥")
+ FileUtils.mkdir_p(path)
+ path
+ end
+
+ let(:path) do
+ root.join("vendor/bundle")
+ end
+
+ before do
+ build_repo4 do
+ build_gem "mygem" do |s|
+ s.write "spec/fixtures/_posts/2016-04-01-错误.html"
+ end
+ end
+ end
+
+ it "works" do
+ bundle "config set path #{app_path}/vendor/bundle", dir: app_path
+
+ install_gemfile app_path.join("Gemfile"),<<~G, dir: app_path
+ source "https://gem.repo4"
+ gem "mygem", "1.0"
+ G
+ end
+ end
+
+ context "after installing with --standalone" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ bundle_config "path bundle"
+ bundle "install", standalone: true
+ end
+
+ it "includes the standalone path" do
+ bundle "binstubs myrack", standalone: true
+ standalone_line = File.read(bundled_app("bin/myrackup")).each_line.find {|line| line.include? "$:.unshift" }.strip
+ expect(standalone_line).to eq %($:.unshift File.expand_path "../bundle", __dir__)
+ end
+ end
+
+ describe "when bundle install is executed with unencoded authentication" do
+ before do
+ gemfile <<-G
+ source 'https://rubygems.org/'
+ gem "."
+ G
+ end
+
+ it "should display a helpful message explaining how to fix it" do
+ bundle :install, env: { "BUNDLE_RUBYGEMS__ORG" => "user:pass{word" }, raise_on_error: false
+ expect(exitstatus).to eq(17)
+ expect(err).to eq("Please CGI escape your usernames and passwords before " \
+ "setting them for authentication.")
+ end
+ end
+
+ context "when current platform not included in the lockfile" do
+ around do |example|
+ build_repo4 do
+ build_gem "libv8", "8.4.255.0" do |s|
+ s.platform = "x86_64-darwin-19"
+ end
+
+ build_gem "libv8", "8.4.255.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "libv8"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ libv8 (8.4.255.0-x86_64-darwin-19)
+
+ PLATFORMS
+ x86_64-darwin-19
+
+ DEPENDENCIES
+ libv8
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform("x86_64-linux", &example)
+ end
+
+ it "adds the current platform to the lockfile" do
+ bundle "install --verbose"
+
+ expect(out).to include("re-resolving dependencies because your lockfile is missing the current platform")
+ expect(out).not_to include("you are adding a new platform to your lockfile")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ libv8 (8.4.255.0-x86_64-darwin-19)
+ libv8 (8.4.255.0-x86_64-linux)
+
+ PLATFORMS
+ x86_64-darwin-19
+ x86_64-linux
+
+ DEPENDENCIES
+ libv8
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "fails loudly if frozen mode set" do
+ bundle_config "deployment true"
+ bundle "install", raise_on_error: false
+
+ expect(err).to eq(
+ "Your bundle only supports platforms [\"x86_64-darwin-19\"] but your local platform is x86_64-linux. " \
+ "Add the current platform to the lockfile with\n`bundle lock --add-platform x86_64-linux` and try again."
+ )
+ end
+ end
+
+ context "with missing platform specific gems in lockfile" do
+ before do
+ build_repo4 do
+ build_gem "racca", "1.5.2"
+
+ build_gem "nokogiri", "1.12.4" do |s|
+ s.platform = "x86_64-darwin"
+ s.add_dependency "racca", "~> 1.4"
+ end
+
+ build_gem "nokogiri", "1.12.4" do |s|
+ s.platform = "x86_64-linux"
+ s.add_dependency "racca", "~> 1.4"
+ end
+
+ build_gem "crass", "1.0.6"
+
+ build_gem "loofah", "2.12.0" do |s|
+ s.add_dependency "crass", "~> 1.0.2"
+ s.add_dependency "nokogiri", ">= 1.5.9"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ ruby "#{Gem.ruby_version}"
+
+ gem "loofah", "~> 2.12.0"
+ G
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo4, "crass", "1.0.6"
+ c.checksum gem_repo4, "loofah", "2.12.0"
+ c.checksum gem_repo4, "nokogiri", "1.12.4", "x86_64-darwin"
+ c.checksum gem_repo4, "racca", "1.5.2"
+ end
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ crass (1.0.6)
+ loofah (2.12.0)
+ crass (~> 1.0.2)
+ nokogiri (>= 1.5.9)
+ nokogiri (1.12.4-x86_64-darwin)
+ racca (~> 1.4)
+ racca (1.5.2)
+
+ PLATFORMS
+ x86_64-darwin-20
+ x86_64-linux
+
+ DEPENDENCIES
+ loofah (~> 2.12.0)
+ #{checksums}
+ RUBY VERSION
+ #{Bundler::RubyVersion.system}
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically fixes the lockfile" do
+ bundle_config "path vendor/bundle"
+
+ simulate_platform "x86_64-linux" do
+ bundle "install", artifice: "compact_index"
+ end
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "crass", "1.0.6"
+ c.checksum gem_repo4, "loofah", "2.12.0"
+ c.checksum gem_repo4, "nokogiri", "1.12.4", "x86_64-darwin"
+ c.checksum gem_repo4, "racca", "1.5.2"
+ c.checksum gem_repo4, "nokogiri", "1.12.4", "x86_64-linux"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ crass (1.0.6)
+ loofah (2.12.0)
+ crass (~> 1.0.2)
+ nokogiri (>= 1.5.9)
+ nokogiri (1.12.4-x86_64-darwin)
+ racca (~> 1.4)
+ nokogiri (1.12.4-x86_64-linux)
+ racca (~> 1.4)
+ racca (1.5.2)
+
+ PLATFORMS
+ x86_64-darwin-20
+ x86_64-linux
+
+ DEPENDENCIES
+ loofah (~> 2.12.0)
+ #{checksums}
+ RUBY VERSION
+ #{Bundler::RubyVersion.system}
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when lockfile has incorrect dependencies" do
+ before do
+ build_repo2
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack_middleware"
+ G
+
+ system_gems "myrack_middleware-1.0", path: default_bundle_path
+
+ # we want to raise when the 1.0 line should be followed by " myrack (= 0.9.1)" but isn't
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack_middleware (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "raises a clear error message when frozen" do
+ bundle_config "frozen true"
+ bundle "install", raise_on_error: false
+
+ expect(exitstatus).to eq(41)
+ expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack_middleware-1.0")
+ expect(err).to include("myrack: gemspec specifies = 0.9.1, not in lockfile")
+ end
+
+ it "updates the lockfile when not frozen" do
+ missing_dep = "myrack (0.9.1)"
+ expect(lockfile).not_to include(missing_dep)
+
+ bundle_config "frozen false"
+ bundle :install
+
+ expect(lockfile).to include(missing_dep)
+ expect(out).to include("now installed")
+ end
+ end
+
+ context "with --local flag" do
+ before do
+ system_gems "myrack-1.0.0", path: default_bundle_path
+ end
+
+ it "respects installed gems without fetching any remote sources" do
+ install_gemfile <<-G, local: true
+ source "https://gem.repo1"
+
+ source "https://not-existing-source" do
+ gem "myrack"
+ end
+ G
+
+ expect(last_command).to be_success
+ end
+ end
+
+ context "with only option" do
+ before do
+ bundle_config "only a:b"
+ end
+
+ it "installs only gems of the specified groups" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ gem "myrack", group: :a
+ gem "rake", group: :b
+ gem "yard", group: :c
+ G
+
+ expect(out).to include("Installing myrack")
+ expect(out).to include("Installing rake")
+ expect(out).not_to include("Installing yard")
+ end
+ end
+
+ context "with --prefer-local flag" do
+ context "and gems available locally" do
+ before do
+ build_repo4 do
+ build_gem "foo", "1.0.1"
+ build_gem "foo", "1.0.0"
+ build_gem "bar", "1.0.0"
+
+ build_gem "a", "1.0.0" do |s|
+ s.add_dependency "foo", "~> 1.0.0"
+ end
+
+ build_gem "b", "1.0.0" do |s|
+ s.add_dependency "foo", "~> 1.0.1"
+ end
+ end
+
+ system_gems "foo-1.0.0", path: default_bundle_path, gem_repo: gem_repo4
+ end
+
+ it "fetches remote sources when not available locally" do
+ install_gemfile <<-G, "prefer-local": true, verbose: true
+ source "https://gem.repo4"
+
+ gem "foo"
+ gem "bar"
+ G
+
+ expect(out).to include("Using foo 1.0.0").and include("Fetching bar 1.0.0").and include("Installing bar 1.0.0")
+ expect(last_command).to be_success
+ end
+
+ it "fetches remote sources when local version does not match requirements" do
+ install_gemfile <<-G, "prefer-local": true, verbose: true
+ source "https://gem.repo4"
+
+ gem "foo", "1.0.1"
+ gem "bar"
+ G
+
+ expect(out).to include("Fetching foo 1.0.1").and include("Installing foo 1.0.1").and include("Fetching bar 1.0.0").and include("Installing bar 1.0.0")
+ expect(last_command).to be_success
+ end
+
+ it "uses the locally available version for sub-dependencies when possible" do
+ install_gemfile <<-G, "prefer-local": true, verbose: true
+ source "https://gem.repo4"
+
+ gem "a"
+ G
+
+ expect(out).to include("Using foo 1.0.0").and include("Fetching a 1.0.0").and include("Installing a 1.0.0")
+ expect(last_command).to be_success
+ end
+
+ it "fetches remote sources for sub-dependencies when the locally available version does not satisfy the requirement" do
+ install_gemfile <<-G, "prefer-local": true, verbose: true
+ source "https://gem.repo4"
+
+ gem "b"
+ G
+
+ expect(out).to include("Fetching foo 1.0.1").and include("Installing foo 1.0.1").and include("Fetching b 1.0.0").and include("Installing b 1.0.0")
+ expect(last_command).to be_success
+ end
+ end
+
+ context "and no gems available locally" do
+ before do
+ build_repo4 do
+ build_gem "myreline", "0.3.8"
+ build_gem "debug", "0.2.1"
+
+ build_gem "debug", "1.10.0" do |s|
+ s.add_dependency "myreline"
+ end
+ end
+ end
+
+ it "resolves to the latest version if no gems are available locally" do
+ install_gemfile <<~G, "prefer-local": true, verbose: true
+ source "https://gem.repo4"
+
+ gem "debug"
+ G
+
+ expect(out).to include("Fetching debug 1.10.0").and include("Installing debug 1.10.0").and include("Fetching myreline 0.3.8").and include("Installing myreline 0.3.8")
+ expect(last_command).to be_success
+ end
+ end
+ end
+
+ context "with a symlinked configured as bundle path and a gem with symlinks" do
+ before do
+ symlinked_bundled_app = tmp("bundled_app-symlink")
+ File.symlink(bundled_app, symlinked_bundled_app)
+ bundle_config "path #{File.join(symlinked_bundled_app, ".vendor")}"
+
+ binman_path = tmp("binman")
+ FileUtils.mkdir_p binman_path
+
+ readme_path = File.join(binman_path, "README.markdown")
+ FileUtils.touch(readme_path)
+
+ man_path = File.join(binman_path, "man", "man0")
+ FileUtils.mkdir_p man_path
+
+ File.symlink("../../README.markdown", File.join(man_path, "README.markdown"))
+
+ build_repo4 do
+ build_gem "binman", path: gem_repo4("gems"), lib_path: binman_path, no_default: true do |s|
+ s.files = ["README.markdown", "man/man0/README.markdown"]
+ end
+ end
+ end
+
+ it "installs fine" do
+ install_gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "binman"
+ G
+ end
+ end
+
+ context "when a gem has equivalent versions with inconsistent dependencies" do
+ before do
+ build_repo4 do
+ build_gem "autobuild", "1.10.rc2" do |s|
+ s.add_dependency "utilrb", ">= 1.6.0"
+ end
+
+ build_gem "autobuild", "1.10.0.rc2" do |s|
+ s.add_dependency "utilrb", ">= 2.0"
+ end
+ end
+ end
+
+ it "does not crash unexpectedly" do
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "autobuild", "1.10.rc2"
+ G
+
+ bundle "install --jobs 1", raise_on_error: false
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+ expect(err).to include("Could not find compatible versions")
+ end
+ end
+
+ context "when a lockfile has unmet dependencies, and the Gemfile has no resolution" do
+ before do
+ build_repo4 do
+ build_gem "aaa", "0.2.0" do |s|
+ s.add_dependency "zzz", "< 0.2.0"
+ end
+
+ build_gem "zzz", "0.2.0"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "aaa"
+ gem "zzz"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ aaa (0.2.0)
+ zzz (< 0.2.0)
+ zzz (0.2.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ aaa!
+ zzz!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "does not install, but raises a resolution error" do
+ bundle "install", raise_on_error: false
+ expect(err).to include("Could not find compatible versions")
+ end
+ end
+
+ context "when --jobs option given" do
+ before do
+ install_gemfile "source 'https://gem.repo1'", jobs: 1
+ end
+
+ it "does not save the flag to config" do
+ expect(bundled_app(".bundle/config")).not_to exist
+ end
+ end
+
+ context "when bundler installation is corrupt" do
+ before do
+ system_gems "bundler-9.99.8"
+
+ replace_version_file("9.99.9", dir: system_gem_path("gems/bundler-9.99.8"))
+ end
+
+ it "shows a proper error" do
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+
+ BUNDLED WITH
+ 9.99.8
+ L
+
+ install_gemfile "source \"https://gem.repo1\"", env: { "BUNDLER_VERSION" => "9.99.8" }, raise_on_error: false
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+ expect(err).to include("The running version of Bundler (9.99.9) does not match the version of the specification installed for it (9.99.8)")
+ end
+ end
+
+ it "only installs executable files in bin" do
+ bundle_config "path vendor/bundle"
+
+ install_gemfile <<~G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expected_executables = [vendored_gems("bin/myrackup").to_s]
+ expected_executables << vendored_gems("bin/myrackup.bat").to_s if Gem.win_platform?
+ expect(Dir.glob(vendored_gems("bin/*"))).to eq(expected_executables)
+ end
+
+ it "prevents removing binstubs when BUNDLE_CLEAN is set" do
+ build_repo4 do
+ build_gem "kamal", "4.0.6" do |s|
+ s.executables = ["kamal"]
+ end
+ end
+
+ gemfile = <<~G
+ source "https://gem.repo4"
+ gem "kamal"
+ G
+
+ install_gemfile(gemfile, env: { "BUNDLE_CLEAN" => "true", "BUNDLE_PATH" => "vendor/bundle" })
+
+ expected_executables = [vendored_gems("bin/kamal").to_s]
+ expected_executables << vendored_gems("bin/kamal.bat").to_s if Gem.win_platform?
+ expect(Dir.glob(vendored_gems("bin/*"))).to eq(expected_executables)
+ end
+
+ it "preserves lockfile versions conservatively" do
+ build_repo4 do
+ build_gem "mypsych", "4.0.6" do |s|
+ s.add_dependency "mystringio"
+ end
+
+ build_gem "mypsych", "5.1.2" do |s|
+ s.add_dependency "mystringio"
+ end
+
+ build_gem "mystringio", "3.1.0"
+ build_gem "mystringio", "3.1.1"
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ mypsych (4.0.6)
+ mystringio
+ mystringio (3.1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ mypsych (~> 4.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<~G
+ source "https://gem.repo4"
+ gem "mypsych", "~> 5.0"
+ G
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ mypsych (5.1.2)
+ mystringio
+ mystringio (3.1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ mypsych (~> 5.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+end
diff --git a/spec/bundler/commands/issue_spec.rb b/spec/bundler/commands/issue_spec.rb
new file mode 100644
index 0000000000..346cdedc42
--- /dev/null
+++ b/spec/bundler/commands/issue_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle issue" do
+ it "exits with a message" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ bundle "issue"
+ expect(out).to include "Did you find an issue with Bundler?"
+ expect(out).to include "## Environment"
+ expect(out).to include "## Gemfile"
+ expect(out).to include "## Bundle Doctor"
+ end
+end
diff --git a/spec/bundler/commands/licenses_spec.rb b/spec/bundler/commands/licenses_spec.rb
new file mode 100644
index 0000000000..ebfad5ed4a
--- /dev/null
+++ b/spec/bundler/commands/licenses_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle licenses" do
+ before :each do
+ build_repo2 do
+ build_gem "with_license" do |s|
+ s.license = "MIT"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails"
+ gem "with_license"
+ G
+ end
+
+ it "prints license information for all gems in the bundle" do
+ bundle "licenses"
+
+ expect(out).to include("bundler: MIT")
+ expect(out).to include("with_license: MIT")
+ end
+
+ it "performs an automatic bundle install" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails"
+ gem "with_license"
+ gem "foo"
+ G
+
+ bundle_config "auto_install 1"
+ bundle :licenses
+ expect(out).to include("Installing foo 1.0")
+ end
+end
diff --git a/spec/bundler/commands/list_spec.rb b/spec/bundler/commands/list_spec.rb
new file mode 100644
index 0000000000..c890646a81
--- /dev/null
+++ b/spec/bundler/commands/list_spec.rb
@@ -0,0 +1,315 @@
+# frozen_string_literal: true
+
+require "json"
+
+RSpec.describe "bundle list" do
+ def find_gem_name(json:, name:)
+ parse_json(json)["gems"].detect {|h| h["name"] == name }
+ end
+
+ def parse_json(json)
+ JSON.parse(json)
+ end
+
+ context "in verbose mode" do
+ it "logs the actual flags passed to the command" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ bundle "list --verbose"
+
+ expect(out).to include("Running `bundle list --verbose`")
+ end
+ end
+
+ context "with name-only and paths option" do
+ it "raises an error" do
+ bundle "list --name-only --paths", raise_on_error: false
+
+ expect(err).to eq "The `--name-only` and `--paths` options cannot be used together"
+ end
+ end
+
+ context "with without-group and only-group option" do
+ it "raises an error" do
+ bundle "list --without-group dev --only-group test", raise_on_error: false
+
+ expect(err).to eq "The `--only-group` and `--without-group` options cannot be used together"
+ end
+ end
+
+ context "with invalid format option" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ G
+ end
+
+ it "raises an error" do
+ bundle "list --format=nope", raise_on_error: false
+
+ expect(err).to eq "Unknown option`--format=nope`. Supported formats: `json`"
+ end
+ end
+
+ describe "with without-group option" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ gem "rspec", :group => [:test]
+ gem "rails", :group => [:production]
+ G
+ end
+
+ context "when group is present" do
+ it "prints the gems not in the specified group" do
+ bundle "list --without-group test"
+
+ expect(out).to include(" * myrack (1.0.0)")
+ expect(out).to include(" * rails (2.3.2)")
+ expect(out).not_to include(" * rspec (1.2.7)")
+ end
+
+ it "prints the gems not in the specified group with json" do
+ bundle "list --without-group test --format=json"
+
+ gem = find_gem_name(json: out, name: "myrack")
+ expect(gem["version"]).to eq("1.0.0")
+ gem = find_gem_name(json: out, name: "rails")
+ expect(gem["version"]).to eq("2.3.2")
+ gem = find_gem_name(json: out, name: "rspec")
+ expect(gem).to be_nil
+ end
+ end
+
+ context "when group is not found" do
+ it "raises an error" do
+ bundle "list --without-group random", raise_on_error: false
+
+ expect(err).to eq "`random` group could not be found."
+ end
+ end
+
+ context "when multiple groups" do
+ it "prints the gems not in the specified groups" do
+ bundle "list --without-group test production"
+
+ expect(out).to include(" * myrack (1.0.0)")
+ expect(out).not_to include(" * rails (2.3.2)")
+ expect(out).not_to include(" * rspec (1.2.7)")
+ end
+
+ it "prints the gems not in the specified groups with json" do
+ bundle "list --without-group test production --format=json"
+
+ gem = find_gem_name(json: out, name: "myrack")
+ expect(gem["version"]).to eq("1.0.0")
+ gem = find_gem_name(json: out, name: "rails")
+ expect(gem).to be_nil
+ gem = find_gem_name(json: out, name: "rspec")
+ expect(gem).to be_nil
+ end
+ end
+ end
+
+ describe "with only-group option" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ gem "rspec", :group => [:test]
+ gem "rails", :group => [:production]
+ G
+ end
+
+ context "when group is present" do
+ it "prints the gems in the specified group" do
+ bundle "list --only-group default"
+
+ expect(out).to include(" * myrack (1.0.0)")
+ expect(out).not_to include(" * rspec (1.2.7)")
+ end
+
+ it "prints the gems in the specified group with json" do
+ bundle "list --only-group default --format=json"
+
+ gem = find_gem_name(json: out, name: "myrack")
+ expect(gem["version"]).to eq("1.0.0")
+ gem = find_gem_name(json: out, name: "rspec")
+ expect(gem).to be_nil
+ end
+ end
+
+ context "when group is not found" do
+ it "raises an error" do
+ bundle "list --only-group random", raise_on_error: false
+
+ expect(err).to eq "`random` group could not be found."
+ end
+ end
+
+ context "when multiple groups" do
+ it "prints the gems in the specified groups" do
+ bundle "list --only-group default production"
+
+ expect(out).to include(" * myrack (1.0.0)")
+ expect(out).to include(" * rails (2.3.2)")
+ expect(out).not_to include(" * rspec (1.2.7)")
+ end
+
+ it "prints the gems in the specified groups with json" do
+ bundle "list --only-group default production --format=json"
+
+ gem = find_gem_name(json: out, name: "myrack")
+ expect(gem["version"]).to eq("1.0.0")
+ gem = find_gem_name(json: out, name: "rails")
+ expect(gem["version"]).to eq("2.3.2")
+ gem = find_gem_name(json: out, name: "rspec")
+ expect(gem).to be_nil
+ end
+ end
+ end
+
+ context "with name-only option" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ gem "rspec", :group => [:test]
+ G
+ end
+
+ it "prints only the name of the gems in the bundle" do
+ bundle "list --name-only"
+
+ expect(out).to include("myrack")
+ expect(out).to include("rspec")
+ end
+
+ it "prints only the name of the gems in the bundle with json" do
+ bundle "list --name-only --format=json"
+
+ gem = find_gem_name(json: out, name: "myrack")
+ expect(gem.keys).to eq(["name"])
+ gem = find_gem_name(json: out, name: "rspec")
+ expect(gem.keys).to eq(["name"])
+ end
+ end
+
+ context "with paths option" do
+ before do
+ build_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "bar"
+ end
+
+ build_git "git_test", "1.0.0", path: lib_path("git_test")
+
+ build_lib("gemspec_test", path: tmp("gemspec_test")) do |s|
+ s.add_dependency "bar", "=1.0.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ gem "rails"
+ gem "git_test", :git => "#{lib_path("git_test")}"
+ gemspec :path => "#{tmp("gemspec_test")}"
+ G
+ end
+
+ it "prints the path of each gem in the bundle" do
+ bundle "list --paths"
+ expect(out).to match(%r{.*\/rails\-2\.3\.2})
+ expect(out).to match(%r{.*\/myrack\-1\.2})
+ expect(out).to match(%r{.*\/git_test\-\w})
+ expect(out).to match(%r{.*\/gemspec_test})
+ end
+
+ it "prints the path of each gem in the bundle with json" do
+ bundle "list --paths --format=json"
+
+ gem = find_gem_name(json: out, name: "rails")
+ expect(gem["path"]).to match(%r{.*\/rails\-2\.3\.2})
+ expect(gem["git_version"]).to be_nil
+
+ gem = find_gem_name(json: out, name: "myrack")
+ expect(gem["path"]).to match(%r{.*\/myrack\-1\.2})
+ expect(gem["git_version"]).to be_nil
+
+ gem = find_gem_name(json: out, name: "git_test")
+ expect(gem["path"]).to match(%r{.*\/git_test\-\w})
+ expect(gem["git_version"]).to be_truthy
+ expect(gem["git_version"].strip).to eq(gem["git_version"])
+
+ gem = find_gem_name(json: out, name: "gemspec_test")
+ expect(gem["path"]).to match(%r{.*\/gemspec_test})
+ expect(gem["git_version"]).to be_nil
+ end
+ end
+
+ context "when no gems are in the gemfile" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ G
+ end
+
+ it "prints message saying no gems are in the bundle" do
+ bundle "list"
+ expect(out).to include("No gems in the Gemfile")
+ end
+
+ it "prints empty json" do
+ bundle "list --format=json"
+ expect(parse_json(out)["gems"]).to eq([])
+ end
+ end
+
+ context "without options" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ gem "rspec", :group => [:test]
+ G
+ end
+
+ it "lists gems installed in the bundle" do
+ bundle "list"
+ expect(out).to include(" * myrack (1.0.0)")
+ end
+
+ it "lists gems installed in the bundle with json" do
+ bundle "list --format=json"
+
+ gem = find_gem_name(json: out, name: "myrack")
+ expect(gem["version"]).to eq("1.0.0")
+ end
+ end
+
+ context "when using the ls alias" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ gem "rspec", :group => [:test]
+ G
+ end
+
+ it "runs the list command" do
+ bundle "ls"
+ expect(out).to include("Gems included by the bundle")
+ end
+ end
+end
diff --git a/spec/bundler/commands/lock_spec.rb b/spec/bundler/commands/lock_spec.rb
new file mode 100644
index 0000000000..8ab3cc7e8d
--- /dev/null
+++ b/spec/bundler/commands/lock_spec.rb
@@ -0,0 +1,2877 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle lock" do
+ let(:expected_lockfile) do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "actionmailer", "2.3.2"
+ c.checksum gem_repo4, "actionpack", "2.3.2"
+ c.checksum gem_repo4, "activerecord", "2.3.2"
+ c.checksum gem_repo4, "activeresource", "2.3.2"
+ c.checksum gem_repo4, "activesupport", "2.3.2"
+ c.checksum gem_repo4, "foo", "1.0"
+ c.checksum gem_repo4, "rails", "2.3.2"
+ c.checksum gem_repo4, "rake", rake_version
+ c.checksum gem_repo4, "weakling", "0.0.3"
+ end
+
+ <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actionmailer (2.3.2)
+ activesupport (= 2.3.2)
+ actionpack (2.3.2)
+ activesupport (= 2.3.2)
+ activerecord (2.3.2)
+ activesupport (= 2.3.2)
+ activeresource (2.3.2)
+ activesupport (= 2.3.2)
+ activesupport (2.3.2)
+ foo (1.0)
+ rails (2.3.2)
+ actionmailer (= 2.3.2)
+ actionpack (= 2.3.2)
+ activerecord (= 2.3.2)
+ activeresource (= 2.3.2)
+ rake (= #{rake_version})
+ rake (#{rake_version})
+ weakling (0.0.3)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ rails
+ weakling
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ let(:outdated_lockfile) do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "actionmailer", "2.3.1"
+ c.checksum gem_repo4, "actionpack", "2.3.1"
+ c.checksum gem_repo4, "activerecord", "2.3.1"
+ c.checksum gem_repo4, "activeresource", "2.3.1"
+ c.checksum gem_repo4, "activesupport", "2.3.1"
+ c.checksum gem_repo4, "foo", "1.0"
+ c.checksum gem_repo4, "rails", "2.3.1"
+ c.checksum gem_repo4, "rake", rake_version
+ c.checksum gem_repo4, "weakling", "0.0.3"
+ end
+
+ <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actionmailer (2.3.1)
+ activesupport (= 2.3.1)
+ actionpack (2.3.1)
+ activesupport (= 2.3.1)
+ activerecord (2.3.1)
+ activesupport (= 2.3.1)
+ activeresource (2.3.1)
+ activesupport (= 2.3.1)
+ activesupport (2.3.1)
+ foo (1.0)
+ rails (2.3.1)
+ actionmailer (= 2.3.1)
+ actionpack (= 2.3.1)
+ activerecord (= 2.3.1)
+ activeresource (= 2.3.1)
+ rake (= #{rake_version})
+ rake (#{rake_version})
+ weakling (0.0.3)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ rails
+ weakling
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ let(:gemfile_with_rails_weakling_and_foo_from_repo4) do
+ build_repo4 do
+ build_gem "rake", "10.0.1"
+ build_gem "rake", rake_version
+
+ %w[2.3.1 2.3.2].each do |version|
+ build_gem "rails", version do |s|
+ s.executables = "rails"
+ s.add_dependency "rake", version == "2.3.1" ? "10.0.1" : rake_version
+ s.add_dependency "actionpack", version
+ s.add_dependency "activerecord", version
+ s.add_dependency "actionmailer", version
+ s.add_dependency "activeresource", version
+ end
+ build_gem "actionpack", version do |s|
+ s.add_dependency "activesupport", version
+ end
+ build_gem "activerecord", version do |s|
+ s.add_dependency "activesupport", version
+ end
+ build_gem "actionmailer", version do |s|
+ s.add_dependency "activesupport", version
+ end
+ build_gem "activeresource", version do |s|
+ s.add_dependency "activesupport", version
+ end
+ build_gem "activesupport", version
+ end
+
+ build_gem "weakling", "0.0.3"
+
+ build_gem "foo"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "rails"
+ gem "weakling"
+ gem "foo"
+ G
+ end
+
+ it "prints a lockfile when there is no existing lockfile with --print" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock --print"
+
+ expect(out).to eq(expected_lockfile.chomp)
+ end
+
+ it "prints a lockfile when there is an existing lockfile with --print" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile expected_lockfile
+
+ bundle "lock --print"
+
+ expect(out).to eq(expected_lockfile.chomp)
+ end
+
+ it "prints a lockfile when there is an existing checksums lockfile with --print" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile expected_lockfile
+
+ bundle "lock --print"
+
+ expect(out).to eq(expected_lockfile.chomp)
+ end
+
+ it "writes a lockfile when there is no existing lockfile" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock"
+
+ expect(read_lockfile).to eq(expected_lockfile)
+ end
+
+ it "prints a lockfile without fetching new checksums if the existing lockfile had no checksums" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile expected_lockfile
+
+ bundle "lock --print"
+
+ expect(out).to eq(expected_lockfile.chomp)
+ end
+
+ it "touches the lockfile when there is an existing lockfile that does not need changes" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile expected_lockfile
+
+ expect do
+ bundle "lock"
+ end.to change { bundled_app_lock.mtime }
+ end
+
+ it "does not touch lockfile with --print" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile expected_lockfile
+
+ expect do
+ bundle "lock --print"
+ end.not_to change { bundled_app_lock.mtime }
+ end
+
+ it "writes a lockfile when there is an outdated lockfile using --update" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile outdated_lockfile
+
+ bundle "lock --update"
+
+ expect(read_lockfile).to eq(expected_lockfile)
+ end
+
+ it "prints an updated lockfile when there is an outdated lockfile using --print --update" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile outdated_lockfile
+
+ bundle "lock --print --update"
+
+ expect(out).to eq(expected_lockfile.rstrip)
+ end
+
+ it "emits info messages to stderr when updating an outdated lockfile using --print --update" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile outdated_lockfile
+
+ bundle "lock --print --update"
+
+ expect(err).to eq(<<~STDERR.rstrip)
+ Fetching gem metadata from https://gem.repo4/...
+ Resolving dependencies...
+ STDERR
+ end
+
+ it "writes a lockfile when there is an outdated lockfile and bundle is frozen" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile outdated_lockfile
+
+ bundle "lock --update", env: { "BUNDLE_FROZEN" => "true" }
+
+ expect(read_lockfile).to eq(expected_lockfile)
+ end
+
+ it "does not fetch remote specs when using the --local option" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock --update --local", raise_on_error: false
+
+ expect(err).to match(/locally installed gems/)
+ end
+
+ it "does not fetch remote checksums with --local" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile expected_lockfile
+
+ bundle "lock --print --local"
+
+ expect(out).to eq(expected_lockfile.chomp)
+ end
+
+ it "works with --gemfile flag" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ gemfile "CustomGemfile", <<-G
+ source "https://gem.repo4"
+ gem "foo"
+ G
+ bundle "lock --gemfile CustomGemfile"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "foo", "1.0"
+ end
+
+ lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ foo (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ expect(out).to match(/Writing lockfile to.+CustomGemfile\.lock/)
+ expect(read_lockfile("CustomGemfile.lock")).to eq(lockfile)
+ expect { read_lockfile }.to raise_error(Errno::ENOENT)
+ end
+
+ it "writes to a custom location using --lockfile" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock --lockfile=lock"
+
+ expect(out).to match(/Writing lockfile to.+lock/)
+ expect(read_lockfile("lock")).to eq(expected_lockfile)
+ expect { read_lockfile }.to raise_error(Errno::ENOENT)
+ end
+
+ it "updates a specific gem and write to a custom location" do
+ build_repo4 do
+ build_gem "foo", %w[1.0.2 1.0.3]
+ build_gem "warning", %w[1.4.0 1.5.0]
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "foo"
+ gem "warning"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ foo (1.0.2)
+ warning (1.4.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ uri
+ warning
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --update foo --lockfile=lock"
+
+ lockfile_content = read_lockfile("lock")
+ expect(lockfile_content).to include("foo (1.0.3)")
+ expect(lockfile_content).to include("warning (1.4.0)")
+ end
+
+ it "writes to custom location using --lockfile when a default lockfile is present" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "install"
+ bundle "lock --lockfile=lock"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "actionmailer", "2.3.2"
+ c.checksum gem_repo4, "actionpack", "2.3.2"
+ c.checksum gem_repo4, "activerecord", "2.3.2"
+ c.checksum gem_repo4, "activeresource", "2.3.2"
+ c.checksum gem_repo4, "activesupport", "2.3.2"
+ c.checksum gem_repo4, "foo", "1.0"
+ c.checksum gem_repo4, "rails", "2.3.2"
+ c.checksum gem_repo4, "rake", rake_version
+ c.checksum gem_repo4, "weakling", "0.0.3"
+ end
+
+ lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actionmailer (2.3.2)
+ activesupport (= 2.3.2)
+ actionpack (2.3.2)
+ activesupport (= 2.3.2)
+ activerecord (2.3.2)
+ activesupport (= 2.3.2)
+ activeresource (2.3.2)
+ activesupport (= 2.3.2)
+ activesupport (2.3.2)
+ foo (1.0)
+ rails (2.3.2)
+ actionmailer (= 2.3.2)
+ actionpack (= 2.3.2)
+ activerecord (= 2.3.2)
+ activeresource (= 2.3.2)
+ rake (= #{rake_version})
+ rake (#{rake_version})
+ weakling (0.0.3)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ rails
+ weakling
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ expect(out).to match(/Writing lockfile to.+lock/)
+ expect(read_lockfile("lock")).to eq(lockfile)
+ end
+
+ it "update specific gems using --update" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "actionmailer", "2.3.1"
+ c.checksum gem_repo4, "actionpack", "2.3.1"
+ c.checksum gem_repo4, "activerecord", "2.3.1"
+ c.checksum gem_repo4, "activeresource", "2.3.1"
+ c.checksum gem_repo4, "activesupport", "2.3.1"
+ c.checksum gem_repo4, "foo", "1.0"
+ c.checksum gem_repo4, "rails", "2.3.1"
+ c.checksum gem_repo4, "rake", "10.0.1"
+ c.checksum gem_repo4, "weakling", "0.0.3"
+ end
+
+ lockfile_with_outdated_rails_and_rake = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actionmailer (2.3.1)
+ activesupport (= 2.3.1)
+ actionpack (2.3.1)
+ activesupport (= 2.3.1)
+ activerecord (2.3.1)
+ activesupport (= 2.3.1)
+ activeresource (2.3.1)
+ activesupport (= 2.3.1)
+ activesupport (2.3.1)
+ foo (1.0)
+ rails (2.3.1)
+ actionmailer (= 2.3.1)
+ actionpack (= 2.3.1)
+ activerecord (= 2.3.1)
+ activeresource (= 2.3.1)
+ rake (= 10.0.1)
+ rake (10.0.1)
+ weakling (0.0.3)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ rails
+ weakling
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile lockfile_with_outdated_rails_and_rake
+
+ bundle "lock --update rails rake"
+
+ expect(read_lockfile).to eq(expected_lockfile)
+ end
+
+ it "updates specific gems using --update, even if that requires unlocking other top level gems" do
+ build_repo4 do
+ build_gem "prism", "0.15.1"
+ build_gem "prism", "0.24.0"
+
+ build_gem "ruby-lsp", "0.12.0" do |s|
+ s.add_dependency "prism", "< 0.24.0"
+ end
+
+ build_gem "ruby-lsp", "0.16.1" do |s|
+ s.add_dependency "prism", ">= 0.24.0"
+ end
+
+ build_gem "tapioca", "0.11.10" do |s|
+ s.add_dependency "prism", "< 0.24.0"
+ end
+
+ build_gem "tapioca", "0.13.1" do |s|
+ s.add_dependency "prism", ">= 0.24.0"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "tapioca"
+ gem "ruby-lsp"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ prism (0.15.1)
+ ruby-lsp (0.12.0)
+ prism (< 0.24.0)
+ tapioca (0.11.10)
+ prism (< 0.24.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ruby-lsp
+ tapioca
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --update tapioca --verbose"
+
+ expect(lockfile).to include("tapioca (0.13.1)")
+ end
+
+ it "updates specific gems using --update, even if that requires unlocking other top level gems, but only as few as possible" do
+ build_repo4 do
+ build_gem "prism", "0.15.1"
+ build_gem "prism", "0.24.0"
+
+ build_gem "ruby-lsp", "0.12.0" do |s|
+ s.add_dependency "prism", "< 0.24.0"
+ end
+
+ build_gem "ruby-lsp", "0.16.1" do |s|
+ s.add_dependency "prism", ">= 0.24.0"
+ end
+
+ build_gem "tapioca", "0.11.10" do |s|
+ s.add_dependency "prism", "< 0.24.0"
+ end
+
+ build_gem "tapioca", "0.13.1" do |s|
+ s.add_dependency "prism", ">= 0.24.0"
+ end
+
+ build_gem "other-prism-dependent", "1.0.0" do |s|
+ s.add_dependency "prism", ">= 0.15.1"
+ end
+
+ build_gem "other-prism-dependent", "1.1.0" do |s|
+ s.add_dependency "prism", ">= 0.15.1"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "tapioca"
+ gem "ruby-lsp"
+ gem "other-prism-dependent"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ other-prism-dependent (1.0.0)
+ prism (>= 0.15.1)
+ prism (0.15.1)
+ ruby-lsp (0.12.0)
+ prism (< 0.24.0)
+ tapioca (0.11.10)
+ prism (< 0.24.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ruby-lsp
+ tapioca
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --update tapioca"
+
+ expect(lockfile).to include("tapioca (0.13.1)")
+ expect(lockfile).to include("other-prism-dependent (1.0.0)")
+ end
+
+ it "preserves unknown checksum algorithms" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile expected_lockfile.gsub(/(sha256=[a-f0-9]+)$/, "constant=true,\\1,xyz=123")
+
+ previous_lockfile = read_lockfile
+
+ bundle "lock"
+
+ expect(read_lockfile).to eq(previous_lockfile)
+ end
+
+ it "does not unlock git sources when only uri shape changes" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ build_git("foo")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ # Change uri format to end with "/" and reinstall
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}/"
+ G
+
+ expect(out).to include("using resolution from the lockfile")
+ expect(out).not_to include("re-resolving dependencies because the list of sources changed")
+ end
+
+ it "updates specific gems using --update using the locked revision of unrelated git gems for resolving" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ ref = build_git("foo").ref_for("HEAD")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "deadbeef"
+ G
+
+ lockfile <<~L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{ref}
+ branch: deadbeef
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ rake (10.0.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ rake
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --update rake --verbose"
+ expect(out).to match(/Writing lockfile to.+lock/)
+ expect(lockfile).to include("rake (#{rake_version})")
+ end
+
+ it "errors when updating a missing specific gems using --update" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile expected_lockfile
+
+ bundle "lock --update blahblah", raise_on_error: false
+ expect(err).to eq("Could not find gem 'blahblah'.")
+
+ expect(read_lockfile).to eq(expected_lockfile)
+ end
+
+ it "can lock without downloading gems" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "myrack_middleware", :group => "test"
+ G
+ bundle_config "without test"
+ bundle_config "path vendor/bundle"
+ bundle "lock", verbose: true
+ expect(bundled_app("vendor/bundle")).not_to exist
+ end
+
+ # see update_spec for more coverage on same options. logic is shared so it's not necessary
+ # to repeat coverage here.
+ context "conservative updates" do
+ before do
+ build_repo4 do
+ build_gem "foo", %w[1.4.3 1.4.4] do |s|
+ s.add_dependency "bar", "~> 2.0"
+ end
+ build_gem "foo", %w[1.4.5 1.5.0] do |s|
+ s.add_dependency "bar", "~> 2.1"
+ end
+ build_gem "foo", %w[1.5.1] do |s|
+ s.add_dependency "bar", "~> 3.0"
+ end
+ build_gem "foo", %w[2.0.0.pre] do |s|
+ s.add_dependency "bar"
+ end
+ build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 2.1.2.pre 3.0.0 3.1.0.pre 4.0.0.pre]
+ build_gem "qux", %w[1.0.0 1.0.1 1.1.0 2.0.0]
+ end
+
+ # establish a lockfile set to 1.4.3
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo', '1.4.3'
+ gem 'bar', '2.0.3'
+ gem 'qux', '1.0.0'
+ G
+
+ # remove 1.4.3 requirement and bar altogether
+ # to setup update specs below
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo'
+ gem 'qux'
+ G
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ it "single gem updates dependent gem to minor" do
+ bundle "lock --update foo --patch"
+
+ expect(the_bundle.locked_specs).to eq(%w[foo-1.4.5 bar-2.1.1 qux-1.0.0].sort)
+ end
+
+ it "minor preferred with strict" do
+ bundle "lock --update --minor --strict"
+
+ expect(the_bundle.locked_specs).to eq(%w[foo-1.5.0 bar-2.1.1 qux-1.1.0].sort)
+ end
+
+ it "shows proper error when Gemfile changes forbid patch upgrades, and --patch --strict is given" do
+ # force next minor via Gemfile
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo', '1.5.0'
+ gem 'qux'
+ G
+
+ bundle "lock --update foo --patch --strict", raise_on_error: false
+
+ expect(err).to include(
+ "foo is locked to 1.4.3, while Gemfile is requesting foo (= 1.5.0). " \
+ "--strict --patch was specified, but there are no patch level upgrades from 1.4.3 satisfying foo (= 1.5.0), so version solving has failed"
+ )
+ end
+
+ context "pre" do
+ it "defaults to major" do
+ bundle "lock --update --pre"
+
+ expect(the_bundle.locked_specs).to eq(%w[foo-2.0.0.pre bar-4.0.0.pre qux-2.0.0].sort)
+ end
+
+ it "patch preferred" do
+ bundle "lock --update --patch --pre"
+
+ expect(the_bundle.locked_specs).to eq(%w[foo-1.4.5 bar-2.1.2.pre qux-1.0.1].sort)
+ end
+
+ it "minor preferred" do
+ bundle "lock --update --minor --pre"
+
+ expect(the_bundle.locked_specs).to eq(%w[foo-1.5.1 bar-3.1.0.pre qux-1.1.0].sort)
+ end
+
+ it "major preferred" do
+ bundle "lock --update --major --pre"
+
+ expect(the_bundle.locked_specs).to eq(%w[foo-2.0.0.pre bar-4.0.0.pre qux-2.0.0].sort)
+ end
+ end
+ end
+
+ context "conservative updates when minor update adds a new dependency" do
+ before do
+ build_repo4 do
+ build_gem "sequel", "5.71.0"
+ build_gem "sequel", "5.72.0" do |s|
+ s.add_dependency "bigdecimal", ">= 0"
+ end
+ build_gem "bigdecimal", %w[1.4.4 99.1.4]
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem 'sequel'
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sequel (5.71.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ sequel
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ it "adds the latest version of the new dependency" do
+ bundle "lock --minor --update sequel"
+
+ expect(the_bundle.locked_specs).to eq(%w[sequel-5.72.0 bigdecimal-99.1.4].sort)
+ end
+ end
+
+ it "updates the bundler version in the lockfile to the latest bundler version" do
+ build_repo4 do
+ build_gem "bundler", "55"
+ end
+
+ system_gems "bundler-55", gem_repo: gem_repo4
+
+ install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ source "https://gem.repo4"
+ G
+ lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, '\11.0.0\2')
+
+ bundle "lock --update --bundler --verbose", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ expect(lockfile).to end_with("BUNDLED WITH\n 55\n")
+
+ build_repo4 do
+ build_gem "bundler", "99"
+ end
+
+ bundle "lock --update --bundler --verbose", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ expect(lockfile).to end_with("BUNDLED WITH\n 99\n")
+ end
+
+ it "supports adding new platforms when there's no previous lockfile" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock --add-platform java x86-mingw32 --verbose"
+ expect(out).to include("Resolving dependencies because there's no lockfile")
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ expect(the_bundle.locked_platforms).to match_array(default_platform_list("java", "x86-mingw32"))
+ end
+
+ it "supports adding new platforms when a previous lockfile exists" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock"
+ bundle "lock --add-platform java x86-mingw32 --verbose"
+ expect(out).to include("Found changes from the lockfile, re-resolving dependencies because you are adding a new platform to your lockfile")
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ expect(the_bundle.locked_platforms).to match_array(default_platform_list("java", "x86-mingw32"))
+ end
+
+ it "supports adding new platforms, when most specific locked platform is not the current platform, and current resolve is not compatible with the target platform" do
+ simulate_platform "arm64-darwin-23" do
+ build_repo4 do
+ build_gem "foo" do |s|
+ s.platform = "arm64-darwin"
+ end
+
+ build_gem "foo" do |s|
+ s.platform = "java"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "foo"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ foo (1.0-arm64-darwin)
+
+ PLATFORMS
+ arm64-darwin
+
+ DEPENDENCIES
+ foo
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --add-platform java"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ foo (1.0-arm64-darwin)
+ foo (1.0-java)
+
+ PLATFORMS
+ arm64-darwin
+ java
+
+ DEPENDENCIES
+ foo
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "supports adding new platforms with force_ruby_platform = true" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ platform_specific (1.0)
+ platform_specific (1.0-x86-64_linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ platform_specific
+ L
+
+ bundle_config "force_ruby_platform true"
+ bundle "lock --add-platform java x86-mingw32"
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ expect(the_bundle.locked_platforms).to contain_exactly(Gem::Platform::RUBY, "x86_64-linux", "java", "x86-mingw32")
+ end
+
+ it "supports adding the `ruby` platform" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock --add-platform ruby"
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ expect(the_bundle.locked_platforms).to match_array(default_platform_list("ruby"))
+ end
+
+ it "fails when adding an unknown platform" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock --add-platform foobarbaz", raise_on_error: false
+ expect(err).to include("The platform `foobarbaz` is unknown to RubyGems and can't be added to the lockfile")
+ expect(last_command).to be_failure
+ end
+
+ it "allows removing platforms" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock --add-platform java x86-mingw32"
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ expect(the_bundle.locked_platforms).to match_array(default_platform_list("java", "x86-mingw32"))
+
+ bundle "lock --remove-platform java"
+
+ expect(the_bundle.locked_platforms).to match_array(default_platform_list("x86-mingw32"))
+ end
+
+ it "also cleans up redundant platform gems when removing platforms" do
+ build_repo4 do
+ build_gem "nokogiri", "1.12.0"
+ build_gem "nokogiri", "1.12.0" do |s|
+ s.platform = "x86_64-darwin"
+ end
+ end
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.12.0"
+ c.checksum gem_repo4, "nokogiri", "1.12.0", "x86_64-darwin"
+ end
+
+ simulate_platform "x86_64-darwin-22" do
+ install_gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.12.0)
+ nokogiri (1.12.0-x86_64-darwin)
+
+ PLATFORMS
+ ruby
+ x86_64-darwin
+
+ DEPENDENCIES
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ checksums.delete("nokogiri", Gem::Platform::RUBY)
+
+ simulate_platform "x86_64-darwin-22" do
+ bundle "lock --remove-platform ruby"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.12.0-x86_64-darwin)
+
+ PLATFORMS
+ x86_64-darwin
+
+ DEPENDENCIES
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "errors when removing all platforms" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock --remove-platform #{local_platform}", raise_on_error: false
+ expect(err).to include("Removing all platforms from the bundle is not allowed")
+ end
+
+ # from https://github.com/rubygems/bundler/issues/4896
+ it "properly adds platforms when platform requirements come from different dependencies" do
+ build_repo4 do
+ build_gem "ffi", "1.9.14"
+ build_gem "ffi", "1.9.14" do |s|
+ s.platform = "x86-mingw32"
+ end
+
+ build_gem "gssapi", "0.1"
+ build_gem "gssapi", "0.2"
+ build_gem "gssapi", "0.3"
+ build_gem "gssapi", "1.2.0" do |s|
+ s.add_dependency "ffi", ">= 1.0.1"
+ end
+
+ build_gem "mixlib-shellout", "2.2.6"
+ build_gem "mixlib-shellout", "2.2.6" do |s|
+ s.platform = "universal-mingw32"
+ s.add_dependency "win32-process", "~> 0.8.2"
+ end
+
+ # we need all these versions to get the sorting the same as it would be
+ # pulling from rubygems.org
+ %w[0.8.3 0.8.2 0.8.1 0.8.0].each do |v|
+ build_gem "win32-process", v do |s|
+ s.add_dependency "ffi", ">= 1.0.0"
+ end
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "mixlib-shellout"
+ gem "gssapi"
+ G
+
+ simulate_platform("x86-mingw32") { bundle :lock }
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "ffi", "1.9.14", "x86-mingw32"
+ c.checksum gem_repo4, "gssapi", "1.2.0"
+ c.checksum gem_repo4, "mixlib-shellout", "2.2.6", "universal-mingw32"
+ c.checksum gem_repo4, "win32-process", "0.8.3"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ ffi (1.9.14-x86-mingw32)
+ gssapi (1.2.0)
+ ffi (>= 1.0.1)
+ mixlib-shellout (2.2.6-universal-mingw32)
+ win32-process (~> 0.8.2)
+ win32-process (0.8.3)
+ ffi (>= 1.0.0)
+
+ PLATFORMS
+ x86-mingw32
+
+ DEPENDENCIES
+ gssapi
+ mixlib-shellout
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ bundle_config "force_ruby_platform true"
+ bundle :lock
+
+ checksums.checksum gem_repo4, "ffi", "1.9.14"
+ checksums.checksum gem_repo4, "mixlib-shellout", "2.2.6"
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ ffi (1.9.14)
+ ffi (1.9.14-x86-mingw32)
+ gssapi (1.2.0)
+ ffi (>= 1.0.1)
+ mixlib-shellout (2.2.6)
+ mixlib-shellout (2.2.6-universal-mingw32)
+ win32-process (~> 0.8.2)
+ win32-process (0.8.3)
+ ffi (>= 1.0.0)
+
+ PLATFORMS
+ ruby
+ x86-mingw32
+
+ DEPENDENCIES
+ gssapi
+ mixlib-shellout
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "doesn't crash when an update candidate doesn't have any matching platform" do
+ build_repo4 do
+ build_gem "libv8", "8.4.255.0"
+ build_gem "libv8", "8.4.255.0" do |s|
+ s.platform = "x86_64-darwin-19"
+ end
+
+ build_gem "libv8", "15.0.71.48.1beta2" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "libv8"
+ G
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ libv8 (8.4.255.0)
+ libv8 (8.4.255.0-x86_64-darwin-19)
+
+ PLATFORMS
+ ruby
+ x86_64-darwin-19
+
+ DEPENDENCIES
+ libv8
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ simulate_platform("x86_64-darwin-19") { bundle "lock --update" }
+
+ expect(out).to match(/Writing lockfile to.+Gemfile\.lock/)
+ end
+
+ it "adds all more specific candidates when they all have the same dependencies" do
+ build_repo4 do
+ build_gem "libv8", "8.4.255.0" do |s|
+ s.platform = "x86_64-darwin-19"
+ end
+
+ build_gem "libv8", "8.4.255.0" do |s|
+ s.platform = "x86_64-darwin-20"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "libv8"
+ G
+
+ simulate_platform("x86_64-darwin-19") { bundle "lock" }
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-19"
+ c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-20"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ libv8 (8.4.255.0-x86_64-darwin-19)
+ libv8 (8.4.255.0-x86_64-darwin-20)
+
+ PLATFORMS
+ x86_64-darwin-19
+ x86_64-darwin-20
+
+ DEPENDENCIES
+ libv8
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "respects the previous lockfile if it had a matching less specific platform already locked, and installs the best variant for each platform" do
+ build_repo4 do
+ build_gem "libv8", "8.4.255.0" do |s|
+ s.platform = "x86_64-darwin-19"
+ end
+
+ build_gem "libv8", "8.4.255.0" do |s|
+ s.platform = "x86_64-darwin-20"
+ end
+ end
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-19"
+ c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-20"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "libv8"
+ G
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ libv8 (8.4.255.0-x86_64-darwin-19)
+ libv8 (8.4.255.0-x86_64-darwin-20)
+
+ PLATFORMS
+ x86_64-darwin
+
+ DEPENDENCIES
+ libv8
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ previous_lockfile = lockfile
+
+ %w[x86_64-darwin-19 x86_64-darwin-20].each do |platform|
+ simulate_platform(platform) do
+ bundle "lock"
+ expect(lockfile).to eq(previous_lockfile)
+
+ bundle "install"
+ expect(the_bundle).to include_gem("libv8 8.4.255.0 #{platform}")
+ end
+ end
+ end
+
+ it "does not conflict on ruby requirements when adding new platforms" do
+ build_repo4 do
+ build_gem "raygun-apm", "1.0.78" do |s|
+ s.platform = "x86_64-linux"
+ s.required_ruby_version = "< #{next_ruby_minor}.dev"
+ end
+
+ build_gem "raygun-apm", "1.0.78" do |s|
+ s.platform = "universal-darwin"
+ s.required_ruby_version = "< #{next_ruby_minor}.dev"
+ end
+
+ build_gem "raygun-apm", "1.0.78" do |s|
+ s.platform = "x64-mingw-ucrt"
+ s.required_ruby_version = "< #{next_ruby_minor}.dev"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "raygun-apm"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ raygun-apm (1.0.78-universal-darwin)
+
+ PLATFORMS
+ x86_64-darwin-19
+
+ DEPENDENCIES
+ raygun-apm
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --add-platform x86_64-linux"
+ end
+
+ it "adds platform specific gems as necessary, even when adding the current platform" do
+ build_repo4 do
+ build_gem "nokogiri", "1.16.0"
+
+ build_gem "nokogiri", "1.16.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.16.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --add-platform x86_64-linux"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.16.0)
+ nokogiri (1.16.0-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "refuses to add platforms incompatible with the lockfile" do
+ build_repo4 do
+ build_gem "sorbet-static", "0.5.11989" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet-static"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet-static (0.5.11989-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ sorbet-static
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --add-platform ruby", raise_on_error: false
+ end
+
+ nice_error = <<~E.strip
+ Could not find gems matching 'sorbet-static' valid for all resolution platforms (x86_64-linux, ruby) in rubygems repository https://gem.repo4/ or installed locally.
+
+ The source contains the following gems matching 'sorbet-static':
+ * sorbet-static-0.5.11989-x86_64-linux
+ E
+ expect(err).to include(nice_error)
+ end
+
+ it "respects lower bound ruby requirements" do
+ build_repo4 do
+ build_gem "our_private_gem", "0.1.0" do |s|
+ s.required_ruby_version = ">= #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://localgemserver.test"
+
+ gem "our_private_gem"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://localgemserver.test/
+ specs:
+ our_private_gem (0.1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ our_private_gem
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ end
+
+ context "when an update is available" do
+ before do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ build_repo4 do
+ build_gem "foo", "2.0"
+ end
+
+ lockfile(expected_lockfile)
+ end
+
+ it "does not implicitly update" do
+ bundle "lock"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "actionmailer", "2.3.2"
+ c.checksum gem_repo4, "actionpack", "2.3.2"
+ c.checksum gem_repo4, "activerecord", "2.3.2"
+ c.checksum gem_repo4, "activeresource", "2.3.2"
+ c.checksum gem_repo4, "activesupport", "2.3.2"
+ c.checksum gem_repo4, "foo", "1.0"
+ c.checksum gem_repo4, "rails", "2.3.2"
+ c.checksum gem_repo4, "rake", rake_version
+ c.checksum gem_repo4, "weakling", "0.0.3"
+ end
+
+ expected_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actionmailer (2.3.2)
+ activesupport (= 2.3.2)
+ actionpack (2.3.2)
+ activesupport (= 2.3.2)
+ activerecord (2.3.2)
+ activesupport (= 2.3.2)
+ activeresource (2.3.2)
+ activesupport (= 2.3.2)
+ activesupport (2.3.2)
+ foo (1.0)
+ rails (2.3.2)
+ actionmailer (= 2.3.2)
+ actionpack (= 2.3.2)
+ activerecord (= 2.3.2)
+ activeresource (= 2.3.2)
+ rake (= #{rake_version})
+ rake (#{rake_version})
+ weakling (0.0.3)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ rails
+ weakling
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ expect(read_lockfile).to eq(expected_lockfile)
+ end
+
+ it "accounts for changes in the gemfile" do
+ gemfile gemfile.gsub('"foo"', '"foo", "2.0"')
+ bundle "lock"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "actionmailer", "2.3.2"
+ c.checksum gem_repo4, "actionpack", "2.3.2"
+ c.checksum gem_repo4, "activerecord", "2.3.2"
+ c.checksum gem_repo4, "activeresource", "2.3.2"
+ c.checksum gem_repo4, "activesupport", "2.3.2"
+ c.checksum gem_repo4, "foo", "2.0"
+ c.checksum gem_repo4, "rails", "2.3.2"
+ c.checksum gem_repo4, "rake", rake_version
+ c.checksum gem_repo4, "weakling", "0.0.3"
+ end
+
+ expected_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actionmailer (2.3.2)
+ activesupport (= 2.3.2)
+ actionpack (2.3.2)
+ activesupport (= 2.3.2)
+ activerecord (2.3.2)
+ activesupport (= 2.3.2)
+ activeresource (2.3.2)
+ activesupport (= 2.3.2)
+ activesupport (2.3.2)
+ foo (2.0)
+ rails (2.3.2)
+ actionmailer (= 2.3.2)
+ actionpack (= 2.3.2)
+ activerecord (= 2.3.2)
+ activeresource (= 2.3.2)
+ rake (= #{rake_version})
+ rake (#{rake_version})
+ weakling (0.0.3)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo (= 2.0)
+ rails
+ weakling
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ expect(read_lockfile).to eq(expected_lockfile)
+ end
+ end
+
+ context "when a system gem has incorrect dependencies, different from the lockfile" do
+ before do
+ build_repo4 do
+ build_gem "debug", "1.6.3" do |s|
+ s.add_dependency "irb", ">= 1.3.6"
+ end
+
+ build_gem "irb", "1.5.0"
+ end
+
+ system_gems "irb-1.5.0", gem_repo: gem_repo4
+ system_gems "debug-1.6.3", gem_repo: gem_repo4
+
+ # simulate gemspec with wrong empty dependencies
+ debug_gemspec_path = system_gem_path("specifications/debug-1.6.3.gemspec")
+ debug_gemspec = Gem::Specification.load(debug_gemspec_path.to_s)
+ debug_gemspec.dependencies.clear
+ File.write(debug_gemspec_path, debug_gemspec.to_ruby)
+ end
+
+ it "respects the existing lockfile, even when reresolving" do
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "debug"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "debug", "1.6.3"
+ c.checksum gem_repo4, "irb", "1.5.0"
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ debug (1.6.3)
+ irb (>= 1.3.6)
+ irb (1.5.0)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ debug
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "arm64-darwin-22" do
+ bundle "lock"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ debug (1.6.3)
+ irb (>= 1.3.6)
+ irb (1.5.0)
+
+ PLATFORMS
+ arm64-darwin-22
+ x86_64-linux
+
+ DEPENDENCIES
+ debug
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when a system gem has incorrect dependencies, different from remote gems" do
+ before do
+ build_repo4 do
+ build_gem "foo", "1.0.0" do |s|
+ s.add_dependency "bar"
+ end
+
+ build_gem "bar", "1.0.0"
+ end
+
+ system_gems "foo-1.0.0", gem_repo: gem_repo4, path: default_bundle_path
+
+ # simulate gemspec with wrong empty dependencies
+ foo_gemspec_path = default_bundle_path("specifications/foo-1.0.0.gemspec")
+ foo_gemspec = Gem::Specification.load(foo_gemspec_path.to_s)
+ foo_gemspec.dependencies.clear
+ File.write(foo_gemspec_path, foo_gemspec.to_ruby)
+ end
+
+ it "generates a lockfile using remote dependencies, and prints a warning" do
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "foo"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "foo", "1.0.0"
+ c.checksum gem_repo4, "bar", "1.0.0"
+ end
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --verbose"
+ end
+
+ expect(err).to eq("Local specification for foo-1.0.0 has different dependencies than the remote gem, ignoring it")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ bar (1.0.0)
+ foo (1.0.0)
+ bar
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ foo
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "properly shows resolution errors including OR requirements" do
+ build_repo4 do
+ build_gem "activeadmin", "2.13.1" do |s|
+ s.add_dependency "railties", ">= 6.1", "< 7.1"
+ end
+ build_gem "actionpack", "6.1.4"
+ build_gem "actionpack", "7.0.3.1"
+ build_gem "actionpack", "7.0.4"
+ build_gem "railties", "6.1.4" do |s|
+ s.add_dependency "actionpack", "6.1.4"
+ end
+ build_gem "rails", "7.0.3.1" do |s|
+ s.add_dependency "railties", "7.0.3.1"
+ end
+ build_gem "rails", "7.0.4" do |s|
+ s.add_dependency "railties", "7.0.4"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "rails", ">= 7.0.3.1"
+ gem "activeadmin", "2.13.1"
+ G
+
+ bundle "lock", raise_on_error: false
+
+ expect(err).to eq <<~ERR.strip
+ Could not find compatible versions
+
+ Because rails >= 7.0.4 depends on railties = 7.0.4
+ and rails < 7.0.4 depends on railties = 7.0.3.1,
+ railties = 7.0.3.1 OR = 7.0.4 is required.
+ So, because railties = 7.0.3.1 OR = 7.0.4 could not be found in rubygems repository https://gem.repo4/ or installed locally,
+ version solving has failed.
+ ERR
+ end
+
+ it "is able to display some explanation on crazy irresolvable cases" do
+ build_repo4 do
+ build_gem "activeadmin", "2.13.1" do |s|
+ s.add_dependency "ransack", "= 3.1.0"
+ end
+
+ # Activemodel is missing as a dependency in lockfile
+ build_gem "ransack", "3.1.0" do |s|
+ s.add_dependency "activemodel", ">= 6.0.4"
+ s.add_dependency "activesupport", ">= 6.0.4"
+ end
+
+ %w[6.0.4 7.0.2.3 7.0.3.1 7.0.4].each do |version|
+ build_gem "activesupport", version
+
+ # Activemodel is only available on 6.0.4
+ if version == "6.0.4"
+ build_gem "activemodel", version do |s|
+ s.add_dependency "activesupport", version
+ end
+ end
+
+ build_gem "rails", version do |s|
+ # Depednencies of Rails 7.0.2.3 are in reverse order
+ if version == "7.0.2.3"
+ s.add_dependency "activesupport", version
+ s.add_dependency "activemodel", version
+ else
+ s.add_dependency "activemodel", version
+ s.add_dependency "activesupport", version
+ end
+ end
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "rails", ">= 7.0.2.3"
+ gem "activeadmin", "= 2.13.1"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ activeadmin (2.13.1)
+ ransack (= 3.1.0)
+ ransack (3.1.0)
+ activemodel (>= 6.0.4)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ activeadmin (= 2.13.1)
+ ransack (= 3.1.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ expected_error = <<~ERR.strip
+ Could not find compatible versions
+
+ Because rails >= 7.0.4 depends on activemodel = 7.0.4
+ and rails >= 7.0.3.1, < 7.0.4 depends on activemodel = 7.0.3.1,
+ rails >= 7.0.3.1 requires activemodel = 7.0.3.1 OR = 7.0.4.
+ (1) So, because rails >= 7.0.2.3, < 7.0.3.1 depends on activemodel = 7.0.2.3
+ and every version of activemodel depends on activesupport = 6.0.4,
+ rails >= 7.0.2.3 requires activesupport = 6.0.4.
+
+ Because rails >= 7.0.2.3, < 7.0.3.1 depends on activesupport = 7.0.2.3
+ and rails >= 7.0.3.1, < 7.0.4 depends on activesupport = 7.0.3.1,
+ rails >= 7.0.2.3, < 7.0.4 requires activesupport = 7.0.2.3 OR = 7.0.3.1.
+ And because rails >= 7.0.4 depends on activesupport = 7.0.4,
+ rails >= 7.0.2.3 requires activesupport = 7.0.2.3 OR = 7.0.3.1 OR = 7.0.4.
+ And because rails >= 7.0.2.3 requires activesupport = 6.0.4 (1),
+ rails >= 7.0.2.3 cannot be used.
+ So, because Gemfile depends on rails >= 7.0.2.3,
+ version solving has failed.
+ ERR
+
+ bundle "lock", raise_on_error: false
+ expect(err).to eq(expected_error)
+
+ lockfile lockfile.gsub(/PLATFORMS\n #{local_platform}/m, "PLATFORMS\n #{lockfile_platforms("ruby")}")
+
+ bundle "lock", raise_on_error: false
+ expect(err).to eq(expected_error)
+ end
+
+ it "does not accidentally resolves to prereleases" do
+ build_repo4 do
+ build_gem "autoproj", "2.0.3" do |s|
+ s.add_dependency "autobuild", ">= 1.10.0.a"
+ s.add_dependency "tty-prompt"
+ end
+
+ build_gem "tty-prompt", "0.6.0"
+ build_gem "tty-prompt", "0.7.0"
+
+ build_gem "autobuild", "1.10.0.b3"
+ build_gem "autobuild", "1.10.1" do |s|
+ s.add_dependency "tty-prompt", "~> 0.6.0"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "autoproj", ">= 2.0.0"
+ G
+
+ bundle "lock"
+ expect(lockfile).to_not include("autobuild (1.10.0.b3)")
+ expect(lockfile).to include("autobuild (1.10.1)")
+ end
+
+ # Newer rails depends on Bundler, while ancient Rails does not. Bundler tries
+ # a first resolution pass that does not consider pre-releases. However, when
+ # using a pre-release Bundler (like the .dev version), that results in that
+ # pre-release being ignored and resolving to a version that does not depend on
+ # Bundler at all. We should avoid that and still consider .dev Bundler.
+ #
+ it "does not ignore prereleases with there's only one candidate" do
+ build_repo4 do
+ build_gem "rails", "7.4.0.2" do |s|
+ s.add_dependency "bundler", ">= 1.15.0"
+ end
+
+ build_gem "rails", "2.3.18"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "rails"
+ G
+
+ bundle "lock"
+ expect(lockfile).to_not include("rails (2.3.18)")
+ expect(lockfile).to include("rails (7.4.0.2)")
+ end
+
+ it "deals with platform specific incompatibilities" do
+ build_repo4 do
+ build_gem "activerecord", "6.0.6"
+ build_gem "activerecord-jdbc-adapter", "60.4" do |s|
+ s.platform = "java"
+ s.add_dependency "activerecord", "~> 6.0.0"
+ end
+ build_gem "activerecord-jdbc-adapter", "61.0" do |s|
+ s.platform = "java"
+ s.add_dependency "activerecord", "~> 6.1.0"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "activerecord", "6.0.6"
+ gem "activerecord-jdbc-adapter", "61.0"
+ G
+
+ simulate_platform "universal-java-19" do
+ bundle "lock", raise_on_error: false
+ end
+
+ expect(err).to include("Could not find compatible versions")
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+ end
+
+ it "adds checksums to an existing lockfile, when re-resolving is necessary" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.2"
+ build_gem "nokogiri", "1.14.2" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ # lockfile has a typo (nogokiri) in the dependencies section, so Bundler
+ # sees dependencies have changed, and re-resolves
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nogokiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --add-checksums"
+ end
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo4, "nokogiri", "1.14.2"
+ c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "adds checksums to an existing lockfile, when no re-resolve is necessary" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.2"
+ build_gem "nokogiri", "1.14.2" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --add-checksums"
+ end
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo4, "nokogiri", "1.14.2"
+ c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "adds checksums when source is not specified" do
+ system_gems(%w[myrack-1.0.0], path: default_bundle_path)
+
+ gemfile <<-G
+ gem "myrack"
+ G
+
+ lockfile <<~L
+ GEM
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --add-checksums"
+ end
+
+ # myrack is coming from gem_repo1
+ # but it's simulated to install in the system gems path
+ checksums = checksums_section do |c|
+ c.checksum gem_repo1, "myrack", "1.0.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "adds checksums to an existing lockfile, when gems are already installed" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.2"
+ build_gem "nokogiri", "1.14.2" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "install"
+
+ bundle "lock --add-checksums"
+ end
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo4, "nokogiri", "1.14.2"
+ c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "generates checksums by default" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.2"
+ build_gem "nokogiri", "1.14.2" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ simulate_platform "x86_64-linux" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+ end
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo4, "nokogiri", "1.14.2"
+ c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "disables checksums if configured to do so" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.2"
+ build_gem "nokogiri", "1.14.2" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ bundle_config "lockfile_checksums false"
+
+ simulate_platform "x86_64-linux" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "add checksums for gems installed on disk" do
+ build_repo4 do
+ build_gem "warning", "18.0.0"
+ end
+
+ bundle_config "lockfile_checksums false"
+
+ simulate_platform "x86_64-linux" do
+ install_gemfile(<<-G, artifice: "endpoint")
+ source "https://gem.repo4"
+
+ gem "warning"
+ G
+
+ bundle "config --delete lockfile_checksums"
+ bundle("lock --add-checksums", artifice: "endpoint")
+ end
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo4, "warning", "18.0.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ warning (18.0.0)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ warning
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "doesn't add checksum for gems not installed on disk" do
+ lockfile(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ warning (18.0.0)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ warning
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile(<<~G)
+ source "https://gem.repo4"
+
+ gem "warning"
+ G
+
+ build_repo4 do
+ build_gem "warning", "18.0.0"
+ end
+
+ FileUtils.rm_rf("#{gem_repo4}/gems")
+
+ bundle("lock --add-checksums", artifice: "endpoint")
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "warning", "18.0.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ warning (18.0.0)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ warning
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ context "when re-resolving to include prereleases" do
+ before do
+ build_repo4 do
+ build_gem "tzinfo-data", "1.2022.7"
+ build_gem "rails", "7.1.0.alpha" do |s|
+ s.add_dependency "activesupport"
+ end
+ build_gem "activesupport", "7.1.0.alpha"
+ end
+ end
+
+ it "does not end up including gems scoped to other platforms in the lockfile" do
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "rails"
+ gem "tzinfo-data", platform: :windows
+ G
+
+ simulate_platform "x86_64-darwin-22" do
+ bundle "lock"
+ end
+
+ expect(lockfile).not_to include("tzinfo-data (1.2022.7)")
+ end
+ end
+
+ context "when resolving platform specific gems as indirect dependencies on truffleruby", :truffleruby_only do
+ before do
+ build_lib "foo", path: bundled_app do |s|
+ s.add_dependency "nokogiri"
+ end
+
+ build_repo4 do
+ build_gem "nokogiri", "1.14.2"
+ build_gem "nokogiri", "1.14.2" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gemspec
+ G
+ end
+
+ it "locks both ruby and platform specific specs" do
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo4, "nokogiri", "1.14.2"
+ c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux"
+ end
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock"
+ end
+
+ expect(lockfile).to eq <<~L
+ PATH
+ remote: .
+ specs:
+ foo (1.0)
+ nokogiri
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ context "and a lockfile with platform specific gems only already exists" do
+ before do
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux"
+ end
+
+ lockfile <<~L
+ PATH
+ remote: .
+ specs:
+ foo (1.0)
+ nokogiri
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "keeps platform specific gems" do
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo4, "nokogiri", "1.14.2"
+ c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux"
+ end
+
+ simulate_platform "x86_64-linux" do
+ bundle "install"
+ end
+
+ expect(lockfile).to eq <<~L
+ PATH
+ remote: .
+ specs:
+ foo (1.0)
+ nokogiri
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+ end
+
+ context "when adding a new gem that requires unlocking other transitive deps" do
+ before do
+ build_repo4 do
+ build_gem "govuk_app_config", "0.1.0"
+
+ build_gem "govuk_app_config", "4.13.0" do |s|
+ s.add_dependency "railties", ">= 5.0"
+ end
+
+ %w[7.0.4.1 7.0.4.3].each do |v|
+ build_gem "railties", v do |s|
+ s.add_dependency "actionpack", v
+ s.add_dependency "activesupport", v
+ end
+
+ build_gem "activesupport", v
+ build_gem "actionpack", v
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "govuk_app_config"
+ gem "activesupport", "7.0.4.3"
+ G
+
+ # Simulate out of sync lockfile because top level dependency on
+ # activesuport has just been added to the Gemfile, and locked to a higher
+ # version
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actionpack (7.0.4.1)
+ activesupport (7.0.4.1)
+ govuk_app_config (4.13.0)
+ railties (>= 5.0)
+ railties (7.0.4.1)
+ actionpack (= 7.0.4.1)
+ activesupport (= 7.0.4.1)
+
+ PLATFORMS
+ arm64-darwin-22
+
+ DEPENDENCIES
+ govuk_app_config
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "does not downgrade top level dependencies" do
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "actionpack", "7.0.4.3"
+ c.no_checksum "activesupport", "7.0.4.3"
+ c.no_checksum "govuk_app_config", "4.13.0"
+ c.no_checksum "railties", "7.0.4.3"
+ end
+
+ simulate_platform "arm64-darwin-22" do
+ bundle "lock"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actionpack (7.0.4.3)
+ activesupport (7.0.4.3)
+ govuk_app_config (4.13.0)
+ railties (>= 5.0)
+ railties (7.0.4.3)
+ actionpack (= 7.0.4.3)
+ activesupport (= 7.0.4.3)
+
+ PLATFORMS
+ arm64-darwin-22
+
+ DEPENDENCIES
+ activesupport (= 7.0.4.3)
+ govuk_app_config
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when lockfile has incorrectly indented platforms" do
+ before do
+ build_repo4 do
+ build_gem "ffi", "1.1.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+
+ build_gem "ffi", "1.1.0" do |s|
+ s.platform = "arm64-darwin"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "ffi"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ ffi (1.1.0-arm64-darwin)
+
+ PLATFORMS
+ arm64-darwin
+
+ DEPENDENCIES
+ ffi
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "does not remove any gems" do
+ simulate_platform "x86_64-linux" do
+ bundle "lock --update"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ ffi (1.1.0-arm64-darwin)
+ ffi (1.1.0-x86_64-linux)
+
+ PLATFORMS
+ arm64-darwin
+ x86_64-linux
+
+ DEPENDENCIES
+ ffi
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ describe "--normalize-platforms on linux" do
+ let(:normalized_lockfile) do
+ <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ irb (1.0.0)
+ irb (1.0.0-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ irb
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ before do
+ build_repo4 do
+ build_gem "irb", "1.0.0"
+
+ build_gem "irb", "1.0.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "irb"
+ G
+ end
+
+ context "when already normalized" do
+ before do
+ lockfile normalized_lockfile
+ end
+
+ it "is a noop" do
+ simulate_platform "x86_64-linux" do
+ bundle "lock --normalize-platforms"
+ end
+
+ expect(lockfile).to eq(normalized_lockfile)
+ end
+ end
+
+ context "when not already normalized" do
+ before do
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ irb (1.0.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ irb
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "normalizes the list of platforms and native gems in the lockfile" do
+ simulate_platform "x86_64-linux" do
+ bundle "lock --normalize-platforms"
+ end
+
+ expect(lockfile).to eq(normalized_lockfile)
+ end
+ end
+ end
+
+ describe "--normalize-platforms on darwin" do
+ let(:normalized_lockfile) do
+ <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ irb (1.0.0)
+ irb (1.0.0-arm64-darwin)
+
+ PLATFORMS
+ arm64-darwin
+ ruby
+
+ DEPENDENCIES
+ irb
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ before do
+ build_repo4 do
+ build_gem "irb", "1.0.0"
+
+ build_gem "irb", "1.0.0" do |s|
+ s.platform = "arm64-darwin"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "irb"
+ G
+ end
+
+ context "when already normalized" do
+ before do
+ lockfile normalized_lockfile
+ end
+
+ it "is a noop" do
+ simulate_platform "arm64-darwin-23" do
+ bundle "lock --normalize-platforms"
+ end
+
+ expect(lockfile).to eq(normalized_lockfile)
+ end
+ end
+
+ context "when having only ruby" do
+ before do
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ irb (1.0.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ irb
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "normalizes the list of platforms and native gems in the lockfile" do
+ simulate_platform "arm64-darwin-23" do
+ bundle "lock --normalize-platforms"
+ end
+
+ expect(lockfile).to eq(normalized_lockfile)
+ end
+ end
+
+ context "when having only the current platform with version" do
+ before do
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ irb (1.0.0-arm64-darwin)
+
+ PLATFORMS
+ arm64-darwin-23
+
+ DEPENDENCIES
+ irb
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "normalizes the list of platforms by removing version" do
+ simulate_platform "arm64-darwin-23" do
+ bundle "lock --normalize-platforms"
+ end
+
+ expect(lockfile).to eq(normalized_lockfile)
+ end
+ end
+
+ context "when having other platforms with version" do
+ before do
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ irb (1.0.0-arm64-darwin)
+
+ PLATFORMS
+ arm64-darwin-22
+
+ DEPENDENCIES
+ irb
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "normalizes the list of platforms by removing version" do
+ simulate_platform "arm64-darwin-23" do
+ bundle "lock --normalize-platforms"
+ end
+
+ expect(lockfile).to eq(normalized_lockfile)
+ end
+ end
+ end
+
+ describe "--normalize-platforms with gems without generic variant" do
+ let(:original_lockfile) do
+ <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet-static (1.0-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ sorbet-static
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ before do
+ build_repo4 do
+ build_gem "sorbet-static" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet-static"
+ G
+
+ lockfile original_lockfile
+ end
+
+ it "removes invalid platforms" do
+ simulate_platform "x86_64-linux" do
+ bundle "lock --normalize-platforms"
+ end
+
+ expect(lockfile).to eq(original_lockfile.gsub(/^ ruby\n/m, ""))
+ end
+ end
+end
diff --git a/spec/bundler/commands/newgem_spec.rb b/spec/bundler/commands/newgem_spec.rb
new file mode 100644
index 0000000000..65fbad05aa
--- /dev/null
+++ b/spec/bundler/commands/newgem_spec.rb
@@ -0,0 +1,2139 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle gem" do
+ def gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to exist
+ expect(bundled_app("#{gem_name}/README.md")).to exist
+ expect(bundled_app("#{gem_name}/Gemfile")).to exist
+ expect(bundled_app("#{gem_name}/Rakefile")).to exist
+ expect(bundled_app("#{gem_name}/lib/#{gem_name}.rb")).to exist
+ expect(bundled_app("#{gem_name}/lib/#{gem_name}/version.rb")).to exist
+
+ expect(ignore_paths).to include("bin/")
+ expect(ignore_paths).to include("Gemfile")
+ end
+
+ def bundle_exec_rubocop
+ prepare_gemspec(bundled_app(gem_name, "#{gem_name}.gemspec"))
+ bundle "config set path #{rubocop_gem_path}", dir: bundled_app(gem_name)
+ bundle "exec rubocop --debug --config .rubocop.yml", dir: bundled_app(gem_name)
+ end
+
+ def bundle_exec_standardrb
+ prepare_gemspec(bundled_app(gem_name, "#{gem_name}.gemspec"))
+ bundle "config set path #{standard_gem_path}", dir: bundled_app(gem_name)
+ bundle "exec standardrb --debug", dir: bundled_app(gem_name)
+ end
+
+ def ignore_paths
+ generated = bundled_app("#{gem_name}/#{gem_name}.gemspec").read
+ matched = generated.match(/^\s+f\.start_with\?\(\*%w\[(?<ignored>.*)\]\)$/)
+ matched[:ignored]&.split(" ")
+ end
+
+ def installed_go?
+ sys_exec("go version", raise_on_error: true)
+ true
+ rescue StandardError
+ false
+ end
+
+ let(:generated_gemspec) { Bundler.load_gemspec_uncached(bundled_app(gem_name).join("#{gem_name}.gemspec")) }
+
+ let(:gem_name) { "mygem" }
+
+ before do
+ git("config --global user.name 'Bundler User'")
+ git("config --global user.email user@example.com")
+ git("config --global github.user bundleuser")
+
+ bundle_config_global "gem.mit false"
+ bundle_config_global "gem.test false"
+ bundle_config_global "gem.coc false"
+ bundle_config_global "gem.linter false"
+ bundle_config_global "gem.ci false"
+ bundle_config_global "gem.changelog false"
+ bundle_config_global "gem.bundle false"
+ end
+
+ describe "git repo initialization" do
+ it "generates a gem skeleton with a .git folder" do
+ bundle "gem #{gem_name}"
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/.git")).to exist
+ end
+
+ it "generates a gem skeleton with a .git folder when passing --git" do
+ bundle "gem #{gem_name} --git"
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/.git")).to exist
+ end
+
+ it "generates a gem skeleton without a .git folder when passing --no-git" do
+ bundle "gem #{gem_name} --no-git"
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/.git")).not_to exist
+ end
+
+ context "on a path with spaces" do
+ before do
+ Dir.mkdir(bundled_app("path with spaces"))
+ end
+
+ it "properly initializes git repo" do
+ skip "path with spaces needs special handling on Windows" if Gem.win_platform?
+
+ bundle "gem #{gem_name}", dir: bundled_app("path with spaces")
+ expect(bundled_app("path with spaces/#{gem_name}/.git")).to exist
+ end
+ end
+ end
+
+ shared_examples_for "--mit flag" do
+ before do
+ bundle "gem #{gem_name} --mit"
+ end
+ it "generates a gem skeleton with MIT license" do
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/LICENSE.txt")).to exist
+ expect(generated_gemspec.license).to eq("MIT")
+ end
+ end
+
+ shared_examples_for "--no-mit flag" do
+ before do
+ bundle "gem #{gem_name} --no-mit"
+ end
+ it "generates a gem skeleton without MIT license" do
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/LICENSE.txt")).to_not exist
+ end
+ end
+
+ shared_examples_for "--coc flag" do
+ it "generates a gem skeleton with MIT license" do
+ bundle "gem #{gem_name} --coc"
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/CODE_OF_CONDUCT.md")).to exist
+ end
+
+ it "generates the README with a section for the Code of Conduct" do
+ bundle "gem #{gem_name} --coc"
+ expect(bundled_app("#{gem_name}/README.md").read).to include("## Code of Conduct")
+ expect(bundled_app("#{gem_name}/README.md").read).to match(%r{https://github\.com/bundleuser/#{gem_name}/blob/.*/CODE_OF_CONDUCT.md})
+ end
+
+ it "generates the README with a section for the Code of Conduct, respecting the configured git default branch", git: ">= 2.28.0" do
+ git("config --global init.defaultBranch main")
+ bundle "gem #{gem_name} --coc"
+
+ expect(bundled_app("#{gem_name}/README.md").read).to include("## Code of Conduct")
+ expect(bundled_app("#{gem_name}/README.md").read).to include("https://github.com/bundleuser/#{gem_name}/blob/main/CODE_OF_CONDUCT.md")
+ end
+ end
+
+ shared_examples_for "--no-coc flag" do
+ before do
+ bundle "gem #{gem_name} --no-coc"
+ end
+ it "generates a gem skeleton without Code of Conduct" do
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/CODE_OF_CONDUCT.md")).to_not exist
+ end
+
+ it "generates the README without a section for the Code of Conduct" do
+ expect(bundled_app("#{gem_name}/README.md").read).not_to include("## Code of Conduct")
+ expect(bundled_app("#{gem_name}/README.md").read).not_to match(%r{https://github\.com/bundleuser/#{gem_name}/blob/.*/CODE_OF_CONDUCT.md})
+ end
+ end
+
+ shared_examples_for "--changelog flag" do
+ before do
+ bundle "gem #{gem_name} --changelog"
+ end
+ it "generates a gem skeleton with a CHANGELOG" do
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/CHANGELOG.md")).to exist
+ end
+ end
+
+ shared_examples_for "--no-changelog flag" do
+ before do
+ bundle "gem #{gem_name} --no-changelog"
+ end
+ it "generates a gem skeleton without a CHANGELOG" do
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/CHANGELOG.md")).to_not exist
+ end
+ end
+
+ shared_examples_for "--bundle flag" do
+ before do
+ bundle "gem #{gem_name} --bundle"
+ end
+ it "generates a gem skeleton with bundle install" do
+ gem_skeleton_assertions
+ expect(out).to include("Running bundle install in the new gem directory.")
+ end
+ end
+
+ shared_examples_for "--no-bundle flag" do
+ before do
+ bundle "gem #{gem_name} --no-bundle"
+ end
+ it "generates a gem skeleton without bundle install" do
+ gem_skeleton_assertions
+ expect(out).to_not include("Running bundle install in the new gem directory.")
+ end
+ end
+
+ shared_examples_for "--linter=rubocop flag" do
+ before do
+ bundle "gem #{gem_name} --linter=rubocop"
+ end
+
+ it "generates a gem skeleton with rubocop" do
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/Rakefile")).to read_as(
+ include("# frozen_string_literal: true").
+ and(include('require "rubocop/rake_task"').
+ and(include("RuboCop::RakeTask.new").
+ and(match(/default:.+:rubocop/))))
+ )
+ end
+
+ it "includes rubocop in generated Gemfile" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ builder = Bundler::Dsl.new
+ builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile"))
+ builder.dependencies
+ rubocop_dep = builder.dependencies.find {|d| d.name == "rubocop" }
+ expect(rubocop_dep).not_to be_specific
+ expect(rubocop_dep.requirement).to eq(Gem::Requirement.new([">= 0"]))
+ end
+
+ it "generates a default .rubocop.yml" do
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist
+ end
+
+ it "includes .rubocop.yml into ignore list" do
+ expect(ignore_paths).to include(".rubocop.yml")
+ end
+ end
+
+ shared_examples_for "--linter=standard flag" do
+ before do
+ bundle "gem #{gem_name} --linter=standard"
+ end
+
+ it "generates a gem skeleton with standard" do
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/Rakefile")).to read_as(
+ include('require "standard/rake"').
+ and(match(/default:.+:standard/))
+ )
+ end
+
+ it "includes standard in generated Gemfile" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ builder = Bundler::Dsl.new
+ builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile"))
+ builder.dependencies
+ standard_dep = builder.dependencies.find {|d| d.name == "standard" }
+ expect(standard_dep).not_to be_specific
+ expect(standard_dep.requirement).to eq(Gem::Requirement.new([">= 0"]))
+ end
+
+ it "generates a default .standard.yml" do
+ expect(bundled_app("#{gem_name}/.standard.yml")).to exist
+ end
+
+ it "includes .standard.yml into ignore list" do
+ expect(ignore_paths).to include(".standard.yml")
+ end
+ end
+
+ shared_examples_for "--no-linter flag" do
+ define_negated_matcher :exclude, :include
+
+ before do
+ bundle "gem #{gem_name} --no-linter"
+ end
+
+ it "generates a gem skeleton without rubocop" do
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/Rakefile")).to read_as(exclude("rubocop"))
+ expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to read_as(exclude("rubocop"))
+ end
+
+ it "does not include rubocop in generated Gemfile" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ builder = Bundler::Dsl.new
+ builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile"))
+ builder.dependencies
+ rubocop_dep = builder.dependencies.find {|d| d.name == "rubocop" }
+ expect(rubocop_dep).to be_nil
+ end
+
+ it "does not include standard in generated Gemfile" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ builder = Bundler::Dsl.new
+ builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile"))
+ builder.dependencies
+ standard_dep = builder.dependencies.find {|d| d.name == "standard" }
+ expect(standard_dep).to be_nil
+ end
+
+ it "doesn't generate a default .rubocop.yml" do
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist
+ end
+
+ it "does not add .rubocop.yml into ignore list" do
+ expect(ignore_paths).not_to include(".rubocop.yml")
+ end
+
+ it "doesn't generate a default .standard.yml" do
+ expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist
+ end
+
+ it "does not add .standard.yml into ignore list" do
+ expect(ignore_paths).not_to include(".standard.yml")
+ end
+ end
+
+ it "has no rubocop offenses when using --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+ bundle "gem #{gem_name} --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ it "has no rubocop offenses when using --ext=c and --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+ bundle "gem #{gem_name} --ext=c --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ it "has no rubocop offenses when using --ext=c, --test=minitest, and --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+ bundle "gem #{gem_name} --ext=c --test=minitest --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ it "has no rubocop offenses when using --ext=c, --test=rspec, and --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+ bundle "gem #{gem_name} --ext=c --test=rspec --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ it "has no rubocop offenses when using --ext=c, --test=test-unit, and --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+ bundle "gem #{gem_name} --ext=c --test=test-unit --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ it "has no standard offenses when using --linter=standard flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+ bundle "gem #{gem_name} --linter=standard"
+ bundle_exec_standardrb
+ expect(last_command).to be_success
+ end
+
+ it "has no rubocop offenses when using --ext=rust and --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+
+ bundle "gem #{gem_name} --ext=rust --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ it "has no rubocop offenses when using --ext=rust, --test=minitest, and --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+
+ bundle "gem #{gem_name} --ext=rust --test=minitest --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ it "has no rubocop offenses when using --ext=rust, --test=rspec, and --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+
+ bundle "gem #{gem_name} --ext=rust --test=rspec --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ it "has no rubocop offenses when using --ext=rust, --test=test-unit, and --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+
+ bundle "gem #{gem_name} --ext=rust --test=test-unit --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ shared_examples_for "CI config is absent" do
+ it "does not create any CI files" do
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist
+ end
+ end
+
+ shared_examples_for "test framework is absent" do
+ it "does not create any test framework files" do
+ expect(bundled_app("#{gem_name}/.rspec")).to_not exist
+ expect(bundled_app("#{gem_name}/spec/#{gem_name}_spec.rb")).to_not exist
+ expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to_not exist
+ expect(bundled_app("#{gem_name}/test/#{gem_name}.rb")).to_not exist
+ expect(bundled_app("#{gem_name}/test/test_helper.rb")).to_not exist
+ end
+
+ it "does not add any test framework files into ignore list" do
+ expect(ignore_paths).not_to include("test/")
+ expect(ignore_paths).not_to include(".rspec")
+ expect(ignore_paths).not_to include("spec/")
+ end
+ end
+
+ context "README.md" do
+ context "git config github.user present" do
+ before do
+ bundle "gem #{gem_name}"
+ end
+
+ it "contribute URL set to git username" do
+ expect(bundled_app("#{gem_name}/README.md").read).not_to include("[USERNAME]")
+ expect(bundled_app("#{gem_name}/README.md").read).to include("github.com/bundleuser")
+ end
+ end
+
+ context "git config github.user is absent" do
+ before do
+ git("config --global --unset github.user")
+ bundle "gem #{gem_name}"
+ end
+
+ it "contribute URL set to [USERNAME]" do
+ expect(bundled_app("#{gem_name}/README.md").read).to include("[USERNAME]")
+ expect(bundled_app("#{gem_name}/README.md").read).not_to include("github.com/bundleuser")
+ end
+ end
+
+ describe "test task name on readme" do
+ shared_examples_for "test task name on readme" do |framework, task_name|
+ before do
+ bundle "gem #{gem_name} --test=#{framework}"
+ end
+
+ it "renders with correct name" do
+ expect(bundled_app("#{gem_name}/README.md").read).to include("Then, run `rake #{task_name}` to run the tests.")
+ end
+ end
+
+ it_behaves_like "test task name on readme", "test-unit", "test"
+ it_behaves_like "test task name on readme", "minitest", "test"
+ it_behaves_like "test task name on readme", "rspec", "spec"
+ end
+ end
+
+ it "creates a new git repository" do
+ bundle "gem #{gem_name}"
+ expect(bundled_app("#{gem_name}/.git")).to exist
+ end
+
+ context "when git is not available" do
+ # This spec cannot have `git` available in the test env
+ before do
+ bundle "gem #{gem_name}", env: { "PATH" => "" }
+ end
+
+ it "creates the gem without the need for git" do
+ expect(bundled_app("#{gem_name}/README.md")).to exist
+ end
+
+ it "doesn't create a git repo" do
+ expect(bundled_app("#{gem_name}/.git")).to_not exist
+ end
+
+ it "doesn't create a .gitignore file" do
+ expect(bundled_app("#{gem_name}/.gitignore")).to_not exist
+ end
+
+ it "does not add .gitignore into ignore list" do
+ expect(ignore_paths).not_to include(".gitignore")
+ end
+ end
+
+ it "generates a valid gemspec" do
+ bundle "gem newgem --bin"
+
+ prepare_gemspec(bundled_app("newgem", "newgem.gemspec"))
+
+ build_repo2 do
+ build_dummy_irb "9.9.9"
+ end
+ gems = ["rake-#{rake_version}", "irb-9.9.9"]
+ system_gems gems, path: system_gem_path, gem_repo: gem_repo2
+ bundle "exec rake build", dir: bundled_app("newgem")
+
+ expect(stdboth).not_to include("ERROR")
+ end
+
+ context "gem naming with relative paths" do
+ it "resolves ." do
+ create_temporary_dir("tmp")
+
+ bundle "gem .", dir: bundled_app("tmp")
+
+ expect(bundled_app("tmp/lib/tmp.rb")).to exist
+ end
+
+ it "resolves .." do
+ create_temporary_dir("temp/empty_dir")
+
+ bundle "gem ..", dir: bundled_app("temp/empty_dir")
+
+ expect(bundled_app("temp/lib/temp.rb")).to exist
+ end
+
+ it "resolves relative directory" do
+ create_temporary_dir("tmp/empty/tmp")
+
+ bundle "gem ../../empty", dir: bundled_app("tmp/empty/tmp")
+
+ expect(bundled_app("tmp/empty/lib/empty.rb")).to exist
+ end
+
+ def create_temporary_dir(dir)
+ FileUtils.mkdir_p(bundled_app(dir))
+ end
+ end
+
+ shared_examples_for "--github-username option" do |github_username|
+ before do
+ bundle "gem #{gem_name} --github-username=#{github_username}"
+ end
+
+ it "generates a gem skeleton" do
+ gem_skeleton_assertions
+ end
+
+ it "contribute URL set to given github username" do
+ expect(bundled_app("#{gem_name}/README.md").read).not_to include("[USERNAME]")
+ expect(bundled_app("#{gem_name}/README.md").read).to include("github.com/#{github_username}")
+ end
+ end
+
+ shared_examples_for "github_username configuration" do
+ context "with github_username setting set to some value" do
+ before do
+ bundle_config_global "gem.github_username different_username"
+ bundle "gem #{gem_name}"
+ end
+
+ it "generates a gem skeleton" do
+ gem_skeleton_assertions
+ end
+
+ it "contribute URL set to bundle config setting" do
+ expect(bundled_app("#{gem_name}/README.md").read).not_to include("[USERNAME]")
+ expect(bundled_app("#{gem_name}/README.md").read).to include("github.com/different_username")
+ end
+ end
+
+ context "with github_username setting set to false" do
+ before do
+ bundle_config_global "gem.github_username false"
+ bundle "gem #{gem_name}"
+ end
+
+ it "generates a gem skeleton" do
+ gem_skeleton_assertions
+ end
+
+ it "contribute URL set to [USERNAME]" do
+ expect(bundled_app("#{gem_name}/README.md").read).to include("[USERNAME]")
+ expect(bundled_app("#{gem_name}/README.md").read).not_to include("github.com/bundleuser")
+ end
+ end
+ end
+
+ it "generates a gem skeleton" do
+ bundle "gem #{gem_name}"
+
+ expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to exist
+ expect(bundled_app("#{gem_name}/Gemfile")).to exist
+ expect(bundled_app("#{gem_name}/Rakefile")).to exist
+ expect(bundled_app("#{gem_name}/lib/#{gem_name}.rb")).to exist
+ expect(bundled_app("#{gem_name}/lib/#{gem_name}/version.rb")).to exist
+ expect(bundled_app("#{gem_name}/sig/#{gem_name}.rbs")).to exist
+ expect(bundled_app("#{gem_name}/.gitignore")).to exist
+
+ expect(bundled_app("#{gem_name}/bin/setup")).to exist
+ expect(bundled_app("#{gem_name}/bin/console")).to exist
+
+ unless Gem.win_platform?
+ expect(bundled_app("#{gem_name}/bin/setup")).to be_executable
+ expect(bundled_app("#{gem_name}/bin/console")).to be_executable
+ end
+
+ expect(bundled_app("#{gem_name}/bin/setup").read).to start_with("#!")
+ expect(bundled_app("#{gem_name}/bin/console").read).to start_with("#!")
+ end
+
+ it "includes bin/ into ignore list" do
+ bundle "gem #{gem_name}"
+
+ expect(ignore_paths).to include("bin/")
+ end
+
+ it "includes Gemfile into ignore list" do
+ bundle "gem #{gem_name}"
+
+ expect(ignore_paths).to include("Gemfile")
+ end
+
+ it "includes .gitignore into ignore list" do
+ bundle "gem #{gem_name}"
+
+ expect(ignore_paths).to include(".gitignore")
+ end
+
+ it "starts with version 0.1.0" do
+ bundle "gem #{gem_name}"
+
+ expect(bundled_app("#{gem_name}/lib/#{gem_name}/version.rb").read).to match(/VERSION = "0.1.0"/)
+ end
+
+ it "declare String type for VERSION constant" do
+ bundle "gem #{gem_name}"
+
+ expect(bundled_app("#{gem_name}/sig/#{gem_name}.rbs").read).to match(/VERSION: String/)
+ end
+
+ context "git config user.{name,email} is set" do
+ before do
+ bundle "gem #{gem_name}"
+ end
+
+ it "sets gemspec author to git user.name if available" do
+ expect(generated_gemspec.authors.first).to eq("Bundler User")
+ end
+
+ it "sets gemspec email to git user.email if available" do
+ expect(generated_gemspec.email.first).to eq("user@example.com")
+ end
+ end
+
+ context "git config user.{name,email} is not set" do
+ before do
+ git("config --global --unset user.name")
+ git("config --global --unset user.email")
+ bundle "gem #{gem_name}"
+ end
+
+ it "sets gemspec author to default message if git user.name is not set or empty" do
+ expect(generated_gemspec.authors.first).to eq("TODO: Write your name")
+ end
+
+ it "sets gemspec email to default message if git user.email is not set or empty" do
+ expect(generated_gemspec.email.first).to eq("TODO: Write your email address")
+ end
+ end
+
+ it "sets gemspec metadata['allowed_push_host']" do
+ bundle "gem #{gem_name}"
+
+ expect(generated_gemspec.metadata["allowed_push_host"]).
+ to match(/example\.com/)
+ end
+
+ it "includes a commented-out rubygems_mfa_required metadata hint" do
+ bundle "gem #{gem_name}"
+
+ gemspec_contents = bundled_app("#{gem_name}/#{gem_name}.gemspec").read
+
+ expect(gemspec_contents).to include('# spec.metadata["rubygems_mfa_required"] = "true"')
+ expect(gemspec_contents).to include("https://guides.rubygems.org/mfa-requirement-opt-in/")
+ end
+
+ it "sets a minimum ruby version" do
+ bundle "gem #{gem_name}"
+
+ expect(generated_gemspec.required_ruby_version.to_s).to start_with(">=")
+ end
+
+ it "does not include the gemspec file in files" do
+ bundle "gem #{gem_name}"
+
+ bundler_gemspec = Bundler::GemHelper.new(bundled_app(gem_name), gem_name).gemspec
+
+ expect(bundler_gemspec.files).not_to include("#{gem_name}.gemspec")
+ end
+
+ it "does not include the Gemfile file in files" do
+ bundle "gem #{gem_name}"
+
+ bundler_gemspec = Bundler::GemHelper.new(bundled_app(gem_name), gem_name).gemspec
+
+ expect(bundler_gemspec.files).not_to include("Gemfile")
+ end
+
+ it "runs rake without problems" do
+ bundle "gem #{gem_name}"
+
+ system_gems ["rake-#{rake_version}"]
+
+ rakefile = <<~RAKEFILE
+ task :default do
+ puts 'SUCCESS'
+ end
+ RAKEFILE
+ File.open(bundled_app("#{gem_name}/Rakefile"), "w") do |file|
+ file.puts rakefile
+ end
+
+ sys_exec("rake", dir: bundled_app(gem_name))
+ expect(out).to include("SUCCESS")
+ end
+
+ context "--exe parameter set" do
+ before do
+ bundle "gem #{gem_name} --exe"
+ end
+
+ it "builds exe skeleton" do
+ expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist
+ unless Gem.win_platform?
+ expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to be_executable
+ end
+ end
+ end
+
+ context "--bin parameter set" do
+ before do
+ bundle "gem #{gem_name} --bin"
+ end
+
+ it "builds exe skeleton" do
+ expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist
+ end
+ end
+
+ context "no --test parameter" do
+ before do
+ bundle "gem #{gem_name}"
+ end
+
+ it_behaves_like "test framework is absent"
+ end
+
+ context "--test parameter set to rspec" do
+ before do
+ bundle "gem #{gem_name} --test=rspec"
+ end
+
+ it "builds spec skeleton" do
+ expect(bundled_app("#{gem_name}/.rspec")).to exist
+ expect(bundled_app("#{gem_name}/spec/#{gem_name}_spec.rb")).to exist
+ expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist
+ end
+
+ it "includes .rspec and spec/ into ignore list" do
+ expect(ignore_paths).to include(".rspec")
+ expect(ignore_paths).to include("spec/")
+ end
+
+ it "depends on a non-specific version of rspec in generated Gemfile" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ builder = Bundler::Dsl.new
+ builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile"))
+ builder.dependencies
+ rspec_dep = builder.dependencies.find {|d| d.name == "rspec" }
+ expect(rspec_dep).not_to be_specific
+ expect(rspec_dep.requirement).to eq(Gem::Requirement.new([">= 0"]))
+ end
+ end
+
+ context "init_gems_rb setting to true" do
+ before do
+ bundle_config "init_gems_rb true"
+ bundle "gem #{gem_name}"
+ end
+
+ it "generates gems.rb instead of Gemfile" do
+ expect(bundled_app("#{gem_name}/gems.rb")).to exist
+ expect(bundled_app("#{gem_name}/Gemfile")).to_not exist
+ end
+
+ it "includes gems.rb and gems.locked into ignore list" do
+ expect(ignore_paths).to include("gems.rb")
+ expect(ignore_paths).to include("gems.locked")
+ expect(ignore_paths).not_to include("Gemfile")
+ end
+ end
+
+ context "init_gems_rb setting to false" do
+ before do
+ bundle_config "init_gems_rb false"
+ bundle "gem #{gem_name}"
+ end
+
+ it "generates Gemfile instead of gems.rb" do
+ expect(bundled_app("#{gem_name}/gems.rb")).to_not exist
+ expect(bundled_app("#{gem_name}/Gemfile")).to exist
+ end
+
+ it "includes Gemfile into ignore list" do
+ expect(ignore_paths).to include("Gemfile")
+ expect(ignore_paths).not_to include("gems.rb")
+ expect(ignore_paths).not_to include("gems.locked")
+ end
+ end
+
+ context "gem.test setting set to rspec" do
+ before do
+ bundle_config "gem.test rspec"
+ bundle "gem #{gem_name}"
+ end
+
+ it "builds spec skeleton" do
+ expect(bundled_app("#{gem_name}/.rspec")).to exist
+ expect(bundled_app("#{gem_name}/spec/#{gem_name}_spec.rb")).to exist
+ expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist
+ end
+
+ it "includes .rspec and spec/ into ignore list" do
+ expect(ignore_paths).to include(".rspec")
+ expect(ignore_paths).to include("spec/")
+ end
+ end
+
+ context "gem.test setting set to rspec and --test is set to minitest" do
+ before do
+ bundle_config "gem.test rspec"
+ bundle "gem #{gem_name} --test=minitest"
+ end
+
+ it "builds spec skeleton" do
+ expect(bundled_app("#{gem_name}/test/test_#{gem_name}.rb")).to exist
+ expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist
+ end
+
+ it "includes test/ into ignore list" do
+ expect(ignore_paths).to include("test/")
+ end
+ end
+
+ context "--test parameter set to minitest" do
+ before do
+ bundle "gem #{gem_name} --test=minitest"
+ end
+
+ it "depends on a non-specific version of minitest" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ builder = Bundler::Dsl.new
+ builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile"))
+ builder.dependencies
+ minitest_dep = builder.dependencies.find {|d| d.name == "minitest" }
+ expect(minitest_dep).not_to be_specific
+ expect(minitest_dep.requirement).to eq(Gem::Requirement.new([">= 0"]))
+ end
+
+ it "builds spec skeleton" do
+ expect(bundled_app("#{gem_name}/test/test_#{gem_name}.rb")).to exist
+ expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist
+ end
+
+ it "includes test/ into ignore list" do
+ expect(ignore_paths).to include("test/")
+ end
+
+ it "creates a default rake task to run the test suite" do
+ rakefile = <<~RAKEFILE
+ # frozen_string_literal: true
+
+ require "bundler/gem_tasks"
+ require "minitest/test_task"
+
+ Minitest::TestTask.create
+
+ task default: :test
+ RAKEFILE
+
+ expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile)
+ end
+ end
+
+ context "gem.test setting set to minitest" do
+ before do
+ bundle_config "gem.test minitest"
+ bundle "gem #{gem_name}"
+ end
+
+ it "creates a default rake task to run the test suite" do
+ rakefile = <<~RAKEFILE
+ # frozen_string_literal: true
+
+ require "bundler/gem_tasks"
+ require "minitest/test_task"
+
+ Minitest::TestTask.create
+
+ task default: :test
+ RAKEFILE
+
+ expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile)
+ end
+ end
+
+ context "--test parameter set to test-unit" do
+ before do
+ bundle "gem #{gem_name} --test=test-unit"
+ end
+
+ it "depends on a non-specific version of test-unit" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ builder = Bundler::Dsl.new
+ builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile"))
+ builder.dependencies
+ test_unit_dep = builder.dependencies.find {|d| d.name == "test-unit" }
+ expect(test_unit_dep).not_to be_specific
+ expect(test_unit_dep.requirement).to eq(Gem::Requirement.new([">= 0"]))
+ end
+
+ it "builds spec skeleton" do
+ expect(bundled_app("#{gem_name}/test/#{gem_name}_test.rb")).to exist
+ expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist
+ end
+
+ it "includes test/ into ignore list" do
+ expect(ignore_paths).to include("test/")
+ end
+
+ it "creates a default rake task to run the test suite" do
+ rakefile = <<~RAKEFILE
+ # frozen_string_literal: true
+
+ require "bundler/gem_tasks"
+ require "rake/testtask"
+
+ Rake::TestTask.new(:test) do |t|
+ t.libs << "test"
+ t.libs << "lib"
+ t.test_files = FileList["test/**/*_test.rb"]
+ end
+
+ task default: :test
+ RAKEFILE
+
+ expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile)
+ end
+ end
+
+ context "--test parameter set to an invalid value" do
+ before do
+ bundle "gem #{gem_name} --test=foo", raise_on_error: false
+ end
+
+ it "fails loudly" do
+ expect(last_command).to be_failure
+ expect(err).to match(/Expected '--test' to be one of .*; got foo/)
+ end
+ end
+
+ context "gem.test set to rspec and --test with no arguments" do
+ before do
+ bundle_config "gem.test rspec"
+ bundle "gem #{gem_name} --test"
+ end
+
+ it "builds spec skeleton" do
+ expect(bundled_app("#{gem_name}/.rspec")).to exist
+ expect(bundled_app("#{gem_name}/spec/#{gem_name}_spec.rb")).to exist
+ expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist
+ end
+
+ it "includes .rspec and spec/ into ignore list" do
+ expect(ignore_paths).to include(".rspec")
+ expect(ignore_paths).to include("spec/")
+ end
+
+ it "hints that --test is already configured" do
+ expect(out).to match("rspec is already configured, ignoring --test flag.")
+ end
+ end
+
+ context "gem.test setting set to false and --test with no arguments", :readline do
+ before do
+ bundle_config "gem.test false"
+ bundle "gem #{gem_name} --test" do |input, _, _|
+ input.puts
+ end
+ end
+
+ it "asks to generate test files" do
+ expect(out).to match("Do you want to generate tests with your gem?")
+ end
+
+ it "hints that the choice will only be applied to the current gem" do
+ expect(out).to match("Your choice will only be applied to this gem.")
+ end
+
+ it_behaves_like "test framework is absent"
+ end
+
+ context "gem.test setting not set and --test with no arguments", :readline do
+ before do
+ bundle_config_global "BUNDLE_GEM__TEST" => nil
+ bundle "gem #{gem_name} --test" do |input, _, _|
+ input.puts
+ end
+ end
+
+ it "asks to generate test files" do
+ expect(out).to match("Do you want to generate tests with your gem?")
+ end
+
+ it "hints that the choice will be applied to future bundle gem calls" do
+ hint = "Future `bundle gem` calls will use your choice. " \
+ "This setting can be changed anytime with `bundle config gem.test`."
+ expect(out).to match(hint)
+ end
+
+ it_behaves_like "test framework is absent"
+ end
+
+ context "gem.test setting set to a test framework and --no-test" do
+ before do
+ bundle_config "gem.test rspec"
+ bundle "gem #{gem_name} --no-test"
+ end
+
+ it_behaves_like "test framework is absent"
+ end
+
+ context "--ci with no argument" do
+ before do
+ bundle "gem #{gem_name}"
+ end
+
+ it "does not generate any CI config" do
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist
+ end
+
+ it "does not add any CI config files into ignore list" do
+ expect(ignore_paths).not_to include(".github/")
+ expect(ignore_paths).not_to include(".gitlab-ci.yml")
+ expect(ignore_paths).not_to include(".circleci/")
+ end
+ end
+
+ context "--ci set to github" do
+ before do
+ bundle "gem #{gem_name} --ci=github"
+ end
+
+ it "generates a GitHub Actions config file" do
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist
+ end
+
+ it "includes .github/ into ignore list" do
+ expect(ignore_paths).to include(".github/")
+ end
+ end
+
+ context "--ci set to gitlab" do
+ before do
+ bundle "gem #{gem_name} --ci=gitlab"
+ end
+
+ it "generates a GitLab CI config file" do
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist
+ end
+
+ it "includes .gitlab-ci.yml into ignore list" do
+ expect(ignore_paths).to include(".gitlab-ci.yml")
+ end
+ end
+
+ context "--ci set to circle" do
+ before do
+ bundle "gem #{gem_name} --ci=circle"
+ end
+
+ it "generates a CircleCI config file" do
+ expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist
+ end
+
+ it "includes .circleci/ into ignore list" do
+ expect(ignore_paths).to include(".circleci/")
+ end
+ end
+
+ context "--ci set to an invalid value" do
+ before do
+ bundle "gem #{gem_name} --ci=foo", raise_on_error: false
+ end
+
+ it "fails loudly" do
+ expect(last_command).to be_failure
+ expect(err).to match(/Expected '--ci' to be one of .*; got foo/)
+ end
+ end
+
+ context "gem.ci setting set to none" do
+ it "doesn't generate any CI config" do
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist
+ end
+ end
+
+ context "gem.ci setting set to github" do
+ it "generates a GitHub Actions config file" do
+ bundle_config "gem.ci github"
+ bundle "gem #{gem_name}"
+
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist
+ end
+ end
+
+ context "gem.ci setting set to gitlab" do
+ it "generates a GitLab CI config file" do
+ bundle_config "gem.ci gitlab"
+ bundle "gem #{gem_name}"
+
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist
+ end
+ end
+
+ context "gem.ci setting set to circle" do
+ it "generates a CircleCI config file" do
+ bundle_config "gem.ci circle"
+ bundle "gem #{gem_name}"
+
+ expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist
+ end
+ end
+
+ context "gem.ci set to github and --ci with no arguments" do
+ before do
+ bundle_config "gem.ci github"
+ bundle "gem #{gem_name} --ci"
+ end
+
+ it "generates a GitHub Actions config file" do
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist
+ end
+
+ it "hints that --ci is already configured" do
+ expect(out).to match("github is already configured, ignoring --ci flag.")
+ end
+ end
+
+ context "gem.ci setting set to false and --ci with no arguments", :readline do
+ before do
+ bundle_config "gem.ci false"
+ bundle "gem #{gem_name} --ci" do |input, _, _|
+ input.puts "github"
+ end
+ end
+
+ it "asks to setup CI" do
+ expect(out).to match("Do you want to set up continuous integration for your gem?")
+ end
+
+ it "hints that the choice will only be applied to the current gem" do
+ expect(out).to match("Your choice will only be applied to this gem.")
+ end
+ end
+
+ context "gem.ci setting not set and --ci with no arguments", :readline do
+ before do
+ bundle_config_global "BUNDLE_GEM__CI" => nil
+ bundle "gem #{gem_name} --ci" do |input, _, _|
+ input.puts "github"
+ end
+ end
+
+ it "asks to setup CI" do
+ expect(out).to match("Do you want to set up continuous integration for your gem?")
+ end
+
+ it "hints that the choice will be applied to future bundle gem calls" do
+ hint = "Future `bundle gem` calls will use your choice. " \
+ "This setting can be changed anytime with `bundle config gem.ci`."
+ expect(out).to match(hint)
+ end
+ end
+
+ context "gem.ci setting set to a CI service and --no-ci" do
+ before do
+ bundle_config "gem.ci github"
+ bundle "gem #{gem_name} --no-ci"
+ end
+
+ it "does not generate any CI config" do
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist
+ end
+ end
+
+ context "--linter with no argument" do
+ before do
+ bundle "gem #{gem_name}"
+ end
+
+ it "does not generate any linter config" do
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist
+ end
+
+ it "does not add any linter config files into ignore list" do
+ expect(ignore_paths).not_to include(".rubocop.yml")
+ expect(ignore_paths).not_to include(".standard.yml")
+ end
+ end
+
+ context "--linter set to rubocop" do
+ before do
+ bundle "gem #{gem_name} --linter=rubocop"
+ end
+
+ it "generates a RuboCop config" do
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist
+ expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist
+ end
+
+ it "includes .rubocop.yml into ignore list" do
+ expect(ignore_paths).to include(".rubocop.yml")
+ expect(ignore_paths).not_to include(".standard.yml")
+ end
+ end
+
+ context "--linter set to standard" do
+ before do
+ bundle "gem #{gem_name} --linter=standard"
+ end
+
+ it "generates a Standard config" do
+ expect(bundled_app("#{gem_name}/.standard.yml")).to exist
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist
+ end
+
+ it "includes .standard.yml into ignore list" do
+ expect(ignore_paths).to include(".standard.yml")
+ expect(ignore_paths).not_to include(".rubocop.yml")
+ end
+ end
+
+ context "--linter set to an invalid value" do
+ before do
+ bundle "gem #{gem_name} --linter=foo", raise_on_error: false
+ end
+
+ it "fails loudly" do
+ expect(last_command).to be_failure
+ expect(err).to match(/Expected '--linter' to be one of .*; got foo/)
+ end
+ end
+
+ context "gem.linter setting set to none" do
+ before do
+ bundle "gem #{gem_name}"
+ end
+
+ it "doesn't generate any linter config" do
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist
+ end
+
+ it "does not add any linter config files into ignore list" do
+ expect(ignore_paths).not_to include(".rubocop.yml")
+ expect(ignore_paths).not_to include(".standard.yml")
+ end
+ end
+
+ context "gem.linter setting set to rubocop" do
+ before do
+ bundle_config "gem.linter rubocop"
+ bundle "gem #{gem_name}"
+ end
+
+ it "generates a RuboCop config file" do
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist
+ end
+
+ it "includes .rubocop.yml into ignore list" do
+ expect(ignore_paths).to include(".rubocop.yml")
+ end
+ end
+
+ context "gem.linter setting set to standard" do
+ before do
+ bundle_config "gem.linter standard"
+ bundle "gem #{gem_name}"
+ end
+
+ it "generates a Standard config file" do
+ expect(bundled_app("#{gem_name}/.standard.yml")).to exist
+ end
+
+ it "includes .standard.yml into ignore list" do
+ expect(ignore_paths).to include(".standard.yml")
+ end
+ end
+
+ context "gem.linter set to rubocop and --linter with no arguments" do
+ before do
+ bundle_config "gem.linter rubocop"
+ bundle "gem #{gem_name} --linter"
+ end
+
+ it "generates a RuboCop config file" do
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist
+ end
+
+ it "includes .rubocop.yml into ignore list" do
+ expect(ignore_paths).to include(".rubocop.yml")
+ end
+
+ it "hints that --linter is already configured" do
+ expect(out).to match("rubocop is already configured, ignoring --linter flag.")
+ end
+ end
+
+ context "gem.linter setting set to false and --linter with no arguments", :readline do
+ before do
+ bundle_config "gem.linter false"
+ bundle "gem #{gem_name} --linter" do |input, _, _|
+ input.puts "rubocop"
+ end
+ end
+
+ it "asks to setup a linter" do
+ expect(out).to match("Do you want to add a code linter and formatter to your gem?")
+ end
+
+ it "hints that the choice will only be applied to the current gem" do
+ expect(out).to match("Your choice will only be applied to this gem.")
+ end
+ end
+
+ context "gem.linter setting not set and --linter with no arguments", :readline do
+ before do
+ bundle_config_global "BUNDLE_GEM__LINTER" => nil
+ bundle "gem #{gem_name} --linter" do |input, _, _|
+ input.puts "rubocop"
+ end
+ end
+
+ it "asks to setup a linter" do
+ expect(out).to match("Do you want to add a code linter and formatter to your gem?")
+ end
+
+ it "hints that the choice will be applied to future bundle gem calls" do
+ hint = "Future `bundle gem` calls will use your choice. " \
+ "This setting can be changed anytime with `bundle config gem.linter`."
+ expect(out).to match(hint)
+ end
+ end
+
+ context "gem.linter setting set to a linter and --no-linter" do
+ before do
+ bundle_config "gem.linter rubocop"
+ bundle "gem #{gem_name} --no-linter"
+ end
+
+ it "does not generate any linter config" do
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist
+ end
+
+ it "does not add any linter config files into ignore list" do
+ expect(ignore_paths).not_to include(".rubocop.yml")
+ expect(ignore_paths).not_to include(".standard.yml")
+ end
+ end
+
+ context "--edit option" do
+ it "opens the generated gemspec in the user's text editor" do
+ output = bundle "gem #{gem_name} --edit=echo"
+ gemspec_path = File.join(bundled_app, gem_name, "#{gem_name}.gemspec")
+ expect(output).to include("echo \"#{gemspec_path}\"")
+ end
+ end
+
+ shared_examples_for "paths that depend on gem name" do
+ it "generates entrypoint, version file and signatures file at the proper path, with the proper content" do
+ bundle "gem #{gem_name}"
+
+ expect(bundled_app("#{gem_name}/lib/#{require_path}.rb")).to exist
+ expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(%r{require_relative "#{require_relative_path}/version"})
+ expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/class Error < StandardError; end$/)
+
+ expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb")).to exist
+ expect(bundled_app("#{gem_name}/sig/#{require_path}.rbs")).to exist
+ end
+
+ context "--exe parameter set" do
+ before do
+ bundle "gem #{gem_name} --exe"
+ end
+
+ it "builds an exe file that requires the proper entrypoint" do
+ expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist
+ expect(bundled_app("#{gem_name}/exe/#{gem_name}").read).to match(/require "#{require_path}"/)
+ end
+ end
+
+ context "--bin parameter set" do
+ before do
+ bundle "gem #{gem_name} --bin"
+ end
+
+ it "builds an exe file that requires the proper entrypoint" do
+ expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist
+ expect(bundled_app("#{gem_name}/exe/#{gem_name}").read).to match(/require "#{require_path}"/)
+ end
+ end
+
+ context "--test parameter set to rspec" do
+ before do
+ bundle "gem #{gem_name} --test=rspec"
+ end
+
+ it "builds a spec helper that requires the proper entrypoint, and a default test in the proper path which fails" do
+ expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist
+ expect(bundled_app("#{gem_name}/spec/spec_helper.rb").read).to include(%(require "#{require_path}"))
+ expect(bundled_app("#{gem_name}/spec/#{require_path}_spec.rb")).to exist
+ expect(bundled_app("#{gem_name}/spec/#{require_path}_spec.rb").read).to include("expect(false).to eq(true)")
+ end
+ end
+
+ context "--test parameter set to minitest" do
+ before do
+ bundle "gem #{gem_name} --test=minitest"
+ end
+
+ it "builds a test helper that requires the proper entrypoint, and default test file in the proper path that defines the proper test class name, requires helper, and fails" do
+ expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist
+ expect(bundled_app("#{gem_name}/test/test_helper.rb").read).to include(%(require "#{require_path}"))
+
+ expect(bundled_app("#{gem_name}/#{minitest_test_file_path}")).to exist
+ expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include(minitest_test_class_name)
+ expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include(%(require "test_helper"))
+ expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include("assert false")
+ end
+ end
+
+ context "--test parameter set to test-unit" do
+ before do
+ bundle "gem #{gem_name} --test=test-unit"
+ end
+
+ it "builds a test helper that requires the proper entrypoint, and default test file in the proper path which requires helper and fails" do
+ expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist
+ expect(bundled_app("#{gem_name}/test/test_helper.rb").read).to include(%(require "#{require_path}"))
+ expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb")).to exist
+ expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb").read).to include(%(require "test_helper"))
+ expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb").read).to include("assert_equal(\"expected\", \"actual\")")
+ end
+ end
+ end
+
+ context "with mit option in bundle config settings set to true" do
+ before do
+ bundle_config_global "gem.mit true"
+ end
+ it_behaves_like "--mit flag"
+ it_behaves_like "--no-mit flag"
+ end
+
+ context "with mit option in bundle config settings set to false" do
+ before do
+ bundle_config_global "gem.mit false"
+ end
+ it_behaves_like "--mit flag"
+ it_behaves_like "--no-mit flag"
+ end
+
+ context "with coc option in bundle config settings set to true" do
+ before do
+ bundle_config_global "gem.coc true"
+ end
+ it_behaves_like "--coc flag"
+ it_behaves_like "--no-coc flag"
+ end
+
+ context "with coc option in bundle config settings set to false" do
+ before do
+ bundle_config_global "gem.coc false"
+ end
+ it_behaves_like "--coc flag"
+ it_behaves_like "--no-coc flag"
+ end
+
+ context "with rubocop option in bundle config settings set to true" do
+ before do
+ bundle_config_global "gem.rubocop true"
+ end
+ it_behaves_like "--linter=rubocop flag"
+ it_behaves_like "--linter=standard flag"
+ it_behaves_like "--no-linter flag"
+ end
+
+ context "with rubocop option in bundle config settings set to false" do
+ before do
+ bundle_config_global "gem.rubocop false"
+ end
+ it_behaves_like "--linter=rubocop flag"
+ it_behaves_like "--linter=standard flag"
+ it_behaves_like "--no-linter flag"
+ end
+
+ context "with linter option in bundle config settings set to rubocop" do
+ before do
+ bundle_config_global "gem.linter rubocop"
+ end
+ it_behaves_like "--linter=rubocop flag"
+ it_behaves_like "--linter=standard flag"
+ it_behaves_like "--no-linter flag"
+ end
+
+ context "with linter option in bundle config settings set to standard" do
+ before do
+ bundle_config_global "gem.linter standard"
+ end
+ it_behaves_like "--linter=rubocop flag"
+ it_behaves_like "--linter=standard flag"
+ it_behaves_like "--no-linter flag"
+ end
+
+ context "with linter option in bundle config settings set to false" do
+ before do
+ bundle_config_global "gem.linter false"
+ end
+ it_behaves_like "--linter=rubocop flag"
+ it_behaves_like "--linter=standard flag"
+ it_behaves_like "--no-linter flag"
+ end
+
+ context "with changelog option in bundle config settings set to true" do
+ before do
+ bundle_config_global "gem.changelog true"
+ end
+ it_behaves_like "--changelog flag"
+ it_behaves_like "--no-changelog flag"
+ end
+
+ context "with changelog option in bundle config settings set to false" do
+ before do
+ bundle_config_global "gem.changelog false"
+ end
+ it_behaves_like "--changelog flag"
+ it_behaves_like "--no-changelog flag"
+ end
+
+ context "with bundle option in bundle config settings set to true" do
+ before do
+ bundle_config_global "gem.bundle true"
+ end
+ it_behaves_like "--bundle flag"
+ it_behaves_like "--no-bundle flag"
+
+ it "runs bundle install" do
+ bundle "gem #{gem_name}"
+ expect(out).to include("Running bundle install in the new gem directory.")
+ end
+ end
+
+ context "with bundle option in bundle config settings set to false" do
+ before do
+ bundle_config_global "gem.bundle false"
+ end
+ it_behaves_like "--bundle flag"
+ it_behaves_like "--no-bundle flag"
+
+ it "does not run bundle install" do
+ bundle "gem #{gem_name}"
+ expect(out).to_not include("Running bundle install in the new gem directory.")
+ end
+ end
+
+ context "without git config github.user set" do
+ before do
+ git("config --global --unset github.user")
+ end
+ context "with github-username option in bundle config settings set to some value" do
+ before do
+ bundle_config_global "gem.github_username different_username"
+ end
+ it_behaves_like "--github-username option", "gh_user"
+ end
+
+ it_behaves_like "github_username configuration"
+
+ context "with github-username option in bundle config settings set to false" do
+ before do
+ bundle_config_global "gem.github_username false"
+ end
+ it_behaves_like "--github-username option", "gh_user"
+ end
+
+ context "when changelog is enabled" do
+ it "sets gemspec changelog_uri, homepage, homepage_uri, source_code_uri to TODOs" do
+ bundle "gem #{gem_name} --changelog"
+
+ expect(generated_gemspec.metadata["changelog_uri"]).
+ to eq("TODO: Put your gem's CHANGELOG.md URL here.")
+ expect(generated_gemspec.homepage).to eq("TODO: Put your gem's website or public repo URL here.")
+ expect(generated_gemspec.metadata["homepage_uri"]).to eq("TODO: Put your gem's website or public repo URL here.")
+ expect(generated_gemspec.metadata["source_code_uri"]).to eq("TODO: Put your gem's public repo URL here.")
+ end
+ end
+
+ context "when changelog is not enabled" do
+ it "sets gemspec homepage, homepage_uri, source_code_uri to TODOs and changelog_uri to nil" do
+ bundle "gem #{gem_name}"
+
+ expect(generated_gemspec.metadata["changelog_uri"]).to be_nil
+ expect(generated_gemspec.homepage).to eq("TODO: Put your gem's website or public repo URL here.")
+ expect(generated_gemspec.metadata["homepage_uri"]).to eq("TODO: Put your gem's website or public repo URL here.")
+ expect(generated_gemspec.metadata["source_code_uri"]).to eq("TODO: Put your gem's public repo URL here.")
+ end
+ end
+ end
+
+ context "with git config github.user set" do
+ context "with github-username option in bundle config settings set to some value" do
+ before do
+ bundle_config_global "gem.github_username different_username"
+ end
+ it_behaves_like "--github-username option", "gh_user"
+ end
+
+ it_behaves_like "github_username configuration"
+
+ context "with github-username option in bundle config settings set to false" do
+ before do
+ bundle_config_global "gem.github_username false"
+ end
+ it_behaves_like "--github-username option", "gh_user"
+ end
+
+ context "when changelog is enabled" do
+ it "sets gemspec changelog_uri, homepage, homepage_uri, source_code_uri based on git username" do
+ bundle "gem #{gem_name} --changelog"
+
+ expect(generated_gemspec.metadata["changelog_uri"]).
+ to eq("https://github.com/bundleuser/#{gem_name}/blob/main/CHANGELOG.md")
+ expect(generated_gemspec.homepage).to eq("https://github.com/bundleuser/#{gem_name}")
+ expect(generated_gemspec.metadata["homepage_uri"]).to eq("https://github.com/bundleuser/#{gem_name}")
+ expect(generated_gemspec.metadata["source_code_uri"]).to eq("https://github.com/bundleuser/#{gem_name}")
+ end
+ end
+
+ context "when changelog is not enabled" do
+ it "sets gemspec source_code_uri, homepage, homepage_uri but not changelog_uri" do
+ bundle "gem #{gem_name}"
+
+ expect(generated_gemspec.metadata["changelog_uri"]).to be_nil
+ expect(generated_gemspec.homepage).to eq("https://github.com/bundleuser/#{gem_name}")
+ expect(generated_gemspec.metadata["homepage_uri"]).to eq("https://github.com/bundleuser/#{gem_name}")
+ expect(generated_gemspec.metadata["source_code_uri"]).to eq("https://github.com/bundleuser/#{gem_name}")
+ end
+ end
+ end
+
+ context "standard gem naming" do
+ let(:require_path) { gem_name }
+
+ let(:require_relative_path) { gem_name }
+
+ let(:minitest_test_file_path) { "test/test_#{gem_name}.rb" }
+
+ let(:minitest_test_class_name) { "class TestMygem < Minitest::Test" }
+
+ include_examples "paths that depend on gem name"
+ end
+
+ context "gem naming with underscore" do
+ let(:gem_name) { "test_gem" }
+
+ let(:require_path) { "test_gem" }
+
+ let(:require_relative_path) { "test_gem" }
+
+ let(:minitest_test_file_path) { "test/test_test_gem.rb" }
+
+ let(:minitest_test_class_name) { "class TestTestGem < Minitest::Test" }
+
+ let(:flags) { nil }
+
+ it "does not nest constants" do
+ bundle ["gem", gem_name, flags].compact.join(" ")
+ expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb").read).to match(/module TestGem/)
+ expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/module TestGem/)
+ end
+
+ include_examples "paths that depend on gem name"
+
+ context "--ext parameter set with C" do
+ let(:flags) { "--ext=c" }
+
+ before do
+ bundle ["gem", gem_name, flags].compact.join(" ")
+ end
+
+ it "builds ext skeleton" do
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.h")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c")).to exist
+ end
+
+ it "generates native extension loading code" do
+ expect(bundled_app("#{gem_name}/lib/#{gem_name}.rb").read).to include(<<~RUBY)
+ require_relative "test_gem/version"
+ require "#{gem_name}/#{gem_name}"
+ RUBY
+ end
+
+ it "includes rake-compiler, but no Rust related changes" do
+ expect(bundled_app("#{gem_name}/Gemfile").read).to include('gem "rake-compiler"')
+
+ expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to_not include('spec.add_dependency "rb_sys"')
+ expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to_not include('spec.required_rubygems_version = ">= ')
+ end
+
+ it "depends on compile task for build" do
+ rakefile = <<~RAKEFILE
+ # frozen_string_literal: true
+
+ require "bundler/gem_tasks"
+ require "rake/extensiontask"
+
+ task build: :compile
+
+ GEMSPEC = Gem::Specification.load("#{gem_name}.gemspec")
+
+ Rake::ExtensionTask.new("#{gem_name}", GEMSPEC) do |ext|
+ ext.lib_dir = "lib/#{gem_name}"
+ end
+
+ task default: %i[clobber compile]
+ RAKEFILE
+
+ expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile)
+ end
+ end
+
+ context "--ext parameter set with rust" do
+ let(:flags) { "--ext=rust" }
+
+ before do
+ bundle ["gem", gem_name, flags].compact.join(" ")
+ end
+
+ it "is not deprecated" do
+ expect(err).not_to include "[DEPRECATED] Option `--ext` without explicit value is deprecated."
+ end
+
+ it "builds ext skeleton" do
+ expect(bundled_app("#{gem_name}/Cargo.toml")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/Cargo.toml")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/src/lib.rs")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/build.rs")).to exist
+ end
+
+ it "includes rake-compiler and rb_sys gems constraint" do
+ expect(bundled_app("#{gem_name}/Gemfile").read).to include('gem "rake-compiler"')
+ expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "rb_sys"')
+ end
+
+ it "depends on compile task for build" do
+ rakefile = <<~RAKEFILE
+ # frozen_string_literal: true
+
+ require "bundler/gem_tasks"
+ require "rb_sys/extensiontask"
+
+ task build: :compile
+
+ GEMSPEC = Gem::Specification.load("#{gem_name}.gemspec")
+
+ RbSys::ExtensionTask.new("#{gem_name}", GEMSPEC) do |ext|
+ ext.lib_dir = "lib/#{gem_name}"
+ end
+
+ task default: :compile
+ RAKEFILE
+
+ expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile)
+ end
+
+ it "configures the crate such that `cargo test` works", :ruby_repo, :mri_only do
+ env = setup_rust_env
+ gem_path = bundled_app(gem_name)
+ result = sys_exec("cargo test", env: env, dir: gem_path, timeout: 300)
+
+ expect(result).to include("1 passed")
+ end
+
+ def setup_rust_env
+ skip "rust toolchain of mingw is broken" if RUBY_PLATFORM.match?("mingw")
+
+ env = {
+ "CARGO_HOME" => ENV.fetch("CARGO_HOME", File.join(ENV["HOME"], ".cargo")),
+ "RUSTUP_HOME" => ENV.fetch("RUSTUP_HOME", File.join(ENV["HOME"], ".rustup")),
+ "RUSTUP_TOOLCHAIN" => ENV.fetch("RUSTUP_TOOLCHAIN", "stable"),
+ }
+
+ system(env, "cargo", "-V", out: IO::NULL, err: [:child, :out])
+ skip "cargo not present" unless $?.success?
+ # Hermetic Cargo setup
+ RbConfig::CONFIG.each {|k, v| env["RBCONFIG_#{k}"] = v }
+ env
+ end
+ end
+
+ context "--ext parameter set with go" do
+ let(:flags) { "--ext=go" }
+
+ before do
+ bundle ["gem", gem_name, flags].compact.join(" ")
+ end
+
+ after do
+ sys_exec("go clean -modcache", raise_on_error: true) if installed_go?
+ end
+
+ it "is not deprecated" do
+ expect(err).not_to include "[DEPRECATED] Option `--ext` without explicit value is deprecated."
+ end
+
+ it "builds ext skeleton" do
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.h")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod")).to exist
+ end
+
+ it "includes extconf.rb in gem_name.gemspec" do
+ expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include(%(spec.extensions = ["ext/#{gem_name}/extconf.rb"]))
+ end
+
+ it "includes go_gem in gem_name.gemspec" do
+ expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "go_gem", ">= 0.2"')
+ end
+
+ it "includes go_gem extension in extconf.rb" do
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(<<~RUBY)
+ require "mkmf"
+ require "go_gem/mkmf"
+ RUBY
+
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(%(create_go_makefile("#{gem_name}/#{gem_name}")))
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).not_to include("create_makefile")
+ end
+
+ it "includes go_gem extension in gem_name.c" do
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c").read).to eq(<<~C)
+ #include "#{gem_name}.h"
+ #include "_cgo_export.h"
+ C
+ end
+
+ it "includes skeleton code in gem_name.go" do
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO)
+ /*
+ #include "#{gem_name}.h"
+
+ VALUE rb_#{gem_name}_sum(VALUE self, VALUE a, VALUE b);
+ */
+ import "C"
+ GO
+
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO)
+ //export rb_#{gem_name}_sum
+ func rb_#{gem_name}_sum(_ C.VALUE, a C.VALUE, b C.VALUE) C.VALUE {
+ GO
+
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO)
+ //export Init_#{gem_name}
+ func Init_#{gem_name}() {
+ GO
+ end
+
+ it "includes valid module name in go.mod" do
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/bundleuser/#{gem_name}")
+ end
+
+ it "includes go_gem extension in Rakefile" do
+ expect(bundled_app("#{gem_name}/Rakefile").read).to include(<<~RUBY)
+ require "go_gem/rake_task"
+
+ GoGem::RakeTask.new("#{gem_name}")
+ RUBY
+ end
+
+ context "with --no-ci" do
+ let(:flags) { "--ext=go --no-ci" }
+
+ it_behaves_like "CI config is absent"
+ end
+
+ context "--ci set to github" do
+ let(:flags) { "--ext=go --ci=github" }
+
+ it "generates .github/workflows/main.yml" do
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml").read).to include("go-version-file: ext/#{gem_name}/go.mod")
+ end
+ end
+
+ context "--ci set to circle" do
+ let(:flags) { "--ext=go --ci=circle" }
+
+ it "generates a .circleci/config.yml" do
+ expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist
+
+ expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML.strip)
+ environment:
+ GO_VERSION:
+ YAML
+
+ expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML)
+ - run:
+ name: Install Go
+ command: |
+ wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz
+ tar -C /usr/local -xzf /tmp/go.tar.gz
+ echo 'export PATH=/usr/local/go/bin:"$PATH"' >> "$BASH_ENV"
+ YAML
+ end
+ end
+
+ context "--ci set to gitlab" do
+ let(:flags) { "--ext=go --ci=gitlab" }
+
+ it "generates a .gitlab-ci.yml" do
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist
+
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML)
+ - wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz
+ - tar -C /usr/local -xzf /tmp/go.tar.gz
+ - export PATH=/usr/local/go/bin:$PATH
+ YAML
+
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML.strip)
+ variables:
+ GO_VERSION:
+ YAML
+ end
+ end
+
+ context "without github.user" do
+ before do
+ # FIXME: GitHub Actions Windows Runner hang up here for some reason...
+ skip "Workaround for hung up" if Gem.win_platform?
+
+ git("config --global --unset github.user")
+ bundle ["gem", gem_name, flags].compact.join(" ")
+ end
+
+ it "includes valid module name in go.mod" do
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/username/#{gem_name}")
+ end
+ end
+ end
+ end
+
+ context "gem naming with dashed" do
+ let(:gem_name) { "test-gem" }
+
+ let(:require_path) { "test/gem" }
+
+ let(:require_relative_path) { "gem" }
+
+ let(:minitest_test_file_path) { "test/test/test_gem.rb" }
+
+ let(:minitest_test_class_name) { "class Test::TestGem < Minitest::Test" }
+
+ it "nests constants so they work" do
+ bundle "gem #{gem_name}"
+ expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb").read).to match(/module Test\n module Gem/)
+ expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/module Test\n module Gem/)
+ end
+
+ include_examples "paths that depend on gem name"
+ end
+
+ describe "uncommon gem names" do
+ it "can deal with two dashes" do
+ bundle "gem a--a"
+
+ expect(bundled_app("a--a/a--a.gemspec")).to exist
+ end
+
+ it "fails gracefully with a ." do
+ bundle "gem foo.gemspec", raise_on_error: false
+ expect(err).to end_with("Invalid gem name foo.gemspec -- `Foo.gemspec` is an invalid constant name")
+ end
+
+ it "fails gracefully with a ^" do
+ bundle "gem ^", raise_on_error: false
+ expect(err).to end_with("Invalid gem name ^ -- `^` is an invalid constant name")
+ end
+
+ it "fails gracefully with a space" do
+ bundle "gem 'foo bar'", raise_on_error: false
+ expect(err).to end_with("Invalid gem name foo bar -- `Foo bar` is an invalid constant name")
+ end
+
+ it "fails gracefully when multiple names are passed" do
+ bundle "gem foo bar baz", raise_on_error: false
+ expect(err).to eq(<<-E.strip)
+ERROR: "bundle gem" was called with arguments ["foo", "bar", "baz"]
+Usage: "bundle gem NAME [OPTIONS]"
+ E
+ end
+ end
+
+ describe "#ensure_safe_gem_name" do
+ before do
+ bundle "gem #{subject}", raise_on_error: false
+ end
+
+ context "with an existing const name" do
+ subject { "gem" }
+ it { expect(err).to include("Invalid gem name #{subject}") }
+ end
+
+ context "with an existing hyphenated const name" do
+ subject { "gem-specification" }
+ it { expect(err).to include("Invalid gem name #{subject}") }
+ end
+
+ context "starting with a number" do
+ subject { "1gem" }
+ it { expect(err).to include("Invalid gem name #{subject}") }
+ end
+
+ context "including capital letter" do
+ subject { "CAPITAL" }
+ it "should warn but not error" do
+ expect(err).to include("Gem names with capital letters are not recommended")
+ expect(bundled_app("#{subject}/#{subject}.gemspec")).to exist
+ end
+ end
+
+ context "starting with an existing const name" do
+ subject { "gem-somenewconstantname" }
+ it { expect(err).not_to include("Invalid gem name #{subject}") }
+ end
+
+ context "ending with an existing const name" do
+ subject { "somenewconstantname-gem" }
+ it { expect(err).not_to include("Invalid gem name #{subject}") }
+ end
+ end
+
+ context "on first run", :readline do
+ it "asks about test framework" do
+ bundle_config_global "BUNDLE_GEM__TEST" => nil
+
+ bundle "gem foobar" do |input, _, _|
+ input.puts "rspec"
+ end
+
+ expect(bundled_app("foobar/spec/spec_helper.rb")).to exist
+ rakefile = <<~RAKEFILE
+ # frozen_string_literal: true
+
+ require "bundler/gem_tasks"
+ require "rspec/core/rake_task"
+
+ RSpec::Core::RakeTask.new(:spec)
+
+ task default: :spec
+ RAKEFILE
+
+ expect(bundled_app("foobar/Rakefile").read).to eq(rakefile)
+ expect(bundled_app("foobar/Gemfile").read).to include('gem "rspec"')
+ end
+
+ it "asks about CI service" do
+ bundle_config_global "BUNDLE_GEM__CI" => nil
+
+ bundle "gem foobar" do |input, _, _|
+ input.puts "github"
+ end
+
+ expect(bundled_app("foobar/.github/workflows/main.yml")).to exist
+ end
+
+ it "asks about MIT license just once" do
+ bundle_config_global "BUNDLE_GEM__MIT" => nil
+
+ bundle "config list"
+
+ bundle "gem foobar" do |input, _, _|
+ input.puts "yes"
+ end
+
+ expect(bundled_app("foobar/LICENSE.txt")).to exist
+ expect(out).to include("Using a MIT license means").once
+ end
+
+ it "asks about CoC just once" do
+ bundle_config_global "BUNDLE_GEM__COC" => nil
+
+ bundle "gem foobar" do |input, _, _|
+ input.puts "yes"
+ end
+
+ expect(bundled_app("foobar/CODE_OF_CONDUCT.md")).to exist
+ expect(out).to include("Codes of conduct can increase contributions to your project").once
+ end
+
+ it "asks about CHANGELOG just once" do
+ bundle_config_global "BUNDLE_GEM__CHANGELOG" => nil
+
+ bundle "gem foobar" do |input, _, _|
+ input.puts "yes"
+ end
+
+ expect(bundled_app("foobar/CHANGELOG.md")).to exist
+ expect(out).to include("A changelog is a file which contains").once
+ end
+ end
+
+ context "on conflicts with a previously created file" do
+ it "should fail gracefully" do
+ FileUtils.touch(bundled_app("conflict-foobar"))
+ bundle "gem conflict-foobar", raise_on_error: false
+ expect(err).to eq("Couldn't create a new gem named `conflict-foobar` because there's an existing file named `conflict-foobar`.")
+ expect(exitstatus).to eql(32)
+ end
+ end
+
+ context "on conflicts with a previously created directory" do
+ it "should succeed" do
+ FileUtils.mkdir_p(bundled_app("conflict-foobar/Gemfile"))
+ bundle "gem conflict-foobar"
+ expect(out).to include("file_clash conflict-foobar/Gemfile").
+ and include "Initializing git repo in #{bundled_app("conflict-foobar")}"
+ end
+ end
+end
diff --git a/spec/bundler/commands/open_spec.rb b/spec/bundler/commands/open_spec.rb
new file mode 100644
index 0000000000..664dc58919
--- /dev/null
+++ b/spec/bundler/commands/open_spec.rb
@@ -0,0 +1,175 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle open" do
+ context "when opening a regular gem" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+ end
+
+ it "opens the gem with BUNDLER_EDITOR as highest priority" do
+ bundle "open rails", env: { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" }
+ expect(out).to include("bundler_editor #{default_bundle_path("gems", "rails-2.3.2")}")
+ end
+
+ it "opens the gem with VISUAL as 2nd highest priority" do
+ bundle "open rails", env: { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("visual #{default_bundle_path("gems", "rails-2.3.2")}")
+ end
+
+ it "opens the gem with EDITOR as 3rd highest priority" do
+ bundle "open rails", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("editor #{default_bundle_path("gems", "rails-2.3.2")}")
+ end
+
+ it "complains if no EDITOR is set" do
+ bundle "open rails", env: { "EDITOR" => "", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to eq("To open a bundled gem, set $EDITOR or $BUNDLER_EDITOR")
+ end
+
+ it "complains if gem not in bundle" do
+ bundle "open missing", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false
+ expect(err).to match(/could not find gem 'missing'/i)
+ end
+
+ it "does not blow up if the gem to open does not have a Gemfile" do
+ git = build_git "foo"
+ ref = git.ref_for("main", 11)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle "open foo", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("editor #{default_bundle_path("bundler", "gems", "foo-1.0-#{ref}")}")
+ end
+
+ it "suggests alternatives for similar-sounding gems" do
+ bundle "open Rails", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false
+ expect(err).to match(/did you mean 'rails'\?/i)
+ end
+
+ it "opens the gem with short words" do
+ bundle "open rec", env: { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" }
+
+ expect(out).to include("bundler_editor #{default_bundle_path("gems", "activerecord-2.3.2")}")
+ end
+
+ it "opens subpath of the gem" do
+ bundle "open activerecord --path lib/activerecord", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("editor #{default_bundle_path("gems", "activerecord-2.3.2")}/lib/activerecord")
+ end
+
+ it "opens subpath file of the gem" do
+ bundle "open activerecord --path lib/version.rb", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("editor #{default_bundle_path("gems", "activerecord-2.3.2")}/lib/version.rb")
+ end
+
+ it "opens deep subpath of the gem" do
+ bundle "open activerecord --path lib/active_record", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("editor #{default_bundle_path("gems", "activerecord-2.3.2")}/lib/active_record")
+ end
+
+ it "requires value for --path arg" do
+ bundle "open activerecord --path", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false
+ expect(err).to eq "Cannot specify `--path` option without a value"
+ end
+
+ it "suggests alternatives for similar-sounding gems when using subpath" do
+ bundle "open Rails --path README.md", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false
+ expect(err).to match(/did you mean 'rails'\?/i)
+ end
+
+ it "suggests alternatives for similar-sounding gems when using deep subpath" do
+ bundle "open Rails --path some/path/here", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false
+ expect(err).to match(/did you mean 'rails'\?/i)
+ end
+
+ it "opens subpath of the short worded gem" do
+ bundle "open rec --path CHANGELOG.md", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("editor #{default_bundle_path("gems", "activerecord-2.3.2")}/CHANGELOG.md")
+ end
+
+ it "opens deep subpath of the short worded gem" do
+ bundle "open rec --path lib/activerecord", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("editor #{default_bundle_path("gems", "activerecord-2.3.2")}/lib/activerecord")
+ end
+
+ it "opens subpath of the selected matching gem", :readline do
+ env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" }
+ bundle "open active --path CHANGELOG.md", env: env do |input, _, _|
+ input.puts "2"
+ end
+
+ expect(out).to include("bundler_editor #{default_bundle_path("gems", "activerecord-2.3.2").join("CHANGELOG.md")}")
+ end
+
+ it "opens deep subpath of the selected matching gem", :readline do
+ env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" }
+ bundle "open active --path lib/activerecord/version.rb", env: env do |input, _, _|
+ input.puts "2"
+ end
+
+ expect(out).to include("bundler_editor #{default_bundle_path("gems", "activerecord-2.3.2").join("lib", "activerecord", "version.rb")}")
+ end
+
+ it "select the gem from many match gems", :readline do
+ env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" }
+ bundle "open active", env: env do |input, _, _|
+ input.puts "2"
+ end
+
+ expect(out).to include("bundler_editor #{default_bundle_path("gems", "activerecord-2.3.2")}")
+ end
+
+ it "allows selecting exit from many match gems", :readline do
+ env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" }
+ bundle "open active", env: env do |input, _, _|
+ input.puts "0"
+ end
+ end
+
+ it "performs an automatic bundle install" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ gem "foo"
+ G
+
+ bundle_config "auto_install 1"
+ bundle "open rails", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("Installing foo 1.0")
+ end
+
+ it "opens the editor with a clean env" do
+ bundle "open", env: { "EDITOR" => "sh -c 'env'", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false
+ expect(out).not_to include("BUNDLE_GEMFILE=")
+ end
+ end
+
+ context "when opening a default gem" do
+ let(:default_gems) do
+ ruby(<<-RUBY).split("\n")
+ if Gem::Specification.is_a?(Enumerable)
+ puts Gem::Specification.select(&:default_gem?).map(&:name)
+ end
+ RUBY
+ end
+
+ before do
+ skip "No default gems available on this test run" if default_gems.empty?
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ G
+ end
+
+ it "throws proper error when trying to open default gem" do
+ bundle "open json", env: { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" }
+ expect(out).to include("Unable to open json because it's a default gem, so the directory it would normally be installed to does not exist.")
+ end
+ end
+end
diff --git a/spec/bundler/commands/outdated_spec.rb b/spec/bundler/commands/outdated_spec.rb
new file mode 100644
index 0000000000..28ed51d61e
--- /dev/null
+++ b/spec/bundler/commands/outdated_spec.rb
@@ -0,0 +1,1369 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle outdated" do
+ describe "with no arguments" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+ end
+
+ it "returns a sorted list of outdated gems" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ build_gem "weakling", "0.2"
+ update_git "foo", path: lib_path("foo")
+ update_git "zebra", path: lib_path("zebra")
+ end
+
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE.gsub("x", "\\\h").tr(".", "\.").strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.5 3.0 = 2.3.5 default
+ foo 1.0 xxxxxxx 1.0 xxxxxxx >= 0 default
+ weakling 0.0.3 0.2 ~> 0.0.1 default
+ zebra 1.0 xxxxxxx 1.0 xxxxxxx >= 0 default
+ TABLE
+
+ expect(out).to match(Regexp.new(expected_output))
+ end
+
+ it "excludes header row from the sorting" do
+ update_repo2 do
+ build_gem "AAA", %w[1.0.0 2.0.0]
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "AAA", "1.0.0"
+ G
+
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE
+ Gem Current Latest Requested Groups Release Date
+ AAA 1.0.0 2.0.0 = 1.0.0 default
+ TABLE
+
+ expect(out).to include(expected_output.strip)
+ end
+
+ it "returns non zero exit status if outdated gems present" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ update_git "foo", path: lib_path("foo")
+ end
+
+ bundle "outdated", raise_on_error: false
+
+ expect(exitstatus).to_not be_zero
+ end
+
+ it "returns success exit status if no outdated gems present" do
+ bundle "outdated"
+ end
+
+ it "adds gem group to dependency output when repo is updated" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "terranova", '8'
+
+ group :development, :test do
+ gem 'activesupport', '2.3.5'
+ end
+ G
+
+ update_repo2 { build_gem "activesupport", "3.0" }
+ update_repo2 { build_gem "terranova", "9" }
+
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.5 3.0 = 2.3.5 development, test
+ terranova 8 9 = 8 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ describe "with --verbose option" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+ end
+
+ it "shows the location of the latest version's gemspec if installed" do
+ bundle_config "clean false"
+
+ update_repo2 { build_gem "activesupport", "3.0" }
+ update_repo2 { build_gem "terranova", "9" }
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "terranova", '9'
+ gem 'activesupport', '2.3.5'
+ G
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "terranova", '8'
+ gem 'activesupport', '2.3.5'
+ G
+
+ bundle "outdated --verbose", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date Path
+ activesupport 2.3.5 3.0 = 2.3.5 default
+ terranova 8 9 = 8 default #{default_bundle_path("specifications/terranova-9.gemspec")}
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ describe "with --group option" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "weakling", "~> 0.0.1"
+ gem "terranova", '8'
+ group :development, :test do
+ gem "duradura", '7.0'
+ gem 'activesupport', '2.3.5'
+ end
+ G
+ end
+
+ def test_group_option(group)
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ build_gem "terranova", "9"
+ build_gem "duradura", "8.0"
+ end
+
+ bundle "outdated --group #{group}", raise_on_error: false
+ end
+
+ it "works when the bundle is up to date" do
+ bundle "outdated --group"
+ expect(out).to end_with("Bundle up to date!")
+ end
+
+ it "works when only out of date gems are not in given group" do
+ update_repo2 do
+ build_gem "terranova", "9"
+ end
+ bundle "outdated --group development"
+ expect(out).to end_with("Bundle up to date!")
+ end
+
+ it "returns a sorted list of outdated gems from one group => 'default'" do
+ test_group_option("default")
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ terranova 8 9 = 8 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "returns a sorted list of outdated gems from one group => 'development'" do
+ test_group_option("development")
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.5 3.0 = 2.3.5 development, test
+ duradura 7.0 8.0 = 7.0 development, test
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "returns a sorted list of outdated gems from one group => 'test'" do
+ test_group_option("test")
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.5 3.0 = 2.3.5 development, test
+ duradura 7.0 8.0 = 7.0 development, test
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ describe "with --groups option and outdated transitive dependencies" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+
+ build_gem "bar", %w[2.0.0]
+
+ build_gem "bar_dependant", "7.0" do |s|
+ s.add_dependency "bar", "~> 2.0"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "bar_dependant", '7.0'
+ G
+
+ update_repo2 do
+ build_gem "bar", %w[3.0.0]
+ end
+ end
+
+ it "returns a sorted list of outdated gems" do
+ bundle "outdated --groups", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ bar 2.0.0 3.0.0
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ describe "with --groups option" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "weakling", "~> 0.0.1"
+ gem "terranova", '8'
+ group :development, :test do
+ gem 'activesupport', '2.3.5'
+ gem "duradura", '7.0'
+ end
+ G
+ end
+
+ it "not outdated gems" do
+ bundle "outdated --groups"
+ expect(out).to end_with("Bundle up to date!")
+ end
+
+ it "returns a sorted list of outdated gems by groups" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ build_gem "terranova", "9"
+ build_gem "duradura", "8.0"
+ end
+
+ bundle "outdated --groups", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.5 3.0 = 2.3.5 development, test
+ duradura 7.0 8.0 = 7.0 development, test
+ terranova 8 9 = 8 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ describe "with --local option" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "weakling", "~> 0.0.1"
+ gem "terranova", '8'
+ group :development, :test do
+ gem 'activesupport', '2.3.5'
+ gem "duradura", '7.0'
+ end
+ G
+ end
+
+ it "uses local cache to return a list of outdated gems" do
+ update_repo2 do
+ build_gem "activesupport", "2.3.4"
+ end
+
+ bundle_config "clean false"
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "2.3.4"
+ G
+
+ bundle "outdated --local", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.4 2.3.5 = 2.3.4 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "doesn't hit repo2" do
+ FileUtils.rm_r(gem_repo2)
+
+ bundle "outdated --local"
+ expect(out).not_to match(/Fetching (gem|version|dependency) metadata from/)
+ end
+ end
+
+ shared_examples_for "a minimal output is desired" do
+ context "and gems are outdated" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+
+ build_gem "activesupport", "3.0"
+ build_gem "weakling", "0.2"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+ end
+
+ it "outputs a sorted list of outdated gems with a more minimal format to stdout" do
+ minimal_output = "activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5)\n" \
+ "weakling (newest 0.2, installed 0.0.3, requested ~> 0.0.1)"
+ subject
+ expect(out).to eq(minimal_output)
+ end
+
+ it "outputs progress to stderr" do
+ subject
+ expect(err).to include("Fetching gem metadata")
+ end
+ end
+
+ context "and no gems are outdated" do
+ before do
+ build_repo2 do
+ build_gem "activesupport", "3.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "3.0"
+ G
+ end
+
+ it "does not output to stdout" do
+ subject
+ expect(out).to be_empty
+ end
+
+ it "outputs progress to stderr" do
+ subject
+ expect(err).to include("Fetching gem metadata")
+ end
+ end
+ end
+
+ describe "with --parseable option" do
+ subject { bundle "outdated --parseable", raise_on_error: false }
+
+ it_behaves_like "a minimal output is desired"
+ end
+
+ describe "with aliased --porcelain option" do
+ subject { bundle "outdated --porcelain", raise_on_error: false }
+
+ it_behaves_like "a minimal output is desired"
+ end
+
+ describe "with specified gems" do
+ it "returns list of outdated gems" do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ update_git "foo", path: lib_path("foo")
+ end
+
+ bundle "outdated foo", raise_on_error: false
+
+ expected_output = <<~TABLE.gsub("x", "\\\h").tr(".", "\.").strip
+ Gem Current Latest Requested Groups Release Date
+ foo 1.0 xxxxxxx 1.0 xxxxxxx >= 0 default
+ TABLE
+
+ expect(out).to match(Regexp.new(expected_output))
+ end
+
+ it "does not require gems to be installed" do
+ build_repo4 do
+ build_gem "zeitwerk", "1.0.0"
+ build_gem "zeitwerk", "2.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "zeitwerk"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ zeitwerk (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ zeitwerk
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "outdated zeitwerk", raise_on_error: false
+
+ expected_output = <<~TABLE.tr(".", "\.").strip
+ Gem Current Latest Requested Groups Release Date
+ zeitwerk 1.0.0 2.0.0 >= 0 default
+ TABLE
+
+ expect(out).to match(Regexp.new(expected_output))
+ expect(err).to be_empty
+ end
+ end
+
+ describe "pre-release gems" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+ end
+
+ context "without the --pre option" do
+ it "ignores pre-release versions" do
+ update_repo2 do
+ build_gem "activesupport", "3.0.0.beta"
+ end
+
+ bundle "outdated"
+
+ expect(out).to end_with("Bundle up to date!")
+ end
+ end
+
+ context "with the --pre option" do
+ it "includes pre-release versions" do
+ update_repo2 do
+ build_gem "activesupport", "3.0.0.beta"
+ end
+
+ bundle "outdated --pre", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.5 3.0.0.beta = 2.3.5 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ context "when current gem is a pre-release" do
+ it "includes the gem" do
+ update_repo2 do
+ build_gem "activesupport", "3.0.0.beta.1"
+ build_gem "activesupport", "3.0.0.beta.2"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "3.0.0.beta.1"
+ G
+
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 3.0.0.beta.1 3.0.0.beta.2 = 3.0.0.beta.1 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+ end
+
+ describe "with --filter-strict option" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+ end
+
+ it "only reports gems that have a newer version that matches the specified dependency version requirements" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ build_gem "weakling", "0.0.5"
+ end
+
+ bundle :outdated, "filter-strict": true, raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ weakling 0.0.3 0.0.5 ~> 0.0.1 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "only reports gems that have a newer version that matches the specified dependency version requirements, using --strict alias" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ build_gem "weakling", "0.0.5"
+ end
+
+ bundle :outdated, strict: true, raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ weakling 0.0.3 0.0.5 ~> 0.0.1 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "doesn't crash when some deps unused on the current platform" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", platforms: [:ruby_22]
+ G
+
+ bundle :outdated, "filter-strict": true
+
+ expect(out).to end_with("Bundle up to date!")
+ end
+
+ it "only reports gem dependencies when they can actually be updated" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack_middleware", "1.0"
+ G
+
+ bundle :outdated, "filter-strict": true
+
+ expect(out).to end_with("Bundle up to date!")
+ end
+
+ describe "and filter options" do
+ it "only reports gems that match requirement and patch filter level" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "~> 2.3"
+ gem "weakling", ">= 0.0.1"
+ G
+
+ update_repo2 do
+ build_gem "activesupport", %w[2.4.0 3.0.0]
+ build_gem "weakling", "0.0.5"
+ end
+
+ bundle :outdated, :"filter-strict" => true, "filter-patch" => true, :raise_on_error => false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ weakling 0.0.3 0.0.5 >= 0.0.1 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "only reports gems that match requirement and minor filter level" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "~> 2.3"
+ gem "weakling", ">= 0.0.1"
+ G
+
+ update_repo2 do
+ build_gem "activesupport", %w[2.3.9]
+ build_gem "weakling", "0.1.5"
+ end
+
+ bundle :outdated, :"filter-strict" => true, "filter-minor" => true, :raise_on_error => false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ weakling 0.0.3 0.1.5 >= 0.0.1 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "only reports gems that match requirement and major filter level" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "~> 2.3"
+ gem "weakling", ">= 0.0.1"
+ G
+
+ update_repo2 do
+ build_gem "activesupport", %w[2.4.0 2.5.0]
+ build_gem "weakling", "1.1.5"
+ end
+
+ bundle :outdated, :"filter-strict" => true, "filter-major" => true, :raise_on_error => false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ weakling 0.0.3 1.1.5 >= 0.0.1 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+ end
+
+ describe "with invalid gem name" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+ end
+
+ it "returns could not find gem name" do
+ bundle "outdated invalid_gem_name", raise_on_error: false
+ expect(err).to include("Could not find gem 'invalid_gem_name'.")
+ end
+
+ it "returns non-zero exit code" do
+ bundle "outdated invalid_gem_name", raise_on_error: false
+ expect(exitstatus).to_not be_zero
+ end
+ end
+
+ it "performs an automatic bundle install" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ gem "foo"
+ G
+
+ bundle_config "auto_install 1"
+ bundle :outdated, raise_on_error: false
+ expect(out).to include("Installing foo 1.0")
+ end
+
+ context "in deployment mode" do
+ before do
+ build_repo2
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "myrack"
+ gem "foo"
+ G
+ bundle :lock
+ bundle_config "deployment true"
+ end
+
+ it "outputs a helpful message about being in deployment mode" do
+ update_repo2 { build_gem "activesupport", "3.0" }
+
+ bundle "outdated", raise_on_error: false
+ expect(last_command).to be_failure
+ expect(err).to include("You are trying to check outdated gems in deployment mode.")
+ expect(err).to include("Run `bundle outdated` elsewhere.")
+ expect(err).to include("If this is a development machine, remove the ")
+ expect(err).to include("Gemfile freeze\nby running `bundle config unset deployment`.")
+ end
+ end
+
+ context "after bundle config set --local deployment true" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "myrack"
+ gem "foo"
+ G
+ bundle_config "deployment true"
+ end
+
+ it "outputs a helpful message about being in deployment mode" do
+ update_repo2 { build_gem "activesupport", "3.0" }
+
+ bundle "outdated", raise_on_error: false
+ expect(last_command).to be_failure
+ expect(err).to include("You are trying to check outdated gems in deployment mode.")
+ expect(err).to include("Run `bundle outdated` elsewhere.")
+ expect(err).to include("If this is a development machine, remove the ")
+ expect(err).to include("Gemfile freeze\nby running `bundle config unset deployment`.")
+ end
+ end
+
+ context "update available for a gem on a different platform" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "laduradura", '= 5.15.2'
+ G
+ end
+
+ it "reports that no updates are available" do
+ bundle "outdated"
+ expect(out).to end_with("Bundle up to date!")
+ end
+ end
+
+ context "update available for a gem on the same platform while multiple platforms used for gem" do
+ before do
+ build_repo2
+ end
+
+ it "reports that updates are available if the Ruby platform is used" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby]
+ G
+
+ bundle "outdated"
+ expect(out).to end_with("Bundle up to date!")
+ end
+
+ it "reports that updates are available if the JRuby platform is used", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby]
+ G
+
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ laduradura 5.15.2 5.15.3 = 5.15.2 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ shared_examples_for "version update is detected" do
+ it "reports that a gem has a newer version" do
+ subject
+
+ outdated_gems = out.split("\n").drop_while {|l| !l.start_with?("Gem") }[1..-1]
+
+ expect(outdated_gems.size).to be > 0
+ end
+ end
+
+ shared_examples_for "major version updates are detected" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+
+ update_repo2 do
+ build_gem "activesupport", "3.3.5"
+ build_gem "weakling", "0.8.0"
+ end
+ end
+
+ it_behaves_like "version update is detected"
+ end
+
+ context "when on a new machine" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+
+ pristine_system_gems
+
+ update_git "foo", path: lib_path("foo")
+ update_repo2 do
+ build_gem "activesupport", "3.3.5"
+ build_gem "weakling", "0.8.0"
+ end
+ end
+
+ subject { bundle "outdated", raise_on_error: false }
+ it_behaves_like "version update is detected"
+ end
+
+ shared_examples_for "minor version updates are detected" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+
+ update_repo2 do
+ build_gem "activesupport", "2.7.5"
+ build_gem "weakling", "2.0.1"
+ end
+ end
+
+ it_behaves_like "version update is detected"
+ end
+
+ shared_examples_for "patch version updates are detected" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+
+ update_repo2 do
+ build_gem "activesupport", "2.3.7"
+ build_gem "weakling", "0.3.1"
+ end
+ end
+
+ it_behaves_like "version update is detected"
+ end
+
+ shared_examples_for "no version updates are detected" do
+ it "does not detect any version updates" do
+ subject
+ expect(out).to end_with("updates to display.")
+ end
+ end
+
+ shared_examples_for "major version is ignored" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+
+ update_repo2 do
+ build_gem "activesupport", "3.3.5"
+ build_gem "weakling", "1.0.1"
+ end
+ end
+
+ it_behaves_like "no version updates are detected"
+ end
+
+ shared_examples_for "minor version is ignored" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+
+ update_repo2 do
+ build_gem "activesupport", "2.4.5"
+ build_gem "weakling", "0.3.1"
+ end
+ end
+
+ it_behaves_like "no version updates are detected"
+ end
+
+ shared_examples_for "patch version is ignored" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+
+ update_repo2 do
+ build_gem "activesupport", "2.3.6"
+ build_gem "weakling", "0.0.4"
+ end
+ end
+
+ it_behaves_like "no version updates are detected"
+ end
+
+ describe "with --filter-major option" do
+ subject { bundle "outdated --filter-major", raise_on_error: false }
+
+ it_behaves_like "major version updates are detected"
+ it_behaves_like "minor version is ignored"
+ it_behaves_like "patch version is ignored"
+ end
+
+ describe "with --filter-minor option" do
+ subject { bundle "outdated --filter-minor", raise_on_error: false }
+
+ it_behaves_like "minor version updates are detected"
+ it_behaves_like "major version is ignored"
+ it_behaves_like "patch version is ignored"
+ end
+
+ describe "with --filter-patch option" do
+ subject { bundle "outdated --filter-patch", raise_on_error: false }
+
+ it_behaves_like "patch version updates are detected"
+ it_behaves_like "major version is ignored"
+ it_behaves_like "minor version is ignored"
+ end
+
+ describe "with --filter-minor --filter-patch options" do
+ subject { bundle "outdated --filter-minor --filter-patch", raise_on_error: false }
+
+ it_behaves_like "minor version updates are detected"
+ it_behaves_like "patch version updates are detected"
+ it_behaves_like "major version is ignored"
+ end
+
+ describe "with --filter-major --filter-minor options" do
+ subject { bundle "outdated --filter-major --filter-minor", raise_on_error: false }
+
+ it_behaves_like "major version updates are detected"
+ it_behaves_like "minor version updates are detected"
+ it_behaves_like "patch version is ignored"
+ end
+
+ describe "with --filter-major --filter-patch options" do
+ subject { bundle "outdated --filter-major --filter-patch", raise_on_error: false }
+
+ it_behaves_like "major version updates are detected"
+ it_behaves_like "patch version updates are detected"
+ it_behaves_like "minor version is ignored"
+ end
+
+ describe "with --filter-major --filter-minor --filter-patch options" do
+ subject { bundle "outdated --filter-major --filter-minor --filter-patch", raise_on_error: false }
+
+ it_behaves_like "major version updates are detected"
+ it_behaves_like "minor version updates are detected"
+ it_behaves_like "patch version updates are detected"
+ end
+
+ context "conservative updates" do
+ before do
+ build_repo4 do
+ build_gem "patch", %w[1.0.0 1.0.1]
+ build_gem "minor", %w[1.0.0 1.0.1 1.1.0]
+ build_gem "major", %w[1.0.0 1.0.1 1.1.0 2.0.0]
+ end
+
+ # establish a lockfile set to 1.0.0
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem 'patch', '1.0.0'
+ gem 'minor', '1.0.0'
+ gem 'major', '1.0.0'
+ G
+
+ # remove all version requirements
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'patch'
+ gem 'minor'
+ gem 'major'
+ G
+ end
+
+ it "shows nothing when patching and filtering to minor" do
+ bundle "outdated --patch --filter-minor"
+
+ expect(out).to end_with("No minor updates to display.")
+ end
+
+ it "shows all gems when patching and filtering to patch" do
+ bundle "outdated --patch --filter-patch", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ major 1.0.0 1.0.1 >= 0 default
+ minor 1.0.0 1.0.1 >= 0 default
+ patch 1.0.0 1.0.1 >= 0 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "shows minor and major when updating to minor and filtering to patch and minor" do
+ bundle "outdated --minor --filter-minor", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ major 1.0.0 1.1.0 >= 0 default
+ minor 1.0.0 1.1.0 >= 0 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "shows minor when updating to major and filtering to minor with parseable" do
+ bundle "outdated --major --filter-minor --parseable", raise_on_error: false
+
+ expect(out).not_to include("patch (newest")
+ expect(out).to include("minor (newest")
+ expect(out).not_to include("major (newest")
+ end
+ end
+
+ context "tricky conservative updates" do
+ before do
+ build_repo4 do
+ build_gem "foo", %w[1.4.3 1.4.4] do |s|
+ s.add_dependency "bar", "~> 2.0"
+ end
+ build_gem "foo", %w[1.4.5 1.5.0] do |s|
+ s.add_dependency "bar", "~> 2.1"
+ end
+ build_gem "foo", %w[1.5.1] do |s|
+ s.add_dependency "bar", "~> 3.0"
+ end
+ build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0]
+ build_gem "qux", %w[1.0.0 1.1.0 2.0.0]
+ end
+
+ # establish a lockfile set to 1.4.3
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo', '1.4.3'
+ gem 'bar', '2.0.3'
+ gem 'qux', '1.0.0'
+ G
+
+ # remove 1.4.3 requirement and bar altogether
+ # to setup update specs below
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo'
+ gem 'qux'
+ G
+ end
+
+ it "shows gems updating to patch and filtering to patch" do
+ bundle "outdated --patch --filter-patch", raise_on_error: false, env: { "DEBUG_RESOLVER" => "1" }
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ bar 2.0.3 2.0.5
+ foo 1.4.3 1.4.4 >= 0 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "shows gems updating to patch and filtering to patch, in debug mode" do
+ bundle "outdated --patch --filter-patch", raise_on_error: false, env: { "DEBUG" => "1" }
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date Path
+ bar 2.0.3 2.0.5
+ foo 1.4.3 1.4.4 >= 0 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ describe "with --only-explicit" do
+ it "does not report outdated dependent gems" do
+ build_repo4 do
+ build_gem "weakling", %w[0.2 0.3] do |s|
+ s.add_dependency "bar", "~> 2.1"
+ end
+ build_gem "bar", %w[2.1 2.2]
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem 'weakling', '0.2'
+ gem 'bar', '2.1'
+ G
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'weakling'
+ G
+
+ bundle "outdated --only-explicit", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ weakling 0.2 0.3 >= 0 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ describe "with a multiplatform lockfile" do
+ before do
+ build_repo4 do
+ build_gem "nokogiri", "1.11.1"
+ build_gem "nokogiri", "1.11.1" do |s|
+ s.platform = Bundler.local_platform
+ end
+
+ build_gem "nokogiri", "1.11.2"
+ build_gem "nokogiri", "1.11.2" do |s|
+ s.platform = Bundler.local_platform
+ end
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.11.1)
+ nokogiri (1.11.1-#{Bundler.local_platform})
+
+ PLATFORMS
+ ruby
+ #{Bundler.local_platform}
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "nokogiri"
+ G
+ end
+
+ it "reports a single entry per gem" do
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ nokogiri 1.11.1 1.11.2 >= 0 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ context "when a gem is no longer a dependency after a full update" do
+ before do
+ build_repo4 do
+ build_gem "mini_portile2", "2.5.2" do |s|
+ s.add_dependency "net-ftp", "~> 0.1"
+ end
+
+ build_gem "mini_portile2", "2.5.3"
+
+ build_gem "net-ftp", "0.1.2"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "mini_portile2"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ mini_portile2 (2.5.2)
+ net-ftp (~> 0.1)
+ net-ftp (0.1.2)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ mini_portile2
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "works" do
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ mini_portile2 2.5.2 2.5.3 >= 0 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+end
diff --git a/spec/bundler/commands/platform_spec.rb b/spec/bundler/commands/platform_spec.rb
new file mode 100644
index 0000000000..9d7354c54f
--- /dev/null
+++ b/spec/bundler/commands/platform_spec.rb
@@ -0,0 +1,1260 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle platform" do
+ context "without flags" do
+ it "returns all the output" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ #{ruby_version_correct}
+
+ gem "foo"
+ G
+
+ bundle "platform"
+ expect(out).to eq(<<-G.chomp)
+Your platform is: #{Gem::Platform.local}
+
+Your app has gems that work on these platforms:
+* #{local_platform}
+
+Your Gemfile specifies a Ruby version requirement:
+* ruby #{Gem.ruby_version}
+
+Your current platform satisfies the Ruby version requirement.
+G
+ end
+
+ it "returns all the output including the patchlevel" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ #{ruby_version_correct_patchlevel}
+
+ gem "foo"
+ G
+
+ bundle "platform"
+ expect(out).to eq(<<-G.chomp)
+Your platform is: #{Gem::Platform.local}
+
+Your app has gems that work on these platforms:
+* #{local_platform}
+
+Your Gemfile specifies a Ruby version requirement:
+* #{Bundler::RubyVersion.system.single_version_string}
+
+Your current platform satisfies the Ruby version requirement.
+G
+ end
+
+ it "doesn't print ruby version requirement if it isn't specified" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "foo"
+ G
+
+ bundle "platform"
+ expect(out).to eq(<<-G.chomp)
+Your platform is: #{Gem::Platform.local}
+
+Your app has gems that work on these platforms:
+* #{local_platform}
+
+Your Gemfile does not specify a Ruby version requirement.
+G
+ end
+
+ it "doesn't match the ruby version requirement" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ #{ruby_version_incorrect}
+
+ gem "foo"
+ G
+
+ bundle "platform"
+ expect(out).to eq(<<-G.chomp)
+Your platform is: #{Gem::Platform.local}
+
+Your app has gems that work on these platforms:
+* #{local_platform}
+
+Your Gemfile specifies a Ruby version requirement:
+* ruby #{not_local_ruby_version}
+
+Your Ruby version is #{Gem.ruby_version}, but your Gemfile specified #{not_local_ruby_version}
+G
+ end
+ end
+
+ context "--ruby" do
+ it "returns ruby version when explicit" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "1.9.3", :engine => 'ruby', :engine_version => '1.9.3'
+
+ gem "foo"
+ G
+
+ bundle "platform --ruby"
+
+ expect(out).to eq("ruby 1.9.3")
+ end
+
+ it "defaults to MRI" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "1.9.3"
+
+ gem "foo"
+ G
+
+ bundle "platform --ruby"
+
+ expect(out).to eq("ruby 1.9.3")
+ end
+
+ it "handles jruby" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "1.8.7", :engine => 'jruby', :engine_version => '1.6.5'
+
+ gem "foo"
+ G
+
+ bundle "platform --ruby"
+
+ expect(out).to eq("ruby 1.8.7 (jruby 1.6.5)")
+ end
+
+ it "handles rbx" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "1.8.7", :engine => 'rbx', :engine_version => '1.2.4'
+
+ gem "foo"
+ G
+
+ bundle "platform --ruby"
+
+ expect(out).to eq("ruby 1.8.7 (rbx 1.2.4)")
+ end
+
+ it "handles truffleruby" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "2.5.1", :engine => 'truffleruby', :engine_version => '1.0.0-rc6'
+
+ gem "foo"
+ G
+
+ bundle "platform --ruby"
+
+ expect(out).to eq("ruby 2.5.1 (truffleruby 1.0.0-rc6)")
+ end
+
+ it "raises an error if engine is used but engine version is not" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "1.8.7", :engine => 'rbx'
+
+ gem "foo"
+ G
+
+ bundle "platform", raise_on_error: false
+
+ expect(exitstatus).not_to eq(0)
+ end
+
+ it "raises an error if engine_version is used but engine is not" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "1.8.7", :engine_version => '1.2.4'
+
+ gem "foo"
+ G
+
+ bundle "platform", raise_on_error: false
+
+ expect(exitstatus).not_to eq(0)
+ end
+
+ it "raises an error if engine version doesn't match ruby version for MRI" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "1.8.7", :engine => 'ruby', :engine_version => '1.2.4'
+
+ gem "foo"
+ G
+
+ bundle "platform", raise_on_error: false
+
+ expect(exitstatus).not_to eq(0)
+ end
+
+ it "should print if no ruby version is specified" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "foo"
+ G
+
+ bundle "platform --ruby"
+
+ expect(out).to eq("No ruby version specified")
+ end
+
+ it "handles when there is a locked requirement" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "< 1.8.7"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+
+ RUBY VERSION
+ ruby 1.0.0p127
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "platform --ruby"
+ expect(out).to eq("ruby 1.0.0")
+ end
+
+ it "handles when there is a lockfile with no requirement" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "platform --ruby"
+ expect(out).to eq("No ruby version specified")
+ end
+
+ it "handles when there is a requirement in the gemfile" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby ">= 1.8.7"
+ G
+
+ bundle "platform --ruby"
+ expect(out).to eq("ruby 1.8.7")
+ end
+
+ it "handles when there are multiple requirements in the gemfile" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby ">= 1.8.7", "< 2.0.0"
+ G
+
+ bundle "platform --ruby"
+ expect(out).to eq("ruby 1.8.7")
+ end
+ end
+
+ let(:ruby_version_correct) { "ruby \"#{Gem.ruby_version}\", :engine => \"#{local_ruby_engine}\", :engine_version => \"#{local_engine_version}\"" }
+ let(:ruby_version_correct_engineless) { "ruby \"#{Gem.ruby_version}\"" }
+ let(:ruby_version_correct_patchlevel) { "#{ruby_version_correct}, :patchlevel => '#{RUBY_PATCHLEVEL}'" }
+ let(:ruby_version_incorrect) { "ruby \"#{not_local_ruby_version}\", :engine => \"#{local_ruby_engine}\", :engine_version => \"#{not_local_ruby_version}\"" }
+ let(:engine_incorrect) { "ruby \"#{Gem.ruby_version}\", :engine => \"#{not_local_tag}\", :engine_version => \"#{Gem.ruby_version}\"" }
+ let(:engine_version_incorrect) { "ruby \"#{Gem.ruby_version}\", :engine => \"#{local_ruby_engine}\", :engine_version => \"#{not_local_engine_version}\"" }
+ let(:patchlevel_incorrect) { "#{ruby_version_correct}, :patchlevel => '#{not_local_patchlevel}'" }
+ let(:patchlevel_fixnum) { "#{ruby_version_correct}, :patchlevel => #{RUBY_PATCHLEVEL}1" }
+
+ def should_be_ruby_version_incorrect
+ expect(exitstatus).to eq(18)
+ expect(err).to be_include("Your Ruby version is #{Gem.ruby_version}, but your Gemfile specified #{not_local_ruby_version}")
+ end
+
+ def should_be_engine_incorrect
+ expect(exitstatus).to eq(18)
+ expect(err).to be_include("Your Ruby engine is #{local_ruby_engine}, but your Gemfile specified #{not_local_tag}")
+ end
+
+ def should_be_engine_version_incorrect
+ expect(exitstatus).to eq(18)
+ expect(err).to be_include("Your #{local_ruby_engine} version is #{local_engine_version}, but your Gemfile specified #{local_ruby_engine} #{not_local_engine_version}")
+ end
+
+ def should_ignore_patchlevel
+ expect(exitstatus).to eq(0)
+ expect(err).to eq("")
+ end
+
+ def should_be_patchlevel_fixnum
+ expect(exitstatus).to eq(18)
+ expect(err).to be_include("The Ruby patchlevel in your Gemfile must be a string")
+ end
+
+ context "bundle install" do
+ it "installs fine when the ruby version matches" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{ruby_version_correct}
+ G
+
+ expect(bundled_app_lock).to exist
+ end
+
+ it "installs fine with any engine", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{ruby_version_correct_engineless}
+ G
+
+ expect(bundled_app_lock).to exist
+ end
+
+ it "installs fine when the patchlevel matches" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{ruby_version_correct_patchlevel}
+ G
+
+ expect(bundled_app_lock).to exist
+ end
+
+ it "doesn't install when the ruby version doesn't match" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{ruby_version_incorrect}
+ G
+
+ expect(bundled_app_lock).not_to exist
+ should_be_ruby_version_incorrect
+ end
+
+ it "doesn't install when engine doesn't match" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{engine_incorrect}
+ G
+
+ expect(bundled_app_lock).not_to exist
+ should_be_engine_incorrect
+ end
+
+ it "doesn't install when engine version doesn't match", :jruby_only do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{engine_version_incorrect}
+ G
+
+ expect(bundled_app_lock).not_to exist
+ should_be_engine_version_incorrect
+ end
+
+ it "does install even when patchlevel doesn't match" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{patchlevel_incorrect}
+ G
+
+ expect(bundled_app_lock).to exist
+ should_ignore_patchlevel
+ end
+ end
+
+ context "bundle check" do
+ it "checks fine when the ruby version matches" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{ruby_version_correct}
+ G
+
+ bundle :check
+ expect(out).to match(/\AResolving dependencies\.\.\.\.*\nThe Gemfile's dependencies are satisfied\z/)
+ end
+
+ it "checks fine with any engine", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{ruby_version_correct_engineless}
+ G
+
+ bundle :check
+ expect(out).to match(/\AResolving dependencies\.\.\.\.*\nThe Gemfile's dependencies are satisfied\z/)
+ end
+
+ it "fails when ruby version doesn't match" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{ruby_version_incorrect}
+ G
+
+ bundle :check, raise_on_error: false
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails when engine doesn't match" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{engine_incorrect}
+ G
+
+ bundle :check, raise_on_error: false
+ should_be_engine_incorrect
+ end
+
+ it "fails when engine version doesn't match", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{engine_version_incorrect}
+ G
+
+ bundle :check, raise_on_error: false
+ should_be_engine_version_incorrect
+ end
+
+ it "checks fine even when patchlevel doesn't match" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{patchlevel_incorrect}
+ G
+
+ bundle :check
+ should_ignore_patchlevel
+ end
+ end
+
+ context "bundle update" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ G
+ end
+
+ it "updates successfully when the ruby version matches" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+
+ #{ruby_version_correct}
+ G
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle "update", all: true
+ expect(the_bundle).to include_gems "myrack 1.2", "myrack-obama 1.0", "activesupport 3.0"
+ end
+
+ it "updates fine with any engine", :jruby_only do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+
+ #{ruby_version_correct_engineless}
+ G
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle "update", all: true
+ expect(the_bundle).to include_gems "myrack 1.2", "myrack-obama 1.0", "activesupport 3.0"
+ end
+
+ it "fails when ruby version doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+
+ #{ruby_version_incorrect}
+ G
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle :update, all: true, raise_on_error: false
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails when ruby engine doesn't match", :jruby_only do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+
+ #{engine_incorrect}
+ G
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle :update, all: true, raise_on_error: false
+ should_be_engine_incorrect
+ end
+
+ it "fails when ruby engine version doesn't match", :jruby_only do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+
+ #{engine_version_incorrect}
+ G
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle :update, all: true, raise_on_error: false
+ should_be_engine_version_incorrect
+ end
+
+ it "updates fine even when patchlevel doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+
+ #{patchlevel_incorrect}
+ G
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle :update, all: true
+ should_ignore_patchlevel
+ expect(the_bundle).to include_gems "activesupport 3.0"
+ end
+ end
+
+ context "bundle info" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+ end
+
+ it "prints path if ruby version is correct" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+
+ #{ruby_version_correct}
+ G
+
+ bundle "info rails --path"
+ expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s)
+ end
+
+ it "prints path if ruby version is correct for any engine", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+
+ #{ruby_version_correct_engineless}
+ G
+
+ bundle "info rails --path"
+ expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s)
+ end
+
+ it "fails if ruby version doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+
+ #{ruby_version_incorrect}
+ G
+
+ bundle "show rails", raise_on_error: false
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails if engine doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+
+ #{engine_incorrect}
+ G
+
+ bundle "show rails", raise_on_error: false
+ should_be_engine_incorrect
+ end
+
+ it "fails if engine version doesn't match", jruby_only: true do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+
+ #{engine_version_incorrect}
+ G
+
+ bundle "show rails", raise_on_error: false
+ should_be_engine_version_incorrect
+ end
+
+ it "prints path even when patchlevel doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+
+ #{patchlevel_incorrect}
+ G
+
+ bundle "show rails"
+ should_ignore_patchlevel
+ expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s)
+ end
+ end
+
+ context "bundle cache" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+ end
+
+ it "copies the .gem file to vendor/cache when ruby version matches" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{ruby_version_correct}
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+
+ it "copies the .gem file to vendor/cache when ruby version matches for any engine", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{ruby_version_correct_engineless}
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+
+ it "fails if the ruby version doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{ruby_version_incorrect}
+ G
+
+ bundle :cache, raise_on_error: false
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails if the engine doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{engine_incorrect}
+ G
+
+ bundle :cache, raise_on_error: false
+ should_be_engine_incorrect
+ end
+
+ it "fails if the engine version doesn't match", :jruby_only do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{engine_version_incorrect}
+ G
+
+ bundle :cache, raise_on_error: false
+ should_be_engine_version_incorrect
+ end
+
+ it "copies the .gem file to vendor/cache even when patchlevel doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{patchlevel_incorrect}
+ G
+
+ bundle :cache
+ should_ignore_patchlevel
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+ end
+
+ context "bundle pack" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+ end
+
+ it "copies the .gem file to vendor/cache when ruby version matches" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{ruby_version_correct}
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+
+ it "copies the .gem file to vendor/cache when ruby version matches any engine", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{ruby_version_correct_engineless}
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+
+ it "fails if the ruby version doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{ruby_version_incorrect}
+ G
+
+ bundle :cache, raise_on_error: false
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails if the engine doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{engine_incorrect}
+ G
+
+ bundle :cache, raise_on_error: false
+ should_be_engine_incorrect
+ end
+
+ it "fails if the engine version doesn't match", :jruby_only do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{engine_version_incorrect}
+ G
+
+ bundle :cache, raise_on_error: false
+ should_be_engine_version_incorrect
+ end
+
+ it "copies the .gem file to vendor/cache even when patchlevel doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{patchlevel_incorrect}
+ G
+
+ bundle :cache
+ should_ignore_patchlevel
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+ end
+
+ context "bundle exec" do
+ before do
+ ENV["BUNDLER_FORCE_TTY"] = "true"
+ system_gems "myrack-1.0.0", "myrack-0.9.1", path: default_bundle_path
+ end
+
+ it "activates the correct gem when ruby version matches" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+
+ #{ruby_version_correct}
+ G
+
+ bundle "exec myrackup"
+ expect(out).to include("0.9.1")
+ end
+
+ it "activates the correct gem when ruby version matches any engine", :jruby_only do
+ system_gems "myrack-1.0.0", "myrack-0.9.1", path: default_bundle_path
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+
+ #{ruby_version_correct_engineless}
+ G
+
+ bundle "exec myrackup"
+ expect(out).to include("0.9.1")
+ end
+
+ it "fails when the ruby version doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+
+ #{ruby_version_incorrect}
+ G
+
+ bundle "exec myrackup", raise_on_error: false
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails when the engine doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+
+ #{engine_incorrect}
+ G
+
+ bundle "exec myrackup", raise_on_error: false
+ should_be_engine_incorrect
+ end
+
+ it "fails when the engine version doesn't match", :jruby_only do
+ gemfile <<-G
+ gem "myrack", "0.9.1"
+
+ #{engine_version_incorrect}
+ G
+
+ bundle "exec myrackup", raise_on_error: false
+ should_be_engine_version_incorrect
+ end
+
+ it "activates the correct gem even when patchlevel doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{patchlevel_incorrect}
+ G
+
+ bundle "exec myrackup"
+ should_ignore_patchlevel
+ expect(out).to include("1.0.0")
+ end
+ end
+
+ context "bundle console" do
+ before do
+ build_repo2 do
+ build_dummy_irb
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "irb"
+ gem "myrack"
+ gem "activesupport", :group => :test
+ gem "myrack_middleware", :group => :development
+ G
+ end
+
+ it "starts IRB with the default group loaded when ruby version matches", :readline do
+ gemfile gemfile + "\n\n#{ruby_version_correct}\n"
+
+ bundle "console" do |input, _, _|
+ input.puts("puts MYRACK")
+ input.puts("exit")
+ end
+ expect(out).to include("0.9.1")
+ end
+
+ it "starts IRB with the default group loaded when ruby version matches", :readline, :jruby_only do
+ gemfile gemfile + "\n\n#{ruby_version_correct_engineless}\n"
+
+ bundle "console" do |input, _, _|
+ input.puts("puts MYRACK")
+ input.puts("exit")
+ end
+ expect(out).to include("0.9.1")
+ end
+
+ it "fails when ruby version doesn't match" do
+ gemfile gemfile + "\n\n#{ruby_version_incorrect}\n"
+
+ bundle "console", raise_on_error: false
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails when engine doesn't match" do
+ gemfile gemfile + "\n\n#{engine_incorrect}\n"
+
+ bundle "console", raise_on_error: false
+ should_be_engine_incorrect
+ end
+
+ it "fails when engine version doesn't match", :jruby_only do
+ gemfile gemfile + "\n\n#{engine_version_incorrect}\n"
+
+ bundle "console", raise_on_error: false
+ should_be_engine_version_incorrect
+ end
+
+ it "starts IRB with the default group loaded even when patchlevel doesn't match", :readline do
+ gemfile gemfile + "\n\n#{patchlevel_incorrect}\n"
+
+ bundle "console" do |input, _, _|
+ input.puts("puts MYRACK")
+ input.puts("exit")
+ end
+ should_ignore_patchlevel
+ expect(out).to include("0.9.1")
+ end
+ end
+
+ context "Bundler.setup" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack", :group => :test
+ G
+
+ ENV["BUNDLER_FORCE_TTY"] = "true"
+ end
+
+ it "makes a Gemfile.lock if setup succeeds" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack"
+
+ #{ruby_version_correct}
+ G
+
+ FileUtils.rm(bundled_app_lock)
+
+ run "1"
+ expect(bundled_app_lock).to exist
+ end
+
+ it "makes a Gemfile.lock if setup succeeds for any engine", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack"
+
+ #{ruby_version_correct_engineless}
+ G
+
+ FileUtils.rm(bundled_app_lock)
+
+ run "1"
+ expect(bundled_app_lock).to exist
+ end
+
+ it "fails when ruby version doesn't match" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack"
+
+ #{ruby_version_incorrect}
+ G
+
+ FileUtils.rm(bundled_app_lock)
+
+ ruby "require 'bundler/setup'", env: { "BUNDLER_VERSION" => Bundler::VERSION }, raise_on_error: false
+
+ expect(bundled_app_lock).not_to exist
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails when engine doesn't match" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack"
+
+ #{engine_incorrect}
+ G
+
+ FileUtils.rm(bundled_app_lock)
+
+ ruby "require 'bundler/setup'", env: { "BUNDLER_VERSION" => Bundler::VERSION }, raise_on_error: false
+
+ expect(bundled_app_lock).not_to exist
+ should_be_engine_incorrect
+ end
+
+ it "fails when engine version doesn't match", :jruby_only do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack"
+
+ #{engine_version_incorrect}
+ G
+
+ FileUtils.rm(bundled_app_lock)
+
+ ruby "require 'bundler/setup'", env: { "BUNDLER_VERSION" => Bundler::VERSION }, raise_on_error: false
+
+ expect(bundled_app_lock).not_to exist
+ should_be_engine_version_incorrect
+ end
+
+ it "makes a Gemfile.lock even when patchlevel doesn't match" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack"
+
+ #{patchlevel_incorrect}
+ G
+
+ FileUtils.rm(bundled_app_lock)
+
+ ruby "require 'bundler/setup'", env: { "BUNDLER_VERSION" => Bundler::VERSION }
+
+ should_ignore_patchlevel
+ expect(bundled_app_lock).to exist
+ end
+ end
+
+ context "bundle outdated" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "2.3.5"
+ gem "foo", :git => "#{lib_path("foo")}"
+ G
+ end
+
+ it "returns list of outdated gems when the ruby version matches" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ update_git "foo", path: lib_path("foo")
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "2.3.5"
+ gem "foo", :git => "#{lib_path("foo")}"
+
+ #{ruby_version_correct}
+ G
+
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE.gsub("x", "\\\h").tr(".", "\.").strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.5 3.0 = 2.3.5 default
+ foo 1.0 xxxxxxx 1.0 xxxxxxx >= 0 default
+ TABLE
+
+ expect(out).to match(Regexp.new(expected_output))
+ end
+
+ it "returns list of outdated gems when the ruby version matches for any engine", :jruby_only do
+ bundle :install
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ update_git "foo", path: lib_path("foo")
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "2.3.5"
+ gem "foo", :git => "#{lib_path("foo")}"
+
+ #{ruby_version_correct_engineless}
+ G
+
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE.gsub("x", "\\\h").tr(".", "\.").strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.5 3.0 = 2.3.5 default
+ foo 1.0 xxxxxxx 1.0 xxxxxxx >= 0 default
+ TABLE
+
+ expect(out).to match(Regexp.new(expected_output))
+ end
+
+ it "fails when the ruby version doesn't match" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ update_git "foo", path: lib_path("foo")
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "2.3.5"
+ gem "foo", :git => "#{lib_path("foo")}"
+
+ #{ruby_version_incorrect}
+ G
+
+ bundle "outdated", raise_on_error: false
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails when the engine doesn't match" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ update_git "foo", path: lib_path("foo")
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "2.3.5"
+ gem "foo", :git => "#{lib_path("foo")}"
+
+ #{engine_incorrect}
+ G
+
+ bundle "outdated", raise_on_error: false
+ should_be_engine_incorrect
+ end
+
+ it "fails when the engine version doesn't match", :jruby_only do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ update_git "foo", path: lib_path("foo")
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "2.3.5"
+ gem "foo", :git => "#{lib_path("foo")}"
+
+ #{engine_version_incorrect}
+ G
+
+ bundle "outdated", raise_on_error: false
+ should_be_engine_version_incorrect
+ end
+
+ it "reports outdated gems even when patchlevel doesn't match" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ update_git "foo", path: lib_path("foo")
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "2.3.5"
+ gem "foo", :git => "#{lib_path("foo")}"
+
+ #{patchlevel_incorrect}
+ G
+
+ bundle "outdated", raise_on_error: false
+ expect(err).not_to include("patchlevel")
+ expect(out).to include("activesupport")
+ expect(out).to include("foo")
+ end
+ end
+end
diff --git a/spec/bundler/commands/post_bundle_message_spec.rb b/spec/bundler/commands/post_bundle_message_spec.rb
new file mode 100644
index 0000000000..088fc29fe1
--- /dev/null
+++ b/spec/bundler/commands/post_bundle_message_spec.rb
@@ -0,0 +1,183 @@
+# frozen_string_literal: true
+
+RSpec.describe "post bundle message" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "activesupport", "2.3.5", :group => [:emo, :test]
+ group :test do
+ gem "rspec"
+ end
+ gem "myrack-obama", :group => :obama
+ G
+ end
+
+ let(:bundle_path) { "./.bundle" }
+ let(:bundle_show_system_message) { "Use `bundle info [gemname]` to see where a bundled gem is installed." }
+ let(:bundle_show_path_message) { "Bundled gems are installed into `#{bundle_path}`" }
+ let(:bundle_complete_message) { "Bundle complete!" }
+ let(:bundle_updated_message) { "Bundle updated!" }
+ let(:installed_gems_stats) { "4 Gemfile dependencies, 4 gems now installed." }
+
+ describe "when installing to system gems" do
+ before do
+ bundle_config "path.system true"
+ end
+
+ it "shows proper messages according to the configured groups" do
+ bundle :install
+ expect(out).to include(bundle_show_system_message)
+ expect(out).not_to include("Gems in the group")
+ expect(out).to include(bundle_complete_message)
+ expect(out).to include(installed_gems_stats)
+
+ bundle_config "without emo"
+ bundle :install
+ expect(out).to include(bundle_show_system_message)
+ expect(out).to include("Gems in the group 'emo' were not installed")
+ expect(out).to include(bundle_complete_message)
+ expect(out).to include(installed_gems_stats)
+
+ bundle_config "without emo test"
+ bundle :install
+ expect(out).to include(bundle_show_system_message)
+ expect(out).to include("Gems in the groups 'emo' and 'test' were not installed")
+ expect(out).to include(bundle_complete_message)
+ expect(out).to include("4 Gemfile dependencies, 2 gems now installed.")
+
+ bundle_config "without emo obama test"
+ bundle :install
+ expect(out).to include(bundle_show_system_message)
+ expect(out).to include("Gems in the groups 'emo', 'obama' and 'test' were not installed")
+ expect(out).to include(bundle_complete_message)
+ expect(out).to include("4 Gemfile dependencies, 1 gem now installed.")
+ end
+
+ describe "for second bundle install run" do
+ it "without any options" do
+ 2.times { bundle :install }
+ expect(out).to include(bundle_show_system_message)
+ expect(out).to_not include("Gems in the groups")
+ expect(out).to include(bundle_complete_message)
+ expect(out).to include(installed_gems_stats)
+ end
+ end
+ end
+
+ describe "with `path` configured" do
+ let(:bundle_path) { "./vendor" }
+
+ it "shows proper messages according to the configured groups" do
+ bundle_config "path vendor"
+ bundle :install
+ expect(out).to include(bundle_show_path_message)
+ expect(out).to_not include("Gems in the group")
+ expect(out).to include(bundle_complete_message)
+
+ bundle_config "path vendor"
+ bundle_config "without emo"
+ bundle :install
+ expect(out).to include(bundle_show_path_message)
+ expect(out).to include("Gems in the group 'emo' were not installed")
+ expect(out).to include(bundle_complete_message)
+
+ bundle_config "path vendor"
+ bundle_config "without emo test"
+ bundle :install
+ expect(out).to include(bundle_show_path_message)
+ expect(out).to include("Gems in the groups 'emo' and 'test' were not installed")
+ expect(out).to include(bundle_complete_message)
+
+ bundle_config "path vendor"
+ bundle_config "without emo obama test"
+ bundle :install
+ expect(out).to include(bundle_show_path_message)
+ expect(out).to include("Gems in the groups 'emo', 'obama' and 'test' were not installed")
+ expect(out).to include(bundle_complete_message)
+ end
+ end
+
+ describe "with an absolute `path` inside the cwd configured" do
+ let(:bundle_path) { bundled_app("cache") }
+
+ it "shows proper messages according to the configured groups" do
+ bundle_config "path #{bundle_path}"
+ bundle :install
+ expect(out).to include("Bundled gems are installed into `./cache`")
+ expect(out).to_not include("Gems in the group")
+ expect(out).to include(bundle_complete_message)
+ end
+ end
+
+ describe "with `path` configured to an absolute path outside the cwd" do
+ let(:bundle_path) { tmp("not_bundled_app") }
+
+ it "shows proper messages according to the configured groups" do
+ bundle_config "path #{bundle_path}"
+ bundle :install
+ expect(out).to include("Bundled gems are installed into `#{tmp("not_bundled_app")}`")
+ expect(out).to_not include("Gems in the group")
+ expect(out).to include(bundle_complete_message)
+ end
+ end
+
+ describe "with misspelled or non-existent gem name" do
+ before do
+ bundle_config "force_ruby_platform true"
+ end
+
+ it "should report a helpful error message" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "not-a-gem", :group => :development
+ G
+ expect(err).to include <<~EOS.strip
+ Could not find gem 'not-a-gem' in rubygems repository https://gem.repo1/ or installed locally.
+ EOS
+ end
+
+ it "should report a helpful error message with reference to cache if available" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ bundle :cache
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "not-a-gem", :group => :development
+ G
+ expect(err).to include("Could not find gem 'not-a-gem' in").
+ and include("or in gems cached in vendor/cache.")
+ end
+ end
+
+ describe "for bundle update" do
+ it "shows proper messages according to the configured groups" do
+ bundle :update, all: true
+ expect(out).not_to include("Gems in the groups")
+ expect(out).to include(bundle_updated_message)
+
+ bundle_config "without emo"
+ bundle :install
+ bundle :update, all: true
+ expect(out).to include("Gems in the group 'emo' were not updated")
+ expect(out).to include(bundle_updated_message)
+
+ bundle_config "without emo test"
+ bundle :install
+ bundle :update, all: true
+ expect(out).to include("Gems in the groups 'emo' and 'test' were not updated")
+ expect(out).to include(bundle_updated_message)
+
+ bundle_config "without emo obama test"
+ bundle :install
+ bundle :update, all: true
+ expect(out).to include("Gems in the groups 'emo', 'obama' and 'test' were not updated")
+ expect(out).to include(bundle_updated_message)
+ end
+ end
+end
diff --git a/spec/bundler/commands/pristine_spec.rb b/spec/bundler/commands/pristine_spec.rb
new file mode 100644
index 0000000000..5f80b9e534
--- /dev/null
+++ b/spec/bundler/commands/pristine_spec.rb
@@ -0,0 +1,275 @@
+# frozen_string_literal: true
+
+require "bundler/vendored_fileutils"
+
+RSpec.describe "bundle pristine" do
+ before :each do
+ build_lib "baz", path: bundled_app do |s|
+ s.version = "1.0.0"
+ s.add_development_dependency "baz-dev", "=1.0.0"
+ end
+
+ build_repo2 do
+ build_gem "weakling"
+ build_gem "baz-dev", "1.0.0"
+ build_gem "very_simple_binary", &:add_c_extension
+ build_git "foo", path: lib_path("foo")
+ build_git "git_with_ext", path: lib_path("git_with_ext"), &:add_c_extension
+ build_lib "bar", path: lib_path("bar")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "weakling"
+ gem "very_simple_binary"
+ gem "foo", :git => "#{lib_path("foo")}", :branch => "main"
+ gem "git_with_ext", :git => "#{lib_path("git_with_ext")}"
+ gem "bar", :path => "#{lib_path("bar")}"
+
+ gemspec
+ G
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ context "when sourced from RubyGems" do
+ it "reverts using cached .gem file" do
+ spec = find_spec("weakling")
+ changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt")
+
+ FileUtils.touch(changes_txt)
+ expect(changes_txt).to be_file
+
+ bundle "pristine"
+ expect(changes_txt).to_not be_file
+ end
+
+ it "does not delete the bundler gem" do
+ bundle "install"
+ bundle "pristine"
+ bundle "-v"
+
+ expect(out).to end_with(Bundler::VERSION)
+ end
+ end
+
+ context "when sourced from git repo" do
+ it "reverts by resetting to current revision`" do
+ spec = find_spec("foo")
+ changed_file = Pathname.new(spec.full_gem_path).join("lib/foo.rb")
+ diff = "#Pristine spec changes"
+
+ File.open(changed_file, "a") {|f| f.puts diff }
+ expect(File.read(changed_file)).to include(diff)
+
+ bundle "pristine"
+ expect(File.read(changed_file)).to_not include(diff)
+ end
+
+ it "removes added files" do
+ spec = find_spec("foo")
+ changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt")
+
+ FileUtils.touch(changes_txt)
+ expect(changes_txt).to be_file
+
+ bundle "pristine"
+ expect(changes_txt).not_to be_file
+ end
+
+ it "displays warning and ignores changes when a local config exists" do
+ spec = find_spec("foo")
+ bundle_config "local.#{spec.name} #{lib_path(spec.name)}"
+
+ changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt")
+ FileUtils.touch(changes_txt)
+ expect(changes_txt).to be_file
+
+ bundle "pristine"
+ expect(changes_txt).to be_file
+ expect(err).to include("Cannot pristine #{spec.name} (#{spec.version}#{spec.git_version}). Gem is locally overridden.")
+ end
+
+ it "doesn't run multiple git processes for the same repository" do
+ nested_gems = [
+ "actioncable",
+ "actionmailer",
+ "actionpack",
+ "actionview",
+ "activejob",
+ "activemodel",
+ "activerecord",
+ "activestorage",
+ "activesupport",
+ "railties",
+ ]
+
+ build_repo2 do
+ nested_gems.each do |gem|
+ build_lib gem, path: lib_path("rails/#{gem}")
+ end
+
+ build_git "rails", path: lib_path("rails") do |s|
+ nested_gems.each do |gem|
+ s.add_dependency gem
+ end
+ end
+ end
+
+ install_gemfile <<-G
+ source 'https://rubygems.org'
+
+ git "#{lib_path("rails")}" do
+ gem "rails"
+ gem "actioncable"
+ gem "actionmailer"
+ gem "actionpack"
+ gem "actionview"
+ gem "activejob"
+ gem "activemodel"
+ gem "activerecord"
+ gem "activestorage"
+ gem "activesupport"
+ gem "railties"
+ end
+ G
+
+ changed_files = []
+ diff = "#Pristine spec changes"
+
+ nested_gems.each do |gem|
+ spec = find_spec(gem)
+ changed_files << Pathname.new(spec.full_gem_path).join("lib/#{gem}.rb")
+ File.open(changed_files.last, "a") {|f| f.puts diff }
+ end
+
+ bundle "pristine"
+
+ changed_files.each do |changed_file|
+ expect(File.read(changed_file)).to_not include(diff)
+ end
+ end
+ end
+
+ context "when sourced from gemspec" do
+ it "displays warning and ignores changes when sourced from gemspec" do
+ spec = find_spec("baz")
+ changed_file = Pathname.new(spec.full_gem_path).join("lib/baz.rb")
+ diff = "#Pristine spec changes"
+
+ File.open(changed_file, "a") {|f| f.puts diff }
+ expect(File.read(changed_file)).to include(diff)
+
+ bundle "pristine"
+ expect(File.read(changed_file)).to include(diff)
+ expect(err).to include("Cannot pristine #{spec.name} (#{spec.version}#{spec.git_version}). Gem is sourced from local path.")
+ end
+
+ it "reinstall gemspec dependency" do
+ spec = find_spec("baz-dev")
+ changed_file = Pathname.new(spec.full_gem_path).join("lib/baz/dev.rb")
+ diff = "#Pristine spec changes"
+
+ File.open(changed_file, "a") {|f| f.puts "#Pristine spec changes" }
+ expect(File.read(changed_file)).to include(diff)
+
+ bundle "pristine"
+ expect(File.read(changed_file)).to_not include(diff)
+ end
+ end
+
+ context "when sourced from path" do
+ it "displays warning and ignores changes when sourced from local path" do
+ spec = find_spec("bar")
+ changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt")
+ FileUtils.touch(changes_txt)
+ expect(changes_txt).to be_file
+ bundle "pristine"
+ expect(err).to include("Cannot pristine #{spec.name} (#{spec.version}#{spec.git_version}). Gem is sourced from local path.")
+ expect(changes_txt).to be_file
+ end
+ end
+
+ context "when passing a list of gems to pristine" do
+ it "resets them" do
+ foo = find_spec("foo")
+ foo_changes_txt = Pathname.new(foo.full_gem_path).join("lib/changes.txt")
+ FileUtils.touch(foo_changes_txt)
+ expect(foo_changes_txt).to be_file
+
+ bar = find_spec("bar")
+ bar_changes_txt = Pathname.new(bar.full_gem_path).join("lib/changes.txt")
+ FileUtils.touch(bar_changes_txt)
+ expect(bar_changes_txt).to be_file
+
+ weakling = find_spec("weakling")
+ weakling_changes_txt = Pathname.new(weakling.full_gem_path).join("lib/changes.txt")
+ FileUtils.touch(weakling_changes_txt)
+ expect(weakling_changes_txt).to be_file
+
+ bundle "pristine foo bar weakling"
+
+ expect(err).to include("Cannot pristine bar (1.0). Gem is sourced from local path.")
+ expect(out).to include("Installing weakling 1.0")
+
+ expect(weakling_changes_txt).not_to be_file
+ expect(foo_changes_txt).not_to be_file
+ expect(bar_changes_txt).to be_file
+ end
+
+ it "raises when one of them is not in the lockfile" do
+ bundle "pristine abcabcabc", raise_on_error: false
+ expect(err).to include("Could not find gem 'abcabcabc'.")
+ end
+ end
+
+ context "when a build config exists for one of the gems" do
+ let(:very_simple_binary) { find_spec("very_simple_binary") }
+ let(:c_ext_dir) { Pathname.new(very_simple_binary.full_gem_path).join("ext") }
+ let(:build_opt) { "--with-ext-lib=#{c_ext_dir}" }
+ before { bundle_config "build.very_simple_binary -- #{build_opt}" }
+
+ # This just verifies that the generated Makefile from the c_ext gem makes
+ # use of the build_args from the bundle config
+ it "applies the config when installing the gem" do
+ bundle "pristine"
+
+ makefile_contents = File.read(c_ext_dir.join("Makefile").to_s)
+ expect(makefile_contents).to match(/libpath =.*#{Regexp.escape(c_ext_dir.to_s)}/)
+ expect(makefile_contents).to match(/LIBPATH =.*-L#{Regexp.escape(c_ext_dir.to_s)}/)
+ end
+ end
+
+ context "when a build config exists for a git sourced gem" do
+ let(:git_with_ext) { find_spec("git_with_ext") }
+ let(:c_ext_dir) { Pathname.new(git_with_ext.full_gem_path).join("ext") }
+ let(:build_opt) { "--with-ext-lib=#{c_ext_dir}" }
+ before { bundle_config "build.git_with_ext -- #{build_opt}" }
+
+ # This just verifies that the generated Makefile from the c_ext gem makes
+ # use of the build_args from the bundle config
+ it "applies the config when installing the gem" do
+ bundle "pristine"
+
+ makefile_contents = File.read(c_ext_dir.join("Makefile").to_s)
+ expect(makefile_contents).to match(/libpath =.*#{Regexp.escape(c_ext_dir.to_s)}/)
+ expect(makefile_contents).to match(/LIBPATH =.*-L#{Regexp.escape(c_ext_dir.to_s)}/)
+ end
+ end
+
+ context "when BUNDLE_GEMFILE doesn't exist" do
+ before do
+ bundle "pristine", env: { "BUNDLE_GEMFILE" => "does/not/exist" }, raise_on_error: false
+ end
+
+ it "shows a meaningful error" do
+ expect(err).to eq("#{bundled_app("does/not/exist")} not found")
+ end
+ end
+
+ def find_spec(name)
+ without_env_side_effects do
+ Bundler.definition.specs[name].first
+ end
+ end
+end
diff --git a/spec/bundler/commands/remove_spec.rb b/spec/bundler/commands/remove_spec.rb
new file mode 100644
index 0000000000..8a2e6778ea
--- /dev/null
+++ b/spec/bundler/commands/remove_spec.rb
@@ -0,0 +1,736 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle remove" do
+ context "when no gems are specified" do
+ it "throws error" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ bundle "remove", raise_on_error: false
+
+ expect(err).to include("Please specify gems to remove.")
+ end
+ end
+
+ context "after 'bundle install' is run" do
+ describe "running 'bundle remove GEM_NAME'" do
+ it "removes it from the lockfile" do
+ myrack_dep = <<~L
+
+ DEPENDENCIES
+ myrack
+
+ L
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+
+ bundle "install"
+
+ expect(lockfile).to include(myrack_dep)
+
+ bundle "remove myrack"
+
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ expect(lockfile).to_not include(myrack_dep)
+ end
+ end
+ end
+
+ describe "remove single gem from gemfile" do
+ context "when gem is present in gemfile" do
+ it "shows success for removed gem" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(the_bundle).to_not include_gems "myrack"
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+
+ context "when gem is specified in multiple lines" do
+ it "shows success for removed gem" do
+ build_git "myrack"
+
+ gemfile <<-G
+ source 'https://gem.repo1'
+
+ gem 'git'
+ gem 'myrack',
+ git: "#{lib_path("myrack-1.0")}",
+ branch: 'main'
+ gem 'nokogiri'
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source 'https://gem.repo1'
+
+ gem 'git'
+ gem 'nokogiri'
+ G
+ end
+ end
+ end
+
+ context "when gem is not present in gemfile" do
+ it "shows warning for gem that could not be removed" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ bundle "remove myrack", raise_on_error: false
+
+ expect(err).to include("`myrack` is not specified in #{bundled_app_gemfile} so it could not be removed.")
+ end
+ end
+ end
+
+ describe "remove multiple gems from gemfile" do
+ context "when all gems are present in gemfile" do
+ it "shows success fir all removed gems" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ gem "rails"
+ G
+
+ bundle "remove myrack rails"
+
+ expect(out).to include("myrack was removed.")
+ expect(out).to include("rails was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ context "when some gems are not present in the gemfile" do
+ it "shows warning for those not present and success for those that can be removed" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "rails"
+ gem "minitest"
+ gem "rspec"
+ G
+
+ bundle "remove rails myrack minitest", raise_on_error: false
+
+ expect(err).to include("`myrack` is not specified in #{bundled_app_gemfile} so it could not be removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ gem "rails"
+ gem "minitest"
+ gem "rspec"
+ G
+ end
+ end
+ end
+
+ context "with inline groups" do
+ it "removes the specified gem" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", :group => [:dev]
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ describe "with group blocks" do
+ context "when single group block with gem to be removed is present" do
+ it "removes the group block" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ group :test do
+ gem "rspec"
+ end
+ G
+
+ bundle "remove rspec"
+
+ expect(out).to include("rspec was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ context "when gem to be removed is outside block" do
+ it "does not modify group" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ group :test do
+ gem "coffee-script-source"
+ end
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ group :test do
+ gem "coffee-script-source"
+ end
+ G
+ end
+ end
+
+ context "when an empty block is also present" do
+ it "removes all empty blocks" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ group :test do
+ gem "rspec"
+ end
+
+ group :dev do
+ end
+ G
+
+ bundle "remove rspec"
+
+ expect(out).to include("rspec was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ context "when the gem belongs to multiple groups" do
+ it "removes the groups" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ group :test, :serioustest do
+ gem "rspec"
+ end
+ G
+
+ bundle "remove rspec"
+
+ expect(out).to include("rspec was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ context "when the gem is present in multiple groups" do
+ it "removes all empty blocks" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ group :one do
+ gem "rspec"
+ end
+
+ group :two do
+ gem "rspec"
+ end
+ G
+
+ bundle "remove rspec"
+
+ expect(out).to include("rspec was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+ end
+
+ describe "nested group blocks" do
+ context "when all the groups will be empty after removal" do
+ it "removes the empty nested blocks" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ group :test do
+ group :serioustest do
+ gem "rspec"
+ end
+ end
+ G
+
+ bundle "remove rspec"
+
+ expect(out).to include("rspec was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ context "when outer group will not be empty after removal" do
+ it "removes only empty blocks" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ group :test do
+ gem "myrack-test"
+
+ group :serioustest do
+ gem "rspec"
+ end
+ end
+ G
+
+ bundle "remove rspec"
+
+ expect(out).to include("rspec was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ group :test do
+ gem "myrack-test"
+
+ end
+ G
+ end
+ end
+
+ context "when inner group will not be empty after removal" do
+ it "removes only empty blocks" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ group :test do
+ group :serioustest do
+ gem "rspec"
+ gem "myrack-test"
+ end
+ end
+ G
+
+ bundle "remove rspec"
+
+ expect(out).to include("rspec was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ group :test do
+ group :serioustest do
+ gem "myrack-test"
+ end
+ end
+ G
+ end
+ end
+ end
+
+ describe "arbitrary gemfile" do
+ context "when multiple gems are present in same line" do
+ it "shows warning for gems not removed" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"; gem "rails"
+ G
+
+ bundle "remove rails", raise_on_error: false
+
+ expect(err).to include("Gems could not be removed. myrack (>= 0) would also have been removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ gem "myrack"; gem "rails"
+ G
+ end
+ end
+
+ context "when some gems could not be removed" do
+ it "shows warning for gems not removed and success for those removed" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem"myrack"
+ gem"rspec"
+ gem "rails"
+ gem "minitest"
+ G
+
+ bundle "remove rails myrack rspec minitest"
+
+ expect(out).to include("rails was removed.")
+ expect(out).to include("minitest was removed.")
+ expect(out).to include("myrack, rspec could not be removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ gem"myrack"
+ gem"rspec"
+ G
+ end
+ end
+ end
+
+ context "with sources" do
+ before do
+ build_repo3 do
+ build_gem "rspec"
+ end
+ end
+
+ it "removes gems and empty source blocks" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+
+ source "https://gem.repo3" do
+ gem "rspec"
+ end
+ G
+
+ bundle "install"
+
+ bundle "remove rspec"
+
+ expect(out).to include("rspec was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+ end
+ end
+
+ describe "with eval_gemfile" do
+ context "when gems are present in both gemfiles" do
+ it "removes the gems" do
+ gemfile "Gemfile-other", <<-G
+ gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+
+ gem "myrack"
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ end
+ end
+
+ context "when gems are present in other gemfile" do
+ it "removes the gems" do
+ gemfile "Gemfile-other", <<-G
+ gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ G
+
+ bundle "remove myrack"
+
+ expect(bundled_app("Gemfile-other").read).to_not include("gem \"myrack\"")
+ expect(out).to include("myrack was removed.")
+ end
+ end
+
+ context "when gems to be removed are not specified in any of the gemfiles" do
+ it "throws error for the gems not present" do
+ # an empty gemfile
+ # indicating the gem is not present in the gemfile
+ create_file "Gemfile-other", <<-G
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ G
+
+ bundle "remove myrack", raise_on_error: false
+
+ expect(err).to include("`myrack` is not specified in #{bundled_app_gemfile} so it could not be removed.")
+ end
+ end
+
+ context "when the gem is present in parent file but not in gemfile specified by eval_gemfile" do
+ it "removes the gem" do
+ gemfile "Gemfile-other", <<-G
+ gem "rails"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ gem "myrack"
+ G
+
+ bundle "remove myrack", raise_on_error: false
+
+ expect(out).to include("myrack was removed.")
+ expect(err).to include("`myrack` is not specified in #{bundled_app("Gemfile-other")} so it could not be removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ G
+ end
+ end
+
+ context "when gems cannot be removed from other gemfile" do
+ it "shows error" do
+ gemfile "Gemfile-other", <<-G
+ gem "rails"; gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ gem "myrack"
+ G
+
+ bundle "remove myrack", raise_on_error: false
+
+ expect(out).to include("myrack was removed.")
+ expect(err).to include("Gems could not be removed. rails (>= 0) would also have been removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ G
+ end
+ end
+
+ context "when gems could not be removed from parent gemfile" do
+ it "shows error" do
+ gemfile "Gemfile-other", <<-G
+ gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ gem "rails"; gem "myrack"
+ G
+
+ bundle "remove myrack", raise_on_error: false
+
+ expect(err).to include("Gems could not be removed. rails (>= 0) would also have been removed.")
+ expect(bundled_app("Gemfile-other").read).to include("gem \"myrack\"")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ gem "rails"; gem "myrack"
+ G
+ end
+ end
+
+ context "when gem present in gemfiles but could not be removed from one from one of them" do
+ it "removes gem which can be removed and shows warning for file from which it cannot be removed" do
+ gemfile "Gemfile-other", <<-G
+ gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ gem"myrack"
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(bundled_app("Gemfile-other").read).to_not include("gem \"myrack\"")
+ end
+ end
+ end
+
+ context "with install_if" do
+ it "removes gems inside blocks and empty blocks" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ install_if(lambda { false }) do
+ gem "myrack"
+ end
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ context "with env" do
+ it "removes gems inside blocks and empty blocks" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ env "BUNDLER_TEST" do
+ gem "myrack"
+ end
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ context "with gemspec" do
+ it "should not remove the gem" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.write("foo.gemspec", "")
+ s.add_dependency "myrack"
+ end
+
+ install_gemfile(<<-G)
+ source "https://gem.repo1"
+ gemspec :path => '#{tmp("foo")}', :name => 'foo'
+ G
+
+ bundle "remove foo"
+
+ expect(out).to include("foo could not be removed.")
+ end
+ end
+
+ describe "with comments that mention gems" do
+ context "when comment is a separate line comment" do
+ it "does not remove the line comment" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ # gem "myrack" might be used in the future
+ gem "myrack"
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ # gem "myrack" might be used in the future
+ G
+ end
+ end
+
+ context "when gem specified for removal has an inline comment" do
+ it "removes the inline comment" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack" # this can be removed
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ context "when gem specified for removal is mentioned in other gem's comment" do
+ it "does not remove other gem" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "puma" # implements interface provided by gem "myrack"
+
+ gem "myrack"
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to_not include("puma was removed.")
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ gem "puma" # implements interface provided by gem "myrack"
+ G
+ end
+ end
+
+ context "when gem specified for removal has a comment that mentions other gem" do
+ it "does not remove other gem" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "puma" # implements interface provided by gem "myrack"
+
+ gem "myrack"
+ G
+
+ bundle "remove puma"
+
+ expect(out).to include("puma was removed.")
+ expect(out).to_not include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+ end
+ end
+ end
+
+ context "when gem definition has parentheses" do
+ it "removes the gem" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem("myrack")
+ gem("myrack", ">= 0")
+ gem("myrack", require: false)
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+end
diff --git a/spec/bundler/commands/show_spec.rb b/spec/bundler/commands/show_spec.rb
new file mode 100644
index 0000000000..d0d55ffbb9
--- /dev/null
+++ b/spec/bundler/commands/show_spec.rb
@@ -0,0 +1,217 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle show" do
+ context "with a standard Gemfile" do
+ before :each do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails"
+ G
+ end
+
+ it "creates a Gemfile.lock if one did not exist" do
+ FileUtils.rm(bundled_app_lock)
+
+ bundle "show"
+
+ expect(bundled_app_lock).to exist
+ end
+
+ it "creates a Gemfile.lock when invoked with a gem name" do
+ FileUtils.rm(bundled_app_lock)
+
+ bundle "show rails"
+
+ expect(bundled_app_lock).to exist
+ end
+
+ it "prints path if gem exists in bundle" do
+ bundle "show rails"
+ expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s)
+ end
+
+ it "prints path if gem exists in bundle (with --paths option)" do
+ bundle "show rails --paths"
+ expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s)
+ end
+
+ it "warns if specification is installed, but path does not exist on disk" do
+ FileUtils.rm_r(default_bundle_path("gems", "rails-2.3.2"))
+
+ bundle "show rails"
+
+ expect(err).to match(/is missing/i)
+ expect(err).to match(default_bundle_path("gems", "rails-2.3.2").to_s)
+ end
+
+ it "prints the path to the running bundler" do
+ bundle "show bundler"
+ expect(out).to eq(root.to_s)
+ end
+
+ it "complains if gem not in bundle" do
+ bundle "show missing", raise_on_error: false
+ expect(err).to match(/could not find gem 'missing'/i)
+ end
+
+ it "prints path of all gems in bundle sorted by name" do
+ bundle "show --paths"
+
+ expect(out).to include(default_bundle_path("gems", "rake-#{rake_version}").to_s)
+ expect(out).to include(default_bundle_path("gems", "rails-2.3.2").to_s)
+
+ # Gem names are the last component of their path.
+ gem_list = out.split.map {|p| p.split("/").last }
+ expect(gem_list).to eq(gem_list.sort)
+ end
+
+ it "prints summary of gems" do
+ bundle "show --verbose"
+
+ expect(out).to include <<~MSG
+ * actionmailer (2.3.2)
+ \tSummary: This is just a fake gem for testing
+ \tHomepage: http://example.com
+ \tStatus: Up to date
+ MSG
+ end
+
+ it "includes bundler in the summary of gems" do
+ bundle "show --verbose"
+
+ expect(out).to include <<~MSG
+ * bundler (#{Bundler::VERSION})
+ \tSummary: The best way to manage your application's dependencies
+ \tHomepage: https://bundler.io
+ \tStatus: Up to date
+ MSG
+ end
+
+ it "includes up to date status in summary of gems" do
+ update_repo2 do
+ build_gem "rails", "3.0.0"
+ end
+
+ bundle "show --verbose"
+
+ expect(out).to include <<~MSG
+ * rails (2.3.2)
+ \tSummary: This is just a fake gem for testing
+ \tHomepage: http://example.com
+ \tStatus: Outdated - 2.3.2 < 3.0.0
+ MSG
+
+ # check lockfile is not accidentally updated
+ expect(lockfile).to include("actionmailer (2.3.2)")
+ end
+ end
+
+ context "with a git repo in the Gemfile" do
+ before :each do
+ @git = build_git "foo", "1.0"
+ end
+
+ it "prints out git info" do
+ install_gemfile <<-G
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+ expect(the_bundle).to include_gems "foo 1.0"
+
+ bundle :show
+ expect(out).to include("foo (1.0 #{@git.ref_for("main", 6)}")
+ end
+
+ it "prints out branch names other than main" do
+ update_git "foo", branch: "omg" do |s|
+ s.write "lib/foo.rb", "FOO = '1.0.omg'"
+ end
+ @revision = revision_for(lib_path("foo-1.0"))[0...6]
+
+ install_gemfile <<-G
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "omg"
+ G
+ expect(the_bundle).to include_gems "foo 1.0.omg"
+
+ bundle :show
+ expect(out).to include("foo (1.0 #{@git.ref_for("omg", 6)}")
+ end
+
+ it "doesn't print the branch when tied to a ref" do
+ sha = revision_for(lib_path("foo-1.0"))
+ install_gemfile <<-G
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{sha}"
+ G
+
+ bundle :show
+ expect(out).to include("foo (1.0 #{sha[0..6]})")
+ end
+
+ it "handles when a version is a '-' prerelease" do
+ @git = build_git("foo", "1.0.0-beta.1", path: lib_path("foo"))
+ install_gemfile <<-G
+ gem "foo", "1.0.0-beta.1", :git => "#{lib_path("foo")}"
+ G
+ expect(the_bundle).to include_gems "foo 1.0.0.pre.beta.1"
+
+ bundle :show
+ expect(out).to include("foo (1.0.0.pre.beta.1")
+ end
+ end
+
+ context "in a fresh gem in a blank git repo" do
+ before :each do
+ build_git "foo", path: lib_path("foo")
+ File.open(lib_path("foo/Gemfile"), "w") {|f| f.puts "gemspec" }
+ sys_exec "rm -rf .git && git init", dir: lib_path("foo")
+ end
+
+ it "does not output git errors" do
+ bundle :show, dir: lib_path("foo")
+ expect(err_without_deprecations).to be_empty
+ end
+ end
+
+ it "performs an automatic bundle install" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo"
+ G
+
+ bundle_config "auto_install 1"
+ bundle :show
+ expect(out).to include("Installing foo 1.0")
+ end
+
+ context "with a valid regexp for gem name" do
+ it "presents alternatives", :readline do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "myrack-obama"
+ G
+
+ bundle "show rac"
+ expect(out).to match(/\A1 : myrack\n2 : myrack-obama\n0 : - exit -(\n>|\z)/)
+ end
+ end
+
+ context "with an invalid regexp for gem name" do
+ it "does not find the gem" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ invalid_regexp = "[]"
+
+ bundle "show #{invalid_regexp}", raise_on_error: false
+ expect(err).to include("Could not find gem '#{invalid_regexp}'.")
+ end
+ end
+end
+
+RSpec.describe "bundle show", bundler: "5" do
+ pending "shows a friendly error about the command removal"
+end
diff --git a/spec/bundler/commands/ssl_spec.rb b/spec/bundler/commands/ssl_spec.rb
new file mode 100644
index 0000000000..4220731b69
--- /dev/null
+++ b/spec/bundler/commands/ssl_spec.rb
@@ -0,0 +1,373 @@
+# frozen_string_literal: true
+
+require "bundler/cli"
+require "bundler/cli/doctor"
+require "bundler/cli/doctor/ssl"
+require_relative "../support/artifice/helpers/artifice"
+require "bundler/vendored_persistent.rb"
+
+RSpec.describe "bundle doctor ssl" do
+ before(:each) do
+ require_rack_test
+ require_relative "../support/artifice/helpers/endpoint"
+
+ @dummy_endpoint = Class.new(Endpoint) do
+ get "/" do
+ end
+ end
+
+ @previous_ui = Bundler.ui
+ Bundler.ui = Bundler::UI::Shell.new
+ Bundler.ui.level = "info"
+
+ @previous_client = Gem::Request::ConnectionPools.client
+ Artifice.activate_with(@dummy_endpoint)
+ Gem::Request::ConnectionPools.client = Gem::Net::HTTP
+ end
+
+ after(:each) do
+ Bundler.ui = @previous_ui
+ Artifice.deactivate
+ Gem::Request::ConnectionPools.client = @previous_client
+ end
+
+ context "when a diagnostic fails" do
+ it "prints the diagnostic when openssl can't be loaded" do
+ subject = Bundler::CLI::Doctor::SSL.new({})
+ allow(subject).to receive(:require).with("openssl").and_raise(LoadError)
+
+ expected_err = <<~MSG
+ Oh no! Your Ruby doesn't have OpenSSL, so it can't connect to rubygems.org.
+ You'll need to recompile or reinstall Ruby with OpenSSL support and try again.
+ MSG
+
+ expect { subject.run }.to output("").to_stdout.and output(expected_err).to_stderr
+ end
+
+ it "fails due to certificate verification", :ruby_repo do
+ net_http = Class.new(Artifice::Net::HTTP) do
+ def connect
+ raise OpenSSL::SSL::SSLError, "certificate verify failed"
+ end
+ end
+
+ Artifice.replace_net_http(net_http)
+ Gem::Request::ConnectionPools.client = net_http
+ Gem::RemoteFetcher.fetcher.close_all
+
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://rubygems.org:
+ MSG
+
+ expected_err = <<~MSG
+ Bundler: failed (certificate verification)
+ RubyGems: failed (certificate verification)
+ Ruby net/http: failed
+
+ Unfortunately, this Ruby can't connect to rubygems.org.
+
+ Below affect only Ruby net/http connections:
+ SSL_CERT_FILE: exists #{OpenSSL::X509::DEFAULT_CERT_FILE}
+ SSL_CERT_DIR: exists #{OpenSSL::X509::DEFAULT_CERT_DIR}
+
+ Your Ruby can't connect to rubygems.org because you are missing the certificate files OpenSSL needs to verify you are connecting to the genuine rubygems.org servers.
+
+ MSG
+
+ subject = Bundler::CLI::Doctor::SSL.new({})
+ expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr
+ end
+
+ it "fails due to a too old tls version" do
+ subject = Bundler::CLI::Doctor::SSL.new({})
+
+ net_http = Class.new(Artifice::Net::HTTP) do
+ def connect
+ raise OpenSSL::SSL::SSLError, "read server hello A"
+ end
+ end
+
+ Artifice.replace_net_http(net_http)
+ Gem::Request::ConnectionPools.client = Gem::Net::HTTP
+ Gem::RemoteFetcher.fetcher.close_all
+
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://rubygems.org:
+ MSG
+
+ expected_err = <<~MSG
+ Bundler: failed (SSL/TLS protocol version mismatch)
+ RubyGems: failed (SSL/TLS protocol version mismatch)
+ Ruby net/http: failed
+
+ Unfortunately, this Ruby can't connect to rubygems.org.
+
+ Your Ruby can't connect to rubygems.org because your version of OpenSSL is too old.
+ You'll need to upgrade your OpenSSL install and/or recompile Ruby to use a newer OpenSSL.
+
+ MSG
+
+ expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr
+ end
+
+ it "fails due to unsupported tls 1.3 version" do
+ net_http = Class.new(Artifice::Net::HTTP) do
+ def connect
+ raise OpenSSL::SSL::SSLError, "read server hello A"
+ end
+ end
+
+ Artifice.replace_net_http(net_http)
+ Gem::Request::ConnectionPools.client = net_http
+ Gem::RemoteFetcher.fetcher.close_all
+
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://rubygems.org:
+ MSG
+
+ expected_err = <<~MSG
+ Bundler: failed (SSL/TLS protocol version mismatch)
+ RubyGems: failed (SSL/TLS protocol version mismatch)
+ Ruby net/http: failed
+
+ Unfortunately, this Ruby can't connect to rubygems.org.
+
+ Your Ruby can't connect to rubygems.org because TLS1_3 isn't supported yet.
+
+ MSG
+
+ subject = Bundler::CLI::Doctor::SSL.new("tls-version": "1.3")
+ expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr
+ end
+
+ it "fails due to a bundler and rubygems connection error" do
+ endpoint = Class.new(Endpoint) do
+ get "/" do
+ raise OpenSSL::SSL::SSLError, "read server hello A"
+ end
+ end
+
+ Artifice.activate_with(endpoint)
+ Gem::Request::ConnectionPools.client = Gem::Net::HTTP
+
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://rubygems.org:
+ Ruby net/http: success
+
+ For some reason, your Ruby installation can connect to rubygems.org, but neither RubyGems nor Bundler can.
+ The most likely fix is to manually upgrade RubyGems by following the instructions at http://ruby.to/ssl-check-failed.
+ After you've done that, run `gem install bundler` to upgrade Bundler, and then run this script again to make sure everything worked. ❣
+
+ MSG
+
+ expected_err = <<~MSG
+ Bundler: failed (SSL/TLS protocol version mismatch)
+ RubyGems: failed (SSL/TLS protocol version mismatch)
+ MSG
+
+ subject = Bundler::CLI::Doctor::SSL.new({})
+ expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr
+ end
+
+ it "fails due to a bundler connection error" do
+ endpoint = Class.new(Endpoint) do
+ get "/" do
+ if request.user_agent.include?("bundler")
+ raise OpenSSL::SSL::SSLError, "read server hello A"
+ end
+ end
+ end
+
+ Artifice.activate_with(endpoint)
+ Gem::Request::ConnectionPools.client = Gem::Net::HTTP
+
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://rubygems.org:
+ RubyGems: success
+ Ruby net/http: success
+
+ Although your Ruby installation and RubyGems can both connect to rubygems.org, Bundler is having trouble.
+ The most likely way to fix this is to upgrade Bundler by running `gem install bundler`.
+ Run this script again after doing that to make sure everything is all set.
+ If you're still having trouble, check out the troubleshooting guide at http://ruby.to/ssl-check-failed.
+
+ MSG
+
+ expected_err = <<~MSG
+ Bundler: failed (SSL/TLS protocol version mismatch)
+ MSG
+
+ subject = Bundler::CLI::Doctor::SSL.new({})
+ expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr
+ end
+
+ it "fails due to a RubyGems connection error" do
+ endpoint = Class.new(Endpoint) do
+ get "/" do
+ if request.user_agent.include?("Ruby, RubyGems")
+ raise OpenSSL::SSL::SSLError, "read server hello A"
+ end
+ end
+ end
+
+ Artifice.activate_with(endpoint)
+ Gem::Request::ConnectionPools.client = Gem::Net::HTTP
+
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://rubygems.org:
+ Bundler: success
+ Ruby net/http: success
+
+ It looks like Ruby and Bundler can connect to rubygems.org, but RubyGems itself cannot.
+ You can likely solve this by manually downloading and installing a RubyGems update.
+ Visit http://ruby.to/ssl-check-failed for instructions on how to manually upgrade RubyGems.
+
+ MSG
+
+ expected_err = <<~MSG
+ RubyGems: failed (SSL/TLS protocol version mismatch)
+ MSG
+
+ subject = Bundler::CLI::Doctor::SSL.new({})
+ expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr
+ end
+ end
+
+ context "when no diagnostic fails" do
+ it "prints the SSL environment" do
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://rubygems.org:
+ Bundler: success
+ RubyGems: success
+ Ruby net/http: success
+
+ Hooray! This Ruby can connect to rubygems.org.
+ You are all set to use Bundler and RubyGems.
+
+ MSG
+
+ subject = Bundler::CLI::Doctor::SSL.new({})
+ expect { subject.run }.to output(expected_out).to_stdout.and output("").to_stderr
+ end
+
+ it "uses the tls_version verify mode and host when given as option" do
+ net_http = Class.new(Artifice::Net::HTTP) do
+ class << self
+ attr_accessor :verify_mode, :min_version, :max_version
+ end
+
+ def connect
+ self.class.verify_mode = verify_mode
+ self.class.min_version = min_version
+ self.class.max_version = max_version
+
+ super
+ end
+ end
+
+ net_http.endpoint = @dummy_endpoint
+ Artifice.replace_net_http(net_http)
+ Gem::Request::ConnectionPools.client = net_http
+ Gem::RemoteFetcher.fetcher.close_all
+
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://example.org:
+ Bundler: success
+ RubyGems: success
+ Ruby net/http: success
+
+ Hooray! This Ruby can connect to example.org.
+ You are all set to use Bundler and RubyGems.
+
+ MSG
+
+ subject = Bundler::CLI::Doctor::SSL.new("tls-version": "1.3", "verify-mode": :none, host: "example.org")
+ expect { subject.run }.to output(expected_out).to_stdout.and output("").to_stderr
+ expect(net_http.verify_mode).to eq(0)
+ expect(net_http.min_version.to_s).to eq("TLS1_3")
+ expect(net_http.max_version.to_s).to eq("TLS1_3")
+ end
+
+ it "warns when TLS1.2 is not supported" do
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://rubygems.org:
+ Bundler: success
+ RubyGems: success
+ Ruby net/http: success
+
+ Hooray! This Ruby can connect to rubygems.org.
+ You are all set to use Bundler and RubyGems.
+
+ MSG
+
+ expected_err = <<~MSG
+
+ WARNING: Although your Ruby can connect to rubygems.org today, your OpenSSL is very old!
+ WARNING: You will need to upgrade OpenSSL to use rubygems.org.
+
+ MSG
+
+ previous_version = OpenSSL::SSL::TLS1_2_VERSION
+ OpenSSL::SSL.send(:remove_const, :TLS1_2_VERSION)
+
+ subject = Bundler::CLI::Doctor::SSL.new({})
+ expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr
+ ensure
+ OpenSSL::SSL.const_set(:TLS1_2_VERSION, previous_version)
+ end
+ end
+end
diff --git a/spec/bundler/commands/update_spec.rb b/spec/bundler/commands/update_spec.rb
new file mode 100644
index 0000000000..03a3786d80
--- /dev/null
+++ b/spec/bundler/commands/update_spec.rb
@@ -0,0 +1,2095 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle update" do
+ describe "with no arguments" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ gem "platform_specific"
+ G
+ end
+
+ it "updates the entire bundle" do
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle "update"
+ expect(out).to include("Bundle updated!")
+ expect(the_bundle).to include_gems "myrack 1.2", "myrack-obama 1.0", "activesupport 3.0"
+ end
+
+ it "doesn't delete the Gemfile.lock file if something goes wrong" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ exit!
+ G
+ bundle "update", raise_on_error: false
+ expect(bundled_app_lock).to exist
+ end
+ end
+
+ describe "with --verbose" do
+ before do
+ build_repo2
+
+ install_gemfile <<~G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+ end
+
+ it "logs the reason for re-resolving" do
+ bundle "update --verbose"
+ expect(out).not_to include("Found changes from the lockfile")
+ expect(out).to include("Re-resolving dependencies because bundler is unlocking")
+ end
+ end
+
+ describe "with --all" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ gem "platform_specific"
+ G
+ end
+
+ it "updates the entire bundle" do
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle "update", all: true
+ expect(out).to include("Bundle updated!")
+ expect(the_bundle).to include_gems "myrack 1.2", "myrack-obama 1.0", "activesupport 3.0"
+ end
+
+ it "doesn't delete the Gemfile.lock file if something goes wrong" do
+ install_gemfile "source 'https://gem.repo1'"
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ exit!
+ G
+ bundle "update", all: true, raise_on_error: false
+ expect(bundled_app_lock).to exist
+ end
+ end
+
+ describe "with --gemfile" do
+ it "creates lockfiles based on the Gemfile name" do
+ gemfile bundled_app("OmgFile"), <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0"
+ G
+
+ bundle "update --gemfile OmgFile", all: true
+
+ expect(bundled_app("OmgFile.lock")).to exist
+ end
+ end
+
+ context "when update_requires_all_flag is set" do
+ before { bundle_config "update_requires_all_flag true" }
+
+ it "errors when passed nothing" do
+ install_gemfile "source 'https://gem.repo1'"
+ bundle :update, raise_on_error: false
+ expect(err).to eq("To update everything, pass the `--all` flag.")
+ end
+
+ it "errors when passed --all and another option" do
+ install_gemfile "source 'https://gem.repo1'"
+ bundle "update --all foo", raise_on_error: false
+ expect(err).to eq("Cannot specify --all along with specific options.")
+ end
+
+ it "updates everything when passed --all" do
+ install_gemfile "source 'https://gem.repo1'"
+ bundle "update --all"
+ expect(out).to include("Bundle updated!")
+ end
+ end
+
+ describe "--quiet argument" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ gem "platform_specific"
+ G
+ end
+
+ it "hides UI messages" do
+ bundle "update --quiet"
+ expect(out).not_to include("Bundle updated!")
+ end
+ end
+
+ describe "with a top level dependency" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ gem "platform_specific"
+ G
+ end
+
+ it "unlocks all child dependencies that are unrelated to other locked dependencies" do
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle "update myrack-obama"
+ expect(the_bundle).to include_gems "myrack 1.2", "myrack-obama 1.0", "activesupport 2.3.5"
+ end
+ end
+
+ describe "with an unknown dependency" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ gem "platform_specific"
+ G
+ end
+
+ it "should inform the user" do
+ bundle "update halting-problem-solver", raise_on_error: false
+ expect(err).to include "Could not find gem 'halting-problem-solver'"
+ end
+ it "should suggest alternatives" do
+ bundle "update platformspecific", raise_on_error: false
+ expect(err).to include "Did you mean 'platform_specific'?"
+ end
+ end
+
+ describe "with a child dependency" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ gem "platform_specific"
+ G
+ end
+
+ it "should update the child dependency" do
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ bundle "update myrack"
+ expect(the_bundle).to include_gems "myrack 1.2"
+ end
+ end
+
+ describe "when a possible resolve requires an older version of a locked gem" do
+ it "does not go to an older version" do
+ build_repo4 do
+ build_gem "tilt", "2.0.8"
+ build_gem "slim", "3.0.9" do |s|
+ s.add_dependency "tilt", [">= 1.3.3", "< 2.1"]
+ end
+ build_gem "slim_lint", "0.16.1" do |s|
+ s.add_dependency "slim", [">= 3.0", "< 5.0"]
+ end
+ build_gem "slim-rails", "0.2.1" do |s|
+ s.add_dependency "slim", ">= 0.9.2"
+ end
+ build_gem "slim-rails", "3.1.3" do |s|
+ s.add_dependency "slim", "~> 3.0"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "slim-rails"
+ gem "slim_lint"
+ G
+
+ expect(the_bundle).to include_gems("slim 3.0.9", "slim-rails 3.1.3", "slim_lint 0.16.1")
+
+ build_repo4 do
+ build_gem "slim", "4.0.0" do |s|
+ s.add_dependency "tilt", [">= 2.0.6", "< 2.1"]
+ end
+ end
+
+ bundle "update", all: true
+
+ expect(the_bundle).to include_gems("slim 3.0.9", "slim-rails 3.1.3", "slim_lint 0.16.1")
+ end
+
+ it "does not go to an older version, even if the version upgrade that could cause another gem to downgrade is activated first" do
+ build_repo4 do
+ # countries is processed before country_select by the resolver due to having less spec groups (groups of versions with the same dependencies) (2 vs 3)
+
+ build_gem "countries", "2.1.4"
+ build_gem "countries", "3.1.0"
+
+ build_gem "countries", "4.0.0" do |s|
+ s.add_dependency "sixarm_ruby_unaccent", "~> 1.1"
+ end
+
+ build_gem "country_select", "1.2.0"
+
+ build_gem "country_select", "2.1.4" do |s|
+ s.add_dependency "countries", "~> 2.0"
+ end
+ build_gem "country_select", "3.1.1" do |s|
+ s.add_dependency "countries", "~> 2.0"
+ end
+
+ build_gem "country_select", "5.1.0" do |s|
+ s.add_dependency "countries", "~> 3.0"
+ end
+
+ build_gem "sixarm_ruby_unaccent", "1.1.0"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "country_select"
+ gem "countries"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo4, "countries", "3.1.0")
+ c.checksum(gem_repo4, "country_select", "5.1.0")
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ countries (3.1.0)
+ country_select (5.1.0)
+ countries (~> 3.0)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ countries
+ country_select
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ previous_lockfile = lockfile
+
+ bundle "lock --update", env: { "DEBUG" => "1" }, verbose: true
+
+ expect(lockfile).to eq(previous_lockfile)
+ end
+
+ it "does not downgrade direct dependencies when run with --conservative" do
+ build_repo4 do
+ build_gem "oauth2", "2.0.6" do |s|
+ s.add_dependency "faraday", ">= 0.17.3", "< 3.0"
+ end
+
+ build_gem "oauth2", "1.4.10" do |s|
+ s.add_dependency "faraday", ">= 0.17.3", "< 3.0"
+ s.add_dependency "multi_json", "~> 1.3"
+ end
+
+ build_gem "faraday", "2.5.2"
+
+ build_gem "multi_json", "1.15.0"
+
+ build_gem "quickbooks-ruby", "1.0.19" do |s|
+ s.add_dependency "oauth2", "~> 1.4"
+ end
+
+ build_gem "quickbooks-ruby", "0.1.9" do |s|
+ s.add_dependency "oauth2"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "oauth2"
+ gem "quickbooks-ruby"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ faraday (2.5.2)
+ multi_json (1.15.0)
+ oauth2 (1.4.10)
+ faraday (>= 0.17.3, < 3.0)
+ multi_json (~> 1.3)
+ quickbooks-ruby (1.0.19)
+ oauth2 (~> 1.4)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ oauth2
+ quickbooks-ruby
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "update --conservative --verbose"
+
+ expect(out).not_to include("Installing quickbooks-ruby 0.1.9")
+ expect(out).to include("Installing quickbooks-ruby 1.0.19").and include("Installing oauth2 1.4.10")
+ end
+
+ it "does not downgrade direct dependencies when using gemspec sources" do
+ create_file("rails.gemspec", <<-G)
+ Gem::Specification.new do |gem|
+ gem.name = "rails"
+ gem.version = "7.1.0.alpha"
+ gem.author = "DHH"
+ gem.summary = "Full-stack web application framework."
+ end
+ G
+
+ build_repo4 do
+ build_gem "rake", "12.3.3"
+ build_gem "rake", "13.0.6"
+
+ build_gem "sneakers", "2.11.0" do |s|
+ s.add_dependency "rake"
+ end
+
+ build_gem "sneakers", "2.12.0" do |s|
+ s.add_dependency "rake", "~> 12.3"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gemspec
+
+ gem "rake"
+ gem "sneakers"
+ G
+
+ lockfile <<~L
+ PATH
+ remote: .
+ specs:
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ rake (13.0.6)
+ sneakers (2.11.0)
+ rake
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ rake
+ sneakers
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "update --verbose"
+
+ expect(out).not_to include("Installing sneakers 2.12.0")
+ expect(out).not_to include("Installing rake 12.3.3")
+ expect(out).to include("Installing sneakers 2.11.0").and include("Installing rake 13.0.6")
+ end
+
+ it "downgrades indirect dependencies if required to fulfill an explicit upgrade request" do
+ build_repo4 do
+ build_gem "rbs", "3.6.1"
+ build_gem "rbs", "3.9.4"
+
+ build_gem "solargraph", "0.56.0" do |s|
+ s.add_dependency "rbs", "~> 3.3"
+ end
+
+ build_gem "solargraph", "0.56.2" do |s|
+ s.add_dependency "rbs", "~> 3.6.1"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem 'solargraph', '~> 0.56.0'
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ rbs (3.9.4)
+ solargraph (0.56.0)
+ rbs (~> 3.3)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ solargraph (~> 0.56.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --update solargraph"
+
+ expect(lockfile).to include("solargraph (0.56.2)")
+ end
+
+ it "does not downgrade direct dependencies unnecessarily" do
+ build_repo4 do
+ build_gem "redis", "4.8.1"
+ build_gem "redis", "5.3.0"
+
+ build_gem "sidekiq", "6.5.5" do |s|
+ s.add_dependency "redis", ">= 4.5.0"
+ end
+
+ build_gem "sidekiq", "6.5.12" do |s|
+ s.add_dependency "redis", ">= 4.5.0", "< 5"
+ end
+
+ # one version of sidekiq above Gemfile's range is needed to make the
+ # resolver choose `redis` first and trying to upgrade it, reproducing
+ # the accidental sidekiq downgrade as a result
+ build_gem "sidekiq", "7.0.0 " do |s|
+ s.add_dependency "redis", ">= 4.2.0"
+ end
+
+ build_gem "sentry-sidekiq", "5.22.0" do |s|
+ s.add_dependency "sidekiq", ">= 3.0"
+ end
+
+ build_gem "sentry-sidekiq", "5.22.4" do |s|
+ s.add_dependency "sidekiq", ">= 3.0"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "redis"
+ gem "sidekiq", "~> 6.5"
+ gem "sentry-sidekiq"
+ G
+
+ original_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ redis (4.8.1)
+ sentry-sidekiq (5.22.0)
+ sidekiq (>= 3.0)
+ sidekiq (6.5.12)
+ redis (>= 4.5.0, < 5)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ redis
+ sentry-sidekiq
+ sidekiq (~> 6.5)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile original_lockfile
+
+ bundle "lock --update sentry-sidekiq"
+
+ expect(lockfile).to eq(original_lockfile.sub("sentry-sidekiq (5.22.0)", "sentry-sidekiq (5.22.4)"))
+ end
+
+ it "does not downgrade indirect dependencies unnecessarily" do
+ build_repo4 do
+ build_gem "a" do |s|
+ s.add_dependency "b"
+ s.add_dependency "c"
+ end
+ build_gem "b"
+ build_gem "c"
+ build_gem "c", "2.0"
+ end
+
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo4"
+ gem "a"
+ G
+
+ expect(the_bundle).to include_gems("a 1.0", "b 1.0", "c 2.0")
+
+ build_repo4 do
+ build_gem "b", "2.0" do |s|
+ s.add_dependency "c", "< 2"
+ end
+ end
+
+ bundle "update", all: true, verbose: true
+ expect(the_bundle).to include_gems("a 1.0", "b 1.0", "c 2.0")
+ end
+
+ it "should still downgrade if forced by the Gemfile" do
+ build_repo4 do
+ build_gem "a"
+ build_gem "b", "1.0"
+ build_gem "b", "2.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "a"
+ gem "b"
+ G
+
+ expect(the_bundle).to include_gems("a 1.0", "b 2.0")
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "a"
+ gem "b", "1.0"
+ G
+
+ bundle "update b"
+
+ expect(the_bundle).to include_gems("a 1.0", "b 1.0")
+ end
+
+ it "should still downgrade if forced by the Gemfile, when transitive dependencies also need downgrade" do
+ build_repo4 do
+ build_gem "activesupport", "6.1.4.1" do |s|
+ s.add_dependency "tzinfo", "~> 2.0"
+ end
+
+ build_gem "activesupport", "6.0.4.1" do |s|
+ s.add_dependency "tzinfo", "~> 1.1"
+ end
+
+ build_gem "tzinfo", "2.0.4"
+ build_gem "tzinfo", "1.2.9"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "activesupport", "~> 6.1.0"
+ G
+
+ expect(the_bundle).to include_gems("activesupport 6.1.4.1", "tzinfo 2.0.4")
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "activesupport", "~> 6.0.0"
+ G
+
+ original_lockfile = lockfile
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "activesupport", "6.0.4.1"
+ c.checksum gem_repo4, "tzinfo", "1.2.9"
+ end
+
+ expected_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ activesupport (6.0.4.1)
+ tzinfo (~> 1.1)
+ tzinfo (1.2.9)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ activesupport (~> 6.0.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "update activesupport"
+ expect(the_bundle).to include_gems("activesupport 6.0.4.1", "tzinfo 1.2.9")
+ expect(lockfile).to eq(expected_lockfile)
+
+ lockfile original_lockfile
+ bundle "update"
+ expect(the_bundle).to include_gems("activesupport 6.0.4.1", "tzinfo 1.2.9")
+ expect(lockfile).to eq(expected_lockfile)
+
+ lockfile original_lockfile
+ bundle "lock --update"
+ expect(the_bundle).to include_gems("activesupport 6.0.4.1", "tzinfo 1.2.9")
+ expect(lockfile).to eq(expected_lockfile)
+ end
+ end
+
+ describe "with --local option" do
+ before do
+ build_repo2
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ gem "platform_specific"
+ G
+ end
+
+ it "doesn't hit repo2" do
+ simulate_platform "x86-darwin-100" do
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ activesupport (2.3.5)
+ platform_specific (1.0-x86-darwin-100)
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ x86-darwin-100
+
+ DEPENDENCIES
+ activesupport
+ platform_specific
+ myrack-obama
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ FileUtils.rm_r(gem_repo2)
+
+ bundle "update --local --all"
+ expect(out).not_to include("Fetching source index")
+ end
+ end
+ end
+
+ describe "with --group option" do
+ before do
+ build_repo2
+ end
+
+ it "should update only specified group gems" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", :group => :development
+ gem "myrack"
+ G
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "activesupport", "3.0"
+ end
+ bundle "update --group development"
+ expect(the_bundle).to include_gems "activesupport 3.0"
+ expect(the_bundle).not_to include_gems "myrack 1.2"
+ end
+
+ context "when conservatively updating a group with non-group sub-deps" do
+ it "should update only specified group gems" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activemerchant", :group => :development
+ gem "activesupport"
+ G
+ update_repo2 do
+ build_gem "activemerchant", "2.0"
+ build_gem "activesupport", "3.0"
+ end
+ bundle "update --conservative --group development"
+ expect(the_bundle).to include_gems "activemerchant 2.0"
+ expect(the_bundle).not_to include_gems "activesupport 3.0"
+ end
+ end
+
+ context "when there is a source with the same name as a gem in a group" do
+ before do
+ build_git "foo", path: lib_path("activesupport")
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", :group => :development
+ gem "foo", :git => "#{lib_path("activesupport")}"
+ G
+ end
+
+ it "should not update the gems from that source" do
+ update_repo2 { build_gem "activesupport", "3.0" }
+ update_git "foo", "2.0", path: lib_path("activesupport")
+
+ bundle "update --group development"
+ expect(the_bundle).to include_gems "activesupport 3.0"
+ expect(the_bundle).not_to include_gems "foo 2.0"
+ end
+ end
+
+ context "when bundler itself is a transitive dependency" do
+ it "executes without error" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport", :group => :development
+ gem "myrack"
+ G
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "activesupport", "3.0"
+ end
+ bundle "update --group development"
+ expect(the_bundle).to include_gems "activesupport 2.3.5"
+ expect(the_bundle).to include_gems "bundler #{Bundler::VERSION}"
+ expect(the_bundle).not_to include_gems "myrack 1.2"
+ end
+ end
+ end
+
+ describe "in a frozen bundle" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ gem "platform_specific"
+ G
+ end
+
+ it "should fail loudly" do
+ bundle_config "deployment true"
+ bundle "update", all: true, raise_on_error: false
+
+ expect(last_command).to be_failure
+ expect(err).to eq <<~ERROR.strip
+ Bundler is unlocking, but the lockfile can't be updated because frozen mode is set
+
+ If this is a development machine, remove the Gemfile.lock freeze by running `bundle config set frozen false`.
+ ERROR
+ end
+
+ it "should fail loudly when frozen is set globally" do
+ bundle_config_global "frozen 1"
+ bundle "update", all: true, raise_on_error: false
+ expect(err).to eq <<~ERROR.strip
+ Bundler is unlocking, but the lockfile can't be updated because frozen mode is set
+
+ If this is a development machine, remove the Gemfile.lock freeze by running `bundle config set frozen false`.
+ ERROR
+ end
+
+ it "should fail loudly when deployment is set globally" do
+ bundle_config_global "deployment true"
+ bundle "update", all: true, raise_on_error: false
+ expect(err).to eq <<~ERROR.strip
+ Bundler is unlocking, but the lockfile can't be updated because frozen mode is set
+
+ If this is a development machine, remove the Gemfile.lock freeze by running `bundle config set frozen false`.
+ ERROR
+ end
+
+ it "should not suggest any command to unfreeze bundler if frozen is set through ENV" do
+ bundle "update", all: true, raise_on_error: false, env: { "BUNDLE_FROZEN" => "true" }
+ expect(err).to eq("Bundler is unlocking, but the lockfile can't be updated because frozen mode is set")
+ end
+ end
+
+ describe "with --source option" do
+ before do
+ build_repo2
+ end
+
+ it "should not update gems not included in the source that happen to have the same name" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ G
+ update_repo2 { build_gem "activesupport", "3.0" }
+
+ bundle "update --source activesupport"
+ expect(the_bundle).not_to include_gem "activesupport 3.0"
+ end
+
+ it "should not update gems not included in the source that happen to have the same name" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ G
+ update_repo2 { build_gem "activesupport", "3.0" }
+
+ bundle "update --source activesupport"
+ expect(the_bundle).not_to include_gems "activesupport 3.0"
+ end
+ end
+
+ context "when there is a child dependency that is also in the gemfile" do
+ before do
+ build_repo2 do
+ build_gem "fred", "1.0"
+ build_gem "harry", "1.0" do |s|
+ s.add_dependency "fred"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "harry"
+ gem "fred"
+ G
+ end
+
+ it "should not update the child dependencies of a gem that has the same name as the source" do
+ update_repo2 do
+ build_gem "fred", "2.0"
+ build_gem "harry", "2.0" do |s|
+ s.add_dependency "fred"
+ end
+ end
+
+ bundle "update --source harry"
+ expect(the_bundle).to include_gems "harry 1.0", "fred 1.0"
+ end
+ end
+
+ context "when there is a child dependency that appears elsewhere in the dependency graph" do
+ before do
+ build_repo2 do
+ build_gem "fred", "1.0" do |s|
+ s.add_dependency "george"
+ end
+ build_gem "george", "1.0"
+ build_gem "harry", "1.0" do |s|
+ s.add_dependency "george"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "harry"
+ gem "fred"
+ G
+ end
+
+ it "should not update the child dependencies of a gem that has the same name as the source" do
+ update_repo2 do
+ build_gem "george", "2.0"
+ build_gem "harry", "2.0" do |s|
+ s.add_dependency "george"
+ end
+ end
+
+ bundle "update --source harry"
+ expect(the_bundle).to include_gems "harry 1.0", "fred 1.0", "george 1.0"
+ end
+ end
+
+ it "shows the previous version of the gem when updated from rubygems source" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ G
+
+ bundle "update", all: true, verbose: true
+ expect(out).to include("Using activesupport 2.3.5")
+
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle "update", all: true
+ expect(out).to include("Installing activesupport 3.0 (was 2.3.5)")
+ end
+
+ it "only prints `Using` for versions that have changed" do
+ build_repo4 do
+ build_gem "bar"
+ build_gem "foo"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "bar"
+ gem "foo"
+ G
+
+ bundle "update", all: true
+ expect(out).to match(/Resolving dependencies\.\.\.\.*\nBundle updated!/)
+
+ build_repo4 do
+ build_gem "foo", "2.0"
+ end
+
+ bundle "update", all: true
+ expect(out.sub("Removing foo (1.0)\n", "")).to match(/Resolving dependencies\.\.\.\.*\nFetching foo 2\.0 \(was 1\.0\)\nInstalling foo 2\.0 \(was 1\.0\)\nBundle updated/)
+ end
+
+ it "shows error message when Gemfile.lock is not preset and gem is specified" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ G
+
+ bundle "update nonexisting", raise_on_error: false
+ expect(err).to include("This Bundle hasn't been installed yet. Run `bundle install` to update and install the bundled gems.")
+ expect(exitstatus).to eq(22)
+ end
+
+ context "with multiple sources and caching enabled" do
+ before do
+ build_repo2 do
+ build_gem "myrack", "1.0.0"
+
+ build_gem "request_store", "1.0.0" do |s|
+ s.add_dependency "myrack", "1.0.0"
+ end
+ end
+
+ build_repo4 do
+ # set up repo with no gems
+ end
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ gem "request_store"
+
+ source "https://gem.repo4" do
+ end
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+ request_store (1.0.0)
+ myrack (= 1.0.0)
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ request_store
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "works" do
+ bundle :install
+ bundle :cache
+
+ update_repo2 do
+ build_gem "request_store", "1.1.0" do |s|
+ s.add_dependency "myrack", "1.0.0"
+ end
+ end
+
+ bundle "update request_store"
+
+ expect(out).to include("Bundle updated!")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+ request_store (1.1.0)
+ myrack (= 1.0.0)
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ request_store
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+end
+
+RSpec.describe "bundle update in more complicated situations" do
+ before do
+ build_repo2
+ end
+
+ it "will eagerly unlock dependencies of a specified gem" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "thin"
+ gem "myrack-obama"
+ G
+
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "thin", "2.0" do |s|
+ s.add_dependency "myrack"
+ end
+ end
+
+ bundle "update thin"
+ expect(the_bundle).to include_gems "thin 2.0", "myrack 1.2", "myrack-obama 1.0"
+ end
+
+ it "will warn when some explicitly updated gems are not updated" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "thin"
+ gem "myrack-obama"
+ G
+
+ update_repo2 do
+ build_gem("thin", "2.0") {|s| s.add_dependency "myrack" }
+ build_gem "myrack", "10.0"
+ end
+
+ bundle "update thin myrack-obama"
+ expect(stdboth).to include "Bundler attempted to update myrack-obama but its version stayed the same"
+ expect(the_bundle).to include_gems "thin 2.0", "myrack 10.0", "myrack-obama 1.0"
+ end
+
+ it "will not warn when an explicitly updated git gem changes sha but not version" do
+ build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+
+ update_git "foo" do |s|
+ s.write "lib/foo2.rb", "puts :foo2"
+ end
+
+ bundle "update foo"
+
+ expect(stdboth).not_to include "attempted to update"
+ end
+
+ it "will not warn when changing gem sources but not versions" do
+ build_git "myrack"
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack", :git => '#{lib_path("myrack-1.0")}'
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "update myrack"
+
+ expect(stdboth).not_to include "attempted to update"
+ end
+
+ it "will update only from pinned source" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ source "https://gem.repo1" do
+ gem "thin"
+ end
+ G
+
+ update_repo2 do
+ build_gem "thin", "2.0"
+ end
+
+ bundle "update", artifice: "compact_index"
+ expect(the_bundle).to include_gems "thin 1.0"
+ end
+
+ context "when the lockfile is for a different platform" do
+ around do |example|
+ build_repo4 do
+ build_gem("a", "0.9")
+ build_gem("a", "0.9") {|s| s.platform = "java" }
+ build_gem("a", "1.1")
+ build_gem("a", "1.1") {|s| s.platform = "java" }
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "a"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ a (0.9-java)
+
+ PLATFORMS
+ java
+
+ DEPENDENCIES
+ a
+ L
+
+ simulate_platform "x86_64-linux", &example
+ end
+
+ it "allows updating" do
+ bundle :update, all: true
+ expect(the_bundle).to include_gem "a 1.1"
+ end
+
+ it "allows updating a specific gem" do
+ bundle "update a"
+ expect(the_bundle).to include_gem "a 1.1"
+ end
+ end
+
+ context "when the dependency is for a different platform" do
+ before do
+ build_repo4 do
+ build_gem("a", "0.9") {|s| s.platform = "java" }
+ build_gem("a", "1.1") {|s| s.platform = "java" }
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "a", platform: :jruby
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ a (0.9-java)
+
+ PLATFORMS
+ java
+
+ DEPENDENCIES
+ a
+ L
+ end
+
+ it "is not updated because it is not actually included in the bundle" do
+ simulate_platform "x86_64-linux" do
+ bundle "update a"
+ expect(stdboth).to include "Bundler attempted to update a but it was not considered because it is for a different platform from the current one"
+ expect(the_bundle).to_not include_gem "a"
+ end
+ end
+ end
+end
+
+RSpec.describe "bundle update without a Gemfile.lock" do
+ it "should not explode" do
+ build_repo2
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "myrack", "1.0"
+ G
+
+ bundle "update", all: true
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+end
+
+RSpec.describe "bundle update when a gem depends on a newer version of bundler" do
+ before do
+ build_repo2 do
+ build_gem "rails", "3.0.1" do |s|
+ s.add_dependency "bundler", "9.9.9"
+ end
+
+ build_gem "bundler", "9.9.9"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "3.0.1"
+ G
+ end
+
+ it "should explain that bundler conflicted and how to resolve the conflict" do
+ bundle "update", all: true, raise_on_error: false
+ expect(stdboth).not_to match(/in snapshot/i)
+ expect(err).to match(/current Bundler version/i).
+ and match(/Install the necessary version with `gem install bundler:9\.9\.9`/i)
+ end
+end
+
+RSpec.describe "bundle update --ruby" do
+ context "when the Gemfile removes the ruby" do
+ before do
+ install_gemfile <<-G
+ ruby '~> #{Gem.ruby_version}'
+ source "https://gem.repo1"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ G
+ end
+
+ it "removes the Ruby from the Gemfile.lock" do
+ bundle "update --ruby"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ #{checksums_section_when_enabled}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when the Gemfile specified an updated Ruby version" do
+ before do
+ install_gemfile <<-G
+ ruby '~> #{Gem.ruby_version}'
+ source "https://gem.repo1"
+ G
+
+ gemfile <<-G
+ ruby '~> #{current_ruby_minor}'
+ source "https://gem.repo1"
+ G
+ end
+
+ it "updates the Gemfile.lock with the latest version" do
+ bundle "update --ruby"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ #{checksums_section_when_enabled}
+ RUBY VERSION
+ #{Bundler::RubyVersion.system}
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when a different Ruby is being used than has been versioned" do
+ before do
+ install_gemfile <<-G
+ ruby '~> #{Gem.ruby_version}'
+ source "https://gem.repo1"
+ G
+
+ gemfile <<-G
+ ruby '~> 2.1.0'
+ source "https://gem.repo1"
+ G
+ end
+ it "shows a helpful error message" do
+ bundle "update --ruby", raise_on_error: false
+
+ expect(err).to include("Your Ruby version is #{Bundler::RubyVersion.system.gem_version}, but your Gemfile specified ~> 2.1.0")
+ end
+ end
+
+ context "when updating Ruby version and Gemfile `ruby`" do
+ before do
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+
+ CHECKSUMS
+
+ RUBY VERSION
+ ruby 2.1.4p222
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile <<-G
+ ruby '~> #{Gem.ruby_version}'
+ source "https://gem.repo1"
+ G
+ end
+
+ it "updates the Gemfile.lock with the latest version" do
+ bundle "update --ruby"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ #{checksums_section_when_enabled}
+ RUBY VERSION
+ #{Bundler::RubyVersion.system}
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+end
+
+RSpec.describe "bundle update --bundler" do
+ it "updates the bundler version in the lockfile" do
+ build_repo4 do
+ build_gem "bundler", "2.5.9"
+ build_gem "myrack", "1.0"
+ end
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo4, "myrack", "1.0")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, '\11.0.0\2')
+
+ bundle :update, bundler: true, verbose: true
+ expect(out).to include("Using bundler #{Bundler::VERSION}")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ expect(the_bundle).to include_gem "myrack 1.0"
+ end
+
+ it "updates the bundler version in the lockfile without re-resolving if the highest version is already installed" do
+ build_repo4 do
+ build_gem "bundler", "2.3.9"
+ build_gem "myrack", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+ lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, "2.3.9")
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo4, "myrack", "1.0")
+ end
+
+ bundle :update, bundler: true, verbose: true
+ expect(out).to include("Using bundler #{Bundler::VERSION}")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ expect(the_bundle).to include_gem "myrack 1.0"
+ end
+
+ it "updates the bundler version in the lockfile even if the latest version is not installed", :ruby_repo do
+ bundle_config "path.system true"
+
+ pristine_system_gems "bundler-9.0.0"
+
+ build_repo4 do
+ build_gem "myrack", "1.0"
+
+ build_bundler "999.0.0"
+ end
+
+ checksums = checksums_section do |c|
+ c.checksum(gem_repo4, "myrack", "1.0")
+ c.checksum(gem_repo4, "bundler", "999.0.0")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+
+ bundle :update, bundler: true, verbose: true
+
+ expect(out).to include("Updating bundler to 999.0.0")
+ expect(out).to include("Running `bundle update --bundler \"> 0.a\" --verbose` with bundler 999.0.0")
+ expect(out).not_to include("Installing Bundler 2.99.9 and restarting using that version.")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ 999.0.0
+ L
+
+ expect(the_bundle).to include_gems "bundler 999.0.0"
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+
+ it "does not claim to update to Bundler version to a wrong version when cached gems are present" do
+ pristine_system_gems "bundler-4.99.0"
+
+ build_repo4 do
+ build_gem "myrack", "3.0.9.1"
+
+ build_bundler "4.99.0"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (3.0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ 2.99.0
+ L
+
+ bundle :cache, verbose: true
+
+ bundle :update, bundler: true, verbose: true
+
+ expect(out).not_to include("Updating bundler to")
+ end
+
+ it "does not update the bundler version in the lockfile if the latest version is not compatible with current ruby", :ruby_repo do
+ pristine_system_gems "bundler-9.9.9"
+
+ build_repo4 do
+ build_gem "myrack", "1.0"
+
+ build_bundler "9.9.9"
+ build_bundler "999.0.0" do |s|
+ s.required_ruby_version = "> #{Gem.ruby_version}"
+ end
+ end
+
+ checksums = checksums_section do |c|
+ c.checksum(gem_repo4, "myrack", "1.0")
+ c.checksum(gem_repo4, "bundler", "9.9.9")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+
+ bundle :update, bundler: true, verbose: true
+
+ expect(out).to include("Using bundler 9.9.9")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ 9.9.9
+ L
+
+ expect(the_bundle).to include_gems "bundler 9.9.9"
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+
+ it "errors if the explicit target version does not exist" do
+ pristine_system_gems "bundler-9.9.9"
+
+ build_repo4 do
+ build_gem "myrack", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+
+ bundle :update, bundler: "999.999.999", raise_on_error: false
+
+ expect(last_command).to be_failure
+ expect(err).to eq("The `bundle update --bundler` target version (999.999.999) does not exist")
+ end
+
+ it "errors if the explicit target version does not exist, even if auto switching is disabled" do
+ pristine_system_gems "bundler-9.9.9"
+
+ build_repo4 do
+ build_gem "myrack", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+
+ bundle :update, bundler: "999.999.999", raise_on_error: false, env: { "BUNDLER_VERSION" => "9.9.9" }
+
+ expect(last_command).to be_failure
+ expect(err).to eq("The `bundle update --bundler` target version (999.999.999) does not exist")
+ end
+
+ it "allows updating to development versions if already installed locally" do
+ system_gems "bundler-9.9.9"
+
+ build_repo4 do
+ build_gem "myrack", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+
+ system_gems "bundler-9.0.0.dev", path: local_gem_path
+ bundle :update, bundler: "9.0.0.dev", verbose: "true"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo4, "myrack", "1.0")
+ end
+ checksums.delete("bundler")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ 9.0.0.dev
+ L
+
+ expect(out).to include("Using bundler 9.0.0.dev")
+ end
+
+ it "does not touch the network if not necessary" do
+ system_gems "bundler-9.9.9"
+
+ build_repo4 do
+ build_gem "myrack", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+ system_gems "bundler-9.0.0", path: local_gem_path
+ bundle :update, bundler: "9.0.0", verbose: true
+
+ expect(out).not_to include("Fetching gem metadata from https://rubygems.org/")
+
+ # Only updates properly on modern RubyGems.
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo4, "myrack", "1.0")
+ c.checksum(local_gem_path, "bundler", "9.0.0", Gem::Platform::RUBY, "cache")
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ 9.0.0
+ L
+
+ expect(out).to include("Using bundler 9.0.0")
+ end
+
+ it "prints an error when trying to update bundler in frozen mode" do
+ system_gems "bundler-9.0.0"
+
+ gemfile <<~G
+ source "https://gem.repo2"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+
+ BUNDLED WITH
+ 9.0.0
+ L
+
+ system_gems "bundler-9.9.9", path: local_gem_path
+
+ bundle "update --bundler=9.9.9", env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
+ expect(err).to include("An update to the version of Bundler itself was requested, but the lockfile can't be updated because frozen mode is set")
+ end
+end
+
+# these specs are slow and focus on integration and therefore are not exhaustive. unit specs elsewhere handle that.
+RSpec.describe "bundle update conservative" do
+ context "patch and minor options" do
+ before do
+ build_repo4 do
+ build_gem "foo", %w[1.4.3 1.4.4] do |s|
+ s.add_dependency "bar", "~> 2.0"
+ end
+ build_gem "foo", %w[1.4.5 1.5.0] do |s|
+ s.add_dependency "bar", "~> 2.1"
+ end
+ build_gem "foo", %w[1.5.1] do |s|
+ s.add_dependency "bar", "~> 3.0"
+ end
+ build_gem "foo", %w[2.0.0.pre] do |s|
+ s.add_dependency "bar"
+ end
+ build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 2.1.2.pre 3.0.0 3.1.0.pre 4.0.0.pre]
+ build_gem "qux", %w[1.0.0 1.0.1 1.1.0 2.0.0]
+ end
+
+ # establish a lockfile set to 1.4.3
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo', '1.4.3'
+ gem 'bar', '2.0.3'
+ gem 'qux', '1.0.0'
+ G
+
+ # remove 1.4.3 requirement and bar altogether
+ # to setup update specs below
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo'
+ gem 'qux'
+ G
+ end
+
+ context "with patch set as default update level in config" do
+ it "should do a patch level update" do
+ bundle_config "prefer_patch true"
+ bundle "update foo"
+
+ expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.0"
+ end
+ end
+
+ context "patch preferred" do
+ it "single gem updates dependent gem to minor" do
+ bundle "update --patch foo"
+
+ expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.0"
+ end
+
+ it "update all" do
+ bundle "update --patch", all: true
+
+ expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.1"
+ end
+ end
+
+ context "minor preferred" do
+ it "single gem updates dependent gem to major" do
+ bundle "update --minor foo"
+
+ expect(the_bundle).to include_gems "foo 1.5.1", "bar 3.0.0", "qux 1.0.0"
+ end
+ end
+
+ context "strict" do
+ it "patch preferred" do
+ bundle "update --patch foo bar --strict"
+
+ expect(the_bundle).to include_gems "foo 1.4.4", "bar 2.0.5", "qux 1.0.0"
+ end
+
+ it "minor preferred" do
+ bundle "update --minor --strict", all: true
+
+ expect(the_bundle).to include_gems "foo 1.5.0", "bar 2.1.1", "qux 1.1.0"
+ end
+ end
+
+ context "pre" do
+ it "defaults to major" do
+ bundle "update --pre foo bar"
+
+ expect(the_bundle).to include_gems "foo 2.0.0.pre", "bar 4.0.0.pre", "qux 1.0.0"
+ end
+
+ it "patch preferred" do
+ bundle "update --patch --pre foo bar"
+
+ expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.2.pre", "qux 1.0.0"
+ end
+
+ it "minor preferred" do
+ bundle "update --minor --pre foo bar"
+
+ expect(the_bundle).to include_gems "foo 1.5.1", "bar 3.1.0.pre", "qux 1.0.0"
+ end
+
+ it "major preferred" do
+ bundle "update --major --pre foo bar"
+
+ expect(the_bundle).to include_gems "foo 2.0.0.pre", "bar 4.0.0.pre", "qux 1.0.0"
+ end
+ end
+ end
+
+ context "eager unlocking" do
+ before do
+ build_repo4 do
+ build_gem "isolated_owner", %w[1.0.1 1.0.2] do |s|
+ s.add_dependency "isolated_dep", "~> 2.0"
+ end
+ build_gem "isolated_dep", %w[2.0.1 2.0.2]
+
+ build_gem "shared_owner_a", %w[3.0.1 3.0.2] do |s|
+ s.add_dependency "shared_dep", "~> 5.0"
+ end
+ build_gem "shared_owner_b", %w[4.0.1 4.0.2] do |s|
+ s.add_dependency "shared_dep", "~> 5.0"
+ end
+ build_gem "shared_dep", %w[5.0.1 5.0.2]
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'isolated_owner'
+
+ gem 'shared_owner_a'
+ gem 'shared_owner_b'
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ isolated_dep (2.0.1)
+ isolated_owner (1.0.1)
+ isolated_dep (~> 2.0)
+ shared_dep (5.0.1)
+ shared_owner_a (3.0.1)
+ shared_dep (~> 5.0)
+ shared_owner_b (4.0.1)
+ shared_dep (~> 5.0)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ isolated_owner
+ shared_owner_a
+ shared_owner_b
+
+ CHECKSUMS
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "should eagerly unlock isolated dependency" do
+ bundle "update isolated_owner"
+
+ expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.2", "shared_dep 5.0.1", "shared_owner_a 3.0.1", "shared_owner_b 4.0.1"
+ end
+
+ it "should eagerly unlock shared dependency" do
+ bundle "update shared_owner_a"
+
+ expect(the_bundle).to include_gems "isolated_owner 1.0.1", "isolated_dep 2.0.1", "shared_dep 5.0.2", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1"
+ end
+
+ it "should not eagerly unlock with --conservative" do
+ bundle "update --conservative shared_owner_a isolated_owner"
+
+ expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1"
+ end
+
+ it "should only update direct dependencies when fully updating with --conservative" do
+ bundle "update --conservative"
+
+ expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.2"
+ end
+
+ it "should only change direct dependencies when updating the lockfile with --conservative" do
+ bundle "lock --update --conservative"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "isolated_dep", "2.0.1"
+ c.checksum gem_repo4, "isolated_owner", "1.0.2"
+ c.checksum gem_repo4, "shared_dep", "5.0.1"
+ c.checksum gem_repo4, "shared_owner_a", "3.0.2"
+ c.checksum gem_repo4, "shared_owner_b", "4.0.2"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ isolated_dep (2.0.1)
+ isolated_owner (1.0.2)
+ isolated_dep (~> 2.0)
+ shared_dep (5.0.1)
+ shared_owner_a (3.0.2)
+ shared_dep (~> 5.0)
+ shared_owner_b (4.0.2)
+ shared_dep (~> 5.0)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ isolated_owner
+ shared_owner_a
+ shared_owner_b
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "should match bundle install conservative update behavior when not eagerly unlocking" do
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'isolated_owner', '1.0.2'
+
+ gem 'shared_owner_a', '3.0.2'
+ gem 'shared_owner_b'
+ G
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1"
+ end
+ end
+
+ context "when Gemfile dependencies have changed" do
+ before do
+ build_repo4 do
+ build_gem "nokogiri", "1.16.4" do |s|
+ s.platform = "arm64-darwin"
+ end
+
+ build_gem "nokogiri", "1.16.4" do |s|
+ s.platform = "x86_64-linux"
+ end
+
+ build_gem "prism", "0.25.0"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "nokogiri", ">=1.16.4"
+ gem "prism", ">=0.25.0"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.16.4-arm64-darwin)
+ nokogiri (1.16.4-x86_64-linux)
+
+ PLATFORMS
+ arm64-darwin
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri (>= 1.16.4)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "still works" do
+ simulate_platform "arm64-darwin-23" do
+ bundle "update"
+ end
+ end
+ end
+
+ context "error handling" do
+ before do
+ gemfile "source 'https://gem.repo1'"
+ end
+
+ it "raises if too many flags are provided" do
+ bundle "update --patch --minor", all: true, raise_on_error: false
+
+ expect(err).to eq "Provide only one of the following options: minor, patch"
+ end
+ end
+end
diff --git a/spec/bundler/commands/version_spec.rb b/spec/bundler/commands/version_spec.rb
new file mode 100644
index 0000000000..4320ad0611
--- /dev/null
+++ b/spec/bundler/commands/version_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require_relative "../support/path"
+
+RSpec.describe "bundle version" do
+ if Spec::Path.ruby_core?
+ COMMIT_HASH = /unknown|[a-fA-F0-9]{7,}/
+ else
+ COMMIT_HASH = /[a-fA-F0-9]{7,}/
+ end
+
+ context "with -v" do
+ it "outputs the version and virtual version if set" do
+ bundle "-v"
+ expect(out).to eq(Bundler::VERSION.to_s)
+
+ bundle_config "simulate_version 5"
+ bundle "-v"
+ expect(out).to eq("#{Bundler::VERSION} (simulating Bundler 5)")
+ end
+ end
+
+ context "with --version" do
+ it "outputs the version and virtual version if set" do
+ bundle "--version"
+ expect(out).to eq(Bundler::VERSION.to_s)
+
+ bundle_config "simulate_version 5"
+ bundle "--version"
+ expect(out).to eq("#{Bundler::VERSION} (simulating Bundler 5)")
+ end
+ end
+
+ context "with version" do
+ context "when released", :ruby_repo do
+ before do
+ system_gems "bundler-4.9.9", released: true
+ end
+
+ it "outputs the version, virtual version if set, and build metadata" do
+ bundle "version"
+ expect(out).to match(/\A4\.9\.9 \(2100-01-01 commit #{COMMIT_HASH}\)\z/)
+
+ bundle_config "simulate_version 5"
+ bundle "version"
+ expect(out).to match(/\A4\.9\.9 \(simulating Bundler 5\) \(2100-01-01 commit #{COMMIT_HASH}\)\z/)
+ end
+ end
+
+ context "when not released" do
+ before do
+ system_gems "bundler-4.9.9", released: false
+ end
+
+ it "outputs the version, virtual version if set, and build metadata" do
+ bundle "version"
+ expect(out).to match(/\A4\.9\.9 \(20\d{2}-\d{2}-\d{2} commit #{COMMIT_HASH}\)\z/)
+
+ bundle_config "simulate_version 5"
+ bundle "version"
+ expect(out).to match(/\A4\.9\.9 \(simulating Bundler 5\) \(20\d{2}-\d{2}-\d{2} commit #{COMMIT_HASH}\)\z/)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/install/allow_offline_install_spec.rb b/spec/bundler/install/allow_offline_install_spec.rb
new file mode 100644
index 0000000000..c7ab7c3d7e
--- /dev/null
+++ b/spec/bundler/install/allow_offline_install_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install allows offline install" do
+ context "with no cached data locally" do
+ it "still installs" do
+ install_gemfile <<-G, artifice: "compact_index"
+ source "http://testgemserver.local"
+ gem "myrack-obama"
+ G
+ expect(the_bundle).to include_gem("myrack 1.0")
+ end
+
+ it "still fails when the network is down" do
+ install_gemfile <<-G, artifice: "fail", raise_on_error: false
+ source "http://testgemserver.local"
+ gem "myrack-obama"
+ G
+ expect(err).to include("Could not reach host testgemserver.local.")
+ expect(the_bundle).to_not be_locked
+ end
+ end
+
+ context "with cached data locally" do
+ it "will install from the compact index" do
+ system_gems ["myrack-1.0.0"], path: default_bundle_path
+
+ bundle_config "clean false"
+ install_gemfile <<-G, artifice: "compact_index"
+ source "http://testgemserver.local"
+ gem "myrack-obama"
+ gem "myrack", "< 1.0"
+ G
+
+ expect(the_bundle).to include_gems("myrack-obama 1.0", "myrack 0.9.1")
+
+ gemfile <<-G
+ source "http://testgemserver.local"
+ gem "myrack-obama"
+ G
+
+ bundle :update, artifice: "fail", all: true
+ expect(stdboth).to include "Using the cached data for the new index because of a network error"
+
+ expect(the_bundle).to include_gems("myrack-obama 1.0", "myrack 1.0.0")
+ end
+
+ def break_git_remote_ops!
+ FileUtils.mkdir_p(tmp("broken_path"))
+ File.open(tmp("broken_path/git"), "w", 0o755) do |f|
+ f.puts <<~RUBY
+ #!/usr/bin/env ruby
+ fetch_args = %w(fetch --force --quiet --no-tags)
+ clone_args = %w(clone --bare --no-hardlinks --quiet)
+
+ if (fetch_args.-(ARGV).empty? || clone_args.-(ARGV).empty?) && File.exist?(ARGV[ARGV.index("--") + 1])
+ warn "git remote ops have been disabled"
+ exit 1
+ end
+ ENV["PATH"] = ENV["PATH"].sub(/^.*?:/, "")
+ exec("git", *ARGV)
+ RUBY
+ end
+
+ old_path = ENV["PATH"]
+ ENV["PATH"] = "#{tmp("broken_path")}:#{ENV["PATH"]}"
+ yield if block_given?
+ ensure
+ ENV["PATH"] = old_path if block_given?
+ end
+
+ it "will install from a cached git repo" do
+ skip "doesn't print errors" if Gem.win_platform?
+
+ git = build_git "a", "1.0.0", path: lib_path("a")
+ update_git("a", path: git.path, branch: "new_branch")
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "a", :git => #{git.path.to_s.dump}
+ G
+
+ break_git_remote_ops! { bundle :update, all: true }
+ expect(err).to include("Using cached git data because of network errors")
+ expect(the_bundle).to be_locked
+
+ break_git_remote_ops! do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "a", :git => #{git.path.to_s.dump}, :branch => "new_branch"
+ G
+ end
+ expect(err).to include("Using cached git data because of network errors")
+ expect(the_bundle).to be_locked
+ end
+ end
+end
diff --git a/spec/bundler/install/binstubs_spec.rb b/spec/bundler/install/binstubs_spec.rb
new file mode 100644
index 0000000000..c2eccb3ef2
--- /dev/null
+++ b/spec/bundler/install/binstubs_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ describe "when system_bindir is set" do
+ it "overrides Gem.bindir" do
+ expect(Pathname.new("/usr/bin")).not_to be_writable
+ gemfile <<-G
+ def Gem.bindir; "/usr/bin"; end
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle_config "BUNDLE_SYSTEM_BINDIR" => system_gem_path("altbin").to_s
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(system_gem_path("altbin/myrackup")).to exist
+ end
+ end
+
+ describe "when multiple gems contain the same exe" do
+ before do
+ build_repo2 do
+ build_gem "fake", "14" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "fake"
+ gem "myrack"
+ G
+ end
+
+ it "warns about the situation" do
+ bundle "exec myrackup"
+
+ expect(last_command.stderr).to include(
+ "The `myrackup` executable in the `fake` gem is being loaded, but it's also present in other gems (myrack).\n" \
+ "If you meant to run the executable for another gem, make sure you use a project specific binstub (`bundle binstub <gem_name>`).\n" \
+ "If you plan to use multiple conflicting executables, generate binstubs for them and disambiguate their names."
+ ).or include(
+ "The `myrackup` executable in the `myrack` gem is being loaded, but it's also present in other gems (fake).\n" \
+ "If you meant to run the executable for another gem, make sure you use a project specific binstub (`bundle binstub <gem_name>`).\n" \
+ "If you plan to use multiple conflicting executables, generate binstubs for them and disambiguate their names."
+ )
+ end
+ end
+end
diff --git a/spec/bundler/install/bundler_spec.rb b/spec/bundler/install/bundler_spec.rb
new file mode 100644
index 0000000000..86c22dad55
--- /dev/null
+++ b/spec/bundler/install/bundler_spec.rb
@@ -0,0 +1,268 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ describe "with bundler dependencies" do
+ before(:each) do
+ build_repo2 do
+ build_gem "rails", "3.0" do |s|
+ s.add_dependency "bundler", ">= 0.9.0"
+ end
+ build_gem "bundler", "0.9.1"
+ build_gem "bundler", Bundler::VERSION
+ end
+ end
+
+ it "are forced to the current bundler version" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "3.0"
+ G
+
+ expect(the_bundle).to include_gems "bundler #{Bundler::VERSION}"
+ end
+
+ it "are forced to the current bundler version even if not already present" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ expect(the_bundle).to include_gems "bundler #{Bundler::VERSION}"
+ end
+
+ it "causes a conflict if explicitly requesting a different version of bundler" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "rails", "3.0"
+ gem "bundler", "0.9.1"
+ G
+
+ nice_error = <<~E.strip
+ Could not find compatible versions
+
+ Because the current Bundler version (#{Bundler::VERSION}) does not satisfy bundler = 0.9.1
+ and Gemfile depends on bundler = 0.9.1,
+ version solving has failed.
+
+ Your bundle requires a different version of Bundler than the one you're running.
+ Install the necessary version with `gem install bundler:0.9.1` and rerun bundler using `bundle _0.9.1_ install`
+ E
+ expect(err).to include(nice_error)
+ end
+
+ it "causes a conflict if explicitly requesting a non matching requirement on bundler" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "rails", "3.0"
+ gem "bundler", "~> 0.8"
+ G
+
+ nice_error = <<~E.strip
+ Could not find compatible versions
+
+ Because rails >= 3.0 depends on bundler >= 0.9.0
+ and the current Bundler version (#{Bundler::VERSION}) does not satisfy bundler >= 0.9.0, < 1.A,
+ rails >= 3.0 requires bundler >= 1.A.
+ So, because Gemfile depends on rails = 3.0
+ and Gemfile depends on bundler ~> 0.8,
+ version solving has failed.
+
+ Your bundle requires a different version of Bundler than the one you're running.
+ Install the necessary version with `gem install bundler:0.9.1` and rerun bundler using `bundle _0.9.1_ install`
+ E
+ expect(err).to include(nice_error)
+ end
+
+ it "causes a conflict if explicitly requesting a version of bundler that doesn't exist" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "rails", "3.0"
+ gem "bundler", "0.9.2"
+ G
+
+ nice_error = <<~E.strip
+ Could not find compatible versions
+
+ Because the current Bundler version (#{Bundler::VERSION}) does not satisfy bundler = 0.9.2
+ and Gemfile depends on bundler = 0.9.2,
+ version solving has failed.
+
+ Your bundle requires a different version of Bundler than the one you're running, and that version could not be found.
+ E
+ expect(err).to include(nice_error)
+ end
+
+ it "works for gems with multiple versions in its dependencies" do
+ build_repo2 do
+ build_gem "multiple_versioned_deps" do |s|
+ s.add_dependency "weakling", ">= 0.0.1", "< 0.1"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "multiple_versioned_deps"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "multiple_versioned_deps"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "multiple_versioned_deps 1.0.0"
+ end
+
+ it "includes bundler in the bundle when it's a child dependency" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "3.0"
+ G
+
+ run "begin; gem 'bundler'; puts 'WIN'; rescue Gem::LoadError; puts 'FAIL'; end"
+ expect(out).to eq("WIN")
+ end
+
+ it "allows gem 'bundler' when Bundler is not in the Gemfile or its dependencies" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ run "begin; gem 'bundler'; puts 'WIN'; rescue Gem::LoadError => e; puts e.backtrace; end"
+ expect(out).to eq("WIN")
+ end
+
+ it "causes a conflict if child dependencies conflict" do
+ bundle_config "force_ruby_platform true"
+
+ update_repo2 do
+ build_gem "rails_pinned_to_old_activesupport" do |s|
+ s.add_dependency "activesupport", "= 1.2.3"
+ end
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "activemerchant"
+ gem "rails_pinned_to_old_activesupport"
+ G
+
+ nice_error = <<~E.strip
+ Could not find compatible versions
+
+ Because every version of rails_pinned_to_old_activesupport depends on activesupport = 1.2.3
+ and every version of activemerchant depends on activesupport >= 2.0.0,
+ every version of rails_pinned_to_old_activesupport is incompatible with activemerchant >= 0.
+ So, because Gemfile depends on activemerchant >= 0
+ and Gemfile depends on rails_pinned_to_old_activesupport >= 0,
+ version solving has failed.
+ E
+ expect(err).to include(nice_error)
+ end
+
+ it "causes a conflict if a child dependency conflicts with the Gemfile" do
+ bundle_config "force_ruby_platform true"
+
+ update_repo2 do
+ build_gem "rails_pinned_to_old_activesupport" do |s|
+ s.add_dependency "activesupport", "= 1.2.3"
+ end
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "rails_pinned_to_old_activesupport"
+ gem "activesupport", "2.3.5"
+ G
+
+ nice_error = <<~E.strip
+ Could not find compatible versions
+
+ Because every version of rails_pinned_to_old_activesupport depends on activesupport = 1.2.3
+ and Gemfile depends on rails_pinned_to_old_activesupport >= 0,
+ activesupport = 1.2.3 is required.
+ So, because Gemfile depends on activesupport = 2.3.5,
+ version solving has failed.
+ E
+ expect(err).to include(nice_error)
+ end
+
+ it "does not cause a conflict if new dependencies in the Gemfile require older dependencies than the lockfile" do
+ update_repo2 do
+ build_gem "rails_pinned_to_old_activesupport" do |s|
+ s.add_dependency "activesupport", "= 1.2.3"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'rails', "2.3.2"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails_pinned_to_old_activesupport"
+ G
+
+ expect(out).to include("Installing activesupport 1.2.3 (was 2.3.2)")
+ expect(err).to be_empty
+ end
+
+ it "prints the previous version when switching to a previously downloaded gem" do
+ build_repo4 do
+ build_gem "rails", "7.0.3"
+ build_gem "rails", "7.0.4"
+ end
+
+ bundle_config "path.system true"
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem 'rails', "7.0.4"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem 'rails', "7.0.3"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem 'rails', "7.0.4"
+ G
+
+ expect(out).to include("Using rails 7.0.4 (was 7.0.3)")
+ expect(err).to be_empty
+ end
+
+ it "can install dependencies with newer bundler version with system gems" do
+ bundle_config "path.system true"
+
+ system_gems "bundler-99999999.99.1"
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "3.0"
+ G
+
+ bundle "check"
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+
+ it "can install dependencies with newer bundler version with a local path" do
+ bundle_config "path .bundle"
+
+ system_gems "bundler-99999999.99.1"
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "3.0"
+ G
+
+ bundle "check"
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+ end
+end
diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb
new file mode 100644
index 0000000000..b3f57d93cc
--- /dev/null
+++ b/spec/bundler/install/cooldown_spec.rb
@@ -0,0 +1,272 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with the cooldown setting" do
+ before do
+ build_repo2
+ end
+
+ context "Gemfile DSL" do
+ it "accepts `source ..., cooldown: N` without error" do
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo2", cooldown: 5
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "accepts `cooldown: 0` to disable cooldown for a source" do
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo2", cooldown: 0
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+ end
+
+ context "CLI flag" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+ end
+
+ it "accepts --cooldown N on install" do
+ bundle "install --cooldown 7", artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "accepts --cooldown 0 as an escape hatch" do
+ bundle "install --cooldown 0", artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "rejects a negative --cooldown value" do
+ bundle "install --cooldown=-7", artifice: "compact_index", raise_on_error: false
+
+ expect(err).to match(/non-negative integer/)
+ end
+ end
+
+ context "configuration" do
+ it "reads BUNDLE_COOLDOWN as an integer" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ bundle "install", env: { "BUNDLE_COOLDOWN" => "7" }, artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "reads `bundle config set cooldown N`" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ bundle "config set cooldown 7"
+ bundle "install", artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+ end
+
+ context "end-to-end with v2 compact index" do
+ before do
+ now = Time.now.utc
+ build_repo3 do
+ build_gem "ripe_gem", "1.0.0" do |s|
+ s.date = now - (30 * 86_400)
+ end
+ build_gem "ripe_gem", "2.0.0" do |s|
+ s.date = now - (1 * 86_400)
+ end
+ end
+ end
+
+ it "excludes versions within the cooldown window" do
+ gemfile <<-G
+ source "https://gem.repo3"
+ gem "ripe_gem"
+ G
+
+ bundle "install --cooldown 7", artifice: "compact_index_cooldown"
+
+ expect(the_bundle).to include_gems("ripe_gem 1.0.0")
+ end
+
+ it "selects the latest version when --cooldown 0 is passed" do
+ gemfile <<-G
+ source "https://gem.repo3"
+ gem "ripe_gem"
+ G
+
+ bundle "install --cooldown 0", artifice: "compact_index_cooldown"
+
+ expect(the_bundle).to include_gems("ripe_gem 2.0.0")
+ end
+
+ it "applies cooldown declared per-source in the Gemfile" do
+ gemfile <<-G
+ source "https://gem.repo3", cooldown: 7
+ gem "ripe_gem"
+ G
+
+ bundle "install", artifice: "compact_index_cooldown"
+
+ expect(the_bundle).to include_gems("ripe_gem 1.0.0")
+ end
+
+ it "is overridden by CLI --cooldown when Gemfile sets a different per-source value" do
+ gemfile <<-G
+ source "https://gem.repo3", cooldown: 0
+ gem "ripe_gem"
+ G
+
+ bundle "install --cooldown 7", artifice: "compact_index_cooldown"
+
+ expect(the_bundle).to include_gems("ripe_gem 1.0.0")
+ end
+
+ it "bypasses cooldown when bundle install uses an existing lockfile" do
+ gemfile <<-G
+ source "https://gem.repo3"
+ gem "ripe_gem"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo3/
+ specs:
+ ripe_gem (2.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ripe_gem
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install --cooldown 7", artifice: "compact_index_cooldown"
+
+ expect(the_bundle).to include_gems("ripe_gem 2.0.0")
+ end
+
+ it "annotates in-cooldown versions in bundle outdated table output" do
+ gemfile <<-G
+ source "https://gem.repo3"
+ gem "ripe_gem", "1.0.0"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo3/
+ specs:
+ ripe_gem (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ripe_gem (= 1.0.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "outdated --cooldown 7", artifice: "compact_index_cooldown", raise_on_error: false
+
+ expect(out).to match(/ripe_gem.*\(cooldown \d+d\)/)
+ end
+
+ it "annotates in-cooldown versions in bundle outdated --parseable output" do
+ gemfile <<-G
+ source "https://gem.repo3"
+ gem "ripe_gem", "1.0.0"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo3/
+ specs:
+ ripe_gem (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ripe_gem (= 1.0.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "outdated --cooldown 7 --parseable", artifice: "compact_index_cooldown", raise_on_error: false
+
+ expect(out).to match(/ripe_gem.*in cooldown for \d+ more day/)
+ end
+
+ it "excludes a locally-installed version that is still within the cooldown window" do
+ system_gems "ripe_gem-2.0.0", gem_repo: gem_repo3
+
+ gemfile <<-G
+ source "https://gem.repo3"
+ gem "ripe_gem"
+ G
+
+ bundle "install --cooldown 7", artifice: "compact_index_cooldown"
+
+ expect(the_bundle).to include_gems("ripe_gem 1.0.0")
+ end
+
+ it "selects a locally-installed in-cooldown version when --cooldown 0 bypasses the filter" do
+ system_gems "ripe_gem-2.0.0", gem_repo: gem_repo3
+
+ gemfile <<-G
+ source "https://gem.repo3"
+ gem "ripe_gem"
+ G
+
+ bundle "install --cooldown 0", artifice: "compact_index_cooldown"
+
+ expect(the_bundle).to include_gems("ripe_gem 2.0.0")
+ end
+
+ it "surfaces a cooldown hint when bundle update filters every candidate" do
+ gemfile <<-G
+ source "https://gem.repo3"
+ gem "ripe_gem"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo3/
+ specs:
+ ripe_gem (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ripe_gem
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "update ripe_gem --cooldown 99999", artifice: "compact_index_cooldown", raise_on_error: false
+
+ expect(err).to match(/excluded by the cooldown setting/)
+ expect(err).to match(/--cooldown 0/)
+ end
+ end
+end
diff --git a/spec/bundler/install/deploy_spec.rb b/spec/bundler/install/deploy_spec.rb
new file mode 100644
index 0000000000..a3b4a87ecf
--- /dev/null
+++ b/spec/bundler/install/deploy_spec.rb
@@ -0,0 +1,493 @@
+# frozen_string_literal: true
+
+RSpec.describe "install in deployment or frozen mode" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "fails without a lockfile and says that deployment requires a lock" do
+ bundle_config "deployment true"
+ bundle "install", raise_on_error: false
+ expect(err).to include("The deployment setting requires a lockfile")
+ end
+
+ it "fails without a lockfile and says that frozen requires a lock" do
+ bundle_config "frozen true"
+ bundle "install", raise_on_error: false
+ expect(err).to include("The frozen setting requires a lockfile")
+ end
+
+ it "still works if you are not in the app directory and specify --gemfile" do
+ bundle "install"
+ pristine_system_gems
+ bundle_config "deployment true"
+ bundle_config "path vendor/bundle"
+ bundle "install --gemfile #{tmp}/bundled_app/Gemfile", dir: tmp
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+
+ it "works if you exclude a group with a git gem" do
+ build_git "foo"
+ gemfile <<-G
+ source "https://gem.repo1"
+ group :test do
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ end
+ G
+ bundle :install
+ bundle_config "deployment true"
+ bundle_config "without test"
+ bundle :install
+ end
+
+ it "works when you bundle exec bundle" do
+ skip "doesn't find bundle" if Gem.win_platform?
+
+ bundle :install
+ bundle_config "deployment true"
+ bundle :install
+ bundle "exec bundle check", env: { "PATH" => path }
+ end
+
+ it "works when using path gems from the same path and the version is specified" do
+ build_lib "foo", path: lib_path("nested/foo")
+ build_lib "bar", path: lib_path("nested/bar")
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", "1.0", :path => "#{lib_path("nested")}"
+ gem "bar", :path => "#{lib_path("nested")}"
+ G
+
+ bundle :install
+ bundle_config "deployment true"
+ bundle :install
+ end
+
+ it "works when path gems are specified twice" do
+ build_lib "foo", path: lib_path("nested/foo")
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("nested/foo")}"
+ gem "foo", :path => "#{lib_path("nested/foo")}"
+ G
+
+ bundle :install
+ bundle_config "deployment true"
+ bundle :install
+ end
+
+ it "works when there are credentials in the source URL" do
+ install_gemfile(<<-G, artifice: "endpoint_strict_basic_authentication", quiet: true)
+ source "http://user:pass@localgemserver.test/"
+
+ gem "myrack-obama", ">= 1.0"
+ G
+
+ bundle_config "deployment true"
+ bundle :install, artifice: "endpoint_strict_basic_authentication"
+ end
+
+ it "works with sources given by a block" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ source "https://gem.repo1" do
+ gem "myrack"
+ end
+ G
+
+ bundle_config "deployment true"
+ bundle :install
+
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+
+ context "when replacing a host with the same host with credentials" do
+ before do
+ bundle_config "path vendor/bundle"
+ bundle "install"
+ gemfile <<-G
+ source "http://user_name:password@localgemserver.test/"
+ gem "myrack"
+ G
+
+ lockfile <<-G
+ GEM
+ remote: http://localgemserver.test/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{generic_local_platform}
+
+ DEPENDENCIES
+ myrack
+ G
+
+ bundle_config "deployment true"
+ end
+
+ it "allows the replace" do
+ bundle :install
+
+ expect(out).to match(/Bundle complete!/)
+ end
+ end
+
+ describe "with an existing lockfile" do
+ before do
+ bundle "install"
+ end
+
+ it "installs gems by default to vendor/bundle" do
+ bundle_config "deployment true"
+ expect do
+ bundle "install"
+ end.not_to change { bundled_app_lock.mtime }
+ expect(out).to include("vendor/bundle")
+ end
+
+ it "installs gems to custom path if specified" do
+ bundle_config "path vendor/bundle2"
+ bundle_config "deployment true"
+ bundle "install"
+ expect(out).to include("vendor/bundle2")
+ end
+
+ it "installs gems to custom path if specified, even when configured through ENV" do
+ bundle_config "deployment true"
+ bundle "install", env: { "BUNDLE_PATH" => "vendor/bundle2" }
+ expect(out).to include("vendor/bundle2")
+ end
+
+ it "works with the `frozen` setting" do
+ bundle_config "frozen true"
+ expect do
+ bundle "install"
+ end.not_to change { bundled_app_lock.mtime }
+ end
+
+ it "works with BUNDLE_FROZEN if you didn't change anything" do
+ expect do
+ bundle :install, env: { "BUNDLE_FROZEN" => "true" }
+ end.not_to change { bundled_app_lock.mtime }
+ end
+
+ it "explodes with the `deployment` setting if you make a change and don't check in the lockfile" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "myrack-obama"
+ G
+
+ bundle_config "deployment true"
+ bundle :install, raise_on_error: false
+ expect(err).to include("frozen mode")
+ expect(err).to include("You have added to the Gemfile")
+ expect(err).to include("* myrack-obama")
+ expect(err).not_to include("You have deleted from the Gemfile")
+ expect(err).not_to include("You have changed in the Gemfile")
+ end
+
+ it "works if a path gem is missing but is in a without group" do
+ build_lib "path_gem"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "path_gem", :path => "#{lib_path("path_gem-1.0")}", :group => :development
+ G
+ expect(the_bundle).to include_gems "path_gem 1.0"
+ FileUtils.rm_r lib_path("path_gem-1.0")
+
+ bundle_config "path .bundle"
+ bundle_config "without development"
+ bundle_config "deployment true"
+ bundle :install, env: { "DEBUG" => "1" }
+ run "puts :WIN"
+ expect(out).to eq("WIN")
+ end
+
+ it "works if a gem is missing, but it's on a different platform" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ source "https://gem.repo1" do
+ gem "rake", platform: :#{not_local_tag}
+ end
+ G
+
+ bundle :install, env: { "BUNDLE_FROZEN" => "true" }
+ expect(last_command).to be_success
+ end
+
+ it "shows a good error if a gem is missing from the lockfile" do
+ build_repo4 do
+ build_gem "foo"
+ build_gem "bar"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "foo"
+ gem "bar"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ foo (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ bar
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle :install, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false, artifice: "compact_index"
+ expect(err).to include("Your lockfile is missing \"bar\", but can't be updated because frozen mode is set")
+ end
+
+ it "explodes if a path gem is missing" do
+ build_lib "path_gem"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "path_gem", :path => "#{lib_path("path_gem-1.0")}", :group => :development
+ G
+ expect(the_bundle).to include_gems "path_gem 1.0"
+ FileUtils.rm_r lib_path("path_gem-1.0")
+
+ bundle_config "path .bundle"
+ bundle_config "deployment true"
+ bundle :install, raise_on_error: false
+ expect(err).to include("The path `#{lib_path("path_gem-1.0")}` does not exist.")
+ end
+
+ it "can have --frozen set via an environment variable" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "myrack-obama"
+ G
+
+ ENV["BUNDLE_FROZEN"] = "1"
+ bundle "install", raise_on_error: false
+ expect(err).to include("frozen mode")
+ expect(err).to include("You have added to the Gemfile")
+ expect(err).to include("* myrack-obama")
+ expect(err).not_to include("You have deleted from the Gemfile")
+ expect(err).not_to include("You have changed in the Gemfile")
+ end
+
+ it "can have --deployment set via an environment variable" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "myrack-obama"
+ G
+
+ ENV["BUNDLE_DEPLOYMENT"] = "true"
+ bundle "install", raise_on_error: false
+ expect(err).to include("frozen mode")
+ expect(err).to include("You have added to the Gemfile")
+ expect(err).to include("* myrack-obama")
+ expect(err).not_to include("You have deleted from the Gemfile")
+ expect(err).not_to include("You have changed in the Gemfile")
+ end
+
+ it "installs gems by default to vendor/bundle when deployment mode is set via an environment variable" do
+ ENV["BUNDLE_DEPLOYMENT"] = "true"
+ bundle "install"
+ expect(out).to include("vendor/bundle")
+ end
+
+ it "installs gems to custom path when deployment mode is set via an environment variable " do
+ ENV["BUNDLE_DEPLOYMENT"] = "true"
+ ENV["BUNDLE_PATH"] = "vendor/bundle2"
+ bundle "install"
+ expect(out).to include("vendor/bundle2")
+ end
+
+ it "can have --frozen set to false via an environment variable" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "myrack-obama"
+ G
+
+ ENV["BUNDLE_FROZEN"] = "false"
+ ENV["BUNDLE_DEPLOYMENT"] = "false"
+ bundle "install"
+ expect(out).not_to include("frozen mode")
+ expect(out).not_to include("You have added to the Gemfile")
+ expect(out).not_to include("* myrack-obama")
+ end
+
+ it "explodes if you replace a gem and don't check in the lockfile" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport"
+ G
+
+ bundle_config "deployment true"
+ bundle :install, raise_on_error: false
+ expect(err).to include("frozen mode")
+ expect(err).to include("You have added to the Gemfile:\n* activesupport\n\n")
+ expect(err).to include("You have deleted from the Gemfile:\n* myrack")
+ expect(err).not_to include("You have changed in the Gemfile")
+ end
+
+ it "explodes if you remove a gem and don't check in the lockfile" do
+ gemfile 'source "https://gem.repo1"'
+
+ bundle_config "deployment true"
+ bundle :install, raise_on_error: false
+ expect(err).to include("Some dependencies were deleted")
+ expect(err).to include("frozen mode")
+ expect(err).to include("You have deleted from the Gemfile:\n* myrack")
+ expect(err).not_to include("You have changed in the Gemfile")
+ end
+
+ it "explodes if you add a source" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "git://hubz.com"
+ G
+
+ bundle_config "deployment true"
+ bundle :install, raise_on_error: false
+ expect(err).to include("frozen mode")
+ expect(err).not_to include("You have added to the Gemfile")
+ expect(err).to include("You have changed in the Gemfile:\n* myrack from `no specified source` to `git://hubz.com`")
+ end
+
+ it "explodes if you change a source from git to the default" do
+ build_git "myrack"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-1.0")}"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle_config "deployment true"
+ bundle :install, raise_on_error: false
+ expect(err).to include("frozen mode")
+ expect(err).not_to include("You have deleted from the Gemfile")
+ expect(err).not_to include("You have added to the Gemfile")
+ expect(err).to include("You have changed in the Gemfile:\n* myrack from `#{lib_path("myrack-1.0")}` to `no specified source`")
+ end
+
+ it "explodes if you change a source from git to the default, in presence of other git sources" do
+ build_lib "foo", path: lib_path("myrack/foo")
+ build_git "myrack", path: lib_path("myrack")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack")}"
+ gem "foo", :git => "#{lib_path("myrack")}"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "foo", :git => "#{lib_path("myrack")}"
+ G
+
+ bundle_config "deployment true"
+ bundle :install, raise_on_error: false
+ expect(err).to include("frozen mode")
+ expect(err).to include("You have changed in the Gemfile:\n* myrack from `#{lib_path("myrack")}` to `no specified source`")
+ expect(err).not_to include("You have added to the Gemfile")
+ expect(err).not_to include("You have deleted from the Gemfile")
+ end
+
+ it "explodes if you change a source from path to git" do
+ build_git "myrack", path: lib_path("myrack")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :path => "#{lib_path("myrack")}"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "https:/my-git-repo-for-myrack"
+ G
+
+ bundle_config "frozen true"
+ bundle :install, raise_on_error: false
+ expect(err).to include("frozen mode")
+ expect(err).to include("You have changed in the Gemfile:\n* myrack from `#{lib_path("myrack")}` to `https:/my-git-repo-for-myrack`")
+ expect(err).not_to include("You have added to the Gemfile")
+ expect(err).not_to include("You have deleted from the Gemfile")
+ end
+
+ it "remembers that the bundle is frozen at runtime" do
+ bundle :lock
+
+ bundle_config "deployment true"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ gem "myrack-obama"
+ G
+
+ run "require 'myrack'", raise_on_error: false
+ expect(err).to include <<~E.strip
+ The dependencies in your gemfile changed, but the lockfile can't be updated because frozen mode is set (Bundler::ProductionError)
+
+ You have added to the Gemfile:
+ * myrack (= 1.0.0)
+ * myrack-obama
+
+ You have deleted from the Gemfile:
+ * myrack
+ E
+ end
+ end
+
+ context "with path in Gemfile and packed" do
+ it "works fine after bundle package and bundle install --local" do
+ build_lib "foo", path: lib_path("foo")
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo")}"
+ G
+
+ bundle :install
+ expect(the_bundle).to include_gems "foo 1.0"
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/foo")).to be_directory
+
+ bundle "install --local"
+ expect(out).to include("Updating files in vendor/cache")
+
+ pristine_system_gems
+ bundle_config "deployment true"
+ bundle "install --verbose"
+ expect(out).not_to include("can't be updated because frozen mode is set")
+ expect(out).not_to include("You have added to the Gemfile")
+ expect(out).not_to include("You have deleted from the Gemfile")
+ expect(out).to include("vendor/cache/foo")
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+ end
+end
diff --git a/spec/bundler/install/failure_spec.rb b/spec/bundler/install/failure_spec.rb
new file mode 100644
index 0000000000..32ca455439
--- /dev/null
+++ b/spec/bundler/install/failure_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ context "installing a gem fails" do
+ it "prints out why that gem was being installed and the underlying error" do
+ build_repo2 do
+ build_gem "activesupport", "2.3.2" do |s|
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ abort "make installing activesupport-2.3.2 fail"
+ end
+ RUBY
+ end
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "rails"
+ G
+ expect(err).to start_with("Gem::Ext::BuildError: ERROR: Failed to build gem native extension.")
+ expect(err).to end_with(<<-M.strip)
+An error occurred while installing activesupport (2.3.2), and Bundler cannot continue.
+
+In Gemfile:
+ rails was resolved to 2.3.2, which depends on
+ actionmailer was resolved to 2.3.2, which depends on
+ activesupport
+ M
+ end
+
+ context "because the downloaded .gem was invalid" do
+ before do
+ build_repo4 do
+ build_gem "a"
+ end
+
+ gem_repo4("gems", "a-1.0.gem").open("w") {|f| f << "<html></html>" }
+ end
+
+ it "removes the downloaded .gem" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo4"
+ gem "a"
+ G
+
+ expect(default_bundle_path("cache", "a-1.0.gem")).not_to exist
+ end
+ end
+ end
+
+ context "when lockfile dependencies don't match the gemspec" do
+ before do
+ build_repo4 do
+ build_gem "myrack", "1.0.0" do |s|
+ s.add_dependency "myrack-test", "~> 1.0"
+ end
+
+ build_gem "myrack-test", "1.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+
+ # First install to generate lockfile
+ bundle :install
+
+ # Manually edit lockfile to have incorrect dependencies
+ lockfile_content = File.read(bundled_app_lock)
+ # Remove the myrack-test dependency from myrack
+ lockfile_content.gsub!(/^ myrack \(1\.0\.0\)\n myrack-test \(~> 1\.0\)\n/, " myrack (1.0.0)\n")
+ File.write(bundled_app_lock, lockfile_content)
+ end
+
+ it "reports the mismatch with detailed information" do
+ bundle :install, raise_on_error: false, env: { "BUNDLE_FROZEN" => "true" }
+
+ expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack-1.0.0")
+ expect(err).to include("myrack-test: gemspec specifies ~> 1.0, not in lockfile")
+ expect(err).to include("Please run `bundle install` to regenerate the lockfile.")
+ end
+ end
+end
diff --git a/spec/bundler/install/force_spec.rb b/spec/bundler/install/force_spec.rb
new file mode 100644
index 0000000000..e0f6fb6364
--- /dev/null
+++ b/spec/bundler/install/force_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ shared_examples_for "an option to force reinstalling gems" do
+ it "re-installs installed gems" do
+ myrack_lib = default_bundle_path("gems/myrack-1.0.0/lib/myrack.rb")
+
+ bundle :install
+ myrack_lib.open("w") {|f| f.write("blah blah blah") }
+ bundle :install, flag => true
+
+ expect(out).to include "Installing myrack 1.0.0"
+ expect(myrack_lib.open(&:read)).to eq("MYRACK = '1.0.0'\n")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "works on first bundle install" do
+ bundle :install, flag => true
+
+ expect(out).to include "Installing myrack 1.0.0"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ context "with a git gem" do
+ let!(:ref) { build_git("foo", "1.0").ref_for("HEAD", 11) }
+
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+ end
+
+ it "re-installs installed gems" do
+ foo_lib = default_bundle_path("bundler/gems/foo-1.0-#{ref}/lib/foo.rb")
+
+ bundle :install
+ foo_lib.open("w") {|f| f.write("blah blah blah") }
+ bundle :install, flag => true
+
+ expect(foo_lib.open(&:read)).to eq("FOO = '1.0'\n")
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "works on first bundle install" do
+ bundle :install, flag => true
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+ end
+ end
+
+ describe "with --force" do
+ it_behaves_like "an option to force reinstalling gems" do
+ let(:flag) { "force" }
+ end
+ end
+
+ describe "with --redownload" do
+ it_behaves_like "an option to force reinstalling gems" do
+ let(:flag) { "redownload" }
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/eval_gemfile_spec.rb b/spec/bundler/install/gemfile/eval_gemfile_spec.rb
new file mode 100644
index 0000000000..3afa4f5daa
--- /dev/null
+++ b/spec/bundler/install/gemfile/eval_gemfile_spec.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with gemfile that uses eval_gemfile" do
+ before do
+ build_lib("gunks", path: bundled_app("gems/gunks")) do |s|
+ s.name = "gunks"
+ s.version = "0.0.1"
+ end
+ end
+
+ context "eval-ed Gemfile points to an internal gemspec" do
+ before do
+ gemfile "Gemfile-other", <<-G
+ source "https://gem.repo1"
+ gemspec :path => 'gems/gunks'
+ G
+ end
+
+ it "installs the gemspec specified gem" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ eval_gemfile 'Gemfile-other'
+ G
+ expect(out).to include("Resolving dependencies")
+ expect(out).to include("Bundle complete")
+
+ expect(the_bundle).to include_gem "gunks 0.0.1", source: "path@#{bundled_app("gems", "gunks")}"
+ end
+ end
+
+ context "eval-ed Gemfile points to an internal gemspec and uses a scoped source that duplicates the main Gemfile global source" do
+ before do
+ build_repo2 do
+ build_gem "rails", "6.1.3.2"
+
+ build_gem "zip-zip", "0.3"
+ end
+
+ gemfile bundled_app("gems/Gemfile"), <<-G
+ source "https://gem.repo2"
+
+ gemspec :path => "\#{__dir__}/gunks"
+
+ source "https://gem.repo2" do
+ gem "zip-zip"
+ end
+ G
+ end
+
+ it "installs and finds gems correctly" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "rails"
+
+ eval_gemfile File.join(__dir__, "gems/Gemfile")
+ G
+ expect(out).to include("Resolving dependencies")
+ expect(out).to include("Bundle complete")
+
+ expect(the_bundle).to include_gem "rails 6.1.3.2"
+ end
+ end
+
+ context "eval-ed Gemfile has relative-path gems" do
+ before do
+ build_lib("a", path: bundled_app("gems/a"))
+ gemfile bundled_app("nested/Gemfile-nested"), <<-G
+ source "https://gem.repo1"
+ gem "a", :path => "../gems/a"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ eval_gemfile "nested/Gemfile-nested"
+ G
+ end
+
+ it "installs the path gem" do
+ bundle :install
+ expect(the_bundle).to include_gem("a 1.0")
+ end
+
+ # Make sure that we are properly comparing path based gems between the
+ # parsed lockfile and the evaluated gemfile.
+ it "bundles with deployment mode configured" do
+ bundle :install
+ bundle_config "deployment true"
+ bundle :install
+ end
+ end
+
+ context "Gemfile uses gemspec paths after eval-ing a Gemfile" do
+ before { create_file "other/Gemfile-other" }
+
+ it "installs the gemspec specified gem" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ eval_gemfile 'other/Gemfile-other'
+ gemspec :path => 'gems/gunks'
+ G
+ expect(out).to include("Resolving dependencies")
+ expect(out).to include("Bundle complete")
+
+ expect(the_bundle).to include_gem "gunks 0.0.1", source: "path@#{bundled_app("gems", "gunks")}"
+ end
+ end
+
+ context "eval-ed Gemfile references other gemfiles" do
+ it "works with relative paths" do
+ gemfile "other/Gemfile-other", "gem 'myrack'"
+ gemfile "other/Gemfile", "eval_gemfile 'Gemfile-other'"
+ gemfile "Gemfile-alt", <<-G
+ source "https://gem.repo1"
+ eval_gemfile "other/Gemfile"
+ G
+ install_gemfile "eval_gemfile File.expand_path('Gemfile-alt')"
+
+ expect(the_bundle).to include_gem "myrack 1.0.0"
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/force_ruby_platform_spec.rb b/spec/bundler/install/gemfile/force_ruby_platform_spec.rb
new file mode 100644
index 0000000000..bcc1f36823
--- /dev/null
+++ b/spec/bundler/install/gemfile/force_ruby_platform_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with force_ruby_platform DSL option", :jruby do
+ context "when no transitive deps" do
+ before do
+ build_repo4 do
+ # Build a gem with platform specific versions
+ build_gem("platform_specific")
+
+ build_gem("platform_specific") do |s|
+ s.platform = Bundler.local_platform
+ end
+
+ # Build the exact same gem with a different name to compare using vs not using the option
+ build_gem("platform_specific_forced")
+
+ build_gem("platform_specific_forced") do |s|
+ s.platform = Bundler.local_platform
+ end
+ end
+ end
+
+ it "pulls the pure ruby variant of the given gem" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "platform_specific_forced", :force_ruby_platform => true
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems "platform_specific_forced 1.0 ruby"
+ expect(the_bundle).to include_gems "platform_specific 1.0 #{Bundler.local_platform}"
+ end
+
+ it "still respects a global `force_ruby_platform` config" do
+ install_gemfile <<-G, env: { "BUNDLE_FORCE_RUBY_PLATFORM" => "true" }
+ source "https://gem.repo4"
+
+ gem "platform_specific_forced", :force_ruby_platform => true
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems "platform_specific_forced 1.0 ruby"
+ expect(the_bundle).to include_gems "platform_specific 1.0 ruby"
+ end
+ end
+
+ context "when also a transitive dependency" do
+ before do
+ build_repo4 do
+ build_gem("depends_on_platform_specific") {|s| s.add_dependency "platform_specific" }
+
+ build_gem("platform_specific")
+
+ build_gem("platform_specific") do |s|
+ s.platform = Bundler.local_platform
+ end
+ end
+ end
+
+ it "still pulls the ruby variant" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "depends_on_platform_specific"
+ gem "platform_specific", :force_ruby_platform => true
+ G
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 ruby"
+ end
+ end
+
+ context "with transitive dependencies with platform specific versions" do
+ before do
+ build_repo4 do
+ build_gem("depends_on_platform_specific") do |s|
+ s.add_dependency "platform_specific"
+ end
+
+ build_gem("depends_on_platform_specific") do |s|
+ s.add_dependency "platform_specific"
+ s.platform = Bundler.local_platform
+ end
+
+ build_gem("platform_specific")
+
+ build_gem("platform_specific") do |s|
+ s.platform = Bundler.local_platform
+ end
+ end
+ end
+
+ it "ignores ruby variants for the transitive dependencies" do
+ install_gemfile <<-G, env: { "DEBUG_RESOLVER" => "true" }
+ source "https://gem.repo4"
+
+ gem "depends_on_platform_specific", :force_ruby_platform => true
+ G
+
+ expect(the_bundle).to include_gems "depends_on_platform_specific 1.0 ruby"
+ expect(the_bundle).to include_gems "platform_specific 1.0 #{Bundler.local_platform}"
+ end
+
+ it "reinstalls the ruby variant when a platform specific variant is already installed, the lockile has only ruby platform, and :force_ruby_platform is used in the Gemfile" do
+ skip "Can't simulate platform reliably on JRuby, installing a platform specific gem fails to activate io-wait because only the -java version is present, and we're simulating a different platform" if RUBY_ENGINE == "jruby"
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ platform_specific (1.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ platform_specific
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86-darwin-100" do
+ system_gems "platform_specific-1.0-x86-darwin-100", path: default_bundle_path
+
+ install_gemfile <<-G, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }, artifice: "compact_index"
+ source "https://gem.repo4"
+
+ gem "platform_specific", :force_ruby_platform => true
+ G
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 ruby"
+ end
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/gemspec_spec.rb b/spec/bundler/install/gemfile/gemspec_spec.rb
new file mode 100644
index 0000000000..e51fc9247d
--- /dev/null
+++ b/spec/bundler/install/gemfile/gemspec_spec.rb
@@ -0,0 +1,737 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install from an existing gemspec" do
+ before(:each) do
+ build_repo2 do
+ build_gem "bar"
+ build_gem "bar-dev"
+ end
+ end
+
+ it "should install runtime and development dependencies" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.write("Gemfile", "source :rubygems\ngemspec")
+ s.add_dependency "bar", "=1.0.0"
+ s.add_development_dependency "bar-dev", "=1.0.0"
+ end
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gemspec :path => '#{tmp("foo")}'
+ G
+
+ expect(the_bundle).to include_gems "bar 1.0.0"
+ expect(the_bundle).to include_gems "bar-dev 1.0.0", groups: :development
+ end
+
+ it "that is hidden should install runtime and development dependencies" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.write("Gemfile", "source :rubygems\ngemspec")
+ s.add_dependency "bar", "=1.0.0"
+ s.add_development_dependency "bar-dev", "=1.0.0"
+ end
+ FileUtils.mv tmp("foo", "foo.gemspec"), tmp("foo", ".gemspec")
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gemspec :path => '#{tmp("foo")}'
+ G
+
+ expect(the_bundle).to include_gems "bar 1.0.0"
+ expect(the_bundle).to include_gems "bar-dev 1.0.0", groups: :development
+ end
+
+ it "should handle a list of requirements" do
+ update_repo2 do
+ build_gem "baz", "1.0"
+ build_gem "baz", "1.1"
+ end
+
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.write("Gemfile", "source :rubygems\ngemspec")
+ s.add_dependency "baz", ">= 1.0", "< 1.1"
+ end
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gemspec :path => '#{tmp("foo")}'
+ G
+
+ expect(the_bundle).to include_gems "baz 1.0"
+ end
+
+ it "should raise if there are no gemspecs available" do
+ build_lib("foo", path: tmp("foo"), gemspec: false)
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gemspec :path => '#{tmp("foo")}'
+ G
+ expect(err).to match(/There are no gemspecs at #{tmp("foo")}/)
+ end
+
+ it "should raise if there are too many gemspecs available" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.write("foo2.gemspec", build_spec("foo", "4.0").first.to_ruby)
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gemspec :path => '#{tmp("foo")}'
+ G
+ expect(err).to match(/There are multiple gemspecs at #{tmp("foo")}/)
+ end
+
+ it "should pick a specific gemspec" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.write("foo2.gemspec", "")
+ s.add_dependency "bar", "=1.0.0"
+ s.add_development_dependency "bar-dev", "=1.0.0"
+ end
+
+ install_gemfile(<<-G)
+ source "https://gem.repo2"
+ gemspec :path => '#{tmp("foo")}', :name => 'foo'
+ G
+
+ expect(the_bundle).to include_gems "bar 1.0.0"
+ expect(the_bundle).to include_gems "bar-dev 1.0.0", groups: :development
+ end
+
+ it "should use a specific group for development dependencies" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.write("foo2.gemspec", "")
+ s.add_dependency "bar", "=1.0.0"
+ s.add_development_dependency "bar-dev", "=1.0.0"
+ end
+
+ install_gemfile(<<-G)
+ source "https://gem.repo2"
+ gemspec :path => '#{tmp("foo")}', :name => 'foo', :development_group => :dev
+ G
+
+ expect(the_bundle).to include_gems "bar 1.0.0"
+ expect(the_bundle).not_to include_gems "bar-dev 1.0.0", groups: :development
+ expect(the_bundle).to include_gems "bar-dev 1.0.0", groups: :dev
+ end
+
+ it "should match a lockfile even if the gemspec defines development dependencies" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.write("Gemfile", "source 'https://gem.repo1'\ngemspec")
+ s.add_dependency "actionpack", "=2.3.2"
+ s.add_development_dependency "rake", rake_version
+ end
+
+ bundle "install", dir: tmp("foo"), artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ # This should really be able to rely on $stderr, but, it's not written
+ # right, so we can't. In fact, this is a bug negation test, and so it'll
+ # ghost pass in future, and will only catch a regression if the message
+ # doesn't change. Exit codes should be used correctly (they can be more
+ # than just 0 and 1).
+ bundle_config "deployment true"
+ output = bundle("install", dir: tmp("foo"), artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s })
+ expect(output).not_to match(/You have added to the Gemfile/)
+ expect(output).not_to match(/You have deleted from the Gemfile/)
+ expect(output).not_to match(/the lockfile can't be updated because frozen mode is set/)
+ end
+
+ it "should match a lockfile without needing to re-resolve" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.add_dependency "myrack"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec :path => '#{tmp("foo")}'
+ G
+
+ bundle "install", verbose: true
+
+ message = "Found no changes, using resolution from the lockfile"
+ expect(out.scan(message).size).to eq(1)
+ end
+
+ it "should match a lockfile without needing to re-resolve with development dependencies" do
+ simulate_platform "java" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.add_dependency "myrack"
+ s.add_development_dependency "thin"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec :path => '#{tmp("foo")}'
+ G
+
+ bundle "install", verbose: true
+
+ message = "Found no changes, using resolution from the lockfile"
+ expect(out.scan(message).size).to eq(1)
+ end
+ end
+
+ it "should match a lockfile on non-ruby platforms with a transitive platform dependency", :jruby_only do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.add_dependency "platform_specific"
+ end
+
+ system_gems "platform_specific-1.0-java", path: default_bundle_path
+
+ install_gemfile <<-G
+ gemspec :path => '#{tmp("foo")}'
+ G
+
+ bundle "update --bundler", artifice: "compact_index", verbose: true
+ expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 java"
+ end
+
+ it "should evaluate the gemspec in its directory" do
+ build_lib("foo", path: tmp("foo"))
+ File.open(tmp("foo/foo.gemspec"), "w") do |s|
+ s.write "raise 'ahh' unless Dir.pwd == '#{tmp("foo")}'"
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ gemspec :path => '#{tmp("foo")}'
+ G
+ expect(stdboth).not_to include("ahh")
+ end
+
+ it "allows the gemspec to activate other gems" do
+ ENV["BUNDLE_PATH__SYSTEM"] = "true"
+ # see https://github.com/rubygems/bundler/issues/5409
+ #
+ # issue was caused by rubygems having an unresolved gem during a require,
+ # so emulate that
+ system_gems %w[myrack-1.0.0 myrack-0.9.1 myrack-obama-1.0]
+
+ build_lib("foo", path: bundled_app)
+ gemspec = bundled_app("foo.gemspec").read
+ bundled_app("foo.gemspec").open("w") do |f|
+ f.write "#{gemspec.strip}.tap { gem 'myrack-obama'; require 'myrack/obama' }"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+
+ it "allows conflicts" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.version = "1.0.0"
+ s.add_dependency "bar", "= 1.0.0"
+ end
+ build_gem "deps", to_bundle: true do |s|
+ s.add_dependency "foo", "= 0.0.1"
+ end
+ build_gem "foo", "0.0.1", to_bundle: true
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "deps"
+ gemspec :path => '#{tmp("foo")}', :name => 'foo'
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0.0"
+ end
+
+ it "does not break Gem.finish_resolve with conflicts" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.version = "1.0.0"
+ s.add_dependency "bar", "= 1.0.0"
+ end
+ update_repo2 do
+ build_gem "deps" do |s|
+ s.add_dependency "foo", "= 0.0.1"
+ end
+ build_gem "foo", "0.0.1"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "deps"
+ gemspec :path => '#{tmp("foo")}', :name => 'foo'
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0.0"
+
+ run "Gem.finish_resolve; puts 'WIN'"
+ expect(out).to eq("WIN")
+ end
+
+ it "does not make Gem.try_activate warn when local gem has extensions" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.version = "1.0.0"
+ s.add_c_extension
+ end
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gemspec :path => '#{tmp("foo")}'
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0.0"
+
+ run "Gem.try_activate('irb/lc/es/error.rb'); puts 'WIN'"
+ expect(out).to eq("WIN")
+ expect(err).to be_empty
+ end
+
+ it "handles downgrades" do
+ build_lib "omg", "2.0", path: lib_path("omg")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec :path => "#{lib_path("omg")}"
+ G
+
+ build_lib "omg", "1.0", path: lib_path("omg")
+
+ bundle :install
+
+ expect(the_bundle).to include_gems "omg 1.0"
+ end
+
+ context "in deployment mode" do
+ context "when the lockfile was not updated after a change to the gemspec's dependencies" do
+ it "reports that installation failed" do
+ build_lib "cocoapods", path: bundled_app do |s|
+ s.add_dependency "activesupport", ">= 1"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ expect(the_bundle).to include_gems("cocoapods 1.0", "activesupport 2.3.5")
+
+ build_lib "cocoapods", path: bundled_app do |s|
+ s.add_dependency "activesupport", ">= 1.0.1"
+ end
+
+ bundle_config "deployment true"
+ bundle :install, raise_on_error: false
+
+ expect(err).to include("changed")
+ end
+ end
+ end
+
+ context "when child gemspecs conflict with a released gemspec" do
+ before do
+ # build the "parent" gem that depends on another gem in the same repo
+ build_lib "source_conflict", path: bundled_app do |s|
+ s.add_dependency "myrack_middleware"
+ end
+
+ # build the "child" gem that is the same version as a released gem, but
+ # has completely different and conflicting dependency requirements
+ build_lib "myrack_middleware", "1.0", path: bundled_app("myrack_middleware") do |s|
+ s.add_dependency "myrack", "1.0" # anything other than 0.9.1
+ end
+ end
+
+ it "should install the child gemspec's deps" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+
+ context "with a lockfile and some missing dependencies" do
+ let(:source_uri) { "http://localgemserver.test" }
+
+ before do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.add_dependency "myrack", "=1.0.0"
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ gemspec :path => "../foo"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ lockfile <<-L
+ PATH
+ remote: ../foo
+ specs:
+ foo (1.0)
+ myrack (= 1.0.0)
+
+ GEM
+ remote: #{source_uri}
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ context "using JRuby with explicit platform", :jruby_only do
+ before do
+ create_file(
+ tmp("foo", "foo-java.gemspec"),
+ build_spec("foo", "1.0", "java") do
+ dep "myrack", "=1.0.0"
+ @spec.authors = "authors"
+ @spec.summary = "summary"
+ end.first.to_ruby
+ )
+ end
+
+ it "should install" do
+ results = bundle "install", artifice: "endpoint"
+ expect(results).to include("Installing myrack 1.0.0")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ it "should install", :jruby do
+ results = bundle "install", artifice: "endpoint"
+ expect(results).to include("Installing myrack 1.0.0")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ context "bundled for multiple platforms" do
+ let(:platform_specific_type) { :runtime }
+ let(:dependency) { "platform_specific" }
+ before do
+ build_repo2 do
+ build_gem "indirect_platform_specific" do |s|
+ s.add_runtime_dependency "platform_specific"
+ end
+ end
+
+ build_lib "foo", path: bundled_app do |s|
+ case platform_specific_type
+ when :runtime
+ s.add_runtime_dependency dependency
+ when :development
+ s.add_development_dependency dependency
+ else
+ raise ArgumentError, "wrong dependency type #{platform_specific_type}, can only be :development or :runtime"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gemspec
+ G
+
+ bundle_config "force_ruby_platform true"
+ bundle "install"
+
+ simulate_new_machine
+ simulate_platform("jruby") { bundle "install" }
+ expect(lockfile).to include("platform_specific (1.0-java)")
+ simulate_platform("x64-mingw-ucrt") { bundle "install" }
+ end
+
+ context "on ruby" do
+ before do
+ bundle_config "force_ruby_platform true"
+ bundle :install
+ end
+
+ context "as a runtime dependency" do
+ it "keeps all platform dependencies in the lockfile" do
+ expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 ruby"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo2, "platform_specific", "1.0"
+ c.checksum gem_repo2, "platform_specific", "1.0", "java"
+ c.checksum gem_repo2, "platform_specific", "1.0", "x64-mingw-ucrt"
+ end
+
+ expect(lockfile).to eq <<~L
+ PATH
+ remote: .
+ specs:
+ foo (1.0)
+ platform_specific
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ platform_specific (1.0)
+ platform_specific (1.0-java)
+ platform_specific (1.0-x64-mingw-ucrt)
+
+ PLATFORMS
+ java
+ ruby
+ x64-mingw-ucrt
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "as a development dependency" do
+ let(:platform_specific_type) { :development }
+
+ it "keeps all platform dependencies in the lockfile" do
+ expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 ruby"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo2, "platform_specific", "1.0"
+ c.checksum gem_repo2, "platform_specific", "1.0", "java"
+ c.checksum gem_repo2, "platform_specific", "1.0", "x64-mingw-ucrt"
+ end
+
+ expect(lockfile).to eq <<~L
+ PATH
+ remote: .
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ platform_specific (1.0)
+ platform_specific (1.0-java)
+ platform_specific (1.0-x64-mingw-ucrt)
+
+ PLATFORMS
+ java
+ ruby
+ x64-mingw-ucrt
+
+ DEPENDENCIES
+ foo!
+ platform_specific
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "with an indirect platform-specific development dependency" do
+ let(:platform_specific_type) { :development }
+ let(:dependency) { "indirect_platform_specific" }
+
+ it "keeps all platform dependencies in the lockfile" do
+ expect(the_bundle).to include_gems "foo 1.0", "indirect_platform_specific 1.0", "platform_specific 1.0 ruby"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo2, "indirect_platform_specific", "1.0"
+ c.checksum gem_repo2, "platform_specific", "1.0"
+ c.checksum gem_repo2, "platform_specific", "1.0", "java"
+ c.checksum gem_repo2, "platform_specific", "1.0", "x64-mingw-ucrt"
+ end
+
+ expect(lockfile).to eq <<~L
+ PATH
+ remote: .
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ indirect_platform_specific (1.0)
+ platform_specific
+ platform_specific (1.0)
+ platform_specific (1.0-java)
+ platform_specific (1.0-x64-mingw-ucrt)
+
+ PLATFORMS
+ java
+ ruby
+ x64-mingw-ucrt
+
+ DEPENDENCIES
+ foo!
+ indirect_platform_specific
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+ end
+ end
+ end
+
+ context "with multiple platforms" do
+ before do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.version = "1.0.0"
+ s.add_development_dependency "myrack"
+ s.write "foo-universal-java.gemspec", build_spec("foo", "1.0.0", "universal-java") {|sj| sj.runtime "myrack", "1.0.0" }.first.to_ruby
+ end
+ end
+
+ it "installs the ruby platform gemspec" do
+ bundle_config "force_ruby_platform true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec :path => '#{tmp("foo")}', :name => 'foo'
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0.0", "myrack 1.0.0"
+ end
+
+ it "installs the ruby platform gemspec and skips dev deps with `without development` configured" do
+ bundle_config "force_ruby_platform true"
+
+ bundle_config "without development"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec :path => '#{tmp("foo")}', :name => 'foo'
+ G
+
+ expect(the_bundle).to include_gem "foo 1.0.0"
+ expect(the_bundle).not_to include_gem "myrack"
+ end
+ end
+
+ context "with multiple platforms and resolving for more specific platforms" do
+ before do
+ build_lib("chef", path: tmp("chef")) do |s|
+ s.version = "17.1.17"
+ s.write "chef-universal-mingw-ucrt.gemspec", build_spec("chef", "17.1.17", "universal-mingw-ucrt") {|sw| sw.runtime "win32-api", "~> 1.5.3" }.first.to_ruby
+ end
+ end
+
+ it "does not remove the platform specific specs from the lockfile when updating" do
+ build_repo4 do
+ build_gem "win32-api", "1.5.3" do |s|
+ s.platform = "universal-mingw-ucrt"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gemspec :path => "../chef"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "chef", "17.1.17"
+ c.no_checksum "chef", "17.1.17", "universal-mingw-ucrt"
+ c.checksum gem_repo4, "win32-api", "1.5.3", "universal-mingw-ucrt"
+ end
+
+ initial_lockfile = <<~L
+ PATH
+ remote: ../chef
+ specs:
+ chef (17.1.17)
+ chef (17.1.17-universal-mingw-ucrt)
+ win32-api (~> 1.5.3)
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ win32-api (1.5.3-universal-mingw-ucrt)
+
+ PLATFORMS
+ #{lockfile_platforms("ruby", "x64-mingw-ucrt", "x86-mingw32")}
+
+ DEPENDENCIES
+ chef!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile initial_lockfile
+
+ bundle "update"
+
+ expect(lockfile).to eq initial_lockfile
+ end
+ end
+
+ context "with multiple locked platforms" do
+ before do
+ build_lib("activeadmin", path: tmp("activeadmin")) do |s|
+ s.version = "2.9.0"
+ s.add_dependency "railties", ">= 5.2", "< 6.2"
+ end
+
+ build_repo4 do
+ build_gem "railties", "6.1.4"
+
+ build_gem "jruby-openssl", "0.10.7" do |s|
+ s.platform = "java"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gemspec :path => "../activeadmin"
+ gem "jruby-openssl", :platform => :jruby
+ G
+
+ bundle "lock --add-platform java"
+ end
+
+ it "does not remove the platform specific specs from the lockfile when re-resolving due to gemspec changes" do
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "activeadmin", "2.9.0"
+ c.checksum gem_repo4, "jruby-openssl", "0.10.7", "java"
+ c.checksum gem_repo4, "railties", "6.1.4"
+ end
+
+ expect(lockfile).to eq <<~L
+ PATH
+ remote: ../activeadmin
+ specs:
+ activeadmin (2.9.0)
+ railties (>= 5.2, < 6.2)
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ jruby-openssl (0.10.7-java)
+ railties (6.1.4)
+
+ PLATFORMS
+ #{lockfile_platforms("java")}
+
+ DEPENDENCIES
+ activeadmin!
+ jruby-openssl
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemspec = tmp("activeadmin/activeadmin.gemspec")
+ File.write(gemspec, File.read(gemspec).sub(">= 5.2", ">= 6.0"))
+
+ previous_lockfile = lockfile
+
+ bundle "install --local"
+
+ expect(lockfile).to eq(previous_lockfile.sub(">= 5.2", ">= 6.0"))
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/git_spec.rb b/spec/bundler/install/gemfile/git_spec.rb
new file mode 100644
index 0000000000..b2a82caf01
--- /dev/null
+++ b/spec/bundler/install/gemfile/git_spec.rb
@@ -0,0 +1,1745 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with git sources" do
+ describe "when floating on main" do
+ let(:base_gemfile) do
+ <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ G
+ end
+
+ let(:install_base_gemfile) do
+ build_git "foo" do |s|
+ s.executables = "foobar"
+ end
+
+ install_gemfile base_gemfile
+ end
+
+ it "fetches gems" do
+ install_base_gemfile
+ expect(the_bundle).to include_gems("foo 1.0")
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" unless defined?(FOO_PREV_REF)
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "does not (yet?) enforce CHECKSUMS" do
+ build_git "foo"
+ revision = revision_for(lib_path("foo-1.0"))
+
+ bundle_config "lockfile_checksums true"
+ gemfile base_gemfile
+
+ lockfile <<~L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{revision}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ CHECKSUMS
+ foo (1.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle_config "frozen true"
+
+ bundle "install"
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ it "caches the git repo" do
+ install_base_gemfile
+ expect(Dir["#{default_cache_path}/git/foo-1.0-*"]).to have_attributes size: 1
+ end
+
+ it "does not write to cache on bundler/setup" do
+ install_base_gemfile
+ FileUtils.rm_r(default_cache_path)
+ ruby "require 'bundler/setup'"
+ expect(default_cache_path).not_to exist
+ end
+
+ it "caches the git repo globally and properly uses the cached repo on the next invocation" do
+ install_base_gemfile
+ pristine_system_gems
+ bundle_config "global_gem_cache true"
+ bundle :install
+ expect(Dir["#{home}/.bundle/cache/git/foo-1.0-*"]).to have_attributes size: 1
+
+ bundle "install --verbose"
+ expect(err).to be_empty
+ expect(out).to include("Using foo 1.0 from #{lib_path("foo")}")
+ end
+
+ it "caches the evaluated gemspec" do
+ install_base_gemfile
+ git = update_git "foo" do |s|
+ s.executables = ["foobar"] # we added this the first time, so keep it now
+ s.files = ["bin/foobar"] # updating git nukes the files list
+ foospec = s.to_ruby.gsub(/s\.files.*/, 's.files = `git ls-files -z`.split("\x0")')
+ s.write "foo.gemspec", foospec
+ end
+
+ bundle "update foo"
+
+ sha = git.ref_for("main", 11)
+ spec_file = default_bundle_path("bundler/gems/foo-1.0-#{sha}/foo.gemspec")
+ expect(spec_file).to exist
+ ruby_code = Gem::Specification.load(spec_file.to_s).to_ruby
+ file_code = File.read(spec_file)
+ expect(file_code).to eq(ruby_code)
+ end
+
+ it "does not update the git source implicitly" do
+ install_base_gemfile
+ update_git "foo"
+
+ install_gemfile bundled_app2("Gemfile"), <<-G, dir: bundled_app2
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ G
+
+ run <<-RUBY
+ require 'foo'
+ puts "fail" if defined?(FOO_PREV_REF)
+ RUBY
+
+ expect(out).to be_empty
+ end
+
+ it "sets up git gem executables on the path" do
+ install_base_gemfile
+ bundle "exec foobar"
+ expect(out).to eq("1.0")
+ end
+
+ it "complains if pinned specs don't exist in the git repo" do
+ build_git "foo"
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "foo", "1.1", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(err).to include("The source contains the following gems matching 'foo':\n * foo-1.0")
+ end
+
+ it "complains with version and platform if pinned specs don't exist in the git repo", :jruby_only do
+ build_git "only_java" do |s|
+ s.platform = "java"
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ platforms :jruby do
+ gem "only_java", "1.2", :git => "#{lib_path("only_java-1.0-java")}"
+ end
+ G
+
+ expect(err).to include("The source contains the following gems matching 'only_java':\n * only_java-1.0-java")
+ end
+
+ it "complains with multiple versions and platforms if pinned specs don't exist in the git repo", :jruby_only do
+ build_git "only_java", "1.0" do |s|
+ s.platform = "java"
+ end
+
+ build_git "only_java", "1.1" do |s|
+ s.platform = "java"
+ s.write "only_java1-0.gemspec", File.read("#{lib_path("only_java-1.0-java")}/only_java.gemspec")
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ platforms :jruby do
+ gem "only_java", "1.2", :git => "#{lib_path("only_java-1.1-java")}"
+ end
+ G
+
+ expect(err).to include("The source contains the following gems matching 'only_java':\n * only_java-1.0-java\n * only_java-1.1-java")
+ end
+
+ it "still works after moving the application directory" do
+ bundle_config "path vendor/bundle"
+ install_base_gemfile
+
+ FileUtils.mv bundled_app, tmp("bundled_app.bck")
+
+ expect(the_bundle).to include_gems "foo 1.0", dir: tmp("bundled_app.bck")
+ end
+
+ it "can still install after moving the application directory" do
+ bundle_config "path vendor/bundle"
+ install_base_gemfile
+
+ FileUtils.mv bundled_app, tmp("bundled_app.bck")
+
+ update_git "foo", "1.1", path: lib_path("foo-1.0")
+
+ gemfile tmp("bundled_app.bck/Gemfile"), <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+
+ gem "myrack", "1.0"
+ G
+
+ bundle "update foo", dir: tmp("bundled_app.bck")
+
+ expect(the_bundle).to include_gems "foo 1.1", "myrack 1.0", dir: tmp("bundled_app.bck")
+ end
+ end
+
+ describe "with an empty git block" do
+ before do
+ build_git "foo"
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ git "#{lib_path("foo-1.0")}" do
+ # this page left intentionally blank
+ end
+ G
+ end
+
+ it "does not explode" do
+ bundle "install"
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+
+ describe "when specifying a revision" do
+ before(:each) do
+ build_git "foo"
+ @revision = revision_for(lib_path("foo-1.0"))
+ update_git "foo"
+ end
+
+ it "works" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}", :ref => "#{@revision}" do
+ gem "foo"
+ end
+ G
+ expect(err).to be_empty
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" unless defined?(FOO_PREV_REF)
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "works when the revision is a symbol" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}", :ref => #{@revision.to_sym.inspect} do
+ gem "foo"
+ end
+ G
+ expect(err).to be_empty
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" unless defined?(FOO_PREV_REF)
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "works when an abbreviated revision is added after an initial, potentially shallow clone" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem "foo"
+ end
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}", :ref => #{@revision[0..7].inspect} do
+ gem "foo"
+ end
+ G
+ end
+
+ it "works when a tag that does not look like a commit hash is used as the value of :ref" do
+ build_git "foo"
+ @remote = build_git("bar", bare: true)
+ update_git "foo", remote: @remote.path
+ update_git "foo", push: "main"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{@remote.path}"
+ G
+
+ # Create a new tag on the remote that needs fetching
+ update_git "foo", tag: "v1.0.0"
+ update_git "foo", push: "v1.0.0"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{@remote.path}", :ref => "v1.0.0"
+ G
+
+ expect(err).to be_empty
+ end
+
+ it "works when the revision is a non-head ref" do
+ # want to ensure we don't fallback to main
+ update_git "foo", path: lib_path("foo-1.0") do |s|
+ s.write("lib/foo.rb", "raise 'FAIL'")
+ end
+
+ git("update-ref -m \"Bundler Spec!\" refs/bundler/1 main~1", lib_path("foo-1.0"))
+
+ # want to ensure we don't fallback to HEAD
+ update_git "foo", path: lib_path("foo-1.0"), branch: "rando" do |s|
+ s.write("lib/foo.rb", "raise 'FAIL_FROM_RANDO'")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}", :ref => "refs/bundler/1" do
+ gem "foo"
+ end
+ G
+ expect(err).to be_empty
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" if defined?(FOO)
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "works when the revision is a non-head ref and it was previously downloaded" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem "foo"
+ end
+ G
+
+ # want to ensure we don't fallback to main
+ update_git "foo", path: lib_path("foo-1.0") do |s|
+ s.write("lib/foo.rb", "raise 'FAIL'")
+ end
+
+ git("update-ref -m \"Bundler Spec!\" refs/bundler/1 main~1", lib_path("foo-1.0"))
+
+ # want to ensure we don't fallback to HEAD
+ update_git "foo", path: lib_path("foo-1.0"), branch: "rando" do |s|
+ s.write("lib/foo.rb", "raise 'FAIL_FROM_RANDO'")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}", :ref => "refs/bundler/1" do
+ gem "foo"
+ end
+ G
+ expect(err).to be_empty
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" if defined?(FOO)
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "does not download random non-head refs" do
+ git("update-ref -m \"Bundler Spec!\" refs/bundler/1 main~1", lib_path("foo-1.0"))
+
+ bundle_config "global_gem_cache true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem "foo"
+ end
+ G
+
+ # ensure we also git fetch after cloning
+ bundle :update, all: true
+
+ git("ls-remote .", Dir[home(".bundle/cache/git/foo-*")].first)
+
+ expect(out).not_to include("refs/bundler/1")
+ end
+ end
+
+ describe "when specifying a branch" do
+ let(:branch) { "branch" }
+ let(:repo) { build_git("foo").path }
+
+ it "works" do
+ update_git("foo", path: repo, branch: branch)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{repo}", :branch => #{branch.dump} do
+ gem "foo"
+ end
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ context "when the branch starts with a `#`" do
+ let(:branch) { "#149/redirect-url-fragment" }
+ it "works" do
+ skip "git does not accept this" if Gem.win_platform?
+
+ update_git("foo", path: repo, branch: branch)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{repo}", :branch => #{branch.dump} do
+ gem "foo"
+ end
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+ end
+
+ context "when the branch includes quotes" do
+ let(:branch) { %('") }
+ it "works" do
+ skip "git does not accept this" if Gem.win_platform?
+
+ update_git("foo", path: repo, branch: branch)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{repo}", :branch => #{branch.dump} do
+ gem "foo"
+ end
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+ end
+ end
+
+ describe "when specifying a tag" do
+ let(:tag) { "tag" }
+ let(:repo) { build_git("foo").path }
+
+ it "works" do
+ update_git("foo", path: repo, tag: tag)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{repo}", :tag => #{tag.dump} do
+ gem "foo"
+ end
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ context "when the tag starts with a `#`" do
+ let(:tag) { "#149/redirect-url-fragment" }
+ it "works" do
+ skip "git does not accept this" if Gem.win_platform?
+
+ update_git("foo", path: repo, tag: tag)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{repo}", :tag => #{tag.dump} do
+ gem "foo"
+ end
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+ end
+
+ context "when the tag includes quotes" do
+ let(:tag) { %('") }
+ it "works" do
+ skip "git does not accept this" if Gem.win_platform?
+
+ update_git("foo", path: repo, tag: tag)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{repo}", :tag => #{tag.dump} do
+ gem "foo"
+ end
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+ end
+ end
+
+ describe "when specifying local override" do
+ it "uses the local repository instead of checking a new one out" do
+ build_git "myrack", "0.8", path: lib_path("local-myrack") do |s|
+ s.write "lib/myrack.rb", "puts :LOCAL"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install
+
+ run "require 'myrack'"
+ expect(out).to eq("LOCAL")
+ end
+
+ it "chooses the local repository on runtime" do
+ build_git "myrack", "0.8"
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ update_git "myrack", "0.8", path: lib_path("local-myrack") do |s|
+ s.write "lib/myrack.rb", "puts :LOCAL"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ run "require 'myrack'"
+ expect(out).to eq("LOCAL")
+ end
+
+ it "unlocks the source when the dependencies have changed while switching to the local" do
+ build_git "myrack", "0.8"
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ update_git "myrack", "0.8", path: lib_path("local-myrack") do |s|
+ s.write "myrack.gemspec", build_spec("myrack", "0.8") { runtime "rspec", "> 0" }.first.to_ruby
+ s.write "lib/myrack.rb", "puts :LOCAL"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install
+ run "require 'myrack'"
+ expect(out).to eq("LOCAL")
+ end
+
+ it "updates specs on runtime" do
+ system_gems "nokogiri-1.4.2"
+
+ build_git "myrack", "0.8"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ lockfile0 = File.read(bundled_app_lock)
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+ update_git "myrack", "0.8", path: lib_path("local-myrack") do |s|
+ s.add_dependency "nokogiri", "1.4.2"
+ end
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ run "require 'myrack'"
+
+ lockfile1 = File.read(bundled_app_lock)
+ expect(lockfile1).not_to eq(lockfile0)
+ end
+
+ it "updates ref on install" do
+ build_git "myrack", "0.8"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ lockfile0 = File.read(bundled_app_lock)
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+ update_git "myrack", "0.8", path: lib_path("local-myrack")
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install
+
+ lockfile1 = File.read(bundled_app_lock)
+ expect(lockfile1).not_to eq(lockfile0)
+ end
+
+ it "explodes and gives correct solution if given path does not exist on install" do
+ build_git "myrack", "0.8"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install, raise_on_error: false
+ expect(err).to match(/Cannot use local override for myrack-0.8 because #{Regexp.escape(lib_path("local-myrack").to_s)} does not exist/)
+
+ solution = "config unset local.myrack"
+ expect(err).to match(/Run `bundle #{solution}` to remove the local override/)
+
+ bundle solution
+ bundle :install
+
+ expect(err).to be_empty
+ end
+
+ it "explodes and gives correct solution if branch is not given on install" do
+ build_git "myrack", "0.8"
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install, raise_on_error: false
+ expect(err).to match(/Cannot use local override for myrack-0.8 at #{Regexp.escape(lib_path("local-myrack").to_s)} because :branch is not specified in Gemfile/)
+
+ solution = "config unset local.myrack"
+ expect(err).to match(/Specify a branch or run `bundle #{solution}` to remove the local override/)
+
+ bundle solution
+ bundle :install
+
+ expect(err).to be_empty
+ end
+
+ it "does not explode if disable_local_branch_check is given" do
+ build_git "myrack", "0.8"
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle %(config set disable_local_branch_check true)
+ bundle :install
+ expect(out).to match(/Bundle complete!/)
+ end
+
+ it "explodes on different branches on install" do
+ build_git "myrack", "0.8"
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ update_git "myrack", "0.8", path: lib_path("local-myrack"), branch: "another" do |s|
+ s.write "lib/myrack.rb", "puts :LOCAL"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install, raise_on_error: false
+ expect(err).to match(/is using branch another but Gemfile specifies main/)
+ end
+
+ it "explodes on invalid revision on install" do
+ build_git "myrack", "0.8"
+
+ build_git "myrack", "0.8", path: lib_path("local-myrack") do |s|
+ s.write "lib/myrack.rb", "puts :LOCAL"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install, raise_on_error: false
+ expect(err).to match(/The Gemfile lock is pointing to revision \w+/)
+ end
+
+ it "does not explode on invalid revision on install" do
+ build_git "myrack", "0.8"
+
+ build_git "myrack", "0.8", path: lib_path("local-myrack") do |s|
+ s.write "lib/myrack.rb", "puts :LOCAL"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle %(config set disable_local_revision_check true)
+ bundle :install
+ expect(out).to match(/Bundle complete!/)
+ end
+ end
+
+ describe "specified inline" do
+ # TODO: Figure out how to write this test so that it is not flaky depending
+ # on the current network situation.
+ # it "supports private git URLs" do
+ # gemfile <<-G
+ # gem "thingy", :git => "git@notthere.fallingsnow.net:somebody/thingy.git"
+ # G
+ #
+ # bundle :install
+ #
+ # # p out
+ # # p err
+ # puts err unless err.empty? # This spec fails randomly every so often
+ # err.should include("notthere.fallingsnow.net")
+ # err.should include("ssh")
+ # end
+
+ it "installs from git even if a newer gem is available elsewhere" do
+ build_git "myrack", "0.8"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}"
+ G
+
+ expect(the_bundle).to include_gems "myrack 0.8"
+ end
+
+ it "installs dependencies from git even if a newer gem is available elsewhere" do
+ system_gems "myrack-1.0.0"
+
+ build_lib "myrack", "1.0", path: lib_path("nested/bar") do |s|
+ s.write "lib/myrack.rb", "puts 'WIN OVERRIDE'"
+ end
+
+ build_git "foo", path: lib_path("nested") do |s|
+ s.add_dependency "myrack", "= 1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("nested")}"
+ G
+
+ run "require 'myrack'"
+ expect(out).to eq("WIN OVERRIDE")
+ end
+
+ it "correctly unlocks when changing to a git source" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+
+ build_git "myrack", path: lib_path("myrack")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0", :git => "#{lib_path("myrack")}"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "correctly unlocks when changing to a git source without versions" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ build_git "myrack", "1.2", path: lib_path("myrack")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack")}"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.2"
+ end
+ end
+
+ describe "block syntax" do
+ it "pulls all gems from a git block" do
+ build_lib "omg", path: lib_path("hi2u/omg")
+ build_lib "hi2u", path: lib_path("hi2u")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path("hi2u")}" do
+ gem "omg"
+ gem "hi2u"
+ end
+ G
+
+ expect(the_bundle).to include_gems "omg 1.0", "hi2u 1.0"
+ end
+ end
+
+ it "uses a ref if specified" do
+ build_git "foo"
+ @revision = revision_for(lib_path("foo-1.0"))
+ update_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{@revision}"
+ G
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" unless defined?(FOO_PREV_REF)
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "correctly handles cases with invalid gemspecs" do
+ build_git "foo" do |s|
+ s.summary = nil
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ gem "rails", "2.3.2"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ expect(the_bundle).to include_gems "rails 2.3.2"
+ end
+
+ it "runs the gemspec in the context of its parent directory" do
+ build_lib "bar", path: lib_path("foo/bar"), gemspec: false do |s|
+ s.write lib_path("foo/bar/lib/version.rb"), %(BAR_VERSION = '1.0')
+ s.write "bar.gemspec", <<-G
+ $:.unshift Dir.pwd
+ require 'lib/version'
+ Gem::Specification.new do |s|
+ s.name = 'bar'
+ s.author = 'no one'
+ s.version = BAR_VERSION
+ s.summary = 'Bar'
+ s.files = Dir["lib/**/*.rb"]
+ end
+ G
+ end
+
+ build_git "foo", path: lib_path("foo") do |s|
+ s.write "bin/foo", ""
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "bar", :git => "#{lib_path("foo")}"
+ gem "rails", "2.3.2"
+ G
+
+ expect(the_bundle).to include_gems "bar 1.0"
+ expect(the_bundle).to include_gems "rails 2.3.2"
+ end
+
+ it "runs the gemspec in the context of its parent directory, when using local overrides" do
+ build_git "foo", path: lib_path("foo"), gemspec: false do |s|
+ s.write lib_path("foo/lib/foo/version.rb"), %(FOO_VERSION = '1.0')
+ s.write "foo.gemspec", <<-G
+ $:.unshift Dir.pwd
+ require 'lib/foo/version'
+ Gem::Specification.new do |s|
+ s.name = 'foo'
+ s.author = 'no one'
+ s.version = FOO_VERSION
+ s.summary = 'Foo'
+ s.files = Dir["lib/**/*.rb"]
+ end
+ G
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "https://github.com/gems/foo", branch: "main"
+ G
+
+ bundle %(config set local.foo #{lib_path("foo")})
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "installs from git even if a rubygems gem is present" do
+ build_gem "foo", "1.0", path: lib_path("fake_foo"), to_system: true do |s|
+ s.write "lib/foo.rb", "raise 'FAIL'"
+ end
+
+ build_git "foo", "1.0"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "fakes the gem out if there is no gemspec" do
+ build_git "foo", gemspec: false
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}"
+ gem "rails", "2.3.2"
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ expect(the_bundle).to include_gems("rails 2.3.2")
+ end
+
+ it "catches git errors and spits out useful output" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", "1.0", :git => "omgomg"
+ G
+
+ bundle :install, raise_on_error: false
+
+ expect(err).to include("Git error:")
+ expect(err).to include("fatal")
+ expect(err).to include("omgomg")
+ end
+
+ it "works when the gem path has spaces in it" do
+ build_git "foo", path: lib_path("foo space-1.0")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo space-1.0")}"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "handles repos that have been force-pushed" do
+ build_git "forced", "1.0"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("forced-1.0")}" do
+ gem 'forced'
+ end
+ G
+ expect(the_bundle).to include_gems "forced 1.0"
+
+ update_git "forced" do |s|
+ s.write "lib/forced.rb", "FORCED = '1.1'"
+ end
+
+ bundle "update", all: true
+ expect(the_bundle).to include_gems "forced 1.1"
+
+ git("reset --hard HEAD^", lib_path("forced-1.0"))
+
+ bundle "update", all: true
+ expect(the_bundle).to include_gems "forced 1.0"
+ end
+
+ it "ignores submodules if :submodule is not passed" do
+ # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/
+ system(*%W[git config --global protocol.file.allow always])
+
+ build_git "submodule", "1.0"
+ build_git "has_submodule", "1.0" do |s|
+ s.add_dependency "submodule"
+ end
+ git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0")
+ git "commit -m \"submodulator\"", lib_path("has_submodule-1.0")
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ git "#{lib_path("has_submodule-1.0")}" do
+ gem "has_submodule"
+ end
+ G
+ expect(err).to match(%r{submodule >= 0 could not be found in rubygems repository https://gem.repo1/ or installed locally})
+
+ expect(the_bundle).not_to include_gems "has_submodule 1.0"
+ end
+
+ it "handles repos with submodules" do
+ # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/
+ system(*%W[git config --global protocol.file.allow always])
+
+ build_git "submodule", "1.0"
+ build_git "has_submodule", "1.0" do |s|
+ s.add_dependency "submodule"
+ end
+ git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0")
+ git "commit -m \"submodulator\"", lib_path("has_submodule-1.0")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("has_submodule-1.0")}", :submodules => true do
+ gem "has_submodule"
+ end
+ G
+
+ expect(the_bundle).to include_gems "has_submodule 1.0"
+ end
+
+ it "does not warn when deiniting submodules" do
+ # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/
+ system(*%W[git config --global protocol.file.allow always])
+
+ build_git "submodule", "1.0"
+ build_git "has_submodule", "1.0"
+
+ git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0")
+ git "commit -m \"submodulator\"", lib_path("has_submodule-1.0")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("has_submodule-1.0")}" do
+ gem "has_submodule"
+ end
+ G
+ expect(err).to be_empty
+
+ expect(the_bundle).to include_gems "has_submodule 1.0"
+ expect(the_bundle).to_not include_gems "submodule 1.0"
+ end
+
+ it "handles implicit updates when modifying the source info" do
+ git = build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem "foo"
+ end
+ G
+
+ update_git "foo"
+ update_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}", :ref => "#{git.ref_for("HEAD^")}" do
+ gem "foo"
+ end
+ G
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" if FOO_PREV_REF == '#{git.ref_for("HEAD^^")}'
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "does not do a remote fetch if the revision is cached locally" do
+ build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ FileUtils.rm_r(lib_path("foo-1.0"))
+
+ bundle "install"
+ expect(out).not_to match(/updating/i)
+ end
+
+ it "doesn't blow up if bundle install is run twice in a row" do
+ build_git "foo"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle "install"
+ bundle "install"
+ end
+
+ it "prints a friendly error if a file blocks the git repo" do
+ build_git "foo"
+
+ FileUtils.mkdir_p(default_bundle_path)
+ FileUtils.touch(default_bundle_path("bundler"))
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(last_command).to be_failure
+ expect(err).to include("Bundler could not install a gem because it " \
+ "needs to create a directory, but a file exists " \
+ "- #{default_bundle_path("bundler")}")
+ end
+
+ it "does not duplicate git gem sources" do
+ build_lib "foo", path: lib_path("nested/foo")
+ build_lib "bar", path: lib_path("nested/bar")
+
+ build_git "foo", path: lib_path("nested")
+ build_git "bar", path: lib_path("nested")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("nested")}"
+ gem "bar", :git => "#{lib_path("nested")}"
+ G
+
+ expect(File.read(bundled_app_lock).scan("GIT").size).to eq(1)
+ end
+
+ describe "switching sources" do
+ it "doesn't explode when switching Path to Git sources" do
+ build_gem "foo", "1.0", to_system: true do |s|
+ s.write "lib/foo.rb", "raise 'fail'"
+ end
+ build_lib "foo", "1.0", path: lib_path("bar/foo")
+ build_git "bar", "1.0", path: lib_path("bar") do |s|
+ s.add_dependency "foo"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "bar", :path => "#{lib_path("bar")}"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "bar", :git => "#{lib_path("bar")}"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0", "bar 1.0"
+ end
+
+ it "doesn't explode when switching Gem to Git source" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack-obama"
+ gem "myrack", "1.0.0"
+ G
+
+ build_git "myrack", "1.0" do |s|
+ s.write "lib/new_file.rb", "puts 'USING GIT'"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack-obama"
+ gem "myrack", "1.0.0", :git => "#{lib_path("myrack-1.0")}"
+ G
+
+ run "require 'new_file'"
+ expect(out).to eq("USING GIT")
+ end
+
+ it "doesn't explode when removing an explicit exact version from a git gem with dependencies" do
+ build_lib "activesupport", "7.1.4", path: lib_path("rails/activesupport")
+ build_git "rails", "7.1.4", path: lib_path("rails") do |s|
+ s.add_dependency "activesupport", "= 7.1.4"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails", "7.1.4", :git => "#{lib_path("rails")}"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails", :git => "#{lib_path("rails")}"
+ G
+
+ expect(the_bundle).to include_gem "rails 7.1.4", "activesupport 7.1.4"
+ end
+
+ it "doesn't explode when adding an explicit ref to a git gem with dependencies" do
+ lib_root = lib_path("rails")
+
+ build_lib "activesupport", "7.1.4", path: lib_root.join("activesupport")
+ build_git "rails", "7.1.4", path: lib_root do |s|
+ s.add_dependency "activesupport", "= 7.1.4"
+ end
+
+ old_revision = revision_for(lib_root)
+ update_git "rails", "7.1.4", path: lib_root
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails", "7.1.4", :git => "#{lib_root}"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails", :git => "#{lib_root}", :ref => "#{old_revision}"
+ G
+
+ expect(the_bundle).to include_gem "rails 7.1.4", "activesupport 7.1.4"
+ end
+ end
+
+ describe "bundle install after the remote has been updated" do
+ it "installs" do
+ build_git "valim"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "valim", :git => "#{lib_path("valim-1.0")}"
+ G
+
+ old_revision = revision_for(lib_path("valim-1.0"))
+ update_git "valim"
+ new_revision = revision_for(lib_path("valim-1.0"))
+
+ old_lockfile = File.read(bundled_app_lock)
+ lockfile(bundled_app_lock, old_lockfile.gsub(/revision: #{old_revision}/, "revision: #{new_revision}"))
+
+ bundle "install"
+
+ run <<-R
+ require "valim"
+ puts VALIM_PREV_REF
+ R
+
+ expect(out).to eq(old_revision)
+ end
+
+ it "gives a helpful error message when the remote ref no longer exists" do
+ build_git "foo"
+ revision = revision_for(lib_path("foo-1.0"))
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{revision}"
+ G
+ expect(out).to_not match(/Revision.*does not exist/)
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "deadbeef"
+ G
+ expect(err).to include("Revision deadbeef does not exist in the repository")
+ end
+
+ it "gives a helpful error message when the remote branch no longer exists" do
+ build_git "foo"
+
+ install_gemfile <<-G, env: { "LANG" => "en" }, raise_on_error: false
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "deadbeef"
+ G
+
+ expect(err).to include("Revision deadbeef does not exist in the repository")
+ end
+ end
+
+ describe "bundle install with deployment mode configured and git sources" do
+ it "works" do
+ build_git "valim", path: lib_path("valim")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "valim", "= 1.0", :git => "#{lib_path("valim")}"
+ G
+
+ pristine_system_gems
+
+ bundle_config "deployment true"
+ bundle :install
+ end
+ end
+
+ describe "gem install hooks" do
+ it "runs pre-install hooks" do
+ build_git "foo"
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ File.open(lib_path("install_hooks.rb"), "w") do |h|
+ h.write <<-H
+ Gem.pre_install_hooks << lambda do |inst|
+ STDERR.puts "Ran pre-install hook: \#{inst.spec.full_name}"
+ end
+ H
+ end
+
+ bundle :install,
+ requires: [lib_path("install_hooks.rb")]
+ expect(err_without_deprecations).to eq("Ran pre-install hook: foo-1.0")
+ end
+
+ it "runs post-install hooks" do
+ build_git "foo"
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ File.open(lib_path("install_hooks.rb"), "w") do |h|
+ h.write <<-H
+ Gem.post_install_hooks << lambda do |inst|
+ STDERR.puts "Ran post-install hook: \#{inst.spec.full_name}"
+ end
+ H
+ end
+
+ bundle :install,
+ requires: [lib_path("install_hooks.rb")]
+ expect(err_without_deprecations).to eq("Ran post-install hook: foo-1.0")
+ end
+
+ it "complains if the install hook fails" do
+ build_git "foo"
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ File.open(lib_path("install_hooks.rb"), "w") do |h|
+ h.write <<-H
+ Gem.pre_install_hooks << lambda do |inst|
+ false
+ end
+ H
+ end
+
+ bundle :install, requires: [lib_path("install_hooks.rb")], raise_on_error: false
+ expect(err).to include("failed for foo-1.0")
+ end
+ end
+
+ context "with an extension" do
+ it "installs the extension" do
+ build_git "foo" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ File.open("\#{path}/foo.rb", "w") do |f|
+ f.puts "FOO = 'YES'"
+ end
+ end
+ RUBY
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ run <<-R
+ require 'foo'
+ puts FOO
+ R
+ expect(out).to eq("YES")
+
+ run <<-R
+ puts $:.grep(/ext/)
+ R
+ expect(out).to include(Pathname.glob(default_bundle_path("bundler/gems/extensions/**/foo-1.0-*")).first.to_s)
+ end
+
+ it "does not use old extension after ref changes" do
+ git_reader = build_git "foo", no_default: true do |s|
+ s.extensions = ["ext/extconf.rb"]
+ s.write "ext/extconf.rb", <<-RUBY
+ require "mkmf"
+ create_makefile("foo")
+ RUBY
+ s.write "ext/foo.c", "void Init_foo() {}"
+ end
+
+ 2.times do |i|
+ File.open(git_reader.path.join("ext/foo.c"), "w") do |file|
+ file.write <<-C
+ #include "ruby.h"
+ VALUE foo(VALUE self) { return INT2FIX(#{i}); }
+ void Init_foo() { rb_define_global_function("foo", &foo, 0); }
+ C
+ end
+ git("commit -m \"commit for iteration #{i}\" ext/foo.c", git_reader.path)
+
+ git_commit_sha = git_reader.ref_for("HEAD")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{git_commit_sha}"
+ G
+
+ run <<-R
+ require 'foo'
+ puts foo
+ R
+
+ expect(out).to eq(i.to_s)
+ end
+ end
+
+ it "does not prompt to gem install if extension fails" do
+ build_git "foo" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ raise
+ end
+ RUBY
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(err).to end_with(<<-M.strip)
+An error occurred while installing foo (1.0), and Bundler cannot continue.
+
+In Gemfile:
+ foo
+ M
+ expect(out).not_to include("gem install foo")
+ end
+
+ it "does not reinstall the extension" do
+ build_git "foo" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ cur_time = Time.now.to_f.to_s
+ File.open("\#{path}/foo.rb", "w") do |f|
+ f.puts "FOO = \#{cur_time}"
+ end
+ end
+ RUBY
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ run <<-R
+ require 'foo'
+ puts FOO
+ R
+
+ installed_time = out
+ expect(installed_time).to match(/\A\d+\.\d+\z/)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ run <<-R
+ require 'foo'
+ puts FOO
+ R
+ expect(out).to eq(installed_time)
+ end
+
+ it "does not reinstall the extension when changing another gem" do
+ build_git "foo" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ cur_time = Time.now.to_f.to_s
+ File.open("\#{path}/foo.rb", "w") do |f|
+ f.puts "FOO = \#{cur_time}"
+ end
+ end
+ RUBY
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ run <<-R
+ require 'foo'
+ puts FOO
+ R
+
+ installed_time = out
+ expect(installed_time).to match(/\A\d+\.\d+\z/)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ run <<-R
+ require 'foo'
+ puts FOO
+ R
+ expect(out).to eq(installed_time)
+ end
+
+ it "does reinstall the extension when changing refs" do
+ build_git "foo" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ cur_time = Time.now.to_f.to_s
+ File.open("\#{path}/foo.rb", "w") do |f|
+ f.puts "FOO = \#{cur_time}"
+ end
+ end
+ RUBY
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ run <<-R
+ require 'foo'
+ puts FOO
+ R
+
+ installed_time = out
+
+ update_git("foo", branch: "branch2")
+
+ expect(installed_time).to match(/\A\d+\.\d+\z/)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "branch2"
+ G
+
+ run <<-R
+ require 'foo'
+ puts FOO
+ R
+ expect(out).not_to eq(installed_time)
+
+ installed_time = out
+
+ update_git("foo")
+ bundle "update foo"
+
+ run <<-R
+ require 'foo'
+ puts FOO
+ R
+ expect(out).not_to eq(installed_time)
+ end
+ end
+
+ it "ignores git environment variables" do
+ build_git "xxxxxx" do |s|
+ s.executables = "xxxxxxbar"
+ end
+
+ Bundler::SharedHelpers.with_clean_git_env do
+ ENV["GIT_DIR"] = "bar"
+ ENV["GIT_WORK_TREE"] = "bar"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("xxxxxx-1.0")}" do
+ gem 'xxxxxx'
+ end
+ G
+
+ expect(ENV["GIT_DIR"]).to eq("bar")
+ expect(ENV["GIT_WORK_TREE"]).to eq("bar")
+ end
+ end
+
+ describe "without git installed" do
+ it "prints a better error message when installing" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "rake", git: "https://github.com/ruby/rake"
+ G
+
+ lockfile <<-L
+ GIT
+ remote: https://github.com/ruby/rake
+ revision: 5c60da8644a9e4f655e819252e3b6ca77f42b7af
+ specs:
+ rake (13.0.6)
+
+ GEM
+ remote: https://rubygems.org/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ rake!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ with_path_as("") do
+ bundle "install", raise_on_error: false
+ end
+ expect(err).
+ to include("You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git")
+ end
+
+ it "prints a better error message when updating" do
+ build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ G
+
+ with_path_as("") do
+ bundle "update", all: true, raise_on_error: false
+ end
+ expect(err).
+ to include("You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git")
+ end
+
+ it "doesn't need git in the new machine if an installed git gem is copied to another machine" do
+ build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ G
+ bundle_config_global "path vendor/bundle"
+ bundle :install
+ pristine_system_gems
+
+ bundle "install", env: { "PATH" => "" }
+ expect(out).to_not include("You need to install git to be able to use gems from git repositories.")
+ end
+ end
+
+ describe "when the git source is overridden with a local git repo" do
+ before do
+ bundle_config_global "local.foo #{lib_path("foo")}"
+ end
+
+ describe "and git output is colorized" do
+ before do
+ File.open("#{ENV["HOME"]}/.gitconfig", "w") do |f|
+ f.write("[color]\n\tui = always\n")
+ end
+ end
+
+ it "installs successfully" do
+ build_git "foo", "1.0", path: lib_path("foo")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}", :branch => "main"
+ G
+
+ bundle :install
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+ end
+ end
+
+ context "git sources that include credentials" do
+ context "that are username and password" do
+ let(:credentials) { "user1:password1" }
+
+ it "does not display the password" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ git "https://#{credentials}@github.com/company/private-repo" do
+ gem "foo"
+ end
+ G
+
+ expect(stdboth).to_not include("password1")
+ expect(out).to include("Fetching https://user1@github.com/company/private-repo")
+ end
+ end
+
+ context "that is an oauth token" do
+ let(:credentials) { "oauth_token" }
+
+ it "displays the oauth scheme but not the oauth token" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ git "https://#{credentials}:x-oauth-basic@github.com/company/private-repo" do
+ gem "foo"
+ end
+ G
+
+ expect(stdboth).to_not include("oauth_token")
+ expect(out).to include("Fetching https://x-oauth-basic@github.com/company/private-repo")
+ end
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/groups_spec.rb b/spec/bundler/install/gemfile/groups_spec.rb
new file mode 100644
index 0000000000..4013b112ec
--- /dev/null
+++ b/spec/bundler/install/gemfile/groups_spec.rb
@@ -0,0 +1,345 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with groups" do
+ describe "installing with no options" do
+ before :each do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ group :emo do
+ gem "activesupport", "2.3.5"
+ end
+ gem "thin", :groups => [:emo]
+ G
+ end
+
+ it "installs gems in the default group" do
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "installs gems in a group block into that group" do
+ expect(the_bundle).to include_gems "activesupport 2.3.5"
+
+ load_error_run <<-R, "activesupport", :default
+ require 'activesupport'
+ puts ACTIVESUPPORT
+ R
+
+ expect(err_without_deprecations).to match(/cannot load such file -- activesupport/)
+ end
+
+ it "installs gems with inline :groups into those groups" do
+ expect(the_bundle).to include_gems "thin 1.0"
+
+ load_error_run <<-R, "thin", :default
+ require 'thin'
+ puts THIN
+ R
+
+ expect(err_without_deprecations).to match(/cannot load such file -- thin/)
+ end
+
+ it "sets up everything if Bundler.setup is used with no groups" do
+ output = run("require 'myrack'; puts MYRACK")
+ expect(output).to eq("1.0.0")
+
+ output = run("require 'activesupport'; puts ACTIVESUPPORT")
+ expect(output).to eq("2.3.5")
+
+ output = run("require 'thin'; puts THIN")
+ expect(output).to eq("1.0")
+ end
+
+ it "removes old groups when new groups are set up" do
+ load_error_run <<-RUBY, "thin", :emo
+ Bundler.setup(:default)
+ require 'thin'
+ puts THIN
+ RUBY
+
+ expect(err_without_deprecations).to match(/cannot load such file -- thin/)
+ end
+
+ it "sets up old groups when they have previously been removed" do
+ output = run <<-RUBY, :emo
+ Bundler.setup(:default)
+ Bundler.setup(:default, :emo)
+ require 'thin'; puts THIN
+ RUBY
+ expect(output).to eq("1.0")
+ end
+ end
+
+ describe "without option" do
+ describe "with gems assigned to a single group" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ group :emo do
+ gem "activesupport", "2.3.5"
+ end
+ group :debugging, :optional => true do
+ gem "thin"
+ end
+ G
+ end
+
+ it "installs gems in the default group" do
+ bundle_config "without emo"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0", groups: [:default]
+ end
+
+ it "respects global `without` configuration, but does not save it locally" do
+ bundle_config_global "without emo"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0", groups: [:default]
+ bundle "config list"
+ expect(out).not_to include("Set for your local app (#{bundled_app(".bundle/config")}): [:emo]")
+ expect(out).to include("Set for the current user (#{home(".bundle/config")}): [:emo]")
+ end
+
+ it "allows running application where groups where configured by a different user" do
+ bundle_config "without emo"
+ bundle :install
+ bundle "exec ruby -e 'puts 42'", env: { "BUNDLE_USER_HOME" => tmp("new_home").to_s }
+ expect(out).to include("42")
+ end
+
+ it "does not install gems from the excluded group" do
+ bundle_config "without emo"
+ bundle :install
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5", groups: [:default]
+ end
+
+ it "does not say it installed gems from the excluded group" do
+ bundle_config "without emo"
+ bundle :install
+ expect(out).not_to include("activesupport")
+ end
+
+ it "allows Bundler.setup for specific groups" do
+ bundle_config "without emo"
+ bundle :install
+ run("require 'myrack'; puts MYRACK", :default)
+ expect(out).to eq("1.0.0")
+ end
+
+ it "does not effect the resolve" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport"
+ group :emo do
+ gem "rails", "2.3.2"
+ end
+ G
+
+ bundle_config "without emo"
+ bundle :install
+ expect(the_bundle).to include_gems "activesupport 2.3.2", groups: [:default]
+ end
+
+ it "still works when BUNDLE_WITHOUT is set" do
+ ENV["BUNDLE_WITHOUT"] = "emo"
+
+ bundle :install
+ expect(out).not_to include("activesupport")
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", groups: [:default]
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5", groups: [:default]
+
+ ENV["BUNDLE_WITHOUT"] = nil
+ end
+
+ it "does not install gems from the optional group" do
+ bundle :install
+ expect(the_bundle).not_to include_gems "thin 1.0"
+ end
+
+ it "installs gems from the optional group when requested" do
+ bundle_config "with debugging"
+ bundle :install
+ expect(the_bundle).to include_gems "thin 1.0"
+ end
+
+ it "installs gems from the optional groups requested with BUNDLE_WITH" do
+ ENV["BUNDLE_WITH"] = "debugging"
+ bundle :install
+ expect(the_bundle).to include_gems "thin 1.0"
+ ENV["BUNDLE_WITH"] = nil
+ end
+
+ it "allows the BUNDLE_WITH setting to override BUNDLE_WITHOUT" do
+ ENV["BUNDLE_WITH"] = "debugging"
+
+ bundle :install
+ expect(the_bundle).to include_gem "thin 1.0"
+
+ ENV["BUNDLE_WITHOUT"] = "debugging"
+ expect(the_bundle).to include_gem "thin 1.0"
+
+ bundle :install
+ expect(the_bundle).to include_gem "thin 1.0"
+ end
+
+ it "has no effect when listing a not optional group in with" do
+ bundle_config "with emo"
+ bundle :install
+ expect(the_bundle).to include_gems "activesupport 2.3.5"
+ end
+
+ it "has no effect when listing an optional group in without" do
+ bundle_config "without debugging"
+ bundle :install
+ expect(the_bundle).not_to include_gems "thin 1.0"
+ end
+ end
+
+ describe "with gems assigned to multiple groups" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ group :emo, :lolercoaster do
+ gem "activesupport", "2.3.5"
+ end
+ G
+ end
+
+ it "installs gems in the default group" do
+ bundle_config "without emo lolercoaster"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "installs the gem if any of its groups are installed" do
+ bundle_config "without emo"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.5"
+ end
+
+ describe "with a gem defined multiple times in different groups" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ group :emo do
+ gem "activesupport", "2.3.5"
+ end
+
+ group :lolercoaster do
+ gem "activesupport", "2.3.5"
+ end
+ G
+ end
+
+ it "installs the gem unless all groups are excluded" do
+ bundle_config "without emo"
+ bundle :install
+ expect(the_bundle).to include_gems "activesupport 2.3.5"
+
+ bundle_config "without lolercoaster"
+ bundle :install
+ expect(the_bundle).to include_gems "activesupport 2.3.5"
+
+ bundle_config "without emo lolercoaster"
+ bundle :install
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5"
+
+ bundle "config set --local without 'emo lolercoaster'"
+ bundle :install
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5"
+ end
+ end
+ end
+
+ describe "nesting groups" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ group :emo do
+ group :lolercoaster do
+ gem "activesupport", "2.3.5"
+ end
+ end
+ G
+ end
+
+ it "installs gems in the default group" do
+ bundle_config "without emo lolercoaster"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "installs the gem if any of its groups are installed" do
+ bundle_config "without emo"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.5"
+ end
+ end
+ end
+
+ describe "when loading only the default group" do
+ it "should not load all groups" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "activesupport", :groups => :development
+ G
+
+ ruby <<-R
+ require "bundler"
+ Bundler.setup :default
+ Bundler.require :default
+ puts MYRACK
+ begin
+ require "activesupport"
+ rescue LoadError
+ puts "no activesupport"
+ end
+ R
+
+ expect(out).to include("1.0")
+ expect(out).to include("no activesupport")
+ end
+ end
+
+ describe "when locked and installed with `without` setting" do
+ before(:each) do
+ build_repo2
+
+ system_gems "myrack-0.9.1"
+
+ bundle_config "without myrack"
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+
+ group :myrack do
+ gem "myrack_middleware"
+ end
+ G
+ end
+
+ it "uses versions from excluded gems in a machine without the without configuration" do
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ expect(the_bundle).not_to include_gems "myrack_middleware 1.0"
+ simulate_new_machine
+
+ bundle :install
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ expect(the_bundle).to include_gems "myrack_middleware 1.0"
+ end
+
+ it "does not hit the remote a second time" do
+ FileUtils.rm_r gem_repo2
+ bundle_config "without myrack"
+ bundle :install, verbose: true
+ expect(stdboth).not_to match(/fetching/i)
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/install_if_spec.rb b/spec/bundler/install/gemfile/install_if_spec.rb
new file mode 100644
index 0000000000..05a6d15129
--- /dev/null
+++ b/spec/bundler/install/gemfile/install_if_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with install_if conditionals" do
+ it "follows the install_if DSL" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ install_if(lambda { true }) do
+ gem "activesupport", "2.3.5"
+ end
+ gem "thin", :install_if => false
+ install_if(lambda { false }) do
+ gem "foo"
+ end
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems("myrack 1.0", "activesupport 2.3.5")
+ expect(the_bundle).not_to include_gems("thin")
+ expect(the_bundle).not_to include_gems("foo")
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo1, "activesupport", "2.3.5"
+ c.checksum gem_repo1, "foo", "1.0"
+ c.checksum gem_repo1, "myrack", "1.0.0"
+ c.checksum gem_repo1, "thin", "1.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ activesupport (2.3.5)
+ foo (1.0)
+ myrack (1.0.0)
+ thin (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ activesupport (= 2.3.5)
+ foo
+ myrack
+ thin
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+end
diff --git a/spec/bundler/install/gemfile/lockfile_spec.rb b/spec/bundler/install/gemfile/lockfile_spec.rb
new file mode 100644
index 0000000000..19bd7074b2
--- /dev/null
+++ b/spec/bundler/install/gemfile/lockfile_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with a lockfile present" do
+ let(:gf) { <<-G }
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ G
+
+ it "touches the lockfile on install even when nothing has changed" do
+ install_gemfile(gf)
+ expect { bundle :install }.to change { bundled_app_lock.mtime }
+ end
+
+ context "gemfile evaluation" do
+ let(:gf) { super() + "\n\n File.open('evals', 'a') {|f| f << %(1\n) } unless ENV['BUNDLER_SPEC_NO_APPEND']" }
+
+ context "with plugins disabled" do
+ before do
+ bundle_config "plugins false"
+ end
+
+ it "does not evaluate the gemfile twice when the gem is already installed" do
+ install_gemfile(gf)
+ bundle :install
+
+ with_env_vars("BUNDLER_SPEC_NO_APPEND" => "1") { expect(the_bundle).to include_gem "myrack 1.0.0" }
+
+ expect(bundled_app("evals").read.lines.to_a.size).to eq(2)
+ end
+
+ it "does not evaluate the gemfile twice when the gem is not installed" do
+ gemfile(gf)
+ bundle :install
+
+ with_env_vars("BUNDLER_SPEC_NO_APPEND" => "1") { expect(the_bundle).to include_gem "myrack 1.0.0" }
+
+ expect(bundled_app("evals").read.lines.to_a.size).to eq(1)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/override_spec.rb b/spec/bundler/install/gemfile/override_spec.rb
new file mode 100644
index 0000000000..02b0e7d772
--- /dev/null
+++ b/spec/bundler/install/gemfile/override_spec.rb
@@ -0,0 +1,401 @@
+# frozen_string_literal: true
+
+RSpec.describe "override DSL" do
+ context "with a version: string operation" do
+ it "replaces a direct dependency requirement with the override version spec" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: "= 0.9.1"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ end
+
+ it "replaces a transitive dependency requirement" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: "= 1.0.0"
+ gem "myrack_middleware"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0"
+ end
+
+ it "replaces the requirement even when the Gemfile pins a different version" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: "= 0.9.1"
+ gem "myrack", "= 1.0.0"
+ G
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ end
+
+ it "applies the override against an existing lockfile" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: "= 0.9.1"
+ gem "myrack"
+ G
+
+ bundle :install
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ end
+
+ it "pins a prerelease version that the Gemfile dependency would otherwise filter out" do
+ build_repo2 do
+ build_gem "has_prerelease", "1.0"
+ build_gem "has_prerelease", "1.1.pre"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ override "has_prerelease", version: "= 1.1.pre"
+ gem "has_prerelease"
+ G
+
+ expect(the_bundle).to include_gems "has_prerelease 1.1.pre"
+ end
+ end
+
+ context "with a version: :ignore_upper operation" do
+ it "strips a < upper bound on a direct dependency" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: :ignore_upper
+ gem "myrack", "< 1.0"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "folds ~> into >= so newer versions become reachable" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: :ignore_upper
+ gem "myrack", "~> 0.9.1"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ context "with a version: nil operation" do
+ it "drops a direct dependency's pin entirely" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: nil
+ gem "myrack", "= 0.9.1"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "drops a transitive dependency's pin entirely" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: nil
+ gem "myrack_middleware"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0"
+ end
+
+ it "applies a transitive-only override against an existing lockfile" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack_middleware"
+ G
+
+ expect(the_bundle).to include_gems "myrack 0.9.1", "myrack_middleware 1.0"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: "= 1.0.0"
+ gem "myrack_middleware"
+ G
+
+ bundle :install
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0"
+ end
+ end
+
+ context "lockfile contents" do
+ it "does not record the override directive in Gemfile.lock" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: "= 0.9.1"
+ gem "myrack"
+ G
+
+ expect(lockfile).not_to match(/override/i)
+ end
+ end
+
+ context "with a required_ruby_version: operation" do
+ it "lets the resolver pick a gem whose required_ruby_version excludes the current Ruby with :ignore_upper" do
+ build_repo2 do
+ build_gem "needs_old_ruby", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ override "needs_old_ruby", required_ruby_version: :ignore_upper
+ gem "needs_old_ruby"
+ G
+
+ bundle :lock
+ expect(lockfile).to include("needs_old_ruby (1.0)")
+ end
+
+ it "lets the resolver pick the gem with required_ruby_version: nil" do
+ build_repo2 do
+ build_gem "needs_old_ruby", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ override "needs_old_ruby", required_ruby_version: nil
+ gem "needs_old_ruby"
+ G
+
+ bundle :lock
+ expect(lockfile).to include("needs_old_ruby (1.0)")
+ end
+
+ it "applies to a transitive dependency's required_ruby_version" do
+ build_repo2 do
+ build_gem "needs_old_ruby", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ build_gem "wraps_old", "1.0" do |s|
+ s.add_dependency "needs_old_ruby"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ override "needs_old_ruby", required_ruby_version: :ignore_upper
+ gem "wraps_old"
+ G
+
+ bundle :lock
+ expect(lockfile).to include("needs_old_ruby (1.0)")
+ expect(lockfile).to include("wraps_old (1.0)")
+ end
+
+ it "re-resolves a direct dep when a metadata override is added against an existing lockfile" do
+ build_repo2 do
+ build_gem "selectable", "1.0"
+ build_gem "selectable", "2.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "selectable"
+ G
+
+ bundle :lock
+ expect(lockfile).to include("selectable (1.0)")
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ override "selectable", required_ruby_version: :ignore_upper
+ gem "selectable"
+ G
+
+ bundle :lock
+ expect(lockfile).to include("selectable (2.0)")
+ end
+ end
+
+ context "with a required_rubygems_version: operation" do
+ it "lets the resolver pick a gem whose required_rubygems_version excludes the current RubyGems with :ignore_upper" do
+ build_repo2 do
+ build_gem "needs_old_rubygems", "1.0" do |s|
+ s.required_rubygems_version = "< #{Gem.rubygems_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ override "needs_old_rubygems", required_rubygems_version: :ignore_upper
+ gem "needs_old_rubygems"
+ G
+
+ bundle :lock
+ expect(lockfile).to include("needs_old_rubygems (1.0)")
+ end
+ end
+
+ context "with an :all target" do
+ it "applies required_ruby_version: :ignore_upper to every gem" do
+ build_repo2 do
+ build_gem "needs_old_ruby_a", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ build_gem "needs_old_ruby_b", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ override :all, required_ruby_version: :ignore_upper
+ gem "needs_old_ruby_a"
+ gem "needs_old_ruby_b"
+ G
+
+ bundle :lock
+ expect(lockfile).to include("needs_old_ruby_a (1.0)")
+ expect(lockfile).to include("needs_old_ruby_b (1.0)")
+ end
+
+ it "is overridden by a per-gem override on the same field" do
+ build_repo2 do
+ build_gem "permissive", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ build_gem "still_blocked", "1.0" do |s|
+ s.required_ruby_version = "= #{Gem.ruby_version}.999"
+ end
+ end
+
+ # :all says ignore_upper (would unblock both), but per-gem on
+ # still_blocked nails it to a hard requirement that still fails.
+ gemfile <<-G
+ source "https://gem.repo2"
+ override :all, required_ruby_version: :ignore_upper
+ override "still_blocked", required_ruby_version: "= #{Gem.ruby_version}.999"
+ gem "permissive"
+ gem "still_blocked"
+ G
+
+ bundle :lock, raise_on_error: false
+ expect(err).to include("still_blocked")
+ end
+
+ it "preserves locked versions when an :all metadata override is added without bundle update" do
+ build_repo2 do
+ build_gem "selectable", "1.0"
+ build_gem "selectable", "2.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "selectable"
+ G
+
+ bundle :lock
+ expect(lockfile).to include("selectable (1.0)")
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ override :all, required_ruby_version: :ignore_upper
+ gem "selectable"
+ G
+
+ # :all override alone does not pre-unlock locked specs; narrow change
+ # should not trigger unrelated lockfile churn.
+ bundle :lock
+ expect(lockfile).to include("selectable (1.0)")
+
+ # bundle update opts the user into re-resolution under the override.
+ bundle "update selectable"
+ expect(lockfile).to include("selectable (2.0)")
+ end
+ end
+
+ context "diagnostic on resolve failure" do
+ it "lists active overrides with their Gemfile location" do
+ build_repo2 do
+ build_gem "needs_old_ruby", "1.0" do |s|
+ s.required_ruby_version = "= #{Gem.ruby_version}.999"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ override "needs_old_ruby", required_ruby_version: "= #{Gem.ruby_version}.999"
+ gem "needs_old_ruby"
+ G
+
+ bundle :lock, raise_on_error: false
+ expect(err).to include("Bundler applied the following overrides")
+ expect(err).to include("override \"needs_old_ruby\", required_ruby_version:")
+ expect(err).to match(/declared at Gemfile:\d+/)
+ end
+ end
+
+ context "install-time compatibility" do
+ it "installs a gem whose required_ruby_version excludes the current Ruby when an override removes the constraint" do
+ build_repo2 do
+ build_gem "needs_old_ruby", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ override "needs_old_ruby", required_ruby_version: nil
+ gem "needs_old_ruby"
+ G
+
+ expect(the_bundle).to include_gems "needs_old_ruby 1.0"
+ end
+
+ it "installs a gem whose required_rubygems_version excludes the current RubyGems when an override removes it" do
+ build_repo2 do
+ build_gem "needs_old_rubygems", "1.0" do |s|
+ s.required_rubygems_version = "< #{Gem.rubygems_version}"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ override "needs_old_rubygems", required_rubygems_version: nil
+ gem "needs_old_rubygems"
+ G
+
+ expect(the_bundle).to include_gems "needs_old_rubygems 1.0"
+ end
+
+ it "installs every gem when :all required_ruby_version override is in effect" do
+ build_repo2 do
+ build_gem "needs_old_ruby_a", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ build_gem "needs_old_ruby_b", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ override :all, required_ruby_version: :ignore_upper
+ gem "needs_old_ruby_a"
+ gem "needs_old_ruby_b"
+ G
+
+ expect(the_bundle).to include_gems "needs_old_ruby_a 1.0", "needs_old_ruby_b 1.0"
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/path_spec.rb b/spec/bundler/install/gemfile/path_spec.rb
new file mode 100644
index 0000000000..b069488531
--- /dev/null
+++ b/spec/bundler/install/gemfile/path_spec.rb
@@ -0,0 +1,1017 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with explicit source paths" do
+ it "fetches gems" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ path "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ it "supports pinned paths" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ gem 'foo', :path => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ it "supports relative paths" do
+ build_lib "foo"
+
+ relative_path = lib_path("foo-1.0").relative_path_from(bundled_app)
+
+ install_gemfile <<-G
+ gem 'foo', :path => "#{relative_path}"
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ it "expands paths" do
+ build_lib "foo"
+
+ relative_path = lib_path("foo-1.0").relative_path_from(Pathname.new("~").expand_path)
+
+ install_gemfile <<-G
+ gem 'foo', :path => "~/#{relative_path}"
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ it "expands paths raise error with not existing user's home dir" do
+ skip "problems with ~ expansion" if Gem.win_platform?
+
+ build_lib "foo"
+ username = "some_unexisting_user"
+ relative_path = lib_path("foo-1.0").relative_path_from(Pathname.new("/home/#{username}").expand_path)
+
+ install_gemfile <<-G, raise_on_error: false
+ gem 'foo', :path => "~#{username}/#{relative_path}"
+ G
+ expect(err).to match("There was an error while trying to use the path `~#{username}/#{relative_path}`.")
+ expect(err).to match("user #{username} doesn't exist")
+ end
+
+ it "expands paths relative to Bundler.root" do
+ build_lib "foo", path: bundled_app("foo-1.0")
+
+ install_gemfile <<-G
+ gem 'foo', :path => "./foo-1.0"
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0", dir: bundled_app("subdir").mkpath)
+ end
+
+ it "sorts paths consistently on install and update when they start with ./" do
+ build_lib "demo", path: lib_path("demo")
+ build_lib "aaa", path: lib_path("demo/aaa")
+
+ gemfile lib_path("demo/Gemfile"), <<-G
+ source "https://gem.repo1"
+ gemspec
+ gem "aaa", :path => "./aaa"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "aaa", "1.0"
+ c.no_checksum "demo", "1.0"
+ end
+
+ lockfile = <<~L
+ PATH
+ remote: .
+ specs:
+ demo (1.0)
+
+ PATH
+ remote: aaa
+ specs:
+ aaa (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ aaa!
+ demo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle :install, dir: lib_path("demo")
+ expect(lib_path("demo/Gemfile.lock")).to read_as(lockfile)
+ bundle :update, all: true, dir: lib_path("demo")
+ expect(lib_path("demo/Gemfile.lock")).to read_as(lockfile)
+ end
+
+ it "expands paths when comparing locked paths to Gemfile paths" do
+ build_lib "foo", path: bundled_app("foo-1.0")
+
+ install_gemfile <<-G
+ gem 'foo', :path => File.expand_path("foo-1.0", __dir__)
+ G
+
+ bundle_config "frozen true"
+ bundle :install
+ end
+
+ it "installs dependencies from the path even if a newer gem is available elsewhere" do
+ system_gems "myrack-1.0.0"
+
+ build_lib "myrack", "1.0", path: lib_path("nested/bar") do |s|
+ s.write "lib/myrack.rb", "puts 'WIN OVERRIDE'"
+ end
+
+ build_lib "foo", path: lib_path("nested") do |s|
+ s.add_dependency "myrack", "= 1.0"
+ end
+
+ install_gemfile <<-G
+ gem "foo", :path => "#{lib_path("nested")}"
+ G
+
+ run "require 'myrack'"
+ expect(out).to eq("WIN OVERRIDE")
+ end
+
+ it "works" do
+ build_gem "foo", "1.0.0", to_system: true do |s|
+ s.write "lib/foo.rb", "puts 'FAIL'"
+ end
+
+ build_lib "omg", "1.0", path: lib_path("omg") do |s|
+ s.add_dependency "foo"
+ end
+
+ build_lib "foo", "1.0.0", path: lib_path("omg/foo")
+
+ install_gemfile <<-G
+ gem "omg", :path => "#{lib_path("omg")}"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "works when using prereleases of 0.0.0" do
+ build_lib "foo", "0.0.0.dev", path: lib_path("foo")
+
+ gemfile <<~G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo")}"
+ G
+
+ lockfile <<~L
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (0.0.0.dev)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle :install
+
+ expect(the_bundle).to include_gems "foo 0.0.0.dev"
+ end
+
+ it "works when using uppercase prereleases of 0.0.0" do
+ build_lib "foo", "0.0.0.SNAPSHOT", path: lib_path("foo")
+
+ gemfile <<~G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo")}"
+ G
+
+ lockfile <<~L
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (0.0.0.SNAPSHOT)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle :install
+
+ expect(the_bundle).to include_gems "foo 0.0.0.SNAPSHOT"
+ end
+
+ it "handles downgrades" do
+ build_lib "omg", "2.0", path: lib_path("omg")
+
+ install_gemfile <<-G
+ gem "omg", :path => "#{lib_path("omg")}"
+ G
+
+ build_lib "omg", "1.0", path: lib_path("omg")
+
+ bundle :install
+
+ expect(the_bundle).to include_gems "omg 1.0"
+ end
+
+ it "prefers gemspecs closer to the path root" do
+ build_lib "premailer", "1.0.0", path: lib_path("premailer") do |s|
+ s.write "gemfiles/ruby187.gemspec", <<-G
+ Gem::Specification.new do |s|
+ s.name = 'premailer'
+ s.version = '1.0.0'
+ s.summary = 'Hi'
+ s.authors = 'Me'
+ end
+ G
+ end
+
+ install_gemfile <<-G
+ gem "premailer", :path => "#{lib_path("premailer")}"
+ G
+
+ # Installation of the 'gemfiles' gemspec would fail since it will be unable
+ # to require 'premailer.rb'
+ expect(the_bundle).to include_gems "premailer 1.0.0"
+ end
+
+ it "warns on invalid specs" do
+ build_lib "foo"
+
+ gemspec = lib_path("foo-1.0").join("foo.gemspec").to_s
+ File.open(gemspec, "w") do |f|
+ f.write <<-G
+ Gem::Specification.new do |s|
+ s.name = "foo"
+ end
+ G
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ gem "foo", :path => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(err).to match(/is not valid. Please fix this gemspec./)
+ expect(err).to match(/The validation error was 'missing value for attribute version'/)
+ expect(err).to match(/You have one or more invalid gemspecs that need to be fixed/)
+ end
+
+ it "supports gemspec syntax" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", "1.0"
+ end
+
+ gemfile lib_path("foo/Gemfile"), <<-G
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ bundle "install", dir: lib_path("foo")
+ expect(the_bundle).to include_gems "foo 1.0", dir: lib_path("foo")
+ expect(the_bundle).to include_gems "myrack 1.0", dir: lib_path("foo")
+ end
+
+ it "does not unlock dependencies of path sources" do
+ build_repo4 do
+ build_gem "graphql", "2.0.15"
+ build_gem "graphql", "2.0.16"
+ end
+
+ build_lib "foo", "0.1.0", path: lib_path("foo") do |s|
+ s.add_dependency "graphql", "~> 2.0"
+ end
+
+ gemfile_path = lib_path("foo/Gemfile")
+
+ gemfile gemfile_path, <<-G
+ source "https://gem.repo4"
+ gemspec
+ G
+
+ lockfile_path = lib_path("foo/Gemfile.lock")
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "0.1.0"
+ c.checksum gem_repo4, "graphql", "2.0.15"
+ end
+
+ original_lockfile = <<~L
+ PATH
+ remote: .
+ specs:
+ foo (0.1.0)
+ graphql (~> 2.0)
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ graphql (2.0.15)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile lockfile_path, original_lockfile
+
+ build_lib "foo", "0.1.1", path: lib_path("foo") do |s|
+ s.add_dependency "graphql", "~> 2.0"
+ end
+
+ bundle "install", dir: lib_path("foo")
+ expect(lockfile_path).to read_as(original_lockfile.gsub("foo (0.1.0)", "foo (0.1.1)"))
+ end
+
+ it "supports gemspec syntax with an alternative path" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec :path => "#{lib_path("foo")}"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+
+ it "doesn't automatically unlock dependencies when using the gemspec syntax" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", ">= 1.0"
+ end
+
+ install_gemfile lib_path("foo/Gemfile"), <<-G, dir: lib_path("foo")
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ build_gem "myrack", "1.0.1", to_system: true
+
+ bundle "install", dir: lib_path("foo")
+
+ expect(the_bundle).to include_gems "foo 1.0", dir: lib_path("foo")
+ expect(the_bundle).to include_gems "myrack 1.0", dir: lib_path("foo")
+ end
+
+ it "doesn't automatically unlock dependencies when using the gemspec syntax and the gem has development dependencies" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", ">= 1.0"
+ s.add_development_dependency "activesupport"
+ end
+
+ install_gemfile lib_path("foo/Gemfile"), <<-G, dir: lib_path("foo")
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ build_gem "myrack", "1.0.1", to_system: true
+
+ bundle "install", dir: lib_path("foo")
+
+ expect(the_bundle).to include_gems "foo 1.0", dir: lib_path("foo")
+ expect(the_bundle).to include_gems "myrack 1.0", dir: lib_path("foo")
+ end
+
+ it "raises if there are multiple gemspecs" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.write "bar.gemspec", build_spec("bar", "1.0").first.to_ruby
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ gemspec :path => "#{lib_path("foo")}"
+ G
+
+ expect(exitstatus).to eq(15)
+ expect(err).to match(/There are multiple gemspecs/)
+ end
+
+ it "allows :name to be specified to resolve ambiguity" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.write "bar.gemspec"
+ end
+
+ install_gemfile <<-G
+ gemspec :path => "#{lib_path("foo")}", :name => "foo"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "sets up executables" do
+ build_lib "foo" do |s|
+ s.executables = "foobar"
+ end
+
+ install_gemfile <<-G, verbose: true
+ path "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ G
+ expect(out).to include("Using foo 1.0 from source at `#{lib_path("foo-1.0")}` and installing its executables")
+ expect(the_bundle).to include_gems "foo 1.0"
+
+ bundle "exec foobar"
+ expect(out).to eq("1.0")
+ end
+
+ it "handles directories in bin/" do
+ build_lib "foo"
+ FileUtils.rm_rf lib_path("foo-1.0").join("foo.gemspec")
+ lib_path("foo-1.0").join("bin/performance").mkpath
+
+ install_gemfile <<-G
+ gem 'foo', '1.0', :path => "#{lib_path("foo-1.0")}"
+ G
+ expect(err).to be_empty
+ end
+
+ it "removes the .gem file after installing" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ gem 'foo', :path => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(lib_path("foo-1.0").join("foo-1.0.gem")).not_to exist
+ end
+
+ describe "block syntax" do
+ it "pulls all gems from a path block" do
+ build_lib "omg"
+ build_lib "hi2u"
+
+ install_gemfile <<-G
+ path "#{lib_path}" do
+ gem "omg"
+ gem "hi2u"
+ end
+ G
+
+ expect(the_bundle).to include_gems "omg 1.0", "hi2u 1.0"
+ end
+ end
+
+ it "keeps source pinning" do
+ build_lib "foo", "1.0", path: lib_path("foo")
+ build_lib "omg", "1.0", path: lib_path("omg")
+ build_lib "foo", "1.0", path: lib_path("omg/foo") do |s|
+ s.write "lib/foo.rb", "puts 'FAIL'"
+ end
+
+ install_gemfile <<-G
+ gem "foo", :path => "#{lib_path("foo")}"
+ gem "omg", :path => "#{lib_path("omg")}"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "works when the path does not have a gemspec" do
+ build_lib "foo", gemspec: false
+
+ gemfile <<-G
+ gem "foo", "1.0", :path => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "works when the path does not have a gemspec but there is a lockfile" do
+ lockfile <<~L
+ PATH
+ remote: vendor/bar
+ specs:
+ L
+
+ FileUtils.mkdir_p(bundled_app("vendor/bar"))
+
+ install_gemfile <<-G
+ gem "bar", "1.0.0", path: "vendor/bar", require: "bar/nyard"
+ G
+ end
+
+ context "existing lockfile" do
+ it "rubygems gems don't re-resolve without changes" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack-obama', '1.0'
+ gem 'net-ssh', '1.0'
+ G
+
+ bundle :check, env: { "DEBUG" => "1" }
+ expect(out).to match(/using resolution from the lockfile/)
+ expect(the_bundle).to include_gems "myrack-obama 1.0", "net-ssh 1.0"
+ end
+
+ it "source path gems w/deps don't re-resolve without changes" do
+ build_lib "myrack-obama", "1.0", path: lib_path("omg") do |s|
+ s.add_dependency "yard"
+ end
+
+ build_lib "net-ssh", "1.0", path: lib_path("omg") do |s|
+ s.add_dependency "yard"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack-obama', :path => "#{lib_path("omg")}"
+ gem 'net-ssh', :path => "#{lib_path("omg")}"
+ G
+
+ bundle :check, env: { "DEBUG" => "1" }
+ expect(out).to match(/using resolution from the lockfile/)
+ expect(the_bundle).to include_gems "myrack-obama 1.0", "net-ssh 1.0"
+ end
+ end
+
+ it "installs executable stubs" do
+ build_lib "foo" do |s|
+ s.executables = ["foo"]
+ end
+
+ install_gemfile <<-G
+ gem "foo", :path => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle "exec foo"
+ expect(out).to eq("1.0")
+ end
+
+ describe "when the gem version in the path is updated" do
+ before :each do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "bar"
+ end
+ build_lib "bar", "1.0", path: lib_path("foo/bar")
+
+ install_gemfile <<-G
+ gem "foo", :path => "#{lib_path("foo")}"
+ G
+ end
+
+ it "unlocks all gems when the top level gem is updated" do
+ build_lib "foo", "2.0", path: lib_path("foo") do |s|
+ s.add_dependency "bar"
+ end
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "foo 2.0", "bar 1.0"
+ end
+
+ it "unlocks all gems when a child dependency gem is updated" do
+ build_lib "bar", "2.0", path: lib_path("foo/bar")
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "foo 1.0", "bar 2.0"
+ end
+ end
+
+ describe "when dependencies in the path are updated" do
+ before :each do
+ build_lib "foo", "1.0", path: lib_path("foo")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo")}"
+ G
+ end
+
+ it "gets dependencies that are updated in the path" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack"
+ end
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "keeps using the same version if it's compatible" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", "0.9.1"
+ end
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo1, "myrack", "0.9.1"
+ end
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (1.0)
+ myrack (= 0.9.1)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack"
+ end
+
+ bundle "install"
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (1.0)
+ myrack
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ end
+
+ it "keeps using the same version even when another dependency is added" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", "0.9.1"
+ end
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo1, "myrack", "0.9.1"
+ end
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (1.0)
+ myrack (= 0.9.1)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack"
+ s.add_dependency "rake", rake_version
+ end
+
+ bundle "install"
+
+ checksums.checksum gem_repo1, "rake", rake_version
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (1.0)
+ myrack
+ rake (= #{rake_version})
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (0.9.1)
+ rake (#{rake_version})
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ end
+
+ it "does not remove existing ruby platform" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", "0.9.1"
+ end
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ lockfile <<~L
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms("ruby")}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock"
+
+ checksums.checksum gem_repo1, "myrack", "0.9.1"
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (1.0)
+ myrack (= 0.9.1)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms("ruby")}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+ end
+
+ context "when platform specific version locked, and having less dependencies that the generic version that's actually installed" do
+ before do
+ build_repo4 do
+ build_gem "racc", "1.8.1"
+ build_gem "mini_portile2", "2.8.2"
+ end
+
+ build_lib "nokogiri", "1.18.9", path: lib_path("nokogiri") do |s|
+ s.add_dependency "mini_portile2", "~> 2.8.2"
+ s.add_dependency "racc", "~> 1.4"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri", path: "#{lib_path("nokogiri")}"
+ G
+
+ lockfile <<~L
+ PATH
+ remote: #{lib_path("nokogiri")}
+ specs:
+ nokogiri (1.18.9)
+ mini_portile2 (~> 2.8.2)
+ racc (~> 1.4)
+ nokogiri (1.18.9-arm64-darwin)
+ racc (~> 1.4)
+
+ GEM
+ remote: https://rubygems.org/
+ specs:
+ racc (1.8.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ nokogiri!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "works" do
+ bundle "install"
+ end
+ end
+
+ describe "switching sources" do
+ it "doesn't switch pinned git sources to rubygems when pinning the parent gem to a path source" do
+ build_gem "foo", "1.0", to_system: true do |s|
+ s.write "lib/foo.rb", "raise 'fail'"
+ end
+ build_lib "foo", "1.0", path: lib_path("bar/foo")
+ build_git "bar", "1.0", path: lib_path("bar") do |s|
+ s.add_dependency "foo"
+ end
+
+ install_gemfile <<-G
+ gem "bar", :git => "#{lib_path("bar")}"
+ G
+
+ install_gemfile <<-G
+ gem "bar", :path => "#{lib_path("bar")}"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0", "bar 1.0"
+ end
+
+ it "switches the source when the gem existed in rubygems and the path was already being used for another gem" do
+ build_lib "foo", "1.0", path: lib_path("foo")
+ build_gem "bar", "1.0", to_bundle: true do |s|
+ s.write "lib/bar.rb", "raise 'fail'"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "bar"
+ path "#{lib_path("foo")}" do
+ gem "foo"
+ end
+ G
+
+ build_lib "bar", "1.0", path: lib_path("foo/bar")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path("foo")}" do
+ gem "foo"
+ gem "bar"
+ end
+ G
+
+ expect(the_bundle).to include_gems "bar 1.0"
+ end
+ end
+
+ describe "when there are both a gemspec and remote gems" do
+ it "doesn't query rubygems for local gemspec name" do
+ build_lib "private_lib", "2.2", path: lib_path("private_lib")
+ gemfile lib_path("private_lib/Gemfile"), <<-G
+ source "http://localgemserver.test"
+ gemspec
+ gem 'myrack'
+ G
+ bundle :install, env: { "DEBUG" => "1" }, artifice: "endpoint", dir: lib_path("private_lib")
+ expect(out).to match(%r{^HTTP GET http://localgemserver\.test/api/v1/dependencies\?gems=myrack$})
+ expect(out).not_to match(/^HTTP GET.*private_lib/)
+ expect(the_bundle).to include_gems "private_lib 2.2", dir: lib_path("private_lib")
+ expect(the_bundle).to include_gems "myrack 1.0", dir: lib_path("private_lib")
+ end
+ end
+
+ describe "gem install hooks" do
+ it "runs pre-install hooks" do
+ build_git "foo"
+ gemfile <<-G
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ File.open(lib_path("install_hooks.rb"), "w") do |h|
+ h.write <<-H
+ Gem.pre_install_hooks << lambda do |inst|
+ STDERR.puts "Ran pre-install hook: \#{inst.spec.full_name}"
+ end
+ H
+ end
+
+ bundle :install,
+ requires: [lib_path("install_hooks.rb")]
+ expect(err_without_deprecations).to eq("Ran pre-install hook: foo-1.0")
+ end
+
+ it "runs post-install hooks" do
+ build_git "foo"
+ gemfile <<-G
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ File.open(lib_path("install_hooks.rb"), "w") do |h|
+ h.write <<-H
+ Gem.post_install_hooks << lambda do |inst|
+ STDERR.puts "Ran post-install hook: \#{inst.spec.full_name}"
+ end
+ H
+ end
+
+ bundle :install,
+ requires: [lib_path("install_hooks.rb")]
+ expect(err_without_deprecations).to eq("Ran post-install hook: foo-1.0")
+ end
+
+ it "complains if the install hook fails" do
+ build_git "foo"
+ gemfile <<-G
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ File.open(lib_path("install_hooks.rb"), "w") do |h|
+ h.write <<-H
+ Gem.pre_install_hooks << lambda do |inst|
+ false
+ end
+ H
+ end
+
+ bundle :install, requires: [lib_path("install_hooks.rb")], raise_on_error: false
+ expect(err).to include("failed for foo-1.0")
+ end
+
+ it "loads plugins from the path gem" do
+ foo_file = home("foo_plugin_loaded")
+ bar_file = home("bar_plugin_loaded")
+ expect(foo_file).not_to be_file
+ expect(bar_file).not_to be_file
+
+ build_lib "foo" do |s|
+ s.write("lib/rubygems_plugin.rb", "require 'fileutils'; FileUtils.touch('#{foo_file}')")
+ end
+
+ build_git "bar" do |s|
+ s.write("lib/rubygems_plugin.rb", "require 'fileutils'; FileUtils.touch('#{bar_file}')")
+ end
+
+ install_gemfile <<-G
+ gem "foo", :path => "#{lib_path("foo-1.0")}"
+ gem "bar", :path => "#{lib_path("bar-1.0")}"
+ G
+
+ expect(foo_file).to be_file
+ expect(bar_file).to be_file
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/platform_spec.rb b/spec/bundler/install/gemfile/platform_spec.rb
new file mode 100644
index 0000000000..e12933ebcf
--- /dev/null
+++ b/spec/bundler/install/gemfile/platform_spec.rb
@@ -0,0 +1,638 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install across platforms" do
+ it "maintains the same lockfile if all gems are compatible across platforms" do
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{not_local}
+
+ DEPENDENCIES
+ myrack
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ end
+
+ it "pulls in the correct platform specific gem" do
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1
+ specs:
+ platform_specific (1.0)
+ platform_specific (1.0-java)
+ platform_specific (1.0-x86-mswin32)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ platform_specific
+ G
+
+ simulate_platform "java" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 java"
+ end
+ end
+
+ it "pulls the pure ruby version on jruby if the java platform is not present in the lockfile and bundler is run in frozen mode", :jruby_only do
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1
+ specs:
+ platform_specific (1.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ platform_specific
+ G
+
+ bundle_config "frozen true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 ruby"
+ expect(err).to be_empty
+ end
+
+ context "on universal Rubies" do
+ before do
+ build_repo4 do
+ build_gem "darwin_single_arch" do |s|
+ s.platform = "ruby"
+ end
+ build_gem "darwin_single_arch" do |s|
+ s.platform = "arm64-darwin"
+ end
+ build_gem "darwin_single_arch" do |s|
+ s.platform = "x86_64-darwin"
+ end
+ end
+ end
+
+ it "pulls in the correct architecture gem" do
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo4
+ specs:
+ darwin_single_arch (1.0)
+ darwin_single_arch (1.0-arm64-darwin)
+ darwin_single_arch (1.0-x86_64-darwin)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ darwin_single_arch
+ G
+
+ simulate_platform "universal-darwin-21" do
+ simulate_ruby_platform "universal.x86_64-darwin21" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "darwin_single_arch"
+ G
+
+ expect(the_bundle).to include_gems "darwin_single_arch 1.0 x86_64-darwin"
+ end
+ end
+ end
+
+ it "pulls in the correct architecture gem on arm64e macOS Ruby" do
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo4
+ specs:
+ darwin_single_arch (1.0)
+ darwin_single_arch (1.0-arm64-darwin)
+ darwin_single_arch (1.0-x86_64-darwin)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ darwin_single_arch
+ G
+
+ simulate_platform "universal-darwin-21" do
+ simulate_ruby_platform "universal.arm64e-darwin21" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "darwin_single_arch"
+ G
+
+ expect(the_bundle).to include_gems "darwin_single_arch 1.0 arm64-darwin"
+ end
+ end
+ end
+ end
+
+ it "works with gems that have different dependencies" do
+ simulate_platform "java" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "nokogiri"
+ G
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2 java", "weakling 0.0.3"
+
+ pristine_system_gems
+ bundle_config "force_ruby_platform true"
+ bundle "install"
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2"
+ expect(the_bundle).not_to include_gems "weakling"
+
+ simulate_new_machine
+ bundle "install"
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2 java", "weakling 0.0.3"
+ end
+ end
+
+ it "does not keep unneeded platforms for gems that are used" do
+ build_repo4 do
+ build_gem "empyrean", "0.1.0"
+ build_gem "coderay", "1.1.2"
+ build_gem "method_source", "0.9.0"
+ build_gem("spoon", "0.0.6") {|s| s.add_dependency "ffi" }
+ build_gem "pry", "0.11.3" do |s|
+ s.platform = "java"
+ s.add_dependency "coderay", "~> 1.1.0"
+ s.add_dependency "method_source", "~> 0.9.0"
+ s.add_dependency "spoon", "~> 0.0"
+ end
+ build_gem "pry", "0.11.3" do |s|
+ s.add_dependency "coderay", "~> 1.1.0"
+ s.add_dependency "method_source", "~> 0.9.0"
+ end
+ build_gem("ffi", "1.9.23") {|s| s.platform = "java" }
+ build_gem("ffi", "1.9.23")
+ end
+
+ simulate_platform "java" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "empyrean", "0.1.0"
+ gem "pry"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "coderay", "1.1.2"
+ c.checksum gem_repo4, "empyrean", "0.1.0"
+ c.checksum gem_repo4, "ffi", "1.9.23", "java"
+ c.checksum gem_repo4, "method_source", "0.9.0"
+ c.checksum gem_repo4, "pry", "0.11.3", "java"
+ c.checksum gem_repo4, "spoon", "0.0.6"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ coderay (1.1.2)
+ empyrean (0.1.0)
+ ffi (1.9.23-java)
+ method_source (0.9.0)
+ pry (0.11.3-java)
+ coderay (~> 1.1.0)
+ method_source (~> 0.9.0)
+ spoon (~> 0.0)
+ spoon (0.0.6)
+ ffi
+
+ PLATFORMS
+ java
+
+ DEPENDENCIES
+ empyrean (= 0.1.0)
+ pry
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --add-platform ruby"
+
+ checksums.checksum gem_repo4, "pry", "0.11.3"
+
+ good_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ coderay (1.1.2)
+ empyrean (0.1.0)
+ ffi (1.9.23-java)
+ method_source (0.9.0)
+ pry (0.11.3)
+ coderay (~> 1.1.0)
+ method_source (~> 0.9.0)
+ pry (0.11.3-java)
+ coderay (~> 1.1.0)
+ method_source (~> 0.9.0)
+ spoon (~> 0.0)
+ spoon (0.0.6)
+ ffi
+
+ PLATFORMS
+ java
+ ruby
+
+ DEPENDENCIES
+ empyrean (= 0.1.0)
+ pry
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ expect(lockfile).to eq good_lockfile
+
+ bad_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ coderay (1.1.2)
+ empyrean (0.1.0)
+ ffi (1.9.23)
+ ffi (1.9.23-java)
+ method_source (0.9.0)
+ pry (0.11.3)
+ coderay (~> 1.1.0)
+ method_source (~> 0.9.0)
+ pry (0.11.3-java)
+ coderay (~> 1.1.0)
+ method_source (~> 0.9.0)
+ spoon (~> 0.0)
+ spoon (0.0.6)
+ ffi
+
+ PLATFORMS
+ java
+ ruby
+
+ DEPENDENCIES
+ empyrean (= 0.1.0)
+ pry
+ #{checksums}
+ BUNDLED WITH
+ 1.16.1
+ L
+
+ aggregate_failures do
+ lockfile bad_lockfile
+ bundle :install, env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ expect(lockfile).to eq good_lockfile
+
+ lockfile bad_lockfile
+ bundle :update, all: true, env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ expect(lockfile).to eq good_lockfile
+
+ lockfile bad_lockfile
+ bundle "update ffi", env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ expect(lockfile).to eq good_lockfile
+
+ lockfile bad_lockfile
+ bundle "update empyrean", env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ expect(lockfile).to eq good_lockfile
+
+ lockfile bad_lockfile
+ bundle :lock, env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ expect(lockfile).to eq good_lockfile
+ end
+ end
+ end
+
+ it "works with gems with platform-specific dependency having different requirements order" do
+ simulate_platform "x86_64-darwin-15" do
+ update_repo2 do
+ build_gem "fspath", "3"
+ build_gem "image_optim_pack", "1.2.3" do |s|
+ s.add_dependency "fspath", ">= 2.1", "< 4"
+ end
+ build_gem "image_optim_pack", "1.2.3" do |s|
+ s.platform = "universal-darwin"
+ s.add_dependency "fspath", "< 4", ">= 2.1"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "image_optim_pack"
+ G
+
+ expect(err).not_to include "Unable to use the platform-specific"
+
+ expect(the_bundle).to include_gem "image_optim_pack 1.2.3 universal-darwin"
+ end
+ end
+
+ it "fetches gems again after changing the version of Ruby" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle :install
+
+ FileUtils.mv(vendored_gems, bundled_app("vendor/bundle", Gem.ruby_engine, "1.8"))
+
+ bundle :install
+ expect(vendored_gems("gems/myrack-1.0.0")).to exist
+ end
+
+ it "keeps existing platforms when installing with force_ruby_platform" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo1, "platform_specific", "1.0"
+ c.checksum gem_repo1, "platform_specific", "1.0", "java"
+ end
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ platform_specific (1.0-java)
+
+ PLATFORMS
+ java
+
+ DEPENDENCIES
+ platform_specific
+ #{checksums}
+ G
+
+ bundle_config "force_ruby_platform true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ checksums.checksum gem_repo1, "platform_specific", "1.0"
+
+ expect(the_bundle).to include_gem "platform_specific 1.0 ruby"
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ platform_specific (1.0)
+ platform_specific (1.0-java)
+
+ PLATFORMS
+ java
+ ruby
+
+ DEPENDENCIES
+ platform_specific
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+end
+
+RSpec.describe "bundle install with platform conditionals" do
+ it "installs gems tagged w/ the current platforms" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ platforms :#{local_tag} do
+ gem "nokogiri"
+ end
+ G
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2"
+ end
+
+ it "does not install gems tagged w/ another platforms" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ platforms :#{not_local_tag} do
+ gem "nokogiri"
+ end
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0"
+ expect(the_bundle).not_to include_gems "nokogiri 1.4.2"
+ end
+
+ it "installs gems tagged w/ another platform but also dependent on the current one transitively" do
+ build_repo4 do
+ build_gem "activesupport", "6.1.4.1" do |s|
+ s.add_dependency "tzinfo", "~> 2.0"
+ end
+
+ build_gem "tzinfo", "2.0.4"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "activesupport"
+
+ platforms :#{not_local_tag} do
+ gem "tzinfo", "~> 1.2"
+ end
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ activesupport (6.1.4.1)
+ tzinfo (~> 2.0)
+ tzinfo (2.0.4)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ activesupport
+ tzinfo (~> 1.2)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install --verbose"
+
+ expect(the_bundle).to include_gems "tzinfo 2.0.4"
+ end
+
+ it "installs gems tagged w/ the current platforms inline" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri", :platforms => :#{local_tag}
+ G
+ expect(the_bundle).to include_gems "nokogiri 1.4.2"
+ end
+
+ it "does not install gems tagged w/ another platforms inline" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "nokogiri", :platforms => :#{not_local_tag}
+ G
+ expect(the_bundle).to include_gems "myrack 1.0"
+ expect(the_bundle).not_to include_gems "nokogiri 1.4.2"
+ end
+
+ it "installs gems tagged w/ the current platform inline" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri", :platform => :#{local_tag}
+ G
+ expect(the_bundle).to include_gems "nokogiri 1.4.2"
+ end
+
+ it "doesn't install gems tagged w/ another platform inline" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri", :platform => :#{not_local_tag}
+ G
+ expect(the_bundle).not_to include_gems "nokogiri 1.4.2"
+ end
+
+ it "does not blow up on sources with all platform-excluded specs" do
+ build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ platform :#{not_local_tag} do
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ end
+ G
+
+ bundle :list
+ end
+
+ it "does not attempt to install gems from :rbx when using --local" do
+ bundle_config "force_ruby_platform true"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "some_gem", :platform => :rbx
+ G
+
+ bundle "install --local"
+ expect(out).not_to match(/Could not find gem 'some_gem/)
+ end
+
+ it "does not attempt to install gems from other rubies when using --local" do
+ bundle_config "force_ruby_platform true"
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "some_gem", platform: :ruby_22
+ G
+
+ bundle "install --local"
+ expect(out).not_to match(/Could not find gem 'some_gem/)
+ end
+
+ it "does not print a warning when a dependency is unused on a platform different from the current one" do
+ bundle_config "force_ruby_platform true"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", :platform => [:windows, :jruby]
+ G
+
+ bundle "install"
+
+ expect(err).to be_empty
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ myrack
+ #{checksums_section_when_enabled}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "resolves fine when a dependency is unused on a platform different from the current one, but reintroduced transitively" do
+ bundle_config "force_ruby_platform true"
+
+ build_repo4 do
+ build_gem "listen", "3.7.1" do |s|
+ s.add_dependency "ffi"
+ end
+
+ build_gem "ffi", "1.15.5"
+ end
+
+ install_gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "listen"
+ gem "ffi", :platform => :windows
+ G
+ expect(err).to be_empty
+ end
+end
+
+RSpec.describe "when a gem has no architecture" do
+ it "still installs correctly" do
+ simulate_platform "x86-mswin32" do
+ build_repo2 do
+ # The rcov gem is platform mswin32, but has no arch
+ build_gem "rcov" do |s|
+ s.platform = Gem::Platform.new([nil, "mswin32", nil])
+ s.write "lib/rcov.rb", "RCOV = '1.0.0'"
+ end
+ end
+
+ gemfile <<-G
+ # Try to install gem with nil arch
+ source "http://localgemserver.test/"
+ gem "rcov"
+ G
+
+ bundle :install, artifice: "windows", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
+ expect(the_bundle).to include_gems "rcov 1.0.0"
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/ruby_spec.rb b/spec/bundler/install/gemfile/ruby_spec.rb
new file mode 100644
index 0000000000..d937abd714
--- /dev/null
+++ b/spec/bundler/install/gemfile/ruby_spec.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: true
+
+RSpec.describe "ruby requirement" do
+ def locked_ruby_version
+ Bundler::RubyVersion.from_string(Bundler::LockfileParser.new(File.read(bundled_app_lock)).ruby_version)
+ end
+
+ # As discovered by https://github.com/rubygems/bundler/issues/4147, there is
+ # no test coverage to ensure that adding a gem is possible with a ruby
+ # requirement. This test verifies the fix, committed in bfbad5c5.
+ it "allows adding gems" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby "#{Gem.ruby_version}"
+ gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby "#{Gem.ruby_version}"
+ gem "myrack"
+ gem "myrack-obama"
+ G
+
+ expect(the_bundle).to include_gems "myrack-obama 1.0"
+ end
+
+ it "allows removing the ruby version requirement" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby "~> #{Gem.ruby_version}"
+ gem "myrack"
+ G
+
+ expect(lockfile).to include("RUBY VERSION")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(lockfile).not_to include("RUBY VERSION")
+ end
+
+ it "allows changing the ruby version requirement to something compatible" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby ">= #{current_ruby_minor}"
+ gem "myrack"
+ G
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ expect(locked_ruby_version).to eq(Bundler::RubyVersion.system)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby ">= #{Gem.ruby_version}"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(locked_ruby_version).to eq(Bundler::RubyVersion.system)
+ end
+
+ it "allows changing the ruby version requirement to something incompatible" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby ">= 1.0.0"
+ gem "myrack"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ RUBY VERSION
+ ruby 2.1.4
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby ">= #{current_ruby_minor}"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(locked_ruby_version).to eq(Bundler::RubyVersion.system)
+ end
+
+ it "allows requirements with trailing whitespace" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby "#{Gem.ruby_version}\\n \t\\n"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "fails gracefully with malformed requirements" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ ruby ">= 0", "-.\\0"
+ gem "myrack"
+ G
+
+ expect(err).to include("There was an error parsing") # i.e. DSL error, not error template
+ end
+
+ it "allows picking up ruby version from a file" do
+ create_file ".ruby-version", Gem.ruby_version.to_s
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby file: ".ruby-version"
+ gem "myrack"
+ G
+
+ expect(lockfile).to include("RUBY VERSION")
+ end
+
+ it "reads the ruby version file from the right folder when nested Gemfiles are involved" do
+ create_file ".ruby-version", Gem.ruby_version.to_s
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby file: ".ruby-version"
+ gem "myrack"
+ G
+
+ nested_dir = bundled_app(".ruby-lsp")
+
+ FileUtils.mkdir nested_dir
+
+ gemfile ".ruby-lsp/Gemfile", <<-G
+ eval_gemfile(File.expand_path("../Gemfile", __dir__))
+ G
+
+ bundle "install", dir: nested_dir
+
+ expect(bundled_app(".ruby-lsp/Gemfile.lock").read).to include("RUBY VERSION")
+ end
+end
diff --git a/spec/bundler/install/gemfile/sources_spec.rb b/spec/bundler/install/gemfile/sources_spec.rb
new file mode 100644
index 0000000000..654d638e1f
--- /dev/null
+++ b/spec/bundler/install/gemfile/sources_spec.rb
@@ -0,0 +1,1313 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with gems on multiple sources" do
+ # repo1 is built automatically before all of the specs run
+ # it contains myrack-obama 1.0.0 and myrack 0.9.1 & 1.0.0 amongst other gems
+
+ context "with source affinity" do
+ context "with sources given by a block" do
+ before do
+ # Oh no! Someone evil is trying to hijack myrack :(
+ # need this to be broken to check for correct source ordering
+ build_repo3 do
+ build_gem "myrack", "1.0.0" do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+
+ build_gem "myrack-obama" do |s|
+ s.add_dependency "myrack"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo3"
+ source "https://gem.repo1" do
+ gem "thin" # comes first to test name sorting
+ gem "myrack"
+ end
+ gem "myrack-obama" # should come from repo3!
+ G
+ end
+
+ it "installs the gems without any warning" do
+ bundle :install, artifice: "compact_index"
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("myrack-obama 1.0.0")
+ expect(the_bundle).to include_gems("myrack 1.0.0", source: "remote1")
+ end
+
+ it "can cache and deploy" do
+ bundle :cache, artifice: "compact_index"
+
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ expect(bundled_app("vendor/cache/myrack-obama-1.0.gem")).to exist
+
+ bundle_config "deployment true"
+ bundle :install, artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("myrack-obama 1.0.0", "myrack 1.0.0")
+ end
+ end
+
+ context "with sources set by an option" do
+ before do
+ # Oh no! Someone evil is trying to hijack myrack :(
+ # need this to be broken to check for correct source ordering
+ build_repo3 do
+ build_gem "myrack", "1.0.0" do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+
+ build_gem "myrack-obama" do |s|
+ s.add_dependency "myrack"
+ end
+ end
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo3"
+ gem "myrack-obama" # should come from repo3!
+ gem "myrack", :source => "https://gem.repo1"
+ G
+ end
+
+ it "installs the gems without any warning" do
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("myrack-obama 1.0.0", "myrack 1.0.0")
+ end
+ end
+
+ context "when a pinned gem has an indirect dependency in the pinned source" do
+ before do
+ build_repo3 do
+ build_gem "depends_on_myrack", "1.0.1" do |s|
+ s.add_dependency "myrack"
+ end
+ end
+
+ # we need a working myrack gem in repo3
+ update_repo gem_repo3 do
+ build_gem "myrack", "1.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ source "https://gem.repo3" do
+ gem "depends_on_myrack"
+ end
+ G
+ end
+
+ context "and not in any other sources" do
+ before do
+ build_repo(gem_repo2) {}
+ end
+
+ it "installs from the same source without any warning" do
+ bundle :install, artifice: "compact_index"
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3")
+ end
+ end
+
+ context "and in another source" do
+ before do
+ # need this to be broken to check for correct source ordering
+ build_repo gem_repo2 do
+ build_gem "myrack", "1.0.0" do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+ end
+ end
+
+ it "installs from the same source without any warning" do
+ bundle :install, artifice: "compact_index"
+
+ expect(err).not_to include("Warning: the gem 'myrack' was found in multiple sources.")
+ expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3")
+
+ # In https://github.com/bundler/bundler/issues/3585 this failed
+ # when there is already a lockfile, and the gems are missing, so try again
+ system_gems []
+ bundle :install, artifice: "compact_index"
+
+ expect(err).not_to include("Warning: the gem 'myrack' was found in multiple sources.")
+ expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3")
+ end
+ end
+ end
+
+ context "when a pinned gem has an indirect dependency in a different source" do
+ before do
+ # In these tests, we need a working myrack gem in repo2 and not repo3
+
+ build_repo3 do
+ build_gem "depends_on_myrack", "1.0.1" do |s|
+ s.add_dependency "myrack"
+ end
+ end
+
+ build_repo gem_repo2 do
+ build_gem "myrack", "1.0.0"
+ end
+ end
+
+ context "and not in any other sources" do
+ before do
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo2"
+ source "https://gem.repo3" do
+ gem "depends_on_myrack"
+ end
+ G
+ end
+
+ it "installs from the other source without any warning" do
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0")
+ end
+ end
+ end
+
+ context "when a top-level gem can only be found in an scoped source" do
+ before do
+ build_repo2
+
+ build_repo3 do
+ build_gem "private_gem_1", "1.0.0"
+ build_gem "private_gem_2", "1.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "private_gem_1"
+
+ source "https://gem.repo3" do
+ gem "private_gem_2"
+ end
+ G
+ end
+
+ it "fails" do
+ bundle :install, artifice: "compact_index", raise_on_error: false
+ expect(err).to include("Could not find gem 'private_gem_1' in rubygems repository https://gem.repo2/ or installed locally.")
+ end
+ end
+
+ context "when a top-level gem has an indirect dependency" do
+ before do
+ build_repo gem_repo2 do
+ build_gem "depends_on_myrack", "1.0.1" do |s|
+ s.add_dependency "myrack"
+ end
+ end
+
+ build_repo3 do
+ build_gem "unrelated_gem", "1.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "depends_on_myrack"
+
+ source "https://gem.repo3" do
+ gem "unrelated_gem"
+ end
+ G
+ end
+
+ context "and the dependency is only in the top-level source" do
+ before do
+ update_repo gem_repo2 do
+ build_gem "myrack", "1.0.0"
+ end
+ end
+
+ it "installs the dependency from the top-level source without warning" do
+ bundle :install, artifice: "compact_index"
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", "unrelated_gem 1.0.0")
+ expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote2")
+ expect(the_bundle).to include_gems("unrelated_gem 1.0.0", source: "remote3")
+ end
+ end
+
+ context "and the dependency is only in a pinned source" do
+ before do
+ update_repo gem_repo3 do
+ build_gem "myrack", "1.0.0" do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+ end
+ end
+
+ it "does not find the dependency" do
+ bundle :install, artifice: "compact_index", raise_on_error: false
+ expect(err).to end_with <<~E.strip
+ Could not find compatible versions
+
+ Because every version of depends_on_myrack depends on myrack >= 0
+ and myrack >= 0 could not be found in rubygems repository https://gem.repo2/ or installed locally,
+ depends_on_myrack cannot be used.
+ So, because Gemfile depends on depends_on_myrack >= 0,
+ version solving has failed.
+ E
+ end
+ end
+
+ context "and the dependency is in both the top-level and a pinned source" do
+ before do
+ update_repo gem_repo2 do
+ build_gem "myrack", "1.0.0"
+ end
+
+ update_repo gem_repo3 do
+ build_gem "myrack", "1.0.0" do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+ end
+ end
+
+ it "installs the dependency from the top-level source without warning" do
+ bundle :install, artifice: "compact_index"
+ expect(err).not_to include("Warning")
+ expect(run("require 'myrack'; puts MYRACK")).to eq("1.0.0")
+ expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", "unrelated_gem 1.0.0")
+ expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote2")
+ expect(the_bundle).to include_gems("unrelated_gem 1.0.0", source: "remote3")
+ end
+ end
+ end
+
+ context "when a scoped gem has a deeply nested indirect dependency" do
+ before do
+ build_repo3 do
+ build_gem "depends_on_depends_on_myrack", "1.0.1" do |s|
+ s.add_dependency "depends_on_myrack"
+ end
+
+ build_gem "depends_on_myrack", "1.0.1" do |s|
+ s.add_dependency "myrack"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ source "https://gem.repo3" do
+ gem "depends_on_depends_on_myrack"
+ end
+ G
+ end
+
+ context "and the dependency is only in the top-level source" do
+ before do
+ update_repo gem_repo2 do
+ build_gem "myrack", "1.0.0"
+ end
+ end
+
+ it "installs the dependency from the top-level source" do
+ bundle :install, artifice: "compact_index"
+ expect(the_bundle).to include_gems("depends_on_depends_on_myrack 1.0.1", "depends_on_myrack 1.0.1", "myrack 1.0.0")
+ expect(the_bundle).to include_gems("myrack 1.0.0", source: "remote2")
+ expect(the_bundle).to include_gems("depends_on_depends_on_myrack 1.0.1", "depends_on_myrack 1.0.1", source: "remote3")
+ end
+ end
+
+ context "and the dependency is only in a pinned source" do
+ before do
+ build_repo2
+
+ update_repo gem_repo3 do
+ build_gem "myrack", "1.0.0"
+ end
+ end
+
+ it "installs the dependency from the pinned source" do
+ bundle :install, artifice: "compact_index"
+ expect(the_bundle).to include_gems("depends_on_depends_on_myrack 1.0.1", "depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3")
+ end
+ end
+
+ context "and the dependency is in both the top-level and a pinned source" do
+ before do
+ update_repo gem_repo2 do
+ build_gem "myrack", "1.0.0" do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+ end
+
+ update_repo gem_repo3 do
+ build_gem "myrack", "1.0.0"
+ end
+ end
+
+ it "installs the dependency from the pinned source without warning" do
+ bundle :install, artifice: "compact_index"
+ expect(the_bundle).to include_gems("depends_on_depends_on_myrack 1.0.1", "depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3")
+ end
+ end
+ end
+
+ context "when a top-level gem has an indirect dependency present in the default source, but with a different version from the one resolved" do
+ before do
+ build_lib "activesupport", "7.0.0.alpha", path: lib_path("rails/activesupport")
+ build_lib "rails", "7.0.0.alpha", path: lib_path("rails") do |s|
+ s.add_dependency "activesupport", "= 7.0.0.alpha"
+ end
+
+ build_repo gem_repo2 do
+ build_gem "activesupport", "6.1.2"
+
+ build_gem "webpacker", "5.2.1" do |s|
+ s.add_dependency "activesupport", ">= 5.2"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gemspec :path => "#{lib_path("rails")}"
+
+ gem "webpacker", "~> 5.0"
+ G
+ end
+
+ it "installs all gems without warning" do
+ bundle :install, artifice: "compact_index"
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("activesupport 7.0.0.alpha", "rails 7.0.0.alpha")
+ expect(the_bundle).to include_gems("activesupport 7.0.0.alpha", source: "path@#{lib_path("rails/activesupport")}")
+ expect(the_bundle).to include_gems("rails 7.0.0.alpha", source: "path@#{lib_path("rails")}")
+ end
+ end
+
+ context "when a pinned gem has an indirect dependency with more than one level of indirection in the default source " do
+ before do
+ build_repo3 do
+ build_gem "handsoap", "0.2.5.5" do |s|
+ s.add_dependency "nokogiri", ">= 1.2.3"
+ end
+ end
+
+ update_repo gem_repo2 do
+ build_gem "nokogiri", "1.11.1" do |s|
+ s.add_dependency "racca", "~> 1.4"
+ end
+
+ build_gem "racca", "1.5.2"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ source "https://gem.repo3" do
+ gem "handsoap"
+ end
+
+ gem "nokogiri"
+ G
+ end
+
+ it "installs from the default source without any warnings or errors and generates a proper lockfile" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo3, "handsoap", "0.2.5.5"
+ c.checksum gem_repo2, "nokogiri", "1.11.1"
+ c.checksum gem_repo2, "racca", "1.5.2"
+ end
+
+ expected_lockfile = <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ nokogiri (1.11.1)
+ racca (~> 1.4)
+ racca (1.5.2)
+
+ GEM
+ remote: https://gem.repo3/
+ specs:
+ handsoap (0.2.5.5)
+ nokogiri (>= 1.2.3)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ handsoap!
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install --verbose", artifice: "compact_index"
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("handsoap 0.2.5.5", "nokogiri 1.11.1", "racca 1.5.2")
+ expect(the_bundle).to include_gems("handsoap 0.2.5.5", source: "remote3")
+ expect(the_bundle).to include_gems("nokogiri 1.11.1", "racca 1.5.2", source: "remote2")
+ expect(lockfile).to eq(expected_lockfile)
+
+ # Even if the gems are already installed
+ FileUtils.rm bundled_app_lock
+ bundle "install --verbose", artifice: "compact_index"
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("handsoap 0.2.5.5", "nokogiri 1.11.1", "racca 1.5.2")
+ expect(the_bundle).to include_gems("handsoap 0.2.5.5", source: "remote3")
+ expect(the_bundle).to include_gems("nokogiri 1.11.1", "racca 1.5.2", source: "remote2")
+ expect(lockfile).to eq(expected_lockfile)
+ end
+ end
+
+ context "with a gem that is only found in the wrong source" do
+ before do
+ build_repo3 do
+ build_gem "not_in_repo1", "1.0.0"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index", raise_on_error: false
+ source "https://gem.repo3"
+ gem "not_in_repo1", :source => "https://gem.repo1"
+ G
+ end
+
+ it "does not install the gem" do
+ expect(err).to include("Could not find gem 'not_in_repo1'")
+ end
+ end
+
+ context "with an existing lockfile" do
+ before do
+ system_gems "myrack-0.9.1", "myrack-1.0.0", path: default_bundle_path
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1
+ specs:
+
+ GEM
+ remote: https://gem.repo3
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack!
+ L
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ source "https://gem.repo3" do
+ gem 'myrack'
+ end
+ G
+ end
+
+ # Reproduction of https://github.com/rubygems/bundler/issues/3298
+ it "does not unlock the installed gem on exec" do
+ expect(the_bundle).to include_gems("myrack 0.9.1")
+ end
+ end
+
+ context "with a path gem in the same Gemfile" do
+ before do
+ build_lib "foo"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :source => "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo-1.0")}"
+ G
+ end
+
+ it "does not unlock the non-path gem after install" do
+ bundle :install, artifice: "compact_index"
+
+ bundle %(exec ruby -e 'puts "OK"')
+
+ expect(out).to include("OK")
+ end
+ end
+ end
+
+ context "when an older version of the same gem also ships with Ruby" do
+ before do
+ system_gems "myrack-0.9.1"
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo1"
+ gem "myrack" # should come from repo1!
+ G
+ end
+
+ it "installs the gems without any warning" do
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+ end
+
+ context "when a single source contains multiple locked gems" do
+ before do
+ # With these gems,
+ build_repo4 do
+ build_gem "foo", "0.1"
+ build_gem "bar", "0.1"
+ end
+
+ # Installing this gemfile...
+ gemfile <<-G
+ source 'https://gem.repo1'
+ gem 'myrack'
+ gem 'foo', '~> 0.1', :source => 'https://gem.repo4'
+ gem 'bar', '~> 0.1', :source => 'https://gem.repo4'
+ G
+
+ bundle_config "path ../gems/system"
+ bundle :install, artifice: "compact_index"
+
+ # And then we add some new versions...
+ build_repo4 do
+ build_gem "foo", "0.2"
+ build_gem "bar", "0.3"
+ end
+ end
+
+ it "allows them to be unlocked separately" do
+ # And install this gemfile, updating only foo.
+ install_gemfile <<-G, artifice: "compact_index"
+ source 'https://gem.repo1'
+ gem 'myrack'
+ gem 'foo', '~> 0.2', :source => 'https://gem.repo4'
+ gem 'bar', '~> 0.1', :source => 'https://gem.repo4'
+ G
+
+ # It should update foo to 0.2, but not the (locked) bar 0.1
+ expect(the_bundle).to include_gems("foo 0.2", "bar 0.1")
+ end
+ end
+
+ context "re-resolving" do
+ context "when there is a mix of sources in the gemfile" do
+ before do
+ build_repo3 do
+ build_gem "myrack"
+ end
+
+ build_lib "path1"
+ build_lib "path2"
+ build_git "git1"
+ build_git "git2"
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo1"
+ gem "rails"
+
+ source "https://gem.repo3" do
+ gem "myrack"
+ end
+
+ gem "path1", :path => "#{lib_path("path1-1.0")}"
+ gem "path2", :path => "#{lib_path("path2-1.0")}"
+ gem "git1", :git => "#{lib_path("git1-1.0")}"
+ gem "git2", :git => "#{lib_path("git2-1.0")}"
+ G
+ end
+
+ it "does not re-resolve" do
+ bundle :install, artifice: "compact_index", verbose: true
+ expect(out).to include("using resolution from the lockfile")
+ expect(out).not_to include("re-resolving dependencies")
+ end
+ end
+ end
+
+ context "when a gem is installed to system gems" do
+ before do
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ context "and the gemfile changes" do
+ it "is still able to find that gem from remote sources" do
+ build_repo4 do
+ build_gem "myrack", "2.0.1.1.forked"
+ build_gem "thor", "0.19.1.1.forked"
+ end
+
+ # When this gemfile is installed...
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo1"
+
+ source "https://gem.repo4" do
+ gem "myrack", "2.0.1.1.forked"
+ gem "thor"
+ end
+ gem "myrack-obama"
+ G
+
+ # Then we change the Gemfile by adding a version to thor
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ source "https://gem.repo4" do
+ gem "myrack", "2.0.1.1.forked"
+ gem "thor", "0.19.1.1.forked"
+ end
+ gem "myrack-obama"
+ G
+
+ # But we should still be able to find myrack 2.0.1.1.forked and install it
+ bundle :install, artifice: "compact_index"
+ end
+ end
+ end
+
+ describe "source changed to one containing a higher version of a dependency" do
+ before do
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+
+ build_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "bar"
+ end
+
+ build_lib("gemspec_test", path: tmp("gemspec_test")) do |s|
+ s.add_dependency "bar", "=1.0.0"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo2"
+ gem "myrack"
+ gemspec :path => "#{tmp("gemspec_test")}"
+ G
+ end
+
+ it "conservatively installs the existing locked version" do
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+ end
+
+ context "when Gemfile overrides a gemspec development dependency to change the default source" do
+ before do
+ build_repo4 do
+ build_gem "bar"
+ end
+
+ build_lib("gemspec_test", path: tmp("gemspec_test")) do |s|
+ s.add_development_dependency "bar"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo1"
+
+ source "https://gem.repo4" do
+ gem "bar"
+ end
+
+ gemspec :path => "#{tmp("gemspec_test")}"
+ G
+ end
+
+ it "does not print warnings" do
+ expect(err).to be_empty
+ end
+ end
+
+ it "doesn't update version when a gem uses a source block but a higher version from another source is already installed locally" do
+ build_repo2 do
+ build_gem "example", "0.1.0"
+ end
+
+ build_repo4 do
+ build_gem "example", "1.0.2"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo4"
+
+ gem "example", :source => "https://gem.repo2"
+ G
+
+ bundle "info example"
+ expect(out).to include("example (0.1.0)")
+
+ system_gems "example-1.0.2", path: default_bundle_path, gem_repo: gem_repo4
+
+ bundle "update example --verbose", artifice: "compact_index"
+ expect(out).not_to include("Using example 1.0.2")
+ expect(out).to include("Using example 0.1.0")
+ end
+
+ it "fails immediately with a helpful error when a rubygems source does not exist and bundler/setup is required" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ source "https://gem.repo4" do
+ gem "example"
+ end
+ G
+
+ ruby <<~R, raise_on_error: false
+ require 'bundler/setup'
+ R
+
+ expect(last_command).to be_failure
+ expect(err).to include("Could not find gem 'example' in locally installed gems.")
+ end
+
+ it "fails immediately with a helpful error when a non retriable network error happens while resolving sources" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ source "https://gem.repo4" do
+ gem "example"
+ end
+ G
+
+ bundle "install", artifice: nil, raise_on_error: false
+
+ expect(last_command).to be_failure
+ expect(err).to include("Could not reach host gem.repo4. Check your network connection and try again.")
+ end
+
+ context "when an indirect dependency is available from multiple ambiguous sources" do
+ it "raises, suggesting a source block" do
+ build_repo4 do
+ build_gem "depends_on_myrack" do |s|
+ s.add_dependency "myrack"
+ end
+ build_gem "myrack"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index_extra_api", raise_on_error: false
+ source "https://global.source"
+
+ source "https://scoped.source/extra" do
+ gem "depends_on_myrack"
+ end
+
+ source "https://scoped.source" do
+ gem "thin"
+ end
+ G
+ expect(last_command).to be_failure
+ expect(err).to eq <<~EOS.strip
+ The gem 'myrack' was found in multiple relevant sources.
+ * rubygems repository https://scoped.source/
+ * rubygems repository https://scoped.source/extra/
+ You must add this gem to the source block for the source you wish it to be installed from.
+ EOS
+ expect(the_bundle).not_to be_locked
+ end
+ end
+
+ context "when default source includes old gems with nil required_ruby_version" do
+ before do
+ build_repo2 do
+ build_gem "ruport", "1.7.0.3" do |s|
+ s.add_dependency "pdf-writer", "1.1.8"
+ end
+ end
+
+ build_repo gem_repo4 do
+ build_gem "pdf-writer", "1.1.8"
+ end
+
+ path = "#{gem_repo4}/#{Gem::MARSHAL_SPEC_DIR}/pdf-writer-1.1.8.gemspec.rz"
+ spec = Marshal.load(Bundler.rubygems.inflate(File.binread(path)))
+ spec.instance_variable_set(:@required_ruby_version, nil)
+ File.open(path, "wb") do |f|
+ f.write Gem.deflate(Marshal.dump(spec))
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "ruport", "= 1.7.0.3", :source => "https://gem.repo4/extra"
+ G
+ end
+
+ it "handles that fine" do
+ bundle "install", artifice: "compact_index_extra"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "pdf-writer", "1.1.8"
+ c.checksum gem_repo2, "ruport", "1.7.0.3"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ pdf-writer (1.1.8)
+
+ GEM
+ remote: https://gem.repo4/extra/
+ specs:
+ ruport (1.7.0.3)
+ pdf-writer (= 1.1.8)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ruport (= 1.7.0.3)!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when default source includes old gems with nil required_rubygems_version" do
+ before do
+ build_repo2 do
+ build_gem "ruport", "1.7.0.3" do |s|
+ s.add_dependency "pdf-writer", "1.1.8"
+ end
+ end
+
+ build_repo gem_repo4 do
+ build_gem "pdf-writer", "1.1.8"
+ end
+
+ path = "#{gem_repo4}/#{Gem::MARSHAL_SPEC_DIR}/pdf-writer-1.1.8.gemspec.rz"
+ spec = Marshal.load(Bundler.rubygems.inflate(File.binread(path)))
+ spec.instance_variable_set(:@required_rubygems_version, nil)
+ File.open(path, "wb") do |f|
+ f.write Gem.deflate(Marshal.dump(spec))
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "ruport", "= 1.7.0.3", :source => "https://gem.repo4/extra"
+ G
+ end
+
+ it "handles that fine" do
+ bundle "install", artifice: "compact_index_extra"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "pdf-writer", "1.1.8"
+ c.checksum gem_repo2, "ruport", "1.7.0.3"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ pdf-writer (1.1.8)
+
+ GEM
+ remote: https://gem.repo4/extra/
+ specs:
+ ruport (1.7.0.3)
+ pdf-writer (= 1.1.8)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ruport (= 1.7.0.3)!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when default source uses the old API and includes old gems with nil required_rubygems_version" do
+ before do
+ build_repo4 do
+ build_gem "pdf-writer", "1.1.8"
+ end
+
+ path = "#{gem_repo4}/#{Gem::MARSHAL_SPEC_DIR}/pdf-writer-1.1.8.gemspec.rz"
+ spec = Marshal.load(Bundler.rubygems.inflate(File.binread(path)))
+ spec.instance_variable_set(:@required_rubygems_version, nil)
+ File.open(path, "wb") do |f|
+ f.write Gem.deflate(Marshal.dump(spec))
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "pdf-writer", "= 1.1.8"
+ G
+ end
+
+ it "handles that fine" do
+ bundle "install --verbose", artifice: "endpoint"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "pdf-writer", "1.1.8"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ pdf-writer (1.1.8)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ pdf-writer (= 1.1.8)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when mistakenly adding a top level gem already depended on and cached under the wrong source" do
+ before do
+ build_repo4 do
+ build_gem "some_private_gem", "0.1.0" do |s|
+ s.add_dependency "example", "~> 1.0"
+ end
+ end
+
+ build_repo2 do
+ build_gem "example", "1.0.0"
+ end
+
+ install_gemfile <<~G, artifice: "compact_index"
+ source "https://gem.repo2"
+
+ source "https://gem.repo4" do
+ gem "some_private_gem"
+ end
+ G
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ source "https://gem.repo4" do
+ gem "some_private_gem"
+ gem "example" # MISTAKE, example is not available at gem.repo4
+ end
+ G
+ end
+
+ it "shows a proper error message and does not generate a corrupted lockfile" do
+ expect do
+ bundle :install, artifice: "compact_index", raise_on_error: false, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ end.not_to change { lockfile }
+
+ expect(err).to include("Could not find gem 'example' in rubygems repository https://gem.repo4/")
+ end
+ end
+
+ context "when a gem has versions in two sources, but only the locked one has updates" do
+ let(:original_lockfile) do
+ <<~L
+ GEM
+ remote: https://main.source/
+ specs:
+ activesupport (1.0)
+ bigdecimal
+ bigdecimal (1.0.0)
+
+ GEM
+ remote: https://main.source/extra/
+ specs:
+ foo (1.0)
+ bigdecimal
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ activesupport
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ before do
+ build_repo3 do
+ build_gem "activesupport" do |s|
+ s.add_dependency "bigdecimal"
+ end
+
+ build_gem "bigdecimal", "1.0.0"
+ build_gem "bigdecimal", "3.3.1"
+ end
+
+ build_repo4 do
+ build_gem "foo" do |s|
+ s.add_dependency "bigdecimal"
+ end
+
+ build_gem "bigdecimal", "1.0.0"
+ end
+
+ gemfile <<~G
+ source "https://main.source"
+
+ gem "activesupport"
+
+ source "https://main.source/extra" do
+ gem "foo"
+ end
+ G
+
+ lockfile original_lockfile
+ end
+
+ it "properly upgrades the lockfile when updating that specific gem" do
+ bundle "update bigdecimal --conservative", artifice: "compact_index_extra_api", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo3.to_s }
+
+ expect(lockfile).to eq original_lockfile.gsub("bigdecimal (1.0.0)", "bigdecimal (3.3.1)")
+ end
+ end
+
+ context "when switching a gem with components from rubygems to git source" do
+ before do
+ build_repo2 do
+ build_gem "rails", "7.0.0" do |s|
+ s.add_dependency "actionpack", "7.0.0"
+ s.add_dependency "activerecord", "7.0.0"
+ end
+ build_gem "actionpack", "7.0.0"
+ build_gem "activerecord", "7.0.0"
+ # propshaft also depends on actionpack, creating the conflict
+ build_gem "propshaft", "1.0.0" do |s|
+ s.add_dependency "actionpack", ">= 7.0.0"
+ end
+ end
+
+ build_git "rails", "7.0.0", path: lib_path("rails") do |s|
+ s.add_dependency "actionpack", "7.0.0"
+ s.add_dependency "activerecord", "7.0.0"
+ end
+
+ build_git "actionpack", "7.0.0", path: lib_path("rails")
+ build_git "activerecord", "7.0.0", path: lib_path("rails")
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "7.0.0"
+ gem "propshaft"
+ G
+ end
+
+ it "moves component gems to the git source in the lockfile" do
+ expect(lockfile).to include("remote: https://gem.repo2")
+ expect(lockfile).to include("rails (7.0.0)")
+ expect(lockfile).to include("actionpack (7.0.0)")
+ expect(lockfile).to include("activerecord (7.0.0)")
+ expect(lockfile).to include("propshaft (1.0.0)")
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", git: "#{lib_path("rails")}"
+ gem "propshaft"
+ G
+
+ bundle "install"
+
+ expect(lockfile).to include("remote: #{lib_path("rails")}")
+ expect(lockfile).to include("rails (7.0.0)")
+ expect(lockfile).to include("actionpack (7.0.0)")
+ expect(lockfile).to include("activerecord (7.0.0)")
+
+ # Component gems should NOT remain in the GEM section
+ # Extract just the GEM section by splitting on GIT first, then GEM
+ gem_section = lockfile.split("GEM\n").last.split(/\n(PLATFORMS|DEPENDENCIES)/)[0]
+ expect(gem_section).not_to include("actionpack (7.0.0)")
+ expect(gem_section).not_to include("activerecord (7.0.0)")
+ end
+ end
+
+ context "when switching a gem with components from rubygems to path source" do
+ before do
+ build_repo2 do
+ build_gem "rails", "7.0.0" do |s|
+ s.add_dependency "actionpack", "7.0.0"
+ s.add_dependency "activerecord", "7.0.0"
+ end
+ build_gem "actionpack", "7.0.0"
+ build_gem "activerecord", "7.0.0"
+ # propshaft also depends on actionpack, creating the conflict
+ build_gem "propshaft", "1.0.0" do |s|
+ s.add_dependency "actionpack", ">= 7.0.0"
+ end
+ end
+
+ build_lib "rails", "7.0.0", path: lib_path("rails") do |s|
+ s.add_dependency "actionpack", "7.0.0"
+ s.add_dependency "activerecord", "7.0.0"
+ end
+
+ build_lib "actionpack", "7.0.0", path: lib_path("rails")
+ build_lib "activerecord", "7.0.0", path: lib_path("rails")
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "7.0.0"
+ gem "propshaft"
+ G
+ end
+
+ it "moves component gems to the path source in the lockfile" do
+ expect(lockfile).to include("remote: https://gem.repo2")
+ expect(lockfile).to include("rails (7.0.0)")
+ expect(lockfile).to include("actionpack (7.0.0)")
+ expect(lockfile).to include("activerecord (7.0.0)")
+ expect(lockfile).to include("propshaft (1.0.0)")
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", path: "#{lib_path("rails")}"
+ gem "propshaft"
+ G
+
+ bundle "install"
+
+ expect(lockfile).to include("remote: #{lib_path("rails")}")
+ expect(lockfile).to include("rails (7.0.0)")
+ expect(lockfile).to include("actionpack (7.0.0)")
+ expect(lockfile).to include("activerecord (7.0.0)")
+
+ # Component gems should NOT remain in the GEM section
+ # Extract just the GEM section by splitting appropriately
+ gem_section = lockfile.split("GEM\n").last.split(/\n(PLATFORMS|DEPENDENCIES)/)[0]
+ expect(gem_section).not_to include("actionpack (7.0.0)")
+ expect(gem_section).not_to include("activerecord (7.0.0)")
+ end
+ end
+
+ context "when a scoped rubygems source is missing a transitive dependency" do
+ before do
+ build_repo2 do
+ build_gem "fallback_dep", "1.0.0"
+ build_gem "foo", "1.0.0"
+ end
+
+ build_repo3 do
+ build_gem "private_parent", "1.0.0" do |s|
+ s.add_dependency "fallback_dep"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "foo"
+
+ source "https://gem.repo3" do
+ gem "private_parent", "1.0.0"
+ end
+ G
+
+ bundle :install, artifice: "compact_index"
+ end
+
+ it "falls back to the default rubygems source for that dependency" do
+ build_repo2 do
+ build_gem "foo", "2.0.0"
+ end
+
+ system_gems []
+
+ bundle "update foo", artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("private_parent 1.0.0", "fallback_dep 1.0.0", "foo 2.0.0")
+ expect(the_bundle).to include_gems("private_parent 1.0.0", source: "remote3")
+ expect(the_bundle).to include_gems("fallback_dep 1.0.0", source: "remote2")
+ end
+ end
+
+ context "when a path gem has a transitive dependency that does not exist in the path source" do
+ before do
+ build_repo2 do
+ build_gem "missing_dep", "1.0.0"
+ build_gem "foo", "1.0.0"
+ end
+
+ build_lib "parent_gem", "1.0.0", path: lib_path("parent_gem") do |s|
+ s.add_dependency "missing_dep"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "foo"
+
+ gem "parent_gem", path: "#{lib_path("parent_gem")}"
+ G
+
+ bundle :install, artifice: "compact_index"
+ end
+
+ it "falls back to the default rubygems source for that dependency when updating" do
+ build_repo2 do
+ build_gem "foo", "2.0.0"
+ end
+
+ system_gems []
+
+ bundle "update foo", artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("parent_gem 1.0.0", "missing_dep 1.0.0", "foo 2.0.0")
+ expect(the_bundle).to include_gems("parent_gem 1.0.0", source: "path@#{lib_path("parent_gem")}")
+ expect(the_bundle).to include_gems("missing_dep 1.0.0", source: "remote2")
+ end
+ end
+
+ context "when a git gem has a transitive dependency that does not exist in the git source" do
+ before do
+ build_repo2 do
+ build_gem "missing_dep", "1.0.0"
+ build_gem "foo", "1.0.0"
+ end
+
+ build_git "parent_gem", "1.0.0", path: lib_path("parent_gem") do |s|
+ s.add_dependency "missing_dep"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "foo"
+
+ gem "parent_gem", git: "#{lib_path("parent_gem")}"
+ G
+
+ bundle :install, artifice: "compact_index"
+ end
+
+ it "falls back to the default rubygems source for that dependency when updating" do
+ build_repo2 do
+ build_gem "foo", "2.0.0"
+ end
+
+ system_gems []
+
+ bundle "update foo", artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("parent_gem 1.0.0", "missing_dep 1.0.0", "foo 2.0.0")
+ expect(the_bundle).to include_gems("parent_gem 1.0.0", source: "git@#{lib_path("parent_gem")}")
+ expect(the_bundle).to include_gems("missing_dep 1.0.0", source: "remote2")
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/specific_platform_spec.rb b/spec/bundler/install/gemfile/specific_platform_spec.rb
new file mode 100644
index 0000000000..97b1d233bf
--- /dev/null
+++ b/spec/bundler/install/gemfile/specific_platform_spec.rb
@@ -0,0 +1,1973 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with specific platforms" do
+ let(:google_protobuf) { <<-G }
+ source "https://gem.repo2"
+ gem "google-protobuf"
+ G
+
+ it "locks to the specific darwin platform" do
+ simulate_platform "x86_64-darwin-15" do
+ setup_multiplatform_gem
+ install_gemfile(google_protobuf)
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ expect(the_bundle.locked_platforms).to include("universal-darwin")
+ expect(the_bundle).to include_gem("google-protobuf 3.0.0.alpha.5.0.5.1 universal-darwin")
+ expect(the_bundle.locked_gems.specs.map(&:full_name)).to include(
+ "google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin"
+ )
+ end
+ end
+
+ it "still installs the platform specific variant when locked only to ruby, and the platform specific variant has different dependencies" do
+ simulate_platform "x86_64-darwin-15" do
+ build_repo4 do
+ build_gem("sass-embedded", "1.72.0") do |s|
+ s.add_dependency "rake"
+ end
+
+ build_gem("sass-embedded", "1.72.0") do |s|
+ s.platform = "x86_64-darwin-15"
+ end
+
+ build_gem "rake"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sass-embedded"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ rake (1.0)
+ sass-embedded (1.72.0)
+ rake
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ sass-embedded
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install --verbose"
+ expect(err).to include("The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version")
+ expect(out).to include("Installing sass-embedded 1.72.0 (x86_64-darwin-15)")
+
+ expect(the_bundle).to include_gem("sass-embedded 1.72.0 x86_64-darwin-15")
+ end
+ end
+
+ it "understands that a non-platform specific gem in a old lockfile doesn't necessarily mean installing the non-specific variant" do
+ simulate_platform "x86_64-darwin-15" do
+ setup_multiplatform_gem
+
+ # Consistent location to install and look for gems
+ bundle_config "path vendor/bundle"
+
+ install_gemfile(google_protobuf)
+
+ # simulate lockfile created with old bundler, which only locks for ruby platform
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ google-protobuf (3.0.0.alpha.5.0.5.1)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ google-protobuf
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ # force strict usage of the lockfile by setting frozen mode
+ bundle_config "frozen true"
+
+ # make sure the platform that got actually installed with the old bundler is used
+ expect(the_bundle).to include_gem("google-protobuf 3.0.0.alpha.5.0.5.1 universal-darwin")
+ end
+ end
+
+ it "understands that a non-platform specific gem in a new lockfile locked only to ruby doesn't necessarily mean installing the non-specific variant" do
+ simulate_platform "x86_64-darwin-15" do
+ setup_multiplatform_gem
+
+ # Consistent location to install and look for gems
+ bundle_config "path vendor/bundle"
+
+ gemfile google_protobuf
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "google-protobuf", "3.0.0.alpha.4.0"
+ end
+
+ # simulate lockfile created with old bundler, which only locks for ruby platform
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ google-protobuf (3.0.0.alpha.4.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ google-protobuf
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "update"
+ expect(err).to include("The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version")
+
+ checksums.checksum gem_repo2, "google-protobuf", "3.0.0.alpha.5.0.5.1"
+
+ # make sure the platform that the platform specific dependency is used, since we're only locked to ruby
+ expect(the_bundle).to include_gem("google-protobuf 3.0.0.alpha.5.0.5.1 universal-darwin")
+
+ # make sure we're still only locked to ruby
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ google-protobuf (3.0.0.alpha.5.0.5.1)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ google-protobuf
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when running on a legacy lockfile locked only to ruby" do
+ # Exercises the legacy lockfile path (use_exact_resolved_specifications? = false)
+ # because most_specific_locked_platform is ruby, matching the generic platform.
+ # Key insight: when target (arm64-darwin-22) != platform (ruby), the code tries
+ # both platforms before falling back, preserving lockfile integrity.
+
+ around do |example|
+ build_repo4 do
+ build_gem "nokogiri", "1.3.10"
+ build_gem "nokogiri", "1.3.10" do |s|
+ s.platform = "arm64-darwin"
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.3.10)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "arm64-darwin-22", &example
+ end
+
+ it "still installs the generic ruby variant if necessary" do
+ bundle "install"
+ expect(the_bundle).to include_gem("nokogiri 1.3.10")
+ expect(the_bundle).not_to include_gem("nokogiri 1.3.10 arm64-darwin")
+ end
+
+ it "still installs the generic ruby variant if necessary, even in frozen mode" do
+ bundle "install", env: { "BUNDLE_FROZEN" => "true" }
+ expect(the_bundle).to include_gem("nokogiri 1.3.10")
+ expect(the_bundle).not_to include_gem("nokogiri 1.3.10 arm64-darwin")
+ end
+ end
+
+ context "when platform-specific gem has incompatible required_ruby_version" do
+ # Key insight: candidate_platforms tries [target, platform, ruby] in order.
+ # Ruby platform is last since it requires compilation, but works when
+ # precompiled gems are incompatible with the current Ruby version.
+ #
+ # Note: This fix requires the lockfile to include both ruby and platform-
+ # specific variants (typical after `bundle lock --add-platform`). If the
+ # lockfile only has platform-specific gems, frozen mode cannot help because
+ # Bundler.setup would still expect the locked (incompatible) gem.
+
+ # Exercises the exact spec path (use_exact_resolved_specifications? = true)
+ # because lockfile has platform-specific entry as most_specific_locked_platform
+ it "falls back to ruby platform in frozen mode when lockfile includes both variants" do
+ build_repo4 do
+ build_gem "nokogiri", "1.18.10"
+ build_gem "nokogiri", "1.18.10" do |s|
+ s.platform = "x86_64-linux"
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ # Lockfile has both ruby and platform-specific gem (typical after `bundle lock --add-platform`)
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.18.10)
+ nokogiri (1.18.10-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "install", env: { "BUNDLE_FROZEN" => "true" }
+ expect(the_bundle).to include_gem("nokogiri 1.18.10")
+ expect(the_bundle).not_to include_gem("nokogiri 1.18.10 x86_64-linux")
+ end
+ end
+ end
+
+ it "doesn't discard previously installed platform specific gem and fall back to ruby on subsequent bundles" do
+ simulate_platform "x86_64-darwin-15" do
+ build_repo2 do
+ build_gem("libv8", "8.4.255.0")
+ build_gem("libv8", "8.4.255.0") {|s| s.platform = "universal-darwin" }
+
+ build_gem("mini_racer", "1.0.0") do |s|
+ s.add_dependency "libv8"
+ end
+ end
+
+ # Consistent location to install and look for gems
+ bundle_config "path vendor/bundle"
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "libv8"
+ G
+
+ # simulate lockfile created with old bundler, which only locks for ruby platform
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ libv8 (8.4.255.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ libv8
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install --verbose"
+ expect(err).to include("The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version")
+ expect(out).to include("Installing libv8 8.4.255.0 (universal-darwin)")
+
+ bundle "add mini_racer --verbose"
+ expect(out).to include("Using libv8 8.4.255.0 (universal-darwin)")
+ end
+ end
+
+ it "chooses platform specific gems even when resolving upon materialization and the API returns more specific platforms first" do
+ simulate_platform "x86_64-darwin-15" do
+ build_repo4 do
+ build_gem("grpc", "1.50.0")
+ build_gem("grpc", "1.50.0") {|s| s.platform = "universal-darwin" }
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "grpc"
+ G
+
+ # simulate lockfile created with old bundler, which only locks for ruby platform
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ grpc (1.50.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ grpc
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install --verbose", artifice: "compact_index_precompiled_before"
+ expect(err).to include("The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version")
+ expect(out).to include("Installing grpc 1.50.0 (universal-darwin)")
+ end
+ end
+
+ it "caches the universal-darwin gem when --all-platforms is passed and properly picks it up on further bundler invocations" do
+ simulate_platform "x86_64-darwin-15" do
+ setup_multiplatform_gem
+ gemfile(google_protobuf)
+ bundle "cache --all-platforms"
+ expect(cached_gem("google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin")).to exist
+
+ bundle "install --verbose"
+ expect(err).to be_empty
+ end
+ end
+
+ it "caches the universal-darwin gem when cache_all_platforms is configured and properly picks it up on further bundler invocations" do
+ simulate_platform "x86_64-darwin-15" do
+ setup_multiplatform_gem
+ gemfile(google_protobuf)
+ bundle_config "cache_all_platforms true"
+ bundle "cache"
+ expect(cached_gem("google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin")).to exist
+
+ bundle "install --verbose"
+ expect(err).to be_empty
+ end
+ end
+
+ it "caches multiplatform git gems with a single gemspec when --all-platforms is passed" do
+ git = build_git "pg_array_parser", "1.0"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "pg_array_parser", :git => "#{lib_path("pg_array_parser-1.0")}"
+ G
+
+ lockfile <<-L
+ GIT
+ remote: #{lib_path("pg_array_parser-1.0")}
+ revision: #{git.ref_for("main")}
+ specs:
+ pg_array_parser (1.0-java)
+ pg_array_parser (1.0)
+
+ GEM
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms("java")}
+
+ DEPENDENCIES
+ pg_array_parser!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "cache --all-platforms"
+
+ expect(err).to be_empty
+ end
+
+ it "uses the platform-specific gem with extra dependencies" do
+ simulate_platform "x86_64-darwin-15" do
+ setup_multiplatform_gem_with_different_dependencies_per_platform
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "facter"
+ G
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+
+ expect(the_bundle.locked_platforms).to include("universal-darwin")
+ expect(the_bundle).to include_gems("facter 2.4.6 universal-darwin", "CFPropertyList 1.0")
+ expect(the_bundle.locked_gems.specs.map(&:full_name)).to include("CFPropertyList-1.0",
+ "facter-2.4.6-universal-darwin")
+ end
+ end
+
+ context "when adding a platform via lock --add_platform" do
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ it "adds the foreign platform" do
+ simulate_platform "x86_64-darwin-15" do
+ setup_multiplatform_gem
+ install_gemfile(google_protobuf)
+ bundle "lock --add-platform=x64-mingw-ucrt"
+
+ expect(the_bundle.locked_platforms).to include("x64-mingw-ucrt", "universal-darwin")
+ expect(the_bundle.locked_gems.specs.map(&:full_name)).to include(*%w[
+ google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin
+ google-protobuf-3.0.0.alpha.5.0.5.1-x64-mingw-ucrt
+ ])
+ end
+ end
+
+ it "falls back on plain ruby when that version doesn't have a platform-specific gem" do
+ simulate_platform "x86_64-darwin-15" do
+ setup_multiplatform_gem
+ install_gemfile(google_protobuf)
+ bundle "lock --add-platform=java"
+
+ expect(the_bundle.locked_platforms).to include("java", "universal-darwin")
+ expect(the_bundle.locked_gems.specs.map(&:full_name)).to include(
+ "google-protobuf-3.0.0.alpha.5.0.5.1",
+ "google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin"
+ )
+ end
+ end
+ end
+
+ it "installs sorbet-static, which does not provide a pure ruby variant, in absence of a lockfile, just fine", :truffleruby do
+ skip "does not apply to Windows" if Gem.win_platform?
+
+ build_repo2 do
+ build_gem("sorbet-static", "0.5.6403") {|s| s.platform = Bundler.local_platform }
+ end
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ gem "sorbet-static", "0.5.6403"
+ G
+
+ bundle "install --verbose"
+ end
+
+ it "installs sorbet-static, which does not provide a pure ruby variant, in presence of a lockfile, just fine", :truffleruby do
+ skip "does not apply to Windows" if Gem.win_platform?
+
+ build_repo2 do
+ build_gem("sorbet-static", "0.5.6403") {|s| s.platform = Bundler.local_platform }
+ end
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ gem "sorbet-static", "0.5.6403"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ sorbet-static (0.5.6403-#{Bundler.local_platform})
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ sorbet-static (= 0.5.6403)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install --verbose"
+ end
+
+ it "does not resolve if the current platform does not match any of available platform specific variants for a top level dependency" do
+ build_repo4 do
+ build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "x86_64-linux" }
+ build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "universal-darwin-20" }
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet-static", "0.5.6433"
+ G
+
+ error_message = <<~ERROR.strip
+ Could not find gem 'sorbet-static (= 0.5.6433)' with platform 'arm64-darwin-21' in rubygems repository https://gem.repo4/ or installed locally.
+
+ The source contains the following gems matching 'sorbet-static (= 0.5.6433)':
+ * sorbet-static-0.5.6433-universal-darwin-20
+ * sorbet-static-0.5.6433-x86_64-linux
+ ERROR
+
+ simulate_platform "arm64-darwin-21" do
+ bundle "lock", raise_on_error: false
+ end
+
+ expect(err).to include(error_message).once
+
+ # Make sure it doesn't print error twice in verbose mode
+
+ simulate_platform "arm64-darwin-21" do
+ bundle "lock --verbose", raise_on_error: false
+ end
+
+ expect(err).to include(error_message).once
+ end
+
+ it "shows a platform mismatch hint when the current platform is not in the lockfile's platforms" do
+ build_repo4 do
+ build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "x86_64-linux-musl" }
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet-static", "0.5.6433"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet-static (0.5.6433-x86_64-linux-musl)
+
+ PLATFORMS
+ x86_64-linux-musl
+
+ DEPENDENCIES
+ sorbet-static (= 0.5.6433)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "install", raise_on_error: false
+ end
+
+ expect(err).to include("Your current platform (x86_64-linux) is not included in the lockfile's platforms (x86_64-linux-musl)")
+ expect(err).to include("bundle lock --add-platform x86_64-linux")
+ end
+
+ it "does not resolve if the current platform does not match any of available platform specific variants for a transitive dependency" do
+ build_repo4 do
+ build_gem("sorbet", "0.5.6433") {|s| s.add_dependency "sorbet-static", "= 0.5.6433" }
+ build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "x86_64-linux" }
+ build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "universal-darwin-20" }
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet", "0.5.6433"
+ G
+
+ error_message = <<~ERROR.strip
+ Could not find compatible versions
+
+ Because every version of sorbet depends on sorbet-static = 0.5.6433
+ and sorbet-static = 0.5.6433 could not be found in rubygems repository https://gem.repo4/ or installed locally for any resolution platforms (arm64-darwin-21),
+ sorbet cannot be used.
+ So, because Gemfile depends on sorbet = 0.5.6433,
+ version solving has failed.
+
+ The source contains the following gems matching 'sorbet-static (= 0.5.6433)':
+ * sorbet-static-0.5.6433-universal-darwin-20
+ * sorbet-static-0.5.6433-x86_64-linux
+ ERROR
+
+ simulate_platform "arm64-darwin-21" do
+ bundle "lock", raise_on_error: false
+ end
+
+ expect(err).to include(error_message).once
+
+ # Make sure it doesn't print error twice in verbose mode
+
+ simulate_platform "arm64-darwin-21" do
+ bundle "lock --verbose", raise_on_error: false
+ end
+
+ expect(err).to include(error_message).once
+ end
+
+ it "does not generate a lockfile if ruby platform is forced and some gem has no ruby variant available" do
+ build_repo4 do
+ build_gem("sorbet-static", "0.5.9889") {|s| s.platform = Gem::Platform.local }
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet-static", "0.5.9889"
+ G
+
+ bundle "lock", raise_on_error: false, env: { "BUNDLE_FORCE_RUBY_PLATFORM" => "true" }
+
+ expect(err).to include <<~ERROR.rstrip
+ Could not find gem 'sorbet-static (= 0.5.9889)' with platform 'ruby' in rubygems repository https://gem.repo4/ or installed locally.
+
+ The source contains the following gems matching 'sorbet-static (= 0.5.9889)':
+ * sorbet-static-0.5.9889-#{Gem::Platform.local}
+ ERROR
+ end
+
+ it "automatically fixes the lockfile if ruby platform is locked and some gem has no ruby variant available" do
+ build_repo4 do
+ build_gem("sorbet-static-and-runtime", "0.5.10160") do |s|
+ s.add_dependency "sorbet", "= 0.5.10160"
+ s.add_dependency "sorbet-runtime", "= 0.5.10160"
+ end
+
+ build_gem("sorbet", "0.5.10160") do |s|
+ s.add_dependency "sorbet-static", "= 0.5.10160"
+ end
+
+ build_gem("sorbet-runtime", "0.5.10160")
+
+ build_gem("sorbet-static", "0.5.10160") do |s|
+ s.platform = Gem::Platform.local
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet-static-and-runtime"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet (0.5.10160)
+ sorbet-static (= 0.5.10160)
+ sorbet-runtime (0.5.10160)
+ sorbet-static (0.5.10160-#{Gem::Platform.local})
+ sorbet-static-and-runtime (0.5.10160)
+ sorbet (= 0.5.10160)
+ sorbet-runtime (= 0.5.10160)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ sorbet-static-and-runtime
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "update"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "sorbet", "0.5.10160"
+ c.checksum gem_repo4, "sorbet-runtime", "0.5.10160"
+ c.checksum gem_repo4, "sorbet-static", "0.5.10160", Gem::Platform.local
+ c.checksum gem_repo4, "sorbet-static-and-runtime", "0.5.10160"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet (0.5.10160)
+ sorbet-static (= 0.5.10160)
+ sorbet-runtime (0.5.10160)
+ sorbet-static (0.5.10160-#{Gem::Platform.local})
+ sorbet-static-and-runtime (0.5.10160)
+ sorbet (= 0.5.10160)
+ sorbet-runtime (= 0.5.10160)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ sorbet-static-and-runtime
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically fixes the lockfile if both ruby platform and a more specific platform are locked, and some gem has no ruby variant available" do
+ build_repo4 do
+ build_gem "nokogiri", "1.12.0"
+ build_gem "nokogiri", "1.12.0" do |s|
+ s.platform = "x86_64-darwin"
+ end
+
+ build_gem "nokogiri", "1.13.0"
+ build_gem "nokogiri", "1.13.0" do |s|
+ s.platform = "x86_64-darwin"
+ end
+
+ build_gem("sorbet-static", "0.5.10601") do |s|
+ s.platform = "x86_64-darwin"
+ end
+ end
+
+ simulate_platform "x86_64-darwin-22" do
+ install_gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ gem "sorbet-static"
+ G
+ end
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.13.0", "x86_64-darwin"
+ c.checksum gem_repo4, "sorbet-static", "0.5.10601", "x86_64-darwin"
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.12.0)
+ nokogiri (1.12.0-x86_64-darwin)
+ sorbet-static (0.5.10601-x86_64-darwin)
+
+ PLATFORMS
+ ruby
+ x86_64-darwin
+
+ DEPENDENCIES
+ nokogiri
+ sorbet-static
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-darwin-22" do
+ bundle "update --conservative nokogiri"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.13.0-x86_64-darwin)
+ sorbet-static (0.5.10601-x86_64-darwin)
+
+ PLATFORMS
+ x86_64-darwin
+
+ DEPENDENCIES
+ nokogiri
+ sorbet-static
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically fixes the lockfile if only ruby platform is locked and some gem has no ruby variant available" do
+ build_repo4 do
+ build_gem("sorbet-static-and-runtime", "0.5.10160") do |s|
+ s.add_dependency "sorbet", "= 0.5.10160"
+ s.add_dependency "sorbet-runtime", "= 0.5.10160"
+ end
+
+ build_gem("sorbet", "0.5.10160") do |s|
+ s.add_dependency "sorbet-static", "= 0.5.10160"
+ end
+
+ build_gem("sorbet-runtime", "0.5.10160")
+
+ build_gem("sorbet-static", "0.5.10160") do |s|
+ s.platform = Gem::Platform.local
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet-static-and-runtime"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet (0.5.10160)
+ sorbet-static (= 0.5.10160)
+ sorbet-runtime (0.5.10160)
+ sorbet-static (0.5.10160-#{Gem::Platform.local})
+ sorbet-static-and-runtime (0.5.10160)
+ sorbet (= 0.5.10160)
+ sorbet-runtime (= 0.5.10160)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ sorbet-static-and-runtime
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "update"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "sorbet", "0.5.10160"
+ c.checksum gem_repo4, "sorbet-runtime", "0.5.10160"
+ c.checksum gem_repo4, "sorbet-static", "0.5.10160", Gem::Platform.local
+ c.checksum gem_repo4, "sorbet-static-and-runtime", "0.5.10160"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet (0.5.10160)
+ sorbet-static (= 0.5.10160)
+ sorbet-runtime (0.5.10160)
+ sorbet-static (0.5.10160-#{Gem::Platform.local})
+ sorbet-static-and-runtime (0.5.10160)
+ sorbet (= 0.5.10160)
+ sorbet-runtime (= 0.5.10160)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ sorbet-static-and-runtime
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically fixes the lockfile when adding a gem that introduces dependencies with no ruby platform variants transitively" do
+ simulate_platform "x86_64-linux" do
+ build_repo4 do
+ build_gem "nokogiri", "1.18.2"
+
+ build_gem "nokogiri", "1.18.2" do |s|
+ s.platform = "x86_64-linux"
+ end
+
+ build_gem("sorbet", "0.5.11835") do |s|
+ s.add_dependency "sorbet-static", "= 0.5.11835"
+ end
+
+ build_gem "sorbet-static", "0.5.11835" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ gem "sorbet"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.18.2)
+ nokogiri (1.18.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.18.2", "x86_64-linux"
+ c.checksum gem_repo4, "sorbet", "0.5.11835"
+ c.checksum gem_repo4, "sorbet-static", "0.5.11835", "x86_64-linux"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.18.2)
+ nokogiri (1.18.2-x86_64-linux)
+ sorbet (0.5.11835)
+ sorbet-static (= 0.5.11835)
+ sorbet-static (0.5.11835-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ sorbet
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "automatically fixes the lockfile if multiple platforms locked, but no valid versions of direct dependencies for all of them" do
+ simulate_platform "x86_64-linux" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "arm-linux"
+ end
+
+ build_gem "sorbet-static", "0.5.10696" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ gem "sorbet-static"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.0-arm-linux)
+ nokogiri (1.14.0-x86_64-linux)
+ sorbet-static (0.5.10696-x86_64-linux)
+
+ PLATFORMS
+ aarch64-linux
+ arm-linux
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ sorbet-static
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "update"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.14.0", "x86_64-linux"
+ c.checksum gem_repo4, "sorbet-static", "0.5.10696", "x86_64-linux"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.0-x86_64-linux)
+ sorbet-static (0.5.10696-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ sorbet-static
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "automatically fixes the lockfile without removing other variants if it's missing platform gems, but they are installed locally" do
+ simulate_platform "x86_64-darwin-21" do
+ build_repo4 do
+ build_gem("sorbet-static", "0.5.10549") do |s|
+ s.platform = "universal-darwin-20"
+ end
+
+ build_gem("sorbet-static", "0.5.10549") do |s|
+ s.platform = "universal-darwin-21"
+ end
+ end
+
+ # Make sure sorbet-static-0.5.10549-universal-darwin-21 is installed
+ install_gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet-static", "= 0.5.10549"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "sorbet-static", "0.5.10549", "universal-darwin-20"
+ c.checksum gem_repo4, "sorbet-static", "0.5.10549", "universal-darwin-21"
+ end
+
+ # Make sure the lockfile is missing sorbet-static-0.5.10549-universal-darwin-21
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet-static (0.5.10549-universal-darwin-20)
+
+ PLATFORMS
+ x86_64-darwin
+
+ DEPENDENCIES
+ sorbet-static (= 0.5.10549)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet-static (0.5.10549-universal-darwin-20)
+ sorbet-static (0.5.10549-universal-darwin-21)
+
+ PLATFORMS
+ x86_64-darwin
+
+ DEPENDENCIES
+ sorbet-static (= 0.5.10549)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "automatically fixes the lockfile if locked only to ruby, and some locked specs don't meet locked dependencies" do
+ simulate_platform "x86_64-linux" do
+ build_repo4 do
+ build_gem("ibandit", "0.7.0") do |s|
+ s.add_dependency "i18n", "~> 0.7.0"
+ end
+
+ build_gem("i18n", "0.7.0.beta1")
+ build_gem("i18n", "0.7.0")
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "ibandit", "~> 0.7.0"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ i18n (0.7.0.beta1)
+ ibandit (0.7.0)
+ i18n (~> 0.7.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ ibandit (~> 0.7.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --update i18n"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ i18n (0.7.0)
+ ibandit (0.7.0)
+ i18n (~> 0.7.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ ibandit (~> 0.7.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "does not remove ruby if gems for other platforms, and not present in the lockfile, exist in the Gemfile" do
+ build_repo4 do
+ build_gem "nokogiri", "1.13.8"
+ build_gem "nokogiri", "1.13.8" do |s|
+ s.platform = Gem::Platform.local
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+
+ gem "tzinfo", "~> 1.2", platform: :#{not_local_tag}
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.13.8"
+ c.checksum gem_repo4, "nokogiri", "1.13.8", Gem::Platform.local
+ end
+
+ original_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.13.8)
+ nokogiri (1.13.8-#{Gem::Platform.local})
+
+ PLATFORMS
+ #{lockfile_platforms("ruby")}
+
+ DEPENDENCIES
+ nokogiri
+ tzinfo (~> 1.2)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile original_lockfile
+
+ bundle "lock --update"
+
+ expect(lockfile).to eq(original_lockfile)
+ end
+
+ it "does not remove ruby if gems for other platforms, and not present in the lockfile, exist in the Gemfile, and the lockfile only has ruby" do
+ build_repo4 do
+ build_gem "nokogiri", "1.13.8"
+ build_gem "nokogiri", "1.13.8" do |s|
+ s.platform = "arm64-darwin"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+
+ gem "tzinfo", "~> 1.2", platforms: %i[windows jruby]
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.13.8"
+ end
+
+ original_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.13.8)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ nokogiri
+ tzinfo (~> 1.2)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile original_lockfile
+
+ simulate_platform "arm64-darwin-23" do
+ bundle "lock --update"
+ end
+
+ expect(lockfile).to eq(original_lockfile)
+ end
+
+ it "does not remove ruby when adding a new gem to the Gemfile" do
+ build_repo4 do
+ build_gem "concurrent-ruby", "1.2.2"
+ build_gem "myrack", "3.0.7"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "concurrent-ruby"
+ gem "myrack"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "concurrent-ruby", "1.2.2"
+ c.checksum gem_repo4, "myrack", "3.0.7"
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ concurrent-ruby (1.2.2)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ concurrent-ruby
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ concurrent-ruby (1.2.2)
+ myrack (3.0.7)
+
+ PLATFORMS
+ #{lockfile_platforms(generic_default_locked_platform || local_platform, defaults: ["ruby"])}
+
+ DEPENDENCIES
+ concurrent-ruby
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "can fallback to a source gem when platform gems are incompatible with current ruby version" do
+ setup_multiplatform_gem_with_source_gem
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ gem "my-precompiled-gem"
+ G
+
+ # simulate lockfile which includes both a precompiled gem with:
+ # - Gem the current platform (with incompatible ruby version)
+ # - A source gem with compatible ruby version
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ my-precompiled-gem (3.0.0)
+ my-precompiled-gem (3.0.0-#{Bundler.local_platform})
+
+ PLATFORMS
+ ruby
+ #{Bundler.local_platform}
+
+ DEPENDENCIES
+ my-precompiled-gem
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle :install
+ end
+
+ it "automatically adds the ruby variant to the lockfile if the specific platform is locked and we move to a newer ruby version for which a native package is not available" do
+ #
+ # Given an existing application using native gems (e.g., nokogiri)
+ # And a lockfile generated with a stable ruby version
+ # When want test the application against ruby-head and `bundle install`
+ # Then bundler should fall back to the generic ruby platform gem
+ #
+ simulate_platform "x86_64-linux" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.0"
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "x86_64-linux"
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri", "1.14.0"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.14.0", "x86_64-linux"
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.0-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri (= 1.14.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle :install
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.14.0"
+ c.checksum gem_repo4, "nokogiri", "1.14.0", "x86_64-linux"
+ end
+
+ expect(lockfile).to eq(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.0)
+ nokogiri (1.14.0-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri (= 1.14.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "automatically fixes the lockfile when only ruby platform locked, and adding a dependency with subdependencies not valid for ruby" do
+ simulate_platform "x86_64-linux" do
+ build_repo4 do
+ build_gem("sorbet", "0.5.10160") do |s|
+ s.add_dependency "sorbet-static", "= 0.5.10160"
+ end
+
+ build_gem("sorbet-static", "0.5.10160") do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet (0.5.10160)
+ sorbet-static (= 0.5.10160)
+ sorbet-static (0.5.10160-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ sorbet
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "locks specific platforms automatically" do
+ simulate_platform "x86_64-linux" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.0"
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "arm-linux"
+ end
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "x64-mingw-ucrt"
+ end
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "java"
+ end
+
+ build_gem "sorbet-static", "0.5.10696" do |s|
+ s.platform = "x86_64-linux"
+ end
+ build_gem "sorbet-static", "0.5.10696" do |s|
+ s.platform = "universal-darwin-22"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ bundle "lock"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.14.0"
+ c.checksum gem_repo4, "nokogiri", "1.14.0", "arm-linux"
+ c.checksum gem_repo4, "nokogiri", "1.14.0", "x86_64-linux"
+ end
+
+ # locks all compatible platforms, excluding Java and Windows
+ expect(lockfile).to eq(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.0)
+ nokogiri (1.14.0-arm-linux)
+ nokogiri (1.14.0-x86_64-linux)
+
+ PLATFORMS
+ arm-linux
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ gem "sorbet-static"
+ G
+
+ FileUtils.rm bundled_app_lock
+
+ bundle "lock"
+
+ checksums.delete "nokogiri", "arm-linux"
+ checksums.checksum gem_repo4, "sorbet-static", "0.5.10696", "universal-darwin-22"
+ checksums.checksum gem_repo4, "sorbet-static", "0.5.10696", "x86_64-linux"
+
+ # locks only platforms compatible with all gems in the bundle
+ expect(lockfile).to eq(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.0)
+ nokogiri (1.14.0-x86_64-linux)
+ sorbet-static (0.5.10696-universal-darwin-22)
+ sorbet-static (0.5.10696-x86_64-linux)
+
+ PLATFORMS
+ universal-darwin-22
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ sorbet-static
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "does not fail when a platform variant is incompatible with the current ruby and another equivalent platform specific variant is part of the resolution" do
+ build_repo4 do
+ build_gem "nokogiri", "1.15.5"
+
+ build_gem "nokogiri", "1.15.5" do |s|
+ s.platform = "x86_64-linux"
+ s.required_ruby_version = "< #{current_ruby_minor}.dev"
+ end
+
+ build_gem "sass-embedded", "1.69.5"
+
+ build_gem "sass-embedded", "1.69.5" do |s|
+ s.platform = "x86_64-linux-gnu"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ gem "sass-embedded"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.15.5"
+ c.checksum gem_repo4, "sass-embedded", "1.69.5"
+ c.checksum gem_repo4, "sass-embedded", "1.69.5", "x86_64-linux-gnu"
+ end
+
+ simulate_platform "x86_64-linux" do
+ bundle "install --verbose"
+
+ # locks all compatible platforms, excluding Java and Windows
+ expect(lockfile).to eq(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.15.5)
+ sass-embedded (1.69.5)
+ sass-embedded (1.69.5-x86_64-linux-gnu)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ sass-embedded
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "does not add ruby platform gem if it brings extra dependencies not resolved originally" do
+ build_repo4 do
+ build_gem "nokogiri", "1.15.5" do |s|
+ s.add_dependency "mini_portile2", "~> 2.8.2"
+ end
+
+ build_gem "nokogiri", "1.15.5" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.15.5", "x86_64-linux"
+ end
+
+ simulate_platform "x86_64-linux" do
+ bundle "install --verbose"
+
+ expect(lockfile).to eq(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.15.5-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ ["x86_64-linux", "x86_64-linux-musl"].each do |host_platform|
+ describe "on host platform #{host_platform}" do
+ it "adds current musl platform" do
+ build_repo4 do
+ build_gem "rcee_precompiled", "0.5.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+
+ build_gem "rcee_precompiled", "0.5.0" do |s|
+ s.platform = "x86_64-linux-musl"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "rcee_precompiled", "0.5.0"
+ G
+
+ simulate_platform host_platform do
+ bundle "lock"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "x86_64-linux"
+ c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "x86_64-linux-musl"
+ end
+
+ expect(lockfile).to eq(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ rcee_precompiled (0.5.0-x86_64-linux)
+ rcee_precompiled (0.5.0-x86_64-linux-musl)
+
+ PLATFORMS
+ x86_64-linux
+ x86_64-linux-musl
+
+ DEPENDENCIES
+ rcee_precompiled (= 0.5.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+ end
+ end
+
+ it "adds current musl platform, when there are also gnu variants" do
+ build_repo4 do
+ build_gem "rcee_precompiled", "0.5.0" do |s|
+ s.platform = "x86_64-linux-gnu"
+ end
+
+ build_gem "rcee_precompiled", "0.5.0" do |s|
+ s.platform = "x86_64-linux-musl"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "rcee_precompiled", "0.5.0"
+ G
+
+ simulate_platform "x86_64-linux-musl" do
+ bundle "lock"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "x86_64-linux-gnu"
+ c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "x86_64-linux-musl"
+ end
+
+ expect(lockfile).to eq(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ rcee_precompiled (0.5.0-x86_64-linux-gnu)
+ rcee_precompiled (0.5.0-x86_64-linux-musl)
+
+ PLATFORMS
+ x86_64-linux-gnu
+ x86_64-linux-musl
+
+ DEPENDENCIES
+ rcee_precompiled (= 0.5.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "does not add current platform if there's an equivalent less specific platform among the ones resolved" do
+ build_repo4 do
+ build_gem "rcee_precompiled", "0.5.0" do |s|
+ s.platform = "universal-darwin"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "rcee_precompiled", "0.5.0"
+ G
+
+ simulate_platform "x86_64-darwin-15" do
+ bundle "lock"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "universal-darwin"
+ end
+
+ expect(lockfile).to eq(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ rcee_precompiled (0.5.0-universal-darwin)
+
+ PLATFORMS
+ universal-darwin
+
+ DEPENDENCIES
+ rcee_precompiled (= 0.5.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "does not re-resolve when a specific platform, but less specific than the current platform, is locked" do
+ build_repo4 do
+ build_gem "nokogiri"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.0)
+
+ PLATFORMS
+ arm64-darwin
+
+ DEPENDENCIES
+ nokogiri!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "arm64-darwin-23" do
+ bundle "install --verbose"
+
+ expect(out).to include("Found no changes, using resolution from the lockfile")
+ end
+ end
+
+ it "does not remove generic platform gems locked for a specific platform from lockfile when unlocking an unrelated gem" do
+ build_repo4 do
+ build_gem "ffi"
+
+ build_gem "ffi" do |s|
+ s.platform = "x86_64-linux"
+ end
+
+ build_gem "nokogiri"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "ffi"
+ gem "nokogiri"
+ G
+
+ original_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ ffi (1.0)
+ nokogiri (1.0)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ ffi
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile original_lockfile
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --update nokogiri"
+
+ expect(lockfile).to eq(original_lockfile)
+ end
+ end
+
+ it "does not remove generic platform gems locked for a specific platform from lockfile when unlocking an unrelated gem, and variants for other platform also locked" do
+ build_repo4 do
+ build_gem "ffi"
+
+ build_gem "ffi" do |s|
+ s.platform = "x86_64-linux"
+ end
+
+ build_gem "ffi" do |s|
+ s.platform = "java"
+ end
+
+ build_gem "nokogiri"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "ffi"
+ gem "nokogiri"
+ G
+
+ original_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ ffi (1.0)
+ ffi (1.0-java)
+ nokogiri (1.0)
+
+ PLATFORMS
+ java
+ x86_64-linux
+
+ DEPENDENCIES
+ ffi
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile original_lockfile
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --update nokogiri"
+
+ expect(lockfile).to eq(original_lockfile)
+ end
+ end
+
+ it "does not remove platform specific gems from lockfile when using a ruby version that does not match their ruby requirements, since they may be useful in other rubies" do
+ build_repo4 do
+ build_gem("google-protobuf", "3.25.5")
+ build_gem("google-protobuf", "3.25.5") do |s|
+ s.required_ruby_version = "< #{current_ruby_minor}.dev"
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "google-protobuf", "~> 3.0"
+ G
+
+ original_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ google-protobuf (3.25.5)
+ google-protobuf (3.25.5-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ google-protobuf (~> 3.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile original_lockfile
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --update"
+ end
+
+ expect(lockfile).to eq(original_lockfile)
+ end
+
+ private
+
+ def setup_multiplatform_gem
+ build_repo2 do
+ build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1")
+ build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x86_64-linux" }
+ build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x64-mingw-ucrt" }
+ build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "universal-darwin" }
+
+ build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x86_64-linux" }
+ build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x64-mingw-ucrt" }
+ build_gem("google-protobuf", "3.0.0.alpha.5.0.5")
+
+ build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "universal-darwin" }
+
+ build_gem("google-protobuf", "3.0.0.alpha.4.0")
+ build_gem("google-protobuf", "3.0.0.alpha.3.1.pre")
+ end
+ end
+
+ def setup_multiplatform_gem_with_different_dependencies_per_platform
+ build_repo2 do
+ build_gem("facter", "2.4.6")
+ build_gem("facter", "2.4.6") do |s|
+ s.platform = "universal-darwin"
+ s.add_dependency "CFPropertyList"
+ end
+ build_gem("CFPropertyList")
+ end
+ end
+
+ def setup_multiplatform_gem_with_source_gem
+ build_repo2 do
+ build_gem("my-precompiled-gem", "3.0.0")
+ build_gem("my-precompiled-gem", "3.0.0") do |s|
+ s.platform = Bundler.local_platform
+
+ # purposely unresolvable
+ s.required_ruby_version = ">= 1000.0.0"
+ end
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile_spec.rb b/spec/bundler/install/gemfile_spec.rb
new file mode 100644
index 0000000000..83875a3d0e
--- /dev/null
+++ b/spec/bundler/install/gemfile_spec.rb
@@ -0,0 +1,192 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ context "with duplicated gems" do
+ it "will display a warning" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+
+ gem 'rails', '~> 4.0.0'
+ gem 'rails', '~> 4.0.0'
+ G
+ expect(err).to include("more than once")
+ end
+ end
+
+ context "with --gemfile" do
+ it "finds the gemfile" do
+ gemfile bundled_app("NotGemfile"), <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle :install, gemfile: bundled_app("NotGemfile")
+
+ # Specify BUNDLE_GEMFILE for `the_bundle`
+ # to retrieve the proper Gemfile
+ ENV["BUNDLE_GEMFILE"] = "NotGemfile"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "respects lockfile and BUNDLE_LOCKFILE" do
+ gemfile bundled_app("NotGemfile"), <<-G
+ lockfile "ReallyNotGemfile.lock"
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle :install, gemfile: bundled_app("NotGemfile")
+
+ ENV["BUNDLE_GEMFILE"] = "NotGemfile"
+ ENV["BUNDLE_LOCKFILE"] = "ReallyNotGemfile.lock"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "respects BUNDLE_LOCKFILE during bundle install" do
+ ENV["BUNDLE_LOCKFILE"] = "ReallyNotGemfile.lock"
+
+ gemfile bundled_app("NotGemfile"), <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle :install, gemfile: bundled_app("NotGemfile")
+ expect(bundled_app("ReallyNotGemfile.lock")).to exist
+
+ ENV["BUNDLE_GEMFILE"] = "NotGemfile"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ context "with gemfile set via config" do
+ before do
+ gemfile bundled_app("NotGemfile"), <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle_config "gemfile #{bundled_app("NotGemfile")}"
+ end
+ it "uses the gemfile to install" do
+ bundle "install"
+ bundle "list"
+
+ expect(out).to include("myrack (1.0.0)")
+ end
+ it "uses the gemfile while in a subdirectory" do
+ bundled_app("subdir").mkpath
+ bundle "install", dir: bundled_app("subdir")
+ bundle "list", dir: bundled_app("subdir")
+
+ expect(out).to include("myrack (1.0.0)")
+ end
+ end
+
+ it "reports that lib is an invalid option" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", :lib => "myrack"
+ G
+
+ bundle :install, raise_on_error: false
+ expect(err).to match(/You passed :lib as an option for gem 'myrack', but it is invalid/)
+ end
+
+ it "reports that type is an invalid option" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", :type => "development"
+ G
+
+ bundle :install, raise_on_error: false
+ expect(err).to match(/You passed :type as an option for gem 'myrack', but it is invalid/)
+ end
+
+ it "reports that gemfile is an invalid option" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", :gemfile => "foo"
+ G
+
+ bundle :install, raise_on_error: false
+ expect(err).to match(/You passed :gemfile as an option for gem 'myrack', but it is invalid/)
+ end
+
+ context "when an internal error happens" do
+ let(:bundler_bug) do
+ create_file("bundler_bug.rb", <<~RUBY)
+ require "bundler"
+
+ module Bundler
+ class Dsl
+ def source(source, *args, &blk)
+ nil.name
+ end
+ end
+ end
+ RUBY
+
+ bundled_app("bundler_bug.rb").to_s
+ end
+
+ it "shows culprit file and line" do
+ skip "ruby-core test setup has always \"lib\" in $LOAD_PATH so `require \"bundler\"` always activates the local version rather than using RubyGems gem activation stuff, causing conflicts" if ruby_core?
+
+ install_gemfile "source 'https://gem.repo1'", requires: [bundler_bug], artifice: nil, raise_on_error: false
+ expect(err).to include("bundler_bug.rb:6")
+ end
+ end
+
+ context "with engine specified in symbol", :jruby_only do
+ it "does not raise any error parsing Gemfile" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby "#{RUBY_VERSION}", :engine => :jruby, :engine_version => "#{RUBY_ENGINE_VERSION}"
+ G
+
+ expect(out).to match(/Bundle complete!/)
+ end
+
+ it "installation succeeds" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby "#{RUBY_VERSION}", :engine => :jruby, :engine_version => "#{RUBY_ENGINE_VERSION}"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ context "with a Gemfile containing non-US-ASCII characters" do
+ it "reads the Gemfile with the UTF-8 encoding by default" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ str = "Il était une fois ..."
+ puts "The source encoding is: " + str.encoding.name
+ G
+
+ expect(out).to include("The source encoding is: UTF-8")
+ expect(out).not_to include("The source encoding is: ASCII-8BIT")
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "respects the magic encoding comment" do
+ # NOTE: This works thanks to #eval interpreting the magic encoding comment
+ install_gemfile <<-G
+ # encoding: iso-8859-1
+ source "https://gem.repo1"
+
+ str = "Il #{"\xE9".dup.force_encoding("binary")}tait une fois ..."
+ puts "The source encoding is: " + str.encoding.name
+ G
+
+ expect(out).to include("The source encoding is: ISO-8859-1")
+ expect(out).to include("Bundle complete!")
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/compact_index_spec.rb b/spec/bundler/install/gems/compact_index_spec.rb
new file mode 100644
index 0000000000..9db73b84b5
--- /dev/null
+++ b/spec/bundler/install/gems/compact_index_spec.rb
@@ -0,0 +1,1012 @@
+# frozen_string_literal: true
+
+RSpec.describe "compact index api" do
+ let(:source_hostname) { "localgemserver.test" }
+ let(:source_uri) { "http://#{source_hostname}" }
+
+ it "should use the API" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "has a debug mode" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index", env: { "DEBUG_COMPACT_INDEX" => "true" }
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(err).to include("[Bundler::CompactIndexClient] available?")
+ expect(err).to include("[Bundler::CompactIndexClient] fetching versions")
+ expect(err).to include("[Bundler::CompactIndexClient] info(myrack)")
+ expect(err).to include("[Bundler::CompactIndexClient] fetching info/myrack")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "should URI encode gem names" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem " sinatra"
+ G
+
+ bundle :install, artifice: "compact_index", raise_on_error: false
+ expect(err).to include("' sinatra' is not a valid gem name because it contains whitespace.")
+ end
+
+ it "should handle nested dependencies" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "rails"
+ G
+
+ bundle :install, artifice: "compact_index"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems(
+ "rails 2.3.2",
+ "actionpack 2.3.2",
+ "activerecord 2.3.2",
+ "actionmailer 2.3.2",
+ "activeresource 2.3.2",
+ "activesupport 2.3.2"
+ )
+ end
+
+ it "should handle case sensitivity conflicts" do
+ build_repo4(build_compact_index: false) do
+ build_gem "myrack", "1.0" do |s|
+ s.add_dependency("Myrack", "0.1")
+ end
+ build_gem "Myrack", "0.1"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ source "#{source_uri}"
+ gem "myrack", "1.0"
+ gem "Myrack", "0.1"
+ G
+
+ # can't use `include_gems` here since the `require` will conflict on a
+ # case-insensitive FS
+ run "Bundler.require; puts Gem.loaded_specs.values_at('myrack', 'Myrack').map(&:full_name)"
+ expect(out).to eq("myrack-1.0\nMyrack-0.1")
+ end
+
+ it "should handle multiple gem dependencies on the same gem" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "net-sftp"
+ G
+
+ bundle :install, artifice: "compact_index"
+ expect(the_bundle).to include_gems "net-sftp 1.1.1"
+ end
+
+ it "should use the endpoint when using deployment mode" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+ bundle :install, artifice: "compact_index"
+
+ bundle_config "deployment true"
+ bundle :install, artifice: "compact_index"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "handles git dependencies that are in rubygems" do
+ build_git "foo" do |s|
+ s.executables = "foobar"
+ s.add_dependency "rails", "2.3.2"
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ git "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ G
+
+ bundle :install, artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("rails 2.3.2")
+ end
+
+ it "handles git dependencies that are in rubygems using deployment mode" do
+ build_git "foo" do |s|
+ s.executables = "foobar"
+ s.add_dependency "rails", "2.3.2"
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle :install, artifice: "compact_index"
+
+ bundle_config "deployment true"
+ bundle :install, artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("rails 2.3.2")
+ end
+
+ it "doesn't fail if you only have a git gem with no deps when using deployment mode" do
+ build_git "foo"
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle "install", artifice: "compact_index"
+ bundle_config "deployment true"
+ bundle :install, artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ it "falls back when the API URL returns 403 Forbidden" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, verbose: true, artifice: "compact_index_forbidden"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "falls back when the versions endpoint has a checksum mismatch" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, verbose: true, artifice: "compact_index_checksum_mismatch"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(out).to include("The checksum of /versions does not match the checksum provided by the server!")
+ expect(out).to include("Calculated checksums #{{ "sha-256" => "8KfZiM/fszVkqhP/m5s9lvE6M9xKu4I1bU4Izddp5Ms=" }.inspect} did not match expected #{{ "sha-256" => "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=" }.inspect}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "shows proper path when permission errors happen", :permissions do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ versions = compact_index_cache_path.join(
+ "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions"
+ )
+ versions.dirname.mkpath
+ versions.write("created_at")
+ FileUtils.chmod("-r", versions)
+
+ bundle :install, artifice: "compact_index", raise_on_error: false
+
+ expect(err).to include(
+ "There was an error while trying to read from `#{versions}`. It is likely that you need to grant read permissions for that path."
+ )
+ end
+
+ it "falls back when the user's home directory does not exist or is not writable" do
+ ENV["HOME"] = tmp("missing_home").to_s
+
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "handles host redirects" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index_host_redirect"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "handles host redirects without Gem::Net::HTTP::Persistent" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ FileUtils.mkdir_p lib_path
+ File.open(lib_path("disable_net_http_persistent.rb"), "w") do |h|
+ h.write <<-H
+ module Kernel
+ alias require_without_disabled_net_http require
+ def require(*args)
+ raise LoadError, 'simulated' if args.first == 'openssl' && !caller.grep(/vendored_persistent/).empty?
+ require_without_disabled_net_http(*args)
+ end
+ end
+ H
+ end
+
+ bundle :install, artifice: "compact_index_host_redirect", requires: [lib_path("disable_net_http_persistent.rb")]
+ expect(out).to_not match(/Too many redirects/)
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "times out when Bundler::Fetcher redirects too much" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index_redirects", raise_on_error: false
+ expect(err).to match(/Too many redirects/)
+ end
+
+ context "when --full-index is specified" do
+ it "should use the modern index for install" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle "install --full-index", artifice: "compact_index"
+ expect(out).to include("Fetching source index from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "should use the modern index for update" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle "update --full-index", artifice: "compact_index", all: true
+ expect(out).to include("Fetching source index from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ it "does not double check for gems that are only installed locally" do
+ build_repo2 do
+ build_gem "net_a" do |s|
+ s.add_dependency "net_b"
+ s.add_dependency "net_build_extensions"
+ end
+
+ build_gem "net_b"
+
+ build_gem "net_build_extensions" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ File.open("\#{path}/net_build_extensions.rb", "w") do |f|
+ f.puts "NET_BUILD_EXTENSIONS = 'YES'"
+ end
+ end
+ RUBY
+ end
+ end
+
+ system_gems %w[myrack-1.0.0 thin-1.0 net_a-1.0], gem_repo: gem_repo2
+ bundle_config "path.system true"
+ ENV["BUNDLER_SPEC_ALL_REQUESTS"] = <<~EOS.strip
+ #{source_uri}/versions
+ #{source_uri}/info/myrack
+ EOS
+
+ install_gemfile <<-G, artifice: "compact_index", verbose: true, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ expect(stdboth).not_to include "Double checking"
+ end
+
+ it "fetches again when more dependencies are found in subsequent sources" do
+ build_repo2 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")]
+ end
+
+ install_gemfile <<-G, artifice: "compact_index_extra", verbose: true
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ expect(the_bundle).to include_gems "back_deps 1.0", "foo 1.0"
+ end
+
+ it "fetches gem versions even when those gems are already installed" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack", "1.0.0"
+ G
+ bundle :install, artifice: "compact_index_extra_api"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+
+ build_repo4 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ gemfile <<-G
+ source "#{source_uri}" do; end
+ source "#{source_uri}/extra"
+ gem "myrack", "1.2"
+ G
+ bundle :install, artifice: "compact_index_extra_api"
+ expect(the_bundle).to include_gems "myrack 1.2"
+ end
+
+ it "resolves indirect dependencies to the most scoped source that includes them" do
+ # In this scenario, the gem "somegem" only exists in repo4. It depends on
+ # specific version of activesupport that exists only in repo1. There
+ # happens also be a version of activesupport in repo4, but not the one that
+ # version 1.0.0 of somegem wants. This test makes sure that bundler tries to
+ # use the version in the most scoped source, even if not compatible, and
+ # gives a resolution error
+ build_repo4 do
+ build_gem "activesupport", "1.2.0"
+ build_gem "somegem", "1.0.0" do |s|
+ s.add_dependency "activesupport", "1.2.3" # This version exists only in repo1
+ end
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem 'somegem', '1.0.0'
+ end
+ G
+
+ bundle :install, artifice: "compact_index_extra_api", raise_on_error: false
+
+ expect(err).to include("Could not find compatible versions")
+ end
+
+ it "prints API output properly with back deps" do
+ build_repo2 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")]
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ bundle :install, artifice: "compact_index_extra"
+
+ expect(out).to include("Fetching gem metadata from http://localgemserver.test/")
+ expect(out).to include("Fetching source index from http://localgemserver.test/extra")
+ end
+
+ it "does not fetch every spec when doing back deps" do
+ build_repo2 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ build_gem "missing"
+
+ FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")]
+ end
+
+ install_gemfile <<-G, artifice: "compact_index_extra_missing"
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ expect(the_bundle).to include_gems "back_deps 1.0"
+ end
+
+ it "does not fetch every spec when doing back deps & everything is the compact index" do
+ build_repo4 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ build_gem "missing"
+
+ FileUtils.rm_r Dir[gem_repo4("gems/foo-*.gem")]
+ end
+
+ install_gemfile <<-G, artifice: "compact_index_extra_api_missing"
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ expect(the_bundle).to include_gem "back_deps 1.0"
+ end
+
+ it "uses the endpoint if all sources support it" do
+ gemfile <<-G
+ source "#{source_uri}"
+
+ gem 'foo'
+ G
+
+ bundle :install, artifice: "compact_index_api_missing"
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "fetches again when more dependencies are found in subsequent sources using deployment mode" do
+ build_repo2 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")]
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ bundle :install, artifice: "compact_index_extra"
+ bundle_config "deployment true"
+ bundle :install, artifice: "compact_index_extra"
+ expect(the_bundle).to include_gems "back_deps 1.0"
+ end
+
+ it "does not refetch if the only unmet dependency is bundler" do
+ build_repo2 do
+ build_gem "bundler_dep" do |s|
+ s.add_dependency "bundler"
+ end
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+
+ gem "bundler_dep"
+ G
+
+ bundle :install, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ end
+
+ it "prints post_install_messages" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack-obama'
+ G
+
+ bundle :install, artifice: "compact_index"
+ expect(out).to include("Post-install message from myrack:")
+ end
+
+ it "should display the post install message for a dependency" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack_middleware'
+ G
+
+ bundle :install, artifice: "compact_index"
+ expect(out).to include("Post-install message from myrack:")
+ expect(out).to include("Myrack's post install message")
+ end
+
+ context "when using basic authentication" do
+ let(:user) { "user" }
+ let(:password) { "pass" }
+ let(:basic_auth_source_uri) do
+ uri = Gem::URI.parse(source_uri)
+ uri.user = user
+ uri.password = password
+
+ uri
+ end
+
+ it "passes basic authentication details and strips out creds" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index_basic_authentication"
+ expect(out).not_to include("#{user}:#{password}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "passes basic authentication details and strips out creds also in verbose mode" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, verbose: true, artifice: "compact_index_basic_authentication"
+ expect(out).not_to include("#{user}:#{password}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "does not pass the user / password to different hosts on redirect" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index_creds_diff_host"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ describe "with authentication details in bundle config" do
+ before do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+ end
+
+ it "reads authentication details by host name from bundle config" do
+ bundle "config set #{source_hostname} #{user}:#{password}"
+
+ bundle :install, artifice: "compact_index_strict_basic_authentication"
+
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "reads authentication details by full url from bundle config" do
+ # The trailing slash is necessary here; Fetcher canonicalizes the URI.
+ bundle "config set #{source_uri}/ #{user}:#{password}"
+
+ bundle :install, artifice: "compact_index_strict_basic_authentication"
+
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "should use the API" do
+ bundle "config set #{source_hostname} #{user}:#{password}"
+ bundle :install, artifice: "compact_index_strict_basic_authentication"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "prefers auth supplied in the source uri" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle "config set #{source_hostname} otheruser:wrong"
+
+ bundle :install, artifice: "compact_index_strict_basic_authentication"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "shows instructions if auth is not provided for the source" do
+ bundle :install, artifice: "compact_index_strict_basic_authentication", raise_on_error: false
+ expect(err).to include("bundle config set --global #{source_hostname} username:password")
+ end
+
+ it "fails if authentication has already been provided, but failed" do
+ bundle "config set #{source_hostname} #{user}:wrong"
+
+ bundle :install, artifice: "compact_index_strict_basic_authentication", raise_on_error: false
+ expect(err).to include("Bad username or password")
+ end
+
+ it "does not fallback to old dependency API if bad authentication is provided" do
+ bundle "config set #{source_hostname} #{user}:wrong"
+
+ bundle :install, artifice: "compact_index_strict_basic_authentication", raise_on_error: false, verbose: true
+ expect(err).to include("Bad username or password")
+ expect(out).to include("HTTP 401 Unauthorized http://user@localgemserver.test/versions")
+ expect(out).not_to include("HTTP 401 Unauthorized http://user@localgemserver.test/api/v1/dependencies")
+ end
+ end
+
+ describe "with no password" do
+ let(:password) { nil }
+
+ it "passes basic authentication details" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index_basic_authentication"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+ end
+
+ context "when ruby is compiled without openssl" do
+ before do
+ # Install a monkeypatch that reproduces the effects of openssl being
+ # missing when the fetcher runs, as happens in real life. The reason
+ # we can't just overwrite openssl.rb is that Artifice uses it.
+ bundled_app("broken_ssl").mkpath
+ bundled_app("broken_ssl/openssl.rb").open("w") do |f|
+ f.write <<-RUBY
+ raise LoadError, "cannot load such file -- openssl"
+ RUBY
+ end
+ end
+
+ it "explains what to do to get it, and includes original error" do
+ gemfile <<-G
+ source "#{source_uri.gsub(/http/, "https")}"
+ gem "myrack"
+ G
+
+ bundle :install, env: { "RUBYOPT" => "-I#{bundled_app("broken_ssl")}" }, raise_on_error: false, artifice: nil
+ expect(err).to include("recompile Ruby").and include("cannot load such file")
+ end
+ end
+
+ context "when SSL certificate verification fails" do
+ it "explains what happened" do
+ # Install a monkeypatch that reproduces the effects of openssl raising
+ # a certificate validation error when RubyGems tries to connect.
+ gemfile <<-G
+ class Gem::Net::HTTP
+ def start
+ raise OpenSSL::SSL::SSLError, "certificate verify failed"
+ end
+ end
+
+ source "#{source_uri.gsub(/http/, "https")}"
+ gem "myrack"
+ G
+
+ bundle :install, raise_on_error: false
+ expect(err).to match(/could not verify the SSL certificate/i)
+ end
+ end
+
+ context ".gemrc with sources is present" do
+ it "uses other sources declared in the Gemfile" do
+ File.open(home(".gemrc"), "w") do |file|
+ file.puts({ sources: ["https://rubygems.org"] }.to_yaml)
+ end
+
+ begin
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack'
+ G
+
+ bundle :install, artifice: "compact_index_forbidden"
+ ensure
+ FileUtils.rm_rf home(".gemrc")
+ end
+ end
+ end
+
+ it "performs update with etag not-modified" do
+ versions_etag = compact_index_cache_path.join(
+ "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions.etag"
+ )
+ expect(versions_etag.file?).to eq(false)
+
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack', '0.9.1'
+ G
+
+ # Initial install creates the cached versions file and etag file
+ bundle :install, artifice: "compact_index"
+
+ expect(versions_etag.file?).to eq(true)
+ previous_content = versions_etag.binread
+
+ # Update the Gemfile so we can check subsequent install was successful
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack', '1.0.0'
+ G
+
+ # Second install should match etag
+ bundle :install, artifice: "compact_index_etag_match"
+
+ expect(versions_etag.binread).to eq(previous_content)
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "performs full update when range is ignored" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack', '0.9.1'
+ G
+
+ # Initial install creates the cached versions file and etag file
+ bundle :install, artifice: "compact_index"
+
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack', '1.0.0'
+ G
+
+ versions = compact_index_cache_path.join(
+ "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions"
+ )
+ # Modify the cached file. The ranged request will be based on this but,
+ # in this test, the range is ignored so this gets overwritten, allowing install.
+ versions.write "ruining this file"
+
+ bundle :install, artifice: "compact_index_range_ignored"
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "performs partial update with a non-empty range" do
+ build_repo4 do
+ build_gem "myrack", "0.9.1"
+ end
+
+ # Initial install creates the cached versions file
+ install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ source "#{source_uri}"
+ gem 'myrack', '0.9.1'
+ G
+
+ build_repo4 do
+ build_gem "myrack", "1.0.0"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index_partial_update", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ source "#{source_uri}"
+ gem 'myrack', '1.0.0'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "performs partial update while local cache is updated by another process" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack'
+ G
+
+ # Create a partial cache versions file
+ versions = compact_index_cache_path.join(
+ "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions"
+ )
+ versions.dirname.mkpath
+ versions.write("created_at")
+
+ bundle :install, artifice: "compact_index_concurrent_download"
+
+ expect(versions.read).to start_with("created_at")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "performs a partial update that fails digest check, then a full update" do
+ build_repo4 do
+ build_gem "myrack", "0.9.1"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ source "#{source_uri}"
+ gem 'myrack', '0.9.1'
+ G
+
+ build_repo4 do
+ build_gem "myrack", "1.0.0"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index_partial_update_bad_digest", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ source "#{source_uri}"
+ gem 'myrack', '1.0.0'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "performs full update if server endpoints serve partial content responses but don't have incremental content and provide no digest" do
+ build_repo4 do
+ build_gem "myrack", "0.9.1"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ source "#{source_uri}"
+ gem 'myrack', '0.9.1'
+ G
+
+ build_repo4 do
+ build_gem "myrack", "1.0.0"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index_partial_update_no_digest_not_incremental", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ source "#{source_uri}"
+ gem 'myrack', '1.0.0'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "performs full update of compact index info cache if range is not satisfiable" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack', '0.9.1'
+ G
+
+ bundle :install, artifice: "compact_index"
+
+ cache_path = compact_index_cache_path.join("localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5")
+
+ # We must remove the etag so that we don't ignore the range and get a 304 Not Modified.
+ myrack_info_etag_path = File.join(cache_path, "info-etags", "myrack-92f3313ce5721296f14445c3a6b9c073")
+ File.unlink(myrack_info_etag_path) if File.exist?(myrack_info_etag_path)
+
+ myrack_info_path = File.join(cache_path, "info", "myrack")
+ expected_myrack_info_content = File.read(myrack_info_path)
+
+ # Modify the cache files to make the range not satisfiable
+ File.open(myrack_info_path, "a") {|f| f << "0.9.2 |checksum:c55b525b421fd833a93171ad3d7f04528ca8e87d99ac273f8933038942a5888c" }
+
+ # Update the Gemfile so the next install does its normal things
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack', '1.0.0'
+ G
+
+ # The cache files now being longer means the requested range is going to be not satisfiable
+ # Bundler must end up requesting the whole file to fix things up.
+ bundle :install, artifice: "compact_index_range_not_satisfiable"
+
+ resulting_myrack_info_content = File.read(myrack_info_path)
+
+ expect(resulting_myrack_info_content).to eq(expected_myrack_info_content)
+ end
+
+ it "fails gracefully when the source URI has an invalid scheme" do
+ install_gemfile <<-G, raise_on_error: false
+ source "htps://rubygems.org"
+ gem "myrack"
+ G
+ expect(exitstatus).to eq(15)
+ expect(err).to end_with(<<-E.strip)
+ The request uri `htps://index.rubygems.org/versions` has an invalid scheme (`htps`). Did you mean `http` or `https`?
+ E
+ end
+
+ describe "checksum validation" do
+ before do
+ lockfile <<-L
+ GEM
+ remote: #{source_uri}
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ #{checksums_section}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "handles checksums from the server in base64" do
+ api_checksum = checksum_digest(gem_repo1, "myrack", "1.0.0")
+ myrack_checksum = [[api_checksum].pack("H*")].pack("m0")
+ install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_MYRACK_CHECKSUM" => myrack_checksum }
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "raises when the checksum does not match" do
+ install_gemfile <<-G, artifice: "compact_index_wrong_gem_checksum", raise_on_error: false
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ gem_path = default_cache_path.dirname.join("myrack-1.0.0.gem")
+
+ expect(exitstatus).to eq(37)
+ expect(err).to eq <<~E.strip
+ Bundler found mismatched checksums. This is a potential security risk.
+ myrack (1.0.0) sha256=2222222222222222222222222222222222222222222222222222222222222222
+ from the API at http://localgemserver.test/
+ #{checksum_to_lock(gem_repo1, "myrack", "1.0.0")}
+ from the gem at #{gem_path}
+
+ If you trust the API at http://localgemserver.test/, to resolve this issue you can:
+ 1. remove the gem at #{gem_path}
+ 2. run `bundle install`
+
+ To ignore checksum security warnings, disable checksum validation with
+ `bundle config set --local disable_checksum_validation true`
+ E
+ end
+
+ it "raises when the checksum is the wrong length" do
+ install_gemfile <<-G, artifice: "compact_index_wrong_gem_checksum", env: { "BUNDLER_SPEC_MYRACK_CHECKSUM" => "checksum!", "DEBUG" => "1" }, verbose: true, raise_on_error: false
+ source "#{source_uri}"
+ gem "myrack"
+ G
+ expect(exitstatus).to eq(14)
+ expect(err).to include('Invalid checksum for myrack-0.9.1: "checksum!" is not a valid SHA256 hex or base64 digest')
+ end
+
+ it "does not raise when disable_checksum_validation is set" do
+ bundle_config "disable_checksum_validation true"
+ install_gemfile <<-G, artifice: "compact_index_wrong_gem_checksum"
+ source "#{source_uri}"
+ gem "myrack"
+ G
+ end
+ end
+
+ it "works when cache dir is world-writable" do
+ install_gemfile <<-G, artifice: "compact_index"
+ File.umask(0000)
+ source "#{source_uri}"
+ gem "myrack"
+ G
+ end
+
+ it "doesn't explode when the API dependencies are wrong" do
+ install_gemfile <<-G, artifice: "compact_index_wrong_dependencies", env: { "DEBUG" => "true" }, raise_on_error: false
+ source "#{source_uri}"
+ gem "rails"
+ G
+ deps = [Gem::Dependency.new("rake", "= #{rake_version}"),
+ Gem::Dependency.new("actionpack", "= 2.3.2"),
+ Gem::Dependency.new("activerecord", "= 2.3.2"),
+ Gem::Dependency.new("actionmailer", "= 2.3.2"),
+ Gem::Dependency.new("activeresource", "= 2.3.2")]
+ expect(out).to include("rails-2.3.2 from rubygems remote at #{source_uri}/ has corrupted API dependencies")
+ expect(err).to include(<<-E.strip)
+Bundler::APIResponseMismatchError: Downloading rails-2.3.2 revealed dependencies not in the API (#{deps.map(&:to_s).join(", ")}).
+Running `bundle update rails` should fix the problem.
+ E
+ end
+
+ it "does not duplicate specs in the lockfile when updating and a dependency is not installed" do
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo1"
+ source "#{source_uri}" do
+ gem "rails"
+ gem "activemerchant"
+ end
+ G
+ uninstall_gem("activemerchant")
+ bundle "update rails", artifice: "compact_index"
+ count = lockfile.match?("CHECKSUMS") ? 2 : 1 # Once in the specs, and once in CHECKSUMS
+ expect(lockfile.scan(/activemerchant \(/).size).to eq(count)
+ end
+
+ it "handles an API that does not provide checksums info (undocumented, support may get removed)" do
+ install_gemfile <<-G, artifice: "compact_index_no_checksums"
+ source "https://gem.repo1"
+ gem "rake"
+ G
+ end
+end
diff --git a/spec/bundler/install/gems/dependency_api_fallback_spec.rb b/spec/bundler/install/gems/dependency_api_fallback_spec.rb
new file mode 100644
index 0000000000..c7b0c537e4
--- /dev/null
+++ b/spec/bundler/install/gems/dependency_api_fallback_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+RSpec.describe "gemcutter's dependency API" do
+ context "when Gemcutter API takes too long to respond" do
+ before do
+ bundle_config "timeout 1"
+ end
+
+ it "times out and falls back on the modern index" do
+ install_gemfile <<-G, artifice: "endpoint_timeout"
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(out).to include("Fetching source index from https://gem.repo1/")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/dependency_api_spec.rb b/spec/bundler/install/gems/dependency_api_spec.rb
new file mode 100644
index 0000000000..32a1b98b6d
--- /dev/null
+++ b/spec/bundler/install/gems/dependency_api_spec.rb
@@ -0,0 +1,656 @@
+# frozen_string_literal: true
+
+RSpec.describe "gemcutter's dependency API" do
+ let(:source_hostname) { "localgemserver.test" }
+ let(:source_uri) { "http://#{source_hostname}" }
+
+ it "should use the API" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "endpoint"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "should URI encode gem names" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem " sinatra"
+ G
+
+ bundle :install, artifice: "endpoint", raise_on_error: false
+ expect(err).to include("' sinatra' is not a valid gem name because it contains whitespace.")
+ end
+
+ it "should handle nested dependencies" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "rails"
+ G
+
+ bundle :install, artifice: "endpoint"
+ expect(out).to include("Fetching gem metadata from #{source_uri}/...")
+ expect(the_bundle).to include_gems(
+ "rails 2.3.2",
+ "actionpack 2.3.2",
+ "activerecord 2.3.2",
+ "actionmailer 2.3.2",
+ "activeresource 2.3.2",
+ "activesupport 2.3.2"
+ )
+ end
+
+ it "should handle multiple gem dependencies on the same gem" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "net-sftp"
+ G
+
+ bundle :install, artifice: "endpoint"
+ expect(the_bundle).to include_gems "net-sftp 1.1.1"
+ end
+
+ it "should use the endpoint when using deployment mode" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+ bundle :install, artifice: "endpoint"
+
+ bundle_config "deployment true"
+ bundle :install, artifice: "endpoint"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "handles git dependencies that are in rubygems" do
+ build_git "foo" do |s|
+ s.executables = "foobar"
+ s.add_dependency "rails", "2.3.2"
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ git "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ G
+
+ bundle :install, artifice: "endpoint"
+
+ expect(the_bundle).to include_gems("rails 2.3.2")
+ end
+
+ it "handles git dependencies that are in rubygems using deployment mode" do
+ build_git "foo" do |s|
+ s.executables = "foobar"
+ s.add_dependency "rails", "2.3.2"
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle :install, artifice: "endpoint"
+
+ bundle_config "deployment true"
+ bundle :install, artifice: "endpoint"
+
+ expect(the_bundle).to include_gems("rails 2.3.2")
+ end
+
+ it "doesn't fail if you only have a git gem with no deps when using deployment mode" do
+ build_git "foo"
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle "install", artifice: "endpoint"
+ bundle_config "deployment true"
+ bundle :install, artifice: "endpoint"
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ it "falls back when the API errors out" do
+ simulate_platform "x86-mswin32" do
+ build_repo2 do
+ # The rcov gem is platform mswin32, but has no arch
+ build_gem "rcov" do |s|
+ s.platform = Gem::Platform.new([nil, "mswin32", nil])
+ s.write "lib/rcov.rb", "RCOV = '1.0.0'"
+ end
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "rcov"
+ G
+
+ bundle :install, artifice: "windows", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
+ expect(out).to include("Fetching source index from #{source_uri}")
+ expect(the_bundle).to include_gems "rcov 1.0.0"
+ end
+ end
+
+ it "falls back when hitting the Gemcutter Dependency Limit" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "activesupport"
+ gem "actionpack"
+ gem "actionmailer"
+ gem "activeresource"
+ gem "thin"
+ gem "myrack"
+ gem "rails"
+ G
+ bundle :install, artifice: "endpoint_fallback"
+ expect(out).to include("Fetching source index from #{source_uri}")
+
+ expect(the_bundle).to include_gems(
+ "activesupport 2.3.2",
+ "actionpack 2.3.2",
+ "actionmailer 2.3.2",
+ "activeresource 2.3.2",
+ "activesupport 2.3.2",
+ "thin 1.0.0",
+ "myrack 1.0.0",
+ "rails 2.3.2"
+ )
+ end
+
+ it "falls back when Gemcutter API doesn't return proper Marshal format" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, verbose: true, artifice: "endpoint_marshal_fail"
+ expect(out).to include("could not fetch from the dependency API, trying the full index")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "falls back when the API URL returns 403 Forbidden" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, verbose: true, artifice: "endpoint_api_forbidden"
+ expect(out).to include("Fetching source index from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "handles host redirects" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "endpoint_host_redirect"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "handles host redirects without Gem::Net::HTTP::Persistent" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ FileUtils.mkdir_p lib_path
+ File.open(lib_path("disable_net_http_persistent.rb"), "w") do |h|
+ h.write <<-H
+ module Kernel
+ alias require_without_disabled_net_http require
+ def require(*args)
+ raise LoadError, 'simulated' if args.first == 'openssl' && !caller.grep(/vendored_persistent/).empty?
+ require_without_disabled_net_http(*args)
+ end
+ end
+ H
+ end
+
+ bundle :install, artifice: "endpoint_host_redirect", requires: [lib_path("disable_net_http_persistent.rb")]
+ expect(out).to_not match(/Too many redirects/)
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "timeouts when Bundler::Fetcher redirects too much" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "endpoint_redirect", raise_on_error: false
+ expect(err).to match(/Too many redirects/)
+ end
+
+ context "when --full-index is specified" do
+ it "should use the modern index for install" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle "install --full-index", artifice: "endpoint"
+ expect(out).to include("Fetching source index from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "should use the modern index for update" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle "update --full-index", artifice: "endpoint", all: true
+ expect(out).to include("Fetching source index from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ it "fetches again when more dependencies are found in subsequent sources" do
+ build_repo2 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")]
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ bundle :install, artifice: "endpoint_extra"
+ expect(the_bundle).to include_gems "back_deps 1.0", "foo 1.0"
+ end
+
+ it "fetches gem versions even when those gems are already installed" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack", "1.0.0"
+ G
+ bundle :install, artifice: "endpoint_extra_api"
+
+ build_repo4 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ gemfile <<-G
+ source "#{source_uri}" do; end
+ source "#{source_uri}/extra"
+ gem "myrack", "1.2"
+ G
+ bundle :install, artifice: "endpoint_extra_api"
+ expect(the_bundle).to include_gems "myrack 1.2"
+ end
+
+ it "resolves indirect dependencies to the most scoped source that includes them" do
+ # In this scenario, the gem "somegem" only exists in repo4. It depends on
+ # specific version of activesupport that exists only in repo1. There
+ # happens also be a version of activesupport in repo4, but not the one that
+ # version 1.0.0 of somegem wants. This test makes sure that bundler tries to
+ # use the version in the most scoped source, even if not compatible, and
+ # gives a resolution error
+ build_repo4 do
+ build_gem "activesupport", "1.2.0"
+ build_gem "somegem", "1.0.0" do |s|
+ s.add_dependency "activesupport", "1.2.3" # This version exists only in repo1
+ end
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem 'somegem', '1.0.0'
+ end
+ G
+
+ bundle :install, artifice: "compact_index_extra_api", raise_on_error: false
+
+ expect(err).to include("Could not find compatible versions")
+ end
+
+ it "prints API output properly with back deps" do
+ build_repo2 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")]
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ bundle :install, artifice: "endpoint_extra"
+
+ expect(out).to include("Fetching gem metadata from http://localgemserver.test/.")
+ expect(out).to include("Fetching source index from http://localgemserver.test/extra")
+ end
+
+ it "does not fetch every spec when doing back deps" do
+ build_repo2 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ build_gem "missing"
+
+ FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")]
+ end
+
+ install_gemfile <<-G, artifice: "endpoint_extra_missing"
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ expect(the_bundle).to include_gems "back_deps 1.0"
+ end
+
+ it "fetches again when more dependencies are found in subsequent sources using deployment mode" do
+ build_repo2 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")]
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ bundle :install, artifice: "endpoint_extra"
+ bundle_config "deployment true"
+ bundle "install", artifice: "endpoint_extra"
+ expect(the_bundle).to include_gems "back_deps 1.0"
+ end
+
+ it "does not fetch all marshaled specs" do
+ build_repo2 do
+ build_gem "foo", "1.0"
+ build_gem "foo", "2.0"
+ end
+
+ install_gemfile <<-G, artifice: "endpoint", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }, verbose: true
+ source "#{source_uri}"
+
+ gem "foo"
+ G
+
+ expect(out).to include("foo-2.0.gemspec.rz")
+ expect(out).not_to include("foo-1.0.gemspec.rz")
+ end
+
+ it "does not refetch if the only unmet dependency is bundler" do
+ build_repo2 do
+ build_gem "bundler_dep" do |s|
+ s.add_dependency "bundler"
+ end
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+
+ gem "bundler_dep"
+ G
+
+ bundle :install, artifice: "endpoint", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ end
+
+ it "prints post_install_messages" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack-obama'
+ G
+
+ bundle :install, artifice: "endpoint"
+ expect(out).to include("Post-install message from myrack:")
+ end
+
+ it "should display the post install message for a dependency" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack_middleware'
+ G
+
+ bundle :install, artifice: "endpoint"
+ expect(out).to include("Post-install message from myrack:")
+ expect(out).to include("Myrack's post install message")
+ end
+
+ context "when using basic authentication" do
+ let(:user) { "user" }
+ let(:password) { "pass" }
+ let(:basic_auth_source_uri) do
+ uri = Gem::URI.parse(source_uri)
+ uri.user = user
+ uri.password = password
+
+ uri
+ end
+
+ it "passes basic authentication details and strips out creds" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "endpoint_basic_authentication"
+ expect(out).not_to include("#{user}:#{password}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "passes basic authentication details and strips out creds also in verbose mode" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, verbose: true, artifice: "endpoint_basic_authentication"
+ expect(out).not_to include("#{user}:#{password}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "strips http basic authentication creds for modern index" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "endpoint_marshal_fail_basic_authentication"
+ expect(out).not_to include("#{user}:#{password}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "strips http basic auth creds when it can't reach the server" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "endpoint_500", raise_on_error: false
+ expect(out).not_to include("#{user}:#{password}")
+ end
+
+ it "does not pass the user / password to different hosts on redirect" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "endpoint_creds_diff_host"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ describe "with host including dashes" do
+ before do
+ gemfile <<-G
+ source "http://local-gemserver.test"
+ gem "myrack"
+ G
+ end
+
+ it "reads authentication details from a valid ENV variable" do
+ bundle :install, artifice: "endpoint_strict_basic_authentication", env: { "BUNDLE_LOCAL___GEMSERVER__TEST" => "#{user}:#{password}" }
+
+ expect(out).to include("Fetching gem metadata from http://local-gemserver.test")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ describe "with authentication details in bundle config" do
+ before do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+ end
+
+ it "reads authentication details by host name from bundle config" do
+ bundle "config set #{source_hostname} #{user}:#{password}"
+
+ bundle :install, artifice: "endpoint_strict_basic_authentication"
+
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "reads authentication details by full url from bundle config" do
+ # The trailing slash is necessary here; Fetcher canonicalizes the URI.
+ bundle "config set #{source_uri}/ #{user}:#{password}"
+
+ bundle :install, artifice: "endpoint_strict_basic_authentication"
+
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "should use the API" do
+ bundle "config set #{source_hostname} #{user}:#{password}"
+ bundle :install, artifice: "endpoint_strict_basic_authentication"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "prefers auth supplied in the source uri" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle "config set #{source_hostname} otheruser:wrong"
+
+ bundle :install, artifice: "endpoint_strict_basic_authentication"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "shows instructions if auth is not provided for the source" do
+ bundle :install, artifice: "endpoint_strict_basic_authentication", raise_on_error: false
+ expect(err).to include("bundle config set --global #{source_hostname} username:password")
+ end
+
+ it "fails if authentication has already been provided, but failed" do
+ bundle "config set #{source_hostname} #{user}:wrong"
+
+ bundle :install, artifice: "endpoint_strict_basic_authentication", raise_on_error: false
+ expect(err).to include("Bad username or password")
+ end
+ end
+
+ describe "with no password" do
+ let(:password) { nil }
+
+ it "passes basic authentication details" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "endpoint_basic_authentication"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+ end
+
+ context "when ruby is compiled without openssl" do
+ before do
+ # Install a monkeypatch that reproduces the effects of openssl being
+ # missing when the fetcher runs, as happens in real life. The reason
+ # we can't just overwrite openssl.rb is that Artifice uses it.
+ bundled_app("broken_ssl").mkpath
+ bundled_app("broken_ssl/openssl.rb").open("w") do |f|
+ f.write <<-RUBY
+ raise LoadError, "cannot load such file -- openssl"
+ RUBY
+ end
+ end
+
+ it "explains what to do to get it, and includes original error" do
+ gemfile <<-G
+ source "#{source_uri.gsub(/http/, "https")}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "fail", env: { "RUBYOPT" => "-I#{bundled_app("broken_ssl")}" }, raise_on_error: false
+ expect(err).to include("recompile Ruby").and include("cannot load such file")
+ end
+ end
+
+ context "when SSL certificate verification fails" do
+ it "explains what happened" do
+ # Install a monkeypatch that reproduces the effects of openssl raising
+ # a certificate validation error when RubyGems tries to connect.
+ gemfile <<-G
+ class Gem::Net::HTTP
+ def start
+ raise OpenSSL::SSL::SSLError, "certificate verify failed"
+ end
+ end
+
+ source "#{source_uri.gsub(/http/, "https")}"
+ gem "myrack"
+ G
+
+ bundle :install, raise_on_error: false
+ expect(err).to match(/could not verify the SSL certificate/i)
+ end
+ end
+
+ context ".gemrc with sources is present" do
+ it "uses other sources declared in the Gemfile" do
+ File.open(home(".gemrc"), "w") do |file|
+ file.puts({ sources: ["https://rubygems.org"] }.to_yaml)
+ end
+
+ begin
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack'
+ G
+
+ bundle "install", artifice: "endpoint_marshal_fail"
+ ensure
+ FileUtils.rm_rf home(".gemrc")
+ end
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/env_spec.rb b/spec/bundler/install/gems/env_spec.rb
new file mode 100644
index 0000000000..6d5aa456fe
--- /dev/null
+++ b/spec/bundler/install/gems/env_spec.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with ENV conditionals" do
+ describe "when just setting an ENV key as a string" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ env "BUNDLER_TEST" do
+ gem "myrack"
+ end
+ G
+ end
+
+ it "excludes the gems when the ENV variable is not set" do
+ bundle :install
+ expect(the_bundle).not_to include_gems "myrack"
+ end
+
+ it "includes the gems when the ENV variable is set" do
+ ENV["BUNDLER_TEST"] = "1"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+
+ describe "when just setting an ENV key as a symbol" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ env :BUNDLER_TEST do
+ gem "myrack"
+ end
+ G
+ end
+
+ it "excludes the gems when the ENV variable is not set" do
+ bundle :install
+ expect(the_bundle).not_to include_gems "myrack"
+ end
+
+ it "includes the gems when the ENV variable is set" do
+ ENV["BUNDLER_TEST"] = "1"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+
+ describe "when setting a string to match the env" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ env "BUNDLER_TEST" => "foo" do
+ gem "myrack"
+ end
+ G
+ end
+
+ it "excludes the gems when the ENV variable is not set" do
+ bundle :install
+ expect(the_bundle).not_to include_gems "myrack"
+ end
+
+ it "excludes the gems when the ENV variable is set but does not match the condition" do
+ ENV["BUNDLER_TEST"] = "1"
+ bundle :install
+ expect(the_bundle).not_to include_gems "myrack"
+ end
+
+ it "includes the gems when the ENV variable is set and matches the condition" do
+ ENV["BUNDLER_TEST"] = "foo"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+
+ describe "when setting a regex to match the env" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ env "BUNDLER_TEST" => /foo/ do
+ gem "myrack"
+ end
+ G
+ end
+
+ it "excludes the gems when the ENV variable is not set" do
+ bundle :install
+ expect(the_bundle).not_to include_gems "myrack"
+ end
+
+ it "excludes the gems when the ENV variable is set but does not match the condition" do
+ ENV["BUNDLER_TEST"] = "fo"
+ bundle :install
+ expect(the_bundle).not_to include_gems "myrack"
+ end
+
+ it "includes the gems when the ENV variable is set and matches the condition" do
+ ENV["BUNDLER_TEST"] = "foobar"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/flex_spec.rb b/spec/bundler/install/gems/flex_spec.rb
new file mode 100644
index 0000000000..a30b53d6ad
--- /dev/null
+++ b/spec/bundler/install/gems/flex_spec.rb
@@ -0,0 +1,403 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle flex_install" do
+ it "installs the gems as expected" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(the_bundle).to be_locked
+ end
+
+ it "installs even when the lockfile is invalid" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(the_bundle).to be_locked
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack', '1.0'
+ G
+
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(the_bundle).to be_locked
+ end
+
+ it "keeps child dependencies at the same version" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack-obama"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0.0"
+
+ update_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack-obama", "1.0"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0.0"
+ end
+
+ describe "adding new gems" do
+ it "installs added gems without updating previously installed gems" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack'
+ G
+
+ update_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack'
+ gem 'activesupport', '2.3.5'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.5"
+ end
+
+ it "keeps child dependencies pinned" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack-obama"
+ G
+
+ update_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack-obama"
+ gem "thin"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0", "thin 1.0"
+ end
+ end
+
+ describe "removing gems" do
+ it "removes gems without changing the versions of remaining gems" do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack'
+ gem 'activesupport', '2.3.5'
+ G
+
+ update_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5"
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack'
+ gem 'activesupport', '2.3.2'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.2"
+ end
+
+ it "removes top level dependencies when removed from the Gemfile while leaving other dependencies intact" do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack'
+ gem 'activesupport', '2.3.5'
+ G
+
+ update_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack'
+ G
+
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5"
+ end
+
+ it "removes child dependencies" do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack-obama'
+ gem 'activesupport'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0.0", "activesupport 2.3.5"
+
+ update_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'activesupport'
+ G
+
+ expect(the_bundle).to include_gems "activesupport 2.3.5"
+ expect(the_bundle).not_to include_gems "myrack-obama", "myrack"
+ end
+ end
+
+ describe "when running bundle install and Gemfile conflicts with lockfile" do
+ before(:each) do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack_middleware"
+ G
+
+ expect(the_bundle).to include_gems "myrack_middleware 1.0", "myrack 0.9.1"
+
+ build_repo2 do
+ build_gem "myrack-obama", "2.0" do |s|
+ s.add_dependency "myrack", "=1.2"
+ end
+ build_gem "myrack_middleware", "2.0" do |s|
+ s.add_dependency "myrack", ">=1.0"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack-obama", "2.0"
+ gem "myrack_middleware"
+ G
+ end
+
+ it "does not install gems whose dependencies are not met" do
+ bundle :install, raise_on_error: false
+ ruby <<-RUBY, raise_on_error: false
+ require 'bundler/setup'
+ RUBY
+ expect(err).to match(/could not find gem 'myrack-obama/i)
+ end
+
+ it "discards the locked gems when the Gemfile requires different versions than the lock" do
+ bundle_config "force_ruby_platform true"
+
+ nice_error = <<~E.strip
+ Could not find compatible versions
+
+ Because myrack-obama >= 2.0 depends on myrack = 1.2
+ and myrack = 1.2 could not be found in rubygems repository https://gem.repo2/ or installed locally,
+ myrack-obama >= 2.0 cannot be used.
+ So, because Gemfile depends on myrack-obama = 2.0,
+ version solving has failed.
+ E
+
+ bundle :install, retry: 0, raise_on_error: false
+ expect(err).to end_with(nice_error)
+ end
+
+ it "does not include conflicts with a single requirement tree, because that can't possibly be a conflict" do
+ bundle_config "force_ruby_platform true"
+
+ bad_error = <<~E.strip
+ Bundler could not find compatible versions for gem "myrack-obama":
+ In Gemfile:
+ myrack-obama (= 2.0)
+ E
+
+ bundle "update myrack_middleware", retry: 0, raise_on_error: false
+ expect(err).not_to end_with(bad_error)
+ end
+ end
+
+ describe "when running bundle update and Gemfile conflicts with lockfile" do
+ before(:each) do
+ build_repo4 do
+ build_gem "jekyll-feed", "0.16.0"
+ build_gem "jekyll-feed", "0.15.1"
+
+ build_gem "github-pages", "226" do |s|
+ s.add_dependency "jekyll-feed", "0.15.1"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "jekyll-feed", "~> 0.12"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "github-pages", "~> 226"
+ gem "jekyll-feed", "~> 0.12"
+ G
+ end
+
+ it "discards the conflicting lockfile information and resolves properly" do
+ bundle :update, raise_on_error: false, all: true
+ expect(err).to be_empty
+ end
+ end
+
+ describe "subtler cases" do
+ before :each do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "myrack-obama"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ gem "myrack-obama"
+ G
+ end
+
+ it "should work when you install" do
+ bundle "install"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo1, "myrack", "0.9.1"
+ c.checksum gem_repo1, "myrack-obama", "1.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (0.9.1)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (= 0.9.1)
+ myrack-obama
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "should work when you update" do
+ bundle "update myrack"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo1, "myrack", "0.9.1"
+ c.checksum gem_repo1, "myrack-obama", "1.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (0.9.1)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (= 0.9.1)
+ myrack-obama
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ describe "when adding a new source" do
+ it "updates the lockfile" do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ source "https://gem.repo2" do
+ end
+ gem "myrack"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo1, "myrack", "1.0.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ # This was written to test github issue #636
+ describe "when a locked child dependency conflicts" do
+ before(:each) do
+ build_repo2 do
+ build_gem "capybara", "0.3.9" do |s|
+ s.add_dependency "myrack", ">= 1.0.0"
+ end
+
+ build_gem "myrack", "1.1.0"
+ build_gem "rails", "3.0.0.rc4" do |s|
+ s.add_dependency "myrack", "~> 1.1.0"
+ end
+
+ build_gem "myrack", "1.2.1"
+ build_gem "rails", "3.0.0" do |s|
+ s.add_dependency "myrack", "~> 1.2.1"
+ end
+ end
+ end
+
+ it "resolves them" do
+ # install Rails 3.0.0.rc
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "3.0.0.rc4"
+ gem "capybara", "0.3.9"
+ G
+
+ # upgrade Rails to 3.0.0 and then install again
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "3.0.0"
+ gem "capybara", "0.3.9"
+ G
+ expect(err).to be_empty
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/fund_spec.rb b/spec/bundler/install/gems/fund_spec.rb
new file mode 100644
index 0000000000..8a3a51270a
--- /dev/null
+++ b/spec/bundler/install/gems/fund_spec.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ context "with gem sources" do
+ before do
+ build_repo2 do
+ build_gem "has_funding_and_other_metadata" do |s|
+ s.metadata = {
+ "bug_tracker_uri" => "https://example.com/user/bestgemever/issues",
+ "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md",
+ "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1",
+ "homepage_uri" => "https://bestgemever.example.io",
+ "mailing_list_uri" => "https://groups.example.com/bestgemever",
+ "funding_uri" => "https://example.com/has_funding_and_other_metadata/funding",
+ "source_code_uri" => "https://example.com/user/bestgemever",
+ "wiki_uri" => "https://example.com/user/bestgemever/wiki",
+ }
+ end
+
+ build_gem "has_funding", "1.2.3" do |s|
+ s.metadata = {
+ "funding_uri" => "https://example.com/has_funding/funding",
+ }
+ end
+
+ build_gem "gem_with_dependent_funding", "1.0" do |s|
+ s.add_dependency "has_funding"
+ end
+ end
+ end
+
+ context "when gems include a fund URI" do
+ it "displays the plural fund message after installing" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'has_funding_and_other_metadata'
+ gem 'has_funding'
+ gem 'myrack-obama'
+ G
+
+ expect(out).to include("2 installed gems you directly depend on are looking for funding.")
+ end
+
+ it "displays the singular fund message after installing" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'has_funding'
+ gem 'myrack-obama'
+ G
+
+ expect(out).to include("1 installed gem you directly depend on is looking for funding.")
+ end
+ end
+
+ context "when gems include a fund URI but `ignore_funding_requests` is configured" do
+ before do
+ bundle_config "ignore_funding_requests true"
+ end
+
+ it "does not display the plural fund message after installing" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'has_funding_and_other_metadata'
+ gem 'has_funding'
+ gem 'myrack-obama'
+ G
+
+ expect(out).not_to include("2 installed gems you directly depend on are looking for funding.")
+ end
+
+ it "does not display the singular fund message after installing" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'has_funding'
+ gem 'myrack-obama'
+ G
+
+ expect(out).not_to include("1 installed gem you directly depend on is looking for funding.")
+ end
+ end
+
+ context "when gems do not include fund messages" do
+ it "does not display any fund messages" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ G
+
+ expect(out).not_to include("gem you depend on")
+ end
+ end
+
+ context "when a dependency includes a fund message" do
+ it "does not display the fund message" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'gem_with_dependent_funding'
+ G
+
+ expect(out).not_to include("gem you depend on")
+ end
+ end
+ end
+
+ context "with git sources" do
+ context "when gems include fund URI" do
+ it "displays the fund message after installing" do
+ build_git "also_has_funding" do |s|
+ s.metadata = {
+ "funding_uri" => "https://example.com/also_has_funding/funding",
+ }
+ end
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.0")}'
+ G
+
+ expect(out).to include("1 installed gem you directly depend on is looking for funding.")
+ end
+
+ it "displays the fund message if repo is updated" do
+ build_git "also_has_funding" do |s|
+ s.metadata = {
+ "funding_uri" => "https://example.com/also_has_funding/funding",
+ }
+ end
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.0")}'
+ G
+
+ build_git "also_has_funding", "1.1" do |s|
+ s.metadata = {
+ "funding_uri" => "https://example.com/also_has_funding/funding",
+ }
+ end
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.1")}'
+ G
+
+ expect(out).to include("1 installed gem you directly depend on is looking for funding.")
+ end
+
+ it "displays the fund message if repo is not updated" do
+ build_git "also_has_funding" do |s|
+ s.metadata = {
+ "funding_uri" => "https://example.com/also_has_funding/funding",
+ }
+ end
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.0")}'
+ G
+
+ bundle :install
+ expect(out).to include("1 installed gem you directly depend on is looking for funding.")
+
+ bundle :install
+ expect(out).to include("1 installed gem you directly depend on is looking for funding.")
+ end
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/gemfile_source_header_spec.rb b/spec/bundler/install/gems/gemfile_source_header_spec.rb
new file mode 100644
index 0000000000..dc35c8d741
--- /dev/null
+++ b/spec/bundler/install/gems/gemfile_source_header_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+RSpec.describe "fetching dependencies with a mirrored source" do
+ let(:mirror) { "https://server.example.org" }
+
+ before do
+ build_repo2
+
+ gemfile <<-G
+ source "#{mirror}"
+ gem 'weakling'
+ G
+
+ bundle_config "mirror.#{mirror} https://gem.repo2"
+ end
+
+ it "sets the 'X-Gemfile-Source' and 'User-Agent' headers and bundles successfully" do
+ bundle :install, artifice: "endpoint_mirror_source"
+
+ expect(out).to include("Installing weakling")
+ expect(out).to include("Bundle complete")
+ expect(the_bundle).to include_gems "weakling 0.0.3"
+ end
+end
diff --git a/spec/bundler/install/gems/mirror_probe_spec.rb b/spec/bundler/install/gems/mirror_probe_spec.rb
new file mode 100644
index 0000000000..564062ccf6
--- /dev/null
+++ b/spec/bundler/install/gems/mirror_probe_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+RSpec.describe "fetching dependencies with a not available mirror" do
+ before do
+ build_repo2
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem 'weakling'
+ G
+ end
+
+ context "with a specific fallback timeout" do
+ before do
+ bundle_config_global("BUNDLE_MIRROR__HTTPS://GEM__REPO2/__FALLBACK_TIMEOUT/" => "true",
+ "BUNDLE_MIRROR__HTTPS://GEM__REPO2/" => "https://gem.mirror")
+ end
+
+ it "install a gem using the original uri when the mirror is not responding" do
+ bundle :install, env: { "BUNDLER_SPEC_FAKE_RESOLVE" => "gem.mirror" }, verbose: true
+
+ expect(out).to include("Installing weakling")
+ expect(out).to include("Bundle complete")
+ expect(the_bundle).to include_gems "weakling 0.0.3"
+ end
+ end
+
+ context "with a global fallback timeout" do
+ before do
+ bundle_config_global("BUNDLE_MIRROR__ALL__FALLBACK_TIMEOUT/" => "1",
+ "BUNDLE_MIRROR__ALL" => "https://gem.mirror")
+ end
+
+ it "install a gem using the original uri when the mirror is not responding" do
+ bundle :install, env: { "BUNDLER_SPEC_FAKE_RESOLVE" => "gem.mirror" }
+
+ expect(out).to include("Installing weakling")
+ expect(out).to include("Bundle complete")
+ expect(the_bundle).to include_gems "weakling 0.0.3"
+ end
+ end
+
+ context "with a specific mirror without a fallback timeout" do
+ before do
+ bundle_config_global("BUNDLE_MIRROR__HTTPS://GEM__REPO2/" => "https://gem.mirror")
+ end
+
+ it "fails to install the gem with a timeout error when the mirror is not responding" do
+ bundle :install, artifice: "compact_index_mirror_down", raise_on_error: false
+
+ expect(out).to be_empty
+ expect(err).to eq("Could not reach host gem.mirror. Check your network connection and try again.")
+ end
+ end
+
+ context "with a global mirror without a fallback timeout" do
+ before do
+ bundle_config_global("BUNDLE_MIRROR__ALL" => "https://gem.mirror")
+ end
+
+ it "fails to install the gem with a timeout error when the mirror is not responding" do
+ bundle :install, artifice: "compact_index_mirror_down", raise_on_error: false
+
+ expect(out).to be_empty
+ expect(err).to eq("Could not reach host gem.mirror. Check your network connection and try again.")
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/mirror_spec.rb b/spec/bundler/install/gems/mirror_spec.rb
new file mode 100644
index 0000000000..e1fbeac454
--- /dev/null
+++ b/spec/bundler/install/gems/mirror_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with a mirror configured" do
+ describe "when the mirror does not match the gem source" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+ bundle_config "mirror.http://gems.example.org http://gem-mirror.example.org"
+ end
+
+ it "installs from the normal location" do
+ bundle :install
+ expect(out).to include("Fetching gem metadata from https://gem.repo1")
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+
+ describe "when the gem source matches a configured mirror" do
+ before :each do
+ gemfile <<-G
+ # This source is bogus and doesn't have the gem we're looking for
+ source "https://gem.repo2"
+
+ gem "myrack"
+ G
+ bundle_config "mirror.https://gem.repo2 https://gem.repo1"
+ end
+
+ it "installs the gem from the mirror" do
+ bundle :install, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ expect(out).to include("Fetching gem metadata from https://gem.repo1")
+ expect(out).not_to include("Fetching gem metadata from https://gem.repo2")
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/native_extensions_spec.rb b/spec/bundler/install/gems/native_extensions_spec.rb
new file mode 100644
index 0000000000..d5b10d2c8f
--- /dev/null
+++ b/spec/bundler/install/gems/native_extensions_spec.rb
@@ -0,0 +1,184 @@
+# frozen_string_literal: true
+
+RSpec.describe "installing a gem with native extensions" do
+ it "installs" do
+ build_repo2 do
+ build_gem "c_extension" do |s|
+ s.extensions = ["ext/extconf.rb"]
+ s.write "ext/extconf.rb", <<-E
+ require "mkmf"
+ name = "c_extension_bundle"
+ dir_config(name)
+ raise ArgumentError unless with_config("c_extension") == "hello"
+ create_makefile(name)
+ E
+
+ s.write "ext/c_extension.c", <<-C
+ #include "ruby.h"
+
+ VALUE c_extension_true(VALUE self) {
+ return Qtrue;
+ }
+
+ void Init_c_extension_bundle() {
+ VALUE c_Extension = rb_define_class("CExtension", rb_cObject);
+ rb_define_method(c_Extension, "its_true", c_extension_true, 0);
+ }
+ C
+
+ s.write "lib/c_extension.rb", <<-C
+ require "c_extension_bundle"
+ C
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "c_extension"
+ G
+
+ bundle_config "build.c_extension --with-c_extension=hello"
+ bundle "install"
+
+ expect(out).to include("Installing c_extension 1.0 with native extensions")
+
+ run "Bundler.require; puts CExtension.new.its_true"
+ expect(out).to eq("true")
+ end
+
+ it "installs from git" do
+ build_git "c_extension" do |s|
+ s.extensions = ["ext/extconf.rb"]
+ s.write "ext/extconf.rb", <<-E
+ require "mkmf"
+ name = "c_extension_bundle"
+ dir_config(name)
+ raise ArgumentError unless with_config("c_extension") == "hello"
+ create_makefile(name)
+ E
+
+ s.write "ext/c_extension.c", <<-C
+ #include "ruby.h"
+
+ VALUE c_extension_true(VALUE self) {
+ return Qtrue;
+ }
+
+ void Init_c_extension_bundle() {
+ VALUE c_Extension = rb_define_class("CExtension", rb_cObject);
+ rb_define_method(c_Extension, "its_true", c_extension_true, 0);
+ }
+ C
+
+ s.write "lib/c_extension.rb", <<-C
+ require "c_extension_bundle"
+ C
+ end
+
+ bundle_config "build.c_extension --with-c_extension=hello"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "c_extension", :git => #{lib_path("c_extension-1.0").to_s.dump}
+ G
+
+ expect(err).to_not include("warning: conflicting chdir during another chdir block")
+
+ run "Bundler.require; puts CExtension.new.its_true"
+ expect(out).to eq("true")
+ end
+
+ it "installs correctly from git when multiple gems with extensions share one repository" do
+ build_repo2 do
+ ["one", "two"].each do |n|
+ build_lib "c_extension_#{n}", "1.0", path: lib_path("gems/c_extension_#{n}") do |s|
+ s.extensions = ["ext/extconf.rb"]
+ s.write "ext/extconf.rb", <<-E
+ require "mkmf"
+ name = "c_extension_bundle_#{n}"
+ dir_config(name)
+ raise ArgumentError unless with_config("c_extension_#{n}") == "#{n}"
+ create_makefile(name)
+ E
+
+ s.write "ext/c_extension_#{n}.c", <<-C
+ #include "ruby.h"
+
+ VALUE c_extension_#{n}_value(VALUE self) {
+ return rb_str_new_cstr("#{n}");
+ }
+
+ void Init_c_extension_bundle_#{n}() {
+ VALUE c_Extension = rb_define_class("CExtension_#{n}", rb_cObject);
+ rb_define_method(c_Extension, "value", c_extension_#{n}_value, 0);
+ }
+ C
+
+ s.write "lib/c_extension_#{n}.rb", <<-C
+ require "c_extension_bundle_#{n}"
+ C
+ end
+ end
+ build_git "gems", path: lib_path("gems"), gemspec: false
+ end
+
+ bundle_config "build.c_extension_one --with-c_extension_one=one"
+ bundle_config "build.c_extension_two --with-c_extension_two=two"
+
+ # 1st time, require only one gem -- only one of the extensions gets built.
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "c_extension_one", :git => #{lib_path("gems").to_s.dump}
+ G
+
+ # 2nd time, require both gems -- we need both extensions to be built now.
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "c_extension_one", :git => #{lib_path("gems").to_s.dump}
+ gem "c_extension_two", :git => #{lib_path("gems").to_s.dump}
+ G
+
+ run "Bundler.require; puts CExtension_one.new.value; puts CExtension_two.new.value"
+ expect(out).to eq("one\ntwo")
+ end
+
+ it "install with multiple build flags" do
+ build_git "c_extension" do |s|
+ s.extensions = ["ext/extconf.rb"]
+ s.write "ext/extconf.rb", <<-E
+ require "mkmf"
+ name = "c_extension_bundle"
+ dir_config(name)
+ raise ArgumentError unless with_config("c_extension") == "hello" && with_config("c_extension_bundle-dir") == "hola"
+ create_makefile(name)
+ E
+
+ s.write "ext/c_extension.c", <<-C
+ #include "ruby.h"
+
+ VALUE c_extension_true(VALUE self) {
+ return Qtrue;
+ }
+
+ void Init_c_extension_bundle() {
+ VALUE c_Extension = rb_define_class("CExtension", rb_cObject);
+ rb_define_method(c_Extension, "its_true", c_extension_true, 0);
+ }
+ C
+
+ s.write "lib/c_extension.rb", <<-C
+ require "c_extension_bundle"
+ C
+ end
+
+ bundle_config "build.c_extension --with-c_extension=hello --with-c_extension_bundle-dir=hola"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "c_extension", :git => #{lib_path("c_extension-1.0").to_s.dump}
+ G
+
+ run "Bundler.require; puts CExtension.new.its_true"
+ expect(out).to eq("true")
+ end
+end
diff --git a/spec/bundler/install/gems/no_build_extension_spec.rb b/spec/bundler/install/gems/no_build_extension_spec.rb
new file mode 100644
index 0000000000..31f0170433
--- /dev/null
+++ b/spec/bundler/install/gems/no_build_extension_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with --no-build-extension" do
+ before do
+ build_repo2 do
+ build_gem "with_extension" do |s|
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ File.open("\#{path}/with_extension.rb", "w") do |f|
+ f.puts "WITH_EXTENSION = 'YES'"
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ it "skips building native extensions and warns when no_build_extension is set" do
+ bundle_config "no_build_extension true"
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_extension"
+ gem "rake"
+ G
+
+ bundle :install
+
+ build_complete = default_bundle_path("extensions").join(
+ Gem::Platform.local.to_s,
+ Gem.extension_api_version.to_s,
+ "with_extension-1.0",
+ "gem.build_complete"
+ )
+ expect(build_complete).not_to exist
+ expect(err).to include("with_extension-1.0 contains native extensions that were not built")
+ expect(err).to include("unset no_build_extension and run `bundle pristine with_extension`")
+ end
+
+ it "builds native extensions by default" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_extension"
+ gem "rake"
+ G
+
+ bundle :install
+
+ expect(out).to include("Installing with_extension 1.0 with native extensions")
+ end
+end
diff --git a/spec/bundler/install/gems/no_install_plugin_spec.rb b/spec/bundler/install/gems/no_install_plugin_spec.rb
new file mode 100644
index 0000000000..e040e6b813
--- /dev/null
+++ b/spec/bundler/install/gems/no_install_plugin_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with --no-install-plugin" do
+ before do
+ build_repo2 do
+ build_gem "with_plugin", "1.0" do |s|
+ s.write "lib/rubygems_plugin.rb", "# plugin code"
+ end
+
+ build_gem "with_plugin", "2.0"
+ end
+ end
+
+ let(:plugin_path) { default_bundle_path("plugins", "with_plugin_plugin.rb") }
+
+ it "does not generate the plugin wrapper and warns when no_install_plugin is set" do
+ bundle_config "no_install_plugin true"
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_plugin", "1.0"
+ G
+
+ expect(plugin_path).not_to exist
+ expect(err).to include("with_plugin-1.0 contains plugins that were not installed")
+ expect(err).to include("unset no_install_plugin and run `bundle pristine with_plugin`")
+ end
+
+ it "removes a stale plugin wrapper from a prior version when no_install_plugin is set" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_plugin", "1.0"
+ G
+ expect(plugin_path).to exist
+
+ bundle_config "no_install_plugin true"
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_plugin", "2.0"
+ G
+
+ expect(plugin_path).not_to exist
+ end
+
+ it "generates the plugin wrapper by default" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_plugin", "1.0"
+ G
+
+ expect(plugin_path).to exist
+ end
+end
diff --git a/spec/bundler/install/gems/post_install_spec.rb b/spec/bundler/install/gems/post_install_spec.rb
new file mode 100644
index 0000000000..e49fd2a9a3
--- /dev/null
+++ b/spec/bundler/install/gems/post_install_spec.rb
@@ -0,0 +1,150 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ context "with gem sources" do
+ context "when gems include post install messages" do
+ it "should display the post-install messages after installing" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ gem 'thin'
+ gem 'myrack-obama'
+ G
+
+ bundle :install
+ expect(out).to include("Post-install message from myrack:")
+ expect(out).to include("Myrack's post install message")
+ expect(out).to include("Post-install message from thin:")
+ expect(out).to include("Thin's post install message")
+ expect(out).to include("Post-install message from myrack-obama:")
+ expect(out).to include("Myrack-obama's post install message")
+ end
+ end
+
+ context "when gems do not include post install messages" do
+ it "should not display any post-install messages" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport"
+ G
+
+ bundle :install
+ expect(out).not_to include("Post-install message")
+ end
+ end
+
+ context "when a dependency includes a post install message" do
+ it "should display the post install message" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack_middleware'
+ G
+
+ bundle :install
+ expect(out).to include("Post-install message from myrack:")
+ expect(out).to include("Myrack's post install message")
+ end
+ end
+ end
+
+ context "with git sources" do
+ context "when gems include post install messages" do
+ it "should display the post-install messages after installing" do
+ build_git "foo" do |s|
+ s.post_install_message = "Foo's post install message"
+ end
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :install
+ expect(out).to include("Post-install message from foo:")
+ expect(out).to include("Foo's post install message")
+ end
+
+ it "should display the post-install messages if repo is updated" do
+ build_git "foo" do |s|
+ s.post_install_message = "Foo's post install message"
+ end
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle :install
+
+ build_git "foo", "1.1" do |s|
+ s.post_install_message = "Foo's 1.1 post install message"
+ end
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => '#{lib_path("foo-1.1")}'
+ G
+ bundle :install
+
+ expect(out).to include("Post-install message from foo:")
+ expect(out).to include("Foo's 1.1 post install message")
+ end
+
+ it "should not display the post-install messages if repo is not updated" do
+ build_git "foo" do |s|
+ s.post_install_message = "Foo's post install message"
+ end
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :install
+ expect(out).to include("Post-install message from foo:")
+ expect(out).to include("Foo's post install message")
+
+ bundle :install
+ expect(out).not_to include("Post-install message")
+ end
+ end
+
+ context "when gems do not include post install messages" do
+ it "should not display any post-install messages" do
+ build_git "foo" do |s|
+ s.post_install_message = nil
+ end
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :install
+ expect(out).not_to include("Post-install message")
+ end
+ end
+ end
+
+ context "when ignore post-install messages for gem is set" do
+ it "doesn't display any post-install messages" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle_config "ignore_messages.myrack true"
+
+ bundle :install
+ expect(out).not_to include("Post-install message")
+ end
+ end
+
+ context "when ignore post-install messages for all gems" do
+ it "doesn't display any post-install messages" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle_config "ignore_messages true"
+
+ bundle :install
+ expect(out).not_to include("Post-install message")
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/resolving_spec.rb b/spec/bundler/install/gems/resolving_spec.rb
new file mode 100644
index 0000000000..111d361aab
--- /dev/null
+++ b/spec/bundler/install/gems/resolving_spec.rb
@@ -0,0 +1,786 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with install-time dependencies" do
+ before do
+ build_repo2 do
+ build_gem "with_implicit_rake_dep" do |s|
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ File.open("\#{path}/implicit_rake_dep.rb", "w") do |f|
+ f.puts "IMPLICIT_RAKE_DEP = 'YES'"
+ end
+ end
+ RUBY
+ end
+
+ build_gem "another_implicit_rake_dep" do |s|
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ File.open("\#{path}/another_implicit_rake_dep.rb", "w") do |f|
+ f.puts "ANOTHER_IMPLICIT_RAKE_DEP = 'YES'"
+ end
+ end
+ RUBY
+ end
+
+ # Test complicated gem dependencies for install
+ build_gem "net_a" do |s|
+ s.add_dependency "net_b"
+ s.add_dependency "net_build_extensions"
+ end
+
+ build_gem "net_b"
+
+ build_gem "net_build_extensions" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ File.open("\#{path}/net_build_extensions.rb", "w") do |f|
+ f.puts "NET_BUILD_EXTENSIONS = 'YES'"
+ end
+ end
+ RUBY
+ end
+
+ build_gem "net_c" do |s|
+ s.add_dependency "net_a"
+ s.add_dependency "net_d"
+ end
+
+ build_gem "net_d"
+
+ build_gem "net_e" do |s|
+ s.add_dependency "net_d"
+ end
+ end
+ end
+
+ it "installs gems with implicit rake dependencies" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_implicit_rake_dep"
+ gem "another_implicit_rake_dep"
+ gem "rake"
+ G
+
+ run <<-R
+ require 'implicit_rake_dep'
+ require 'another_implicit_rake_dep'
+ puts IMPLICIT_RAKE_DEP
+ puts ANOTHER_IMPLICIT_RAKE_DEP
+ R
+ expect(out).to eq("YES\nYES")
+ end
+
+ it "installs gems with implicit rake dependencies without rake previously installed" do
+ with_path_as("") do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_implicit_rake_dep"
+ gem "another_implicit_rake_dep"
+ gem "rake"
+ G
+ end
+
+ run <<-R
+ require 'implicit_rake_dep'
+ require 'another_implicit_rake_dep'
+ puts IMPLICIT_RAKE_DEP
+ puts ANOTHER_IMPLICIT_RAKE_DEP
+ R
+ expect(out).to eq("YES\nYES")
+ end
+
+ it "does not install gems with a dependency with no type" do
+ build_repo2
+
+ path = "#{gem_repo2}/#{Gem::MARSHAL_SPEC_DIR}/actionpack-2.3.2.gemspec.rz"
+ spec = Marshal.load(Bundler.rubygems.inflate(File.binread(path)))
+ spec.dependencies.each do |d|
+ d.instance_variable_set(:@type, "fail")
+ end
+ File.open(path, "wb") do |f|
+ f.write Gem.deflate(Marshal.dump(spec))
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "actionpack", "2.3.2"
+ G
+
+ expect(err).to include("Downloading actionpack-2.3.2 revealed dependencies not in the API (activesupport (= 2.3.2)).")
+
+ expect(the_bundle).not_to include_gems "actionpack 2.3.2", "activesupport 2.3.2"
+ end
+
+ describe "with crazy rubygem plugin stuff" do
+ it "installs plugins" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "net_b"
+ G
+
+ expect(the_bundle).to include_gems "net_b 1.0"
+ end
+
+ it "installs plugins depended on by other plugins" do
+ install_gemfile <<-G, env: { "DEBUG" => "1" }
+ source "https://gem.repo2"
+ gem "net_a"
+ G
+
+ expect(the_bundle).to include_gems "net_a 1.0", "net_b 1.0"
+ end
+
+ it "installs multiple levels of dependencies" do
+ install_gemfile <<-G, env: { "DEBUG" => "1" }
+ source "https://gem.repo2"
+ gem "net_c"
+ gem "net_e"
+ G
+
+ expect(the_bundle).to include_gems "net_a 1.0", "net_b 1.0", "net_c 1.0", "net_d 1.0", "net_e 1.0"
+ end
+
+ context "with ENV['BUNDLER_DEBUG_RESOLVER'] set" do
+ it "produces debug output" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "net_c"
+ gem "net_e"
+ G
+
+ bundle :install, env: { "BUNDLER_DEBUG_RESOLVER" => "1", "DEBUG" => "1" }
+
+ expect(out).to include("Resolving dependencies...")
+ end
+ end
+
+ context "with ENV['DEBUG_RESOLVER'] set" do
+ it "produces debug output" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "net_c"
+ gem "net_e"
+ G
+
+ bundle :install, env: { "DEBUG_RESOLVER" => "1", "DEBUG" => "1" }
+
+ expect(out).to include("Resolving dependencies...")
+ end
+ end
+
+ context "with ENV['DEBUG_RESOLVER_TREE'] set" do
+ it "produces debug output" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "net_c"
+ gem "net_e"
+ G
+
+ bundle :install, env: { "DEBUG_RESOLVER_TREE" => "1", "DEBUG" => "1" }
+
+ expect(out).to include(" net_b").
+ and include("Resolving dependencies...").
+ and include("Solution found after 1 attempts:").
+ and include("selected net_b 1.0")
+ end
+ end
+ end
+
+ describe "when a required ruby version" do
+ context "allows only an older version" do
+ it "installs the older version" do
+ build_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "myrack", "9001.0.0" do |s|
+ s.required_ruby_version = "> 9000"
+ end
+ end
+
+ install_gemfile <<-G
+ ruby "#{Gem.ruby_version}"
+ source "https://gem.repo2"
+ gem 'myrack'
+ G
+
+ expect(err).to_not include("myrack-9001.0.0 requires ruby version > 9000")
+ expect(the_bundle).to include_gems("myrack 1.2")
+ end
+
+ it "installs the older version when using servers not implementing the compact index API" do
+ build_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "myrack", "9001.0.0" do |s|
+ s.required_ruby_version = "> 9000"
+ end
+ end
+
+ install_gemfile <<-G, artifice: "endpoint"
+ ruby "#{Gem.ruby_version}"
+ source "https://gem.repo2"
+ gem 'myrack'
+ G
+
+ expect(err).to_not include("myrack-9001.0.0 requires ruby version > 9000")
+ expect(the_bundle).to include_gems("myrack 1.2")
+ end
+
+ context "when there is a lockfile using the newer incompatible version" do
+ before do
+ build_repo2 do
+ build_gem "parallel_tests", "3.7.0" do |s|
+ s.required_ruby_version = ">= #{current_ruby_minor}"
+ end
+
+ build_gem "parallel_tests", "3.8.0" do |s|
+ s.required_ruby_version = ">= #{next_ruby_minor}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem 'parallel_tests'
+ G
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo2, "parallel_tests", "3.8.0"
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ parallel_tests (3.8.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ parallel_tests
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically updates lockfile to use the older version" do
+ bundle "install --verbose"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "parallel_tests", "3.7.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ parallel_tests (3.7.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ parallel_tests
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "gives a meaningful error if we're in frozen mode" do
+ expect do
+ bundle "install", env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
+ end.not_to change { lockfile }
+
+ expect(err).to eq("parallel_tests-3.8.0 requires ruby version >= #{next_ruby_minor}, which is incompatible with the current version, #{Gem.ruby_version}")
+ end
+ end
+
+ context "with transitive dependencies in a lockfile" do
+ before do
+ build_repo2 do
+ build_gem "rubocop", "1.28.2" do |s|
+ s.required_ruby_version = ">= #{current_ruby_minor}"
+
+ s.add_dependency "rubocop-ast", ">= 1.17.0", "< 2.0"
+ end
+
+ build_gem "rubocop", "1.35.0" do |s|
+ s.required_ruby_version = ">= #{next_ruby_minor}"
+
+ s.add_dependency "rubocop-ast", ">= 1.20.1", "< 2.0"
+ end
+
+ build_gem "rubocop-ast", "1.17.0" do |s|
+ s.required_ruby_version = ">= #{current_ruby_minor}"
+ end
+
+ build_gem "rubocop-ast", "1.21.0" do |s|
+ s.required_ruby_version = ">= #{next_ruby_minor}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem 'rubocop'
+ G
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo2, "rubocop", "1.35.0"
+ c.checksum gem_repo2, "rubocop-ast", "1.21.0"
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ rubocop (1.35.0)
+ rubocop-ast (>= 1.20.1, < 2.0)
+ rubocop-ast (1.21.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ rubocop
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically updates lockfile to use the older compatible versions" do
+ bundle "install --verbose"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "rubocop", "1.28.2"
+ c.checksum gem_repo2, "rubocop-ast", "1.17.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ rubocop (1.28.2)
+ rubocop-ast (>= 1.17.0, < 2.0)
+ rubocop-ast (1.17.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ rubocop
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "with a Gemfile and lockfile that don't resolve under the current platform" do
+ before do
+ build_repo4 do
+ build_gem "sorbet", "0.5.10554" do |s|
+ s.add_dependency "sorbet-static", "0.5.10554"
+ end
+
+ build_gem "sorbet-static", "0.5.10554" do |s|
+ s.platform = "universal-darwin-21"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem 'sorbet', '= 0.5.10554'
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet (0.5.10554)
+ sorbet-static (= 0.5.10554)
+ sorbet-static (0.5.10554-universal-darwin-21)
+
+ PLATFORMS
+ arm64-darwin-21
+
+ DEPENDENCIES
+ sorbet (= 0.5.10554)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "raises a proper error" do
+ simulate_platform "aarch64-linux" do
+ bundle "install", raise_on_error: false
+ end
+
+ nice_error = <<~E.strip
+ Could not find gems matching 'sorbet-static (= 0.5.10554)' valid for all resolution platforms (arm64-darwin-21, aarch64-linux) in rubygems repository https://gem.repo4/ or installed locally.
+
+ The source contains the following gems matching 'sorbet-static (= 0.5.10554)':
+ * sorbet-static-0.5.10554-universal-darwin-21
+ E
+ expect(err).to include(nice_error)
+ expect(err).to include("Your current platform (aarch64-linux) is not included in the lockfile's platforms (arm64-darwin-21)")
+ expect(err).to include("bundle lock --add-platform aarch64-linux")
+ end
+ end
+
+ context "when adding a new gem that does not resolve under all locked platforms" do
+ before do
+ simulate_platform "x86_64-linux" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "arm-linux"
+ end
+
+ build_gem "sorbet-static", "0.5.10696" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.0-arm-linux)
+ nokogiri (1.14.0-x86_64-linux)
+
+ PLATFORMS
+ arm-linux
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ gem "sorbet-static"
+ G
+
+ bundle "lock", raise_on_error: false
+ end
+ end
+
+ it "raises a proper error" do
+ nice_error = <<~E.strip
+ Could not find gems matching 'sorbet-static' valid for all resolution platforms (arm-linux, x86_64-linux) in rubygems repository https://gem.repo4/ or installed locally.
+
+ The source contains the following gems matching 'sorbet-static':
+ * sorbet-static-0.5.10696-x86_64-linux
+ E
+ expect(err).to end_with(nice_error)
+ end
+ end
+
+ context "when locked generic variant supports current Ruby, but locked specific variant does not" do
+ let(:original_lockfile) do
+ <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.16.3)
+ nokogiri (1.16.3-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ before do
+ build_repo4 do
+ build_gem "nokogiri", "1.16.3"
+ build_gem "nokogiri", "1.16.3" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ lockfile original_lockfile
+ end
+
+ it "keeps both variants in the lockfile when installing, and uses the generic one since it's compatible" do
+ simulate_platform "x86_64-linux" do
+ bundle "install --verbose"
+
+ expect(lockfile).to eq(original_lockfile)
+ expect(the_bundle).to include_gems("nokogiri 1.16.3")
+ end
+ end
+
+ it "keeps both variants in the lockfile when updating, and uses the generic one since it's compatible" do
+ simulate_platform "x86_64-linux" do
+ bundle "update --verbose"
+
+ expect(lockfile).to eq(original_lockfile)
+ expect(the_bundle).to include_gems("nokogiri 1.16.3")
+ end
+ end
+ end
+
+ it "gives a meaningful error on ruby version mismatches between dependencies" do
+ build_repo4 do
+ build_gem "requires-old-ruby" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ build_lib("foo", path: bundled_app) do |s|
+ s.required_ruby_version = ">= #{Gem.ruby_version}"
+
+ s.add_dependency "requires-old-ruby"
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo4"
+ gemspec
+ G
+
+ expect(err).to end_with <<~E.strip
+ Could not find compatible versions
+
+ Because every version of foo depends on requires-old-ruby >= 0
+ and every version of requires-old-ruby depends on Ruby < #{Gem.ruby_version},
+ every version of foo requires Ruby < #{Gem.ruby_version}.
+ So, because Gemfile depends on foo >= 0
+ and current Ruby version is = #{Gem.ruby_version},
+ version solving has failed.
+ E
+ end
+
+ it "installs the older version under rate limiting conditions" do
+ build_repo4 do
+ build_gem "myrack", "9001.0.0" do |s|
+ s.required_ruby_version = "> 9000"
+ end
+ build_gem "myrack", "1.2"
+ build_gem "foo1", "1.0"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index_rate_limited"
+ ruby "#{Gem.ruby_version}"
+ source "https://gem.repo4"
+ gem 'myrack'
+ gem 'foo1'
+ G
+
+ expect(err).to_not include("myrack-9001.0.0 requires ruby version > 9000")
+ expect(the_bundle).to include_gems("myrack 1.2")
+ end
+
+ it "installs the older not platform specific version" do
+ build_repo4 do
+ build_gem "myrack", "9001.0.0" do |s|
+ s.required_ruby_version = "> 9000"
+ end
+ build_gem "myrack", "1.2" do |s|
+ s.platform = "x86-mingw32"
+ s.required_ruby_version = "> 9000"
+ end
+ build_gem "myrack", "1.2"
+ end
+
+ simulate_platform "x86-mingw32" do
+ install_gemfile <<-G, artifice: "compact_index"
+ ruby "#{Gem.ruby_version}"
+ source "https://gem.repo4"
+ gem 'myrack'
+ G
+ end
+
+ expect(err).to_not include("myrack-9001.0.0 requires ruby version > 9000")
+ expect(err).to_not include("myrack-1.2-#{Bundler.local_platform} requires ruby version > 9000")
+ expect(the_bundle).to include_gems("myrack 1.2")
+ end
+ end
+
+ context "allows no gems" do
+ before do
+ build_repo2 do
+ build_gem "require_ruby" do |s|
+ s.required_ruby_version = "> 9000"
+ end
+ end
+ end
+
+ let(:ruby_requirement) { %("#{Gem.ruby_version}") }
+ let(:error_message_requirement) { "= #{Gem.ruby_version}" }
+
+ it "raises a proper error that mentions the current Ruby version during resolution" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem 'require_ruby'
+ G
+
+ expect(out).to_not include("Gem::InstallError: require_ruby requires Ruby version > 9000")
+
+ nice_error = <<~E.strip
+ Could not find compatible versions
+
+ Because every version of require_ruby depends on Ruby > 9000
+ and Gemfile depends on require_ruby >= 0,
+ Ruby > 9000 is required.
+ So, because current Ruby version is #{error_message_requirement},
+ version solving has failed.
+ E
+ expect(err).to end_with(nice_error)
+ end
+
+ shared_examples_for "ruby version conflicts" do
+ it "raises an error during resolution" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ ruby #{ruby_requirement}
+ gem 'require_ruby'
+ G
+
+ expect(out).to_not include("Gem::InstallError: require_ruby requires Ruby version > 9000")
+
+ nice_error = <<~E.strip
+ Could not find compatible versions
+
+ Because every version of require_ruby depends on Ruby > 9000
+ and Gemfile depends on require_ruby >= 0,
+ Ruby > 9000 is required.
+ So, because current Ruby version is #{error_message_requirement},
+ version solving has failed.
+ E
+ expect(err).to end_with(nice_error)
+ end
+ end
+
+ it_behaves_like "ruby version conflicts"
+
+ describe "with a < requirement" do
+ let(:ruby_requirement) { %("< 5000") }
+
+ it_behaves_like "ruby version conflicts"
+ end
+
+ describe "with a compound requirement" do
+ let(:reqs) { ["> 0.1", "< 5000"] }
+ let(:ruby_requirement) { reqs.map(&:dump).join(", ") }
+
+ it_behaves_like "ruby version conflicts"
+ end
+ end
+ end
+
+ describe "when a required rubygems version disallows a gem" do
+ it "does not try to install those gems" do
+ build_repo2 do
+ build_gem "require_rubygems" do |s|
+ s.required_rubygems_version = "> 9000"
+ end
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem 'require_rubygems'
+ G
+
+ expect(err).to_not include("Gem::InstallError: require_rubygems requires RubyGems version > 9000")
+ nice_error = <<~E.strip
+ Because every version of require_rubygems depends on RubyGems > 9000
+ and Gemfile depends on require_rubygems >= 0,
+ RubyGems > 9000 is required.
+ So, because current RubyGems version is = #{Gem::VERSION},
+ version solving has failed.
+ E
+ expect(err).to end_with(nice_error)
+ end
+ end
+
+ context "when non platform specific gems bring more dependencies", :truffleruby_only do
+ before do
+ build_repo4 do
+ build_gem "foo", "1.0" do |s|
+ s.add_dependency "bar"
+ end
+
+ build_gem "foo", "2.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+
+ build_gem "bar"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "foo"
+ G
+ end
+
+ it "locks both ruby and current platform, and resolve to ruby variants that install on truffleruby" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "foo", "1.0"
+ c.checksum gem_repo4, "bar", "1.0"
+ end
+
+ simulate_platform "x86_64-linux" do
+ bundle "install"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ bar (1.0)
+ foo (1.0)
+ bar
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ foo
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/standalone_spec.rb b/spec/bundler/install/gems/standalone_spec.rb
new file mode 100644
index 0000000000..96a305bb76
--- /dev/null
+++ b/spec/bundler/install/gems/standalone_spec.rb
@@ -0,0 +1,524 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install --standalone" do
+ shared_examples "common functionality" do
+ it "still makes the gems available to normal bundler" do
+ args = expected_gems.map {|k, v| "#{k} #{v}" }
+ expect(the_bundle).to include_gems(*args)
+ end
+
+ it "still makes system gems unavailable to normal bundler" do
+ system_gems "myrack-1.0.0"
+
+ expect(the_bundle).to_not include_gems("myrack")
+ end
+
+ it "generates a bundle/bundler/setup.rb" do
+ expect(bundled_app("bundle/bundler/setup.rb")).to exist
+ end
+
+ it "makes the gems available without bundler" do
+ testrb = String.new <<-RUBY
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ RUBY
+ expected_gems.each do |k, _|
+ testrb << "\nrequire \"#{k}\""
+ testrb << "\nputs #{k.upcase}"
+ end
+ ruby testrb
+
+ expect(out).to eq(expected_gems.values.join("\n"))
+ end
+
+ it "makes the gems available without bundler nor rubygems" do
+ testrb = String.new <<-RUBY
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ RUBY
+ expected_gems.each do |k, _|
+ testrb << "\nrequire \"#{k}\""
+ testrb << "\nputs #{k.upcase}"
+ end
+ in_bundled_app %(#{Gem.ruby} --disable-gems -w -e #{testrb.shellescape})
+
+ expect(out).to eq(expected_gems.values.join("\n"))
+ end
+
+ it "makes the gems available without bundler via Kernel.require" do
+ testrb = String.new <<-RUBY
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ RUBY
+ expected_gems.each do |k, _|
+ testrb << "\nKernel.require \"#{k}\""
+ testrb << "\nputs #{k.upcase}"
+ end
+ ruby testrb
+
+ expect(out).to eq(expected_gems.values.join("\n"))
+ end
+
+ it "makes system gems unavailable without bundler" do
+ system_gems "myrack-1.0.0"
+
+ testrb = String.new <<-RUBY
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ begin
+ require "myrack"
+ rescue LoadError
+ puts "LoadError"
+ end
+ RUBY
+ ruby testrb
+
+ expect(out).to eq("LoadError")
+ end
+
+ it "works on a different system" do
+ begin
+ FileUtils.mv(bundled_app, "#{bundled_app}2")
+ rescue Errno::ENOTEMPTY
+ puts "Couldn't rename test app since the target folder has these files: #{Dir.glob("#{bundled_app}2/*")}"
+ raise
+ end
+
+ testrb = String.new <<-RUBY
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ RUBY
+ expected_gems.each do |k, _|
+ testrb << "\nrequire \"#{k}\""
+ testrb << "\nputs #{k.upcase}"
+ end
+ ruby testrb, dir: "#{bundled_app}2"
+
+ expect(out).to eq(expected_gems.values.join("\n"))
+ end
+
+ it "skips activating gems" do
+ testrb = String.new <<-RUBY
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ gem "do_not_activate_me"
+ RUBY
+ expected_gems.each do |k, _|
+ testrb << "\nrequire \"#{k}\""
+ testrb << "\nputs #{k.upcase}"
+ end
+ in_bundled_app %(#{Gem.ruby} -w -e #{testrb.shellescape})
+
+ expect(out).to eq(expected_gems.values.join("\n"))
+ end
+ end
+
+ describe "with simple gems" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+ bundle_config "path #{bundled_app("bundle")}"
+ bundle :install, standalone: true, dir: cwd
+ end
+
+ let(:expected_gems) do
+ {
+ "actionpack" => "2.3.2",
+ "rails" => "2.3.2",
+ }
+ end
+
+ include_examples "common functionality"
+ end
+
+ describe "with default gems and a lockfile", :ruby_repo do
+ it "works and points to the vendored copies, not to the default copies" do
+ base_system_gems "stringio", "psych", "etc", path: scoped_gem_path(bundled_app("bundle"))
+
+ build_gem "foo", "1.0.0", to_system: true, default: true do |s|
+ s.add_dependency "bar"
+ end
+
+ build_gem "bar", "1.0.0", to_system: true, default: true
+
+ build_repo4 do
+ build_gem "foo", "1.0.0" do |s|
+ s.add_dependency "bar"
+ end
+
+ build_gem "bar", "1.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "foo"
+ G
+
+ bundle "lock", dir: cwd
+
+ bundle_config "path #{bundled_app("bundle")}"
+
+ bundle :install, standalone: true, dir: cwd, env: { "BUNDLER_GEM_DEFAULT_DIR" => system_gem_path.to_s }
+
+ load_path_lines = bundled_app("bundle/bundler/setup.rb").read.split("\n").select {|line| line.start_with?("$:.unshift") }
+
+ expect(load_path_lines).to eq [
+ '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/bar-1.0.0/lib")',
+ '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/foo-1.0.0/lib")',
+ ]
+ end
+
+ it "works for gems with extensions and points to the vendored copies, not to the default copies" do
+ simulate_platform "arm64-darwin-23" do
+ base_system_gems "stringio", "psych", "etc", "shellwords", "open3", path: scoped_gem_path(bundled_app("bundle"))
+
+ build_gem "baz", "1.0.0", to_system: true, default: true, &:add_c_extension
+
+ build_repo4 do
+ build_gem "baz", "1.0.0", &:add_c_extension
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "baz"
+ G
+
+ bundle_config "path #{bundled_app("bundle")}"
+
+ bundle "lock", dir: cwd
+
+ bundle :install, standalone: true, dir: cwd, env: { "BUNDLER_GEM_DEFAULT_DIR" => system_gem_path.to_s }
+ end
+
+ load_path_lines = bundled_app("bundle/bundler/setup.rb").read.split("\n").select {|line| line.start_with?("$:.unshift") }
+
+ expect(load_path_lines).to eq [
+ '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/extensions/arm64-darwin-23/#{Gem.extension_api_version}/baz-1.0.0")',
+ '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/baz-1.0.0/lib")',
+ ]
+ end
+ end
+
+ describe "with Gemfiles using absolute path sources and resulting bundle moved to a folder hierarchy with different nesting" do
+ before do
+ build_lib "minitest", "1.0.0", path: lib_path("minitest")
+
+ Dir.mkdir bundled_app("app")
+
+ gemfile bundled_app("app/Gemfile"), <<-G
+ source "https://gem.repo1"
+ gem "minitest", :path => "#{lib_path("minitest")}"
+ G
+
+ bundle "install", standalone: true, dir: bundled_app("app")
+
+ Dir.mkdir tmp("one_more_level")
+ FileUtils.mv bundled_app, tmp("one_more_level")
+ end
+
+ it "also works" do
+ ruby <<-RUBY, dir: tmp("one_more_level/bundled_app/app")
+ require "./bundle/bundler/setup"
+
+ require "minitest"
+ puts MINITEST
+ RUBY
+
+ expect(out).to eq("1.0.0")
+ expect(err).to be_empty
+ end
+ end
+
+ let(:cwd) { bundled_app }
+
+ describe "with Gemfiles using relative path sources and app moved to a different root" do
+ before do
+ FileUtils.mkdir_p bundled_app("app/vendor")
+
+ build_lib "minitest", "1.0.0", path: bundled_app("app/vendor/minitest")
+
+ gemfile bundled_app("app/Gemfile"), <<-G
+ source "https://gem.repo1"
+ gem "minitest", :path => "vendor/minitest"
+ G
+
+ bundle "install", standalone: true, dir: bundled_app("app")
+
+ FileUtils.mv(bundled_app("app"), bundled_app2("app"))
+ end
+
+ it "also works" do
+ ruby <<-RUBY, dir: bundled_app2("app")
+ require "./bundle/bundler/setup"
+
+ require "minitest"
+ puts MINITEST
+ RUBY
+
+ expect(out).to eq("1.0.0")
+ expect(err).to be_empty
+ end
+ end
+
+ describe "with gems with native extension" do
+ before do
+ bundle_config "path #{bundled_app("bundle")}"
+ install_gemfile <<-G, standalone: true, dir: cwd
+ source "https://gem.repo1"
+ gem "very_simple_binary"
+ G
+ end
+
+ it "generates a bundle/bundler/setup.rb with the proper paths" do
+ expected_path = bundled_app("bundle/bundler/setup.rb")
+ script_content = File.read(expected_path)
+ expect(script_content).to include("def self.ruby_api_version")
+ expect(script_content).to include("def self.extension_api_version")
+ extension_line = script_content.each_line.find {|line| line.include? "/extensions/" }.strip
+ platform = Gem::Platform.local
+ expect(extension_line).to start_with '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/extensions/'
+ expect(extension_line).to end_with platform.to_s + '/#{Gem.extension_api_version}/very_simple_binary-1.0")'
+ end
+ end
+
+ describe "with gem that has an invalid gemspec" do
+ before do
+ build_git "bar", gemspec: false do |s|
+ s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0')
+ s.write "bar.gemspec", <<-G
+ lib = File.expand_path('lib/', __dir__)
+ $:.unshift lib unless $:.include?(lib)
+ require 'bar/version'
+
+ Gem::Specification.new do |s|
+ s.name = 'bar'
+ s.version = BAR_VERSION
+ s.summary = 'Bar'
+ s.files = Dir["lib/**/*.rb"]
+ s.author = 'Anonymous'
+ s.require_path = [1,2]
+ end
+ G
+ end
+ bundle_config "path #{bundled_app("bundle")}"
+ install_gemfile <<-G, standalone: true, dir: cwd, raise_on_error: false
+ source "https://gem.repo1"
+ gem "bar", :git => "#{lib_path("bar-1.0")}"
+ G
+ end
+
+ it "outputs a helpful error message" do
+ expect(err).to include("You have one or more invalid gemspecs that need to be fixed.")
+ expect(err).to include("bar.gemspec is not valid")
+ end
+ end
+
+ describe "with a combination of gems and git repos" do
+ before do
+ build_git "devise", "1.0"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ gem "devise", :git => "#{lib_path("devise-1.0")}"
+ G
+ bundle_config "path #{bundled_app("bundle")}"
+ bundle :install, standalone: true, dir: cwd
+ end
+
+ let(:expected_gems) do
+ {
+ "actionpack" => "2.3.2",
+ "devise" => "1.0",
+ "rails" => "2.3.2",
+ }
+ end
+
+ include_examples "common functionality"
+ end
+
+ describe "with groups" do
+ before do
+ build_git "devise", "1.0"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+
+ group :test do
+ gem "rspec"
+ gem "myrack-test"
+ end
+ G
+ bundle_config "path #{bundled_app("bundle")}"
+ bundle :install, standalone: true, dir: cwd
+ end
+
+ let(:expected_gems) do
+ {
+ "actionpack" => "2.3.2",
+ "rails" => "2.3.2",
+ }
+ end
+
+ include_examples "common functionality"
+
+ it "allows creating a standalone file with limited groups" do
+ bundle_config "path #{bundled_app("bundle")}"
+ bundle :install, standalone: "default", dir: cwd
+
+ load_error_ruby <<-RUBY, "spec"
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ require "actionpack"
+ puts ACTIONPACK
+ require "spec"
+ RUBY
+
+ expect(out).to eq("2.3.2")
+ expect(err_without_deprecations).to match(/cannot load such file -- spec/)
+ end
+
+ it "allows `without` configuration to limit the groups used in a standalone" do
+ bundle_config "path #{bundled_app("bundle")}"
+ bundle_config "without test"
+ bundle :install, standalone: true, dir: cwd
+
+ load_error_ruby <<-RUBY, "spec"
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ require "actionpack"
+ puts ACTIONPACK
+ require "spec"
+ RUBY
+
+ expect(out).to eq("2.3.2")
+ expect(err_without_deprecations).to match(/cannot load such file -- spec/)
+ end
+
+ it "allows `path` configuration to change the location of the standalone bundle" do
+ bundle_config "path path/to/bundle"
+ bundle "install", standalone: true, dir: cwd
+
+ ruby <<-RUBY
+ $:.unshift File.expand_path("path/to/bundle")
+ require "bundler/setup"
+
+ require "actionpack"
+ puts ACTIONPACK
+ RUBY
+
+ expect(out).to eq("2.3.2")
+ end
+
+ it "allows `without` to limit the groups used in a standalone" do
+ bundle_config "without test"
+ bundle :install, dir: cwd
+ bundle_config "path #{bundled_app("bundle")}"
+ bundle :install, standalone: true, dir: cwd
+
+ load_error_ruby <<-RUBY, "spec"
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ require "actionpack"
+ puts ACTIONPACK
+ require "spec"
+ RUBY
+
+ expect(out).to eq("2.3.2")
+ expect(err_without_deprecations).to match(/cannot load such file -- spec/)
+ end
+ end
+
+ describe "with gemcutter's dependency API" do
+ let(:source_uri) { "http://localgemserver.test" }
+
+ describe "simple gems" do
+ before do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "rails"
+ G
+ bundle_config "path #{bundled_app("bundle")}"
+ bundle :install, standalone: true, artifice: "endpoint", dir: cwd
+ end
+
+ let(:expected_gems) do
+ {
+ "actionpack" => "2.3.2",
+ "rails" => "2.3.2",
+ }
+ end
+
+ include_examples "common functionality"
+ end
+ end
+end
+
+RSpec.describe "bundle install --standalone run in a subdirectory" do
+ let(:cwd) { bundled_app("bob").tap(&:mkpath) }
+
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+ end
+
+ it "generates the script in the proper place" do
+ bundle :install, standalone: true, dir: cwd
+
+ expect(bundled_app("bundle/bundler/setup.rb")).to exist
+ end
+
+ context "when path set to a relative path" do
+ before do
+ bundle_config "path bundle"
+ end
+
+ it "generates the script in the proper place" do
+ bundle :install, standalone: true, dir: cwd
+
+ expect(bundled_app("bundle/bundler/setup.rb")).to exist
+ end
+ end
+end
+
+RSpec.describe "bundle install --standalone --local" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ system_gems "myrack-1.0.0", path: default_bundle_path
+ end
+
+ it "generates script pointing to system gems" do
+ bundle "install --standalone --local --verbose"
+
+ expect(out).to include("Using myrack 1.0.0")
+
+ load_error_ruby <<-RUBY, "spec"
+ require "./bundler/setup"
+
+ require "myrack"
+ puts MYRACK
+ require "spec"
+ RUBY
+
+ expect(out).to eq("1.0.0")
+ expect(err_without_deprecations).to match(/cannot load such file -- spec/)
+ end
+end
diff --git a/spec/bundler/install/gems/win32_spec.rb b/spec/bundler/install/gems/win32_spec.rb
new file mode 100644
index 0000000000..be37673aa1
--- /dev/null
+++ b/spec/bundler/install/gems/win32_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with win32-generated lockfile" do
+ it "should read lockfile" do
+ File.open(bundled_app_lock, "wb") do |f|
+ f << "GEM\r\n"
+ f << " remote: https://gem.repo1/\r\n"
+ f << " specs:\r\n"
+ f << "\r\n"
+ f << " myrack (1.0.0)\r\n"
+ f << "\r\n"
+ f << "PLATFORMS\r\n"
+ f << " ruby\r\n"
+ f << "\r\n"
+ f << "DEPENDENCIES\r\n"
+ f << " myrack\r\n"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+ end
+end
diff --git a/spec/bundler/install/gemspecs_spec.rb b/spec/bundler/install/gemspecs_spec.rb
new file mode 100644
index 0000000000..fb2271c830
--- /dev/null
+++ b/spec/bundler/install/gemspecs_spec.rb
@@ -0,0 +1,179 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ describe "when a gem has a YAML gemspec" do
+ before :each do
+ build_repo2 do
+ build_gem "yaml_spec", gemspec: :yaml
+ end
+ end
+
+ it "still installs correctly" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "yaml_spec"
+ G
+ bundle :install
+ expect(err).to be_empty
+ end
+
+ it "still installs correctly when using path" do
+ build_lib "yaml_spec", gemspec: :yaml
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'yaml_spec', :path => "#{lib_path("yaml_spec-1.0")}"
+ G
+ expect(err).to be_empty
+ end
+ end
+
+ it "should use gemspecs in the system cache when available" do
+ gemfile <<-G
+ source "http://localgemserver.test"
+ gem 'myrack'
+ G
+
+ system_gems "myrack-1.0.0", path: default_bundle_path
+
+ FileUtils.mkdir_p "#{default_bundle_path}/specifications"
+ File.open("#{default_bundle_path}/specifications/myrack-1.0.0.gemspec", "w+") do |f|
+ spec = Gem::Specification.new do |s|
+ s.name = "myrack"
+ s.version = "1.0.0"
+ s.add_dependency "activesupport", "2.3.2"
+ end
+ f.write spec.to_ruby
+ end
+ bundle :install, artifice: "endpoint_marshal_fail" # force gemspec load
+ expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.2"
+ end
+
+ it "does not hang when gemspec has incompatible encoding" do
+ create_file("foo.gemspec", <<-G)
+ Gem::Specification.new do |gem|
+ gem.name = "pry-byebug"
+ gem.version = "3.4.2"
+ gem.author = "David Rodríguez"
+ gem.summary = "Good stuff"
+ end
+ G
+
+ install_gemfile <<-G, env: { "LANG" => "C" }
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "reads gemspecs respecting their encoding" do
+ create_file "version.rb", <<-RUBY
+ module Persistent💎
+ VERSION = "0.0.1"
+ end
+ RUBY
+
+ create_file "persistent-dmnd.gemspec", <<-G
+ require_relative "version"
+
+ Gem::Specification.new do |gem|
+ gem.name = "persistent-dmnd"
+ gem.version = Persistent💎::VERSION
+ gem.author = "Ivo Anjo"
+ gem.summary = "Unscratchable stuff"
+ end
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ expect(out).to include("Bundle complete!")
+ end
+
+ context "when ruby version is specified in gemspec and gemfile" do
+ it "installs when patch level is not specified and the version matches",
+ if: RUBY_PATCHLEVEL >= 0 do
+ build_lib("foo", path: bundled_app) do |s|
+ s.required_ruby_version = "~> #{RUBY_VERSION}.0"
+ end
+
+ install_gemfile <<-G
+ ruby '#{RUBY_VERSION}', :engine_version => '#{RUBY_VERSION}', :engine => 'ruby'
+ source "https://gem.repo1"
+ gemspec
+ G
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "installs when patch level is specified and the version still matches the current version",
+ if: RUBY_PATCHLEVEL >= 0 do
+ build_lib("foo", path: bundled_app) do |s|
+ s.required_ruby_version = "#{RUBY_VERSION}.#{RUBY_PATCHLEVEL}"
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ ruby '#{RUBY_VERSION}', :engine_version => '#{RUBY_VERSION}', :engine => 'ruby', :patchlevel => '#{RUBY_PATCHLEVEL}'
+ source "https://gem.repo1"
+ gemspec
+ G
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "installs gems ignoring the mismatch even when patchlevel is mismatch",
+ if: RUBY_PATCHLEVEL >= 0 do
+ patchlevel = RUBY_PATCHLEVEL.to_i + 1
+ build_lib("foo", path: bundled_app) do |s|
+ s.required_ruby_version = "#{RUBY_VERSION}.#{patchlevel}"
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ ruby '#{RUBY_VERSION}', :engine_version => '#{RUBY_VERSION}', :engine => 'ruby', :patchlevel => '#{patchlevel}'
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "fails and complains about version on version mismatch" do
+ version = Gem::Requirement.create(RUBY_VERSION).requirements.first.last.bump.version
+
+ build_lib("foo", path: bundled_app) do |s|
+ s.required_ruby_version = version
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ ruby '#{version}', :engine_version => '#{version}', :engine => 'ruby'
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ expect(err).to include("Ruby version")
+ expect(err).to include("but your Gemfile specified")
+ expect(exitstatus).to eq(18)
+ end
+
+ it "validates gemspecs just once when everything installed and lockfile up to date" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec path: "#{lib_path("foo-1.0")}"
+
+ module Monkey
+ def validate(spec)
+ puts "Validate called on \#{spec.full_name}"
+ end
+ end
+ Bundler.rubygems.extend(Monkey)
+ G
+
+ bundle "install"
+
+ expect(out).to include("Validate called on foo-1.0").once
+ end
+ end
+end
diff --git a/spec/bundler/install/git_spec.rb b/spec/bundler/install/git_spec.rb
new file mode 100644
index 0000000000..1172d661ae
--- /dev/null
+++ b/spec/bundler/install/git_spec.rb
@@ -0,0 +1,369 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ context "git sources" do
+ it "displays the revision hash of the gem repository" do
+ build_git "foo", "1.0", path: lib_path("foo")
+
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}"
+ G
+
+ expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at main@#{revision_for(lib_path("foo"))[0..6]})")
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+ end
+
+ it "displays the revision hash of the gem repository when passed a relative local path" do
+ build_git "foo", "1.0", path: lib_path("foo")
+
+ relative_path = lib_path("foo").relative_path_from(bundled_app)
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo1"
+ gem "foo", :git => "#{relative_path}"
+ G
+
+ expect(out).to include("Using foo 1.0 from #{relative_path} (at main@#{revision_for(lib_path("foo"))[0..6]})")
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+ end
+
+ it "displays the correct default branch", git: ">= 2.28.0" do
+ build_git "foo", "1.0", path: lib_path("foo"), default_branch: "non-standard"
+
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}"
+ G
+
+ expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at non-standard@#{revision_for(lib_path("foo"))[0..6]})")
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+ end
+
+ it "displays the ref of the gem repository when using branch~num as a ref" do
+ skip "maybe branch~num notation doesn't work on Windows' git" if Gem.win_platform?
+
+ build_git "foo", "1.0", path: lib_path("foo")
+ rev = revision_for(lib_path("foo"))[0..6]
+ update_git "foo", "2.0", path: lib_path("foo"), gemspec: true
+ rev2 = revision_for(lib_path("foo"))[0..6]
+ update_git "foo", "3.0", path: lib_path("foo"), gemspec: true
+
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}", :ref => "main~2"
+ G
+
+ expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at main~2@#{rev})")
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+
+ update_git "foo", "4.0", path: lib_path("foo"), gemspec: true
+
+ bundle :update, all: true, verbose: true
+ expect(out).to include("Using foo 2.0 (was 1.0) from #{lib_path("foo")} (at main~2@#{rev2})")
+ expect(the_bundle).to include_gems "foo 2.0", source: "git@#{lib_path("foo")}"
+ end
+
+ it "allows git repos that are missing but not being installed" do
+ revision = build_git("foo").ref_for("HEAD")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :group => :development
+ G
+
+ lockfile <<-L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{revision}
+ specs:
+ foo (1.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ foo!
+ L
+
+ bundle_config "path vendor/bundle"
+ bundle_config "without development"
+ bundle :install
+
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "allows multiple gems from the same git source" do
+ build_repo2 do
+ build_lib "foo", "1.0", path: lib_path("gems/foo")
+ build_lib "zebra", "2.0", path: lib_path("gems/zebra")
+ build_git "gems", path: lib_path("gems"), gemspec: false
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "foo", :git => "#{lib_path("gems")}", :glob => "foo/*.gemspec"
+ gem "zebra", :git => "#{lib_path("gems")}", :glob => "zebra/*.gemspec"
+ G
+
+ bundle "info foo"
+ expect(out).to include("* foo (1.0 #{revision_for(lib_path("gems"))[0..6]})")
+
+ bundle "info zebra"
+ expect(out).to include("* zebra (2.0 #{revision_for(lib_path("gems"))[0..6]})")
+ end
+
+ it "should always sort dependencies in the same order" do
+ # This Gemfile + lockfile had a problem where the first
+ # `bundle install` would change the order, but the second would
+ # change it back.
+
+ # NOTE: both gems MUST have the same path! It has to be two gems in one repo.
+
+ test = build_git "test", "1.0.0", path: lib_path("test-and-other")
+ other = build_git "other", "1.0.0", path: lib_path("test-and-other")
+ test_ref = test.ref_for("HEAD")
+ other_ref = other.ref_for("HEAD")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "test", git: #{test.path.to_s.inspect}
+ gem "other", ref: #{other_ref.inspect}, git: #{other.path.to_s.inspect}
+ G
+
+ lockfile <<-L
+ GIT
+ remote: #{test.path}
+ revision: #{test_ref}
+ specs:
+ test (1.0.0)
+
+ GIT
+ remote: #{other.path}
+ revision: #{other_ref}
+ ref: #{other_ref}
+ specs:
+ other (1.0.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ other!
+ test!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ # If GH#6743 is present, the first `bundle install` will change the
+ # lockfile, by flipping the order (`other` would be moved to the top).
+ #
+ # The second `bundle install` would then change the lockfile back
+ # to the original.
+ #
+ # The fix makes it so it may change it once, but it will not change
+ # it a second time.
+ #
+ # So, we run `bundle install` once, and store the value of the
+ # modified lockfile.
+ bundle :install
+ modified_lockfile = lockfile
+
+ # If GH#6743 is present, the second `bundle install` would change the
+ # lockfile back to what it was originally.
+ #
+ # This `expect` makes sure it doesn't change a second time.
+ bundle :install
+ expect(lockfile).to eq(modified_lockfile)
+
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "allows older revisions of git source when clean true" do
+ build_git "foo", "1.0", path: lib_path("foo")
+ rev = revision_for(lib_path("foo"))
+
+ bundle_config "path vendor/bundle"
+ bundle_config "clean true"
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}"
+ G
+
+ expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at main@#{rev[0..6]})")
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+
+ old_lockfile = lockfile
+
+ update_git "foo", "2.0", path: lib_path("foo"), gemspec: true
+ rev2 = revision_for(lib_path("foo"))
+
+ bundle :update, all: true, verbose: true
+ expect(out).to include("Using foo 2.0 (was 1.0) from #{lib_path("foo")} (at main@#{rev2[0..6]})")
+ expect(out).to include("Removing foo (#{rev[0..11]})")
+ expect(the_bundle).to include_gems "foo 2.0", source: "git@#{lib_path("foo")}"
+
+ lockfile(old_lockfile)
+
+ bundle :install, verbose: true
+ expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at main@#{rev[0..6]})")
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+ end
+
+ context "when install directory exists" do
+ let(:checkout_confirmation_log_message) { "Checking out revision" }
+ let(:using_foo_confirmation_log_message) { "Using foo 1.0 from #{lib_path("foo")} (at main@#{revision_for(lib_path("foo"))[0..6]})" }
+
+ context "and no contents besides .git directory are present" do
+ it "reinstalls gem" do
+ build_git "foo", "1.0", path: lib_path("foo")
+
+ gemfile = <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}"
+ G
+
+ install_gemfile gemfile, verbose: true
+
+ expect(out).to include(checkout_confirmation_log_message)
+ expect(out).to include(using_foo_confirmation_log_message)
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+
+ # validate that the installed directory exists and has some expected contents
+ install_directory = default_bundle_path("bundler/gems/foo-#{revision_for(lib_path("foo"))[0..11]}")
+ dot_git_directory = install_directory.join(".git")
+ lib_directory = install_directory.join("lib")
+ gemspec = install_directory.join("foo.gemspec")
+ expect([install_directory, dot_git_directory, lib_directory, gemspec]).to all exist
+
+ # remove all elements in the install directory except .git directory
+ FileUtils.rm_r(lib_directory)
+ gemspec.delete
+
+ expect(dot_git_directory).to exist
+ expect(lib_directory).not_to exist
+ expect(gemspec).not_to exist
+
+ # rerun bundle install
+ install_gemfile gemfile, verbose: true
+
+ expect(out).to include(checkout_confirmation_log_message)
+ expect(out).to include(using_foo_confirmation_log_message)
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+
+ # validate that it reinstalls all components
+ expect([install_directory, dot_git_directory, lib_directory, gemspec]).to all exist
+ end
+ end
+
+ context "and contents besides .git directory are present" do
+ # we want to confirm that the change to try to detect partial installs and reinstall does not
+ # result in repeatedly reinstalling the gem when it is fully installed
+ it "does not reinstall gem" do
+ build_git "foo", "1.0", path: lib_path("foo")
+
+ gemfile = <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}"
+ G
+
+ install_gemfile gemfile, verbose: true
+
+ expect(out).to include(checkout_confirmation_log_message)
+ expect(out).to include(using_foo_confirmation_log_message)
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+
+ # rerun bundle install
+ install_gemfile gemfile, verbose: true
+
+ # it isn't altogether straight-forward to validate that bundle didn't do soething on the second run, however,
+ # the presence of the 2nd log message confirms install got past the point that it would have logged the above if
+ # it was going to
+ expect(out).not_to include(checkout_confirmation_log_message)
+ expect(out).to include(using_foo_confirmation_log_message)
+ end
+ end
+ end
+ end
+
+ describe "with excluded groups" do
+ it "works if you exclude a group with a git gem", ruby: ">= 3.3" do
+ build_git "production_gem", "1.0"
+ build_git "development_gem", "1.0"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "production_gem", :git => "#{lib_path("production_gem-1.0")}"
+
+ group :development do
+ gem "development_gem", :git => "#{lib_path("development_gem-1.0")}"
+ end
+ G
+
+ # First install all groups to create lockfile
+ bundle :install
+
+ # Set without and reinstall
+ bundle_config "without development"
+ bundle :install
+
+ # Verify only production gem is available
+ expect(the_bundle).to include_gems("production_gem 1.0")
+ expect(the_bundle).not_to include_gems("development_gem 1.0")
+ end
+
+ it "resolves indirect dependencies from a git source not in the requested groups" do
+ build_lib "activesupport", "1.0", path: lib_path("rails/activesupport")
+ build_git "activerecord", "1.0", path: lib_path("rails") do |s|
+ s.add_dependency "activesupport", "= 1.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "activerecord", :git => "#{lib_path("rails")}"
+
+ group :ci do
+ gem "myrack"
+ end
+ G
+
+ bundle_config "only ci"
+ bundle :install
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ expect(the_bundle).not_to include_gems("activerecord 1.0")
+ end
+
+ it "resolves indirect dependencies from a git source not in the requested groups (without compact_index dependency API)" do
+ build_lib "activesupport", "1.0", path: lib_path("rails/activesupport")
+ build_git "activerecord", "1.0", path: lib_path("rails") do |s|
+ s.add_dependency "activesupport", "= 1.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "activerecord", :git => "#{lib_path("rails")}"
+
+ group :ci do
+ gem "myrack"
+ end
+ G
+
+ # Force the RubygemsAggregate code path in find_source_requirements by
+ # making the dependency API unavailable.
+ bundle_config "only ci"
+ bundle :install, artifice: "endpoint_api_forbidden"
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ expect(the_bundle).not_to include_gems("activerecord 1.0")
+ end
+ end
+end
diff --git a/spec/bundler/install/global_cache_spec.rb b/spec/bundler/install/global_cache_spec.rb
new file mode 100644
index 0000000000..4cffa65b2a
--- /dev/null
+++ b/spec/bundler/install/global_cache_spec.rb
@@ -0,0 +1,305 @@
+# frozen_string_literal: true
+
+RSpec.describe "global gem caching" do
+ # Uses subprocess because this setting must apply across multiple app directories (bundled_app and bundled_app2)
+ before { bundle "config set global_gem_cache true" }
+
+ describe "using the cross-application user cache" do
+ let(:source) { "http://localgemserver.test" }
+ let(:source2) { "http://gemserver.example.org" }
+
+ def cache_base
+ # Use the unified global gem cache path if available (from RubyGems),
+ # otherwise fall back to the Bundler-specific cache location
+ if Gem.respond_to?(:global_gem_cache_path)
+ Pathname.new(Gem.global_gem_cache_path)
+ else
+ home(".bundle", "cache", "gems")
+ end
+ end
+
+ def source_global_cache(*segments)
+ cache_base.join("localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", *segments)
+ end
+
+ def source2_global_cache(*segments)
+ cache_base.join("gemserver.example.org.80.1ae1663619ffe0a3c9d97712f44c705b", *segments)
+ end
+
+ it "caches gems into the global cache on download" do
+ install_gemfile <<-G, artifice: "compact_index"
+ source "#{source}"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ end
+
+ it "uses globally cached gems if they exist" do
+ source_global_cache.mkpath
+ FileUtils.cp(gem_repo1("gems/myrack-1.0.0.gem"), source_global_cache("myrack-1.0.0.gem"))
+
+ install_gemfile <<-G, artifice: "compact_index_no_gem"
+ source "#{source}"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "shows a proper error message if a cached gem is corrupted" do
+ skip "This example is not working on ruby/ruby repo" if ruby_core?
+
+ source_global_cache.mkpath
+ FileUtils.touch(source_global_cache("myrack-1.0.0.gem"))
+
+ install_gemfile <<-G, artifice: "compact_index_no_gem", raise_on_error: false
+ source "#{source}"
+ gem "myrack"
+ G
+
+ expect(err).to include("Gem::Package::FormatError: package metadata is missing in #{source_global_cache("myrack-1.0.0.gem")}")
+ end
+
+ it "uses a shorter path for the cache to not hit filesystem limits" do
+ install_gemfile <<-G, artifice: "compact_index", verbose: true
+ source "http://#{"a" * 255}.test"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ source_segment = "a" * 222 + ".a3cb26de2edfce9f509a65c611d99c4b"
+ source_cache = cache_base.join(source_segment)
+ cached_gem = source_cache.join("myrack-1.0.0.gem")
+ expect(cached_gem).to exist
+ ensure
+ # We cleanup dummy files created by this spec manually because due to a
+ # Ruby on Windows bug, `FileUtils.rm_rf` (run in our global after hook)
+ # cannot traverse directories with such long names. So we delete
+ # everything explicitly to workaround the bug. An alternative workaround
+ # would be to shell out to `rm -rf`. That also works fine, but I went with
+ # the more verbose and explicit approach. This whole ensure block can be
+ # removed once/if https://bugs.ruby-lang.org/issues/21177 is fixed, and
+ # once the fix propagates to all supported rubies.
+ File.delete cached_gem
+ Dir.rmdir source_cache
+
+ File.delete compact_index_cache_path.join(source_segment, "info", "myrack")
+ Dir.rmdir compact_index_cache_path.join(source_segment, "info")
+ File.delete compact_index_cache_path.join(source_segment, "info-etags", "myrack-92f3313ce5721296f14445c3a6b9c073")
+ Dir.rmdir compact_index_cache_path.join(source_segment, "info-etags")
+ Dir.rmdir compact_index_cache_path.join(source_segment, "info-special-characters")
+ File.delete compact_index_cache_path.join(source_segment, "versions")
+ File.delete compact_index_cache_path.join(source_segment, "versions.etag")
+ Dir.rmdir compact_index_cache_path.join(source_segment)
+ end
+
+ describe "when the same gem from different sources is installed" do
+ it "should use the appropriate one from the global cache" do
+ bundle_config "path.system true"
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "#{source}"
+ gem "myrack"
+ G
+
+ pristine_system_gems
+ expect(the_bundle).not_to include_gems "myrack 1.0.0"
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ # myrack 1.0.0 is not installed and it is in the global cache
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "#{source2}"
+ gem "myrack", "0.9.1"
+ G
+
+ pristine_system_gems
+ expect(the_bundle).not_to include_gems "myrack 0.9.1"
+ expect(source2_global_cache("myrack-0.9.1.gem")).to exist
+ # myrack 0.9.1 is not installed and it is in the global cache
+
+ gemfile <<-G
+ source "#{source}"
+ gem "myrack", "1.0.0"
+ G
+
+ bundle :install, artifice: "compact_index_no_gem"
+ # myrack 1.0.0 is installed and myrack 0.9.1 is not
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(the_bundle).not_to include_gems "myrack 0.9.1"
+ pristine_system_gems
+
+ gemfile <<-G
+ source "#{source2}"
+ gem "myrack", "0.9.1"
+ G
+
+ bundle :install, artifice: "compact_index_no_gem"
+ # myrack 0.9.1 is installed and myrack 1.0.0 is not
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ expect(the_bundle).not_to include_gems "myrack 1.0.0"
+ end
+
+ it "should not install if the wrong source is provided" do
+ bundle_config "path.system true"
+
+ gemfile <<-G
+ source "#{source}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index"
+ pristine_system_gems
+ expect(the_bundle).not_to include_gems "myrack 1.0.0"
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ # myrack 1.0.0 is not installed and it is in the global cache
+
+ gemfile <<-G
+ source "#{source2}"
+ gem "myrack", "0.9.1"
+ G
+
+ bundle :install, artifice: "compact_index"
+ pristine_system_gems
+ expect(the_bundle).not_to include_gems "myrack 0.9.1"
+ expect(source2_global_cache("myrack-0.9.1.gem")).to exist
+ # myrack 0.9.1 is not installed and it is in the global cache
+
+ gemfile <<-G
+ source "#{source2}"
+ gem "myrack", "1.0.0"
+ G
+
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ expect(source2_global_cache("myrack-0.9.1.gem")).to exist
+ bundle :install, artifice: "compact_index_no_gem", raise_on_error: false
+ expect(err).to include("Internal Server Error 500")
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+
+ # myrack 1.0.0 is not installed and myrack 0.9.1 is not
+ expect(the_bundle).not_to include_gems "myrack 1.0.0"
+ expect(the_bundle).not_to include_gems "myrack 0.9.1"
+
+ gemfile <<-G
+ source "#{source}"
+ gem "myrack", "0.9.1"
+ G
+
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ expect(source2_global_cache("myrack-0.9.1.gem")).to exist
+ bundle :install, artifice: "compact_index_no_gem", raise_on_error: false
+ expect(err).to include("Internal Server Error 500")
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+
+ # myrack 0.9.1 is not installed and myrack 1.0.0 is not
+ expect(the_bundle).not_to include_gems "myrack 0.9.1"
+ expect(the_bundle).not_to include_gems "myrack 1.0.0"
+ end
+ end
+
+ describe "when installing gems from a different directory" do
+ it "uses the global cache as a source" do
+ bundle_config "path.system true"
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "#{source}"
+ gem "myrack"
+ gem "activesupport"
+ G
+
+ # Both gems are installed and in the global cache
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(the_bundle).to include_gems "activesupport 2.3.5"
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ expect(source_global_cache("activesupport-2.3.5.gem")).to exist
+ pristine_system_gems
+ # Both gems are now only in the global cache
+ expect(the_bundle).not_to include_gems "myrack 1.0.0"
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5"
+
+ install_gemfile <<-G, artifice: "compact_index_no_gem"
+ source "#{source}"
+ gem "myrack"
+ G
+
+ # myrack is installed and both are in the global cache
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5"
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ expect(source_global_cache("activesupport-2.3.5.gem")).to exist
+
+ create_file bundled_app2("gems.rb"), <<-G
+ source "#{source}"
+ gem "activesupport"
+ G
+
+ # Neither gem is installed and both are in the global cache
+ expect(the_bundle).not_to include_gems "myrack 1.0.0", dir: bundled_app2
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5", dir: bundled_app2
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ expect(source_global_cache("activesupport-2.3.5.gem")).to exist
+
+ # Install using the global cache instead of by downloading the .gem
+ # from the server
+ bundle :install, artifice: "compact_index_no_gem", dir: bundled_app2
+
+ # activesupport is installed and both are in the global cache
+ expect(the_bundle).not_to include_gems "myrack 1.0.0", dir: bundled_app2
+ expect(the_bundle).to include_gems "activesupport 2.3.5", dir: bundled_app2
+
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ expect(source_global_cache("activesupport-2.3.5.gem")).to exist
+ end
+ end
+ end
+
+ describe "extension caching" do
+ it "works" do
+ skip "gets incorrect ref in path" if Gem.win_platform?
+ skip "fails for unknown reason when run by ruby-core" if ruby_core?
+
+ build_git "very_simple_git_binary", &:add_c_extension
+ build_lib "very_simple_path_binary", &:add_c_extension
+ revision = revision_for(lib_path("very_simple_git_binary-1.0"))[0, 12]
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "very_simple_binary"
+ gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}"
+ gem "very_simple_path_binary", :path => "#{lib_path("very_simple_path_binary-1.0")}"
+ G
+
+ gem_binary_cache = home(".bundle", "cache", "extensions", local_platform.to_s, Bundler.ruby_scope,
+ "gem.repo1.443.#{Digest(:MD5).hexdigest("gem.repo1.443./")}", "very_simple_binary-1.0")
+ git_binary_cache = home(".bundle", "cache", "extensions", local_platform.to_s, Bundler.ruby_scope,
+ "very_simple_git_binary-1.0-#{revision}", "very_simple_git_binary-1.0")
+
+ cached_extensions = Pathname.glob(home(".bundle", "cache", "extensions", "*", "*", "*", "*", "*")).sort
+ expect(cached_extensions).to eq [gem_binary_cache, git_binary_cache].sort
+
+ run <<-R
+ require 'very_simple_binary_c'; puts ::VERY_SIMPLE_BINARY_IN_C
+ require 'very_simple_git_binary_c'; puts ::VERY_SIMPLE_GIT_BINARY_IN_C
+ R
+ expect(out).to eq "VERY_SIMPLE_BINARY_IN_C\nVERY_SIMPLE_GIT_BINARY_IN_C"
+
+ FileUtils.rm_r Dir[home(".bundle", "cache", "extensions", "**", "*binary_c*")]
+
+ gem_binary_cache.join("very_simple_binary_c.rb").open("w") {|f| f << "puts File.basename(__FILE__)" }
+ git_binary_cache.join("very_simple_git_binary_c.rb").open("w") {|f| f << "puts File.basename(__FILE__)" }
+
+ bundle_config "path different_path"
+ bundle :install
+
+ expect(Dir[home(".bundle", "cache", "extensions", "**", "*binary_c*")]).to all(end_with(".rb"))
+
+ run <<-R
+ require 'very_simple_binary_c'
+ require 'very_simple_git_binary_c'
+ R
+ expect(out).to eq "very_simple_binary_c.rb\nvery_simple_git_binary_c.rb"
+ end
+ end
+end
diff --git a/spec/bundler/install/path_spec.rb b/spec/bundler/install/path_spec.rb
new file mode 100644
index 0000000000..49360e511e
--- /dev/null
+++ b/spec/bundler/install/path_spec.rb
@@ -0,0 +1,214 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ describe "with path configured" do
+ before :each do
+ build_gem "myrack", "1.0.0", to_system: true do |s|
+ s.write "lib/myrack.rb", "puts 'FAIL'"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "does not use available system gems with `vendor/bundle" do
+ bundle_config "path vendor/bundle"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "uses system gems with `path.system` configured with more priority than `path`" do
+ bundle_config "path.system true"
+ bundle_config_global "path vendor/bundle"
+ bundle :install
+ run "require 'myrack'", raise_on_error: false
+ expect(out).to include("FAIL")
+ end
+
+ it "handles paths with regex characters in them" do
+ dir = bundled_app("bun++dle")
+ dir.mkpath
+
+ bundle_config "path #{dir.join("vendor/bundle")}"
+ bundle :install, dir: dir
+ expect(out).to include("installed into `./vendor/bundle`")
+
+ FileUtils.rm_rf dir
+ end
+
+ it "prints a message to let the user know where gems where installed" do
+ bundle_config "path vendor/bundle"
+ bundle :install
+ expect(out).to include("gems are installed into `./vendor/bundle`")
+ end
+
+ it "installs the bundle relatively to repository root, when Bundler run from the same directory" do
+ bundle "config set path vendor/bundle", dir: bundled_app.parent
+ bundle "install --gemfile='#{bundled_app}/Gemfile'", dir: bundled_app.parent
+ expect(out).to include("installed into `./bundled_app/vendor/bundle`")
+ expect(bundled_app("vendor/bundle")).to be_directory
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "installs the bundle relatively to repository root, when Bundler run from a different directory" do
+ bundle "config set path vendor/bundle", dir: bundled_app
+ bundle "install --gemfile='#{bundled_app}/Gemfile'", dir: bundled_app.parent
+ expect(out).to include("installed into `./bundled_app/vendor/bundle`")
+ expect(bundled_app("vendor/bundle")).to be_directory
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "installs the standalone bundle relative to the cwd" do
+ bundle :install, gemfile: bundled_app_gemfile, standalone: true, dir: bundled_app.parent
+ expect(out).to include("installed into `./bundled_app/bundle`")
+ expect(bundled_app("bundle")).to be_directory
+ expect(bundled_app("bundle/ruby")).to be_directory
+
+ bundle :install, gemfile: bundled_app_gemfile, standalone: true, dir: bundled_app("subdir").tap(&:mkpath)
+ expect(out).to include("installed into `../bundle`")
+ expect(bundled_app("bundle")).to be_directory
+ expect(bundled_app("bundle/ruby")).to be_directory
+ end
+ end
+
+ describe "when BUNDLE_PATH or the global path config is set" do
+ before :each do
+ build_lib "myrack", "1.0.0", to_system: true do |s|
+ s.write "lib/myrack.rb", "raise 'FAIL'"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ def set_bundle_path(type, location)
+ if type == :env
+ ENV["BUNDLE_PATH"] = location
+ elsif type == :global
+ bundle "config set path #{location}", "no-color" => nil
+ end
+ end
+
+ [:env, :global].each do |type|
+ context "when set via #{type}" do
+ it "installs gems to a path if one is specified" do
+ set_bundle_path(type, bundled_app("vendor2").to_s)
+ bundle_config "path vendor/bundle"
+ bundle :install
+
+ expect(vendored_gems("gems/myrack-1.0.0")).to be_directory
+ expect(bundled_app("vendor2")).not_to be_directory
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "installs gems to ." do
+ set_bundle_path(type, ".")
+ bundle_config_global "disable_shared_gems true"
+
+ bundle :install
+
+ paths_to_exist = %w[cache/myrack-1.0.0.gem gems/myrack-1.0.0 specifications/myrack-1.0.0.gemspec].map {|path| bundled_app(Bundler.ruby_scope, path) }
+ expect(paths_to_exist).to all exist
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "installs gems to the path" do
+ set_bundle_path(type, bundled_app("vendor").to_s)
+
+ bundle :install
+
+ expect(bundled_app("vendor", Bundler.ruby_scope, "gems/myrack-1.0.0")).to be_directory
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "installs gems to the path relative to root when relative" do
+ set_bundle_path(type, "vendor")
+
+ FileUtils.mkdir_p bundled_app("lol")
+ bundle :install, dir: bundled_app("lol")
+
+ expect(bundled_app("vendor", Bundler.ruby_scope, "gems/myrack-1.0.0")).to be_directory
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+ end
+
+ it "installs gems to BUNDLE_PATH from .bundle/config" do
+ bundle_config "BUNDLE_PATH" => bundled_app("vendor/bundle").to_s
+
+ bundle :install
+
+ expect(vendored_gems("gems/myrack-1.0.0")).to be_directory
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "sets BUNDLE_PATH as the first argument to bundle install" do
+ bundle_config "path ./vendor/bundle"
+ bundle :install
+
+ expect(vendored_gems("gems/myrack-1.0.0")).to be_directory
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "disables system gems when passing a path to install" do
+ # This is so that vendored gems can be distributed to others
+ build_gem "myrack", "1.1.0", to_system: true
+ bundle_config "path ./vendor/bundle"
+ bundle :install
+
+ expect(vendored_gems("gems/myrack-1.0.0")).to be_directory
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "re-installs gems whose extensions have been deleted" do
+ build_lib "very_simple_binary", "1.0.0", to_system: true do |s|
+ s.write "lib/very_simple_binary.rb", "raise 'FAIL'"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "very_simple_binary"
+ G
+
+ bundle_config "path ./vendor/bundle"
+ bundle :install
+
+ expect(vendored_gems("gems/very_simple_binary-1.0")).to be_directory
+ expect(vendored_gems("extensions")).to be_directory
+ expect(the_bundle).to include_gems "very_simple_binary 1.0", source: "remote1"
+
+ FileUtils.rm_rf vendored_gems("extensions")
+
+ run "require 'very_simple_binary_c'", raise_on_error: false
+ expect(err).to include("Bundler::GemNotFound")
+
+ bundle_config "path ./vendor/bundle"
+ bundle :install
+
+ expect(vendored_gems("gems/very_simple_binary-1.0")).to be_directory
+ expect(vendored_gems("extensions")).to be_directory
+ expect(the_bundle).to include_gems "very_simple_binary 1.0", source: "remote1"
+ end
+ end
+
+ describe "to a file" do
+ before do
+ FileUtils.touch bundled_app("bundle")
+ end
+
+ it "reports the file exists" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle_config "path bundle"
+ bundle :install, raise_on_error: false
+ expect(err).to include("file already exists")
+ end
+ end
+end
diff --git a/spec/bundler/install/prereleases_spec.rb b/spec/bundler/install/prereleases_spec.rb
new file mode 100644
index 0000000000..9f764d127c
--- /dev/null
+++ b/spec/bundler/install/prereleases_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ before do
+ build_repo2 do
+ build_gem "not_released", "1.0.pre"
+
+ build_gem "has_prerelease", "1.0"
+ build_gem "has_prerelease", "1.1.pre"
+ end
+ end
+
+ describe "when prerelease gems are available" do
+ it "finds prereleases" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "not_released"
+ G
+ expect(the_bundle).to include_gems "not_released 1.0.pre"
+ end
+
+ it "uses regular releases if available" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "has_prerelease"
+ G
+ expect(the_bundle).to include_gems "has_prerelease 1.0"
+ end
+
+ it "uses prereleases if requested" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "has_prerelease", "1.1.pre"
+ G
+ expect(the_bundle).to include_gems "has_prerelease 1.1.pre"
+ end
+ end
+
+ describe "when prerelease gems are not available" do
+ it "still works" do
+ build_repo3 do
+ build_gem "myrack"
+ end
+ FileUtils.rm_r Dir[gem_repo3("prerelease*")]
+
+ install_gemfile <<-G
+ source "https://gem.repo3"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+end
diff --git a/spec/bundler/install/process_lock_spec.rb b/spec/bundler/install/process_lock_spec.rb
new file mode 100644
index 0000000000..b096291d1a
--- /dev/null
+++ b/spec/bundler/install/process_lock_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+RSpec.describe "process lock spec" do
+ describe "when an install operation is already holding a process lock" do
+ before { FileUtils.mkdir_p(default_bundle_path) }
+
+ it "will not run a second concurrent bundle install until the lock is released" do
+ thread = Thread.new do
+ Bundler::ProcessLock.lock(default_bundle_path) do
+ sleep 1 # ignore quality_spec
+ expect(the_bundle).not_to include_gems "myrack 1.0"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ thread.join
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+
+ context "when creating a lock raises Errno::ENOTSUP" do
+ before { allow(File).to receive(:open).and_raise(Errno::ENOTSUP) }
+
+ it "skips creating the lockfile and yields" do
+ processed = false
+ Bundler::ProcessLock.lock(default_bundle_path) { processed = true }
+
+ expect(processed).to eq true
+ end
+ end
+
+ context "when creating a lock raises Errno::EPERM" do
+ before { allow(File).to receive(:open).and_raise(Errno::EPERM) }
+
+ it "skips creating the lockfile and yields" do
+ processed = false
+ Bundler::ProcessLock.lock(default_bundle_path) { processed = true }
+
+ expect(processed).to eq true
+ end
+ end
+
+ context "when creating a lock raises Errno::EROFS" do
+ before { allow(File).to receive(:open).and_raise(Errno::EROFS) }
+
+ it "skips creating the lockfile and yields" do
+ processed = false
+ Bundler::ProcessLock.lock(default_bundle_path) { processed = true }
+
+ expect(processed).to eq true
+ end
+ end
+
+ it "refreshes gem specification cache after waiting for lock" do
+ build_repo2 do
+ build_gem "myrack", "1.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ # First, install the gem so it's available
+ bundle "install"
+ expect(out).to include("Installing myrack")
+
+ # Queue for thread-safe communication
+ lock_acquired = Queue.new
+ can_release_lock = Queue.new
+ install_output = Queue.new
+
+ # Thread holds lock (simulating another bundle process that just finished installing)
+ thread = Thread.new do
+ Bundler::ProcessLock.lock(default_bundle_path) do
+ # Signal that we have the lock
+ lock_acquired << true
+ # Wait until main thread signals we can release
+ can_release_lock.pop
+ end
+ end
+
+ # Wait for thread to acquire lock
+ lock_acquired.pop
+
+ # Start another install in a thread - it will wait for the lock
+ install_thread = Thread.new do
+ bundle "install", verbose: true
+ install_output << out
+ end
+
+ # Give subprocess time to start and begin waiting for lock
+ sleep 0.5
+
+ # Signal thread to release the lock
+ can_release_lock << true
+
+ # Wait for both threads to complete
+ thread.join
+ install_thread.join
+
+ second_install_out = install_output.pop
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ # The second install should have refreshed its cache after acquiring
+ # the lock and seen that myrack was already installed
+ expect(second_install_out).to include("Using myrack")
+ end
+ end
+end
diff --git a/spec/bundler/install/security_policy_spec.rb b/spec/bundler/install/security_policy_spec.rb
new file mode 100644
index 0000000000..e7f64dc227
--- /dev/null
+++ b/spec/bundler/install/security_policy_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require "rubygems/security"
+
+# unfortunately, testing signed gems with a provided CA is extremely difficult
+# as 'gem cert' is currently the only way to add CAs to the system.
+
+RSpec.describe "policies with unsigned gems" do
+ before do
+ build_security_repo
+ gemfile <<-G
+ source "https://gems.security"
+ gem "myrack"
+ gem "signed_gem"
+ G
+ end
+
+ it "will work after you try to deploy without a lock" do
+ bundle "install --deployment", raise_on_error: false
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0", "signed_gem 1.0"
+ end
+
+ it "will fail when given invalid security policy" do
+ bundle "install --trust-policy=InvalidPolicyName", raise_on_error: false
+ expect(err).to include("RubyGems doesn't know about trust policy")
+ end
+
+ it "will fail with High Security setting due to presence of unsigned gem" do
+ bundle "install --trust-policy=HighSecurity", raise_on_error: false
+ expect(err).to include("security policy didn't allow")
+ end
+
+ it "will fail with Medium Security setting due to presence of unsigned gem" do
+ bundle "install --trust-policy=MediumSecurity", raise_on_error: false
+ expect(err).to include("security policy didn't allow")
+ end
+
+ it "will succeed with no policy" do
+ bundle "install"
+ end
+end
+
+RSpec.describe "policies with signed gems and no CA" do
+ before do
+ build_security_repo
+ gemfile <<-G
+ source "https://gems.security"
+ gem "signed_gem"
+ G
+ end
+
+ it "will fail with High Security setting, gem is self-signed" do
+ bundle "install --trust-policy=HighSecurity", raise_on_error: false
+ expect(err).to include("security policy didn't allow")
+ end
+
+ it "will fail with Medium Security setting, gem is self-signed" do
+ bundle "install --trust-policy=MediumSecurity", raise_on_error: false
+ expect(err).to include("security policy didn't allow")
+ end
+
+ it "will succeed with Low Security setting, low security accepts self signed gem" do
+ bundle "install --trust-policy=LowSecurity"
+ expect(the_bundle).to include_gems "signed_gem 1.0"
+ end
+
+ it "will succeed with no policy" do
+ bundle "install"
+ expect(the_bundle).to include_gems "signed_gem 1.0"
+ end
+end
diff --git a/spec/bundler/install/yanked_spec.rb b/spec/bundler/install/yanked_spec.rb
new file mode 100644
index 0000000000..c92af7bfb0
--- /dev/null
+++ b/spec/bundler/install/yanked_spec.rb
@@ -0,0 +1,254 @@
+# frozen_string_literal: true
+
+RSpec.context "when installing a bundle that includes yanked gems" do
+ it "throws an error when the original gem version is yanked" do
+ build_repo4 do
+ build_gem "foo", "9.0.0"
+ end
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ foo (10.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo (= 10.0.0)
+
+ L
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo4"
+ gem "foo", "10.0.0"
+ G
+
+ expect(err).to include("Your bundle is locked to foo (10.0.0)")
+ end
+
+ context "when a platform specific yanked version is included in the lockfile, and a generic variant is available remotely" do
+ let(:original_lockfile) do
+ <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actiontext (6.1.6)
+ nokogiri (>= 1.8)
+ foo (1.0.0)
+ nokogiri (1.13.8-#{Bundler.local_platform})
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ actiontext (= 6.1.6)
+ foo (= 1.0.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ before do
+ skip "Materialization on Windows is not yet strict, so the example does not detect the gem has been yanked" if Gem.win_platform?
+
+ build_repo4 do
+ build_gem "foo", "1.0.0"
+ build_gem "foo", "1.0.1"
+ build_gem "actiontext", "6.1.7" do |s|
+ s.add_dependency "nokogiri", ">= 1.8"
+ end
+ build_gem "actiontext", "6.1.6" do |s|
+ s.add_dependency "nokogiri", ">= 1.8"
+ end
+ build_gem "actiontext", "6.1.7" do |s|
+ s.add_dependency "nokogiri", ">= 1.8"
+ end
+ build_gem "nokogiri", "1.13.8"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "foo", "1.0.0"
+ gem "actiontext", "6.1.6"
+ G
+
+ lockfile original_lockfile
+ end
+
+ context "and a re-resolve is necessary" do
+ before do
+ gemfile gemfile.sub('"foo", "1.0.0"', '"foo", "1.0.1"')
+ end
+
+ it "reresolves, and replaces the yanked gem with the generic version, printing a warning, when the old index is used" do
+ bundle "install", artifice: "endpoint", verbose: true
+
+ expect(out).to include("Installing nokogiri 1.13.8").and include("Installing foo 1.0.1")
+ expect(lockfile).to eq(original_lockfile.sub("nokogiri (1.13.8-#{Bundler.local_platform})", "nokogiri (1.13.8)").gsub("1.0.0", "1.0.1"))
+ expect(err).to include("Some locked specs have possibly been yanked (nokogiri-1.13.8-#{Bundler.local_platform}). Ignoring them...")
+ end
+
+ it "reresolves, and replaces the yanked gem with the generic version, printing a warning, when the compact index API is used" do
+ bundle "install", artifice: "compact_index", verbose: true
+
+ expect(out).to include("Installing nokogiri 1.13.8").and include("Installing foo 1.0.1")
+ expect(lockfile).to eq(original_lockfile.sub("nokogiri (1.13.8-#{Bundler.local_platform})", "nokogiri (1.13.8)").gsub("1.0.0", "1.0.1"))
+ expect(err).to include("Some locked specs have possibly been yanked (nokogiri-1.13.8-#{Bundler.local_platform}). Ignoring them...")
+ end
+ end
+
+ it "reports the yanked gem properly when the old index is used" do
+ bundle "install", artifice: "endpoint", raise_on_error: false
+
+ expect(err).to include("Your bundle is locked to nokogiri (1.13.8-#{Bundler.local_platform})")
+ end
+
+ it "reports the yanked gem properly when the compact index API is used" do
+ bundle "install", artifice: "compact_index", raise_on_error: false
+
+ expect(err).to include("Your bundle is locked to nokogiri (1.13.8-#{Bundler.local_platform})")
+ end
+ end
+
+ it "throws the original error when only the Gemfile specifies a gem version that doesn't exist" do
+ build_repo4 do
+ build_gem "foo", "9.0.0"
+ end
+
+ bundle_config "force_ruby_platform true"
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo4"
+ gem "foo", "10.0.0"
+ G
+
+ expect(err).not_to include("Your bundle is locked to foo (10.0.0)")
+ expect(err).to include("Could not find gem 'foo (= 10.0.0)' in")
+ end
+end
+
+RSpec.context "when resolving a bundle that includes yanked gems, but unlocking an unrelated gem" do
+ before(:each) do
+ build_repo4 do
+ build_gem "foo", "10.0.0"
+
+ build_gem "bar", "1.0.0"
+ build_gem "bar", "2.0.0"
+ end
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ foo (9.0.0)
+ bar (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ bar
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "foo"
+ gem "bar"
+ G
+ end
+
+ it "does not update the yanked gem" do
+ bundle "lock --update bar"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ bar (2.0.0)
+ foo (9.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ bar
+ foo
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+end
+
+RSpec.context "when using gem before installing" do
+ it "does not suggest the author has yanked the gem" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (= 0.9.1)
+ L
+
+ bundle :list, raise_on_error: false
+
+ expect(err).to include("Could not find myrack-0.9.1 in locally installed gems")
+ expect(err).to_not include("Your bundle is locked to myrack (0.9.1) from")
+ expect(err).to_not include("If you haven't changed sources, that means the author of myrack (0.9.1) has removed it.")
+ expect(err).to_not include("You'll need to update your bundle to a different version of myrack (0.9.1) that hasn't been removed in order to install.")
+
+ # Check error message is still correct when multiple platforms are locked
+ lockfile lockfile.gsub(/PLATFORMS\n #{lockfile_platforms}/m, "PLATFORMS\n #{lockfile_platforms("ruby")}")
+
+ bundle :list, raise_on_error: false
+ expect(err).to include("Could not find myrack-0.9.1 in locally installed gems")
+ end
+
+ it "does not suggest the author has yanked the gem when using more than one gem, but shows all gems that couldn't be found in the source" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ gem "myrack_middleware", "1.0"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1
+ specs:
+ myrack (0.9.1)
+ myrack_middleware (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (= 0.9.1)
+ myrack_middleware (1.0)
+ L
+
+ bundle :list, raise_on_error: false
+
+ expect(err).to include("Could not find myrack-0.9.1, myrack_middleware-1.0 in locally installed gems")
+ expect(err).to include("Install missing gems with `bundle install`.")
+ expect(err).to_not include("Your bundle is locked to myrack (0.9.1) from")
+ expect(err).to_not include("If you haven't changed sources, that means the author of myrack (0.9.1) has removed it.")
+ expect(err).to_not include("You'll need to update your bundle to a different version of myrack (0.9.1) that hasn't been removed in order to install.")
+ end
+end
diff --git a/spec/bundler/lock/git_spec.rb b/spec/bundler/lock/git_spec.rb
new file mode 100644
index 0000000000..c9f76115dc
--- /dev/null
+++ b/spec/bundler/lock/git_spec.rb
@@ -0,0 +1,258 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle lock with git gems" do
+ let(:install_gemfile_with_foo_as_a_git_dependency) do
+ build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}"
+ G
+ end
+
+ it "doesn't break right after running lock" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ expect(the_bundle).to include_gems "foo 1.0.0"
+ end
+
+ it "doesn't print errors even if running lock after removing the cache" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ FileUtils.rm_r(Dir[default_cache_path("git/foo-1.0-*")].first)
+
+ bundle "lock --verbose"
+
+ expect(err).to be_empty
+ end
+
+ it "prints a proper error when changing a locked Gemfile to point to a bad branch" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}", :branch => "bad"
+ G
+
+ bundle "lock --update foo", env: { "LANG" => "en" }, raise_on_error: false
+
+ expect(err).to include("Revision bad does not exist in the repository")
+ end
+
+ it "prints a proper error when installing a Gemfile with a locked ref that does not exist" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ lockfile <<~L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{"a" * 40}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install", raise_on_error: false
+
+ expect(err).to include("Revision #{"a" * 40} does not exist in the repository")
+ end
+
+ it "locks a git source to the current ref" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ update_git "foo"
+ bundle :install
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" unless defined?(FOO_PREV_REF)
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "properly clones a git source locked to an out of date ref" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ update_git "foo"
+
+ bundle :install, env: { "BUNDLE_PATH" => "foo" }
+ expect(err).to be_empty
+ end
+
+ it "properly fetches a git source locked to an unreachable ref" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ # Create a commit and make it unreachable
+ git "checkout -b foo ", lib_path("foo-1.0")
+ unreachable_sha = update_git("foo").ref_for("HEAD")
+ git "checkout main ", lib_path("foo-1.0")
+ git "branch -D foo ", lib_path("foo-1.0")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}"
+ G
+
+ lockfile <<-L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{unreachable_sha}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(err).to be_empty
+ end
+
+ it "properly fetches a git source locked to an annotated tag" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ # Create an annotated tag
+ git("tag -a v1.0 -m 'Annotated v1.0'", lib_path("foo-1.0"))
+ annotated_tag = git("rev-parse v1.0", lib_path("foo-1.0"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}"
+ G
+
+ lockfile <<-L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{annotated_tag}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(err).to be_empty
+ end
+
+ it "provides correct #full_gem_path" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ run <<-RUBY
+ puts Bundler.rubygems.find_name('foo').first.full_gem_path
+ RUBY
+ expect(out).to eq(bundle("info foo --path"))
+ end
+
+ it "does not lock versions that don't exist in the repository when changing a GEM transitive dep to a GIT direct dep" do
+ build_repo4 do
+ build_gem "activesupport", "8.0.0" do |s|
+ s.add_dependency "securerandom"
+ end
+
+ build_gem "securerandom", "0.3.1"
+ end
+
+ path = lib_path("securerandom")
+
+ build_git "securerandom", "0.3.2", path: path
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ activesupport (8.0.0)
+ securerandom
+ securerandom (0.3.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ activesupport
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "activesupport"
+ gem "securerandom", git: "#{path}"
+ G
+
+ bundle "lock"
+
+ expect(lockfile).to include("securerandom (0.3.2)")
+ end
+
+ it "does not lock versions that don't exist in the repository when changing a GIT direct dep to a GEM direct dep" do
+ build_repo4 do
+ build_gem "ruby-lsp", "0.16.1"
+ end
+
+ path = lib_path("ruby-lsp")
+ revision = build_git("ruby-lsp", "0.16.2", path: path).ref_for("HEAD")
+
+ lockfile <<~L
+ GIT
+ remote: #{path}
+ revision: #{revision}
+ specs:
+ ruby-lsp (0.16.2)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ruby-lsp!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "ruby-lsp"
+ G
+
+ bundle "lock"
+
+ expect(lockfile).to include("ruby-lsp (0.16.1)")
+ end
+end
diff --git a/spec/bundler/lock/lockfile_spec.rb b/spec/bundler/lock/lockfile_spec.rb
new file mode 100644
index 0000000000..654ac02aa7
--- /dev/null
+++ b/spec/bundler/lock/lockfile_spec.rb
@@ -0,0 +1,2416 @@
+# frozen_string_literal: true
+
+RSpec.describe "the lockfile format" do
+ before do
+ build_repo2
+ end
+
+ it "generates a simple lockfile for a single source, gem" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo2, "myrack", "1.0.0")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "myrack"
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "updates the lockfile's bundler version if current ver. is newer, and version was forced through BUNDLER_VERSION" do
+ system_gems "bundler-1.8.2"
+
+ lockfile <<-L
+ GIT
+ remote: git://github.com/nex3/haml.git
+ revision: 8a2271f
+ specs:
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ omg!
+ myrack
+
+ BUNDLED WITH
+ 1.8.2
+ L
+
+ install_gemfile <<-G, verbose: true, env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ source "https://gem.repo2"
+
+ gem "myrack"
+ G
+
+ expect(out).not_to include("Bundler #{Bundler::VERSION} is running, but your lockfile was generated with 1.8.2.")
+ expect(out).to include("Using bundler #{Bundler::VERSION}")
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "does not update the lockfile's bundler version if nothing changed during bundle install, but uses the locked version" do
+ version = "2.3.0"
+
+ build_repo4 do
+ build_gem "myrack", "1.0.0"
+
+ build_bundler version
+ end
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{version}
+ L
+
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo4"
+
+ gem "myrack"
+ G
+
+ expect(out).to include("Bundler #{Bundler::VERSION} is running, but your lockfile was generated with #{version}.")
+ expect(out).to include("Using bundler #{version}")
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{version}
+ G
+ end
+
+ it "adds the BUNDLED WITH section if not present" do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ L
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "myrack", "> 0"
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (> 0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "update the bundler major version just fine" do
+ current_version = Bundler::VERSION
+ older_major = previous_major(current_version)
+
+ system_gems "bundler-#{older_major}"
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{older_major}
+ L
+
+ install_gemfile <<-G, env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ source "https://gem.repo2/"
+
+ gem "myrack"
+ G
+
+ expect(err).to be_empty
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{current_version}
+ G
+ end
+
+ it "generates a simple lockfile for a single source, gem with dependencies" do
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "myrack-obama"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ c.checksum gem_repo2, "myrack-obama", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack-obama
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "generates a simple lockfile for a single source, gem with a version requirement" do
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "myrack-obama", ">= 1.0"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ c.checksum gem_repo2, "myrack-obama", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack-obama (>= 1.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "generates a lockfile without credentials" do
+ bundle "config set https://localgemserver.test/ user:pass"
+
+ install_gemfile(<<-G, artifice: "endpoint_strict_basic_authentication", quiet: true)
+ source "https://gem.repo1"
+
+ source "https://localgemserver.test/" do
+
+ end
+
+ source "https://user:pass@othergemserver.test/" do
+ gem "myrack-obama", ">= 1.0"
+ end
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ c.checksum gem_repo2, "myrack-obama", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ GEM
+ remote: https://localgemserver.test/
+ specs:
+
+ GEM
+ remote: https://othergemserver.test/
+ specs:
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack-obama (>= 1.0)!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "does not add credentials to lockfile when it does not have them already" do
+ bundle "config set http://localgemserver.test/ user:pass"
+
+ gemfile <<~G
+ source "https://gem.repo1"
+
+ source "http://localgemserver.test/" do
+
+ end
+
+ source "http://user:pass@othergemserver.test/" do
+ gem "myrack-obama", ">= 1.0"
+ end
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ c.checksum gem_repo2, "myrack-obama", "1.0"
+ end
+
+ lockfile_without_credentials = <<~L
+ GEM
+ remote: http://localgemserver.test/
+ specs:
+
+ GEM
+ remote: http://othergemserver.test/
+ specs:
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack-obama (>= 1.0)!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile lockfile_without_credentials
+
+ # when not re-resolving
+ bundle "install", artifice: "endpoint_strict_basic_authentication", quiet: true
+ expect(lockfile).to eq lockfile_without_credentials
+
+ # when re-resolving with full unlock
+ bundle "update", artifice: "endpoint_strict_basic_authentication"
+ expect(lockfile).to eq lockfile_without_credentials
+
+ # when re-resolving without ful unlocking
+ bundle "update myrack-obama", artifice: "endpoint_strict_basic_authentication"
+ expect(lockfile).to eq lockfile_without_credentials
+ end
+
+ it "keeps credentials in lockfile if already there" do
+ bundle "config set http://localgemserver.test/ user:pass"
+
+ gemfile <<~G
+ source "https://gem.repo1"
+
+ source "http://localgemserver.test/" do
+
+ end
+
+ source "http://user:pass@othergemserver.test/" do
+ gem "myrack-obama", ">= 1.0"
+ end
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ c.checksum gem_repo2, "myrack-obama", "1.0"
+ end
+
+ lockfile_with_credentials = <<~L
+ GEM
+ remote: http://localgemserver.test/
+ specs:
+
+ GEM
+ remote: http://user:pass@othergemserver.test/
+ specs:
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack-obama (>= 1.0)!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile lockfile_with_credentials
+
+ bundle "install", artifice: "endpoint_strict_basic_authentication", quiet: true
+
+ expect(lockfile).to eq lockfile_with_credentials
+ end
+
+ it "generates lockfiles with multiple requirements" do
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+ gem "net-sftp"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "net-sftp", "1.1.1"
+ c.checksum gem_repo2, "net-ssh", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ net-sftp (1.1.1)
+ net-ssh (>= 1.0.0, < 1.99.0)
+ net-ssh (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ net-sftp
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ expect(the_bundle).to include_gems "net-sftp 1.1.1", "net-ssh 1.0.0"
+ end
+
+ it "generates a simple lockfile for a single pinned source, gem with a version requirement" do
+ git = build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{git.ref_for("main")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "does not asplode when a platform specific dependency is present and the Gemfile has not been resolved on that platform" do
+ build_lib "omg", path: lib_path("omg")
+
+ gemfile <<-G
+ source "https://gem.repo2/"
+
+ platforms :#{not_local_tag} do
+ gem "omg", :path => "#{lib_path("omg")}"
+ end
+
+ gem "myrack"
+ G
+
+ lockfile <<-L
+ GIT
+ remote: git://github.com/nex3/haml.git
+ revision: 8a2271f
+ specs:
+
+ GEM
+ remote: https://gem.repo2//
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{not_local}
+
+ DEPENDENCIES
+ omg!
+ myrack
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "serializes global git sources" do
+ git = build_git "foo"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem "foo"
+ end
+ G
+
+ expect(lockfile).to eq <<~G
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{git.ref_for("main")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "generates a lockfile with a ref for a single pinned source, git gem with a branch requirement" do
+ git = build_git "foo"
+ update_git "foo", branch: "omg"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "omg"
+ G
+
+ expect(lockfile).to eq <<~G
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{git.ref_for("omg")}
+ branch: omg
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "generates a lockfile with a ref for a single pinned source, git gem with a tag requirement" do
+ git = build_git "foo"
+ update_git "foo", tag: "omg"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :tag => "omg"
+ G
+
+ expect(lockfile).to eq <<~G
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{git.ref_for("omg")}
+ tag: omg
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "is conservative with dependencies of git gems" do
+ build_repo4 do
+ build_gem "orm_adapter", "0.4.1"
+ build_gem "orm_adapter", "0.5.0"
+ end
+
+ FileUtils.mkdir_p lib_path("ckeditor/lib")
+
+ @remote = build_git("ckeditor_remote", bare: true)
+
+ build_git "ckeditor", path: lib_path("ckeditor") do |s|
+ s.write "lib/ckeditor.rb", "CKEDITOR = '4.0.7'"
+ s.version = "4.0.7"
+ s.add_dependency "orm_adapter"
+ end
+
+ update_git "ckeditor", path: lib_path("ckeditor"), remote: @remote.path
+ update_git "ckeditor", path: lib_path("ckeditor"), tag: "v4.0.7"
+ old_git = update_git "ckeditor", path: lib_path("ckeditor"), push: "v4.0.7"
+
+ update_git "ckeditor", path: lib_path("ckeditor"), gemspec: true do |s|
+ s.write "lib/ckeditor.rb", "CKEDITOR = '4.0.8'"
+ s.version = "4.0.8"
+ s.add_dependency "orm_adapter"
+ end
+ update_git "ckeditor", path: lib_path("ckeditor"), tag: "v4.0.8"
+
+ new_git = update_git "ckeditor", path: lib_path("ckeditor"), push: "v4.0.8"
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "ckeditor", :git => "#{@remote.path}", :tag => "v4.0.8"
+ G
+
+ lockfile <<~L
+ GIT
+ remote: #{@remote.path}
+ revision: #{old_git.ref_for("v4.0.7")}
+ tag: v4.0.7
+ specs:
+ ckeditor (4.0.7)
+ orm_adapter
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ orm_adapter (0.4.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ckeditor!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock"
+
+ # Bumps the git gem, but keeps its dependency locked
+ expect(lockfile).to eq <<~L
+ GIT
+ remote: #{@remote.path}
+ revision: #{new_git.ref_for("v4.0.8")}
+ tag: v4.0.8
+ specs:
+ ckeditor (4.0.8)
+ orm_adapter
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ orm_adapter (0.4.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ckeditor!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "serializes pinned path sources to the lockfile" do
+ build_lib "foo"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: #{lib_path("foo-1.0")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "serializes pinned path sources to the lockfile even when packaging" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo-1.0")}"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ bundle :cache
+ bundle :install, local: true
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: #{lib_path("foo-1.0")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "sorts serialized sources by type" do
+ build_lib "foo"
+ bar = build_git "bar"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.no_checksum "bar", "1.0"
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "myrack"
+ gem "foo", :path => "#{lib_path("foo-1.0")}"
+ gem "bar", :git => "#{lib_path("bar-1.0")}"
+ G
+
+ expect(lockfile).to eq <<~G
+ GIT
+ remote: #{lib_path("bar-1.0")}
+ revision: #{bar.ref_for("main")}
+ specs:
+ bar (1.0)
+
+ PATH
+ remote: #{lib_path("foo-1.0")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ bar!
+ foo!
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "removes redundant sources" do
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "myrack", :source => "https://gem.repo2/"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "lists gems alphabetically" do
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "thin"
+ gem "actionpack"
+ gem "myrack-obama"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "actionpack", "2.3.2"
+ c.checksum gem_repo2, "activesupport", "2.3.2"
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ c.checksum gem_repo2, "myrack-obama", "1.0"
+ c.checksum gem_repo2, "thin", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ actionpack (2.3.2)
+ activesupport (= 2.3.2)
+ activesupport (2.3.2)
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+ thin (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ actionpack
+ myrack-obama
+ thin
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "orders dependencies' dependencies in alphabetical order" do
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "rails"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "actionmailer", "2.3.2"
+ c.checksum gem_repo2, "actionpack", "2.3.2"
+ c.checksum gem_repo2, "activerecord", "2.3.2"
+ c.checksum gem_repo2, "activeresource", "2.3.2"
+ c.checksum gem_repo2, "activesupport", "2.3.2"
+ c.checksum gem_repo2, "rails", "2.3.2"
+ c.checksum gem_repo2, "rake", rake_version
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ 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)
+ 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})
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ rails
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "orders dependencies by version" do
+ update_repo2 do
+ # Capistrano did this (at least until version 2.5.10)
+ # RubyGems 2.2 doesn't allow the specifying of a dependency twice
+ # See https://github.com/ruby/rubygems/commit/03dbac93a3396a80db258d9bc63500333c25bd2f
+ build_gem "double_deps", "1.0", skip_validation: true do |s|
+ s.add_dependency "net-ssh", ">= 1.0.0"
+ s.add_dependency "net-ssh"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'double_deps'
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "double_deps", "1.0"
+ c.checksum gem_repo2, "net-ssh", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ double_deps (1.0)
+ net-ssh
+ net-ssh (>= 1.0.0)
+ net-ssh (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ double_deps
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "does not add the :require option to the lockfile" do
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "myrack-obama", ">= 1.0", :require => "myrack/obama"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ c.checksum gem_repo2, "myrack-obama", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack-obama (>= 1.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "does not add the :group option to the lockfile" do
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "myrack-obama", ">= 1.0", :group => :test
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ c.checksum gem_repo2, "myrack-obama", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack-obama (>= 1.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "stores relative paths when the path is provided in a relative fashion and in Gemfile dir" do
+ build_lib "foo", path: bundled_app("foo")
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ path "foo" do
+ gem "foo"
+ end
+ G
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: foo
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "stores relative paths when the path is provided in a relative fashion and is above Gemfile dir" do
+ build_lib "foo", path: bundled_app(File.join("..", "foo"))
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ path "../foo" do
+ gem "foo"
+ end
+ G
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: ../foo
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "stores relative paths when the path is provided in an absolute fashion but is relative" do
+ build_lib "foo", path: bundled_app("foo")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ path File.expand_path("foo", __dir__) do
+ gem "foo"
+ end
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: foo
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "stores relative paths when the path is provided for gemspec" do
+ build_lib("foo", path: tmp("foo"))
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec :path => "../foo"
+ G
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: ../foo
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "keeps existing platforms in the lockfile" do
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "myrack", "1.0.0"
+ end
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ java
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "myrack"
+ G
+
+ checksums.checksum(gem_repo2, "myrack", "1.0.0")
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms("java", local_platform, defaults: [])}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "adds compatible platform specific variants to the lockfile, even if resolution fallback to ruby due to some other incompatible platform specific variant" do
+ simulate_platform "arm64-darwin-23" do
+ build_repo4 do
+ build_gem "google-protobuf", "3.25.1"
+ build_gem "google-protobuf", "3.25.1" do |s|
+ s.platform = "arm64-darwin-23"
+ end
+ build_gem "google-protobuf", "3.25.1" do |s|
+ s.platform = "x64-mingw-ucrt"
+ s.required_ruby_version = "> #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "google-protobuf"
+ G
+ bundle "lock --add-platform x64-mingw-ucrt"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "google-protobuf", "3.25.1"
+ c.checksum gem_repo4, "google-protobuf", "3.25.1", "arm64-darwin-23"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ google-protobuf (3.25.1)
+ google-protobuf (3.25.1-arm64-darwin-23)
+
+ PLATFORMS
+ arm64-darwin-23
+ ruby
+ x64-mingw-ucrt
+
+ DEPENDENCIES
+ google-protobuf
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "persists the spec's specific platform to the lockfile" do
+ build_repo2 do
+ build_gem "platform_specific", "1.0" do |s|
+ s.platform = Gem::Platform.new("universal-java-16")
+ end
+ end
+
+ simulate_platform "universal-java-16" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "platform_specific"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "platform_specific", "1.0", "universal-java-16"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ platform_specific (1.0-universal-java-16)
+
+ PLATFORMS
+ universal-java-16
+
+ DEPENDENCIES
+ platform_specific
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+ end
+
+ it "does not add duplicate gems" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo2, "activesupport", "2.3.5")
+ c.checksum(gem_repo2, "myrack", "1.0.0")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+ gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+ gem "myrack"
+ gem "activesupport"
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ activesupport (2.3.5)
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ activesupport
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "does not add duplicate dependencies" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo2, "myrack", "1.0.0")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+ gem "myrack"
+ gem "myrack"
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "does not add duplicate dependencies with versions" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo2, "myrack", "1.0.0")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+ gem "myrack", "1.0"
+ gem "myrack", "1.0"
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (= 1.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "does not add duplicate dependencies in different groups" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo2, "myrack", "1.0.0")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+ gem "myrack", "1.0", :group => :one
+ gem "myrack", "1.0", :group => :two
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (= 1.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "raises if two different versions are used" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2/"
+ gem "myrack", "1.0"
+ gem "myrack", "1.1"
+ G
+
+ expect(bundled_app_lock).not_to exist
+ expect(err).to include "myrack (= 1.0) and myrack (= 1.1)"
+ end
+
+ it "raises if two different sources are used" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2/"
+ gem "myrack"
+ gem "myrack", :git => "git://hubz.com"
+ G
+
+ expect(bundled_app_lock).not_to exist
+ expect(err).to include "myrack (>= 0) should come from an unspecified source and git://hubz.com"
+ end
+
+ it "works correctly with multiple version dependencies" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo2, "myrack", "0.9.1")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+ gem "myrack", "> 0.9", "< 1.0"
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (> 0.9, < 1.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "captures the Ruby version in the lockfile" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo2, "myrack", "0.9.1")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+ ruby '#{Gem.ruby_version}'
+ gem "myrack", "> 0.9", "< 1.0"
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (> 0.9, < 1.0)
+ #{checksums}
+ RUBY VERSION
+ #{Bundler::RubyVersion.system}
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "automatically fixes the lockfile when it's missing deps" do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack_middleware (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack_middleware"
+ G
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (0.9.1)
+ myrack_middleware (1.0)
+ myrack (= 0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "raises a clear error when frozen mode is set and lockfile is missing deps, and does not install any gems" do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack_middleware (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
+ source "https://gem.repo2"
+ gem "myrack_middleware"
+ G
+
+ expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack_middleware-1.0")
+ expect(err).to include("myrack: gemspec specifies = 0.9.1, not in lockfile")
+ expect(the_bundle).not_to include_gems "myrack_middleware 1.0"
+ end
+
+ it "raises a clear error when frozen mode is set and lockfile is missing entries in CHECKSUMS section, and does not install any gems" do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack_middleware (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+
+ CHECKSUMS
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
+ source "https://gem.repo2"
+ gem "myrack_middleware"
+ G
+
+ expect(err).to eq <<~L.strip
+ Your lockfile is missing a CHECKSUMS entry for \"myrack_middleware\", but can't be updated because frozen mode is set
+
+ Run `bundle install` elsewhere and add the updated Gemfile.lock to version control.
+ L
+
+ expect(the_bundle).not_to include_gems "myrack_middleware 1.0"
+ end
+
+ it "raises a clear error when frozen mode is set and lockfile has empty checksums in CHECKSUMS section, and does not install any gems" do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ CHECKSUMS
+ myrack (0.9.1)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ expect(err).to eq <<~L.strip
+ Your lockfile has an empty CHECKSUMS entry for \"myrack\", but can't be updated because frozen mode is set
+
+ Run `bundle install` elsewhere and add the updated Gemfile.lock to version control.
+ L
+
+ expect(the_bundle).not_to include_gems "myrack 0.9.1"
+ end
+
+ it "automatically fixes the lockfile when it's missing deps, they conflict with other locked deps, but conflicts are fixable" do
+ build_repo4 do
+ build_gem "other_dep", "0.9"
+ build_gem "other_dep", "1.0"
+
+ build_gem "myrack", "0.9.1"
+
+ build_gem "myrack_middleware", "1.0" do |s|
+ s.add_dependency "myrack", "= 0.9.1"
+ s.add_dependency "other_dep", "= 0.9"
+ end
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack_middleware (1.0)
+ other_dep (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+ other_dep
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack_middleware"
+ gem "other_dep"
+ G
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (0.9.1)
+ myrack_middleware (1.0)
+ myrack (= 0.9.1)
+ other_dep (= 0.9)
+ other_dep (0.9)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+ other_dep
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically fixes the lockfile when it's missing multiple deps, they conflict with other locked deps, but conflicts are fixable" do
+ build_repo4 do
+ build_gem "other_dep", "0.9"
+ build_gem "other_dep", "1.0"
+
+ build_gem "myrack", "0.9.1"
+
+ build_gem "myrack_middleware", "1.0" do |s|
+ s.add_dependency "myrack", "= 0.9.1"
+ end
+
+ build_gem "another_dep_middleware", "1.0" do |s|
+ s.add_dependency "other_dep", "= 0.9"
+ end
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack_middleware (1.0)
+ another_dep_middleware (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+ another_dep_middleware
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack_middleware"
+ gem "another_dep_middleware"
+ G
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ another_dep_middleware (1.0)
+ other_dep (= 0.9)
+ myrack (0.9.1)
+ myrack_middleware (1.0)
+ myrack (= 0.9.1)
+ other_dep (0.9)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ another_dep_middleware
+ myrack_middleware
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "raises a resolution error when lockfile is missing deps, they conflict with other locked deps, and conflicts are not fixable" do
+ build_repo4 do
+ build_gem "other_dep", "0.9"
+ build_gem "other_dep", "1.0"
+
+ build_gem "myrack", "0.9.1"
+
+ build_gem "myrack_middleware", "1.0" do |s|
+ s.add_dependency "myrack", "= 0.9.1"
+ s.add_dependency "other_dep", "= 0.9"
+ end
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack_middleware (1.0)
+ other_dep (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+ other_dep
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo4"
+ gem "myrack_middleware"
+ gem "other_dep", "1.0"
+ G
+
+ expect(err).to eq <<~ERROR.strip
+ Could not find compatible versions
+
+ Because every version of myrack_middleware depends on other_dep = 0.9
+ and Gemfile depends on myrack_middleware >= 0,
+ other_dep = 0.9 is required.
+ So, because Gemfile depends on other_dep = 1.0,
+ version solving has failed.
+ ERROR
+ end
+
+ it "regenerates a lockfile with no specs" do
+ build_repo4 do
+ build_gem "indirect_dependency", "1.2.3" do |s|
+ s.metadata["funding_uri"] = "https://example.com/donate"
+ end
+
+ build_gem "direct_dependency", "4.5.6" do |s|
+ s.add_dependency "indirect_dependency", ">= 0"
+ end
+ end
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo4/
+ specs:
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ direct_dependency
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "direct_dependency"
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ direct_dependency (4.5.6)
+ indirect_dependency
+ indirect_dependency (1.2.3)
+
+ PLATFORMS
+ #{lockfile_platforms(generic_default_locked_platform || local_platform, defaults: ["ruby"])}
+
+ DEPENDENCIES
+ direct_dependency
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "automatically fixes the lockfile when it's missing deps and the full index is in use" do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack_middleware (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G
+ source "#{file_uri_for(gem_repo2)}"
+ gem "myrack_middleware"
+ G
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: #{file_uri_for(gem_repo2)}/
+ specs:
+ myrack (0.9.1)
+ myrack_middleware (1.0)
+ myrack (= 0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically fixes the lockfile when it includes a gem under the correct GIT section, but also under an incorrect GEM section, with a higher version, and with no explicit Gemfile requirement" do
+ git = build_git "foo"
+
+ gemfile <<~G
+ source "https://gem.repo1/"
+ gem "foo", git: "#{lib_path("foo-1.0")}"
+ G
+
+ # If the lockfile erroneously lists platform versions of the gem
+ # that don't match the locked version of the git repo we should remove them.
+
+ lockfile <<~L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{git.ref_for("main")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ foo (1.1-x86_64-linux-gnu)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(lockfile).to eq <<~L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{git.ref_for("main")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically fixes the lockfile when it includes a gem under the correct GIT section, but also under an incorrect GEM section, with a higher version" do
+ git = build_git "foo"
+
+ gemfile <<~G
+ source "https://gem.repo1/"
+ gem "foo", "= 1.0", git: "#{lib_path("foo-1.0")}"
+ G
+
+ # If the lockfile erroneously lists platform versions of the gem
+ # that don't match the locked version of the git repo we should remove them.
+
+ lockfile <<~L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{git.ref_for("main")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ foo (1.1-x86_64-linux-gnu)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo (= 1.0)!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(lockfile).to eq <<~L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{git.ref_for("main")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo (= 1.0)!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically fixes the lockfile when it has incorrect deps, keeping the locked version" do
+ build_repo4 do
+ build_gem "net-smtp", "0.5.0" do |s|
+ s.add_dependency "net-protocol"
+ end
+
+ build_gem "net-smtp", "0.5.1" do |s|
+ s.add_dependency "net-protocol"
+ end
+
+ build_gem "net-protocol", "0.2.2"
+ end
+
+ gemfile <<~G
+ source "#{file_uri_for(gem_repo4)}"
+ gem "net-smtp"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: #{file_uri_for(gem_repo4)}/
+ specs:
+ net-protocol (0.2.2)
+ net-smtp (0.5.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ net-smtp
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: #{file_uri_for(gem_repo4)}/
+ specs:
+ net-protocol (0.2.2)
+ net-smtp (0.5.0)
+ net-protocol
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ net-smtp
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "successfully updates the lockfile when a new gem is added in the Gemfile includes a gem that shouldn't be included" do
+ build_repo4 do
+ build_gem "logger", "1.7.0"
+ build_gem "rack", "3.2.0"
+ build_gem "net-smtp", "0.5.0"
+ end
+
+ gemfile <<~G
+ source "#{file_uri_for(gem_repo4)}"
+ gem "logger"
+ gem "net-smtp"
+
+ install_if -> { false } do
+ gem 'rack', github: 'rack/rack'
+ end
+ G
+
+ lockfile <<~L
+ GIT
+ remote: https://github.com/rack/rack.git
+ revision: 2fface9ac09fc582a81386becd939c987ad33f99
+ specs:
+ rack (3.2.0)
+
+ GEM
+ remote: #{file_uri_for(gem_repo4)}/
+ specs:
+ logger (1.7.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ logger
+ rack!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(lockfile).to eq <<~L
+ GIT
+ remote: https://github.com/rack/rack.git
+ revision: 2fface9ac09fc582a81386becd939c987ad33f99
+ specs:
+ rack (3.2.0)
+
+ GEM
+ remote: #{file_uri_for(gem_repo4)}/
+ specs:
+ logger (1.7.0)
+ net-smtp (0.5.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ logger
+ net-smtp
+ rack!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ shared_examples_for "a lockfile missing dependent specs" do
+ it "auto-heals" do
+ build_repo4 do
+ build_gem "minitest-bisect", "1.6.0" do |s|
+ s.add_dependency "path_expander", "~> 1.1"
+ end
+
+ build_gem "path_expander", "1.1.1"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "minitest-bisect"
+ G
+
+ # Corrupt lockfile (completely missing path_expander)
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ minitest-bisect (1.6.0)
+
+ PLATFORMS
+ #{platforms}
+
+ DEPENDENCIES
+ minitest-bisect
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ cache_gems "minitest-bisect-1.6.0", "path_expander-1.1.1", gem_repo: gem_repo4
+ bundle :install
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ minitest-bisect (1.6.0)
+ path_expander (~> 1.1)
+ path_expander (1.1.1)
+
+ PLATFORMS
+ #{platforms}
+
+ DEPENDENCIES
+ minitest-bisect
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "with just specific platform" do
+ let(:platforms) { lockfile_platforms }
+
+ it_behaves_like "a lockfile missing dependent specs"
+ end
+
+ context "with both ruby and specific platform" do
+ let(:platforms) { lockfile_platforms("ruby") }
+
+ it_behaves_like "a lockfile missing dependent specs"
+ end
+
+ it "auto-heals when the lockfile is missing specs" do
+ build_repo4 do
+ build_gem "minitest-bisect", "1.6.0" do |s|
+ s.add_dependency "path_expander", "~> 1.1"
+ end
+
+ build_gem "path_expander", "1.1.1"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "minitest-bisect"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ minitest-bisect (1.6.0)
+ path_expander (~> 1.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ minitest-bisect
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install --verbose"
+ expect(out).to include("re-resolving dependencies because your lockfile includes \"minitest-bisect\" but not some of its dependencies")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ minitest-bisect (1.6.0)
+ path_expander (~> 1.1)
+ path_expander (1.1.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ minitest-bisect
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ describe "a line ending" do
+ def set_lockfile_mtime_to_known_value
+ time = Time.local(2000, 1, 1, 0, 0, 0)
+ File.utime(time, time, bundled_app_lock)
+ end
+ before(:each) do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+ set_lockfile_mtime_to_known_value
+ end
+
+ it "generates Gemfile.lock with \\n line endings" do
+ expect(File.read(bundled_app_lock)).not_to match("\r\n")
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+
+ context "during updates" do
+ it "preserves Gemfile.lock \\n line endings" do
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ expect { bundle "update", all: true }.to change { File.mtime(bundled_app_lock) }
+ expect(File.read(bundled_app_lock)).not_to match("\r\n")
+ expect(the_bundle).to include_gems "myrack 1.2"
+ end
+
+ it "preserves Gemfile.lock \\n\\r line endings" do
+ skip "needs to be adapted" if Gem.win_platform?
+
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ win_lock = File.read(bundled_app_lock).gsub(/\n/, "\r\n")
+ File.open(bundled_app_lock, "wb") {|f| f.puts(win_lock) }
+ set_lockfile_mtime_to_known_value
+
+ expect { bundle "update", all: true }.to change { File.mtime(bundled_app_lock) }
+ expect(File.read(bundled_app_lock)).to match("\r\n")
+
+ expect(the_bundle).to include_gems "myrack 1.2"
+ end
+ end
+
+ context "when nothing changes" do
+ it "preserves Gemfile.lock \\n line endings" do
+ expect do
+ ruby <<-RUBY
+ require 'bundler'
+ Bundler.setup
+ RUBY
+ end.not_to change { File.mtime(bundled_app_lock) }
+ end
+
+ it "preserves Gemfile.lock \\n\\r line endings" do
+ win_lock = File.read(bundled_app_lock).gsub(/\n/, "\r\n")
+ File.open(bundled_app_lock, "wb") {|f| f.puts(win_lock) }
+ set_lockfile_mtime_to_known_value
+
+ expect do
+ ruby <<-RUBY
+ require 'bundler'
+ Bundler.setup
+ RUBY
+ end.not_to change { File.mtime(bundled_app_lock) }
+ end
+ end
+ end
+
+ it "refuses to install if Gemfile.lock contains conflict markers" do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2//
+ specs:
+ <<<<<<<
+ myrack (1.0.0)
+ =======
+ myrack (1.0.1)
+ >>>>>>>
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2/"
+ gem "myrack"
+ G
+
+ expect(err).to match(/your Gemfile.lock contains merge conflicts/i)
+ expect(err).to match(/git checkout HEAD -- Gemfile.lock/i)
+ end
+
+ private
+
+ def previous_major(version)
+ version.split(".").map.with_index {|v, i| i == 0 ? v.to_i - 1 : v }.join(".")
+ end
+end
diff --git a/spec/bundler/other/cli_dispatch_spec.rb b/spec/bundler/other/cli_dispatch_spec.rb
new file mode 100644
index 0000000000..a2c745b070
--- /dev/null
+++ b/spec/bundler/other/cli_dispatch_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle command names" do
+ it "work when given fully" do
+ bundle "install", raise_on_error: false
+ expect(err).to eq("Could not locate Gemfile")
+ expect(stdboth).not_to include("Ambiguous command")
+ end
+
+ it "work when not ambiguous" do
+ bundle "ins", raise_on_error: false
+ expect(err).to eq("Could not locate Gemfile")
+ expect(stdboth).not_to include("Ambiguous command")
+ end
+
+ it "print a friendly error when ambiguous" do
+ bundle "in", raise_on_error: false
+ expect(err).to eq("Ambiguous command in matches [info, init, install]")
+ end
+end
diff --git a/spec/bundler/other/cli_man_pages_spec.rb b/spec/bundler/other/cli_man_pages_spec.rb
new file mode 100644
index 0000000000..4e8f155309
--- /dev/null
+++ b/spec/bundler/other/cli_man_pages_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle commands" do
+ it "expects all commands to have all options and subcommands documented" do
+ check_commands!(Bundler::CLI)
+
+ Bundler::CLI.subcommand_classes.each_value do |klass|
+ check_commands!(klass)
+ end
+ end
+
+ private
+
+ def check_commands!(command_class)
+ command_class.commands.each do |command_name, command|
+ if command.is_a?(Bundler::Thor::HiddenCommand)
+ man_page = man_page(command_name)
+ expect(man_page).not_to exist
+ expect(main_man_page.read).not_to include("bundle #{command_name}")
+ elsif command_class == Bundler::CLI
+ man_page = man_page(command_name)
+ expect(man_page).to exist
+
+ check_options!(command, man_page)
+ else
+ man_page = man_page(command.ancestor_name)
+ expect(man_page).to exist
+
+ check_options!(command, man_page)
+ check_subcommand!(command_name, man_page)
+ end
+ end
+ end
+
+ def check_options!(command, man_page)
+ command.options.each do |_, option|
+ check_option!(option, man_page)
+ end
+ end
+
+ def check_option!(option, man_page)
+ man_page_content = man_page.read
+
+ aliases = option.aliases
+ formatted_aliases = aliases.sort.map {|name| "`#{name}`" }.join(", ") if aliases
+
+ help = if option.type == :boolean
+ "* #{append_aliases("`#{option.switch_name}`", formatted_aliases)}:"
+ elsif option.enum
+ formatted_aliases = "`#{option.switch_name}`" if aliases.empty? && option.lazy_default
+ "* #{prepend_aliases(option.enum.sort.map {|enum| "`#{option.switch_name}=#{enum}`" }.join(", "), formatted_aliases)}:"
+ else
+ names = [option.switch_name, *aliases]
+ value =
+ case option.type
+ when :array then "<list>"
+ when :numeric then "<number>"
+ else option.name.upcase
+ end
+
+ value = option.type != :numeric && option.lazy_default ? "[=#{value}]" : "=#{value}"
+
+ "* #{names.map {|name| "`#{name}#{value}`" }.join(", ")}:"
+ end
+
+ if option.banner.include?("(removed)")
+ expect(man_page_content).not_to include(help)
+ else
+ expect(man_page_content).to include(help)
+ end
+ end
+
+ def check_subcommand!(name, man_page)
+ expect(man_page.read).to match(name)
+ end
+
+ def append_aliases(text, aliases)
+ return text if aliases.empty?
+
+ "#{text}, #{aliases}"
+ end
+
+ def prepend_aliases(text, aliases)
+ return text if aliases.empty?
+
+ "#{aliases}, #{text}"
+ end
+
+ def man_page_content(command_name)
+ man_page(command_name).read
+ end
+
+ def man_page(command_name)
+ source_root.join("lib/bundler/man/bundle-#{command_name}.1.ronn")
+ end
+
+ def main_man_page
+ source_root.join("lib/bundler/man/bundle.1.ronn")
+ end
+end
diff --git a/spec/bundler/other/ext_spec.rb b/spec/bundler/other/ext_spec.rb
new file mode 100644
index 0000000000..a883eefe06
--- /dev/null
+++ b/spec/bundler/other/ext_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+RSpec.describe "Gem::Specification#installable_on_platform?" do
+ it "does not match platforms other than the gem platform" do
+ darwin = gem "lol", "1.0", "platform_specific-1.0-x86-darwin-10"
+ expect(darwin.installable_on_platform?(pl("java"))).to eq(false)
+ end
+
+ context "when platform is a string" do
+ it "matches when platform is a string" do
+ lazy_spec = Bundler::LazySpecification.new("lol", "1.0", "universal-mingw32")
+ expect(lazy_spec.installable_on_platform?(pl("x86-mingw32"))).to eq(true)
+ expect(lazy_spec.installable_on_platform?(pl("x64-mingw32"))).to eq(true)
+ end
+ end
+end
+
+RSpec.describe "Gem::SourceIndex#refresh!" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "does not explode when called" do
+ run "Gem.source_index.refresh!", raise_on_error: false
+ run "Gem::SourceIndex.new([]).refresh!", raise_on_error: false
+ end
+end
+
+RSpec.describe "Gem::NameTuple" do
+ describe "#initialize" do
+ it "creates a Gem::NameTuple with equality regardless of platform type" do
+ gem_platform = Gem::NameTuple.new "a", v("1"), pl("x86_64-linux")
+ str_platform = Gem::NameTuple.new "a", v("1"), "x86_64-linux"
+ expect(gem_platform).to eq(str_platform)
+ expect(gem_platform.hash).to eq(str_platform.hash)
+ expect(gem_platform.to_a).to eq(str_platform.to_a)
+ end
+ end
+
+ describe "#lock_name" do
+ it "returns the lock name" do
+ expect(Gem::NameTuple.new("a", v("1.0.0"), pl("x86_64-linux")).lock_name).to eq("a (1.0.0-x86_64-linux)")
+ expect(Gem::NameTuple.new("a", v("1.0.0"), "ruby").lock_name).to eq("a (1.0.0)")
+ expect(Gem::NameTuple.new("a", v("1.0.0")).lock_name).to eq("a (1.0.0)")
+ end
+ end
+end
diff --git a/spec/bundler/other/major_deprecation_spec.rb b/spec/bundler/other/major_deprecation_spec.rb
new file mode 100644
index 0000000000..ab7589d698
--- /dev/null
+++ b/spec/bundler/other/major_deprecation_spec.rb
@@ -0,0 +1,798 @@
+# frozen_string_literal: true
+
+RSpec.describe "major deprecations" do
+ let(:warnings) { err }
+
+ describe "Bundler" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ describe ".clean_env" do
+ before do
+ source = "Bundler.clean_env"
+ bundle "exec ruby -e #{source.dump}", raise_on_error: false
+ end
+
+ it "is removed in favor of .unbundled_env and shows a helpful error message about it" do
+ expect(err).to include \
+ "`Bundler.clean_env` has been removed in favor of `Bundler.unbundled_env`. " \
+ "If you instead want the environment before bundler was originally loaded, use `Bundler.original_env`" \
+ end
+ end
+
+ describe ".with_clean_env" do
+ before do
+ source = "Bundler.with_clean_env {}"
+ bundle "exec ruby -e #{source.dump}", raise_on_error: false
+ end
+
+ it "is removed in favor of .unbundled_env and shows a helpful error message about it" do
+ expect(err).to include(
+ "`Bundler.with_clean_env` has been removed in favor of `Bundler.with_unbundled_env`. " \
+ "If you instead want the environment before bundler was originally loaded, use `Bundler.with_original_env`"
+ )
+ end
+ end
+
+ describe ".clean_system" do
+ before do
+ source = "Bundler.clean_system('ls')"
+ bundle "exec ruby -e #{source.dump}", raise_on_error: false
+ end
+
+ it "is removed in favor of .unbundled_system and shows a helpful error message about it" do
+ expect(err).to include(
+ "`Bundler.clean_system` has been removed in favor of `Bundler.unbundled_system`. " \
+ "If you instead want to run the command in the environment before bundler was originally loaded, use `Bundler.original_system`" \
+ )
+ end
+ end
+
+ describe ".clean_exec" do
+ before do
+ source = "Bundler.clean_exec('ls')"
+ bundle "exec ruby -e #{source.dump}", raise_on_error: false
+ end
+
+ it "is removed in favor of .unbundled_exec and shows a helpful error message about it" do
+ expect(err).to include(
+ "`Bundler.clean_exec` has been removed in favor of `Bundler.unbundled_exec`. " \
+ "If you instead want to exec to a command in the environment before bundler was originally loaded, use `Bundler.original_exec`" \
+ )
+ end
+ end
+
+ describe ".environment" do
+ before do
+ source = "Bundler.environment"
+ bundle "exec ruby -e #{source.dump}", raise_on_error: false
+ end
+
+ it "is removed in favor of .load and shows a helpful error message about it" do
+ expect(err).to include "Bundler.environment has been removed in favor of Bundler.load"
+ end
+ end
+ end
+
+ describe "bundle exec --no-keep-file-descriptors" do
+ before do
+ bundle "exec --no-keep-file-descriptors -e 1", raise_on_error: false
+ end
+
+ it "is removed and shows a helpful error message about it" do
+ expect(err).to include "The `--no-keep-file-descriptors` has been removed. `bundle exec` no longer mess with your file descriptors. Close them in the exec'd script if you need to"
+ end
+ end
+
+ describe "bundle update --quiet" do
+ it "does not print any deprecations" do
+ bundle :update, quiet: true, raise_on_error: false
+ expect(deprecations).to be_empty
+ end
+ end
+
+ context "bundle check --path" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "check --path vendor/bundle", raise_on_error: false
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "The `--path` flag has been removed because it relied on being " \
+ "remembered across bundler invocations, which bundler no longer " \
+ "does. Instead please use `bundle config set path 'vendor/bundle'`, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ context "bundle check --path=" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "check --path=vendor/bundle", raise_on_error: false
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "The `--path` flag has been removed because it relied on being " \
+ "remembered across bundler invocations, which bundler no longer " \
+ "does. Instead please use `bundle config set path 'vendor/bundle'`, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ context "bundle binstubs --path=" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack --path=binpath", raise_on_error: false
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "The `--path` flag has been removed because it relied on being " \
+ "remembered across bundler invocations, which bundler no longer " \
+ "does. Instead please use `bundle config set bin 'binpath'`, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ context "bundle cache --all" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "cache --all --verbose", raise_on_error: false
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "The `--all` flag has been removed because it relied on being " \
+ "remembered across bundler invocations, which bundler no longer " \
+ "does. Instead please use `bundle config set cache_all true`, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ context "bundle cache --no-all" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "cache --no-all", raise_on_error: false
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "The `--no-all` flag has been removed because it relied on being " \
+ "remembered across bundler invocations, which bundler no longer " \
+ "does. Instead please use `bundle config set cache_all false`, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ context "bundle cache --path" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "cache --path foo", raise_on_error: false
+ end
+
+ it "should print a removal error" do
+ expect(err).to include(
+ "The `--path` flag has been removed because its semantics were unclear. " \
+ "Use `bundle config cache_path` to configure the path of your cache of gems, " \
+ "and `bundle config path` to configure the path where your gems are installed, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ context "bundle cache --path=" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "cache --path=foo", raise_on_error: false
+ end
+
+ it "should print a deprecation warning" do
+ expect(err).to include(
+ "The `--path` flag has been removed because its semantics were unclear. " \
+ "Use `bundle config cache_path` to configure the path of your cache of gems, " \
+ "and `bundle config path` to configure the path where your gems are installed, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ context "bundle cache --frozen" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "cache --frozen", raise_on_error: false
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "The `--frozen` flag has been removed because it relied on being " \
+ "remembered across bundler invocations, which bundler no longer " \
+ "does. Instead please use `bundle config set frozen true`, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ context "bundle cache --no-prune" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "cache --no-prune", raise_on_error: false
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "The `--no-prune` flag has been removed because it relied on being " \
+ "remembered across bundler invocations, which bundler no longer " \
+ "does. Instead please use `bundle config set no_prune true`, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ describe "bundle config" do
+ describe "old list interface" do
+ before do
+ bundle "config"
+ end
+
+ it "warns", bundler: "4" do
+ expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config list` instead.")
+ end
+
+ pending "fails with a helpful error", bundler: "5"
+ end
+
+ describe "old get interface" do
+ before do
+ bundle "config waka", raise_on_error: false
+ end
+
+ it "warns", bundler: "4" do
+ expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config get waka` instead.")
+ end
+
+ pending "fails with a helpful error", bundler: "5"
+ end
+
+ describe "old set interface" do
+ before do
+ bundle "config waka wakapun"
+ end
+
+ it "warns", bundler: "4" do
+ expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config set waka wakapun` instead.")
+ end
+
+ pending "fails with a helpful error", bundler: "5"
+ end
+
+ describe "old set interface with --local" do
+ before do
+ bundle "config --local waka wakapun"
+ end
+
+ it "warns", bundler: "4" do
+ expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config set --local waka wakapun` instead.")
+ end
+
+ pending "fails with a helpful error", bundler: "5"
+ end
+
+ describe "old set interface with --global" do
+ before do
+ bundle "config --global waka wakapun"
+ end
+
+ it "warns", bundler: "4" do
+ expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config set --global waka wakapun` instead.")
+ end
+
+ pending "fails with a helpful error", bundler: "5"
+ end
+
+ describe "old unset interface" do
+ before do
+ bundle "config --delete waka"
+ end
+
+ it "warns", bundler: "4" do
+ expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config unset waka` instead.")
+ end
+
+ pending "fails with a helpful error", bundler: "5"
+ end
+
+ describe "old unset interface with --local" do
+ before do
+ bundle "config --delete --local waka"
+ end
+
+ it "warns", bundler: "4" do
+ expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config unset --local waka` instead.")
+ end
+
+ pending "fails with a helpful error", bundler: "5"
+ end
+
+ describe "old unset interface with --global" do
+ before do
+ bundle "config --delete --global waka"
+ end
+
+ it "warns", bundler: "4" do
+ expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config unset --global waka` instead.")
+ end
+
+ pending "fails with a helpful error", bundler: "5"
+ end
+ end
+
+ describe "bundle update" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "warns when no options are given", bundler: "4" do
+ bundle "update"
+ expect(deprecations).to include("Pass --all to `bundle update` to update everything")
+ end
+
+ pending "fails with a helpful error when no options are given", bundler: "5"
+
+ it "does not warn when --all is passed" do
+ bundle "update --all"
+ expect(deprecations).to be_empty
+ end
+ end
+
+ describe "bundle install --binstubs" do
+ before do
+ install_gemfile <<-G, binstubs: true, raise_on_error: false
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include("The --binstubs option has been removed in favor of `bundle binstubs --all`")
+ end
+ end
+
+ context "bundle install with both gems.rb and Gemfile present" do
+ it "should not warn about gems.rb" do
+ gemfile "gems.rb", <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle :install
+ expect(deprecations).to be_empty
+ end
+
+ it "should print a proper warning, and use gems.rb" do
+ gemfile "gems.rb", "source 'https://gem.repo1'"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(warnings).to include(
+ "Multiple gemfiles (gems.rb and Gemfile) detected. Make sure you remove Gemfile and Gemfile.lock since bundler is ignoring them in favor of gems.rb and gems.locked."
+ )
+
+ expect(the_bundle).not_to include_gem "myrack 1.0"
+ end
+ end
+
+ context "bundle install with flags" do
+ before do
+ bundle_config "path vendor/bundle"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ {
+ "clean" => ["clean", "true"],
+ "deployment" => ["deployment", "true"],
+ "frozen" => ["frozen", "true"],
+ "no-deployment" => ["deployment", "false"],
+ "no-prune" => ["no_prune", "true"],
+ "path" => ["path", "'vendor/bundle'"],
+ "shebang" => ["shebang", "'ruby27'"],
+ "system" => ["path.system", "true"],
+ "without" => ["without", "'development'"],
+ "with" => ["with", "'development'"],
+ }.each do |name, expectations|
+ option_name, value = *expectations
+ flag_name = "--#{name}"
+ args = %w[true false].include?(value) ? flag_name : "#{flag_name} #{value}"
+
+ context "with the #{flag_name} flag" do
+ before do
+ bundle "install" # to create a lockfile, which deployment or frozen need
+
+ bundle "install #{args}", raise_on_error: false
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "The `#{flag_name}` flag has been removed because it relied on " \
+ "being remembered across bundler invocations, which bundler no " \
+ "longer does. Instead please use `bundle config set " \
+ "#{option_name} #{value}`, and stop using this flag"
+ )
+ end
+ end
+ end
+ end
+
+ context "bundle install with multiple sources" do
+ before do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo3"
+ source "https://gem.repo1"
+ G
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "This Gemfile contains multiple global sources. " \
+ "Each source after the first must include a block to indicate which gems " \
+ "should come from that source"
+ )
+ end
+
+ it "doesn't show lockfile deprecations if there's a lockfile" do
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo3/
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ bundle "install", raise_on_error: false
+
+ expect(err).to include(
+ "This Gemfile contains multiple global sources. " \
+ "Each source after the first must include a block to indicate which gems " \
+ "should come from that source"
+ )
+ expect(err).not_to include(
+ "Your lockfile contains a single rubygems source section with multiple remotes, which is insecure. " \
+ "Make sure you run `bundle install` in non frozen mode and commit the result to make your lockfile secure."
+ )
+ bundle_config "frozen true"
+ bundle "install", raise_on_error: false
+
+ expect(err).to include(
+ "This Gemfile contains multiple global sources. " \
+ "Each source after the first must include a block to indicate which gems " \
+ "should come from that source"
+ )
+ expect(err).not_to include(
+ "Your lockfile contains a single rubygems source section with multiple remotes, which is insecure. " \
+ "Make sure you run `bundle install` in non frozen mode and commit the result to make your lockfile secure."
+ )
+ end
+ end
+
+ context "bundle install with a lockfile with a single rubygems section with multiple remotes" do
+ before do
+ build_repo3 do
+ build_gem "myrack", "0.9.1"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ source "https://gem.repo3" do
+ gem 'myrack'
+ end
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo1/
+ remote: https://gem.repo3/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ myrack!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "shows an error" do
+ bundle "install", raise_on_error: false
+
+ expect(err).to include("Your lockfile contains a single rubygems source section with multiple remotes, which is insecure. Make sure you run `bundle install` in non frozen mode and commit the result to make your lockfile secure.")
+ end
+ end
+
+ context "bundle install with a lockfile including X64_MINGW_LEGACY platform" do
+ before do
+ gemfile <<~G
+ source "https://gem.repo1"
+ gem "rake"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://rubygems.org/
+ specs:
+ rake (10.3.2)
+
+ PLATFORMS
+ ruby
+ x64-mingw32
+
+ DEPENDENCIES
+ rake
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "warns a helpful error" do
+ bundle "install", raise_on_error: false
+
+ expect(err).to include("Found x64-mingw32 in lockfile, which is deprecated and will be removed in the future.")
+ end
+ end
+
+ context "with a global path source" do
+ before do
+ build_lib "foo"
+
+ install_gemfile <<-G, raise_on_error: false
+ path "#{lib_path("foo-1.0")}"
+ gem 'foo'
+ G
+ end
+
+ it "shows an error" do
+ expect(err).to include("You can no longer specify a path source by itself")
+ end
+ end
+
+ context "when Bundler.setup is run in a ruby script" do
+ before do
+ create_file "gems.rb", "source 'https://gem.repo1'"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :group => :test
+ G
+
+ ruby <<-RUBY
+ require 'bundler'
+
+ Bundler.setup
+ Bundler.setup
+ RUBY
+ end
+
+ it "should print a single deprecation warning" do
+ expect(warnings).to include(
+ "Multiple gemfiles (gems.rb and Gemfile) detected. Make sure you remove Gemfile and Gemfile.lock since bundler is ignoring them in favor of gems.rb and gems.locked."
+ )
+ end
+ end
+
+ context "when `bundler/deployment` is required in a ruby script" do
+ before do
+ ruby <<-RUBY, raise_on_error: false
+ require 'bundler/deployment'
+ RUBY
+ end
+
+ it "should print a capistrano deprecation warning" do
+ expect(err).to include("Bundler no longer integrates " \
+ "with Capistrano, but Capistrano provides " \
+ "its own integration with Bundler via the " \
+ "capistrano-bundler gem. Use it instead.")
+ end
+ end
+
+ context "when `bundler/capistrano` is required in a ruby script" do
+ before do
+ ruby <<-RUBY, raise_on_error: false
+ require 'bundler/capistrano'
+ RUBY
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include("[REMOVED] The Bundler task for Capistrano. Please use https://github.com/capistrano/bundler")
+ end
+ end
+
+ context "when `bundler/vlad` is required in a ruby script" do
+ before do
+ ruby <<-RUBY, raise_on_error: false
+ require 'bundler/vlad'
+ RUBY
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include("[REMOVED] The Bundler task for Vlad")
+ end
+ end
+
+ context "bundle show" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ context "with --outdated flag" do
+ before do
+ bundle "show --outdated", raise_on_error: false
+ end
+
+ it "fails with a helpful message" do
+ expect(err).to include("the `--outdated` flag to `bundle show` has been removed in favor of `bundle show --verbose`")
+ end
+ end
+ end
+
+ context "bundle remove" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ context "with --install" do
+ it "fails with a helpful message" do
+ bundle "remove myrack --install", raise_on_error: false
+
+ expect(err).to include "The `--install` flag has been removed. `bundle install` is triggered by default."
+ end
+ end
+ end
+
+ context "bundle viz" do
+ before do
+ bundle "viz", raise_on_error: false
+ end
+
+ it "fails with a helpful message" do
+ expect(err).to include "The `viz` command has been renamed to `graph` and moved to a plugin. See https://github.com/rubygems/bundler-graph"
+ end
+ end
+
+ context "bundle inject" do
+ before do
+ bundle "inject", raise_on_error: false
+ end
+
+ it "fails with a helpful message" do
+ expect(err).to include "The `inject` command has been replaced by the `add` command"
+ end
+ end
+
+ context "bundle plugin install --local_git" do
+ before do
+ build_git "foo" do |s|
+ s.write "plugins.rb"
+ end
+ end
+
+ it "fails with a helpful message" do
+ bundle "plugin install foo --local_git #{lib_path("foo-1.0")}", raise_on_error: false
+
+ expect(err).to include "--local_git has been removed, use --git"
+ end
+ end
+
+ describe "removing rubocop" do
+ before do
+ bundle_config_global "gem.mit false"
+ bundle_config_global "gem.test false"
+ bundle_config_global "gem.coc false"
+ bundle_config_global "gem.ci false"
+ bundle_config_global "gem.changelog false"
+ end
+
+ context "bundle gem --rubocop" do
+ before do
+ bundle "gem my_new_gem --rubocop", raise_on_error: false
+ end
+
+ it "prints an error" do
+ expect(err).to include \
+ "--rubocop has been removed, use --linter=rubocop"
+ end
+ end
+
+ context "bundle gem --no-rubocop" do
+ before do
+ bundle "gem my_new_gem --no-rubocop", raise_on_error: false
+ end
+
+ it "prints an error" do
+ expect(err).to include \
+ "--no-rubocop has been removed, use --no-linter"
+ end
+ end
+ end
+
+ context " bundle gem --ext parameter with no value" do
+ it "prints error when used before gem name" do
+ bundle "gem --ext foo", raise_on_error: false
+ expect(err).to include "Extensions can now be generated using C or Rust, so `--ext` with no arguments has been removed. Please select a language, e.g. `--ext=rust` to generate a Rust extension."
+ end
+
+ it "prints error when used after gem name" do
+ bundle "gem foo --ext", raise_on_error: false
+ expect(err).to include "Extensions can now be generated using C or Rust, so `--ext` with no arguments has been removed. Please select a language, e.g. `--ext=rust` to generate a Rust extension."
+ end
+ end
+end
diff --git a/spec/bundler/plugins/command_spec.rb b/spec/bundler/plugins/command_spec.rb
new file mode 100644
index 0000000000..05d535a70c
--- /dev/null
+++ b/spec/bundler/plugins/command_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+RSpec.describe "command plugins" do
+ before do
+ build_repo2 do
+ build_plugin "command-mah" do |s|
+ s.write "plugins.rb", <<-RUBY
+ module Mah
+ class Plugin < Bundler::Plugin::API
+ command "mahcommand" # declares the command
+
+ def exec(command, args)
+ puts "MahHello"
+ end
+ end
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install command-mah --source https://gem.repo2"
+ end
+
+ it "executes without arguments" do
+ expect(out).to include("Installed plugin command-mah")
+
+ bundle "mahcommand"
+ expect(out).to eq("MahHello")
+ end
+
+ it "accepts the arguments" do
+ update_repo2 do
+ build_plugin "the-echoer" do |s|
+ s.write "plugins.rb", <<-RUBY
+ module Resonance
+ class Echoer
+ # Another method to declare the command
+ Bundler::Plugin::API.command "echo", self
+
+ def exec(command, args)
+ puts "You gave me \#{args.join(", ")}"
+ end
+ end
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install the-echoer --source https://gem.repo2"
+ expect(out).to include("Installed plugin the-echoer")
+
+ bundle "echo tacos tofu lasange"
+ expect(out).to eq("You gave me tacos, tofu, lasange")
+ end
+
+ it "passes help flag to plugin" do
+ update_repo2 do
+ build_plugin "helpful" do |s|
+ s.write "plugins.rb", <<-RUBY
+ module Helpful
+ class Command
+ Bundler::Plugin::API.command "greet", self
+
+ def exec(command, args)
+ if args.include?("--help") || args.include?("-h")
+ puts "Usage: bundle greet [NAME]"
+ else
+ puts "Hello"
+ end
+ end
+ end
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install helpful --source https://gem.repo2"
+ expect(out).to include("Installed plugin helpful")
+
+ bundle "greet --help"
+ expect(out).to eq("Usage: bundle greet [NAME]")
+
+ bundle "greet -h"
+ expect(out).to eq("Usage: bundle greet [NAME]")
+
+ bundle "help greet"
+ expect(out).to eq("Usage: bundle greet [NAME]")
+ end
+
+ it "raises error on redeclaration of command" do
+ update_repo2 do
+ build_plugin "copycat" do |s|
+ s.write "plugins.rb", <<-RUBY
+ module CopyCat
+ class Cheater < Bundler::Plugin::API
+ command "mahcommand", self
+
+ def exec(command, args)
+ end
+ end
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install copycat --source https://gem.repo2", raise_on_error: false
+
+ expect(out).not_to include("Installed plugin copycat")
+
+ expect(err).to include("Failed to install plugin `copycat`, due to Bundler::Plugin::Index::CommandConflict (Command(s) `mahcommand` declared by copycat are already registered.)")
+ end
+end
diff --git a/spec/bundler/plugins/hook_spec.rb b/spec/bundler/plugins/hook_spec.rb
new file mode 100644
index 0000000000..ad8a4daeff
--- /dev/null
+++ b/spec/bundler/plugins/hook_spec.rb
@@ -0,0 +1,331 @@
+# frozen_string_literal: true
+
+RSpec.describe "hook plugins" do
+ context "before-install-all hook" do
+ before do
+ build_repo2 do
+ build_plugin "before-install-all-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_INSTALL_ALL do |deps|
+ puts "gems to be installed \#{deps.map(&:name).join(", ")}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install before-install-all-plugin --source https://gem.repo2"
+ end
+
+ it "runs before all rubygems are installed" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "myrack"
+ G
+
+ expect(out).to include "gems to be installed rake, myrack"
+ end
+ end
+
+ context "before-install hook" do
+ before do
+ build_repo2 do
+ build_plugin "before-install-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_INSTALL do |spec_install|
+ puts "installing gem \#{spec_install.name}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install before-install-plugin --source https://gem.repo2"
+ end
+
+ it "runs before each rubygem is installed" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "myrack"
+ G
+
+ expect(out).to include "installing gem rake"
+ expect(out).to include "installing gem myrack"
+ end
+ end
+
+ context "after-install-all hook" do
+ before do
+ build_repo2 do
+ build_plugin "after-install-all-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_INSTALL_ALL do |deps|
+ puts "installed gems \#{deps.map(&:name).join(", ")}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install after-install-all-plugin --source https://gem.repo2"
+ end
+
+ it "runs after each all rubygems are installed" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "myrack"
+ G
+
+ expect(out).to include "installed gems rake, myrack"
+ end
+ end
+
+ context "after-install hook" do
+ before do
+ build_repo2 do
+ build_plugin "after-install-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_INSTALL do |spec_install|
+ puts "installed gem \#{spec_install.name} : \#{spec_install.state}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install after-install-plugin --source https://gem.repo2"
+ end
+
+ it "runs after each rubygem is installed" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "myrack"
+ G
+
+ expect(out).to include "installed gem rake : installed"
+ expect(out).to include "installed gem myrack : installed"
+ end
+ end
+
+ context "before-require-all hook" do
+ before do
+ build_repo2 do
+ build_plugin "before-require-all-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_REQUIRE_ALL do |deps|
+ puts "gems to be required \#{deps.map(&:name).join(", ")}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install before-require-all-plugin --source https://gem.repo2"
+ end
+
+ it "runs before all rubygems are required" do
+ install_gemfile_and_bundler_require
+ expect(out).to include "gems to be required rake, myrack"
+ end
+ end
+
+ context "before-require hook" do
+ before do
+ build_repo2 do
+ build_plugin "before-require-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_REQUIRE do |dep|
+ puts "requiring gem \#{dep.name}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install before-require-plugin --source https://gem.repo2"
+ end
+
+ it "runs before each rubygem is required" do
+ install_gemfile_and_bundler_require
+ expect(out).to include "requiring gem rake"
+ expect(out).to include "requiring gem myrack"
+ end
+ end
+
+ context "after-require-all hook" do
+ before do
+ build_repo2 do
+ build_plugin "after-require-all-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_REQUIRE_ALL do |deps|
+ puts "required gems \#{deps.map(&:name).join(", ")}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install after-require-all-plugin --source https://gem.repo2"
+ end
+
+ it "runs after all rubygems are required" do
+ install_gemfile_and_bundler_require
+ expect(out).to include "required gems rake, myrack"
+ end
+ end
+
+ context "after-require hook" do
+ before do
+ build_repo2 do
+ build_plugin "after-require-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_REQUIRE do |dep|
+ puts "required gem \#{dep.name}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install after-require-plugin --source https://gem.repo2"
+ end
+
+ it "runs after each rubygem is required" do
+ install_gemfile_and_bundler_require
+ expect(out).to include "required gem rake"
+ expect(out).to include "required gem myrack"
+ end
+ end
+
+ context "before-eval hook" do
+ before do
+ build_repo2 do
+ build_plugin "before-eval-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_EVAL do |gemfile, lockfile|
+ puts "hooked eval start of \#{File.basename(gemfile)} to \#{File.basename(lockfile)}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install before-eval-plugin --source https://gem.repo2"
+ end
+
+ it "runs before the Gemfile is evaluated" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ G
+
+ expect(out).to include "hooked eval start of Gemfile to Gemfile.lock"
+ end
+ end
+
+ context "after-eval hook" do
+ before do
+ build_repo2 do
+ build_plugin "after-eval-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_EVAL do |defn|
+ puts "hooked eval after with gems \#{defn.dependencies.map(&:name).join(", ")}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install after-eval-plugin --source https://gem.repo2"
+ end
+
+ it "runs after the Gemfile is evaluated" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "rake"
+ G
+
+ expect(out).to include "hooked eval after with gems myrack, rake"
+ end
+ end
+
+ context "before-fetch and after-fetch hooks" do
+ before do
+ build_repo2 do
+ build_plugin "fetch-timing-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ @timing_start = nil
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_FETCH do |spec|
+ @timing_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ puts "gem \#{spec.name} started fetch at \#{@timing_start}"
+ end
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_FETCH do |spec|
+ timing_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ puts "gem \#{spec.name} took \#{timing_end - @timing_start} to fetch"
+ @timing_start = nil
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install fetch-timing-plugin --source https://gem.repo2"
+ end
+
+ it "runs around each gem download" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "myrack"
+ G
+
+ expect(out).to include "gem rake started fetch at"
+ expect(out).to match(/gem rake took \d+\.\d+ to fetch/)
+ expect(out).to include "gem myrack started fetch at"
+ expect(out).to match(/gem myrack took \d+\.\d+ to fetch/)
+ end
+ end
+
+ context "before-git-fetch and after-git-fetch hooks" do
+ before do
+ build_repo2 do
+ build_plugin "git-fetch-timing-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ @timing_start = nil
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GIT_BEFORE_FETCH do |source|
+ @timing_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ puts "git source \#{source.name} started fetch at \#{@timing_start}"
+ end
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GIT_AFTER_FETCH do |source|
+ timing_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ puts "git source \#{source.name} took \#{timing_end - @timing_start} to fetch"
+ @timing_start = nil
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install git-fetch-timing-plugin --source https://gem.repo2"
+ end
+
+ it "runs around each git source fetch" do
+ build_git "foo", "1.0", path: lib_path("foo")
+
+ relative_path = lib_path("foo").relative_path_from(bundled_app)
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo1"
+ gem "foo", :git => "#{relative_path}"
+ G
+
+ expect(out).to include "git source foo started fetch at"
+ expect(out).to match(/git source foo took \d+\.\d+ to fetch/)
+ end
+ end
+
+ def install_gemfile_and_bundler_require
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "myrack"
+ G
+
+ ruby <<-RUBY
+ require "bundler"
+ Bundler.require
+ RUBY
+ end
+end
diff --git a/spec/bundler/plugins/install_spec.rb b/spec/bundler/plugins/install_spec.rb
new file mode 100644
index 0000000000..dcacf764be
--- /dev/null
+++ b/spec/bundler/plugins/install_spec.rb
@@ -0,0 +1,466 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundler plugin install" do
+ before do
+ build_repo2 do
+ build_plugin "foo"
+ build_plugin "kung-foo"
+ end
+ end
+
+ it "shows proper message when gem in not found in the source" do
+ bundle "plugin install no-foo --source https://gem.repo1", raise_on_error: false
+
+ expect(err).to include("Could not find")
+ plugin_should_not_be_installed("no-foo")
+ end
+
+ it "installs from rubygems source" do
+ bundle "plugin install foo --source https://gem.repo2"
+
+ expect(out).to include("Installed plugin foo")
+ plugin_should_be_installed("foo")
+ end
+
+ it "installs from rubygems source in frozen mode" do
+ bundle "plugin install foo --source https://gem.repo2", env: { "BUNDLE_DEPLOYMENT" => "true" }
+
+ expect(out).to include("Installed plugin foo")
+ plugin_should_be_installed("foo")
+ end
+
+ it "installs from sources configured as Gem.sources without any flags" do
+ bundle "plugin install foo", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_SOURCES" => "https://gem.repo2" }
+
+ expect(out).to include("Installed plugin foo")
+ plugin_should_be_installed("foo")
+ end
+
+ it "shows help when --help flag is given" do
+ bundle "plugin install --help"
+
+ # The help message defined in ../../lib/bundler/man/bundle-plugin.1.ronn will be output.
+ expect(out).to include("You can install, uninstall, and list plugin(s)")
+ end
+
+ context "plugin is already installed" do
+ before do
+ bundle "plugin install foo --source https://gem.repo2"
+ end
+
+ it "doesn't install plugin again" do
+ bundle "plugin install foo --source https://gem.repo2"
+ expect(out).not_to include("Installing plugin foo")
+ expect(out).not_to include("Installed plugin foo")
+ end
+ end
+
+ it "installs multiple plugins" do
+ bundle "plugin install foo kung-foo --source https://gem.repo2"
+
+ expect(out).to include("Installed plugin foo")
+ expect(out).to include("Installed plugin kung-foo")
+
+ plugin_should_be_installed("foo", "kung-foo")
+ end
+
+ it "uses the same version for multiple plugins" do
+ update_repo2 do
+ build_plugin "foo", "1.1"
+ build_plugin "kung-foo", "1.1"
+ end
+
+ bundle "plugin install foo kung-foo --version '1.0' --source https://gem.repo2"
+
+ expect(out).to include("Installing foo 1.0")
+ expect(out).to include("Installing kung-foo 1.0")
+ plugin_should_be_installed("foo", "kung-foo")
+ end
+
+ it "installs the latest version if not installed" do
+ update_repo2 do
+ build_plugin "foo", "1.1"
+ end
+
+ bundle "plugin install foo --version 1.0 --source https://gem.repo2 --verbose"
+ expect(out).to include("Installing foo 1.0")
+
+ bundle "plugin install foo --source https://gem.repo2 --verbose"
+ expect(out).to include("Installing foo 1.1")
+
+ bundle "plugin install foo --source https://gem.repo2 --verbose"
+ expect(out).to include("Using foo 1.1")
+ end
+
+ it "raises an error when when --branch specified" do
+ bundle "plugin install foo --branch main --source https://gem.repo2", raise_on_error: false
+
+ expect(out).not_to include("Installed plugin foo")
+
+ expect(err).to include("--branch can only be used with git sources")
+ end
+
+ it "raises an error when --ref specified" do
+ bundle "plugin install foo --ref v1.2.3 --source https://gem.repo2", raise_on_error: false
+
+ expect(err).to include("--ref can only be used with git sources")
+ end
+
+ it "raises error when both --branch and --ref options are specified" do
+ bundle "plugin install foo --source https://gem.repo2 --branch main --ref v1.2.3", raise_on_error: false
+
+ expect(out).not_to include("Installed plugin foo")
+
+ expect(err).to include("You cannot specify `--branch` and `--ref` at the same time.")
+ end
+
+ it "works with different load paths" do
+ build_repo2 do
+ build_plugin "testing" do |s|
+ s.write "plugins.rb", <<-RUBY
+ require "fubar"
+ class Test < Bundler::Plugin::API
+ command "check2"
+
+ def exec(command, args)
+ puts "mate"
+ end
+ end
+ RUBY
+ s.require_paths = %w[lib src]
+ s.write("src/fubar.rb")
+ end
+ end
+ bundle "plugin install testing --source https://gem.repo2"
+
+ bundle "check2", "no-color" => false
+ expect(out).to eq("mate")
+ end
+
+ context "malformatted plugin" do
+ it "fails when plugins.rb is missing" do
+ update_repo2 do
+ build_plugin "foo", "1.1"
+ build_plugin "kung-foo", "1.1"
+ end
+
+ bundle "plugin install foo kung-foo --version '1.0' --source https://gem.repo2"
+
+ expect(out).to include("Installing foo 1.0")
+ expect(out).to include("Installing kung-foo 1.0")
+ plugin_should_be_installed("foo", "kung-foo")
+
+ update_repo2 do
+ build_gem "charlie"
+ end
+
+ bundle "plugin install charlie --source https://gem.repo2", raise_on_error: false
+
+ expect(err).to include("Failed to install plugin `charlie`, due to Bundler::Plugin::MalformattedPlugin (plugins.rb was not found in the plugin.)")
+
+ expect(global_plugin_gem("charlie-1.0")).not_to be_directory
+
+ plugin_should_be_installed("foo", "kung-foo")
+ plugin_should_not_be_installed("charlie")
+ end
+
+ it "fails when plugins.rb throws exception on load" do
+ build_repo2 do
+ build_plugin "chaplin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ raise RuntimeError, "threw exception on load"
+ RUBY
+ end
+ end
+
+ bundle "plugin install chaplin --source https://gem.repo2", raise_on_error: false
+
+ expect(global_plugin_gem("chaplin-1.0")).not_to be_directory
+
+ plugin_should_not_be_installed("chaplin")
+ end
+ end
+
+ context "git plugins" do
+ it "installs form a git source" do
+ build_git "foo" do |s|
+ s.write "plugins.rb"
+ end
+
+ bundle "plugin install foo --git #{lib_path("foo-1.0")}"
+
+ expect(out).to include("Installed plugin foo")
+ plugin_should_be_installed("foo")
+ end
+
+ it "installs form a local git source" do
+ build_git "foo" do |s|
+ s.write "plugins.rb"
+ end
+
+ bundle "plugin install foo --git #{lib_path("foo-1.0")}"
+
+ expect(out).to include("Installed plugin foo")
+ plugin_should_be_installed("foo")
+ end
+ end
+
+ context "path plugins" do
+ it "installs from a path source" do
+ build_lib "path_plugin" do |s|
+ s.write "plugins.rb"
+ end
+ bundle "plugin install path_plugin --path #{lib_path("path_plugin-1.0")}"
+
+ expect(out).to include("Installed plugin path_plugin")
+ plugin_should_be_installed("path_plugin")
+ end
+
+ it "installs from a relative path source" do
+ build_lib "path_plugin" do |s|
+ s.write "plugins.rb"
+ end
+ path = lib_path("path_plugin-1.0").relative_path_from(bundled_app)
+ bundle "plugin install path_plugin --path #{path}"
+
+ expect(out).to include("Installed plugin path_plugin")
+ plugin_should_be_installed("path_plugin")
+ end
+
+ it "installs from a relative path source when inside an app" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ gemfile ""
+
+ build_lib "ga-plugin" do |s|
+ s.write "plugins.rb"
+ end
+
+ path = lib_path("ga-plugin-1.0").relative_path_from(bundled_app)
+ bundle "plugin install ga-plugin --path #{path}"
+
+ plugin_should_be_installed("ga-plugin")
+ expect(local_plugin_gem("foo-1.0")).not_to be_directory
+ end
+ end
+
+ context "Gemfile eval" do
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ it "installs plugins listed in gemfile" do
+ gemfile <<-G
+ source 'https://gem.repo2'
+ plugin 'foo'
+ gem 'myrack', "1.0.0"
+ G
+
+ bundle "install"
+
+ expect(out).to include("Installed plugin foo")
+
+ expect(out).to include("Bundle complete!")
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ plugin_should_be_installed("foo")
+ end
+
+ it "overrides the index with the new plugin version" do
+ gemfile <<-G
+ source 'https://gem.repo2'
+ plugin 'foo', "1.0"
+ gem 'myrack', "1.0.0"
+ G
+
+ bundle "install"
+
+ update_repo2 do
+ build_plugin "foo", "2.0.0"
+ end
+
+ gemfile <<-G
+ source 'https://gem.repo2'
+ plugin 'foo', "2.0"
+ gem 'myrack', "1.0.0"
+ G
+
+ bundle "install"
+
+ expected = local_plugin_gem("foo-2.0.0", "lib").to_s
+ expect(Bundler::Plugin.index.load_paths("foo")).to eq([expected])
+ end
+
+ it "respects bundler groups" do
+ gemfile <<-G
+ source 'https://gem.repo2'
+ plugin 'foo'
+ gem 'myrack', "1.0.0"
+ G
+
+ bundle "install", env: { "BUNDLE_WITHOUT" => "default" }
+
+ expect(out).to include("Bundle complete! 1 Gemfile dependency, 0 gems now installed.")
+ end
+
+ it "accepts plugin version" do
+ update_repo2 do
+ build_plugin "foo", "1.1.0"
+ end
+
+ gemfile <<-G
+ source 'https://gem.repo2'
+ plugin 'foo', "1.0"
+ G
+
+ bundle "install"
+
+ expect(out).to include("Installing foo 1.0")
+
+ plugin_should_be_installed("foo")
+
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "accepts git sources" do
+ build_git "ga-plugin" do |s|
+ s.write "plugins.rb"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ plugin 'ga-plugin', :git => "#{lib_path("ga-plugin-1.0")}"
+ G
+
+ expect(out).to include("Installed plugin ga-plugin")
+ plugin_should_be_installed("ga-plugin")
+ end
+
+ it "accepts path sources" do
+ build_lib "ga-plugin" do |s|
+ s.write "plugins.rb"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ plugin 'ga-plugin', :path => "#{lib_path("ga-plugin-1.0")}"
+ G
+
+ expect(out).to include("Installed plugin ga-plugin")
+ plugin_should_be_installed("ga-plugin")
+ end
+
+ it "accepts relative path sources" do
+ build_lib "ga-plugin" do |s|
+ s.write "plugins.rb"
+ end
+
+ path = lib_path("ga-plugin-1.0").relative_path_from(bundled_app)
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ plugin 'ga-plugin', :path => "#{path}"
+ G
+
+ expect(out).to include("Installed plugin ga-plugin")
+ plugin_should_be_installed("ga-plugin")
+ end
+
+ context "in deployment mode" do
+ it "installs plugins" do
+ install_gemfile <<-G
+ source 'https://gem.repo2'
+ gem 'myrack', "1.0.0"
+ G
+
+ bundle_config "deployment true"
+ install_gemfile <<-G
+ source 'https://gem.repo2'
+ plugin 'foo'
+ gem 'myrack', "1.0.0"
+ G
+
+ expect(out).to include("Installed plugin foo")
+
+ expect(out).to include("Bundle complete!")
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ plugin_should_be_installed("foo")
+ end
+ end
+ end
+
+ context "inline gemfiles" do
+ it "installs the listed plugins" do
+ code = <<-RUBY
+ require "bundler/inline"
+
+ gemfile do
+ source 'https://gem.repo2'
+ plugin 'foo'
+ end
+ RUBY
+
+ ruby code, artifice: "compact_index", env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ expect(local_plugin_gem("foo-1.0", "plugins.rb")).to exist
+ end
+ end
+
+ describe "local plugin" do
+ it "is installed when inside an app" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ gemfile ""
+ bundle "plugin install foo --source https://gem.repo2"
+
+ plugin_should_be_installed("foo")
+ expect(local_plugin_gem("foo-1.0")).to be_directory
+ end
+
+ context "conflict with global plugin" do
+ before do
+ update_repo2 do
+ build_plugin "fubar" do |s|
+ s.write "plugins.rb", <<-RUBY
+ class Fubar < Bundler::Plugin::API
+ command "shout"
+
+ def exec(command, args)
+ puts "local_one"
+ end
+ end
+ RUBY
+ end
+ end
+
+ # inside the app
+ gemfile "source 'https://gem.repo2'\nplugin 'fubar'"
+ bundle "install"
+
+ update_repo2 do
+ build_plugin "fubar", "1.1" do |s|
+ s.write "plugins.rb", <<-RUBY
+ class Fubar < Bundler::Plugin::API
+ command "shout"
+
+ def exec(command, args)
+ puts "global_one"
+ end
+ end
+ RUBY
+ end
+ end
+
+ # outside the app
+ bundle "plugin install fubar --source https://gem.repo2", dir: tmp
+ end
+
+ it "inside the app takes precedence over global plugin" do
+ bundle "shout"
+ expect(out).to eq("local_one")
+ end
+
+ it "outside the app global plugin is used" do
+ bundle "shout", dir: tmp
+ expect(out).to eq("global_one")
+ end
+ end
+ end
+end
diff --git a/spec/bundler/plugins/list_spec.rb b/spec/bundler/plugins/list_spec.rb
new file mode 100644
index 0000000000..30e3f82467
--- /dev/null
+++ b/spec/bundler/plugins/list_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundler plugin list" do
+ before do
+ build_repo2 do
+ build_plugin "foo" do |s|
+ s.write "plugins.rb", <<-RUBY
+ class Foo < Bundler::Plugin::API
+ command "shout"
+
+ def exec(command, args)
+ puts "Foo shout"
+ end
+ end
+ RUBY
+ end
+ build_plugin "bar" do |s|
+ s.write "plugins.rb", <<-RUBY
+ class Bar < Bundler::Plugin::API
+ command "scream"
+
+ def exec(command, args)
+ puts "Bar scream"
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ context "no plugins installed" do
+ it "shows proper no plugins installed message" do
+ bundle "plugin list"
+
+ expect(out).to include("No plugins installed")
+ end
+ end
+
+ context "single plugin installed" do
+ it "shows plugin name with commands list" do
+ bundle "plugin install foo --source https://gem.repo2"
+ plugin_should_be_installed("foo")
+ bundle "plugin list"
+
+ expected_output = "foo\n-----\n shout"
+ expect(out).to include(expected_output)
+ end
+ end
+
+ context "multiple plugins installed" do
+ it "shows plugin names with commands list" do
+ bundle "plugin install foo bar --source https://gem.repo2"
+ plugin_should_be_installed("foo", "bar")
+ bundle "plugin list"
+
+ expected_output = "foo\n-----\n shout\n\nbar\n-----\n scream"
+ expect(out).to include(expected_output)
+ end
+ end
+end
diff --git a/spec/bundler/plugins/source/example_spec.rb b/spec/bundler/plugins/source/example_spec.rb
new file mode 100644
index 0000000000..4cd4a1a931
--- /dev/null
+++ b/spec/bundler/plugins/source/example_spec.rb
@@ -0,0 +1,456 @@
+# frozen_string_literal: true
+
+RSpec.describe "real source plugins" do
+ context "with a minimal source plugin" do
+ before do
+ build_repo2 do
+ build_plugin "bundler-source-mpath" do |s|
+ s.write "plugins.rb", <<-RUBY
+ require "bundler-source-mpath"
+
+ class MPath < Bundler::Plugin::API
+ source "mpath"
+
+ attr_reader :path
+
+ def initialize(opts)
+ super
+
+ @path = Pathname.new options["uri"]
+ end
+
+ def fetch_gemspec_files
+ @spec_files ||= begin
+ glob = "{,*,*/*}.gemspec"
+ if installed?
+ search_path = install_path
+ else
+ search_path = path
+ end
+ Dir["\#{search_path.to_s}/\#{glob}"]
+ end
+ end
+
+ def install(spec, opts)
+ mkdir_p(install_path.parent)
+ require 'fileutils'
+ FileUtils.cp_r(path, install_path)
+
+ spec_path = install_path.join("\#{spec.full_name}.gemspec")
+ spec_path.open("wb") {|f| f.write spec.to_ruby }
+ spec.loaded_from = spec_path.to_s
+
+ post_install(spec)
+
+ nil
+ end
+ end
+ RUBY
+ end # build_plugin
+ end
+
+ build_lib "a-path-gem"
+
+ gemfile <<-G
+ source "https://gem.repo2" # plugin source
+ source "#{lib_path("a-path-gem-1.0")}", :type => :mpath do
+ gem "a-path-gem"
+ end
+ G
+ end
+
+ it "installs" do
+ bundle "install"
+
+ expect(out).to include("Bundle complete!")
+
+ expect(the_bundle).to include_gems("a-path-gem 1.0")
+ end
+
+ it "writes to lockfile" do
+ bundle "install"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "a-path-gem", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ PLUGIN SOURCE
+ remote: #{lib_path("a-path-gem-1.0")}
+ type: mpath
+ specs:
+ a-path-gem (1.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ a-path-gem!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "provides correct #full_gem_path" do
+ bundle "install"
+ run <<-RUBY
+ puts Bundler.rubygems.find_name('a-path-gem').first.full_gem_path
+ RUBY
+ expect(out).to eq(bundle("info a-path-gem --path"))
+ end
+
+ it "installs the gem executables" do
+ build_lib "gem_with_bin" do |s|
+ s.executables = ["foo"]
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2" # plugin source
+ source "#{lib_path("gem_with_bin-1.0")}", :type => :mpath do
+ gem "gem_with_bin"
+ end
+ G
+
+ bundle "exec foo"
+ expect(out).to eq("1.0")
+ end
+
+ describe "bundle cache/package" do
+ let(:uri_hash) { Digest(:SHA1).hexdigest(lib_path("a-path-gem-1.0").to_s) }
+ it "copies repository to vendor cache and uses it" do
+ bundle "install"
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist
+ expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}/.git")).not_to exist
+ expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}/.bundlecache")).to be_file
+
+ FileUtils.rm_r lib_path("a-path-gem-1.0")
+ expect(the_bundle).to include_gems("a-path-gem 1.0")
+ end
+
+ it "copies repository to vendor cache and uses it even when installed with `path` configured" do
+ bundle_config "path vendor/bundle"
+ bundle :install
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist
+
+ FileUtils.rm_r lib_path("a-path-gem-1.0")
+ expect(the_bundle).to include_gems("a-path-gem 1.0")
+ end
+
+ it "bundler package copies repository to vendor cache" do
+ bundle_config "path vendor/bundle"
+ bundle :install
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist
+
+ FileUtils.rm_r lib_path("a-path-gem-1.0")
+ expect(the_bundle).to include_gems("a-path-gem 1.0")
+ end
+ end
+
+ context "with lockfile" do
+ before do
+ lockfile <<-G
+ PLUGIN SOURCE
+ remote: #{lib_path("a-path-gem-1.0")}
+ type: mpath
+ specs:
+ a-path-gem (1.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+
+ PLATFORMS
+ #{generic_local_platform}
+
+ DEPENDENCIES
+ a-path-gem!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "installs" do
+ bundle "install"
+
+ expect(the_bundle).to include_gems("a-path-gem 1.0")
+ end
+ end
+ end
+
+ context "with a more elaborate source plugin" do
+ before do
+ build_repo2 do
+ build_plugin "bundler-source-gitp" do |s|
+ s.write "plugins.rb", <<-RUBY
+ require "open3"
+
+ class SPlugin < Bundler::Plugin::API
+ source "gitp"
+
+ attr_reader :ref
+
+ def initialize(opts)
+ super
+
+ @ref = options["ref"] || options["branch"] || options["tag"] || "main"
+ @unlocked = false
+ end
+
+ def eql?(other)
+ other.is_a?(self.class) && uri == other.uri && ref == other.ref
+ end
+
+ alias_method :==, :eql?
+
+ def fetch_gemspec_files
+ @spec_files ||= begin
+ glob = "{,*,*/*}.gemspec"
+ if !cached?
+ cache_repo
+ end
+
+ if installed? && !@unlocked
+ path = install_path
+ else
+ path = cache_path
+ end
+
+ Dir["\#{path}/\#{glob}"]
+ end
+ end
+
+ def install(spec, opts)
+ mkdir_p(install_path.dirname)
+ rm_rf(install_path)
+ `git clone --no-checkout --quiet "\#{cache_path}" "\#{install_path}"`
+ Open3.capture2e("git reset --hard \#{revision}", :chdir => install_path)
+
+ spec_path = install_path.join("\#{spec.full_name}.gemspec")
+ spec_path.open("wb") {|f| f.write spec.to_ruby }
+ spec.loaded_from = spec_path.to_s
+
+ post_install(spec)
+
+ nil
+ end
+
+ def options_to_lock
+ opts = {"revision" => revision}
+ opts["ref"] = ref if ref != "main"
+ opts
+ end
+
+ def unlock!
+ @unlocked = true
+ @revision = latest_revision
+ end
+
+ def app_cache_dirname
+ "\#{base_name}-\#{shortref_for_path(revision)}"
+ end
+
+ private
+
+ def cache_path
+ @cache_path ||= cache_dir.join("gitp", base_name)
+ end
+
+ def cache_repo
+ `git clone --quiet \#{@options["uri"]} \#{cache_path}`
+ end
+
+ def cached?
+ File.directory?(cache_path)
+ end
+
+ def locked_revision
+ options["revision"]
+ end
+
+ def revision
+ @revision ||= locked_revision || latest_revision
+ end
+
+ def latest_revision
+ if !cached? || @unlocked
+ rm_rf(cache_path)
+ cache_repo
+ end
+
+ output, _status = Open3.capture2e("git rev-parse --verify \#{@ref}", :chdir => cache_path)
+ output.strip
+ end
+
+ def base_name
+ File.basename(uri.sub(%r{^(\w+://)?([^/:]+:)?(//\w*/)?(\w*/)*}, ""), ".git")
+ end
+
+ def shortref_for_path(ref)
+ ref[0..11]
+ end
+
+ def install_path
+ @install_path ||= begin
+ git_scope = "\#{base_name}-\#{shortref_for_path(revision)}"
+
+ gem_install_dir.join(git_scope)
+ end
+ end
+
+ def installed?
+ File.directory?(install_path)
+ end
+ end
+ RUBY
+ end
+ end
+
+ build_git "ma-gitp-gem"
+
+ gemfile <<-G
+ source "https://gem.repo2" # plugin source
+ source "#{lib_path("ma-gitp-gem-1.0")}", :type => :gitp do
+ gem "ma-gitp-gem"
+ end
+ G
+ end
+
+ it "handles the source option" do
+ bundle "install"
+ expect(out).to include("Bundle complete!")
+ expect(the_bundle).to include_gems("ma-gitp-gem 1.0")
+ end
+
+ it "writes to lockfile" do
+ revision = revision_for(lib_path("ma-gitp-gem-1.0"))
+ bundle "install"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "ma-gitp-gem", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ PLUGIN SOURCE
+ remote: #{lib_path("ma-gitp-gem-1.0")}
+ type: gitp
+ revision: #{revision}
+ specs:
+ ma-gitp-gem (1.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ma-gitp-gem!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ context "with lockfile" do
+ before do
+ revision = revision_for(lib_path("ma-gitp-gem-1.0"))
+ lockfile <<-G
+ PLUGIN SOURCE
+ remote: #{lib_path("ma-gitp-gem-1.0")}
+ type: gitp
+ revision: #{revision}
+ specs:
+ ma-gitp-gem (1.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+
+ PLATFORMS
+ #{generic_local_platform}
+
+ DEPENDENCIES
+ ma-gitp-gem!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "installs" do
+ bundle "install"
+ expect(the_bundle).to include_gems("ma-gitp-gem 1.0")
+ end
+
+ it "uses the locked ref" do
+ update_git "ma-gitp-gem"
+ bundle "install"
+
+ run <<-RUBY
+ require 'ma/gitp/gem'
+ puts "WIN" unless defined?(MAGITPGEM_PREV_REF)
+ RUBY
+ expect(out).to eq("WIN")
+ end
+
+ it "updates the deps on bundler update" do
+ update_git "ma-gitp-gem"
+ bundle "update ma-gitp-gem"
+
+ run <<-RUBY
+ require 'ma/gitp/gem'
+ puts "WIN" if defined?(MAGITPGEM_PREV_REF)
+ RUBY
+ expect(out).to eq("WIN")
+ end
+
+ it "updates the deps on change in gemfile" do
+ update_git "ma-gitp-gem", "1.1", path: lib_path("ma-gitp-gem-1.0"), gemspec: true
+ gemfile <<-G
+ source "https://gem.repo2" # plugin source
+ source "#{lib_path("ma-gitp-gem-1.0")}", :type => :gitp do
+ gem "ma-gitp-gem", "1.1"
+ end
+ G
+ bundle "install"
+
+ expect(the_bundle).to include_gems("ma-gitp-gem 1.1")
+ end
+ end
+
+ describe "bundle cache with gitp" do
+ it "copies repository to vendor cache and uses it" do
+ git = build_git "foo"
+ ref = git.ref_for("main", 11)
+
+ install_gemfile <<-G
+ source "https://gem.repo2" # plugin source
+ source '#{lib_path("foo-1.0")}', :type => :gitp do
+ gem "foo"
+ end
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.git")).not_to exist
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.bundlecache")).to be_file
+
+ FileUtils.rm_r lib_path("foo-1.0")
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+ end
+ end
+end
diff --git a/spec/bundler/plugins/source_spec.rb b/spec/bundler/plugins/source_spec.rb
new file mode 100644
index 0000000000..995e50e653
--- /dev/null
+++ b/spec/bundler/plugins/source_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundler source plugin" do
+ describe "plugins dsl eval for #source with :type option" do
+ before do
+ update_repo2 do
+ build_plugin "bundler-source-psource" do |s|
+ s.write "plugins.rb", <<-RUBY
+ class OPSource < Bundler::Plugin::API
+ source "psource"
+ end
+ RUBY
+ end
+ end
+ end
+
+ it "installs bundler-source-* gem when no handler for source is present" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ source "#{lib_path("gitp")}", :type => :psource do
+ end
+ G
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ plugin_should_be_installed("bundler-source-psource")
+ end
+
+ it "enables the plugin to require a lib path" do
+ update_repo2 do
+ build_plugin "bundler-source-psource" do |s|
+ s.write "plugins.rb", <<-RUBY
+ require "bundler-source-psource"
+ class PSource < Bundler::Plugin::API
+ source "psource"
+ end
+ RUBY
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ source "#{lib_path("gitp")}", :type => :psource do
+ end
+ G
+
+ expect(out).to include("Bundle complete!")
+ end
+
+ context "with an explicit handler" do
+ before do
+ update_repo2 do
+ build_plugin "another-psource" do |s|
+ s.write "plugins.rb", <<-RUBY
+ class Cheater < Bundler::Plugin::API
+ source "psource"
+ end
+ RUBY
+ end
+ end
+ end
+
+ context "explicit presence in gemfile" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ plugin "another-psource"
+
+ source "#{lib_path("gitp")}", :type => :psource do
+ end
+ G
+ end
+
+ it "completes successfully" do
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "installs the explicit one" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ plugin_should_be_installed("another-psource")
+ end
+
+ it "doesn't install the default one" do
+ plugin_should_not_be_installed("bundler-source-psource")
+ end
+ end
+
+ context "explicit default source" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ plugin "bundler-source-psource"
+
+ source "#{lib_path("gitp")}", :type => :psource do
+ end
+ G
+ end
+
+ it "completes successfully" do
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "installs the default one" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ plugin_should_be_installed("bundler-source-psource")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/plugins/uninstall_spec.rb b/spec/bundler/plugins/uninstall_spec.rb
new file mode 100644
index 0000000000..dedcc9f37c
--- /dev/null
+++ b/spec/bundler/plugins/uninstall_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundler plugin uninstall" do
+ before do
+ build_repo2 do
+ build_plugin "foo"
+ build_plugin "kung-foo"
+ end
+ end
+
+ it "shows proper error message when plugins are not specified" do
+ bundle "plugin uninstall"
+ expect(err).to include("No plugins to uninstall")
+ end
+
+ it "uninstalls specified plugins" do
+ bundle "plugin install foo kung-foo --source https://gem.repo2"
+ plugin_should_be_installed("foo")
+ plugin_should_be_installed("kung-foo")
+
+ bundle "plugin uninstall foo"
+ expect(out).to include("Uninstalled plugin foo")
+ plugin_should_not_be_installed("foo")
+ plugin_should_be_installed("kung-foo")
+ end
+
+ it "shows proper message when plugin is not installed" do
+ bundle "plugin uninstall foo"
+ expect(err).to include("Plugin foo is not installed")
+ plugin_should_not_be_installed("foo")
+ end
+
+ it "doesn't wipe out path plugins" do
+ build_lib "path_plugin" do |s|
+ s.write "plugins.rb"
+ end
+ path = lib_path("path_plugin-1.0")
+ expect(path).to be_a_directory
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+
+ install_gemfile <<-G
+ source 'https://gem.repo2'
+ plugin 'path_plugin', :path => "#{path}"
+ gem 'myrack', '1.0.0'
+ G
+
+ plugin_should_be_installed("path_plugin")
+ expect(Bundler::Plugin.index.plugin_path("path_plugin")).to eq path
+
+ bundle "plugin uninstall path_plugin"
+ expect(out).to include("Uninstalled plugin path_plugin")
+ plugin_should_not_be_installed("path_plugin")
+ # the actual gem still exists though
+ expect(path).to be_a_directory
+ end
+
+ describe "with --all" do
+ it "uninstalls all installed plugins" do
+ bundle "plugin install foo kung-foo --source https://gem.repo2"
+ plugin_should_be_installed("foo")
+ plugin_should_be_installed("kung-foo")
+
+ bundle "plugin uninstall --all"
+ plugin_should_not_be_installed("foo")
+ plugin_should_not_be_installed("kung-foo")
+ end
+
+ it "shows proper no plugins installed message when no plugins installed" do
+ bundle "plugin uninstall --all"
+ expect(out).to include("No plugins installed")
+ end
+ end
+end
diff --git a/spec/bundler/quality_es_spec.rb b/spec/bundler/quality_es_spec.rb
new file mode 100644
index 0000000000..e68674c030
--- /dev/null
+++ b/spec/bundler/quality_es_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+RSpec.describe "La biblioteca si misma" do
+ def check_for_expendable_words(filename)
+ failing_line_message = []
+ useless_words = %w[
+ básicamente
+ claramente
+ sólo
+ solamente
+ obvio
+ obviamente
+ fácil
+ fácilmente
+ sencillamente
+ simplemente
+ ]
+ pattern = /\b#{Regexp.union(useless_words)}\b/i
+
+ File.readlines(File.expand_path(filename, source_root)).each_with_index do |line, number|
+ next unless word_found = pattern.match(line)
+ failing_line_message << "#{filename}:#{number.succ} contiene '#{word_found}'. Esta palabra tiene un significado subjetivo y es mejor obviarla en textos técnicos."
+ end
+
+ failing_line_message unless failing_line_message.empty?
+ end
+
+ def check_for_specific_pronouns(filename)
+ failing_line_message = []
+ specific_pronouns = /\b(él|ella|ellos|ellas)\b/i
+
+ File.readlines(File.expand_path(filename, source_root)).each_with_index do |line, number|
+ next unless word_found = specific_pronouns.match(line)
+ failing_line_message << "#{filename}:#{number.succ} contiene '#{word_found}'. Use pronombres más genéricos en la documentación."
+ end
+
+ failing_line_message unless failing_line_message.empty?
+ end
+
+ it "mantiene la calidad de lenguaje de la documentación" do
+ included = /ronn/
+ error_messages = []
+ man_tracked_files.each do |filename|
+ next unless filename&.match?(included)
+ error_messages << check_for_expendable_words(filename)
+ error_messages << check_for_specific_pronouns(filename)
+ end
+ expect(error_messages.compact).to be_well_formed
+ end
+
+ it "mantiene la calidad de lenguaje de oraciones usadas en el código fuente" do
+ error_messages = []
+ exempt = /vendor/
+ lib_tracked_files.each do |filename|
+ next if filename&.match?(exempt)
+ error_messages << check_for_expendable_words(filename)
+ error_messages << check_for_specific_pronouns(filename)
+ end
+ expect(error_messages.compact).to be_well_formed
+ end
+end
diff --git a/spec/bundler/quality_spec.rb b/spec/bundler/quality_spec.rb
new file mode 100644
index 0000000000..16b7f18788
--- /dev/null
+++ b/spec/bundler/quality_spec.rb
@@ -0,0 +1,261 @@
+# frozen_string_literal: true
+
+require "set"
+
+RSpec.describe "The library itself" do
+ def check_for_git_merge_conflicts(filename)
+ merge_conflicts_regex = /
+ <<<<<<<|
+ =======|
+ >>>>>>>
+ /x
+
+ failing_lines = []
+ each_line(filename) do |line, number|
+ failing_lines << number + 1 if line&.match?(merge_conflicts_regex)
+ end
+
+ return if failing_lines.empty?
+ "#{filename} has unresolved git merge conflicts on lines #{failing_lines.join(", ")}"
+ end
+
+ def check_for_tab_characters(filename)
+ # Because Go uses hard tabs
+ return if filename.end_with?(".go.tt")
+
+ failing_lines = []
+ each_line(filename) do |line, number|
+ failing_lines << number + 1 if line.include?("\t")
+ end
+
+ return if failing_lines.empty?
+ "#{filename} has tab characters on lines #{failing_lines.join(", ")}"
+ end
+
+ def check_for_extra_spaces(filename)
+ failing_lines = []
+ each_line(filename) do |line, number|
+ next if /^\s+#.*\s+\n$/.match?(line)
+ failing_lines << number + 1 if /\s+\n$/.match?(line)
+ end
+
+ return if failing_lines.empty?
+ "#{filename} has spaces on the EOL on lines #{failing_lines.join(", ")}"
+ end
+
+ def check_for_extraneous_quotes(filename)
+ failing_lines = []
+ each_line(filename) do |line, number|
+ failing_lines << number + 1 if /\u{2019}/.match?(line)
+ end
+
+ return if failing_lines.empty?
+ "#{filename} has an extraneous quote on lines #{failing_lines.join(", ")}"
+ end
+
+ def check_for_expendable_words(filename)
+ failing_line_message = []
+ useless_words = %w[
+ actually
+ basically
+ clearly
+ just
+ obviously
+ really
+ simply
+ ]
+ pattern = /\b#{Regexp.union(useless_words)}\b/i
+
+ each_line(filename) do |line, number|
+ next unless word_found = pattern.match(line)
+ failing_line_message << "#{filename}:#{number.succ} has '#{word_found}'. Avoid using these kinds of weak modifiers."
+ end
+
+ failing_line_message unless failing_line_message.empty?
+ end
+
+ def check_for_specific_pronouns(filename)
+ failing_line_message = []
+ specific_pronouns = /\b(he|she|his|hers|him|her|himself|herself)\b/i
+
+ each_line(filename) do |line, number|
+ next unless word_found = specific_pronouns.match(line)
+ failing_line_message << "#{filename}:#{number.succ} has '#{word_found}'. Use more generic pronouns in documentation."
+ end
+
+ failing_line_message unless failing_line_message.empty?
+ end
+
+ it "has no malformed whitespace" do
+ exempt = /\.gitmodules|fixtures|vendor|LICENSE|vcr_cassettes|rbreadline\.diff|index\.txt$/
+ error_messages = []
+ tracked_files.each do |filename|
+ next if filename&.match?(exempt)
+ error_messages << check_for_tab_characters(filename)
+ error_messages << check_for_extra_spaces(filename)
+ end
+ expect(error_messages.compact).to be_well_formed
+ end
+
+ it "has no extraneous quotes" do
+ exempt = /vendor|vcr_cassettes|LICENSE|rbreadline\.diff/
+ error_messages = []
+ tracked_files.each do |filename|
+ next if filename&.match?(exempt)
+ error_messages << check_for_extraneous_quotes(filename)
+ end
+ expect(error_messages.compact).to be_well_formed
+ end
+
+ it "does not include any unresolved merge conflicts" do
+ error_messages = []
+ exempt = %r{lock/lockfile_spec|quality_spec|vcr_cassettes|\.ronn|lockfile_parser}
+ tracked_files.each do |filename|
+ next if filename&.match?(exempt)
+ error_messages << check_for_git_merge_conflicts(filename)
+ end
+ expect(error_messages.compact).to be_well_formed
+ end
+
+ it "maintains language quality of the documentation" do
+ error_messages = []
+ man_tracked_files.each do |filename|
+ error_messages << check_for_expendable_words(filename)
+ error_messages << check_for_specific_pronouns(filename)
+ end
+ expect(error_messages.compact).to be_well_formed
+ end
+
+ it "maintains language quality of sentences used in source code" do
+ error_messages = []
+ exempt = /vendor|vcr_cassettes|CODE_OF_CONDUCT/
+ lib_tracked_files.each do |filename|
+ next if filename&.match?(exempt)
+ error_messages << check_for_expendable_words(filename)
+ error_messages << check_for_specific_pronouns(filename)
+ end
+ expect(error_messages.compact).to be_well_formed
+ end
+
+ it "documents all used settings" do
+ exemptions = %w[
+ gem.changelog
+ gem.ci
+ gem.coc
+ gem.linter
+ gem.mit
+ gem.bundle
+ gem.rubocop
+ gem.test
+ git.allow_insecure
+ inline
+ trust-policy
+ ]
+
+ all_settings = Hash.new {|h, k| h[k] = [] }
+ documented_settings = []
+
+ Bundler::Settings::BOOL_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::BOOL_KEYS" }
+ Bundler::Settings::NUMBER_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::NUMBER_KEYS" }
+ Bundler::Settings::ARRAY_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::ARRAY_KEYS" }
+ Bundler::Settings::STRING_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::STRING_KEYS" }
+
+ key_pattern = /([a-z\._-]+)/i
+ lib_tracked_files.each do |filename|
+ each_line(filename) do |line, number|
+ line.scan(/Bundler\.settings\[:#{key_pattern}\]/).flatten.each {|s| all_settings[s] << "referenced at `#{filename}:#{number.succ}`" }
+ end
+ end
+ settings_section = File.read(source_root.join("lib/bundler/man/bundle-config.1.ronn")).split(/^## /).find {|section| section.start_with?("LIST OF AVAILABLE KEYS") }
+ documented_settings = settings_section.scan(/^\* `#{key_pattern}`/).flatten
+
+ documented_settings.each do |s|
+ all_settings.delete(s)
+ expect(exemptions.delete(s)).to be_nil, "setting #{s} was exempted but was actually documented"
+ end
+
+ exemptions.each do |s|
+ expect(all_settings.delete(s)).to be_truthy, "setting #{s} was exempted but unused"
+ end
+ error_messages = all_settings.map do |setting, refs|
+ "The `#{setting}` setting is undocumented\n\t- #{refs.join("\n\t- ")}\n"
+ end
+
+ expect(error_messages.sort).to be_well_formed
+
+ expect(documented_settings).to be_sorted
+ end
+
+ it "can still be built" do
+ with_built_bundler do |gem_path|
+ expect(File.exist?(gem_path)).to be true
+ end
+ end
+
+ it "ships the correct set of files" do
+ git_list = tracked_files.reject {|f| f.start_with?("spec/") }
+
+ gem_list = loaded_gemspec.files
+ gem_list.map! {|f| f.sub(%r{\Aexe/}, "libexec/") } if ruby_core?
+
+ expect(git_list).to match_array(gem_list)
+ end
+
+ it "does not contain any warnings" do
+ exclusions = %w[
+ lib/bundler/capistrano.rb
+ lib/bundler/deployment.rb
+ lib/bundler/gem_tasks.rb
+ lib/bundler/vlad.rb
+ ]
+ files_to_require = lib_tracked_files.grep(/\.rb$/) - exclusions
+ files_to_require.reject! {|f| f.start_with?("lib/bundler/vendor") }
+ files_to_require.map! {|f| File.expand_path(f, source_root) }
+ files_to_require.sort!
+ sys_exec("ruby -w") do |input, _, _|
+ files_to_require.each do |f|
+ input.puts "require '#{f}'"
+ end
+ end
+
+ warnings = stdboth.split("\n")
+ # ignore warnings around deprecated Object#=~ method in RubyGems
+ warnings.reject! {|w| w =~ %r{rubygems\/version.rb.*deprecated\ Object#=~} }
+
+ expect(warnings).to be_well_formed
+ end
+
+ it "does not use require internally, but require_relative" do
+ exempt = %r{templates/|\.5|\.1|vendor/}
+ all_bad_requires = []
+ lib_tracked_files.each do |filename|
+ next if filename&.match?(exempt)
+ each_line(filename) do |line, number|
+ line.scan(/^ *require "bundler/).each { all_bad_requires << "#{filename}:#{number.succ}" }
+ end
+ end
+
+ expect(all_bad_requires).to be_empty, "#{all_bad_requires.size} internal requires that should use `require_relative`: #{all_bad_requires}"
+ end
+
+ # We don't want our artifice code to activate bundler, but it needs to use the
+ # namespaced implementation of `Net::HTTP`. So we duplicate the file in
+ # bundler that loads that.
+ it "keeps vendored_net_http spec code in sync with the lib implementation" do
+ lib_implementation_path = File.join(source_lib_dir, "bundler", "vendored_net_http.rb")
+ expect(File.exist?(lib_implementation_path)).to be_truthy
+ lib_code = File.read(lib_implementation_path)
+
+ spec_implementation_path = File.join(spec_dir, "support", "vendored_net_http.rb")
+ expect(File.exist?(spec_implementation_path)).to be_truthy
+ spec_code = File.read(spec_implementation_path)
+
+ expect(lib_code).to eq(spec_code)
+ end
+
+ private
+
+ def each_line(filename, &block)
+ File.readlines(File.expand_path(filename, source_root), encoding: "UTF-8").each_with_index(&block)
+ end
+end
diff --git a/spec/bundler/realworld/double_check_spec.rb b/spec/bundler/realworld/double_check_spec.rb
new file mode 100644
index 0000000000..0741560395
--- /dev/null
+++ b/spec/bundler/realworld/double_check_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+RSpec.describe "double checking sources", realworld: true do
+ it "finds already-installed gems" do
+ create_file("rails.gemspec", <<-RUBY)
+ Gem::Specification.new do |s|
+ s.name = "rails"
+ s.version = "5.1.4"
+ s.summary = ""
+ s.description = ""
+ s.author = ""
+ s.add_dependency "actionpack", "5.1.4"
+ end
+ RUBY
+
+ create_file("actionpack.gemspec", <<-RUBY)
+ Gem::Specification.new do |s|
+ s.name = "actionpack"
+ s.version = "5.1.4"
+ s.summary = ""
+ s.description = ""
+ s.author = ""
+ s.add_dependency "rack", "~> 2.0.0"
+ end
+ RUBY
+
+ cmd = <<-RUBY
+ require "bundler"
+ require "#{spec_dir}/support/artifice/vcr"
+ require "bundler/inline"
+ gemfile(true) do
+ source "https://rubygems.org"
+ gem "rails", path: "."
+ end
+ RUBY
+
+ ruby cmd
+ ruby cmd
+ end
+end
diff --git a/spec/bundler/realworld/edgecases_spec.rb b/spec/bundler/realworld/edgecases_spec.rb
new file mode 100644
index 0000000000..391aa0cef6
--- /dev/null
+++ b/spec/bundler/realworld/edgecases_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+RSpec.describe "real world edgecases", realworld: true do
+ def rubygems_version(name, requirement)
+ ruby <<-RUBY
+ require "#{spec_dir}/support/artifice/vcr"
+ require "bundler"
+ require "bundler/source/rubygems/remote"
+ require "bundler/fetcher"
+ rubygem = Bundler.ui.silence do
+ remote = Bundler::Source::Rubygems::Remote.new(Gem::URI("https://rubygems.org"))
+ source = Bundler::Source::Rubygems.new
+ fetcher = Bundler::Fetcher.new(remote)
+ index = fetcher.specs([#{name.dump}], source)
+ requirement = Gem::Requirement.create(#{requirement.dump})
+ index.search(#{name.dump}).select {|spec| requirement.satisfied_by?(spec.version) }.last
+ end
+ if rubygem.nil?
+ raise ArgumentError, "Could not find #{name} (#{requirement}) on rubygems.org!\n" \
+ "Found specs:\n\#{index.send(:specs).inspect}"
+ end
+ puts "#{name} (\#{rubygem.version})"
+ RUBY
+ end
+
+ it "resolves dependencies correctly" do
+ gemfile <<-G
+ source "https://rubygems.org"
+
+ gem 'rails', '~> 5.0'
+ gem 'capybara', '~> 2.2.0'
+ gem 'rack-cache', '1.2.0' # last version that works on Ruby 1.9
+ G
+ bundle :lock
+ expect(lockfile).to include(rubygems_version("rails", "~> 5.0"))
+ expect(lockfile).to include("capybara (2.2.1)")
+ end
+
+ it "installs the latest version of gxapi_rails" do
+ gemfile <<-G
+ source "https://rubygems.org"
+
+ gem "sass-rails"
+ gem "rails", "~> 5"
+ gem "gxapi_rails", "< 0.1.0" # 0.1.0 was released way after the test was written
+ gem 'rack-cache', '1.2.0' # last version that works on Ruby 1.9
+ G
+ bundle :lock
+ expect(lockfile).to include("gxapi_rails (0.0.6)")
+ end
+
+ it "installs the latest version of i18n" do
+ gemfile <<-G
+ source "https://rubygems.org"
+
+ gem "i18n", "~> 0.6.0"
+ gem "activesupport", "~> 3.0"
+ gem "activerecord", "~> 3.0"
+ gem "builder", "~> 2.1.2"
+ G
+ bundle :lock
+ expect(lockfile).to include(rubygems_version("i18n", "~> 0.6.0"))
+ expect(lockfile).to include(rubygems_version("activesupport", "~> 3.0"))
+ end
+
+ it "outputs a helpful warning when gems have a gemspec with invalid `require_paths`" do
+ install_gemfile <<-G, standalone: true, env: { "BUNDLE_FORCE_RUBY_PLATFORM" => "1" }
+ source 'https://rubygems.org'
+ gem "resque-scheduler", "2.2.0"
+ gem "redis-namespace", "1.6.0" # for a consistent resolution including ruby 2.3.0
+ gem "ruby2_keywords", "0.0.5"
+ G
+ expect(err).to include("resque-scheduler 2.2.0 includes a gemspec with `require_paths` set to an array of arrays. Newer versions of this gem might've already fixed this").once
+ end
+end
diff --git a/spec/bundler/realworld/ffi_spec.rb b/spec/bundler/realworld/ffi_spec.rb
new file mode 100644
index 0000000000..bede372b41
--- /dev/null
+++ b/spec/bundler/realworld/ffi_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+RSpec.describe "loading dynamically linked library on a bundle exec context", realworld: true do
+ it "passes ENV right after argv in memory" do
+ create_file "foo.rb", <<~RUBY
+ require 'ffi'
+
+ module FOO
+ extend FFI::Library
+ ffi_lib './libfoo.so'
+
+ attach_function :Hello, [], :void
+ end
+
+ FOO.Hello()
+ RUBY
+
+ create_file "libfoo.c", <<~'C'
+ #include <stdio.h>
+
+ static int foo_init(int argc, char** argv, char** envp) {
+ if (argv[argc+1] == NULL) {
+ printf("FAIL\n");
+ } else {
+ printf("OK\n");
+ }
+
+ return 0;
+ }
+
+ #if defined(__APPLE__) && defined(__MACH__)
+ __attribute__((section("__DATA,__mod_init_func"), used, aligned(sizeof(void*))))
+ #else
+ __attribute__((section(".init_array")))
+ #endif
+ static void *ctr = &foo_init;
+
+ extern char** environ;
+
+ void Hello() {
+ return;
+ }
+ C
+
+ in_bundled_app "gcc -g -o libfoo.so -shared -fpic libfoo.c"
+
+ install_gemfile <<-G
+ source "https://rubygems.org"
+
+ gem 'ffi', force_ruby_platform: true
+ G
+
+ bundle "exec ruby foo.rb"
+
+ expect(out).to eq("OK")
+ end
+end
diff --git a/spec/bundler/realworld/fixtures/tapioca/Gemfile b/spec/bundler/realworld/fixtures/tapioca/Gemfile
new file mode 100644
index 0000000000..447d715706
--- /dev/null
+++ b/spec/bundler/realworld/fixtures/tapioca/Gemfile
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+gem "tapioca"
diff --git a/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock b/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock
new file mode 100644
index 0000000000..c2df2f9229
--- /dev/null
+++ b/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock
@@ -0,0 +1,49 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ erubi (1.13.1)
+ netrc (0.11.0)
+ parallel (1.26.3)
+ prism (1.3.0)
+ rbi (0.2.2)
+ prism (~> 1.0)
+ sorbet-runtime (>= 0.5.9204)
+ sorbet (0.5.11725)
+ sorbet-static (= 0.5.11725)
+ sorbet-runtime (0.5.11725)
+ sorbet-static (0.5.11725-aarch64-linux)
+ sorbet-static (0.5.11725-universal-darwin)
+ sorbet-static (0.5.11725-x86_64-linux)
+ sorbet-static-and-runtime (0.5.11725)
+ sorbet (= 0.5.11725)
+ sorbet-runtime (= 0.5.11725)
+ spoom (1.5.0)
+ erubi (>= 1.10.0)
+ prism (>= 0.28.0)
+ sorbet-static-and-runtime (>= 0.5.10187)
+ thor (>= 0.19.2)
+ tapioca (0.16.6)
+ bundler (>= 2.2.25)
+ netrc (>= 0.11.0)
+ parallel (>= 1.21.0)
+ rbi (~> 0.2)
+ sorbet-static-and-runtime (>= 0.5.11087)
+ spoom (>= 1.2.0)
+ thor (>= 1.2.0)
+ yard-sorbet
+ thor (1.4.0)
+ yard (0.9.42)
+ yard-sorbet (0.9.0)
+ sorbet-runtime
+ yard
+
+PLATFORMS
+ aarch64-linux
+ universal-darwin
+ x86_64-linux
+
+DEPENDENCIES
+ tapioca
+
+BUNDLED WITH
+ 4.1.0.dev
diff --git a/spec/bundler/realworld/fixtures/warbler/.gitignore b/spec/bundler/realworld/fixtures/warbler/.gitignore
new file mode 100644
index 0000000000..d392f0e82c
--- /dev/null
+++ b/spec/bundler/realworld/fixtures/warbler/.gitignore
@@ -0,0 +1 @@
+*.jar
diff --git a/spec/bundler/realworld/fixtures/warbler/Gemfile b/spec/bundler/realworld/fixtures/warbler/Gemfile
new file mode 100644
index 0000000000..5687bbd975
--- /dev/null
+++ b/spec/bundler/realworld/fixtures/warbler/Gemfile
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+gem "demo", path: "./demo"
+gem "jruby-jars", "~> 10.0"
+gem "warbler", "~> 2.1"
diff --git a/spec/bundler/realworld/fixtures/warbler/Gemfile.lock b/spec/bundler/realworld/fixtures/warbler/Gemfile.lock
new file mode 100644
index 0000000000..05f3bc4e3f
--- /dev/null
+++ b/spec/bundler/realworld/fixtures/warbler/Gemfile.lock
@@ -0,0 +1,35 @@
+PATH
+ remote: demo
+ specs:
+ demo (1.0)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ jruby-jars (10.0.0.1)
+ jruby-rack (1.2.7)
+ ostruct (0.6.3)
+ rake (13.3.0)
+ rexml (3.4.2)
+ rubyzip (3.3.0)
+ warbler (2.1.0)
+ jruby-jars (>= 9.4, < 10.1)
+ jruby-rack (>= 1.2.3, < 1.3)
+ ostruct (~> 0.6.2)
+ rake (~> 13.0, >= 13.0.3)
+ rexml (~> 3.0)
+ rubyzip (>= 3.0.0)
+
+PLATFORMS
+ arm64-darwin
+ java
+ ruby
+ universal-java
+
+DEPENDENCIES
+ demo!
+ jruby-jars (~> 10.0)
+ warbler (~> 2.1)
+
+BUNDLED WITH
+ 4.1.0.dev
diff --git a/spec/bundler/realworld/fixtures/warbler/bin/warbler-example.rb b/spec/bundler/realworld/fixtures/warbler/bin/warbler-example.rb
new file mode 100644
index 0000000000..25f614ecc2
--- /dev/null
+++ b/spec/bundler/realworld/fixtures/warbler/bin/warbler-example.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+puts require "bundler/setup"
diff --git a/spec/bundler/realworld/fixtures/warbler/demo/demo.gemspec b/spec/bundler/realworld/fixtures/warbler/demo/demo.gemspec
new file mode 100644
index 0000000000..ed5a0dc080
--- /dev/null
+++ b/spec/bundler/realworld/fixtures/warbler/demo/demo.gemspec
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+Gem::Specification.new do |spec|
+ spec.name = "demo"
+ spec.version = "1.0"
+ spec.author = "Somebody"
+ spec.summary = "A demo gem"
+ spec.license = "MIT"
+ spec.homepage = "https://example.org"
+end
diff --git a/spec/bundler/realworld/git_spec.rb b/spec/bundler/realworld/git_spec.rb
new file mode 100644
index 0000000000..9eff74f1c9
--- /dev/null
+++ b/spec/bundler/realworld/git_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+RSpec.describe "github source", realworld: true do
+ it "properly fetches PRs" do
+ install_gemfile <<-G
+ source "https://rubygems.org"
+
+ gem "reline", github: "https://github.com/ruby/reline/pull/488"
+ G
+ end
+end
diff --git a/spec/bundler/realworld/parallel_spec.rb b/spec/bundler/realworld/parallel_spec.rb
new file mode 100644
index 0000000000..b57fdfd0ee
--- /dev/null
+++ b/spec/bundler/realworld/parallel_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+RSpec.describe "parallel", realworld: true do
+ it "installs" do
+ gemfile <<-G
+ source "https://rubygems.org"
+ gem 'activesupport', '~> 3.2.13'
+ gem 'faker', '~> 1.1.2'
+ gem 'i18n', '~> 0.6.0' # Because 0.7+ requires Ruby 1.9.3+
+ G
+
+ bundle :install, jobs: 4, env: { "DEBUG" => "1" }
+
+ expect(out).to match(/[1-3]: /)
+
+ bundle "info activesupport --path"
+ expect(out).to match(/activesupport/)
+
+ bundle "info faker --path"
+ expect(out).to match(/faker/)
+ end
+
+ it "updates" do
+ install_gemfile <<-G
+ source "https://rubygems.org"
+ gem 'activesupport', '3.2.12'
+ gem 'faker', '~> 1.1.2'
+ G
+
+ gemfile <<-G
+ source "https://rubygems.org"
+ gem 'activesupport', '~> 3.2.12'
+ gem 'faker', '~> 1.1.2'
+ gem 'i18n', '~> 0.6.0' # Because 0.7+ requires Ruby 1.9.3+
+ G
+
+ bundle :update, jobs: 4, env: { "DEBUG" => "1" }, all: true
+
+ expect(out).to match(/[1-3]: /)
+
+ bundle "info activesupport --path"
+ expect(out).to match(/activesupport-3\.2\.\d+/)
+
+ bundle "info faker --path"
+ expect(out).to match(/faker/)
+ end
+
+ it "works with --standalone" do
+ gemfile <<-G
+ source "https://rubygems.org"
+ gem "diff-lcs"
+ G
+
+ bundle :install, standalone: true, jobs: 4
+
+ ruby <<-RUBY
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ require "diff/lcs"
+ puts Diff::LCS
+ RUBY
+
+ expect(out).to eq("Diff::LCS")
+ end
+end
diff --git a/spec/bundler/realworld/slow_perf_spec.rb b/spec/bundler/realworld/slow_perf_spec.rb
new file mode 100644
index 0000000000..5d36ba7455
--- /dev/null
+++ b/spec/bundler/realworld/slow_perf_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe "bundle install with complex dependencies", realworld: true do
+ it "resolves quickly" do
+ gemfile <<-G
+ source 'https://rubygems.org'
+
+ gem "actionmailer"
+ gem "mongoid", ">= 0.10.2"
+ G
+
+ bundle "lock", env: { "DEBUG_RESOLVER" => "1" }
+ expect(out).to include("Solution found after 1 attempts")
+ end
+
+ it "resolves quickly (case 2)" do
+ gemfile <<-G
+ source "https://rubygems.org"
+
+ gem 'metasploit-erd'
+ gem 'rails-erd'
+ gem 'yard'
+
+ gem 'coveralls'
+ gem 'rails'
+ gem 'simplecov'
+ gem 'rspec-rails'
+ G
+
+ bundle "lock", env: { "DEBUG_RESOLVER" => "1" }
+ expect(out).to include("Solution found after 2 attempts")
+ end
+end
diff --git a/spec/bundler/resolver/basic_spec.rb b/spec/bundler/resolver/basic_spec.rb
new file mode 100644
index 0000000000..185df1b1c7
--- /dev/null
+++ b/spec/bundler/resolver/basic_spec.rb
@@ -0,0 +1,422 @@
+# frozen_string_literal: true
+
+RSpec.describe "Resolving" do
+ before :each do
+ @index = an_awesome_index
+ end
+
+ it "resolves a single gem" do
+ dep "myrack"
+
+ should_resolve_as %w[myrack-1.1]
+ end
+
+ it "resolves a gem with dependencies" do
+ dep "actionpack"
+
+ should_resolve_as %w[actionpack-2.3.5 activesupport-2.3.5 myrack-1.0]
+ end
+
+ it "resolves a conflicting index" do
+ @index = a_conflict_index
+ dep "my_app"
+ should_resolve_as %w[activemodel-3.2.11 builder-3.0.4 grape-0.2.6 my_app-1.0.0]
+ end
+
+ it "resolves a complex conflicting index" do
+ @index = a_complex_conflict_index
+ dep "my_app"
+ should_resolve_as %w[a-1.4.0 b-0.3.5 c-3.2 d-0.9.8 my_app-1.1.0]
+ end
+
+ it "resolves a index with conflict on child" do
+ @index = index_with_conflict_on_child
+ dep "chef_app"
+ should_resolve_as %w[berkshelf-2.0.7 chef-10.26 chef_app-1.0.0 json-1.7.7]
+ end
+
+ it "prefers explicitly requested dependencies when resolving an index which would otherwise be ambiguous" do
+ @index = an_ambiguous_index
+ dep "a"
+ dep "b"
+ should_resolve_as %w[a-1.0.0 b-2.0.0 c-1.0.0 d-1.0.0]
+ end
+
+ it "prefers non-prerelease resolutions in sort order" do
+ @index = optional_prereleases_index
+ dep "a"
+ dep "b"
+ should_resolve_as %w[a-1.0.0 b-1.5.0]
+ end
+
+ it "resolves a index with root level conflict on child" do
+ @index = a_index_with_root_conflict_on_child
+ dep "i18n", "~> 0.4"
+ dep "activesupport", "~> 3.0"
+ dep "activerecord", "~> 3.0"
+ dep "builder", "~> 2.1.2"
+ should_resolve_as %w[activesupport-3.0.5 i18n-0.4.2 builder-2.1.2 activerecord-3.0.5 activemodel-3.0.5]
+ end
+
+ it "resolves a gem specified with a pre-release version" do
+ dep "activesupport", "~> 3.0.0.beta"
+ dep "activemerchant"
+ should_resolve_as %w[activemerchant-2.3.5 activesupport-3.0.0.beta1]
+ end
+
+ it "doesn't select a pre-release if not specified in the Gemfile" do
+ dep "activesupport"
+ dep "reform"
+ should_resolve_as %w[reform-1.0.0 activesupport-2.3.5]
+ end
+
+ it "doesn't select a pre-release for sub-dependencies" do
+ dep "reform"
+ should_resolve_as %w[reform-1.0.0 activesupport-2.3.5]
+ end
+
+ it "selects a pre-release for sub-dependencies if it's the only option" do
+ dep "need-pre"
+ should_resolve_as %w[need-pre-1.0.0 activesupport-3.0.0.beta1]
+ end
+
+ it "selects a pre-release if it's specified in the Gemfile" do
+ dep "activesupport", "= 3.0.0.beta"
+ dep "actionpack"
+
+ should_resolve_as %w[activesupport-3.0.0.beta actionpack-3.0.0.beta myrack-1.1 myrack-mount-0.6]
+ end
+
+ it "prefers non-pre-releases when doing conservative updates" do
+ @index = build_index do
+ gem "mail", "2.7.0"
+ gem "mail", "2.7.1.rc1"
+ gem "RubyGems\0", Gem::VERSION
+ end
+ dep "mail"
+ @locked = locked ["mail", "2.7.0"]
+ @base = locked
+ should_conservative_resolve_and_include [:patch], [], ["mail-2.7.0"]
+ end
+
+ it "raises an exception if a child dependency is not resolved" do
+ @index = a_unresolvable_child_index
+ dep "chef_app_error"
+ expect do
+ resolve
+ end.to raise_error(Bundler::SolveFailure)
+ end
+
+ it "does not try to re-resolve including prereleases if gems involved don't have prereleases" do
+ @index = a_unresolvable_child_index
+ dep "chef_app_error"
+ expect(Bundler.ui).not_to receive(:debug).with("Retrying resolution...", any_args)
+ expect do
+ resolve
+ end.to raise_error(Bundler::SolveFailure)
+ end
+
+ it "raises an exception with the minimal set of conflicting dependencies" do
+ @index = build_index do
+ %w[0.9 1.0 2.0].each {|v| gem("a", v) }
+ gem("b", "1.0") { dep "a", ">= 2" }
+ gem("c", "1.0") { dep "a", "< 1" }
+ end
+ dep "a"
+ dep "b"
+ dep "c"
+ expect do
+ resolve
+ end.to raise_error(Bundler::SolveFailure, <<~E.strip)
+ Could not find compatible versions
+
+ Because every version of c depends on a < 1
+ and every version of b depends on a >= 2,
+ every version of c is incompatible with b >= 0.
+ So, because Gemfile depends on b >= 0
+ and Gemfile depends on c >= 0,
+ version solving has failed.
+ E
+ end
+
+ it "should throw error in case of circular dependencies" do
+ @index = a_circular_index
+ dep "circular_app"
+
+ expect do
+ Bundler::SpecSet.new(resolve).sort
+ end.to raise_error(Bundler::CyclicDependencyError, /please remove either gem 'bar' or gem 'foo'/i)
+ end
+
+ # Issue #3459
+ it "should install the latest possible version of a direct requirement with no constraints given" do
+ @index = a_complicated_index
+ dep "foo"
+ should_resolve_and_include %w[foo-3.0.5]
+ end
+
+ # Issue #3459
+ it "should install the latest possible version of a direct requirement with constraints given" do
+ @index = a_complicated_index
+ dep "foo", ">= 3.0.0"
+ should_resolve_and_include %w[foo-3.0.5]
+ end
+
+ it "takes into account required_ruby_version" do
+ @index = build_index do
+ gem "foo", "1.0.0" do
+ dep "bar", ">= 0"
+ end
+
+ gem "foo", "2.0.0" do |s|
+ dep "bar", ">= 0"
+ s.required_ruby_version = "~> 2.0.0"
+ end
+
+ gem "bar", "1.0.0"
+
+ gem "bar", "2.0.0" do |s|
+ s.required_ruby_version = "~> 2.0.0"
+ end
+
+ gem "Ruby\0", "1.8.7"
+ end
+ dep "foo"
+ dep "Ruby\0", "1.8.7"
+
+ should_resolve_and_include %w[foo-1.0.0 bar-1.0.0]
+ end
+
+ context "conservative" do
+ before :each do
+ @index = build_index do
+ gem("foo", "1.3.7") { dep "bar", "~> 2.0" }
+ gem("foo", "1.3.8") { dep "bar", "~> 2.0" }
+ gem("foo", "1.4.3") { dep "bar", "~> 2.0" }
+ gem("foo", "1.4.4") { dep "bar", "~> 2.0" }
+ gem("foo", "1.4.5") { dep "bar", "~> 2.1" }
+ gem("foo", "1.5.0") { dep "bar", "~> 2.1" }
+ gem("foo", "1.5.1") { dep "bar", "~> 3.0" }
+ gem("foo", "2.0.0") { dep "bar", "~> 3.0" }
+ gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0]
+ end
+ dep "foo"
+
+ # base represents declared dependencies in the Gemfile that are still satisfied by the lockfile
+ @base = Bundler::SpecSet.new([])
+
+ # locked represents versions in lockfile
+ @locked = locked(%w[foo 1.4.3], %w[bar 2.0.3])
+ end
+
+ it "resolves all gems to latest patch" do
+ # strict is not set, so bar goes up a minor version due to dependency from foo 1.4.5
+ should_conservative_resolve_and_include :patch, true, %w[foo-1.4.5 bar-2.1.1]
+ end
+
+ it "resolves all gems to latest patch strict" do
+ # strict is set, so foo can only go up to 1.4.4 to avoid bar going up a minor version, and bar can go up to 2.0.5
+ should_conservative_resolve_and_include [:patch, :strict], true, %w[foo-1.4.4 bar-2.0.5]
+ end
+
+ it "resolves foo only to latest patch - same dependency case" do
+ @locked = locked(%w[foo 1.3.7], %w[bar 2.0.3])
+ # bar is locked, and the lock holds here because the dependency on bar doesn't change on the matching foo version.
+ should_conservative_resolve_and_include :patch, ["foo"], %w[foo-1.3.8 bar-2.0.3]
+ end
+
+ it "resolves foo only to latest patch - changing dependency not declared case" do
+ # foo is the only gem being requested for update, therefore bar is locked, but bar is NOT
+ # declared as a dependency in the Gemfile. In this case, locks don't apply to _changing_
+ # dependencies and since the dependency of the selected foo gem changes, the latest matching
+ # dependency of "bar", "~> 2.1" -- bar-2.1.1 -- is selected. This is not a bug and follows
+ # the long-standing documented Conservative Updating behavior of bundle install.
+ # https://bundler.io/v1.12/man/bundle-install.1.html#CONSERVATIVE-UPDATING
+ should_conservative_resolve_and_include :patch, ["foo"], %w[foo-1.4.5 bar-2.1.1]
+ end
+
+ it "resolves foo only to latest patch - changing dependency declared case" do
+ # bar is locked AND a declared dependency in the Gemfile, so it will not move, and therefore
+ # foo can only move up to 1.4.4.
+ @base = Bundler::SpecSet.new([Bundler::LazySpecification.new("bar", Gem::Version.new("2.0.3"), nil)])
+ should_conservative_resolve_and_include :patch, ["foo"], %w[foo-1.4.4 bar-2.0.3]
+ end
+
+ it "resolves foo only to latest patch strict" do
+ # adding strict helps solve the possibly unexpected behavior of bar changing in the prior test case,
+ # because no versions will be returned for bar ~> 2.1, so the engine falls back to ~> 2.0 (turn on
+ # debugging to see this happen).
+ should_conservative_resolve_and_include [:patch, :strict], ["foo"], %w[foo-1.4.4 bar-2.0.3]
+ end
+
+ it "resolves bar only to latest patch" do
+ # bar is locked, so foo can only go up to 1.4.4
+ should_conservative_resolve_and_include :patch, ["bar"], %w[foo-1.4.3 bar-2.0.5]
+ end
+
+ it "resolves all gems to latest minor" do
+ # strict is not set, so bar goes up a major version due to dependency from foo 1.4.5
+ should_conservative_resolve_and_include :minor, true, %w[foo-1.5.1 bar-3.0.0]
+ end
+
+ it "resolves all gems to latest minor strict" do
+ # strict is set, so foo can only go up to 1.5.0 to avoid bar going up a major version
+ should_conservative_resolve_and_include [:minor, :strict], true, %w[foo-1.5.0 bar-2.1.1]
+ end
+
+ it "resolves all gems to latest major" do
+ should_conservative_resolve_and_include :major, true, %w[foo-2.0.0 bar-3.0.0]
+ end
+
+ it "resolves all gems to latest major strict" do
+ should_conservative_resolve_and_include [:major, :strict], true, %w[foo-2.0.0 bar-3.0.0]
+ end
+
+ # Why would this happen in real life? If bar 2.2 has a bug that the author of foo wants to bypass
+ # by reverting the dependency, the author of foo could release a new gem with an older requirement.
+ context "revert to previous" do
+ before :each do
+ @index = build_index do
+ gem("foo", "1.4.3") { dep "bar", "~> 2.2" }
+ gem("foo", "1.4.4") { dep "bar", "~> 2.1.0" }
+ gem("foo", "1.5.0") { dep "bar", "~> 2.0.0" }
+ gem "bar", %w[2.0.5 2.1.1 2.2.3]
+ end
+ dep "foo"
+
+ # base represents declared dependencies in the Gemfile that are still satisfied by the lockfile
+ @base = Bundler::SpecSet.new([])
+
+ # locked represents versions in lockfile
+ @locked = locked(%w[foo 1.4.3], %w[bar 2.2.3])
+ end
+
+ it "could revert to a previous version level patch" do
+ should_conservative_resolve_and_include :patch, true, %w[foo-1.4.4 bar-2.1.1]
+ end
+
+ it "cannot revert to a previous version in strict mode level patch" do
+ # fall back to the locked resolution since strict means we can't regress either version
+ should_conservative_resolve_and_include [:patch, :strict], true, %w[foo-1.4.3 bar-2.2.3]
+ end
+
+ it "could revert to a previous version level minor" do
+ should_conservative_resolve_and_include :minor, true, %w[foo-1.5.0 bar-2.0.5]
+ end
+
+ it "cannot revert to a previous version in strict mode level minor" do
+ # fall back to the locked resolution since strict means we can't regress either version
+ should_conservative_resolve_and_include [:minor, :strict], true, %w[foo-1.4.3 bar-2.2.3]
+ end
+ end
+ end
+
+ it "handles versions that redundantly depend on themselves" do
+ @index = build_index do
+ gem "myrack", "3.0.0"
+
+ gem "standalone_migrations", "7.1.0" do
+ dep "myrack", "~> 2.0"
+ end
+
+ gem "standalone_migrations", "2.0.4" do
+ dep "standalone_migrations", ">= 0"
+ end
+
+ gem "standalone_migrations", "1.0.13" do
+ dep "myrack", ">= 0"
+ end
+ end
+
+ dep "myrack", "~> 3.0"
+ dep "standalone_migrations"
+
+ should_resolve_as %w[myrack-3.0.0 standalone_migrations-2.0.4]
+ end
+
+ it "ignores versions that incorrectly depend on themselves" do
+ @index = build_index do
+ gem "myrack", "3.0.0"
+
+ gem "standalone_migrations", "7.1.0" do
+ dep "myrack", "~> 2.0"
+ end
+
+ gem "standalone_migrations", "2.0.4" do
+ dep "standalone_migrations", ">= 2.0.5"
+ end
+
+ gem "standalone_migrations", "1.0.13" do
+ dep "myrack", ">= 0"
+ end
+ end
+
+ dep "myrack", "~> 3.0"
+ dep "standalone_migrations"
+
+ should_resolve_as %w[myrack-3.0.0 standalone_migrations-1.0.13]
+ end
+
+ it "does not ignore versions that incorrectly depend on themselves when dependency_api is not available" do
+ @index = build_index do
+ gem "myrack", "3.0.0"
+
+ gem "standalone_migrations", "7.1.0" do
+ dep "myrack", "~> 2.0"
+ end
+
+ gem "standalone_migrations", "2.0.4" do
+ dep "standalone_migrations", ">= 2.0.5"
+ end
+
+ gem "standalone_migrations", "1.0.13" do
+ dep "myrack", ">= 0"
+ end
+ end
+
+ dep "myrack", "~> 3.0"
+ dep "standalone_migrations"
+
+ should_resolve_without_dependency_api %w[myrack-3.0.0 standalone_migrations-2.0.4]
+ end
+
+ it "resolves fine cases that need joining unbounded disjoint ranges" do
+ @index = build_index do
+ gem "inspec", "5.22.3" do
+ dep "ruby", ">= 3.2.2"
+ dep "train-kubernetes", ">= 0.1.7"
+ end
+
+ gem "ruby", "3.2.2"
+
+ gem "train-kubernetes", "0.1.12" do
+ dep "k8s-ruby", ">= 0.14.0"
+ end
+
+ gem "train-kubernetes", "0.1.10" do
+ dep "k8s-ruby", "= 0.10.5"
+ end
+
+ gem "train-kubernetes", "0.1.7" do
+ dep "k8s-ruby", ">= 0.10.5"
+ end
+
+ gem "k8s-ruby", "0.10.5" do
+ dep "ruby","< 3.2.2"
+ end
+
+ gem "k8s-ruby", "0.11.0" do
+ dep "ruby", ">= 3.2.2"
+ end
+
+ gem "k8s-ruby", "0.14.0" do
+ dep "ruby", "< 3.2.2"
+ end
+ end
+
+ dep "inspec", "5.22.3"
+ dep "ruby", "3.2.2"
+
+ should_resolve_as %w[inspec-5.22.3 ruby-3.2.2 train-kubernetes-0.1.7 k8s-ruby-0.11.0]
+ end
+end
diff --git a/spec/bundler/resolver/platform_spec.rb b/spec/bundler/resolver/platform_spec.rb
new file mode 100644
index 0000000000..a1d095d024
--- /dev/null
+++ b/spec/bundler/resolver/platform_spec.rb
@@ -0,0 +1,423 @@
+# frozen_string_literal: true
+
+RSpec.describe "Resolving platform craziness" do
+ describe "with cross-platform gems" do
+ before :each do
+ @index = an_awesome_index
+ end
+
+ it "resolves a simple multi platform gem" do
+ dep "nokogiri"
+ platforms "ruby", "java"
+
+ should_resolve_as %w[nokogiri-1.4.2 nokogiri-1.4.2-java weakling-0.0.3]
+ end
+
+ it "doesn't pull gems that don't exist for the current platform" do
+ dep "nokogiri"
+ platforms "ruby"
+
+ should_resolve_as %w[nokogiri-1.4.2]
+ end
+
+ it "doesn't pull gems when the version is available for all requested platforms" do
+ dep "nokogiri"
+ platforms "mswin32"
+
+ should_resolve_as %w[nokogiri-1.4.2.1-x86-mswin32]
+ end
+ end
+
+ it "resolves multiplatform gems with redundant platforms correctly" do
+ @index = build_index do
+ gem "zookeeper", "1.4.11"
+ gem "zookeeper", "1.4.11", "java" do
+ dep "slyphon-log4j", "= 1.2.15"
+ dep "slyphon-zookeeper_jar", "= 3.3.5"
+ end
+ gem "slyphon-log4j", "1.2.15"
+ gem "slyphon-zookeeper_jar", "3.3.5", "java"
+ end
+
+ dep "zookeeper"
+ platforms "java", "ruby", "universal-java-11"
+
+ should_resolve_as %w[zookeeper-1.4.11 zookeeper-1.4.11-java slyphon-log4j-1.2.15 slyphon-zookeeper_jar-3.3.5-java]
+ end
+
+ it "takes the latest ruby gem, even if an older platform specific version is available" do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x64-mingw-ucrt"
+ gem "foo", "1.1.0"
+ end
+ dep "foo"
+ platforms "x64-mingw-ucrt"
+
+ should_resolve_as %w[foo-1.1.0]
+ end
+
+ it "takes the ruby version if the platform version is incompatible" do
+ @index = build_index do
+ gem "bar", "1.0.0"
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x64-mingw-ucrt" do
+ dep "bar", "< 1"
+ end
+ end
+ dep "foo"
+ platforms "x64-mingw-ucrt"
+
+ should_resolve_as %w[foo-1.0.0]
+ end
+
+ it "prefers the platform specific gem to the ruby version" do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x64-mingw-ucrt"
+ end
+ dep "foo"
+ platforms "x64-mingw-ucrt"
+
+ should_resolve_as %w[foo-1.0.0-x64-mingw-ucrt]
+ end
+
+ describe "on a linux platform" do
+ # Ruby's platform is *-linux => platform's libc is glibc, so not musl
+ # Ruby's platform is *-linux-musl => platform's libc is musl, so not glibc
+ # Gem's platform is *-linux => gem is glibc + maybe musl compatible
+ # Gem's platform is *-linux-musl => gem is musl compatible but not glibc
+
+ it "favors the platform version-specific gem on a version-specifying linux platform" do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x86_64-linux"
+ gem "foo", "1.0.0", "x86_64-linux-musl"
+ end
+ dep "foo"
+ platforms "x86_64-linux-musl"
+
+ should_resolve_as %w[foo-1.0.0-x86_64-linux-musl]
+ end
+
+ it "favors the version-less gem over the version-specific gem on a gnu linux platform" do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x86_64-linux"
+ gem "foo", "1.0.0", "x86_64-linux-musl"
+ end
+ dep "foo"
+ platforms "x86_64-linux"
+
+ should_resolve_as %w[foo-1.0.0-x86_64-linux]
+ end
+
+ it "ignores the platform version-specific gem on a gnu linux platform" do
+ @index = build_index do
+ gem "foo", "1.0.0", "x86_64-linux-musl"
+ end
+ dep "foo"
+ platforms "x86_64-linux"
+
+ should_not_resolve
+ end
+
+ it "falls back to the platform version-less gem on a linux platform with a version" do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x86_64-linux"
+ end
+ dep "foo"
+ platforms "x86_64-linux-musl"
+
+ should_resolve_as %w[foo-1.0.0-x86_64-linux]
+ end
+
+ it "falls back to the ruby platform gem on a gnu linux platform when only a version-specifying gem is available" do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x86_64-linux-musl"
+ end
+ dep "foo"
+ platforms "x86_64-linux"
+
+ should_resolve_as %w[foo-1.0.0]
+ end
+
+ it "falls back to the platform version-less gem on a version-specifying linux platform and no ruby platform gem is available" do
+ @index = build_index do
+ gem "foo", "1.0.0", "x86_64-linux"
+ end
+ dep "foo"
+ platforms "x86_64-linux-musl"
+
+ should_resolve_as %w[foo-1.0.0-x86_64-linux]
+ end
+ end
+
+ context "when the platform specific gem doesn't match the required_ruby_version" do
+ before do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x64-mingw-ucrt"
+ gem "foo", "1.1.0"
+ gem "foo", "1.1.0", "x64-mingw-ucrt" do |s|
+ s.required_ruby_version = [">= 2.0", "< 2.4"]
+ end
+ gem "Ruby\0", "2.5.1"
+ end
+ dep "Ruby\0", "2.5.1"
+ platforms "x64-mingw-ucrt"
+ end
+
+ it "takes the latest ruby gem" do
+ dep "foo"
+
+ should_resolve_as %w[foo-1.1.0]
+ end
+
+ it "takes the latest ruby gem, even if requirement does not match previous versions with the same ruby requirement" do
+ dep "foo", "1.1.0"
+
+ should_resolve_as %w[foo-1.1.0]
+ end
+ end
+
+ it "takes the latest ruby gem with required_ruby_version if the platform specific gem doesn't match the required_ruby_version" do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x64-mingw-ucrt"
+ gem "foo", "1.1.0" do |s|
+ s.required_ruby_version = [">= 2.0"]
+ end
+ gem "foo", "1.1.0", "x64-mingw-ucrt" do |s|
+ s.required_ruby_version = [">= 2.0", "< 2.4"]
+ end
+ gem "Ruby\0", "2.5.1"
+ end
+ dep "foo"
+ dep "Ruby\0", "2.5.1"
+ platforms "x64-mingw-ucrt"
+
+ should_resolve_as %w[foo-1.1.0]
+ end
+
+ it "takes the latest ruby gem if the platform specific gem doesn't match the required_ruby_version with multiple platforms" do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x64-mingw-ucrt"
+ gem "foo", "1.1.0" do |s|
+ s.required_ruby_version = [">= 2.0"]
+ end
+ gem "foo", "1.1.0", "x64-mingw-ucrt" do |s|
+ s.required_ruby_version = [">= 2.0", "< 2.4"]
+ end
+ gem "Ruby\0", "2.5.1"
+ end
+ dep "foo"
+ dep "Ruby\0", "2.5.1"
+ platforms "x86_64-linux", "x64-mingw-ucrt"
+
+ should_resolve_as %w[foo-1.1.0]
+ end
+
+ it "includes gems needed for at least one platform" do
+ @index = build_index do
+ gem "empyrean", "0.1.0"
+ gem "coderay", "1.1.2"
+ gem "method_source", "0.9.0"
+
+ gem "spoon", "0.0.6" do
+ dep "ffi", ">= 0"
+ end
+
+ gem "pry", "0.11.3", "java" do
+ dep "coderay", "~> 1.1.0"
+ dep "method_source", "~> 0.9.0"
+ dep "spoon", "~> 0.0"
+ end
+
+ gem "pry", "0.11.3" do
+ dep "coderay", "~> 1.1.0"
+ dep "method_source", "~> 0.9.0"
+ end
+
+ gem "ffi", "1.9.23", "java"
+ gem "ffi", "1.9.23"
+
+ gem "extra", "1.0.0" do
+ dep "ffi", ">= 0"
+ end
+ end
+
+ dep "empyrean", "0.1.0"
+ dep "pry"
+ dep "extra"
+
+ platforms "ruby", "java"
+
+ should_resolve_as %w[coderay-1.1.2 empyrean-0.1.0 extra-1.0.0 ffi-1.9.23 ffi-1.9.23-java method_source-0.9.0 pry-0.11.3 pry-0.11.3-java spoon-0.0.6]
+ end
+
+ it "includes gems needed for at least one platform even when the platform specific requirement is processed earlier than the generic requirement" do
+ @index = build_index do
+ gem "empyrean", "0.1.0"
+ gem "coderay", "1.1.2"
+ gem "method_source", "0.9.0"
+
+ gem "spoon", "0.0.6" do
+ dep "ffi", ">= 0"
+ end
+
+ gem "pry", "0.11.3", "java" do
+ dep "coderay", "~> 1.1.0"
+ dep "method_source", "~> 0.9.0"
+ dep "spoon", "~> 0.0"
+ end
+
+ gem "pry", "0.11.3" do
+ dep "coderay", "~> 1.1.0"
+ dep "method_source", "~> 0.9.0"
+ end
+
+ gem "ffi", "1.9.23", "java"
+ gem "ffi", "1.9.23"
+
+ gem "extra", "1.0.0" do
+ dep "extra2", ">= 0"
+ end
+
+ gem "extra2", "1.0.0" do
+ dep "extra3", ">= 0"
+ end
+
+ gem "extra3", "1.0.0" do
+ dep "ffi", ">= 0"
+ end
+ end
+
+ dep "empyrean", "0.1.0"
+ dep "pry"
+ dep "extra"
+
+ platforms "ruby", "java"
+
+ should_resolve_as %w[coderay-1.1.2 empyrean-0.1.0 extra-1.0.0 extra2-1.0.0 extra3-1.0.0 ffi-1.9.23 ffi-1.9.23-java method_source-0.9.0 pry-0.11.3 pry-0.11.3-java spoon-0.0.6]
+ end
+
+ it "properly adds platforms when platform requirements come from different dependencies" do
+ @index = build_index do
+ gem "ffi", "1.9.14"
+ gem "ffi", "1.9.14", "universal-mingw32"
+
+ gem "gssapi", "0.1"
+ gem "gssapi", "0.2"
+ gem "gssapi", "0.3"
+ gem "gssapi", "1.2.0" do
+ dep "ffi", ">= 1.0.1"
+ end
+
+ gem "mixlib-shellout", "2.2.6"
+ gem "mixlib-shellout", "2.2.6", "universal-mingw32" do
+ dep "win32-process", "~> 0.8.2"
+ end
+
+ # we need all these versions to get the sorting the same as it would be
+ # pulling from rubygems.org
+ %w[0.8.3 0.8.2 0.8.1 0.8.0].each do |v|
+ gem "win32-process", v do
+ dep "ffi", ">= 1.0.0"
+ end
+ end
+ end
+
+ dep "mixlib-shellout"
+ dep "gssapi"
+
+ platforms "universal-mingw32", "ruby"
+
+ should_resolve_as %w[ffi-1.9.14 ffi-1.9.14-universal-mingw32 gssapi-1.2.0 mixlib-shellout-2.2.6 mixlib-shellout-2.2.6-universal-mingw32 win32-process-0.8.3]
+ end
+
+ describe "with mingw32" do
+ before :each do
+ @index = build_index do
+ platforms "mingw32 mswin32 x64-mingw-ucrt" do |platform|
+ gem "thin", "1.2.7", platform
+ end
+ gem "win32-api", "1.5.1", "universal-mingw32"
+ end
+ end
+
+ it "finds mswin gems" do
+ # win32 is hardcoded to get CPU x86 in rubygems
+ platforms "mswin32"
+ dep "thin"
+ should_resolve_as %w[thin-1.2.7-x86-mswin32]
+ end
+
+ it "finds mingw gems" do
+ # mingw is _not_ hardcoded to add CPU x86 in rubygems
+ platforms "x86-mingw32"
+ dep "thin"
+ should_resolve_as %w[thin-1.2.7-mingw32]
+ end
+
+ it "finds x64-mingw-ucrt gems" do
+ platforms "x64-mingw-ucrt"
+ dep "thin"
+ should_resolve_as %w[thin-1.2.7-x64-mingw-ucrt]
+ end
+
+ it "finds universal-mingw gems on x86-mingw" do
+ platform "x86-mingw32"
+ dep "win32-api"
+ should_resolve_as %w[win32-api-1.5.1-universal-mingw32]
+ end
+
+ it "finds universal-mingw gems on x64-mingw" do
+ platform "x64-mingw-ucrt"
+ dep "win32-api"
+ should_resolve_as %w[win32-api-1.5.1-universal-mingw32]
+ end
+
+ it "finds x64-mingw-ucrt gems" do
+ platforms "x64-mingw-ucrt"
+ dep "thin"
+ should_resolve_as %w[thin-1.2.7-x64-mingw-ucrt]
+ end
+
+ it "finds universal-mingw gems on x64-mingw-ucrt" do
+ platform "x64-mingw-ucrt"
+ dep "win32-api"
+ should_resolve_as %w[win32-api-1.5.1-universal-mingw32]
+ end
+ end
+
+ describe "with conflicting cases" do
+ before :each do
+ @index = build_index do
+ gem "foo", "1.0.0" do
+ dep "bar", ">= 0"
+ end
+
+ gem "bar", "1.0.0" do
+ dep "baz", "~> 1.0.0"
+ end
+
+ gem "bar", "1.0.0", "java" do
+ dep "baz", " ~> 1.1.0"
+ end
+
+ gem "baz", %w[1.0.0 1.1.0 1.2.0]
+ end
+ end
+
+ it "takes the ruby version as fallback" do
+ platforms "ruby", "java"
+ dep "foo"
+
+ should_resolve_as %w[bar-1.0.0 baz-1.0.0 foo-1.0.0]
+ end
+ end
+end
diff --git a/spec/bundler/runtime/env_helpers_spec.rb b/spec/bundler/runtime/env_helpers_spec.rb
new file mode 100644
index 0000000000..c4ebdd1fd2
--- /dev/null
+++ b/spec/bundler/runtime/env_helpers_spec.rb
@@ -0,0 +1,236 @@
+# frozen_string_literal: true
+
+RSpec.describe "env helpers" do
+ def bundle_exec_ruby(args, options = {})
+ build_bundler_context options.dup
+ bundle "exec '#{Gem.ruby}' #{args}", options
+ end
+
+ def build_bundler_context(options = {})
+ bundle "config set path vendor/bundle", options.dup
+ gemfile "source 'https://gem.repo1'"
+ bundle "install", options
+ end
+
+ def run_bundler_script(env, script)
+ system(env, "ruby", "-I#{lib_dir}", "-rbundler", script.to_s)
+ end
+
+ describe "Bundler.original_env" do
+ it "should return the PATH present before bundle was activated" do
+ create_file("source.rb", <<-RUBY)
+ print Bundler.original_env["PATH"]
+ RUBY
+ path = `getconf PATH`.strip + "#{File::PATH_SEPARATOR}/foo"
+ with_path_as(path) do
+ bundle_exec_ruby(bundled_app("source.rb").to_s)
+ expect(stdboth).to eq(path)
+ end
+ end
+
+ it "should return the GEM_PATH present before bundle was activated" do
+ create_file("source.rb", <<-RUBY)
+ print Bundler.original_env['GEM_PATH']
+ RUBY
+ gem_path = ENV["GEM_PATH"] + "#{File::PATH_SEPARATOR}/foo"
+ with_gem_path_as(gem_path) do
+ bundle_exec_ruby(bundled_app("source.rb").to_s)
+ expect(stdboth).to eq(gem_path)
+ end
+ end
+
+ it "works with nested bundle exec invocations", :ruby_repo do
+ create_file("exe.rb", <<-'RUBY')
+ count = ARGV.first.to_i
+ exit if count < 0
+ STDERR.puts "#{count} #{ENV["PATH"].end_with?("#{File::PATH_SEPARATOR}/foo")}"
+ if count == 2
+ ENV["PATH"] = "#{ENV["PATH"]}#{File::PATH_SEPARATOR}/foo"
+ end
+ exec(Gem.ruby, __FILE__, (count - 1).to_s)
+ RUBY
+ path = `getconf PATH`.strip + File::PATH_SEPARATOR + File.dirname(Gem.ruby)
+ with_path_as(path) do
+ build_bundler_context
+ bundle_exec_ruby("#{bundled_app("exe.rb")} 2")
+ end
+ expect(err).to eq <<-EOS.strip
+2 false
+1 true
+0 true
+ EOS
+ end
+
+ it "removes variables that bundler added", :ruby_repo do
+ original = ruby('puts ENV.to_a.map {|e| e.join("=") }.sort.join("\n")', artifice: "fail")
+ create_file("source.rb", <<-RUBY)
+ puts Bundler.original_env.to_a.map {|e| e.join("=") }.sort.join("\n")
+ RUBY
+ bundle_exec_ruby bundled_app("source.rb"), artifice: "fail"
+ expect(out).to eq original
+ end
+ end
+
+ describe "Bundler.unbundled_env" do
+ it "should delete BUNDLE_PATH" do
+ create_file("source.rb", <<-RUBY)
+ print Bundler.unbundled_env.has_key?('BUNDLE_PATH')
+ RUBY
+ ENV["BUNDLE_PATH"] = "./foo"
+ bundle_exec_ruby bundled_app("source.rb")
+ expect(stdboth).to include "false"
+ end
+
+ it "should remove absolute path to 'bundler/setup' from RUBYOPT even if it was present in original env" do
+ create_file("source.rb", <<-RUBY)
+ print Bundler.unbundled_env['RUBYOPT']
+ RUBY
+ setup_require = "-r#{lib_dir}/bundler/setup"
+ ENV["BUNDLER_ORIG_RUBYOPT"] = "-W2 #{setup_require} #{ENV["RUBYOPT"]}"
+ bundle_exec_ruby bundled_app("source.rb")
+ expect(stdboth).not_to include(setup_require)
+ end
+
+ it "should remove relative path to 'bundler/setup' from RUBYOPT even if it was present in original env" do
+ create_file("source.rb", <<-RUBY)
+ print Bundler.unbundled_env['RUBYOPT']
+ RUBY
+ ENV["BUNDLER_ORIG_RUBYOPT"] = "-W2 -rbundler/setup #{ENV["RUBYOPT"]}"
+ bundle_exec_ruby bundled_app("source.rb")
+ expect(stdboth).not_to include("-rbundler/setup")
+ end
+
+ it "should delete BUNDLER_SETUP even if it was present in original env" do
+ create_file("source.rb", <<-RUBY)
+ print Bundler.unbundled_env.has_key?('BUNDLER_SETUP')
+ RUBY
+ ENV["BUNDLER_ORIG_BUNDLER_SETUP"] = system_gem_path("gems/bundler-#{Bundler::VERSION}/lib/bundler/setup").to_s
+ bundle_exec_ruby bundled_app("source.rb")
+ expect(stdboth).to include "false"
+ end
+
+ it "should restore RUBYLIB", :ruby_repo do
+ create_file("source.rb", <<-RUBY)
+ print Bundler.unbundled_env['RUBYLIB']
+ RUBY
+ ENV["RUBYLIB"] = lib_dir.to_s + File::PATH_SEPARATOR + "/foo"
+ ENV["BUNDLER_ORIG_RUBYLIB"] = lib_dir.to_s + File::PATH_SEPARATOR + "/foo-original"
+ bundle_exec_ruby bundled_app("source.rb")
+ expect(stdboth).to include("/foo-original")
+ end
+
+ it "should restore the original MANPATH" do
+ create_file("source.rb", <<-RUBY)
+ print Bundler.unbundled_env['MANPATH']
+ RUBY
+ ENV["MANPATH"] = "/foo"
+ ENV["BUNDLER_ORIG_MANPATH"] = "/foo-original"
+ bundle_exec_ruby bundled_app("source.rb")
+ expect(stdboth).to include("/foo-original")
+ end
+ end
+
+ describe "Bundler.with_original_env" do
+ it "should set ENV to original_env in the block" do
+ expected = Bundler.original_env
+ actual = Bundler.with_original_env { ENV.to_hash }
+ expect(actual).to eq(expected)
+ end
+
+ it "should restore the environment after execution" do
+ Bundler.with_original_env do
+ ENV["FOO"] = "hello"
+ end
+
+ expect(ENV).not_to have_key("FOO")
+ end
+ end
+
+ describe "Bundler.with_unbundled_env" do
+ it "should set ENV to unbundled_env in the block" do
+ expected = Bundler.unbundled_env
+ actual = Bundler.with_unbundled_env { ENV.to_hash }
+ expect(actual).to eq(expected)
+ end
+
+ it "should restore the environment after execution" do
+ Bundler.with_unbundled_env do
+ ENV["FOO"] = "hello"
+ end
+
+ expect(ENV).not_to have_key("FOO")
+ end
+ end
+
+ describe "Bundler.original_system" do
+ before do
+ create_file("source.rb", <<-'RUBY')
+ Bundler.original_system("ruby", "-e", "exit(42) if ENV['BUNDLE_FOO'] == 'bar'")
+
+ exit $?.exitstatus
+ RUBY
+ end
+
+ it "runs system inside with_original_env" do
+ run_bundler_script({ "BUNDLE_FOO" => "bar" }, bundled_app("source.rb"))
+ expect($?.exitstatus).to eq(42)
+ end
+ end
+
+ describe "Bundler.unbundled_system" do
+ before do
+ create_file("source.rb", <<-'RUBY')
+ Bundler.unbundled_system("ruby", "-e", "exit(42) unless ENV['BUNDLE_FOO'] == 'bar'")
+
+ exit $?.exitstatus
+ RUBY
+ end
+
+ it "runs system inside with_unbundled_env" do
+ run_bundler_script({ "BUNDLE_FOO" => "bar" }, bundled_app("source.rb"))
+ expect($?.exitstatus).to eq(42)
+ end
+ end
+
+ describe "Bundler.original_exec" do
+ before do
+ create_file("source.rb", <<-'RUBY')
+ Process.fork do
+ exit Bundler.original_exec(%(test "\$BUNDLE_FOO" = "bar"))
+ end
+
+ _, status = Process.wait2
+
+ exit(status.exitstatus)
+ RUBY
+ end
+
+ it "runs exec inside with_original_env" do
+ skip "Fork not implemented" if Gem.win_platform?
+
+ run_bundler_script({ "BUNDLE_FOO" => "bar" }, bundled_app("source.rb"))
+ expect($?.exitstatus).to eq(0)
+ end
+ end
+
+ describe "Bundler.unbundled_exec" do
+ before do
+ create_file("source.rb", <<-'RUBY')
+ Process.fork do
+ exit Bundler.unbundled_exec(%(test "\$BUNDLE_FOO" = "bar"))
+ end
+
+ _, status = Process.wait2
+
+ exit(status.exitstatus)
+ RUBY
+ end
+
+ it "runs exec inside with_unbundled_env" do
+ skip "Fork not implemented" if Gem.win_platform?
+
+ run_bundler_script({ "BUNDLE_FOO" => "bar" }, bundled_app("source.rb"))
+ expect($?.exitstatus).to eq(1)
+ end
+ end
+end
diff --git a/spec/bundler/runtime/executable_spec.rb b/spec/bundler/runtime/executable_spec.rb
new file mode 100644
index 0000000000..89cee21b00
--- /dev/null
+++ b/spec/bundler/runtime/executable_spec.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+RSpec.describe "Running bin/* commands" do
+ before :each do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "runs the bundled command when in the bundle" do
+ bundle "binstubs myrack"
+
+ build_gem "myrack", "2.0", to_system: true do |s|
+ s.executables = "myrackup"
+ end
+
+ gembin "myrackup"
+ expect(out).to eq("1.0.0")
+ end
+
+ it "allows the location of the gem stubs to be configured" do
+ bundle_config "bin gbin"
+ bundle "binstubs myrack"
+
+ expect(bundled_app("bin")).not_to exist
+ expect(bundled_app("gbin/myrackup")).to exist
+
+ gembin bundled_app("gbin/myrackup")
+ expect(out).to eq("1.0.0")
+ end
+
+ it "allows absolute paths as a specification of where to install bin stubs" do
+ bundle_config "bin #{tmp("bin")}"
+ bundle "binstubs myrack"
+
+ gembin tmp("bin/myrackup")
+ expect(out).to eq("1.0.0")
+ end
+
+ it "uses the default ruby install name when shebang is not specified" do
+ bundle "binstubs myrack"
+ expect(File.readlines(bundled_app("bin/myrackup")).first).to eq("#!/usr/bin/env #{RbConfig::CONFIG["ruby_install_name"]}\n")
+ end
+
+ it "allows the name of the shebang executable to be specified" do
+ bundle "binstubs myrack", shebang: "ruby-foo"
+ expect(File.readlines(bundled_app("bin/myrackup")).first).to eq("#!/usr/bin/env ruby-foo\n")
+ end
+
+ it "runs the bundled command when out of the bundle" do
+ bundle "binstubs myrack"
+
+ build_gem "myrack", "2.0", to_system: true do |s|
+ s.executables = "myrackup"
+ end
+
+ gembin "myrackup", dir: tmp
+ expect(out).to eq("1.0.0")
+ end
+
+ it "works with gems in path" do
+ build_lib "myrack", path: lib_path("myrack") do |s|
+ s.executables = "myrackup"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :path => "#{lib_path("myrack")}"
+ G
+
+ bundle "binstubs myrack"
+
+ build_gem "myrack", "2.0", to_system: true do |s|
+ s.executables = "myrackup"
+ end
+
+ gembin "myrackup"
+ expect(out).to eq("1.0")
+ end
+
+ it "does not create a bundle binstub" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "bundler"
+ G
+
+ bundle "binstubs bundler"
+
+ expect(bundled_app("bin/bundle")).not_to exist
+
+ expect(err).to include("Bundler itself does not use binstubs because its version is selected by RubyGems")
+ end
+
+ it "does not generate bin stubs if the option was not specified" do
+ bundle "install"
+
+ expect(bundled_app("bin/myrackup")).not_to exist
+ end
+
+ it "rewrites bins on binstubs with --force option" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ create_file("bin/myrackup", "OMG")
+
+ bundle "binstubs myrack", { force: true }
+
+ expect(bundled_app("bin/myrackup").read.strip).to_not eq("OMG")
+ end
+
+ it "use BUNDLE_GEMFILE gemfile for binstub" do
+ # context with bin/bundler w/ default Gemfile
+ bundle "binstubs bundler"
+
+ # generate other Gemfile with executable gem
+ build_repo2 do
+ build_gem("bindir") {|s| s.executables = "foo" }
+ end
+
+ gemfile("OtherGemfile", <<-G)
+ source "https://gem.repo2"
+ gem 'bindir'
+ G
+
+ # generate binstub for executable from non default Gemfile (other then bin/bundler version)
+ ENV["BUNDLE_GEMFILE"] = "OtherGemfile"
+ bundle "install"
+ bundle "binstubs bindir"
+
+ # remove user settings
+ ENV["BUNDLE_GEMFILE"] = nil
+
+ # run binstub for non default Gemfile
+ gembin "foo"
+
+ expect(out).to eq("1.0")
+ end
+end
diff --git a/spec/bundler/runtime/gem_tasks_spec.rb b/spec/bundler/runtime/gem_tasks_spec.rb
new file mode 100644
index 0000000000..b855142e60
--- /dev/null
+++ b/spec/bundler/runtime/gem_tasks_spec.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: true
+
+RSpec.describe "require 'bundler/gem_tasks'" do
+ let(:define_local_gem_using_gem_tasks) do
+ bundled_app("foo.gemspec").open("w") do |f|
+ f.write <<-GEMSPEC
+ Gem::Specification.new do |s|
+ s.name = "foo"
+ s.version = "1.0"
+ s.summary = "dummy"
+ s.author = "Perry Mason"
+ end
+ GEMSPEC
+ end
+
+ bundled_app("Rakefile").open("w") do |f|
+ f.write <<-RAKEFILE
+ require "bundler/gem_tasks"
+ RAKEFILE
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "rake"
+ G
+ end
+
+ let(:define_local_gem_with_extensions_using_gem_tasks_and_gemspec_dsl) do
+ bundled_app("foo.gemspec").open("w") do |f|
+ f.write <<-GEMSPEC
+ Gem::Specification.new do |s|
+ s.name = "foo"
+ s.version = "1.0"
+ s.summary = "dummy"
+ s.author = "Perry Mason"
+ s.extensions = "ext/extconf.rb"
+ end
+ GEMSPEC
+ end
+
+ bundled_app("Rakefile").open("w") do |f|
+ f.write <<-RAKEFILE
+ require "bundler/gem_tasks"
+ RAKEFILE
+ end
+
+ Dir.mkdir bundled_app("ext")
+
+ bundled_app("ext/extconf.rb").open("w") do |f|
+ f.write <<-EXTCONF
+ require "mkmf"
+ File.write("Makefile", dummy_makefile($srcdir).join)
+ EXTCONF
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gemspec
+
+ gem "rake"
+ G
+ end
+
+ it "includes the relevant tasks" do
+ define_local_gem_using_gem_tasks
+
+ in_bundled_app "rake -T"
+
+ expect(err).to be_empty
+ expected_tasks = [
+ "rake build",
+ "rake clean",
+ "rake clobber",
+ "rake install",
+ "rake release[remote]",
+ ]
+ tasks = out.lines.to_a.map {|s| s.split("#").first.strip }
+ expect(tasks & expected_tasks).to eq(expected_tasks)
+ end
+
+ it "defines a working `rake install` task", :ruby_repo do
+ define_local_gem_using_gem_tasks
+
+ in_bundled_app "rake install"
+
+ expect(err).to be_empty
+
+ bundle "exec rake install"
+
+ expect(err).to be_empty
+ end
+
+ it "defines a working `rake install` task for local gems with extensions", :ruby_repo do
+ define_local_gem_with_extensions_using_gem_tasks_and_gemspec_dsl
+
+ bundle "exec rake install"
+
+ expect(err).to be_empty
+ end
+
+ context "rake build when path has spaces", :ruby_repo do
+ before do
+ define_local_gem_using_gem_tasks
+
+ spaced_bundled_app = tmp("bundled app")
+ FileUtils.cp_r bundled_app, spaced_bundled_app
+ bundle "exec rake build", dir: spaced_bundled_app
+ end
+
+ it "still runs successfully" do
+ expect(err).to be_empty
+ end
+ end
+
+ context "rake build when path has brackets", :ruby_repo do
+ before do
+ define_local_gem_using_gem_tasks
+
+ bracketed_bundled_app = tmp("bundled[app")
+ FileUtils.cp_r bundled_app, bracketed_bundled_app
+ bundle "exec rake build", dir: bracketed_bundled_app
+ end
+
+ it "still runs successfully" do
+ expect(err).to be_empty
+ end
+ end
+
+ context "bundle path configured locally" do
+ before do
+ define_local_gem_using_gem_tasks
+
+ bundle_config "path vendor/bundle"
+ end
+
+ it "works", :ruby_repo do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "rake"
+ G
+
+ bundle "exec rake -T"
+
+ expect(err).to be_empty
+ end
+ end
+
+ it "adds 'pkg' to rake/clean's CLOBBER" do
+ define_local_gem_using_gem_tasks
+
+ in_bundled_app %(rake -e 'load "Rakefile"; puts CLOBBER.inspect')
+
+ expect(out).to eq '["pkg"]'
+ end
+end
diff --git a/spec/bundler/runtime/inline_spec.rb b/spec/bundler/runtime/inline_spec.rb
new file mode 100644
index 0000000000..c6f9bbdbd7
--- /dev/null
+++ b/spec/bundler/runtime/inline_spec.rb
@@ -0,0 +1,751 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundler/inline#gemfile" do
+ def script(code, options = {})
+ options[:artifice] ||= "compact_index"
+ options[:env] ||= { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ ruby("require 'bundler/inline'\n\n" + code, options)
+ end
+
+ before :each do
+ build_lib "one", "1.0.0" do |s|
+ s.write "lib/baz.rb", "puts 'baz'"
+ s.write "lib/qux.rb", "puts 'qux'"
+ end
+
+ build_lib "two", "1.0.0" do |s|
+ s.write "lib/two.rb", "puts 'two'"
+ s.add_dependency "three", "= 1.0.0"
+ end
+
+ build_lib "three", "1.0.0" do |s|
+ s.write "lib/three.rb", "puts 'three'"
+ s.add_dependency "seven", "= 1.0.0"
+ end
+
+ build_lib "four", "1.0.0" do |s|
+ s.write "lib/four.rb", "puts 'four'"
+ end
+
+ build_lib "five", "1.0.0", no_default: true do |s|
+ s.write "lib/mofive.rb", "puts 'five'"
+ end
+
+ build_lib "six", "1.0.0" do |s|
+ s.write "lib/six.rb", "puts 'six'"
+ end
+
+ build_lib "seven", "1.0.0" do |s|
+ s.write "lib/seven.rb", "puts 'seven'"
+ end
+
+ build_lib "eight", "1.0.0" do |s|
+ s.write "lib/eight.rb", "puts 'eight'"
+ end
+ end
+
+ it "requires the gems" do
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "two"
+ end
+ end
+ RUBY
+
+ expect(out).to eq("two")
+
+ script <<-RUBY, raise_on_error: false
+ gemfile do
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "eleven"
+ end
+ end
+
+ puts "success"
+ RUBY
+
+ expect(err).to include "Could not find gem 'eleven'"
+ expect(out).not_to include "success"
+
+ script <<-RUBY
+ gemfile(true) do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+ RUBY
+
+ expect(out).to include("Myrack's post install message")
+
+ script <<-RUBY, artifice: "endpoint"
+ gemfile(true) do
+ source "https://notaserver.test"
+ gem "activesupport", :require => true
+ end
+ RUBY
+
+ expect(out).to include("Installing activesupport")
+ err_lines = err.split("\n")
+ err_lines.reject! {|line| line =~ /\.rb:\d+: warning: / }
+ expect(err_lines).to be_empty
+ end
+
+ it "lets me use my own ui object" do
+ script <<-RUBY, artifice: "endpoint"
+ require 'bundler'
+ class MyBundlerUI < Bundler::UI::Shell
+ def confirm(msg, newline = nil)
+ puts "CONFIRMED!"
+ end
+ end
+ my_ui = MyBundlerUI.new
+ my_ui.level = "confirm"
+ gemfile(true, :ui => my_ui) do
+ source "https://notaserver.test"
+ gem "activesupport", :require => true
+ end
+ RUBY
+
+ expect(out).to eq("CONFIRMED!\nCONFIRMED!")
+ end
+
+ it "has an option for quiet installation" do
+ script <<-RUBY, artifice: "endpoint"
+ require 'bundler/inline'
+
+ gemfile(true, :quiet => true) do
+ source "https://notaserver.test"
+ gem "activesupport", :require => true
+ end
+ RUBY
+
+ expect(out).to be_empty
+ end
+
+ it "raises an exception if passed unknown arguments" do
+ script <<-RUBY, raise_on_error: false
+ gemfile(true, :arglebargle => true) do
+ path "#{lib_path}"
+ gem "two"
+ end
+
+ puts "success"
+ RUBY
+ expect(err).to include "Unknown options: arglebargle"
+ expect(out).not_to include "success"
+ end
+
+ it "does not mutate the option argument" do
+ script <<-RUBY
+ require 'bundler'
+ options = { :ui => Bundler::UI::Shell.new }
+ gemfile(false, options) do
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "two"
+ end
+ end
+ puts "OKAY" if options.key?(:ui)
+ RUBY
+
+ expect(out).to match("OKAY")
+ end
+
+ it "installs quietly if necessary when the install option is not set" do
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ puts MYRACK
+ RUBY
+
+ expect(out).to eq("1.0.0")
+ expect(err).to be_empty
+ end
+
+ it "installs subdependencies quietly if necessary when the install option is not set" do
+ build_repo4 do
+ build_gem "myrack" do |s|
+ s.add_dependency "myrackdep"
+ end
+
+ build_gem "myrackdep", "1.0.0"
+ end
+
+ script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ gemfile do
+ source "https://gem.repo4"
+ gem "myrack"
+ end
+
+ require "myrackdep"
+ puts MYRACKDEP
+ RUBY
+
+ expect(out).to eq("1.0.0")
+ expect(err).to be_empty
+ end
+
+ it "installs subdependencies quietly if necessary when the install option is not set, and multiple sources used" do
+ build_repo4 do
+ build_gem "myrack" do |s|
+ s.add_dependency "myrackdep"
+ end
+
+ build_gem "myrackdep", "1.0.0"
+ end
+
+ script <<-RUBY, artifice: "compact_index_extra_api"
+ gemfile do
+ source "https://test.repo"
+ source "https://test.repo/extra" do
+ gem "myrack"
+ end
+ end
+
+ require "myrackdep"
+ puts MYRACKDEP
+ RUBY
+
+ expect(out).to eq("1.0.0")
+ expect(err).to be_empty
+ end
+
+ it "installs quietly from git if necessary when the install option is not set" do
+ build_git "foo", "1.0.0"
+ baz_ref = build_git("baz", "2.0.0").ref_for("HEAD")
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ gem "foo", :git => #{lib_path("foo-1.0.0").to_s.dump}
+ gem "baz", :git => #{lib_path("baz-2.0.0").to_s.dump}, :ref => #{baz_ref.dump}
+ end
+
+ puts FOO
+ puts BAZ
+ RUBY
+
+ expect(out).to eq("1.0.0\n2.0.0")
+ expect(err).to be_empty
+ end
+
+ it "allows calling gemfile twice" do
+ script <<-RUBY
+ gemfile do
+ path "#{lib_path}" do
+ source "https://gem.repo1"
+ gem "two"
+ end
+ end
+
+ gemfile do
+ path "#{lib_path}" do
+ source "https://gem.repo1"
+ gem "four"
+ end
+ end
+ RUBY
+
+ expect(out).to eq("two\nfour")
+ expect(err).to be_empty
+ end
+
+ it "doesn't reinstall already installed gems" do
+ system_gems "myrack-1.0.0"
+
+ script <<-RUBY
+ require 'bundler'
+ ui = Bundler::UI::Shell.new
+ ui.level = "confirm"
+
+ gemfile(true, ui: ui) do
+ source "https://gem.repo1"
+ gem "activesupport"
+ gem "myrack"
+ end
+ RUBY
+
+ expect(out).to include("Installing activesupport")
+ expect(out).not_to include("Installing myrack")
+ expect(err).to be_empty
+ end
+
+ it "installs gems in later gemfile calls" do
+ system_gems "myrack-1.0.0"
+
+ script <<-RUBY
+ require 'bundler'
+ ui = Bundler::UI::Shell.new
+ ui.level = "confirm"
+ gemfile(true, ui: ui) do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ gemfile(true, ui: ui) do
+ source "https://gem.repo1"
+ gem "activesupport"
+ end
+ RUBY
+
+ expect(out).to include("Installing activesupport")
+ expect(out).not_to include("Installing myrack")
+ expect(err).to be_empty
+ end
+
+ it "doesn't reinstall already installed gems in later gemfile calls" do
+ system_gems "myrack-1.0.0"
+
+ script <<-RUBY
+ require 'bundler'
+ ui = Bundler::UI::Shell.new
+ ui.level = "confirm"
+ gemfile(true, ui: ui) do
+ source "https://gem.repo1"
+ gem "activesupport"
+ end
+
+ gemfile(true, ui: ui) do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+ RUBY
+
+ expect(out).to include("Installing activesupport")
+ expect(out).not_to include("Installing myrack")
+ expect(err).to be_empty
+ end
+
+ it "installs gems with native extensions in later gemfile calls" do
+ system_gems "myrack-1.0.0"
+
+ build_git "foo" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ File.open("\#{path}/foo.rb", "w") do |f|
+ f.puts "FOO = 'YES'"
+ end
+ end
+ RUBY
+ end
+
+ script <<-RUBY
+ require 'bundler'
+ ui = Bundler::UI::Shell.new
+ ui.level = "confirm"
+ gemfile(true, ui: ui) do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ gemfile(true, ui: ui) do
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ end
+
+ require 'foo'
+ puts FOO
+ puts $:.grep(/ext/)
+ RUBY
+
+ expect(out).to include("YES")
+ expect(out).to include(Pathname.glob(default_bundle_path("bundler/gems/extensions/**/foo-1.0-*")).first.to_s)
+ expect(err).to be_empty
+ end
+
+ it "installs inline gems when a Gemfile.lock is present" do
+ gemfile <<-G
+ source "https://notaserver.test"
+ gem "rake"
+ G
+
+ lockfile <<-G
+ GEM
+ remote: https://rubygems.org/
+ specs:
+ rake (11.3.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ rake
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ puts MYRACK
+ RUBY
+
+ expect(err).to be_empty
+ end
+
+ it "does not leak Gemfile.lock versions to the installation output" do
+ gemfile <<-G
+ source "https://notaserver.test"
+ gem "rake"
+ G
+
+ lockfile <<-G
+ GEM
+ remote: https://rubygems.org/
+ specs:
+ rake (11.3.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ rake
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ script <<-RUBY
+ gemfile(true) do
+ source "https://gem.repo1"
+ gem "rake", "#{rake_version}"
+ end
+ RUBY
+
+ expect(out).to include("Installing rake #{rake_version}")
+ expect(out).not_to include("was 11.3.0")
+ expect(err).to be_empty
+ end
+
+ it "installs inline gems when frozen is set" do
+ script <<-RUBY, env: { "BUNDLE_FROZEN" => "true", "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ puts MYRACK
+ RUBY
+
+ expect(last_command.stderr).to be_empty
+ end
+
+ it "installs inline gems when deployment is set" do
+ script <<-RUBY, env: { "BUNDLE_DEPLOYMENT" => "true", "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ puts MYRACK
+ RUBY
+
+ expect(last_command.stderr).to be_empty
+ end
+
+ it "installs inline gems when BUNDLE_GEMFILE is set to an empty string" do
+ ENV["BUNDLE_GEMFILE"] = ""
+
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ puts MYRACK
+ RUBY
+
+ expect(err).to be_empty
+ end
+
+ it "installs inline gems when BUNDLE_BIN is set" do
+ ENV["BUNDLE_BIN"] = "/usr/local/bundle/bin"
+
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack" # has the myrackup executable
+ end
+
+ puts MYRACK
+ RUBY
+ expect(last_command).to be_success
+ expect(out).to eq "1.0.0"
+ end
+
+ context "when BUNDLE_PATH is set" do
+ it "installs inline gems to the system path regardless" do
+ script <<-RUBY, env: { "BUNDLE_PATH" => "./vendor/inline", "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ gemfile(true) do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+ RUBY
+ expect(last_command).to be_success
+ expect(system_gem_path("gems/myrack-1.0.0")).to exist
+ end
+ end
+
+ it "skips platform warnings" do
+ bundle_config "force_ruby_platform true"
+
+ script <<-RUBY
+ gemfile(true) do
+ source "https://gem.repo1"
+ gem "myrack", platform: :jruby
+ end
+ RUBY
+
+ expect(err).to be_empty
+ end
+
+ it "still installs if the application has `bundle package` no_install config set" do
+ bundle_config "no_install true"
+
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+ RUBY
+
+ expect(last_command).to be_success
+ expect(system_gem_path("gems/myrack-1.0.0")).to exist
+ end
+
+ it "preserves previous BUNDLE_GEMFILE value" do
+ ENV["BUNDLE_GEMFILE"] = ""
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ puts "BUNDLE_GEMFILE is empty" if ENV["BUNDLE_GEMFILE"].empty?
+ system("#{Gem.ruby} -w -e '42'") # this should see original value of BUNDLE_GEMFILE
+ exit $?.exitstatus
+ RUBY
+
+ expect(last_command).to be_success
+ expect(out).to include("BUNDLE_GEMFILE is empty")
+ end
+
+ it "resets BUNDLE_GEMFILE to the empty string if it wasn't set previously" do
+ ENV["BUNDLE_GEMFILE"] = nil
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ puts "BUNDLE_GEMFILE is empty" if ENV["BUNDLE_GEMFILE"].empty?
+ system("#{Gem.ruby} -w -e '42'") # this should see original value of BUNDLE_GEMFILE
+ exit $?.exitstatus
+ RUBY
+
+ expect(last_command).to be_success
+ expect(out).to include("BUNDLE_GEMFILE is empty")
+ end
+
+ it "does not error out if library requires optional dependencies" do
+ Dir.mkdir tmp("path_without_gemfile")
+
+ foo_code = <<~RUBY
+ begin
+ gem "bar"
+ rescue LoadError
+ end
+
+ puts "WIN"
+ RUBY
+
+ build_lib "foo", "1.0.0" do |s|
+ s.write "lib/foo.rb", foo_code
+ end
+
+ script <<-RUBY, dir: tmp("path_without_gemfile"), env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
+ gemfile do
+ source "https://gem.repo2"
+ path "#{lib_path}" do
+ gem "foo", require: false
+ end
+ end
+
+ require "foo"
+ RUBY
+
+ expect(out).to eq("WIN")
+ expect(err).to be_empty
+ end
+
+ it "does not load default timeout", rubygems: ">= 3.5.0" do
+ default_timeout_version = ruby "gem 'timeout', '< 999999'; require 'timeout'; puts Timeout::VERSION", raise_on_error: false
+ skip "timeout isn't a default gem" if default_timeout_version.empty?
+
+ build_repo4 do
+ build_gem "timeout", "999"
+ end
+
+ script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ require "bundler/inline"
+
+ gemfile(true) do
+ source "https://gem.repo4"
+
+ gem "timeout"
+ end
+ RUBY
+
+ expect(out).to include("Installing timeout 999")
+ end
+
+ it "does not upcase ENV" do
+ script <<-RUBY
+ require 'bundler/inline'
+
+ ENV['Test_Variable'] = 'value string'
+ puts("before: \#{ENV.each_key.select { |key| key.match?(/test_variable/i) }}")
+
+ gemfile do
+ source "https://gem.repo1"
+ end
+
+ puts("after: \#{ENV.each_key.select { |key| key.match?(/test_variable/i) }}")
+ RUBY
+
+ expect(out).to include("before: [\"Test_Variable\"]")
+ expect(out).to include("after: [\"Test_Variable\"]")
+ end
+
+ it "does not create a lockfile" do
+ script <<-RUBY
+ require 'bundler/inline'
+
+ gemfile do
+ source "https://gem.repo1"
+ end
+
+ puts Dir.glob("Gemfile.lock")
+ RUBY
+
+ expect(out).to be_empty
+ end
+
+ it "does not reset ENV" do
+ script <<-RUBY
+ require 'bundler/inline'
+
+ gemfile do
+ source "https://gem.repo1"
+
+ ENV['FOO'] = 'bar'
+ end
+
+ puts ENV['FOO']
+ RUBY
+
+ expect(out).to eq("bar")
+ end
+
+ it "does not load specified version of psych and stringio", :ruby_repo do
+ build_repo4 do
+ build_gem "psych", "999"
+ build_gem "stringio", "999"
+ end
+
+ script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ require "bundler/inline"
+
+ gemfile(true) do
+ source "https://gem.repo4"
+
+ gem "psych"
+ gem "stringio"
+ end
+ RUBY
+
+ expect(out).to include("Installing psych 999")
+ expect(out).to include("Installing stringio 999")
+ if Gem.respond_to?(:use_psych?) && Gem.use_psych?
+ expect(out).to include("The psych gem was resolved to 999")
+ expect(out).to include("The stringio gem was resolved to 999")
+ end
+ end
+
+ it "installs a conflicting default gem and non-default gems together" do
+ build_repo4 do
+ build_gem "securerandom", "999"
+ build_gem "myrack", "1.0.0"
+ end
+
+ script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ gemfile(true) do
+ source "https://gem.repo4"
+ gem "securerandom"
+ gem "myrack"
+ end
+
+ puts MYRACK
+ RUBY
+
+ expect(out).to include("Installing securerandom 999")
+ expect(out).to include("Installing myrack 1.0.0")
+ expect(out).to include("1.0.0")
+ expect(err).to be_empty
+ end
+
+ it "installs a conflicting default gem alongside git sources" do
+ build_repo4 do
+ build_gem "securerandom", "999"
+ end
+
+ build_git "foo", "1.0.0"
+
+ script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ gemfile(true) do
+ source "https://gem.repo4"
+ gem "securerandom"
+ gem "foo", :git => #{lib_path("foo-1.0.0").to_s.dump}
+ end
+
+ puts FOO
+ RUBY
+
+ expect(out).to include("Installing securerandom 999")
+ expect(out).to include("1.0.0")
+ expect(err).to be_empty
+ end
+
+ it "leaves a lockfile in the same directory as the inline script alone" do
+ install_gemfile <<~G
+ source "https://gem.repo1"
+ gem "foo"
+ G
+
+ original_lockfile = lockfile
+
+ script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ require "bundler/inline"
+
+ gemfile(true) do
+ source "https://gem.repo1"
+
+ gem "myrack"
+ end
+ RUBY
+
+ expect(lockfile).to eq(original_lockfile)
+ end
+end
diff --git a/spec/bundler/runtime/load_spec.rb b/spec/bundler/runtime/load_spec.rb
new file mode 100644
index 0000000000..472cde87c5
--- /dev/null
+++ b/spec/bundler/runtime/load_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+RSpec.describe "Bundler.load" do
+ describe "with a gemfile" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ allow(Bundler::SharedHelpers).to receive(:pwd).and_return(bundled_app)
+ end
+
+ it "provides a list of the env dependencies" do
+ expect(Bundler.load.dependencies).to have_dep("myrack", ">= 0")
+ end
+
+ it "provides a list of the resolved gems" do
+ expect(Bundler.load.gems).to have_gem("myrack-1.0.0", "bundler-#{Bundler::VERSION}")
+ end
+
+ it "ignores blank BUNDLE_GEMFILEs" do
+ expect do
+ ENV["BUNDLE_GEMFILE"] = ""
+ Bundler.load
+ end.not_to raise_error
+ end
+ end
+
+ describe "with a gems.rb file" do
+ before(:each) do
+ gemfile "gems.rb", <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ bundle :install
+ allow(Bundler::SharedHelpers).to receive(:pwd).and_return(bundled_app)
+ end
+
+ it "provides a list of the env dependencies" do
+ expect(Bundler.load.dependencies).to have_dep("myrack", ">= 0")
+ end
+
+ it "provides a list of the resolved gems" do
+ expect(Bundler.load.gems).to have_gem("myrack-1.0.0", "bundler-#{Bundler::VERSION}")
+ end
+ end
+
+ describe "without a gemfile" do
+ it "raises an exception if the default gemfile is not found" do
+ expect do
+ Bundler.load
+ end.to raise_error(Bundler::GemfileNotFound, /could not locate gemfile/i)
+ end
+
+ it "raises an exception if a specified gemfile is not found" do
+ expect do
+ ENV["BUNDLE_GEMFILE"] = "omg.rb"
+ Bundler.load
+ end.to raise_error(Bundler::GemfileNotFound, /omg\.rb/)
+ end
+
+ it "does not find a Gemfile above the testing directory" do
+ bundler_gemfile = Pathname.new(__dir__).join("../../Gemfile")
+ unless File.exist?(bundler_gemfile)
+ FileUtils.touch(bundler_gemfile)
+ @remove_bundler_gemfile = true
+ end
+ begin
+ expect { Bundler.load }.to raise_error(Bundler::GemfileNotFound)
+ ensure
+ FileUtils.rm_rf bundler_gemfile if @remove_bundler_gemfile
+ end
+ end
+ end
+
+ describe "when called twice" do
+ it "doesn't try to load the runtime twice" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "activesupport", :group => :test
+ G
+
+ ruby <<-RUBY
+ require "bundler"
+ Bundler.setup :default
+ Bundler.require :default
+ puts MYRACK
+ begin
+ require "activesupport"
+ rescue LoadError
+ puts "no activesupport"
+ end
+ RUBY
+
+ expect(out.split("\n")).to eq(["1.0.0", "no activesupport"])
+ end
+ end
+
+ describe "not hurting brittle rubygems" do
+ it "does not inject #source into the generated YAML of the gem specs" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activerecord"
+ G
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ Bundler.load.specs.each do |spec|
+ expect(spec.to_yaml).not_to match(/^\s+source:/)
+ expect(spec.to_yaml).not_to match(/^\s+groups:/)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/runtime/platform_spec.rb b/spec/bundler/runtime/platform_spec.rb
new file mode 100644
index 0000000000..6d96758956
--- /dev/null
+++ b/spec/bundler/runtime/platform_spec.rb
@@ -0,0 +1,468 @@
+# frozen_string_literal: true
+
+RSpec.describe "Bundler.setup with multi platform stuff" do
+ it "raises a friendly error when gems are missing locally" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ #{local_tag}
+
+ DEPENDENCIES
+ myrack
+ G
+
+ ruby <<-R
+ begin
+ require 'bundler'
+ Bundler.ui.silence { Bundler.setup }
+ rescue Bundler::GemNotFound => e
+ puts "WIN"
+ end
+ R
+
+ expect(out).to eq("WIN")
+ end
+
+ it "will resolve correctly on the current platform when the lockfile was targeted for a different one" do
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ nokogiri (1.4.2-java)
+ weakling (= 0.0.3)
+ weakling (0.0.3)
+
+ PLATFORMS
+ java
+
+ DEPENDENCIES
+ nokogiri
+ G
+
+ simulate_platform "x86-darwin-10" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri"
+ G
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2"
+ end
+ end
+
+ it "will keep both platforms when both ruby and a specific ruby platform are locked and the bundle is unlocked" do
+ build_repo4 do
+ build_gem "nokogiri", "1.11.1" do |s|
+ s.add_dependency "mini_portile2", "~> 2.5.0"
+ s.add_dependency "racca", "~> 1.5.2"
+ end
+
+ build_gem "nokogiri", "1.11.1" do |s|
+ s.platform = Bundler.local_platform
+ s.add_dependency "racca", "~> 1.4"
+ end
+
+ build_gem "mini_portile2", "2.5.0"
+ build_gem "racca", "1.5.2"
+ end
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo4, "mini_portile2", "2.5.0"
+ c.checksum gem_repo4, "nokogiri", "1.11.1"
+ c.checksum gem_repo4, "nokogiri", "1.11.1", Bundler.local_platform
+ c.checksum gem_repo4, "racca", "1.5.2"
+ end
+
+ good_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ mini_portile2 (2.5.0)
+ nokogiri (1.11.1)
+ mini_portile2 (~> 2.5.0)
+ racca (~> 1.5.2)
+ nokogiri (1.11.1-#{Bundler.local_platform})
+ racca (~> 1.4)
+ racca (1.5.2)
+
+ PLATFORMS
+ #{lockfile_platforms("ruby")}
+
+ DEPENDENCIES
+ nokogiri (~> 1.11)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "nokogiri", "~> 1.11"
+ G
+
+ lockfile good_lockfile
+
+ bundle "update nokogiri"
+
+ expect(lockfile).to eq(good_lockfile)
+ end
+
+ it "will not try to install platform specific gems when they don't match the current ruby if locked only to ruby" do
+ build_repo4 do
+ build_gem "nokogiri", "1.11.1"
+
+ build_gem "nokogiri", "1.11.1" do |s|
+ s.platform = Bundler.local_platform
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "nokogiri"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.11.1)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(out).to include("Fetching nokogiri 1.11.1")
+ expect(the_bundle).to include_gems "nokogiri 1.11.1"
+ expect(the_bundle).not_to include_gems "nokogiri 1.11.1 #{Bundler.local_platform}"
+ end
+
+ it "will use the java platform if both generic java and generic ruby platforms are locked", :jruby_only do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri"
+ G
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ nokogiri (1.4.2)
+ nokogiri (1.4.2-java)
+ weakling (>= 0.0.3)
+ weakling (0.0.3)
+
+ PLATFORMS
+ java
+ ruby
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ bundle "install"
+
+ expect(out).to include("Fetching nokogiri 1.4.2 (java)")
+ expect(the_bundle).to include_gems "nokogiri 1.4.2 java"
+ end
+
+ it "will add the resolve for the current platform" do
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ nokogiri (1.4.2-java)
+ weakling (= 0.0.3)
+ weakling (0.0.3)
+
+ PLATFORMS
+ java
+
+ DEPENDENCIES
+ nokogiri
+ G
+
+ simulate_platform "x86-darwin-100" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri"
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2", "platform_specific 1.0 x86-darwin-100"
+ end
+ end
+
+ it "allows specifying only-ruby-platform on jruby", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri"
+ gem "platform_specific"
+ G
+
+ bundle_config "force_ruby_platform true"
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2", "platform_specific 1.0 ruby"
+ end
+
+ it "allows specifying only-ruby-platform" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri"
+ gem "platform_specific"
+ G
+
+ bundle_config "force_ruby_platform true"
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2", "platform_specific 1.0 ruby"
+ end
+
+ it "allows specifying only-ruby-platform even if the lockfile is locked to a specific compatible platform" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri"
+ gem "platform_specific"
+ G
+
+ bundle_config "force_ruby_platform true"
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2", "platform_specific 1.0 ruby"
+ end
+
+ it "doesn't pull platform specific gems on truffleruby", :truffleruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 ruby"
+ end
+
+ it "doesn't pull platform specific gems on truffleruby (except when whitelisted) even if lockfile was generated with an older version that declared ruby as platform", :truffleruby_only do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ platform_specific (1.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ platform_specific
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 ruby"
+
+ simulate_platform "x86_64-linux" do
+ build_repo4 do
+ build_gem "libv8"
+
+ build_gem "libv8" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "libv8"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ libv8 (1.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ libv8
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "libv8 1.0 x86_64-linux"
+ end
+ end
+
+ it "doesn't pull platform specific gems on truffleruby, even if lockfile only includes those", :truffleruby_only do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ platform_specific (1.0-x86-darwin-100)
+
+ PLATFORMS
+ x86-darwin-100
+
+ DEPENDENCIES
+ platform_specific
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 ruby"
+ end
+
+ it "pulls platform specific gems correctly on musl" do
+ build_repo4 do
+ build_gem "nokogiri", "1.13.8" do |s|
+ s.platform = "aarch64-linux"
+ end
+ end
+
+ simulate_platform "aarch64-linux-musl" do
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo4"
+ gem "nokogiri"
+ G
+ end
+
+ expect(out).to include("Fetching nokogiri 1.13.8 (aarch64-linux)")
+ end
+
+ it "allows specifying only-ruby-platform on windows with dependency platforms" do
+ simulate_platform "x86-mswin32" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri", :platforms => [:windows, :jruby]
+ gem "platform_specific"
+ G
+
+ bundle_config "force_ruby_platform true"
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 ruby"
+ expect(the_bundle).to not_include_gems "nokogiri"
+ end
+ end
+
+ it "allows specifying only-ruby-platform on windows with gemspec dependency" do
+ build_lib("foo", "1.0", path: bundled_app) do |s|
+ s.add_dependency "myrack"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gemspec
+ G
+ bundle :lock
+
+ simulate_platform "x86-mswin32" do
+ bundle_config "force_ruby_platform true"
+ bundle "install"
+
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+
+ it "recovers when the lockfile is missing a platform-specific gem" do
+ build_repo2 do
+ build_gem "requires_platform_specific" do |s|
+ s.add_dependency "platform_specific"
+ end
+ end
+ simulate_platform "x64-mingw-ucrt" do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ platform_specific (1.0-x86-mingw32)
+ requires_platform_specific (1.0)
+ platform_specific
+
+ PLATFORMS
+ x64-mingw-ucrt
+ x86-mingw32
+
+ DEPENDENCIES
+ requires_platform_specific
+ L
+
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo2"
+ gem "requires_platform_specific"
+ G
+
+ expect(out).to include("lockfile does not have all gems needed for the current platform")
+ expect(the_bundle).to include_gem "platform_specific 1.0 x64-mingw-ucrt"
+ end
+ end
+
+ %w[x86-mswin32 x64-mswin64 x86-mingw32 x64-mingw-ucrt aarch64-mingw-ucrt].each do |platform|
+ it "allows specifying platform windows on #{platform} platform" do
+ simulate_platform platform do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ platform_specific (1.0-#{platform})
+ requires_platform_specific (1.0)
+ platform_specific
+
+ PLATFORMS
+ #{platform}
+
+ DEPENDENCIES
+ requires_platform_specific
+ L
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific", :platforms => [:windows]
+ G
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 #{platform}"
+ end
+ end
+ end
+end
diff --git a/spec/bundler/runtime/require_spec.rb b/spec/bundler/runtime/require_spec.rb
new file mode 100644
index 0000000000..46613286d2
--- /dev/null
+++ b/spec/bundler/runtime/require_spec.rb
@@ -0,0 +1,480 @@
+# frozen_string_literal: true
+
+RSpec.describe "Bundler.require" do
+ before :each do
+ build_lib "one", "1.0.0" do |s|
+ s.write "lib/baz.rb", "puts 'baz'"
+ s.write "lib/qux.rb", "puts 'qux'"
+ end
+
+ build_lib "two", "1.0.0" do |s|
+ s.write "lib/two.rb", "puts 'two'"
+ s.add_dependency "three", "= 1.0.0"
+ end
+
+ build_lib "three", "1.0.0" do |s|
+ s.write "lib/three.rb", "puts 'three'"
+ s.add_dependency "seven", "= 1.0.0"
+ end
+
+ build_lib "four", "1.0.0" do |s|
+ s.write "lib/four.rb", "puts 'four'"
+ end
+
+ build_lib "five", "1.0.0", no_default: true do |s|
+ s.write "lib/mofive.rb", "puts 'five'"
+ end
+
+ build_lib "six", "1.0.0" do |s|
+ s.write "lib/six.rb", "puts 'six'"
+ end
+
+ build_lib "seven", "1.0.0" do |s|
+ s.write "lib/seven.rb", "puts 'seven'"
+ end
+
+ build_lib "eight", "1.0.0" do |s|
+ s.write "lib/eight.rb", "puts 'eight'"
+ end
+
+ build_lib "nine", "1.0.0" do |s|
+ s.write "lib/nine.rb", "puts 'nine'"
+ end
+
+ build_lib "ten", "1.0.0" do |s|
+ s.write "lib/ten.rb", "puts 'ten'"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "one", :group => :bar, :require => %w[baz qux]
+ gem "two"
+ gem "three", :group => :not
+ gem "four", :require => false
+ gem "five"
+ gem "six", :group => "string"
+ gem "seven", :group => :not
+ gem "eight", :require => true, :group => :require_true
+ env "BUNDLER_TEST" => "nine" do
+ gem "nine", :require => true
+ end
+ gem "ten", :install_if => lambda { ENV["BUNDLER_TEST"] == "ten" }
+ end
+ G
+ end
+
+ it "requires the gems" do
+ # default group
+ run "Bundler.require"
+ expect(out).to eq("two")
+
+ # specific group
+ run "Bundler.require(:bar)"
+ expect(out).to eq("baz\nqux")
+
+ # default and specific group
+ run "Bundler.require(:default, :bar)"
+ expect(out).to eq("baz\nqux\ntwo")
+
+ # specific group given as a string
+ run "Bundler.require('bar')"
+ expect(out).to eq("baz\nqux")
+
+ # specific group declared as a string
+ run "Bundler.require(:string)"
+ expect(out).to eq("six")
+
+ # required in resolver order instead of gemfile order
+ run("Bundler.require(:not)")
+ expect(out.split("\n").sort).to eq(%w[seven three])
+
+ # test require: true
+ run "Bundler.require(:require_true)"
+ expect(out).to eq("eight")
+ end
+
+ it "allows requiring gems with non standard names explicitly" do
+ run "Bundler.require ; require 'mofive'"
+ expect(out).to eq("two\nfive")
+ end
+
+ it "allows requiring gems which are scoped by env" do
+ ENV["BUNDLER_TEST"] = "nine"
+ run "Bundler.require"
+ expect(out).to eq("two\nnine")
+ end
+
+ it "allows requiring gems which are scoped by install_if" do
+ ENV["BUNDLER_TEST"] = "ten"
+ run "Bundler.require"
+ expect(out).to eq("two\nten")
+ end
+
+ it "raises an exception if a require is specified but the file does not exist" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "two", :require => 'fail'
+ end
+ G
+
+ run "Bundler.require", raise_on_error: false
+
+ expect(err_without_deprecations).to include("cannot load such file -- fail")
+ end
+
+ it "displays a helpful message if the required gem throws an error" do
+ build_lib "faulty", "1.0.0" do |s|
+ s.write "lib/faulty.rb", "raise RuntimeError.new(\"Gem Internal Error Message\")"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "faulty"
+ end
+ G
+
+ run "Bundler.require", raise_on_error: false
+ expect(err).to match("error while trying to load the gem 'faulty'")
+ expect(err).to match("Gem Internal Error Message")
+ end
+
+ it "doesn't swallow the error when the library has an unrelated error" do
+ build_lib "loadfuuu", "1.0.0" do |s|
+ s.write "lib/loadfuuu.rb", "raise LoadError.new(\"cannot load such file -- load-bar\")"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "loadfuuu"
+ end
+ G
+
+ run "Bundler.require", raise_on_error: false
+
+ expect(err_without_deprecations).to include("cannot load such file -- load-bar")
+ end
+
+ describe "with namespaced gems" do
+ before :each do
+ build_lib "jquery-rails", "1.0.0" do |s|
+ s.write "lib/jquery/rails.rb", "puts 'jquery/rails'"
+ end
+ end
+
+ it "requires gem names that are namespaced" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ path '#{lib_path}' do
+ gem 'jquery-rails'
+ end
+ G
+
+ run "Bundler.require"
+ expect(out).to eq("jquery/rails")
+ end
+
+ it "silently passes if the require fails" do
+ build_lib "bcrypt-ruby", "1.0.0", no_default: true do |s|
+ s.write "lib/brcrypt.rb", "BCrypt = '1.0.0'"
+ end
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ path "#{lib_path}" do
+ gem "bcrypt-ruby"
+ end
+ G
+
+ cmd = <<-RUBY
+ require 'bundler'
+ Bundler.require
+ RUBY
+ ruby(cmd)
+
+ expect(err).to be_empty
+ end
+
+ it "does not mangle explicitly given requires" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem 'jquery-rails', :require => 'jquery-rails'
+ end
+ G
+
+ run "Bundler.require", raise_on_error: false
+
+ expect(err_without_deprecations).to include("cannot load such file -- jquery-rails")
+ end
+
+ it "handles the case where regex fails" do
+ build_lib "load-fuuu", "1.0.0" do |s|
+ s.write "lib/load-fuuu.rb", "raise LoadError.new(\"Could not open library 'libfuuu-1.0': libfuuu-1.0: cannot open shared object file: No such file or directory.\")"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "load-fuuu"
+ end
+ G
+
+ run "Bundler.require", raise_on_error: false
+
+ expect(err_without_deprecations).to include("libfuuu-1.0").and include("cannot open shared object file")
+ end
+
+ it "doesn't swallow the error when the library has an unrelated error" do
+ build_lib "load-fuuu", "1.0.0" do |s|
+ s.write "lib/load/fuuu.rb", "raise LoadError.new(\"cannot load such file -- load-bar\")"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "load-fuuu"
+ end
+ G
+
+ run "Bundler.require", raise_on_error: false
+
+ expect(err_without_deprecations).to include("cannot load such file -- load-bar")
+ end
+ end
+
+ describe "using bundle exec" do
+ it "requires the locked gems" do
+ bundle "exec ruby -e 'Bundler.require'"
+ expect(out).to eq("two")
+
+ bundle "exec ruby -e 'Bundler.require(:bar)'"
+ expect(out).to eq("baz\nqux")
+
+ bundle "exec ruby -e 'Bundler.require(:default, :bar)'"
+ expect(out).to eq("baz\nqux\ntwo")
+ end
+ end
+
+ describe "order" do
+ before(:each) do
+ build_lib "one", "1.0.0" do |s|
+ s.write "lib/one.rb", <<-ONE
+ if defined?(Two)
+ Two.two
+ else
+ puts "two_not_loaded"
+ end
+ puts 'one'
+ ONE
+ end
+
+ build_lib "two", "1.0.0" do |s|
+ s.write "lib/two.rb", <<-TWO
+ module Two
+ def self.two
+ puts 'module_two'
+ end
+ end
+ puts 'two'
+ TWO
+ end
+ end
+
+ it "works when the gems are in the Gemfile in the correct order" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "two"
+ gem "one"
+ end
+ G
+
+ run "Bundler.require"
+ expect(out).to eq("two\nmodule_two\none")
+ end
+
+ describe "a gem with different requires for different envs" do
+ before(:each) do
+ build_gem "multi_gem", to_bundle: true do |s|
+ s.write "lib/one.rb", "puts 'ONE'"
+ s.write "lib/two.rb", "puts 'TWO'"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "multi_gem", :require => "one", :group => :one
+ gem "multi_gem", :require => "two", :group => :two
+ G
+ end
+
+ it "requires both with Bundler.require(both)" do
+ run "Bundler.require(:one, :two)"
+ expect(out).to eq("ONE\nTWO")
+ end
+
+ it "requires one with Bundler.require(:one)" do
+ run "Bundler.require(:one)"
+ expect(out).to eq("ONE")
+ end
+
+ it "requires :two with Bundler.require(:two)" do
+ run "Bundler.require(:two)"
+ expect(out).to eq("TWO")
+ end
+ end
+
+ it "fails when the gems are in the Gemfile in the wrong order" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "one"
+ gem "two"
+ end
+ G
+
+ run "Bundler.require"
+ expect(out).to eq("two_not_loaded\none\ntwo")
+ end
+
+ describe "with busted gems" do
+ it "should be busted" do
+ build_gem "busted_require", to_bundle: true do |s|
+ s.write "lib/busted_require.rb", "require 'no_such_file_omg'"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "busted_require"
+ G
+
+ run "Bundler.require", raise_on_error: false
+
+ expect(err_without_deprecations).to include("cannot load such file -- no_such_file_omg")
+ end
+ end
+ end
+
+ it "does not load rubygems gemspecs that are used" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ run <<-R
+ path = File.join(Gem.dir, "specifications", "myrack-1.0.0.gemspec")
+ contents = File.read(path)
+ contents = contents.lines.to_a.insert(-2, "\n raise 'broken gemspec'\n").join
+ File.open(path, "w") do |f|
+ f.write contents
+ end
+ R
+
+ run <<-R
+ Bundler.require
+ puts "WIN"
+ R
+
+ expect(out).to eq("WIN")
+ end
+
+ it "does not load git gemspecs that are used" do
+ build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ run <<-R
+ path = Gem.loaded_specs["foo"].loaded_from
+ contents = File.read(path)
+ contents = contents.lines.to_a.insert(-2, "\n raise 'broken gemspec'\n").join
+ File.open(path, "w") do |f|
+ f.write contents
+ end
+ R
+
+ run <<-R
+ Bundler.require
+ puts "WIN"
+ R
+
+ expect(out).to eq("WIN")
+ end
+
+ it "does not load plugins" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ create_file "plugins/rubygems_plugin.rb", "puts 'FAIL'"
+
+ run <<~R, env: { "RUBYLIB" => rubylib.unshift(bundled_app("plugins").to_s).join(File::PATH_SEPARATOR) }
+ Bundler.require
+ puts "WIN"
+ R
+
+ expect(out).to eq("WIN")
+ end
+
+ it "does not extract gemspecs from application cache packages" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle :cache
+
+ path = cached_gem("myrack-1.0.0")
+
+ run <<-R
+ File.open("#{path}", "w") do |f|
+ f.write "broken package"
+ end
+ R
+
+ run <<-R
+ Bundler.require
+ puts "WIN"
+ R
+
+ expect(out).to eq("WIN")
+ end
+end
+
+RSpec.describe "Bundler.require with platform specific dependencies" do
+ it "does not require the gems that are pinned to other platforms" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ platforms :#{not_local_tag} do
+ gem "platform_specific", :require => "omgomg"
+ end
+
+ gem "myrack", "1.0.0"
+ G
+
+ run "Bundler.require"
+ expect(err).to be_empty
+ end
+
+ it "requires gems pinned to multiple platforms, including the current one" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ platforms :#{not_local_tag}, :#{local_tag} do
+ gem "myrack", :require => "myrack"
+ end
+ G
+
+ run "Bundler.require; puts MYRACK"
+
+ expect(out).to eq("1.0.0")
+ expect(err).to be_empty
+ end
+end
diff --git a/spec/bundler/runtime/requiring_spec.rb b/spec/bundler/runtime/requiring_spec.rb
new file mode 100644
index 0000000000..f0e0aeacaf
--- /dev/null
+++ b/spec/bundler/runtime/requiring_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+RSpec.describe "Requiring bundler" do
+ it "takes care of requiring rubygems when entrypoint is bundler/setup" do
+ sys_exec("#{Gem.ruby} -I#{lib_dir} -rbundler/setup -e'puts true'", env: { "RUBYOPT" => "--disable=gems" })
+
+ expect(stdboth).to eq("true")
+ end
+
+ it "takes care of requiring rubygems when requiring just bundler" do
+ sys_exec("#{Gem.ruby} -I#{lib_dir} -rbundler -e'puts true'", env: { "RUBYOPT" => "--disable=gems" })
+
+ expect(stdboth).to eq("true")
+ end
+end
diff --git a/spec/bundler/runtime/self_management_spec.rb b/spec/bundler/runtime/self_management_spec.rb
new file mode 100644
index 0000000000..176c2a3121
--- /dev/null
+++ b/spec/bundler/runtime/self_management_spec.rb
@@ -0,0 +1,279 @@
+# frozen_string_literal: true
+
+RSpec.describe "Self management" do
+ describe "auto switching" do
+ let(:previous_minor) do
+ "9.3.0"
+ end
+
+ let(:current_version) do
+ "9.4.0"
+ end
+
+ before do
+ build_repo4 do
+ build_bundler previous_minor
+
+ build_bundler current_version
+
+ build_gem "myrack", "1.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "myrack"
+ G
+
+ pristine_system_gems "bundler-#{current_version}"
+ end
+
+ it "installs locked version when using system path and uses it" do
+ lockfile_bundled_with(previous_minor)
+
+ bundle_config "path.system true"
+ bundle "install"
+ expect(out).to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.")
+
+ # It uninstalls the older system bundler
+ bundle "clean --force", artifice: nil
+ expect(out).to eq("Removing bundler (#{current_version})")
+
+ # App now uses locked version
+ bundle "-v", artifice: nil
+ expect(out).to eq(previous_minor)
+
+ # ruby-core test setup has always "lib" in $LOAD_PATH so `require "bundler/setup"` always activate the local version rather than using RubyGems gem activation stuff
+ unless ruby_core?
+ # App now uses locked version, even when not using the CLI directly
+ file = bundled_app("bin/bundle_version.rb")
+ create_file file, <<-RUBY
+ #!#{Gem.ruby}
+ require 'bundler/setup'
+ puts '#{previous_minor}'
+ RUBY
+ file.chmod(0o777)
+ cmd = Gem.win_platform? ? "#{Gem.ruby} bin/bundle_version.rb" : "bin/bundle_version.rb"
+ in_bundled_app cmd
+ expect(out).to eq(previous_minor)
+ end
+
+ # Subsequent installs use the locked version without reinstalling
+ bundle "install --verbose", artifice: nil
+ expect(out).to include("Using bundler #{previous_minor}")
+ expect(out).not_to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.")
+ end
+
+ it "installs locked version when using local path and uses it" do
+ lockfile_bundled_with(previous_minor)
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+ expect(out).to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.")
+ expect(vendored_gems("gems/bundler-#{previous_minor}")).to exist
+
+ # It does not uninstall the locked bundler
+ bundle "clean"
+ expect(out).to be_empty
+
+ # App now uses locked version
+ bundle "-v"
+ expect(out).to eq(previous_minor)
+
+ # Preserves original gem home when auto-switching
+ bundle "exec ruby -e 'puts Bundler.original_env[\"GEM_HOME\"]'"
+ expect(out).to eq(ENV["GEM_HOME"])
+
+ # ruby-core test setup has always "lib" in $LOAD_PATH so `require "bundler/setup"` always activate the local version rather than using RubyGems gem activation stuff
+ unless ruby_core?
+ # App now uses locked version, even when not using the CLI directly
+ file = bundled_app("bin/bundle_version.rb")
+ create_file file, <<-RUBY
+ #!#{Gem.ruby}
+ require 'bundler/setup'
+ puts '#{previous_minor}'
+ RUBY
+ file.chmod(0o777)
+ cmd = Gem.win_platform? ? "#{Gem.ruby} bin/bundle_version.rb" : "bin/bundle_version.rb"
+ in_bundled_app cmd
+ expect(out).to eq(previous_minor)
+ end
+
+ # Subsequent installs use the locked version without reinstalling
+ bundle "install --verbose"
+ expect(out).to include("Using bundler #{previous_minor}")
+ expect(out).not_to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.")
+ end
+
+ it "installs locked version when using deployment option and uses it" do
+ lockfile_bundled_with(previous_minor)
+
+ bundle_config "deployment true"
+ bundle "install"
+ expect(out).to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.")
+ expect(vendored_gems("gems/bundler-#{previous_minor}")).to exist
+
+ # It does not uninstall the locked bundler
+ bundle "clean"
+ expect(out).to be_empty
+
+ # App now uses locked version
+ bundle "-v"
+ expect(out).to eq(previous_minor)
+
+ # Subsequent installs use the locked version without reinstalling
+ bundle "install --verbose"
+ expect(out).to include("Using bundler #{previous_minor}")
+ expect(out).not_to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.")
+ end
+
+ it "does not try to install a development version" do
+ lockfile_bundled_with("#{previous_minor}.dev")
+
+ bundle "install --verbose"
+ expect(out).not_to match(/restarting using that version/)
+
+ bundle "-v"
+ expect(out).to eq(current_version)
+ end
+
+ it "does not try to install when --local is passed" do
+ lockfile_bundled_with(previous_minor)
+ system_gems "myrack-1.0.0", path: local_gem_path
+
+ bundle "install --local"
+ expect(out).not_to match(/Installing Bundler/)
+
+ bundle "-v"
+ expect(out).to eq(current_version)
+ end
+
+ it "shows a discrete message if locked bundler does not exist" do
+ missing_minor = "#{current_version[0]}.999.999"
+
+ lockfile_bundled_with(missing_minor)
+
+ bundle "install"
+ expect(err).to eq("Your lockfile is locked to a version of bundler (#{missing_minor}) that doesn't exist at https://rubygems.org/. Going on using #{current_version}")
+
+ bundle "-v"
+ expect(out).to eq(current_version)
+ end
+
+ it "installs BUNDLE_VERSION version when using bundle config version x.y.z" do
+ lockfile_bundled_with(current_version)
+
+ bundle_config "version #{previous_minor}"
+ bundle "install"
+ expect(out).to include("Bundler #{current_version} is running, but your configuration was #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.")
+
+ bundle "-v"
+ expect(out).to eq(previous_minor)
+ end
+
+ it "requires the right bundler version from the config and run bundle CLI without re-exec" do
+ unless Bundler.rubygems.provides?(">= 4.1.0.dev")
+ skip "This spec can only run when Gem::BundlerVersionFinder.bundler_versions reads bundler configs"
+ end
+
+ lockfile_bundled_with(current_version)
+
+ bundle_config "version #{previous_minor}"
+ bundle_config "path.system true"
+ bundle "install"
+
+ script = bundled_app("script.rb")
+ create_file(script, "p 'executed once'")
+
+ bundle "-v", env: { "RUBYOPT" => "-r#{script}" }
+ expect(out).to eq(%("executed once"\n9.3.0))
+ end
+
+ it "does not try to install when using bundle config version global" do
+ lockfile_bundled_with(previous_minor)
+
+ bundle_config "version system"
+ bundle "install"
+ expect(out).not_to match(/restarting using that version/)
+
+ bundle "-v"
+ expect(out).to eq(current_version)
+ end
+
+ it "does not try to install when using bundle config version <dev-version>" do
+ lockfile_bundled_with(previous_minor)
+
+ bundle_config "version #{previous_minor}.dev"
+ bundle "install"
+ expect(out).not_to match(/restarting using that version/)
+
+ bundle "-v"
+ expect(out).to eq(current_version)
+ end
+
+ it "ignores malformed lockfile version" do
+ lockfile_bundled_with("2.3.")
+
+ bundle "install --verbose"
+ expect(out).to include("Using bundler #{current_version}")
+ end
+
+ it "uses the right original script when re-execing, if `$0` has been changed to something that's not a script", :ruby_repo do
+ system_gems "bundler-9.9.9", path: local_gem_path
+
+ test = bundled_app("test.rb")
+
+ create_file test, <<~RUBY
+ $0 = "this is the program name"
+ require "bundler/setup"
+ RUBY
+
+ lockfile_bundled_with("9.9.9")
+
+ in_bundled_app "#{Gem.ruby} #{test}", raise_on_error: false
+ expect(err).to include("Could not find myrack-1.0.0")
+ expect(err).not_to include("this is the program name")
+ end
+
+ it "uses modified $0 when re-execing, if `$0` has been changed to a script", :ruby_repo do
+ system_gems "bundler-9.9.9", path: local_gem_path
+
+ runner = bundled_app("runner.rb")
+
+ create_file runner, <<~RUBY
+ $0 = ARGV.shift
+ load $0
+ RUBY
+
+ script = bundled_app("script.rb")
+ create_file script, <<~RUBY
+ require "bundler/setup"
+ RUBY
+
+ lockfile_bundled_with("9.9.9")
+
+ in_bundled_app "#{Gem.ruby} #{runner} #{script}", raise_on_error: false
+ expect(err).to include("Could not find myrack-1.0.0")
+ end
+
+ private
+
+ def lockfile_bundled_with(version)
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{version}
+ L
+ end
+ end
+end
diff --git a/spec/bundler/runtime/setup_spec.rb b/spec/bundler/runtime/setup_spec.rb
new file mode 100644
index 0000000000..ceb6fcf66a
--- /dev/null
+++ b/spec/bundler/runtime/setup_spec.rb
@@ -0,0 +1,1710 @@
+# frozen_string_literal: true
+
+require "tmpdir"
+
+RSpec.describe "Bundler.setup" do
+ describe "with no arguments" do
+ it "makes all groups available" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :group => :test
+ G
+
+ ruby <<-RUBY
+ require 'bundler'
+ Bundler.setup
+
+ require 'myrack'
+ puts MYRACK
+ RUBY
+ expect(err).to be_empty
+ expect(out).to eq("1.0.0")
+ end
+ end
+
+ describe "when called with groups" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack", :group => :test
+ G
+ end
+
+ it "doesn't make all groups available" do
+ ruby <<-RUBY
+ require 'bundler'
+ Bundler.setup(:default)
+
+ begin
+ require 'myrack'
+ rescue LoadError
+ puts "WIN"
+ end
+ RUBY
+ expect(err).to be_empty
+ expect(out).to eq("WIN")
+ end
+
+ it "accepts string for group name" do
+ ruby <<-RUBY
+ require 'bundler'
+ Bundler.setup(:default, 'test')
+
+ require 'myrack'
+ puts MYRACK
+ RUBY
+ expect(err).to be_empty
+ expect(out).to eq("1.0.0")
+ end
+
+ it "leaves all groups available if they were already" do
+ ruby <<-RUBY
+ require 'bundler'
+ Bundler.setup
+ Bundler.setup(:default)
+
+ require 'myrack'
+ puts MYRACK
+ RUBY
+ expect(err).to be_empty
+ expect(out).to eq("1.0.0")
+ end
+
+ it "leaves :default available if setup is called twice" do
+ ruby <<-RUBY
+ require 'bundler'
+ Bundler.setup(:default)
+ Bundler.setup(:default, :test)
+
+ begin
+ require 'yard'
+ puts "WIN"
+ rescue LoadError
+ puts "FAIL"
+ end
+ RUBY
+ expect(err).to be_empty
+ expect(out).to match("WIN")
+ end
+
+ it "handles multiple non-additive invocations" do
+ ruby <<-RUBY, raise_on_error: false
+ require 'bundler'
+ Bundler.setup(:default, :test)
+ Bundler.setup(:default)
+ require 'myrack'
+
+ puts "FAIL"
+ RUBY
+
+ expect(err).to match("myrack")
+ expect(err).to match("LoadError")
+ expect(out).not_to match("FAIL")
+ end
+ end
+
+ context "load order" do
+ def clean_load_path(lp)
+ without_bundler_load_path = ruby("puts $LOAD_PATH").split("\n")
+ lp -= [*without_bundler_load_path, lib_dir.to_s]
+ lp.map! {|p| p.sub(system_gem_path.to_s, "") }
+ end
+
+ it "puts loaded gems after -I and RUBYLIB", :ruby_repo do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ ENV["RUBYOPT"] = "#{ENV["RUBYOPT"]} -Idash_i_dir"
+ ENV["RUBYLIB"] = "rubylib_dir"
+
+ ruby <<-RUBY
+ require 'bundler'
+ Bundler.setup
+ puts $LOAD_PATH
+ RUBY
+
+ load_path = out.split("\n")
+ myrack_load_order = load_path.index {|path| path.include?("myrack") }
+
+ expect(err).to be_empty
+ expect(load_path).to include(a_string_ending_with("dash_i_dir"), "rubylib_dir")
+ expect(myrack_load_order).to be > 0
+ end
+
+ it "orders the load path correctly when there are dependencies" do
+ bundle_config "path.system true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ ruby <<-RUBY
+ require 'bundler'
+ gem "bundler", "#{Bundler::VERSION}" if #{ruby_core?}
+ Bundler.setup
+ puts $LOAD_PATH
+ RUBY
+
+ load_path = clean_load_path(out.split("\n"))
+
+ expect(load_path).to start_with(
+ "/gems/rails-2.3.2/lib",
+ "/gems/activeresource-2.3.2/lib",
+ "/gems/activerecord-2.3.2/lib",
+ "/gems/actionpack-2.3.2/lib",
+ "/gems/actionmailer-2.3.2/lib",
+ "/gems/activesupport-2.3.2/lib",
+ "/gems/rake-#{rake_version}/lib"
+ )
+ end
+
+ it "falls back to order the load path alphabetically for backwards compatibility" do
+ bundle_config "path.system true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "weakling"
+ gem "duradura"
+ gem "terranova"
+ G
+
+ ruby <<-RUBY
+ require 'bundler/setup'
+ puts $LOAD_PATH
+ RUBY
+
+ load_path = clean_load_path(out.split("\n"))
+
+ expect(load_path).to start_with(
+ "/gems/weakling-0.0.3/lib",
+ "/gems/terranova-8/lib",
+ "/gems/duradura-7.0/lib"
+ )
+ end
+ end
+
+ it "raises if the Gemfile was not yet installed" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ ruby <<-R
+ require 'bundler'
+
+ begin
+ Bundler.setup
+ puts "FAIL"
+ rescue Bundler::GemNotFound
+ puts "WIN"
+ end
+ R
+
+ expect(out).to eq("WIN")
+ end
+
+ it "doesn't create a Gemfile.lock if the setup fails" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ ruby <<-R, raise_on_error: false
+ require 'bundler'
+
+ Bundler.setup
+ R
+
+ expect(bundled_app_lock).not_to exist
+ end
+
+ it "doesn't change the Gemfile.lock if the setup fails" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ lockfile = File.read(bundled_app_lock)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "nosuchgem", "10.0"
+ G
+
+ ruby <<-R, raise_on_error: false
+ require 'bundler'
+
+ Bundler.setup
+ R
+
+ expect(File.read(bundled_app_lock)).to eq(lockfile)
+ end
+
+ it "makes a Gemfile.lock if setup succeeds" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ File.read(bundled_app_lock)
+
+ FileUtils.rm(bundled_app_lock)
+
+ run "1"
+ expect(bundled_app_lock).to exist
+ end
+
+ describe "$BUNDLE_GEMFILE" do
+ context "user provides an absolute path" do
+ it "uses BUNDLE_GEMFILE to locate the gemfile if present" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ gemfile bundled_app("4realz"), <<-G
+ source "https://gem.repo1"
+ gem "activesupport", "2.3.5"
+ G
+
+ ENV["BUNDLE_GEMFILE"] = bundled_app("4realz").to_s
+ bundle :install
+
+ expect(the_bundle).to include_gems "activesupport 2.3.5"
+ end
+ end
+
+ context "an absolute path is not provided" do
+ it "uses BUNDLE_GEMFILE to locate the gemfile if present and doesn't fail in deployment mode" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ bundle "install"
+ bundle_config "deployment true"
+
+ ENV["BUNDLE_GEMFILE"] = "Gemfile"
+ ruby <<-R
+ require 'bundler'
+
+ begin
+ Bundler.setup
+ puts "WIN"
+ rescue ArgumentError => e
+ puts "FAIL"
+ end
+ R
+
+ expect(out).to eq("WIN")
+ end
+ end
+
+ context "user sets it via `config set --local gemfile`" do
+ it "uses the value in the config" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ gemfile bundled_app("CustomGemfile"), <<-G
+ source "https://gem.repo1"
+ gem "activesupport", "2.3.5"
+ G
+
+ bundle_config "gemfile #{bundled_app("CustomGemfile")}"
+ bundle "install"
+
+ ruby <<-R
+ require 'bundler'
+ Bundler.setup
+ require 'activesupport'
+ puts ACTIVESUPPORT
+ R
+
+ expect(out).to eq("2.3.5")
+ end
+ end
+ end
+
+ it "prioritizes gems in BUNDLE_PATH over gems in GEM_HOME" do
+ ENV["BUNDLE_PATH"] = bundled_app(".bundle").to_s
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ G
+
+ build_gem "myrack", "1.0", to_system: true do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ describe "integrate with rubygems" do
+ describe "by replacing #gem" do
+ before :each do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+ end
+
+ it "replaces #gem but raises when the gem is missing" do
+ run <<-R
+ begin
+ gem "activesupport"
+ puts "FAIL"
+ rescue LoadError
+ puts "WIN"
+ end
+ R
+
+ expect(out).to eq("WIN")
+ end
+
+ it "replaces #gem but raises when the version is wrong" do
+ run <<-R
+ begin
+ gem "myrack", "1.0.0"
+ puts "FAIL"
+ rescue LoadError
+ puts "WIN"
+ end
+ R
+
+ expect(out).to eq("WIN")
+ end
+ end
+
+ describe "by hiding system gems" do
+ before :each do
+ system_gems "activesupport-2.3.5"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "yard"
+ G
+ end
+
+ it "removes system gems from Gem.source_index" do
+ run "require 'yard'"
+ expect(out).to include("bundler-#{Bundler::VERSION}").and include("yard-1.0")
+ expect(out).not_to include("activesupport-2.3.5")
+ end
+
+ context "when the ruby stdlib is a substring of Gem.path" do
+ it "does not reject the stdlib from $LOAD_PATH" do
+ substring = "/" + $LOAD_PATH.find {|p| p.include?("vendor_ruby") }.split("/")[2]
+ run "puts 'worked!'", env: { "GEM_PATH" => substring }
+ expect(out).to eq("worked!")
+ end
+ end
+ end
+ end
+
+ describe "with paths" do
+ it "activates the gems in the path source" do
+ system_gems "myrack-1.0.0"
+
+ build_lib "myrack", "1.0.0" do |s|
+ s.write "lib/myrack.rb", "puts 'WIN'"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path("myrack-1.0.0")}" do
+ gem "myrack"
+ end
+ G
+
+ run "require 'myrack'"
+ expect(out).to eq("WIN")
+ end
+ end
+
+ describe "with git" do
+ before do
+ build_git "myrack", "1.0.0"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-1.0.0")}"
+ G
+ end
+
+ it "provides a useful exception when the git repo is not checked out yet" do
+ run "1", raise_on_error: false
+ expect(err).to match(/the git source #{lib_path("myrack-1.0.0")} is not yet checked out. Please run `bundle install`/i)
+ end
+
+ it "does not hit the git binary if the lockfile is available and up to date" do
+ bundle "install"
+
+ break_git!
+
+ ruby <<-R
+ require 'bundler'
+
+ begin
+ Bundler.setup
+ puts "WIN"
+ rescue Exception => e
+ puts "FAIL"
+ end
+ R
+
+ expect(out).to eq("WIN")
+ end
+
+ it "provides a good exception if the lockfile is unavailable" do
+ bundle "install"
+
+ FileUtils.rm(bundled_app_lock)
+
+ break_git!
+
+ ruby <<-R
+ require "bundler"
+
+ begin
+ Bundler.setup
+ puts "FAIL"
+ rescue Bundler::GitError => e
+ puts e.message
+ end
+ R
+
+ run "puts 'FAIL'", raise_on_error: false
+
+ expect(err).not_to include "This is not the git you are looking for"
+ end
+
+ it "works even when the cache directory has been deleted" do
+ bundle :install
+ FileUtils.rm_r default_cache_path
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "does not randomly change the path when specifying --path and the bundle directory becomes read only" do
+ bundle_config "path vendor/bundle"
+ bundle :install
+
+ with_read_only("#{bundled_app}/**/*") do
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ it "finds git gem when default bundle path becomes read only" do
+ bundle_config "path .bundle"
+ bundle "install"
+
+ with_read_only("#{bundled_app(".bundle")}/**/*") do
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+ end
+
+ describe "when specifying local override" do
+ it "explodes if given path does not exist on runtime" do
+ build_git "myrack", "0.8"
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install
+
+ FileUtils.rm_r(lib_path("local-myrack"))
+ run "require 'myrack'", raise_on_error: false
+ expect(err).to match(/Cannot use local override for myrack-0.8 because #{Regexp.escape(lib_path("local-myrack").to_s)} does not exist/)
+ end
+
+ it "explodes if branch is not given on runtime" do
+ build_git "myrack", "0.8"
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}"
+ G
+
+ run "require 'myrack'", raise_on_error: false
+ expect(err).to match(/because :branch is not specified in Gemfile/)
+ end
+
+ it "explodes on different branches on runtime" do
+ build_git "myrack", "0.8"
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "changed"
+ G
+
+ run "require 'myrack'", raise_on_error: false
+ expect(err).to match(/is using branch main but Gemfile specifies changed/)
+ end
+
+ it "explodes on refs with different branches on runtime" do
+ build_git "myrack", "0.8"
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :ref => "main", :branch => "main"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :ref => "main", :branch => "nonexistent"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ run "require 'myrack'", raise_on_error: false
+ expect(err).to match(/is using branch main but Gemfile specifies nonexistent/)
+ end
+ end
+
+ describe "when excluding groups" do
+ it "doesn't change the resolve if --without is used" do
+ bundle_config "without rails"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport"
+
+ group :rails do
+ gem "rails", "2.3.2"
+ end
+ G
+
+ system_gems "activesupport-2.3.5"
+
+ expect(the_bundle).to include_gems "activesupport 2.3.2", groups: :default
+ end
+
+ it "remembers --without and does not bail on bare Bundler.setup" do
+ bundle_config "without rails"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport"
+
+ group :rails do
+ gem "rails", "2.3.2"
+ end
+ G
+
+ system_gems "activesupport-2.3.5"
+
+ expect(the_bundle).to include_gems "activesupport 2.3.2"
+ end
+
+ it "remembers --without and does not bail on bare Bundler.setup, even in the case of path gems no longer available" do
+ bundle_config "without development"
+
+ path = bundled_app(File.join("vendor", "foo"))
+ build_lib "foo", path: path
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport", "2.3.2"
+ gem 'foo', :path => 'vendor/foo', :group => :development
+ G
+
+ FileUtils.rm_r(path)
+
+ ruby "require 'bundler'; Bundler.setup", env: { "DEBUG" => "1" }
+ expect(out).to include("Assuming that source at `vendor/foo` has not changed since fetching its specs errored")
+ expect(out).to include("Found no changes, using resolution from the lockfile")
+ expect(err).to be_empty
+ end
+
+ it "doesn't re-resolve when a pre-release bundler is used and a dependency includes a dependency on bundler" do
+ system_gems "bundler-9.99.9.beta1"
+
+ build_repo4 do
+ build_gem "depends_on_bundler", "1.0" do |s|
+ s.add_dependency "bundler", ">= 1.5.0"
+ end
+ end
+
+ install_gemfile <<~G
+ source "https://gem.repo4"
+ gem "depends_on_bundler"
+ G
+
+ ruby "require '#{system_gem_path("gems/bundler-9.99.9.beta1/lib/bundler.rb")}'; Bundler.setup", env: { "DEBUG" => "1" }
+ expect(out).to include("Found no changes, using resolution from the lockfile")
+ expect(out).not_to include("lockfile does not have all gems needed for the current platform")
+ expect(err).to be_empty
+ end
+
+ it "doesn't fail in frozen mode when bundler is a Gemfile dependency" do
+ install_gemfile <<~G
+ source "https://gem.repo4"
+ gem "bundler"
+ G
+
+ bundle "install --verbose", env: { "BUNDLE_FROZEN" => "true" }
+ expect(err).to be_empty
+ end
+
+ it "doesn't re-resolve when deleting dependencies" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "actionpack"
+ G
+
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(out).to include("Some dependencies were deleted, using a subset of the resolution from the lockfile")
+ expect(err).to be_empty
+ end
+
+ it "remembers --without and does not include groups passed to Bundler.setup" do
+ bundle_config "without rails"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport"
+
+ group :myrack do
+ gem "myrack"
+ end
+
+ group :rails do
+ gem "rails", "2.3.2"
+ end
+ G
+
+ expect(the_bundle).not_to include_gems "activesupport 2.3.2", groups: :myrack
+ expect(the_bundle).to include_gems "myrack 1.0.0", groups: :myrack
+ end
+ end
+
+ # RubyGems returns loaded_from as a string
+ it "has loaded_from as a string on all specs" do
+ build_git "foo"
+ build_git "no-gemspec", gemspec: false
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ gem "no-gemspec", "1.0", :git => "#{lib_path("no-gemspec-1.0")}"
+ G
+
+ run <<-R
+ Gem.loaded_specs.each do |n, s|
+ puts "FAIL" unless s.loaded_from.is_a?(String)
+ end
+ R
+
+ expect(out).to be_empty
+ end
+
+ it "has gem_dir pointing to local repo" do
+ build_lib "foo", "1.0", path: bundled_app
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ run <<-R
+ puts Gem.loaded_specs['foo'].gem_dir
+ R
+
+ expect(out).to eq(bundled_app.to_s)
+ end
+
+ it "does not load all gemspecs" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ run <<-R
+ File.open(File.join(Gem.dir, "specifications", "invalid.gemspec"), "w") do |f|
+ f.write <<-RUBY
+# -*- encoding: utf-8 -*-
+# stub: invalid 1.0.0 ruby lib
+
+Gem::Specification.new do |s|
+ s.name = "invalid"
+ s.version = "1.0.0"
+ s.authors = ["Invalid Author"]
+ s.files = ["lib/invalid.rb"]
+ s.add_dependency "nonexistent-gem", "~> 999.999.999"
+ s.validate!
+end
+ RUBY
+ end
+ R
+
+ run <<-R
+ File.open(File.join(Gem.dir, "specifications", "invalid-ext.gemspec"), "w") do |f|
+ f.write <<-RUBY
+# -*- encoding: utf-8 -*-
+# stub: invalid-ext 1.0.0 ruby lib
+# stub: a.ext\\0b.ext
+
+Gem::Specification.new do |s|
+ s.name = "invalid-ext"
+ s.version = "1.0.0"
+ s.authors = ["Invalid Author"]
+ s.files = ["lib/invalid.rb"]
+ s.required_ruby_version = "~> 0.8.0"
+ s.validate!
+end
+ RUBY
+ end
+ # Need to write the gem.build_complete file,
+ # otherwise the full spec is loaded to check the installed_by_version
+ extensions_dir = Gem.default_ext_dir_for(Gem.dir) || File.join(Gem.dir, "extensions", Gem::Platform.local.to_s, Gem.extension_api_version)
+ Bundler::FileUtils.mkdir_p(File.join(extensions_dir, "invalid-ext-1.0.0"))
+ File.open(File.join(extensions_dir, "invalid-ext-1.0.0", "gem.build_complete"), "w") {}
+ R
+
+ run <<-R
+ puts "Success"
+ R
+
+ expect(out).to eq("Success")
+ end
+
+ it "ignores empty gem paths" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ ENV["GEM_HOME"] = ""
+ bundle %(exec ruby -e "require 'set'")
+
+ expect(err).to be_empty
+ end
+
+ it "can require rubygems without warnings, when using a local cache", :truffleruby do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "package"
+ bundle %(exec ruby -w -e "require 'rubygems'")
+
+ expect(err).to be_empty
+ end
+
+ context "when the user has `MANPATH` set", :man do
+ before { ENV["MANPATH"] = "/foo#{File::PATH_SEPARATOR}" }
+
+ it "adds the gem's man dir to the MANPATH" do
+ build_repo4 do
+ build_gem "with_man" do |s|
+ s.write("man/man1/page.1", "MANPAGE")
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "with_man"
+ G
+
+ run "puts ENV['MANPATH']"
+ expect(out).to eq("#{default_bundle_path("gems/with_man-1.0/man")}#{File::PATH_SEPARATOR}/foo")
+ end
+ end
+
+ context "when the user does not have `MANPATH` set", :man do
+ before { ENV.delete("MANPATH") }
+
+ it "adds the gem's man dir to the MANPATH, leaving : in the end so that system man pages still work" do
+ build_repo4 do
+ build_gem "with_man" do |s|
+ s.write("man/man1/page.1", "MANPAGE")
+ end
+
+ build_gem "with_man_overriding_system_man" do |s|
+ s.write("man/man1/ls.1", "LS MANPAGE")
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "with_man"
+ G
+
+ run <<~RUBY
+ puts ENV['MANPATH']
+ require "open3"
+ puts Open3.capture2e("man", "ls")[1].success?
+ RUBY
+
+ expect(out).to eq("#{default_bundle_path("gems/with_man-1.0/man")}#{File::PATH_SEPARATOR}\ntrue")
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "with_man_overriding_system_man"
+ G
+
+ run <<~RUBY
+ puts ENV['MANPATH']
+ require "open3"
+ puts Open3.capture2e({ "LC_ALL" => "C" }, "man", "ls")[0]
+ RUBY
+
+ lines = out.split("\n")
+
+ expect(lines).to include("#{default_bundle_path("gems/with_man_overriding_system_man-1.0/man")}#{File::PATH_SEPARATOR}")
+ expect(lines).to include("LS MANPAGE")
+ end
+ end
+
+ it "should prepend gemspec require paths to $LOAD_PATH in order" do
+ update_repo2 do
+ build_gem("requirepaths") do |s|
+ s.write("lib/rq.rb", "puts 'yay'")
+ s.write("src/rq.rb", "puts 'nooo'")
+ s.require_paths = %w[lib src]
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "requirepaths", :require => nil
+ G
+
+ run "require 'rq'"
+ expect(out).to eq("yay")
+ end
+
+ it "should clean $LOAD_PATH properly" do
+ gem_name = "very_simple_binary"
+ full_gem_name = gem_name + "-1.0"
+
+ system_gems full_gem_name
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ ruby <<-R
+ require 'bundler'
+ gem '#{gem_name}'
+
+ puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} >= 2
+
+ Bundler.setup
+
+ puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} == 0
+ R
+
+ expect(out).to eq("true\ntrue")
+ end
+
+ context "with bundler is located in symlinked GEM_HOME" do
+ let(:gem_home) { Dir.mktmpdir }
+ let(:symlinked_gem_home) { tmp("gem_home-symlink").to_s }
+ let(:full_name) { "bundler-#{Bundler::VERSION}" }
+
+ before do
+ File.symlink(gem_home, symlinked_gem_home)
+ gems_dir = File.join(gem_home, "gems")
+ specifications_dir = File.join(gem_home, "specifications")
+ Dir.mkdir(gems_dir)
+ Dir.mkdir(specifications_dir)
+
+ File.symlink(source_root, File.join(gems_dir, full_name))
+
+ gemspec_content = File.binread(gemspec).
+ sub("Bundler::VERSION", %("#{Bundler::VERSION}")).
+ lines.reject {|line| line.include?("lib/bundler/version") }.join
+
+ File.open(File.join(specifications_dir, "#{full_name}.gemspec"), "wb") do |f|
+ f.write(gemspec_content)
+ end
+ end
+
+ it "should not remove itself from the LOAD_PATH and require a different copy of 'bundler/setup'" do
+ install_gemfile "source 'https://gem.repo1'"
+
+ ruby <<-R, env: { "GEM_PATH" => symlinked_gem_home }
+ TracePoint.trace(:class) do |tp|
+ if tp.path.include?("bundler") && !tp.path.start_with?("#{source_root}")
+ puts "OMG. Defining a class from another bundler at \#{tp.path}:\#{tp.lineno}"
+ end
+ end
+ gem 'bundler', '#{Bundler::VERSION}'
+ require 'bundler/setup'
+ R
+
+ expect(out).to be_empty
+ end
+ end
+
+ it "does not reveal system gems even when Gem.refresh is called" do
+ system_gems "myrack-1.0.0"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport"
+ G
+
+ run <<-R
+ puts Bundler.rubygems.installed_specs.map(&:name)
+ Gem.refresh
+ puts Bundler.rubygems.installed_specs.map(&:name)
+ R
+
+ expect(out).to eq("activesupport\nbundler\nactivesupport\nbundler")
+ end
+
+ describe "when a vendored gem specification uses the :path option" do
+ let(:filesystem_root) do
+ current = Pathname.new(Dir.pwd)
+ current = current.parent until current == current.parent
+ current
+ end
+
+ it "should resolve paths relative to the Gemfile" do
+ path = bundled_app(File.join("vendor", "foo"))
+ build_lib "foo", path: path
+
+ # If the .gemspec exists, then Bundler handles the path differently.
+ # See Source::Path.load_spec_files for details.
+ FileUtils.rm(File.join(path, "foo.gemspec"))
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', '1.2.3', :path => 'vendor/foo'
+ G
+
+ run <<-R, env: { "BUNDLE_GEMFILE" => bundled_app_gemfile.to_s }, dir: bundled_app.parent
+ require 'foo'
+ R
+ expect(err).to be_empty
+ end
+
+ it "should make sure the Bundler.root is really included in the path relative to the Gemfile" do
+ relative_path = File.join("vendor", Dir.pwd.gsub(/^#{filesystem_root}/, ""))
+ absolute_path = bundled_app(relative_path)
+ FileUtils.mkdir_p(absolute_path)
+ build_lib "foo", path: absolute_path
+
+ # If the .gemspec exists, then Bundler handles the path differently.
+ # See Source::Path.load_spec_files for details.
+ FileUtils.rm(File.join(absolute_path, "foo.gemspec"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', '1.2.3', :path => '#{relative_path}'
+ G
+
+ bundle :install
+
+ run <<-R, env: { "BUNDLE_GEMFILE" => bundled_app_gemfile.to_s }, dir: bundled_app.parent
+ require 'foo'
+ R
+
+ expect(err).to be_empty
+ end
+ end
+
+ describe "with git gems that don't have gemspecs" do
+ before :each do
+ build_git "no_gemspec", gemspec: false
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "no_gemspec", "1.0", :git => "#{lib_path("no_gemspec-1.0")}"
+ G
+ end
+
+ it "loads the library via a virtual spec" do
+ run <<-R
+ require 'no_gemspec'
+ puts NO_GEMSPEC
+ R
+
+ expect(out).to eq("1.0")
+ end
+ end
+
+ describe "with bundled and system gems" do
+ before :each do
+ system_gems "myrack-1.0.0"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "activesupport", "2.3.5"
+ G
+ end
+
+ it "does not pull in system gems" do
+ run <<-R
+ begin;
+ require 'myrack'
+ rescue LoadError
+ puts 'WIN'
+ end
+ R
+
+ expect(out).to eq("WIN")
+ end
+
+ it "provides a gem method" do
+ run <<-R
+ gem 'activesupport'
+ require 'activesupport'
+ puts ACTIVESUPPORT
+ R
+
+ expect(out).to eq("2.3.5")
+ end
+
+ it "raises an exception if gem is used to invoke a system gem not in the bundle" do
+ run <<-R
+ begin
+ gem 'myrack'
+ rescue LoadError => e
+ puts e.message
+ end
+ R
+
+ expect(out).to eq("myrack is not part of the bundle. Add it to your Gemfile.")
+ end
+
+ it "sets GEM_HOME appropriately" do
+ run "puts ENV['GEM_HOME']"
+ expect(out).to eq(default_bundle_path.to_s)
+ end
+ end
+
+ describe "with system gems in the bundle" do
+ before :each do
+ bundle_config "path.system true"
+ system_gems "myrack-1.0.0"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ gem "activesupport", "2.3.5"
+ G
+ end
+
+ it "sets GEM_PATH appropriately" do
+ run "puts Gem.path"
+ paths = out.split("\n")
+ expect(paths).to include(system_gem_path.to_s)
+ end
+ end
+
+ describe "with a gemspec that requires other files" do
+ before :each do
+ build_git "bar", gemspec: false do |s|
+ s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0')
+ s.write "bar.gemspec", <<-G
+ require_relative 'lib/bar/version'
+
+ Gem::Specification.new do |s|
+ s.name = 'bar'
+ s.version = BAR_VERSION
+ s.summary = 'Bar'
+ s.files = Dir["lib/**/*.rb"]
+ s.author = 'no one'
+ end
+ G
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "bar", :git => "#{lib_path("bar-1.0")}"
+ G
+ end
+
+ it "evals each gemspec in the context of its parent directory" do
+ bundle :install
+ run "require 'bar'; puts BAR"
+ expect(out).to eq("1.0")
+ end
+
+ it "error intelligently if the gemspec has a LoadError" do
+ skip "whitespace issue?" if Gem.win_platform?
+
+ ref = update_git "bar", gemspec: false do |s|
+ s.write "bar.gemspec", "require 'foobarbaz'"
+ end.ref_for("HEAD")
+ bundle :install, raise_on_error: false
+
+ expect(err.lines.map(&:chomp)).to include(
+ a_string_starting_with("[!] There was an error while loading `bar.gemspec`:"),
+ " # from #{default_bundle_path "bundler", "gems", "bar-1.0-#{ref[0, 12]}", "bar.gemspec"}:1",
+ " > require 'foobarbaz'"
+ )
+ end
+
+ it "evals each gemspec with a binding from the top level" do
+ bundle "install"
+
+ ruby <<-RUBY
+ require 'bundler'
+ bundler_module = class << Bundler; self; end
+ bundler_module.send(:remove_method, :require)
+ def Bundler.require(path)
+ raise StandardError, "didn't use binding from top level"
+ end
+ Bundler.load
+ RUBY
+
+ expect(err).to be_empty
+ expect(out).to be_empty
+ end
+ end
+
+ describe "when Bundler is bundled" do
+ it "doesn't blow up" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "bundler", :path => "#{root}"
+ G
+
+ bundle %(exec ruby -e "require 'bundler'; Bundler.setup")
+ expect(err).to be_empty
+ end
+ end
+
+ describe "when BUNDLED WITH" do
+ def lock_with(bundler_version = nil)
+ lock = <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ L
+
+ if bundler_version
+ lock += "\nBUNDLED WITH\n #{bundler_version}\n"
+ end
+
+ lock
+ end
+
+ before do
+ bundle_config "path.system true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ context "is not present" do
+ it "does not change the lock" do
+ lockfile lock_with(nil)
+ ruby "require 'bundler/setup'"
+ expect(lockfile).to eq lock_with(nil)
+ end
+ end
+
+ context "is newer" do
+ it "does not change the lock or warn" do
+ lockfile lock_with(Bundler::VERSION.succ)
+ ruby "require 'bundler/setup'"
+ expect(out).to be_empty
+ expect(err).to be_empty
+ expect(lockfile).to eq lock_with(Bundler::VERSION.succ)
+ end
+ end
+
+ context "is older" do
+ it "does not change the lock" do
+ system_gems "bundler-1.10.1"
+ lockfile lock_with("1.10.1")
+ ruby "require 'bundler/setup'"
+ expect(lockfile).to eq lock_with("1.10.1")
+ end
+ end
+ end
+
+ describe "when RUBY VERSION" do
+ let(:ruby_version) { nil }
+
+ def lock_with(ruby_version = nil)
+ checksums = checksums_section do |c|
+ c.checksum gem_repo1, "myrack", "1.0.0"
+ end
+
+ lock = <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ L
+
+ if ruby_version
+ lock += "\nRUBY VERSION\n ruby #{ruby_version}\n"
+ end
+
+ lock += <<~L
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lock
+ end
+
+ before do
+ install_gemfile <<-G
+ ruby ">= 0"
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ lockfile lock_with(ruby_version)
+ end
+
+ context "is not present" do
+ # Skipped on ruby-core because `ruby "require 'bundler/setup'"` does not
+ # activate bundler as a gem there, so Source::Metadata falls back to a
+ # synthetic spec whose cache_file does not exist on disk and
+ # LockfileGenerator#bundler_checksum drops the bundler checksum, while
+ # the on-disk lockfile still has it.
+ it "does not change the lock", :ruby_repo do
+ expect { ruby "require 'bundler/setup'" }.not_to change { lockfile }
+ end
+ end
+
+ context "is newer" do
+ let(:ruby_version) { "5.5.5" }
+ it "does not change the lock or warn" do
+ expect { ruby "require 'bundler/setup'" }.not_to change { lockfile }
+ expect(out).to be_empty
+ expect(err).to be_empty
+ end
+ end
+
+ context "is older" do
+ let(:ruby_version) { "1.0.0" }
+ it "does not change the lock" do
+ expect { ruby "require 'bundler/setup'" }.not_to change { lockfile }
+ end
+ end
+ end
+
+ describe "with gemified standard libraries" do
+ it "does not load Digest", :ruby_repo do
+ build_git "bar", gemspec: false do |s|
+ s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0')
+ s.write "bar.gemspec", <<-G
+ require_relative 'lib/bar/version'
+
+ Gem::Specification.new do |s|
+ s.name = 'bar'
+ s.version = BAR_VERSION
+ s.summary = 'Bar'
+ s.files = Dir["lib/**/*.rb"]
+ s.author = 'no one'
+
+ s.add_dependency 'digest'
+ end
+ G
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "bar", :git => "#{lib_path("bar-1.0")}"
+ G
+
+ bundle :install, env: { "BUNDLE_LOCKFILE_CHECKSUMS" => "false" }
+
+ ruby <<-RUBY, artifice: nil
+ require 'bundler/setup'
+ puts defined?(::Digest) ? "Digest defined" : "Digest undefined"
+ require 'digest'
+ RUBY
+ expect(out).to eq("Digest undefined")
+ end
+
+ it "does not load Psych" do
+ gemfile "source 'https://gem.repo1'"
+ ruby <<-RUBY
+ require 'bundler/setup'
+ puts defined?(Psych::VERSION) ? Psych::VERSION : "undefined"
+ require 'psych'
+ puts Psych::VERSION
+ RUBY
+ pre_bundler, post_bundler = out.split("\n")
+ expect(pre_bundler).to eq("undefined")
+ expect(post_bundler).to match(/\d+\.\d+\.\d+/)
+ end
+
+ it "does not load openssl" do
+ install_gemfile "source 'https://gem.repo1'"
+ ruby <<-RUBY, artifice: nil
+ require "bundler/setup"
+ puts defined?(OpenSSL) || "undefined"
+ require "openssl"
+ puts defined?(OpenSSL) || "undefined"
+ RUBY
+ expect(out).to eq("undefined\nconstant")
+ end
+
+ it "does not load uri while reading gemspecs", rubygems: ">= 3.6.0.dev" do
+ Dir.mkdir bundled_app("test")
+
+ create_file(bundled_app("test/test.gemspec"), <<-G)
+ Gem::Specification.new do |s|
+ s.name = "test"
+ s.version = "1.0.0"
+ s.summary = "test"
+ s.authors = ['John Doe']
+ s.homepage = 'https://example.com'
+ end
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "test", path: "#{bundled_app("test")}"
+ G
+
+ ruby <<-RUBY, artifice: nil
+ require "bundler/setup"
+ puts defined?(URI) || "undefined"
+ require "uri"
+ puts defined?(URI) || "undefined"
+ RUBY
+ expect(out).to eq("undefined\nconstant")
+ end
+
+ it "activates default gems when they are part of the bundle, but not installed explicitly", :ruby_repo do
+ default_delegate_version = ruby "gem 'delegate'; require 'delegate'; puts Delegator::VERSION"
+
+ build_repo2 do
+ build_gem "delegate", default_delegate_version
+ end
+
+ gemfile "source \"https://gem.repo2\"; gem 'delegate'"
+
+ ruby <<-RUBY
+ require "bundler/setup"
+ require "delegate"
+ puts defined?(::Delegator) ? "Delegator defined" : "Delegator undefined"
+ RUBY
+
+ expect(out).to eq("Delegator defined")
+ expect(err).to be_empty
+ end
+
+ describe "default gem activation" do
+ let(:exemptions) do
+ exempts = %w[did_you_mean bundler uri pathname]
+ exempts << "error_highlight" # added in Ruby 3.1 as a default gem
+ exempts << "ruby2_keywords" # added in Ruby 3.1 as a default gem
+ exempts << "syntax_suggest" # added in Ruby 3.2 as a default gem
+ exempts
+ end
+
+ let(:activation_warning_hack) { <<~RUBY }
+ require #{spec_dir.join("support/hax").to_s.dump}
+
+ Gem::Specification.send(:alias_method, :bundler_spec_activate, :activate)
+ Gem::Specification.send(:define_method, :activate) do
+ unless #{exemptions.inspect}.include?(name)
+ warn '-' * 80
+ warn "activating \#{full_name}"
+ warn(*caller)
+ warn '*' * 80
+ end
+ bundler_spec_activate
+ end
+ RUBY
+
+ let(:activation_warning_hack_rubyopt) do
+ create_file("activation_warning_hack.rb", activation_warning_hack)
+ "-r#{bundled_app("activation_warning_hack.rb")} #{ENV["RUBYOPT"]}"
+ end
+
+ let(:code) { <<~RUBY }
+ require "pp"
+ loaded_specs = Gem.loaded_specs.dup
+ #{exemptions.inspect}.each {|s| loaded_specs.delete(s) }
+ pp loaded_specs
+
+ # not a default gem, but harmful to have loaded
+ open_uri = $LOADED_FEATURES.grep(/open.uri/)
+ unless open_uri.empty?
+ warn "open_uri: \#{open_uri}"
+ end
+ RUBY
+
+ it "activates no gems with -rbundler/setup" do
+ install_gemfile "source 'https://gem.repo1'"
+ ruby code, env: { "RUBYOPT" => activation_warning_hack_rubyopt + " -rbundler/setup" }, artifice: nil
+ expect(out).to eq("{}")
+ end
+
+ it "activates no gems with bundle exec" do
+ install_gemfile "source 'https://gem.repo1'"
+ create_file("script.rb", code)
+ bundle "exec ruby ./script.rb", env: { "RUBYOPT" => activation_warning_hack_rubyopt }
+ expect(out).to eq("{}")
+ end
+
+ it "activates no gems with bundle exec that is loaded" do
+ skip "not executable" if Gem.win_platform?
+
+ install_gemfile "source 'https://gem.repo1'"
+ create_file("script.rb", "#!/usr/bin/env ruby\n\n#{code}")
+ FileUtils.chmod(0o777, bundled_app("script.rb"))
+ bundle "exec ./script.rb", env: { "RUBYOPT" => activation_warning_hack_rubyopt }
+ expect(out).to eq("{}")
+ end
+
+ it "does not load net-http-pipeline too early" do
+ build_repo4 do
+ build_gem "net-http-pipeline", "1.0.1"
+ end
+
+ system_gems "net-http-pipeline-1.0.1", gem_repo: gem_repo4
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "net-http-pipeline", "1.0.1"
+ G
+
+ bundle_config "path vendor/bundle"
+
+ bundle :install
+
+ bundle :check
+
+ expect(out).to eq("The Gemfile's dependencies are satisfied")
+ end
+
+ Gem::Specification.select(&:default_gem?).map(&:name).each do |g|
+ it "activates newer versions of #{g}", :ruby_repo do
+ skip if exemptions.include?(g)
+
+ build_repo4 do
+ build_gem g, "999999"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "#{g}", "999999"
+ G
+
+ expect(the_bundle).to include_gem("#{g} 999999", env: { "RUBYOPT" => activation_warning_hack_rubyopt }, artifice: nil)
+ end
+
+ it "activates older versions of #{g}", :ruby_repo do
+ skip if exemptions.include?(g)
+
+ build_repo4 do
+ build_gem g, "0.0.0.a"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "#{g}", "0.0.0.a"
+ G
+
+ expect(the_bundle).to include_gem("#{g} 0.0.0.a", env: { "RUBYOPT" => activation_warning_hack_rubyopt }, artifice: nil)
+ end
+ end
+ end
+ end
+
+ describe "after setup" do
+ it "keeps Kernel#gem private" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ ruby <<-RUBY, raise_on_error: false
+ require "bundler/setup"
+ Object.new.gem "myrack"
+ puts "FAIL"
+ RUBY
+
+ expect(stdboth).not_to include "FAIL"
+ expect(err).to match(/private method [`']gem'/)
+ end
+
+ it "keeps Kernel#require private" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ ruby <<-RUBY, raise_on_error: false
+ require "bundler/setup"
+ Object.new.require "myrack"
+ puts "FAIL"
+ RUBY
+
+ expect(stdboth).not_to include "FAIL"
+ expect(err).to match(/private method [`']require'/)
+ end
+
+ it "memoizes initial set of specs when requiring bundler/setup, so that even if further code mutates dependencies, Bundler.definition.specs is not affected" do
+ install_gemfile <<~G
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack", :group => :test
+ G
+
+ ruby <<-RUBY, raise_on_error: false
+ require "bundler/setup"
+ Bundler.require(:test).select! {|d| (d.groups & [:test]).any? }
+ puts Bundler.definition.specs.map(&:name).join(", ")
+ RUBY
+
+ expect(out).to include("myrack, yard")
+ end
+
+ it "does not cause double loads when higher versions of default gems are activated before bundler" do
+ build_repo2 do
+ build_gem "json", "999.999.999" do |s|
+ s.write "lib/json.rb", <<~RUBY
+ module JSON
+ VERSION = "999.999.999"
+ end
+ RUBY
+ end
+ end
+
+ system_gems "json-999.999.999", gem_repo: gem_repo2
+
+ install_gemfile "source 'https://gem.repo1'"
+ ruby <<-RUBY
+ require "json"
+ require "bundler/setup"
+ require "json"
+ RUBY
+
+ expect(err).to be_empty
+ end
+ end
+
+ it "does not undo the Kernel.require decorations", rubygems: ">= 3.4.6" do
+ install_gemfile "source 'https://gem.repo1'"
+ script = bundled_app("bin/script")
+ create_file(script, <<~RUBY)
+ module Kernel
+ module_function
+
+ alias_method :require_before_extra_monkeypatches, :require
+
+ def require(path)
+ puts "requiring \#{path} used the monkeypatch"
+
+ require_before_extra_monkeypatches(path)
+ end
+ end
+
+ require "bundler/setup"
+
+ require "foo"
+ RUBY
+
+ sys_exec "#{Gem.ruby} #{script}", raise_on_error: false
+ expect(out).to include("requiring foo used the monkeypatch")
+ end
+
+ it "performs an automatic bundle install" do
+ build_repo4 do
+ build_gem "myrack", "1.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :group => :test
+ G
+
+ bundle_config "auto_install 1"
+
+ ruby <<-RUBY, artifice: "compact_index"
+ require 'bundler/setup'
+ RUBY
+ expect(err).to be_empty
+ expect(out).to include("Installing myrack 1.0.0")
+ end
+
+ context "in a read-only filesystem" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo4"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+
+ PLATFORMS
+ x86_64-darwin-19
+
+ DEPENDENCIES
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "should fail loudly if the lockfile platforms don't include the current platform" do
+ simulate_platform "x86_64-linux" do
+ ruby <<-RUBY, raise_on_error: false, env: { "BUNDLER_SPEC_READ_ONLY" => "true", "BUNDLER_FORCE_TTY" => "true" }
+ require "bundler/setup"
+ RUBY
+ end
+
+ expect(err).to include("Your lockfile is missing the current platform, but can't be updated because file system is read-only")
+ end
+ end
+end
diff --git a/spec/bundler/spec_helper.rb b/spec/bundler/spec_helper.rb
new file mode 100644
index 0000000000..27ddc6a771
--- /dev/null
+++ b/spec/bundler/spec_helper.rb
@@ -0,0 +1,192 @@
+# frozen_string_literal: true
+
+require "psych"
+require "bundler/vendored_fileutils"
+require "bundler/vendored_uri"
+require "digest"
+
+if File.expand_path(__FILE__) =~ %r{([^\w/\.:\-])}
+ abort "The bundler specs cannot be run from a path that contains special characters (particularly #{$1.inspect})"
+end
+
+# Bundler CLI will have different help text depending on whether any of these
+# variables is set, since the `-e` flag `bundle gem` with require an explicit
+# value if they are not set, but will use their value by default if set. So make
+# sure they are `nil` before loading bundler to get a consistent help text,
+# since some tests rely on that.
+ENV["EDITOR"] = nil
+ENV["VISUAL"] = nil
+ENV["BUNDLER_EDITOR"] = nil
+require "bundler"
+
+# If we use shared GEM_HOME and install multiple versions, it may cause
+# unexpected test failures.
+gem "diff-lcs", "< 2.0"
+
+require "rspec/core"
+require "rspec/expectations"
+require "rspec/mocks"
+require "rspec/support/differ"
+gem "rubygems-generate_index"
+require "rubygems/indexer"
+
+require_relative "support/builders"
+require_relative "support/checksums"
+require_relative "support/filters"
+require_relative "support/helpers"
+require_relative "support/indexes"
+require_relative "support/matchers"
+require_relative "support/permissions"
+require_relative "support/platforms"
+require_relative "support/shards"
+
+begin
+ raise LoadError if File.exist?(File.expand_path("../../lib/bundler/bundler.gemspec", __dir__))
+
+ gem "simplecov_json_formatter"
+ require "simplecov"
+
+ SimpleCov.start do
+ command_name "bundler:#{Process.pid}"
+ root File.expand_path("../bundler", __dir__)
+ coverage_dir File.expand_path("../coverage", __dir__)
+
+ add_filter "/spec/"
+ add_filter "/test/"
+ add_filter "/lib/rubygems/"
+ add_filter "/lib/bundler/vendor/"
+ add_filter "/tool/"
+ add_filter "/tmp/"
+ add_filter ".gemspec"
+ end
+
+ SimpleCov.print_error_status = false
+ SimpleCov.at_exit do
+ $stdout = File.open(File::NULL, "w")
+ SimpleCov.result.format!
+ ensure
+ $stdout = STDOUT
+ end
+rescue LoadError
+ # SimpleCov is not installed
+end
+
+$debug = false
+
+module Gem
+ def self.ruby=(ruby)
+ @ruby = ruby
+ end
+end
+
+RSpec.configure do |config|
+ config.include Spec::Builders
+ config.include Spec::Checksums
+ config.include Spec::Helpers
+ config.include Spec::Indexes
+ config.include Spec::Matchers
+ config.include Spec::Path
+ config.include Spec::Platforms
+ config.include Spec::Permissions
+ config.include Spec::Shards
+
+ # Enable flags like --only-failures and --next-failure
+ config.example_status_persistence_file_path = ".rspec_status"
+
+ config.silence_filter_announcements = !ENV["TEST_ENV_NUMBER"].nil?
+
+ config.backtrace_exclusion_patterns <<
+ %r{./spec/(spec_helper\.rb|support/.+)}
+
+ config.disable_monkey_patching!
+
+ # Since failures cause us to keep a bunch of long strings in memory, stop
+ # once we have a large number of failures (indicative of core pieces of
+ # bundler being broken) so that running the full test suite doesn't take
+ # forever due to memory constraints
+ config.fail_fast ||= 25 if ENV["CI"]
+
+ config.bisect_runner = :shell
+
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+
+ c.max_formatted_output_length = 1000
+ end
+
+ config.mock_with :rspec do |mocks|
+ mocks.allow_message_expectations_on_nil = false
+ end
+
+ config.before :suite do
+ Gem.ruby = ENV["RUBY"] if ENV["RUBY"]
+
+ require_relative "support/rubygems_ext"
+ Spec::Rubygems.test_setup
+
+ # Disable retry delays in tests to speed them up
+ Bundler::Retry.default_base_delay = 0
+
+ # Simulate bundler has not yet been loaded
+ ENV.replace(ENV.to_hash.delete_if {|k, _v| k.start_with?(Bundler::EnvironmentPreserver::BUNDLER_PREFIX) })
+
+ ENV["BUNDLER_SPEC_RUN"] = "true"
+ ENV["BUNDLE_USER_CONFIG"] = ENV["BUNDLE_USER_CACHE"] = ENV["BUNDLE_USER_PLUGIN"] = nil
+ ENV["BUNDLE_APP_CONFIG"] = nil
+ ENV["BUNDLE_SILENCE_ROOT_WARNING"] = nil
+ ENV["RUBYGEMS_GEMDEPS"] = nil
+ ENV["XDG_CONFIG_HOME"] = nil
+ ENV["XDG_CACHE_HOME"] = nil
+ ENV["GEMRC"] = nil
+
+ # Prevent tests from modifying the user's global git config.
+ # GIT_CONFIG_GLOBAL and GIT_CONFIG_NOSYSTEM are available since Git 2.32.
+ git_version = `git --version`[/(\d+\.\d+\.\d+)/, 1]
+ if Gem::Version.new(git_version) >= Gem::Version.new("2.32")
+ ENV["GIT_CONFIG_GLOBAL"] = File.join(ENV["HOME"], ".gitconfig")
+ ENV["GIT_CONFIG_NOSYSTEM"] = "1"
+ end
+
+ # Don't wrap output in tests
+ ENV["THOR_COLUMNS"] = "10000"
+
+ extend(Spec::Builders)
+
+ build_repo1
+
+ reset!
+ end
+
+ config.around :each do |example|
+ default_system_gems
+
+ with_gem_path_as(system_gem_path) do
+ Bundler.ui.silence { example.run }
+
+ all_output = all_commands_output
+ if example.exception && !all_output.empty?
+ message = all_output + "\n" + example.exception.message
+ (class << example.exception; self; end).send(:define_method, :message) do
+ message
+ end
+ end
+ end
+ ensure
+ reset!
+ end
+
+ Spec::Shards::EXAMPLE_MAPPINGS.each do |tag, file_paths|
+ file_pattern = Regexp.union(file_paths.map {|path| Regexp.new(Regexp.escape(path) + "$") })
+
+ config.define_derived_metadata(file_path: file_pattern) do |metadata|
+ metadata[tag] = true
+ end
+ end
+
+ config.before(:context) do |example|
+ metadata = example.class.metadata
+ if metadata[:type] != :aruba && !metadata[:realworld] && metadata.keys.none? {|k| Spec::Shards::EXAMPLE_MAPPINGS.keys.include?(k) }
+ warn "#{metadata[:file_path]} is not assigned to any shard. see spec/support/shards.rb for details."
+ end
+ end unless Spec::Path.ruby_core?
+end
diff --git a/spec/bundler/support/activate.rb b/spec/bundler/support/activate.rb
new file mode 100644
index 0000000000..143b77833d
--- /dev/null
+++ b/spec/bundler/support/activate.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require "rubygems"
+Gem.instance_variable_set(:@ruby, ENV["RUBY"]) if ENV["RUBY"]
+
+require_relative "path"
+bundler_gemspec = Spec::Path.loaded_gemspec
+bundler_gemspec.instance_variable_set(:@full_gem_path, Spec::Path.source_root.to_s)
+bundler_gemspec.activate if bundler_gemspec.respond_to?(:activate)
diff --git a/spec/bundler/support/artifice/compact_index.rb b/spec/bundler/support/artifice/compact_index.rb
new file mode 100644
index 0000000000..ebc4d0ae5b
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexAPI)
diff --git a/spec/bundler/support/artifice/compact_index_api_missing.rb b/spec/bundler/support/artifice/compact_index_api_missing.rb
new file mode 100644
index 0000000000..f771f7d1f0
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_api_missing.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexApiMissing < CompactIndexAPI
+ get "/fetch/actual/gem/:id" do
+ halt 404
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexApiMissing)
diff --git a/spec/bundler/support/artifice/compact_index_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_basic_authentication.rb
new file mode 100644
index 0000000000..b9115cdd86
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_basic_authentication.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexBasicAuthentication < CompactIndexAPI
+ before do
+ unless env["HTTP_AUTHORIZATION"]
+ halt 401, "Authentication info not supplied"
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexBasicAuthentication)
diff --git a/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb
new file mode 100644
index 0000000000..83b147d2ae
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexChecksumMismatch < CompactIndexAPI
+ get "/versions" do
+ headers "Repr-Digest" => "sha-256=:ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=:"
+ headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60"
+ content_type "text/plain"
+ body "content does not match the checksum"
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexChecksumMismatch)
diff --git a/spec/bundler/support/artifice/compact_index_concurrent_download.rb b/spec/bundler/support/artifice/compact_index_concurrent_download.rb
new file mode 100644
index 0000000000..5d55b8a72b
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_concurrent_download.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexConcurrentDownload < CompactIndexAPI
+ get "/versions" do
+ versions = File.join(Bundler.rubygems.user_home, ".bundle", "cache", "compact_index",
+ "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions")
+
+ # Verify the original content hasn't been deleted, e.g. on a retry
+ data = File.binread(versions)
+ data == "created_at" || raise("Original file should be present with expected content")
+
+ # Verify this is only requested once for a partial download
+ env["HTTP_RANGE"] == "bytes=#{data.bytesize - 1}-" || raise("Missing Range header for expected partial download")
+
+ # Overwrite the file in parallel, which should be then overwritten
+ # after a successful download to prevent corruption
+ File.open(versions, "w") {|f| f.puts "another process" }
+
+ etag_response do
+ file = tmp("versions.list")
+ FileUtils.rm_f(file)
+ file = CompactIndex::VersionsFile.new(file.to_s)
+ file.create(gems)
+ file.contents
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexConcurrentDownload)
diff --git a/spec/bundler/support/artifice/compact_index_cooldown.rb b/spec/bundler/support/artifice/compact_index_cooldown.rb
new file mode 100644
index 0000000000..85e3173c98
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_cooldown.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index_cooldown"
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexCooldownAPI)
diff --git a/spec/bundler/support/artifice/compact_index_creds_diff_host.rb b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb
new file mode 100644
index 0000000000..282e9c8961
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexCredsDiffHost < CompactIndexAPI
+ helpers do
+ def auth
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
+ end
+
+ def authorized?
+ auth.provided? && auth.basic? && auth.credentials && auth.credentials == %w[user pass]
+ end
+
+ def protected!
+ return if authorized?
+ response["WWW-Authenticate"] = %(Basic realm="Testing HTTP Auth")
+ throw(:halt, [401, "Not authorized\n"])
+ end
+ end
+
+ before do
+ protected! unless request.path_info.include?("/no/creds/")
+ end
+
+ get "/gems/:id" do
+ redirect "http://diffhost.test/no/creds/#{params[:id]}"
+ end
+
+ get "/no/creds/:id" do
+ if request.host.include?("diffhost") && !auth.provided?
+ File.binread("#{gem_repo1}/gems/#{params[:id]}")
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexCredsDiffHost)
diff --git a/spec/bundler/support/artifice/compact_index_etag_match.rb b/spec/bundler/support/artifice/compact_index_etag_match.rb
new file mode 100644
index 0000000000..6c62166051
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_etag_match.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexEtagMatch < CompactIndexAPI
+ get "/versions" do
+ raise ArgumentError, "ETag header should be present" unless env["HTTP_IF_NONE_MATCH"]
+ headers "ETag" => env["HTTP_IF_NONE_MATCH"]
+ status 304
+ body ""
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexEtagMatch)
diff --git a/spec/bundler/support/artifice/compact_index_extra.rb b/spec/bundler/support/artifice/compact_index_extra.rb
new file mode 100644
index 0000000000..cd41b3ecca
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_extra.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index_extra"
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexExtra)
diff --git a/spec/bundler/support/artifice/compact_index_extra_api.rb b/spec/bundler/support/artifice/compact_index_extra_api.rb
new file mode 100644
index 0000000000..8b9d304ab4
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_extra_api.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index_extra_api"
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexExtraApi)
diff --git a/spec/bundler/support/artifice/compact_index_extra_api_missing.rb b/spec/bundler/support/artifice/compact_index_extra_api_missing.rb
new file mode 100644
index 0000000000..df6ede584c
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_extra_api_missing.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index_extra_api"
+
+class CompactIndexExtraAPIMissing < CompactIndexExtraApi
+ get "/extra/fetch/actual/gem/:id" do
+ if params[:id] == "missing-1.0.gemspec.rz"
+ halt 404
+ else
+ File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}")
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexExtraAPIMissing)
diff --git a/spec/bundler/support/artifice/compact_index_extra_missing.rb b/spec/bundler/support/artifice/compact_index_extra_missing.rb
new file mode 100644
index 0000000000..255c89afdb
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_extra_missing.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index_extra"
+
+class CompactIndexExtraMissing < CompactIndexExtra
+ get "/extra/fetch/actual/gem/:id" do
+ if params[:id] == "missing-1.0.gemspec.rz"
+ halt 404
+ else
+ File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}")
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexExtraMissing)
diff --git a/spec/bundler/support/artifice/compact_index_forbidden.rb b/spec/bundler/support/artifice/compact_index_forbidden.rb
new file mode 100644
index 0000000000..18c30ed9a2
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_forbidden.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexForbidden < CompactIndexAPI
+ get "/versions" do
+ halt 403
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexForbidden)
diff --git a/spec/bundler/support/artifice/compact_index_host_redirect.rb b/spec/bundler/support/artifice/compact_index_host_redirect.rb
new file mode 100644
index 0000000000..4f82bf3812
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_host_redirect.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexHostRedirect < CompactIndexAPI
+ get "/fetch/actual/gem/:id", host_name: "localgemserver.test" do
+ redirect "http://bundler.localgemserver.test#{request.path_info}"
+ end
+
+ get "/versions" do
+ status 404
+ end
+
+ get "/api/v1/dependencies" do
+ status 404
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexHostRedirect)
diff --git a/spec/bundler/support/artifice/compact_index_mirror_down.rb b/spec/bundler/support/artifice/compact_index_mirror_down.rb
new file mode 100644
index 0000000000..88983c715d
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_mirror_down.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+require_relative "helpers/artifice"
+require_relative "helpers/rack_request"
+
+module Artifice
+ module Net
+ class HTTPMirrorDown < HTTP
+ def connect
+ raise SocketError if address == "gem.mirror"
+
+ super
+ end
+ end
+
+ HTTP.endpoint = CompactIndexAPI
+ end
+
+ replace_net_http(Net::HTTPMirrorDown)
+end
diff --git a/spec/bundler/support/artifice/compact_index_no_checksums.rb b/spec/bundler/support/artifice/compact_index_no_checksums.rb
new file mode 100644
index 0000000000..ecb7fc7d7c
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_no_checksums.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexNoChecksums < CompactIndexAPI
+ get "/info/:name" do
+ etag_response do
+ gem = gems.find {|g| g.name == params[:name] }
+ gem.versions.map(&:number).join("\n")
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexNoChecksums)
diff --git a/spec/bundler/support/artifice/compact_index_no_gem.rb b/spec/bundler/support/artifice/compact_index_no_gem.rb
new file mode 100644
index 0000000000..71f6629688
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_no_gem.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexNoGem < CompactIndexAPI
+ get "/gems/:id" do
+ halt 500
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexNoGem)
diff --git a/spec/bundler/support/artifice/compact_index_partial_update.rb b/spec/bundler/support/artifice/compact_index_partial_update.rb
new file mode 100644
index 0000000000..f111d91ef9
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_partial_update.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexPartialUpdate < CompactIndexAPI
+ # Stub the server to never return 304s. This simulates the behaviour of
+ # Fastly / Rubygems ignoring ETag headers.
+ def not_modified?(_checksum)
+ false
+ end
+
+ get "/versions" do
+ cached_versions_path = File.join(
+ Bundler.rubygems.user_home, ".bundle", "cache", "compact_index",
+ "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions"
+ )
+
+ # Verify a cached copy of the versions file exists
+ unless File.binread(cached_versions_path).start_with?("created_at: ")
+ raise("Cached versions file should be present and have content")
+ end
+
+ # Verify that a partial request is made, starting from the index of the
+ # final byte of the cached file.
+ unless env["HTTP_RANGE"] == "bytes=#{File.binread(cached_versions_path).bytesize - 1}-"
+ raise("Range header should be present, and start from the index of the final byte of the cache. #{env["HTTP_RANGE"].inspect}")
+ end
+
+ etag_response do
+ # Return the exact contents of the cache.
+ File.binread(cached_versions_path)
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexPartialUpdate)
diff --git a/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb b/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb
new file mode 100644
index 0000000000..ac04336636
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+# The purpose of this Artifice is to test that an incremental response is invalidated
+# and a second request is issued for the full content.
+class CompactIndexPartialUpdateBadDigest < CompactIndexAPI
+ def partial_update_bad_digest
+ response_body = yield
+ if request.env["HTTP_RANGE"]
+ headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest("wrong digest on ranged request")}:"
+ else
+ headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest(response_body)}:"
+ end
+ headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60"
+ content_type "text/plain"
+ requested_range_for(response_body)
+ end
+
+ get "/versions" do
+ partial_update_bad_digest do
+ file = tmp("versions.list")
+ FileUtils.rm_f(file)
+ file = CompactIndex::VersionsFile.new(file.to_s)
+ file.create(gems)
+ file.contents([], calculate_info_checksums: true)
+ end
+ end
+
+ get "/info/:name" do
+ partial_update_bad_digest do
+ gem = gems.find {|g| g.name == params[:name] }
+ CompactIndex.info(gem ? gem.versions : [])
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexPartialUpdateBadDigest)
diff --git a/spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb b/spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb
new file mode 100644
index 0000000000..99bae039f0
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+# The purpose of this Artifice is to test that an incremental response is ignored
+# when the digest is not present to verify that the partial response is valid.
+class CompactIndexPartialUpdateNoDigestNotIncremental < CompactIndexAPI
+ def partial_update_no_digest
+ response_body = yield
+ headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60"
+ content_type "text/plain"
+ requested_range_for(response_body)
+ end
+
+ get "/versions" do
+ partial_update_no_digest do
+ file = tmp("versions.list")
+ FileUtils.rm_f(file)
+ file = CompactIndex::VersionsFile.new(file.to_s)
+ file.create(gems)
+ lines = file.contents([], calculate_info_checksums: true).split("\n")
+ name, versions, checksum = lines.last.split(" ")
+
+ # shuffle versions so new versions are not appended to the end
+ [*lines[0..-2], [name, versions.split(",").reverse.join(","), checksum].join(" ")].join("\n")
+ end
+ end
+
+ get "/info/:name" do
+ partial_update_no_digest do
+ gem = gems.find {|g| g.name == params[:name] }
+ lines = CompactIndex.info(gem ? gem.versions : []).split("\n")
+
+ # shuffle versions so new versions are not appended to the end
+ [lines.first, lines.last, *lines[1..-2]].join("\n")
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexPartialUpdateNoDigestNotIncremental)
diff --git a/spec/bundler/support/artifice/compact_index_precompiled_before.rb b/spec/bundler/support/artifice/compact_index_precompiled_before.rb
new file mode 100644
index 0000000000..b5f72f546a
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_precompiled_before.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexPrecompiledBefore < CompactIndexAPI
+ get "/info/:name" do
+ etag_response do
+ gem = gems.find {|g| g.name == params[:name] }
+ move_ruby_variant_to_the_end(CompactIndex.info(gem ? gem.versions : []))
+ end
+ end
+
+ private
+
+ def move_ruby_variant_to_the_end(response)
+ lines = response.split("\n")
+ ruby = lines.find {|line| /\A\d+\.\d+\.\d* \|/.match(line) }
+ lines.delete(ruby)
+ lines.push(ruby).join("\n")
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexPrecompiledBefore)
diff --git a/spec/bundler/support/artifice/compact_index_range_ignored.rb b/spec/bundler/support/artifice/compact_index_range_ignored.rb
new file mode 100644
index 0000000000..2303682c1f
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_range_ignored.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexRangeIgnored < CompactIndexAPI
+ # Stub the server to not return 304 so that we don't bypass all the logic
+ def not_modified?(_checksum)
+ false
+ end
+
+ get "/versions" do
+ cached_versions_path = File.join(
+ Bundler.rubygems.user_home, ".bundle", "cache", "compact_index",
+ "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions"
+ )
+
+ # Verify a cached copy of the versions file exists
+ unless File.binread(cached_versions_path).size > 0
+ raise("Cached versions file should be present and have content")
+ end
+
+ # Verify that a partial request is made, starting from the index of the
+ # final byte of the cached file.
+ unless env.delete("HTTP_RANGE")
+ raise("Expected client to write the full response on the first try")
+ end
+
+ etag_response do
+ file = tmp("versions.list")
+ FileUtils.rm_f(file)
+ file = CompactIndex::VersionsFile.new(file.to_s)
+ file.create(gems)
+ file.contents
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexRangeIgnored)
diff --git a/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb b/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb
new file mode 100644
index 0000000000..8a7c4b79b0
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexRangeNotSatisfiable < CompactIndexAPI
+ get "/versions" do
+ if env["HTTP_RANGE"]
+ status 416
+ else
+ etag_response do
+ file = tmp("versions.list")
+ FileUtils.rm_f(file)
+ file = CompactIndex::VersionsFile.new(file.to_s)
+ file.create(gems)
+ file.contents
+ end
+ end
+ end
+
+ get "/info/:name" do
+ if env["HTTP_RANGE"]
+ status 416
+ else
+ etag_response do
+ gem = gems.find {|g| g.name == params[:name] }
+ CompactIndex.info(gem ? gem.versions : [])
+ end
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexRangeNotSatisfiable)
diff --git a/spec/bundler/support/artifice/compact_index_rate_limited.rb b/spec/bundler/support/artifice/compact_index_rate_limited.rb
new file mode 100644
index 0000000000..4495491635
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_rate_limited.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexRateLimited < CompactIndexAPI
+ class RequestCounter
+ def self.queue
+ @queue ||= Thread::Queue.new
+ end
+
+ def self.size
+ @queue.size
+ end
+
+ def self.enq(name)
+ @queue.enq(name)
+ end
+
+ def self.deq
+ @queue.deq
+ end
+ end
+
+ configure do
+ RequestCounter.queue
+ end
+
+ get "/info/:name" do
+ RequestCounter.enq(params[:name])
+
+ begin
+ if RequestCounter.size == 1
+ etag_response do
+ gem = gems.find {|g| g.name == params[:name] }
+ CompactIndex.info(gem ? gem.versions : [])
+ end
+ else
+ status 429
+ end
+ ensure
+ RequestCounter.deq
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexRateLimited)
diff --git a/spec/bundler/support/artifice/compact_index_redirects.rb b/spec/bundler/support/artifice/compact_index_redirects.rb
new file mode 100644
index 0000000000..f7ba393239
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_redirects.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexRedirect < CompactIndexAPI
+ get "/fetch/actual/gem/:id" do
+ redirect "/fetch/actual/gem/#{params[:id]}"
+ end
+
+ get "/versions" do
+ status 404
+ end
+
+ get "/api/v1/dependencies" do
+ status 404
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexRedirect)
diff --git a/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb
new file mode 100644
index 0000000000..96259385e7
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexStrictBasicAuthentication < CompactIndexAPI
+ before do
+ unless env["HTTP_AUTHORIZATION"]
+ halt 401, "Authentication info not supplied"
+ end
+
+ # Only accepts password == "password"
+ unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz"
+ halt 401, "Authentication failed"
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexStrictBasicAuthentication)
diff --git a/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb
new file mode 100644
index 0000000000..15850599b6
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexWrongDependencies < CompactIndexAPI
+ get "/info/:name" do
+ etag_response do
+ gem = gems.find {|g| g.name == params[:name] }
+ gem.versions.each {|gv| gv.dependencies.clear } if gem
+ CompactIndex.info(gem ? gem.versions : [])
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexWrongDependencies)
diff --git a/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb
new file mode 100644
index 0000000000..9bd2ca0a9d
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexWrongGemChecksum < CompactIndexAPI
+ get "/info/:name" do
+ etag_response do
+ name = params[:name]
+ gem = gems.find {|g| g.name == name }
+ # This generates the hexdigest "2222222222222222222222222222222222222222222222222222222222222222"
+ checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") { "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI=" }
+ versions = gem ? gem.versions : []
+ versions.each {|v| v.checksum = checksum }
+ CompactIndex.info(versions)
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexWrongGemChecksum)
diff --git a/spec/bundler/support/artifice/endpoint.rb b/spec/bundler/support/artifice/endpoint.rb
new file mode 100644
index 0000000000..15242a7942
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+require_relative "helpers/artifice"
+
+Artifice.activate_with(Endpoint)
diff --git a/spec/bundler/support/artifice/endpoint_500.rb b/spec/bundler/support/artifice/endpoint_500.rb
new file mode 100644
index 0000000000..9dd373bbf6
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_500.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "../path"
+
+$LOAD_PATH.unshift(*Spec::Path.sinatra_dependency_paths)
+
+require "sinatra/base"
+
+class Endpoint500 < Sinatra::Base
+ before do
+ halt 500
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(Endpoint500)
diff --git a/spec/bundler/support/artifice/endpoint_api_forbidden.rb b/spec/bundler/support/artifice/endpoint_api_forbidden.rb
new file mode 100644
index 0000000000..6bdc5896d6
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_api_forbidden.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointApiForbidden < Endpoint
+ get "/api/v1/dependencies" do
+ halt 403
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointApiForbidden)
diff --git a/spec/bundler/support/artifice/endpoint_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_basic_authentication.rb
new file mode 100644
index 0000000000..e8e3569e63
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_basic_authentication.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointBasicAuthentication < Endpoint
+ before do
+ unless env["HTTP_AUTHORIZATION"]
+ halt 401, "Authentication info not supplied"
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointBasicAuthentication)
diff --git a/spec/bundler/support/artifice/endpoint_creds_diff_host.rb b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb
new file mode 100644
index 0000000000..9cbb4de61a
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointCredsDiffHost < Endpoint
+ helpers do
+ def auth
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
+ end
+
+ def authorized?
+ auth.provided? && auth.basic? && auth.credentials && auth.credentials == %w[user pass]
+ end
+
+ def protected!
+ return if authorized?
+ response["WWW-Authenticate"] = %(Basic realm="Testing HTTP Auth")
+ throw(:halt, [401, "Not authorized\n"])
+ end
+ end
+
+ before do
+ protected! unless request.path_info.include?("/no/creds/")
+ end
+
+ get "/gems/:id" do
+ redirect "http://diffhost.test/no/creds/#{params[:id]}"
+ end
+
+ get "/no/creds/:id" do
+ if request.host.include?("diffhost") && !auth.provided?
+ File.binread("#{gem_repo1}/gems/#{params[:id]}")
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointCredsDiffHost)
diff --git a/spec/bundler/support/artifice/endpoint_extra.rb b/spec/bundler/support/artifice/endpoint_extra.rb
new file mode 100644
index 0000000000..021fd435fe
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_extra.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointExtra < Endpoint
+ get "/extra/api/v1/dependencies" do
+ halt 404
+ end
+
+ get "/extra/specs.4.8.gz" do
+ File.binread("#{gem_repo2}/specs.4.8.gz")
+ end
+
+ get "/extra/prerelease_specs.4.8.gz" do
+ File.binread("#{gem_repo2}/prerelease_specs.4.8.gz")
+ end
+
+ get "/extra/quick/Marshal.4.8/:id" do
+ redirect "/extra/fetch/actual/gem/#{params[:id]}"
+ end
+
+ get "/extra/fetch/actual/gem/:id" do
+ File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}")
+ end
+
+ get "/extra/gems/:id" do
+ File.binread("#{gem_repo2}/gems/#{params[:id]}")
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointExtra)
diff --git a/spec/bundler/support/artifice/endpoint_extra_api.rb b/spec/bundler/support/artifice/endpoint_extra_api.rb
new file mode 100644
index 0000000000..a965af6e73
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_extra_api.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointExtraApi < Endpoint
+ get "/extra/api/v1/dependencies" do
+ deps = dependencies_for(params[:gems], gem_repo4)
+ Marshal.dump(deps)
+ end
+
+ get "/extra/specs.4.8.gz" do
+ File.binread("#{gem_repo4}/specs.4.8.gz")
+ end
+
+ get "/extra/prerelease_specs.4.8.gz" do
+ File.binread("#{gem_repo4}/prerelease_specs.4.8.gz")
+ end
+
+ get "/extra/quick/Marshal.4.8/:id" do
+ redirect "/extra/fetch/actual/gem/#{params[:id]}"
+ end
+
+ get "/extra/fetch/actual/gem/:id" do
+ File.binread("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}")
+ end
+
+ get "/extra/gems/:id" do
+ File.binread("#{gem_repo4}/gems/#{params[:id]}")
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointExtraApi)
diff --git a/spec/bundler/support/artifice/endpoint_extra_missing.rb b/spec/bundler/support/artifice/endpoint_extra_missing.rb
new file mode 100644
index 0000000000..73e2defb32
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_extra_missing.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint_extra"
+
+class EndpointExtraMissing < EndpointExtra
+ get "/extra/fetch/actual/gem/:id" do
+ if params[:id] == "missing-1.0.gemspec.rz"
+ halt 404
+ else
+ File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}")
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointExtraMissing)
diff --git a/spec/bundler/support/artifice/endpoint_fallback.rb b/spec/bundler/support/artifice/endpoint_fallback.rb
new file mode 100644
index 0000000000..742e563f07
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_fallback.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointFallback < Endpoint
+ DEPENDENCY_LIMIT = 60
+
+ get "/api/v1/dependencies" do
+ if params[:gems] && params[:gems].size <= DEPENDENCY_LIMIT
+ Marshal.dump(dependencies_for(params[:gems]))
+ else
+ halt 413, "Too many gems to resolve, please request less than #{DEPENDENCY_LIMIT} gems"
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointFallback)
diff --git a/spec/bundler/support/artifice/endpoint_host_redirect.rb b/spec/bundler/support/artifice/endpoint_host_redirect.rb
new file mode 100644
index 0000000000..6ce51bed93
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_host_redirect.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointHostRedirect < Endpoint
+ get "/fetch/actual/gem/:id", host_name: "localgemserver.test" do
+ redirect "http://bundler.localgemserver.test#{request.path_info}"
+ end
+
+ get "/api/v1/dependencies" do
+ status 404
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointHostRedirect)
diff --git a/spec/bundler/support/artifice/endpoint_marshal_fail.rb b/spec/bundler/support/artifice/endpoint_marshal_fail.rb
new file mode 100644
index 0000000000..74ce321de6
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_marshal_fail.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint_marshal_fail"
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointMarshalFail)
diff --git a/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb
new file mode 100644
index 0000000000..ea4cfbe965
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint_marshal_fail"
+
+class EndpointMarshalFailBasicAuthentication < EndpointMarshalFail
+ before do
+ unless env["HTTP_AUTHORIZATION"]
+ halt 401, "Authentication info not supplied"
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointMarshalFailBasicAuthentication)
diff --git a/spec/bundler/support/artifice/endpoint_mirror_source.rb b/spec/bundler/support/artifice/endpoint_mirror_source.rb
new file mode 100644
index 0000000000..fed7a746b9
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_mirror_source.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointMirrorSource < Endpoint
+ get "/gems/:id" do
+ if request.env["HTTP_X_GEMFILE_SOURCE"] == "https://server.example.org/" && request.env["HTTP_USER_AGENT"].start_with?("bundler")
+ File.binread("#{gem_repo1}/gems/#{params[:id]}")
+ else
+ halt 500
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointMirrorSource)
diff --git a/spec/bundler/support/artifice/endpoint_redirect.rb b/spec/bundler/support/artifice/endpoint_redirect.rb
new file mode 100644
index 0000000000..84f546ba9d
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_redirect.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointRedirect < Endpoint
+ get "/fetch/actual/gem/:id" do
+ redirect "/fetch/actual/gem/#{params[:id]}"
+ end
+
+ get "/api/v1/dependencies" do
+ status 404
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointRedirect)
diff --git a/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb
new file mode 100644
index 0000000000..dff360c5c5
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointStrictBasicAuthentication < Endpoint
+ before do
+ unless env["HTTP_AUTHORIZATION"]
+ halt 401, "Authentication info not supplied"
+ end
+
+ # Only accepts password == "password"
+ unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz"
+ halt 401, "Authentication failed"
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointStrictBasicAuthentication)
diff --git a/spec/bundler/support/artifice/endpoint_timeout.rb b/spec/bundler/support/artifice/endpoint_timeout.rb
new file mode 100644
index 0000000000..86b793e499
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_timeout.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint_fallback"
+
+class EndpointTimeout < EndpointFallback
+ SLEEP_TIMEOUT = 3
+
+ get "/api/v1/dependencies" do
+ sleep(SLEEP_TIMEOUT)
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointTimeout)
diff --git a/spec/bundler/support/artifice/fail.rb b/spec/bundler/support/artifice/fail.rb
new file mode 100644
index 0000000000..5ddbc4e590
--- /dev/null
+++ b/spec/bundler/support/artifice/fail.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require_relative "../vendored_net_http"
+
+class Fail < Gem::Net::HTTP
+ # Gem::Net::HTTP uses a @newimpl instance variable to decide whether
+ # to use a legacy implementation. Since we are subclassing
+ # Gem::Net::HTTP, we must set it
+ @newimpl = true
+
+ def request(req, body = nil, &block)
+ raise(exception(req))
+ end
+
+ # Ensure we don't start a connect here
+ def connect
+ end
+
+ def exception(req)
+ Errno::ENETUNREACH.new("host down: Bundler spec artifice fail! #{req["PATH_INFO"]}")
+ end
+end
+
+require_relative "helpers/artifice"
+
+# Replace Gem::Net::HTTP with our failing subclass
+Artifice.replace_net_http(::Fail)
diff --git a/spec/bundler/support/artifice/helpers/artifice.rb b/spec/bundler/support/artifice/helpers/artifice.rb
new file mode 100644
index 0000000000..788268295c
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/artifice.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+# This module was initially borrowed from https://github.com/wycats/artifice
+module Artifice
+ # Activate Artifice with a particular Rack endpoint.
+ #
+ # Calling this method will replace the Gem::Net::HTTP system
+ # with a replacement that routes all requests to the
+ # Rack endpoint.
+ #
+ # @param [#call] endpoint A valid Rack endpoint
+ def self.activate_with(endpoint)
+ require_relative "rack_request"
+
+ Net::HTTP.endpoint = endpoint
+ replace_net_http(Artifice::Net::HTTP)
+ end
+
+ # Deactivate the Artifice replacement.
+ def self.deactivate
+ replace_net_http(::Gem::Net::HTTP)
+ end
+
+ def self.replace_net_http(value)
+ ::Gem::Net.class_eval do
+ remove_const(:HTTP)
+ const_set(:HTTP, value)
+ end
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/compact_index.rb b/spec/bundler/support/artifice/helpers/compact_index.rb
new file mode 100644
index 0000000000..e684aa8628
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/compact_index.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require_relative "endpoint"
+
+$LOAD_PATH.unshift Spec::Path.tmp_root.join("compact_index/lib").to_s
+require "compact_index"
+require "digest"
+
+class CompactIndexAPI < Endpoint
+ helpers do
+ include Spec::Path
+
+ def load_spec(name, version, platform, gem_repo)
+ full_name = "#{name}-#{version}"
+ full_name += "-#{platform}" if platform != "ruby"
+ Marshal.load(Bundler.rubygems.inflate(File.binread(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz"))))
+ end
+
+ def etag_response
+ response_body = yield
+ etag = Digest::MD5.hexdigest(response_body)
+ headers "ETag" => quote(etag)
+ return if not_modified?(etag)
+ headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest(response_body)}:"
+ headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60"
+ content_type "text/plain"
+ requested_range_for(response_body)
+ rescue StandardError => e
+ puts e
+ puts e.backtrace
+ raise
+ end
+
+ def not_modified?(etag)
+ etags = parse_etags(request.env["HTTP_IF_NONE_MATCH"])
+
+ return unless etags.include?(etag)
+ status 304
+ body ""
+ end
+
+ def requested_range_for(response_body)
+ ranges = Rack::Utils.get_byte_ranges(env["HTTP_RANGE"], response_body.bytesize)
+
+ if ranges
+ status 206
+ body ranges.map! {|range| slice_body(response_body, range) }.join
+ else
+ status 200
+ body response_body
+ end
+ end
+
+ def quote(string)
+ %("#{string}")
+ end
+
+ def parse_etags(value)
+ value ? value.split(/, ?/).select {|s| s.sub!(/"(.*)"/, '\1') } : []
+ end
+
+ def slice_body(body, range)
+ body.byteslice(range)
+ end
+
+ def gems(gem_repo = default_gem_repo)
+ @gems ||= {}
+ @gems[gem_repo] ||= begin
+ specs = Bundler::Deprecate.skip_during do
+ %w[specs.4.8 prerelease_specs.4.8].flat_map do |filename|
+ spec_index = gem_repo.join(filename)
+ next [] unless File.exist?(spec_index)
+
+ Marshal.load(File.binread(spec_index)).map do |name, version, platform|
+ load_spec(name, version, platform, gem_repo)
+ end
+ end
+ end
+
+ specs.group_by(&:name).map do |name, versions|
+ gem_versions = versions.map do |spec|
+ deps = spec.runtime_dependencies.map do |d|
+ reqs = d.requirement.requirements.map {|r| r.join(" ") }.join(", ")
+ CompactIndex::Dependency.new(d.name, reqs)
+ end
+ begin
+ checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") do
+ Digest(:SHA256).file("#{gem_repo}/gems/#{spec.original_name}.gem").hexdigest
+ end
+ rescue StandardError
+ checksum = nil
+ end
+ build_gem_version(spec, deps, checksum)
+ end
+ CompactIndex::Gem.new(name, gem_versions)
+ end
+ end
+ end
+
+ def build_gem_version(spec, deps, checksum)
+ CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, checksum, nil,
+ deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s)
+ end
+ end
+
+ get "/names" do
+ etag_response do
+ CompactIndex.names(gems.map(&:name))
+ end
+ end
+
+ get "/versions" do
+ etag_response do
+ file = tmp("versions.list")
+ FileUtils.rm_f(file)
+ file = CompactIndex::VersionsFile.new(file.to_s)
+ file.create(gems)
+ file.contents
+ end
+ end
+
+ get "/info/:name" do
+ etag_response do
+ gem = gems.find {|g| g.name == params[:name] }
+ CompactIndex.info(gem ? gem.versions : [])
+ end
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/compact_index_cooldown.rb b/spec/bundler/support/artifice/helpers/compact_index_cooldown.rb
new file mode 100644
index 0000000000..9920fd2c95
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/compact_index_cooldown.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require_relative "compact_index"
+
+class CompactIndexCooldownAPI < CompactIndexAPI
+ helpers do
+ def build_gem_version(spec, deps, checksum)
+ created_at = spec.date&.utc&.iso8601
+ CompactIndex::GemVersionV2.new(spec.version.version, spec.platform.to_s, checksum, nil,
+ deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s, created_at)
+ end
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/compact_index_extra.rb b/spec/bundler/support/artifice/helpers/compact_index_extra.rb
new file mode 100644
index 0000000000..9e742630dd
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/compact_index_extra.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require_relative "compact_index"
+
+class CompactIndexExtra < CompactIndexAPI
+ get "/extra/versions" do
+ halt 404
+ end
+
+ get "/extra/api/v1/dependencies" do
+ halt 404
+ end
+
+ get "/extra/specs.4.8.gz" do
+ File.binread("#{gem_repo2}/specs.4.8.gz")
+ end
+
+ get "/extra/prerelease_specs.4.8.gz" do
+ File.binread("#{gem_repo2}/prerelease_specs.4.8.gz")
+ end
+
+ get "/extra/quick/Marshal.4.8/:id" do
+ redirect "/extra/fetch/actual/gem/#{params[:id]}"
+ end
+
+ get "/extra/fetch/actual/gem/:id" do
+ File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}")
+ end
+
+ get "/extra/gems/:id" do
+ File.binread("#{gem_repo2}/gems/#{params[:id]}")
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb b/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb
new file mode 100644
index 0000000000..d9a7d83d23
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require_relative "compact_index"
+
+class CompactIndexExtraApi < CompactIndexAPI
+ get "/extra/names" do
+ etag_response do
+ CompactIndex.names(gems(gem_repo4).map(&:name))
+ end
+ end
+
+ get "/extra/versions" do
+ etag_response do
+ file = tmp("versions.list")
+ FileUtils.rm_f(file)
+ file = CompactIndex::VersionsFile.new(file.to_s)
+ file.create(gems(gem_repo4))
+ file.contents
+ end
+ end
+
+ get "/extra/info/:name" do
+ etag_response do
+ gem = gems(gem_repo4).find {|g| g.name == params[:name] }
+ CompactIndex.info(gem ? gem.versions : [])
+ end
+ end
+
+ get "/extra/specs.4.8.gz" do
+ File.binread("#{gem_repo4}/specs.4.8.gz")
+ end
+
+ get "/extra/prerelease_specs.4.8.gz" do
+ File.binread("#{gem_repo4}/prerelease_specs.4.8.gz")
+ end
+
+ get "/extra/quick/Marshal.4.8/:id" do
+ redirect "/extra/fetch/actual/gem/#{params[:id]}"
+ end
+
+ get "/extra/fetch/actual/gem/:id" do
+ File.binread("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}")
+ end
+
+ get "/extra/gems/:id" do
+ File.binread("#{gem_repo4}/gems/#{params[:id]}")
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/endpoint.rb b/spec/bundler/support/artifice/helpers/endpoint.rb
new file mode 100644
index 0000000000..9590611dfe
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/endpoint.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require_relative "../../path"
+
+$LOAD_PATH.unshift(*Spec::Path.sinatra_dependency_paths)
+
+require "sinatra/base"
+
+ALL_REQUESTS = [] # rubocop:disable Style/MutableConstant
+ALL_REQUESTS_MUTEX = Thread::Mutex.new
+
+at_exit do
+ if expected = ENV["BUNDLER_SPEC_ALL_REQUESTS"]
+ expected = expected.split("\n").sort
+ actual = ALL_REQUESTS.sort
+
+ unless expected == actual
+ raise "Unexpected requests!\nExpected:\n\t#{expected.join("\n\t")}\n\nActual:\n\t#{actual.join("\n\t")}"
+ end
+ end
+end
+
+class Endpoint < Sinatra::Base
+ def self.all_requests
+ @all_requests ||= []
+ end
+
+ set :raise_errors, true
+ set :show_exceptions, false
+ set :host_authorization, permitted_hosts: [".example.org", ".local", ".mirror", ".repo", ".repo1", ".repo2", ".repo3", ".repo4", ".rubygems.org", ".security", ".source", ".test", "127.0.0.1"]
+
+ def call!(*)
+ super.tap do
+ ALL_REQUESTS_MUTEX.synchronize do
+ ALL_REQUESTS << @request.url
+ end
+ end
+ end
+
+ helpers do
+ include Spec::Path
+
+ def default_gem_repo
+ if ENV["BUNDLER_SPEC_GEM_REPO"]
+ Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"])
+ else
+ case request.host
+ when "gem.repo1"
+ Spec::Path.gem_repo1
+ when "gem.repo2"
+ Spec::Path.gem_repo2
+ when "gem.repo3"
+ Spec::Path.gem_repo3
+ when "gem.repo4"
+ Spec::Path.gem_repo4
+ else
+ Spec::Path.gem_repo1
+ end
+ end
+ end
+
+ def dependencies_for(gem_names, gem_repo = default_gem_repo)
+ return [] if gem_names.nil? || gem_names.empty?
+
+ all_specs = %w[specs.4.8 prerelease_specs.4.8].map do |filename|
+ Marshal.load(File.binread(gem_repo.join(filename)))
+ end.inject(:+)
+
+ all_specs.filter_map do |name, version, platform|
+ spec = load_spec(name, version, platform, gem_repo)
+ next unless gem_names.include?(spec.name)
+ {
+ name: spec.name,
+ number: spec.version.version,
+ platform: spec.platform.to_s,
+ dependencies: spec.runtime_dependencies.map do |dep|
+ [dep.name, dep.requirement.requirements.map {|a| a.join(" ") }.join(", ")]
+ end,
+ }
+ end
+ end
+
+ def load_spec(name, version, platform, gem_repo)
+ full_name = "#{name}-#{version}"
+ full_name += "-#{platform}" if platform != "ruby"
+ Marshal.load(Bundler.rubygems.inflate(File.binread(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz"))))
+ end
+ end
+
+ get "/quick/Marshal.4.8/:id" do
+ redirect "/fetch/actual/gem/#{params[:id]}"
+ end
+
+ get "/fetch/actual/gem/:id" do
+ File.binread("#{default_gem_repo}/quick/Marshal.4.8/#{params[:id]}")
+ end
+
+ get "/gems/:id" do
+ File.binread("#{default_gem_repo}/gems/#{params[:id]}")
+ end
+
+ get "/api/v1/dependencies" do
+ Marshal.dump(dependencies_for(params[:gems]))
+ end
+
+ get "/specs.4.8.gz" do
+ File.binread("#{default_gem_repo}/specs.4.8.gz")
+ end
+
+ get "/prerelease_specs.4.8.gz" do
+ File.binread("#{default_gem_repo}/prerelease_specs.4.8.gz")
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/endpoint_extra.rb b/spec/bundler/support/artifice/helpers/endpoint_extra.rb
new file mode 100644
index 0000000000..ad08495b50
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/endpoint_extra.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require_relative "endpoint"
+
+class EndpointExtra < Endpoint
+ get "/extra/api/v1/dependencies" do
+ halt 404
+ end
+
+ get "/extra/specs.4.8.gz" do
+ File.binread("#{gem_repo2}/specs.4.8.gz")
+ end
+
+ get "/extra/prerelease_specs.4.8.gz" do
+ File.binread("#{gem_repo2}/prerelease_specs.4.8.gz")
+ end
+
+ get "/extra/quick/Marshal.4.8/:id" do
+ redirect "/extra/fetch/actual/gem/#{params[:id]}"
+ end
+
+ get "/extra/fetch/actual/gem/:id" do
+ File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}")
+ end
+
+ get "/extra/gems/:id" do
+ File.binread("#{gem_repo2}/gems/#{params[:id]}")
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/endpoint_fallback.rb b/spec/bundler/support/artifice/helpers/endpoint_fallback.rb
new file mode 100644
index 0000000000..a232930b67
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/endpoint_fallback.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require_relative "endpoint"
+
+class EndpointFallback < Endpoint
+ DEPENDENCY_LIMIT = 60
+
+ get "/api/v1/dependencies" do
+ if params[:gems] && params[:gems].size <= DEPENDENCY_LIMIT
+ Marshal.dump(dependencies_for(params[:gems]))
+ else
+ halt 413, "Too many gems to resolve, please request less than #{DEPENDENCY_LIMIT} gems"
+ end
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb b/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb
new file mode 100644
index 0000000000..c409d39d99
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require_relative "endpoint_fallback"
+
+class EndpointMarshalFail < EndpointFallback
+ get "/api/v1/dependencies" do
+ "f0283y01hasf"
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/rack_request.rb b/spec/bundler/support/artifice/helpers/rack_request.rb
new file mode 100644
index 0000000000..05ff034463
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/rack_request.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require "rack/test"
+require_relative "../../vendored_net_http"
+
+module Artifice
+ module Net
+ # This is an internal object that can receive Rack requests
+ # to the application using the Rack::Test API
+ class RackRequest
+ include Rack::Test::Methods
+ attr_reader :app
+
+ def initialize(app)
+ @app = app
+ end
+ end
+
+ class HTTP < ::Gem::Net::HTTP
+ class << self
+ attr_accessor :endpoint
+ end
+
+ # Gem::Net::HTTP uses a @newimpl instance variable to decide whether
+ # to use a legacy implementation. Since we are subclassing
+ # Gem::Net::HTTP, we must set it
+ @newimpl = true
+
+ # We don't need to connect, so blank out this method
+ def connect
+ end
+
+ # Replace the Gem::Net::HTTP request method with a method
+ # that converts the request into a Rack request and
+ # dispatches it to the Rack endpoint.
+ #
+ # @param [Net::HTTPRequest] req A Gem::Net::HTTPRequest
+ # object, or one if its subclasses
+ # @param [optional, String, #read] body This should
+ # be sent as "rack.input". If it's a String, it will
+ # be converted to a StringIO.
+ # @return [Net::HTTPResponse]
+ #
+ # @yield [Net::HTTPResponse] If a block is provided,
+ # this method will yield the Gem::Net::HTTPResponse to
+ # it after the body is read.
+ def request(req, body = nil, &block)
+ rack_request = RackRequest.new(self.class.endpoint)
+
+ req.each_header do |header, value|
+ rack_request.header(header, value)
+ end
+
+ scheme = use_ssl? ? "https" : "http"
+ prefix = "#{scheme}://#{addr_port}"
+ body_stream_contents = req.body_stream.read if req.body_stream
+
+ response = rack_request.request("#{prefix}#{req.path}",
+ { method: req.method, input: body || req.body || body_stream_contents })
+
+ make_net_http_response(response, &block)
+ end
+
+ private
+
+ # This method takes a Rack response and creates a Gem::Net::HTTPResponse
+ # Instead of trying to mock HTTPResponse directly, we just convert
+ # the Rack response into a String that looks like a normal HTTP
+ # response and call Gem::Net::HTTPResponse.read_new
+ #
+ # @param [Array(#to_i, Hash, #each)] response a Rack response
+ # @return [Net::HTTPResponse]
+ # @yield [Net::HTTPResponse] If a block is provided, yield the
+ # response to it after the body is read
+ def make_net_http_response(response)
+ status = response.status
+ headers = response.headers
+ body = response.body
+
+ response_string = []
+ response_string << "HTTP/1.1 #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}"
+
+ headers.each do |header, value|
+ response_string << "#{header}: #{value}"
+ end
+
+ response_string << "" << body
+
+ response_io = ::Gem::Net::BufferedIO.new(StringIO.new(response_string.join("\n")))
+ res = ::Gem::Net::HTTPResponse.read_new(response_io)
+
+ res.reading_body(response_io, true) do
+ yield res if block_given?
+ end
+
+ res
+ end
+ end
+ end
+end
diff --git a/spec/bundler/support/artifice/vcr.rb b/spec/bundler/support/artifice/vcr.rb
new file mode 100644
index 0000000000..0bf5ade8f6
--- /dev/null
+++ b/spec/bundler/support/artifice/vcr.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+
+require_relative "../vendored_net_http"
+require_relative "../path"
+
+CASSETTE_PATH = "#{Spec::Path.spec_dir}/support/artifice/vcr_cassettes".freeze
+USED_CASSETTES_PATH = "#{Spec::Path.spec_dir}/support/artifice/used_cassettes.txt".freeze
+CASSETTE_NAME = ENV.fetch("BUNDLER_SPEC_VCR_CASSETTE_NAME") { "realworld" }
+
+class BundlerVCRHTTP < Gem::Net::HTTP
+ class RequestHandler
+ attr_reader :http, :request, :body, :response_block
+ def initialize(http, request, body = nil, &response_block)
+ @http = http
+ @request = request
+ @body = body
+ @response_block = response_block
+ end
+
+ def handle_request
+ handler = self
+ request.instance_eval do
+ @__vcr_request_handler = handler
+ end
+
+ File.open(USED_CASSETTES_PATH, "a+") do |f|
+ f.puts request_pair_paths.map {|path| Pathname.new(path).relative_path_from(Spec::Path.git_root).to_s }.join("\n")
+ end
+
+ if recorded_response?
+ recorded_response
+ else
+ record_response
+ end
+ end
+
+ def recorded_response?
+ return true if ENV["BUNDLER_SPEC_PRE_RECORDED"]
+ request_pair_paths.all? {|f| File.exist?(f) }
+ end
+
+ def recorded_response
+ File.open(request_pair_paths.last, "rb:ASCII-8BIT") do |response_file|
+ response_io = ::Gem::Net::BufferedIO.new(response_file)
+ ::Gem::Net::HTTPResponse.read_new(response_io).tap do |response|
+ response.decode_content = request.decode_content if request.respond_to?(:decode_content)
+ response.uri = request.uri
+
+ response.reading_body(response_io, request.response_body_permitted?) do
+ response_block&.call(response)
+ end
+ end
+ end
+ end
+
+ def record_response
+ request_path, response_path = *request_pair_paths
+
+ @recording = true
+
+ response = http.request_without_vcr(request, body, &response_block)
+ @recording = false
+ unless @recording
+ require "fileutils"
+ FileUtils.mkdir_p(File.dirname(request_path))
+ binwrite(request_path, request_to_string(request))
+ binwrite(response_path, response_to_string(response))
+ end
+ response
+ end
+
+ def key
+ [request["host"] || http.address, request.path, request.method].compact
+ end
+
+ def file_name_for_key(key)
+ File.join(*key).gsub(/[\:*?"<>|]/, "-")
+ end
+
+ def request_pair_paths
+ %w[request response].map do |kind|
+ File.join(CASSETTE_PATH, CASSETTE_NAME, file_name_for_key(key), kind)
+ end
+ end
+
+ def request_to_string(request)
+ request_string = []
+ request_string << "> #{request.method.upcase} #{request.path}"
+ request.to_hash.each do |key, value|
+ request_string << "> #{key}: #{Array(value).first}"
+ end
+ request << "" << request.body if request.body
+ request_string.join("\n")
+ end
+
+ def response_to_string(response)
+ headers = response.to_hash
+ body = response.body
+
+ response_string = []
+ response_string << "HTTP/1.1 #{response.code} #{response.message}"
+
+ headers["content-length"] = [body.bytesize.to_s] if body
+
+ headers.each do |header, value|
+ response_string << "#{header}: #{value.join(", ")}"
+ end
+
+ response_string << "" << body
+
+ response_string = response_string.join("\n")
+ if response_string.respond_to?(:force_encoding)
+ response_string.force_encoding("ASCII-8BIT")
+ else
+ response_string
+ end
+ end
+
+ def binwrite(path, contents)
+ File.open(path, "wb:ASCII-8BIT") {|f| f.write(contents) }
+ end
+ end
+
+ def start_with_vcr
+ if ENV["BUNDLER_SPEC_PRE_RECORDED"]
+ raise IOError, "HTTP session already opened" if @started
+ @socket = nil
+ @started = true
+ else
+ start_without_vcr
+ end
+ end
+
+ alias_method :start_without_vcr, :start
+ alias_method :start, :start_with_vcr
+
+ def request_with_vcr(request, *args, &block)
+ handler = request.instance_eval do
+ remove_instance_variable(:@__vcr_request_handler) if defined?(@__vcr_request_handler)
+ end || RequestHandler.new(self, request, *args, &block)
+
+ handler.handle_request
+ end
+
+ alias_method :request_without_vcr, :request
+ alias_method :request, :request_with_vcr
+end
+
+require_relative "helpers/artifice"
+
+# Replace Gem::Net::HTTP with our VCR subclass
+Artifice.replace_net_http(BundlerVCRHTTP)
diff --git a/spec/bundler/support/artifice/windows.rb b/spec/bundler/support/artifice/windows.rb
new file mode 100644
index 0000000000..3056540beb
--- /dev/null
+++ b/spec/bundler/support/artifice/windows.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require_relative "../path"
+
+$LOAD_PATH.unshift(*Spec::Path.sinatra_dependency_paths)
+
+require "sinatra/base"
+
+class Windows < Sinatra::Base
+ set :raise_errors, true
+ set :show_exceptions, false
+
+ helpers do
+ def default_gem_repo
+ Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"] || Spec::Path.gem_repo1)
+ end
+ end
+
+ files = ["specs.4.8.gz",
+ "prerelease_specs.4.8.gz",
+ "quick/Marshal.4.8/rcov-1.0-mswin32.gemspec.rz",
+ "gems/rcov-1.0-mswin32.gem"]
+
+ files.each do |file|
+ get "/#{file}" do
+ File.binread default_gem_repo.join(file)
+ end
+ end
+
+ get "/gems/rcov-1.0-x86-mswin32.gem" do
+ halt 404
+ end
+
+ get "/api/v1/dependencies" do
+ halt 404
+ end
+
+ get "/versions" do
+ halt 500
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(Windows)
diff --git a/spec/bundler/support/build_metadata.rb b/spec/bundler/support/build_metadata.rb
new file mode 100644
index 0000000000..2eade4137b
--- /dev/null
+++ b/spec/bundler/support/build_metadata.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require_relative "path"
+require_relative "helpers"
+
+module Spec
+ module BuildMetadata
+ include Spec::Path
+ include Spec::Helpers
+
+ def write_build_metadata(dir: source_root, version: Bundler::VERSION)
+ build_metadata = {
+ git_commit_sha: git_commit_sha,
+ built_at: release_date_for(version, dir: dir),
+ }
+
+ replace_build_metadata(build_metadata, dir: dir)
+ end
+
+ def reset_build_metadata(dir: source_root)
+ build_metadata = {
+ built_at: nil,
+ }
+
+ replace_build_metadata(build_metadata, dir: dir)
+ end
+
+ private
+
+ def replace_build_metadata(build_metadata, dir:)
+ build_metadata_file = File.expand_path("lib/bundler/build_metadata.rb", dir)
+
+ ivars = build_metadata.sort.map do |k, v|
+ " @#{k} = #{loaded_gemspec.send(:ruby_code, v)}"
+ end.join("\n")
+
+ contents = File.read(build_metadata_file)
+ contents.sub!(/^(\s+# begin ivars).+(^\s+# end ivars)/m, "\\1\n#{ivars}\n\\2")
+ File.open(build_metadata_file, "w") {|f| f << contents }
+ end
+
+ def git_commit_sha
+ ruby_core_tarball? ? "unknown" : git("rev-parse --short HEAD", source_root).strip
+ end
+
+ def release_date_for(version, dir:)
+ changelog = File.expand_path("CHANGELOG.md", dir)
+ File.readlines(changelog)[2].scan(/^## #{Regexp.escape(version)} \((.*)\)/).first&.first if File.exist?(changelog)
+ end
+
+ extend self
+ end
+end
diff --git a/spec/bundler/support/builders.rb b/spec/bundler/support/builders.rb
new file mode 100644
index 0000000000..43ab7e053d
--- /dev/null
+++ b/spec/bundler/support/builders.rb
@@ -0,0 +1,749 @@
+# frozen_string_literal: true
+
+require "bundler/shared_helpers"
+require "shellwords"
+require "fileutils"
+require "rubygems/package"
+
+require_relative "build_metadata"
+
+module Spec
+ module Builders
+ def self.extended(mod)
+ mod.extend Path
+ mod.extend Helpers
+ end
+
+ def self.constantize(name)
+ name.delete("-").upcase
+ end
+
+ def v(version)
+ Gem::Version.new(version)
+ end
+
+ def pl(platform)
+ Gem::Platform.new(platform)
+ end
+
+ def build_repo1
+ build_repo gem_repo1 do
+ FileUtils.cp rake_path, "#{gem_repo1}/gems/"
+
+ build_gem "coffee-script-source"
+ build_gem "git"
+ build_gem "puma"
+ build_gem "minitest"
+
+ build_gem "myrack", %w[0.9.1 1.0.0] do |s|
+ s.executables = "myrackup"
+ s.post_install_message = "Myrack's post install message"
+ end
+
+ build_gem "thin" do |s|
+ s.add_dependency "myrack"
+ s.post_install_message = "Thin's post install message"
+ end
+
+ build_gem "myrack-obama" do |s|
+ s.add_dependency "myrack"
+ s.post_install_message = "Myrack-obama's post install message"
+ end
+
+ build_gem "myrack_middleware", "1.0" do |s|
+ s.add_dependency "myrack", "0.9.1"
+ end
+
+ build_gem "rails", "2.3.2" do |s|
+ s.executables = "rails"
+ s.add_dependency "rake", rake_version
+ s.add_dependency "actionpack", "2.3.2"
+ s.add_dependency "activerecord", "2.3.2"
+ s.add_dependency "actionmailer", "2.3.2"
+ s.add_dependency "activeresource", "2.3.2"
+ end
+ build_gem "actionpack", "2.3.2" do |s|
+ s.add_dependency "activesupport", "2.3.2"
+ end
+ build_gem "activerecord", ["2.3.1", "2.3.2"] do |s|
+ s.add_dependency "activesupport", "2.3.2"
+ end
+ build_gem "actionmailer", "2.3.2" do |s|
+ s.add_dependency "activesupport", "2.3.2"
+ end
+ build_gem "activeresource", "2.3.2" do |s|
+ s.add_dependency "activesupport", "2.3.2"
+ end
+ build_gem "activesupport", %w[1.2.3 2.3.2 2.3.5]
+
+ build_gem "activemerchant" do |s|
+ s.add_dependency "activesupport", ">= 2.0.0"
+ end
+
+ build_gem "rspec", "1.2.7", no_default: true do |s|
+ s.write "lib/spec.rb", "SPEC = '1.2.7'"
+ end
+
+ build_gem "myrack-test", no_default: true do |s|
+ s.write "lib/myrack/test.rb", "MYRACK_TEST = '1.0'"
+ end
+
+ build_gem "platform_specific" do |s|
+ s.platform = "java"
+ end
+
+ build_gem "platform_specific" do |s|
+ s.platform = "ruby"
+ end
+
+ build_gem "platform_specific" do |s|
+ s.platform = "x86-mswin32"
+ end
+
+ build_gem "platform_specific" do |s|
+ s.platform = "x64-mswin64"
+ end
+
+ build_gem "platform_specific" do |s|
+ s.platform = "x86-mingw32"
+ end
+
+ build_gem "platform_specific" do |s|
+ s.platform = "x64-mingw-ucrt"
+ end
+
+ build_gem "platform_specific" do |s|
+ s.platform = "aarch64-mingw-ucrt"
+ end
+
+ build_gem "platform_specific" do |s|
+ s.platform = "x86-darwin-100"
+ end
+
+ build_gem "only_java", "1.0" do |s|
+ s.platform = "java"
+ end
+
+ build_gem "only_java", "1.1" do |s|
+ s.platform = "java"
+ end
+
+ build_gem "nokogiri", "1.4.2"
+ build_gem "nokogiri", "1.4.2" do |s|
+ s.platform = "java"
+ s.add_dependency "weakling", ">= 0.0.3"
+ end
+
+ build_gem "laduradura", "5.15.2"
+ build_gem "laduradura", "5.15.2" do |s|
+ s.platform = "java"
+ end
+ build_gem "laduradura", "5.15.3" do |s|
+ s.platform = "java"
+ end
+
+ build_gem "weakling", "0.0.3"
+
+ build_gem "terranova", "8"
+
+ build_gem "duradura", "7.0"
+
+ build_gem "very_simple_binary", &:add_c_extension
+ build_gem "simple_binary", &:add_c_extension
+
+ build_gem "bundler", "0.9" do |s|
+ s.executables = "bundle"
+ s.write "bin/bundle", "#!/usr/bin/env ruby\nputs 'FAIL'"
+ end
+
+ # The bundler 0.8 gem has a rubygems plugin that always loads :(
+ build_gem "bundler", "0.8.1" do |s|
+ s.write "lib/bundler/omg.rb", ""
+ s.write "lib/rubygems_plugin.rb", "require 'bundler/omg' ; puts 'FAIL'"
+ end
+
+ # The yard gem iterates over Gem.source_index looking for plugins
+ build_gem "yard" do |s|
+ s.write "lib/yard.rb", <<-Y
+ Gem::Specification.sort_by(&:name).each do |gem|
+ puts gem.full_name
+ end
+ Y
+ end
+
+ build_gem "net-ssh"
+ build_gem "net-sftp", "1.1.1" do |s|
+ s.add_dependency "net-ssh", ">= 1.0.0", "< 1.99.0"
+ end
+
+ build_gem "foo"
+ end
+ end
+
+ def build_repo2(**kwargs, &blk)
+ FileUtils.cp_r gem_repo1, gem_repo2, remove_destination: true
+ update_repo2(**kwargs, &blk) if block_given?
+ end
+
+ # A repo that has no pre-installed gems included. (The caller completely
+ # determines the contents with the block.)
+ #
+ # If the repo already exists, `#update_repo` will be called.
+ def build_repo3(**kwargs, &blk)
+ if File.exist?(gem_repo3)
+ update_repo(gem_repo3, &blk)
+ else
+ build_repo gem_repo3, **kwargs, &blk
+ end
+ end
+
+ # Like build_repo3, this is a repo that has no pre-installed gems included.
+ #
+ # If the repo already exists, `#udpate_repo` will be called
+ def build_repo4(**kwargs, &blk)
+ if File.exist?(gem_repo4)
+ update_repo gem_repo4, &blk
+ else
+ build_repo gem_repo4, **kwargs, &blk
+ end
+ end
+
+ def update_repo2(**kwargs, &blk)
+ update_repo(gem_repo2, **kwargs, &blk)
+ end
+
+ def update_repo3(&blk)
+ update_repo(gem_repo3, &blk)
+ end
+
+ def build_security_repo
+ build_repo security_repo do
+ build_gem "myrack"
+
+ build_gem "signed_gem" do |s|
+ cert = "signing-cert.pem"
+ pkey = "signing-pkey.pem"
+ s.write cert, TEST_CERT
+ s.write pkey, TEST_PKEY
+ s.signing_key = pkey
+ s.cert_chain = [cert]
+ end
+ end
+ end
+
+ # A minimal fake irb console
+ def build_dummy_irb(version = "9.9.9")
+ build_gem "irb", version do |s|
+ s.write "lib/irb.rb", <<-RUBY
+ class IRB
+ class << self
+ def toplevel_binding
+ unless defined?(@toplevel_binding) && @toplevel_binding
+ TOPLEVEL_BINDING.eval %{
+ def self.__irb__; binding; end
+ IRB.instance_variable_set(:@toplevel_binding, __irb__)
+ class << self; undef __irb__; end
+ }
+ end
+ @toplevel_binding.eval('private')
+ @toplevel_binding
+ end
+
+ def __irb__
+ while line = gets
+ begin
+ puts eval(line, toplevel_binding).inspect.sub(/^"(.*)"$/, '=> \\1')
+ rescue Exception => e
+ puts "\#{e.class}: \#{e.message}"
+ puts e.backtrace.first
+ end
+ end
+ end
+ alias start __irb__
+ end
+ end
+ RUBY
+ end
+ end
+
+ def build_repo(path, **kwargs, &blk)
+ return if File.directory?(path)
+
+ FileUtils.mkdir_p("#{path}/gems")
+
+ update_repo(path,**kwargs, &blk)
+ end
+
+ def update_repo(path, build_compact_index: true)
+ exempted_caller = Gem.ruby_version >= Gem::Version.new("3.4.0.dev") && RUBY_ENGINE != "jruby" ? "#{Module.nesting.first}#build_repo" : "build_repo"
+ if path == gem_repo1 && caller_locations(1, 1).first.label != exempted_caller
+ raise "Updating gem_repo1 is unsupported -- use gem_repo2 instead"
+ end
+ return unless block_given?
+ @_build_path = "#{path}/gems"
+ @_build_repo = File.basename(path)
+ yield
+ options = { build_compact: build_compact_index }
+ Gem::Indexer.new(path, options).generate_index
+ ensure
+ @_build_path = nil
+ @_build_repo = nil
+ end
+
+ def build_index(&block)
+ index = Bundler::Index.new
+ IndexBuilder.run(index, &block) if block_given?
+ index
+ end
+
+ def build_spec(name, version = "0.0.1", platform = nil, &block)
+ Array(version).map do |v|
+ Gem::Specification.new do |s|
+ s.name = name
+ s.version = Gem::Version.new(v)
+ s.platform = platform
+ s.authors = ["no one in particular"]
+ s.summary = "a gemspec used only for testing"
+ DepBuilder.run(s, &block) if block_given?
+ end
+ end
+ end
+
+ def build_lib(name, *args, &blk)
+ build_with(LibBuilder, name, args, &blk)
+ end
+
+ def build_bundler(*args, &blk)
+ build_with(BundlerBuilder, "bundler", args, &blk)
+ end
+
+ def build_gem(name, *args, &blk)
+ build_with(GemBuilder, name, args, &blk)
+ end
+
+ def build_git(name, *args, &block)
+ opts = args.last.is_a?(Hash) ? args.last : {}
+ builder = opts[:bare] ? GitBareBuilder : GitBuilder
+ spec = build_with(builder, name, args, &block)
+ GitReader.new(self, opts[:path] || lib_path(spec.full_name))
+ end
+
+ def update_git(name, *args, &block)
+ opts = args.last.is_a?(Hash) ? args.last : {}
+ spec = build_with(GitUpdater, name, args, &block)
+ GitReader.new(self, opts[:path] || lib_path(spec.full_name))
+ end
+
+ def build_plugin(name, *args, &blk)
+ build_with(PluginBuilder, name, args, &blk)
+ end
+
+ private
+
+ def build_with(builder, name, args, &blk)
+ @_build_path ||= nil
+ @_build_repo ||= nil
+ options = args.last.is_a?(Hash) ? args.pop : {}
+ versions = args.last || "1.0"
+ spec = nil
+
+ options[:path] ||= @_build_path
+ options[:source] ||= @_build_repo
+
+ Array(versions).each do |version|
+ spec = builder.new(self, name, version)
+ yield spec if block_given?
+ spec._build(options)
+ end
+
+ spec
+ end
+
+ class IndexBuilder
+ include Builders
+
+ def self.run(index, &block)
+ new(index).run(&block)
+ end
+
+ def initialize(index)
+ @index = index
+ end
+
+ def run(&block)
+ instance_eval(&block)
+ end
+
+ def gem(*args, &block)
+ build_spec(*args, &block).each do |s|
+ @index << s
+ end
+ end
+
+ def platforms(platforms)
+ platforms.split(/\s+/).each do |platform|
+ platform.gsub!(/^(mswin32)$/, 'x86-\1')
+ yield Gem::Platform.new(platform)
+ end
+ end
+
+ def versions(versions)
+ versions.split(/\s+/).each {|version| yield v(version) }
+ end
+ end
+
+ class DepBuilder
+ include Builders
+
+ def self.run(spec, &block)
+ new(spec).run(&block)
+ end
+
+ def initialize(spec)
+ @spec = spec
+ end
+
+ def run(&block)
+ instance_eval(&block)
+ end
+
+ def runtime(name, requirements)
+ @spec.add_runtime_dependency(name, requirements)
+ end
+
+ def development(name, requirements)
+ @spec.add_development_dependency(name, requirements)
+ end
+
+ def required_ruby_version=(*reqs)
+ @spec.required_ruby_version = *reqs
+ end
+
+ alias_method :dep, :runtime
+ end
+
+ class BundlerBuilder
+ def initialize(context, name, version)
+ @context = context
+ @spec = Spec::Path.loaded_gemspec.dup
+ @spec.version = version || Bundler::VERSION
+ end
+
+ def required_ruby_version
+ @spec.required_ruby_version
+ end
+
+ def required_ruby_version=(x)
+ @spec.required_ruby_version = x
+ end
+
+ def _build(options = {})
+ full_name = "bundler-#{@spec.version}"
+ build_path = (options[:build_path] || @context.tmp) + full_name
+ bundler_path = build_path + "#{full_name}.gem"
+
+ FileUtils.mkdir_p build_path
+
+ @context.shipped_files.each do |shipped_file|
+ target_shipped_file = shipped_file
+ target_shipped_file = shipped_file.sub(/\Alibexec/, "exe") if @context.ruby_core?
+ target_shipped_file = build_path + target_shipped_file
+ target_shipped_dir = File.dirname(target_shipped_file)
+ FileUtils.mkdir_p target_shipped_dir unless File.directory?(target_shipped_dir)
+ FileUtils.cp File.expand_path(shipped_file, @context.source_root), target_shipped_file, preserve: true
+ end
+
+ @context.replace_version_file(@spec.version, dir: build_path)
+ @context.replace_changelog(@spec.version, dir: build_path) if options[:released]
+
+ Spec::BuildMetadata.write_build_metadata(dir: build_path, version: @spec.version.to_s)
+
+ Dir.chdir build_path do
+ Gem::DefaultUserInteraction.use_ui(Gem::SilentUI.new) do
+ Gem::Package.build(@spec)
+ end
+ end
+
+ if block_given?
+ yield(bundler_path)
+ else
+ FileUtils.mv bundler_path, options[:path]
+ end
+ ensure
+ FileUtils.rm_rf build_path
+ end
+ end
+
+ class LibBuilder
+ def initialize(context, name, version)
+ @context = context
+ @name = name
+ @spec = Gem::Specification.new do |s|
+ s.name = name
+ s.version = version
+ s.summary = "This is just a fake gem for testing"
+ s.description = "This is a completely fake gem, for testing purposes."
+ s.author = "no one"
+ s.email = "foo@bar.baz"
+ s.homepage = "http://example.com"
+ s.license = "MIT"
+ s.required_ruby_version = ">= 3.0"
+ end
+ @files = {}
+ end
+
+ def method_missing(*args, &blk)
+ @spec.send(*args, &blk)
+ end
+
+ def write(file, source = "")
+ @files[file] = source
+ end
+
+ def executables=(val)
+ @spec.executables = Array(val)
+ @spec.executables.each do |file|
+ executable = "#{@spec.bindir}/#{file}"
+ shebang = "#!/usr/bin/env ruby\n"
+ @spec.files << executable
+ write executable, "#{shebang}require_relative '../lib/#{@name}' ; puts #{Builders.constantize(@name)}"
+ end
+ end
+
+ def add_c_extension
+ extensions << "ext/extconf.rb"
+ write "ext/extconf.rb", <<-RUBY
+ require "mkmf"
+
+ extension_name = "#{name}_c"
+ if extra_lib_dir = with_config("ext-lib")
+ # add extra libpath if --with-ext-lib is
+ # passed in as a build_arg
+ dir_config extension_name, nil, extra_lib_dir
+ else
+ dir_config extension_name
+ end
+ create_makefile extension_name
+ RUBY
+ write "ext/#{name}.c", <<-C
+ #include "ruby.h"
+
+ void Init_#{name}_c(void) {
+ rb_define_module("#{Builders.constantize(name)}_IN_C");
+ }
+ C
+ end
+
+ def _build(options)
+ path = options[:path] || _default_path
+
+ if options[:rubygems_version]
+ @spec.rubygems_version = options[:rubygems_version]
+
+ def @spec.validate(*); end
+ end
+
+ unless options[:no_default]
+ gem_source = options[:source] || "path@#{path}"
+ @files = _default_files.
+ merge("lib/#{entrypoint}/source.rb" => "#{Builders.constantize(name)}_SOURCE = #{gem_source.to_s.dump}").
+ merge(@files)
+ end
+
+ @spec.authors = ["no one"]
+ @spec.files += @files.keys
+
+ case options[:gemspec]
+ when false
+ # do nothing
+ when :yaml
+ @files["#{name}.gemspec"] = @spec.to_yaml
+ else
+ @files["#{name}.gemspec"] = @spec.to_ruby
+ end
+
+ @files.each do |file, source|
+ full_path = Pathname.new(path).join(file)
+ FileUtils.mkdir_p(full_path.dirname)
+ File.open(full_path, "w") {|f| f.puts source }
+ FileUtils.chmod("+x", full_path) if @spec.executables.map {|exe| "#{@spec.bindir}/#{exe}" }.include?(file)
+ end
+ path
+ end
+
+ def _default_files
+ @_default_files ||= { "lib/#{entrypoint}.rb" => "#{Builders.constantize(name)} = '#{version}#{platform_string}'" }
+ end
+
+ def entrypoint
+ name.tr("-", "/")
+ end
+
+ def _default_path
+ @context.tmp("libs", @spec.full_name)
+ end
+
+ def platform_string
+ " #{@spec.platform}" unless @spec.platform == Gem::Platform::RUBY
+ end
+ end
+
+ class GitBuilder < LibBuilder
+ def _build(options)
+ default_branch = options[:default_branch] || "main"
+ path = options[:path] || _default_path
+ source = options[:source] || "git@#{path}"
+ super(options.merge(path: path, source: source))
+ @context.git("config --global init.defaultBranch #{default_branch}", path)
+ @context.git("init", path)
+ @context.git("add *", path)
+ @context.git("config user.email lol@wut.com", path)
+ @context.git("config user.name lolwut", path)
+ @context.git("config commit.gpgsign false", path)
+ @context.git("commit -m OMG_INITIAL_COMMIT", path)
+ end
+ end
+
+ class GitBareBuilder < LibBuilder
+ def _build(options)
+ path = options[:path] || _default_path
+ super(options.merge(path: path))
+ @context.git("init --bare", path)
+ end
+ end
+
+ class GitUpdater < LibBuilder
+ def _build(options)
+ libpath = options[:path] || _default_path
+ update_gemspec = options[:gemspec] || false
+ source = options[:source] || "git@#{libpath}"
+
+ if branch = options[:branch]
+ @context.git("checkout -b #{Shellwords.shellescape(branch)}", libpath)
+ elsif tag = options[:tag]
+ @context.git("tag #{Shellwords.shellescape(tag)}", libpath)
+ elsif options[:remote]
+ @context.git("remote add origin #{options[:remote]}", libpath)
+ elsif options[:push]
+ @context.git("push origin #{options[:push]}", libpath)
+ end
+
+ current_ref = @context.git("rev-parse HEAD", libpath).strip
+ _default_files.keys.each do |path|
+ _default_files[path] += "\n#{Builders.constantize(name)}_PREV_REF = '#{current_ref}'"
+ end
+ super(options.merge(path: libpath, gemspec: update_gemspec, source: source))
+ @context.git("commit -am BUMP", libpath)
+ end
+ end
+
+ class GitReader
+ attr_reader :context, :path
+
+ def initialize(context, path)
+ @context = context
+ @path = path
+ end
+
+ def ref_for(ref, len = nil)
+ ref = context.git "rev-parse #{ref}", path
+ ref = ref[0..len] if len
+ ref
+ end
+ end
+
+ class GemBuilder < LibBuilder
+ def _build(opts)
+ lib_path = opts[:lib_path] || @context.tmp(".tmp/#{@spec.full_name}")
+ lib_path = super(opts.merge(path: lib_path, no_default: opts[:no_default]))
+ destination = opts[:path] || _default_path
+ FileUtils.mkdir_p(lib_path.join(destination))
+
+ if [:yaml, false].include?(opts[:gemspec])
+ Dir.chdir(lib_path) do
+ Bundler.rubygems.build(@spec, opts[:skip_validation])
+ end
+ elsif opts[:skip_validation]
+ Dir.chdir(lib_path) { Gem::Package.build(@spec, true) }
+ else
+ Dir.chdir(lib_path) { Gem::Package.build(@spec) }
+ end
+
+ gem_path = File.expand_path("#{@spec.full_name}.gem", lib_path)
+ if opts[:to_system]
+ @context.system_gems gem_path, default: opts[:default]
+ elsif opts[:to_bundle]
+ @context.system_gems gem_path, path: @context.default_bundle_path
+ else
+ FileUtils.mv(gem_path, destination)
+ end
+ end
+
+ def _default_path
+ @context.gem_repo1("gems")
+ end
+ end
+
+ class PluginBuilder < GemBuilder
+ def _default_files
+ @_default_files ||= {
+ "lib/#{name}.rb" => "#{Builders.constantize(name)} = '#{version}#{platform_string}'",
+ "plugins.rb" => "",
+ }
+ end
+ end
+
+ TEST_CERT = <<~CERT
+ -----BEGIN CERTIFICATE-----
+ MIIDNTCCAh2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAnMQwwCgYDVQQDDAN5b3Ux
+ FzAVBgoJkiaJk/IsZAEZFgdleGFtcGxlMB4XDTE1MDIwODAwMTIyM1oXDTQyMDYy
+ NTAwMTIyM1owJzEMMAoGA1UEAwwDeW91MRcwFQYKCZImiZPyLGQBGRYHZXhhbXBs
+ ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMkupYkg3Nd1oXM3fo0d
+ mVJBWNrni88lKDuIIQXwcKe6XCgiloZG708ecLTOws9+o9MkTl9Wtpf/WGXT98NK
+ EPUYakd2Fv1SuD1jWYlP7iDR6hB3RkWBm5ziujYftVJ4ZrPD42PLjDASvlh75Tvr
+ MeM7yq/qkcgNsd9dQyUvMNPks3tla9je7Dt7Auli2IN3CNXys7gIOfwJH0Bb/M6t
+ y7oUfpoUKAfLzwe61abztgDu1lSNgdFBM1kcxYflyh/FkX5TlAcWeAXzLrnxAXGR
+ UxXrxW4oPC+kZi/pDRBd7X4zQDx7bCmr1+FsS3M05i3w5E08Tt9iKRk4V8nCmE4i
+ k6UCAwEAAaNsMGowCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYE
+ FOOOFw5TNAqt/TcRRZEU3Dg/58XuMBYGA1UdEQQPMA2BC3lvdUBleGFtcGxlMBYG
+ A1UdEgQPMA2BC3lvdUBleGFtcGxlMA0GCSqGSIb3DQEBCwUAA4IBAQAy3xnmobxU
+ 1SyhHvoIXTJmG0wt1DQ/Dqwjy362LpEf1UHt29wtg1Mph58eVtl93z5Vd2t4/O77
+ E2BHpSu9ujc6/Br4+2uA/Qk/xRyLBtZAwty6J4uFvOOg985HonN+RCUZbKSUTmtA
+ TZvNtIDAZFQ8Tu75K4gIBxDcz7biGi4i1VJ3F3GNCNeossr9IQwKvb+UWFq14U5R
+ IzUnGgMIzcjUG2kKQvddRD1CjS+egtcLvShbOfm5bs4w4rfQ2FPF+Aaf9v7fxa/c
+ Jrf3K+cB19eAy7O4nlPG1xurvnZd0QpqRk++werrBuKe1Pgga7YBLePfJhzwqcZv
+ wVOSsB870yeO
+ -----END CERTIFICATE-----
+ CERT
+
+ TEST_PKEY = <<~PKEY
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIEowIBAAKCAQEAyS6liSDc13Whczd+jR2ZUkFY2ueLzyUoO4ghBfBwp7pcKCKW
+ hkbvTx5wtM7Cz36j0yROX1a2l/9YZdP3w0oQ9RhqR3YW/VK4PWNZiU/uINHqEHdG
+ RYGbnOK6Nh+1Unhms8PjY8uMMBK+WHvlO+sx4zvKr+qRyA2x311DJS8w0+Sze2Vr
+ 2N7sO3sC6WLYg3cI1fKzuAg5/AkfQFv8zq3LuhR+mhQoB8vPB7rVpvO2AO7WVI2B
+ 0UEzWRzFh+XKH8WRflOUBxZ4BfMuufEBcZFTFevFbig8L6RmL+kNEF3tfjNAPHts
+ KavX4WxLczTmLfDkTTxO32IpGThXycKYTiKTpQIDAQABAoIBABpyrHEWRed5X7aN
+ kXCBzKSN/LLChT8VNnB6bppLnV501yVbmV2hDlg2EJZkfCMvwIptwnPcKs2uqZ4G
+ u2gMC6X9Bgkg/YK4u4nZJBiIzoMNYEUL48wYGYS1dcokaapO3nQ8M1+XjyAexrFL
+ 5btL1IIisScRTQWiGe6FtzcN43sSNkBISyDF5zG4Kodynqi0ekITmMl2q5XLWcsM
+ KBnmZcRFEmFae2YYczVy8SXNApkZEvN69znvAX1iDNnZ3sJFchXo1nRPt4stOOKw
+ mydgIYqaNQ22aF3OkblvoA4Y4m+X2Qt1sfkryKa5xTT7DSE81GmmazNI64EWqtES
+ 6Xde6P0CgYEA+V1vuSnE5fWX188abWMbVwNMC71WfHbntFmI+qwWYPEpickm+RGX
+ DDfXs5unlVX4KUmjfplgavO29op1GZTuD9TlRnUAV0+0aJnNq4DY6XsHfD84qsBr
+ gQGEHeJ1cMGNDnZR/EV3eudMalj9Qjpx9NoXNzMykb0/SUYZQemiqwcCgYEAzokC
+ s0GoHVJqan4dfU0h0G5QPncrajW9DGG1ySxK/A2eqbVB8W2ZQx39OS26/Gydb31p
+ cR7zm8PZpNbzLqlIMEbD4F6q22xxvYVtDx/HHPjxHMi87yxwQ9uLDUHoMa/LciTO
+ djv3D1xTDDGxbpjmsdmINetunAs3htxku7JY5PMCgYBs3/TVvXzwgmhHm28Ib4sS
+ VKgxP/uw4CGORsFd4SDsNp9SP3c6rAltFjyheMaUlzKApFwz/DdyuvIZdp5mCvZe
+ BzALsS3y8SPtv6lixiDu3/6GqvvM4bKOYuESQzvPfVJfDB4DrTjben2MuUnqTqZO
+ p6IXQc1EgIJPNcH1W1LgpQKBgAKZlPAevngIBpDqn4JpSyititMOevxuSr/yJvCu
+ Xw9HOJ0YTAk3APvoT7y9h6IP1/eEU6R56EUotP+vOQZ4WRFKgsK7TllOxyvElzfe
+ hYom1BoxqLc2Dv+7rsdu8fZWKTB5qCOy44xM9DquEXa79AN/IojTOuQ5++v1sErw
+ ls/jAoGBANneGe9ogN51mYkrLyg1fhU1i24gFRq+sPGEvsCUoE6Vjw/lawQQ80T8
+ v45TFqvhoGpgznqy3qxDJyguquZg6HN2yW6HE2Dvk7uk3XogcjdXgNDmWqb2j0eE
+ z9pKzHCqfwNVPuYf44Znyo2YeyZ2kHn42MU73oXuFshUs3QHcH+P
+ -----END RSA PRIVATE KEY-----
+ PKEY
+ end
+end
diff --git a/spec/bundler/support/bundle b/spec/bundler/support/bundle
new file mode 100755
index 0000000000..8f8b535295
--- /dev/null
+++ b/spec/bundler/support/bundle
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require_relative "../bundler/support/activate"
+
+load File.expand_path("bundle", Spec::Path.exedir)
diff --git a/spec/bundler/support/bundle.rb b/spec/bundler/support/bundle.rb
new file mode 100644
index 0000000000..aa7b121706
--- /dev/null
+++ b/spec/bundler/support/bundle.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require_relative "path"
+
+warn "#{__FILE__} is deprecated. Please use #{Spec::Path.dev_binstub} instead"
+
+load Spec::Path.dev_binstub
diff --git a/spec/bundler/support/checksums.rb b/spec/bundler/support/checksums.rb
new file mode 100644
index 0000000000..7b69bba668
--- /dev/null
+++ b/spec/bundler/support/checksums.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+module Spec
+ module Checksums
+ class ChecksumsBuilder
+ attr_reader :bundler_registered
+
+ def initialize(enabled = true, &block)
+ @enabled = enabled
+ @checksums = {}
+ yield self if block_given?
+ end
+
+ def initialize_copy(original)
+ super
+ @checksums = @checksums.dup
+ end
+
+ def checksum(repo, name, version, platform = Gem::Platform::RUBY, folder = "gems")
+ @bundler_registered = true if name == "bundler"
+
+ name_tuple = Gem::NameTuple.new(name, version, platform)
+ gem_file = File.join(repo, folder, "#{name_tuple.full_name}.gem")
+ File.open(gem_file, "rb") do |f|
+ register(name_tuple, Bundler::Checksum.from_gem(f, "#{gem_file} (via ChecksumsBuilder#checksum)"))
+ end
+ end
+
+ def no_checksum(name, version, platform = Gem::Platform::RUBY)
+ name_tuple = Gem::NameTuple.new(name, version, platform)
+ register(name_tuple, nil)
+ end
+
+ def delete(name, platform = nil)
+ @checksums.reject! {|k, _| k.name == name && (platform.nil? || k.platform == platform) }
+ end
+
+ def to_s
+ return "" unless @enabled
+
+ locked_checksums = @checksums.map do |name_tuple, checksum|
+ checksum &&= " #{checksum.to_lock}"
+ " #{name_tuple.lock_name}#{checksum}\n"
+ end
+
+ "\nCHECKSUMS\n#{locked_checksums.sort.join}"
+ end
+
+ private
+
+ def register(name_tuple, checksum)
+ delete(name_tuple.name, name_tuple.platform)
+ @checksums[name_tuple] = checksum
+ end
+ end
+
+ def checksums_section(enabled = true, bundler_checksum: true, &block)
+ ChecksumsBuilder.new(enabled, &block).tap do |builder|
+ next if builder.bundler_registered || !bundler_checksum
+
+ next if Bundler::VERSION.to_s.end_with?(".dev")
+ builder.checksum(system_gem_path, "bundler", Bundler::VERSION, Gem::Platform::RUBY, "cache")
+ end
+ end
+
+ def checksums_section_when_enabled(target_lockfile = nil, &block)
+ begin
+ enabled = (target_lockfile || lockfile).match?(/^CHECKSUMS$/)
+ rescue Errno::ENOENT
+ enabled = true
+ end
+ checksums_section(enabled, &block)
+ end
+
+ def checksum_to_lock(*args)
+ checksums_section(true, bundler_checksum: false) do |c|
+ c.checksum(*args)
+ end.to_s.sub(/^CHECKSUMS\n/, "").strip
+ end
+
+ def checksum_digest(*args)
+ checksum_to_lock(*args).split(Bundler::Checksum::ALGO_SEPARATOR, 2).last
+ end
+
+ # if prefixes is given, removes all checksums where the line
+ # has any of the prefixes on the line before the checksum
+ # otherwise, removes all checksums from the lockfile
+ def remove_checksums_from_lockfile(lockfile, *prefixes)
+ head, remaining = lockfile.split(/^CHECKSUMS$/, 2)
+ return lockfile unless remaining
+ checksums, tail = remaining.split("\n\n", 2)
+
+ prefixes =
+ if prefixes.empty?
+ nil
+ else
+ /(#{prefixes.map {|p| Regexp.escape(p) }.join("|")})/
+ end
+
+ checksums = checksums.each_line.map do |line|
+ if prefixes.nil? || line.match?(prefixes)
+ line.gsub(/ sha256=[a-f0-9]{64}/i, "")
+ else
+ line
+ end
+ end
+
+ head.concat(
+ "CHECKSUMS",
+ checksums.join,
+ "\n\n",
+ tail
+ )
+ end
+
+ def remove_checksums_section_from_lockfile(lockfile)
+ head, remaining = lockfile.split(/^CHECKSUMS$/, 2)
+ return lockfile unless remaining
+ _checksums, tail = remaining.split("\n\n", 2)
+ head.concat(tail)
+ end
+
+ def checksum_from_package(gem_file, name, version)
+ name_tuple = Gem::NameTuple.new(name, version)
+
+ checksum = nil
+
+ File.open(gem_file, "rb") do |f|
+ checksum = Bundler::Checksum.from_gem(f, gemfile)
+ end
+
+ "#{name_tuple.lock_name} #{checksum.to_lock}"
+ end
+ end
+end
diff --git a/spec/bundler/support/command_execution.rb b/spec/bundler/support/command_execution.rb
new file mode 100644
index 0000000000..e2915b996d
--- /dev/null
+++ b/spec/bundler/support/command_execution.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module Spec
+ class CommandExecution
+ def initialize(command, timeout:)
+ @command = command
+ @timeout = timeout
+ @original_stdout = String.new
+ @original_stderr = String.new
+ end
+
+ attr_accessor :exitstatus, :command, :original_stdout, :original_stderr
+ attr_reader :timeout
+ attr_writer :failure_reason
+
+ def raise_error!
+ return unless failure?
+
+ error_header = if failure_reason == :timeout
+ "Invoking `#{command}` was aborted after #{timeout} seconds with output:"
+ else
+ "Invoking `#{command}` failed with output:"
+ end
+
+ raise <<~ERROR
+ #{error_header}
+
+ ----------------------------------------------------------------------
+ #{stdboth}
+ ----------------------------------------------------------------------
+ ERROR
+ end
+
+ def to_s
+ "$ #{command}"
+ end
+ alias_method :inspect, :to_s
+
+ def stdboth
+ @stdboth ||= [stderr, stdout].join("\n").strip
+ end
+
+ def stdout
+ normalize(original_stdout)
+ end
+
+ def stderr
+ normalize(original_stderr)
+ end
+
+ def to_s_verbose
+ [
+ to_s,
+ stdout,
+ stderr,
+ exitstatus ? "# $? => #{exitstatus}" : "",
+ ].reject(&:empty?).join("\n")
+ end
+
+ def success?
+ return true unless exitstatus
+ exitstatus == 0
+ end
+
+ def failure?
+ return true unless exitstatus
+ exitstatus > 0
+ end
+
+ private
+
+ attr_reader :failure_reason
+
+ def normalize(string)
+ string.dup.force_encoding(Encoding::UTF_8).scrub.strip.gsub("\r\n", "\n")
+ end
+ end
+end
diff --git a/spec/bundler/support/env.rb b/spec/bundler/support/env.rb
new file mode 100644
index 0000000000..0899bd82a3
--- /dev/null
+++ b/spec/bundler/support/env.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Spec
+ module Env
+ def ruby_core?
+ File.exist?(File.expand_path("../../../lib/bundler/bundler.gemspec", __dir__))
+ end
+
+ def rubylib
+ ENV["RUBYLIB"].to_s.split(File::PATH_SEPARATOR)
+ end
+ end
+end
diff --git a/spec/bundler/support/filters.rb b/spec/bundler/support/filters.rb
new file mode 100644
index 0000000000..2be25b4a78
--- /dev/null
+++ b/spec/bundler/support/filters.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+class RequirementChecker < Proc
+ def self.against(provided)
+ new do |required|
+ requirement = Gem::Requirement.new(required)
+
+ !requirement.satisfied_by?(provided)
+ end.tap do |checker|
+ checker.provided = provided
+ end
+ end
+
+ attr_accessor :provided
+
+ def inspect
+ "\"#{provided}\""
+ end
+end
+
+git_version = Gem::Version.new(`git --version`[/(\d+\.\d+\.\d+)/, 1])
+
+RSpec.configure do |config|
+ config.filter_run_excluding realworld: true
+
+ config.filter_run_excluding rubygems: RequirementChecker.against(Gem.rubygems_version)
+ config.filter_run_excluding git: RequirementChecker.against(git_version)
+ config.filter_run_excluding ruby_repo: !ENV["GEM_COMMAND"].nil?
+ config.filter_run_excluding no_color_tty: Gem.win_platform? || !ENV["GITHUB_ACTION"].nil?
+ config.filter_run_excluding permissions: Gem.win_platform?
+ config.filter_run_excluding readline: Gem.win_platform?
+ config.filter_run_excluding jruby_only: RUBY_ENGINE != "jruby"
+ config.filter_run_excluding truffleruby_only: RUBY_ENGINE != "truffleruby"
+ config.filter_run_excluding man: Gem.win_platform?
+ config.filter_run_excluding mri_only: RUBY_ENGINE != "ruby"
+
+ config.filter_run_when_matching :focus unless ENV["CI"]
+
+ config.before(:each, :bundler) do |example|
+ bundle_config "simulate_version #{example.metadata[:bundler]}"
+ end
+end
diff --git a/spec/bundler/support/hax.rb b/spec/bundler/support/hax.rb
new file mode 100644
index 0000000000..46718f5fa4
--- /dev/null
+++ b/spec/bundler/support/hax.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+if ENV["BUNDLER_SPEC_RUBY_PLATFORM"]
+ Object.send(:remove_const, :RUBY_PLATFORM)
+ RUBY_PLATFORM = ENV["BUNDLER_SPEC_RUBY_PLATFORM"]
+end
+
+module Gem
+ def self.ruby=(ruby)
+ @ruby = ruby
+ end
+
+ if ENV["RUBY"]
+ Gem.ruby = ENV["RUBY"]
+ end
+
+ if ENV["BUNDLER_GEM_DEFAULT_DIR"]
+ @default_dir = ENV["BUNDLER_GEM_DEFAULT_DIR"]
+ @default_specifications_dir = nil
+ end
+
+ spec_platform = ENV["BUNDLER_SPEC_PLATFORM"]
+ if spec_platform
+ if /mingw|mswin/.match?(spec_platform)
+ @@win_platform = nil # rubocop:disable Style/ClassVars
+ RbConfig::CONFIG["host_os"] = spec_platform.gsub(/^[^-]+-/, "").tr("-", "_")
+ end
+
+ RbConfig::CONFIG["arch"] = spec_platform
+
+ class Platform
+ @local = nil
+ end
+ @platforms = []
+ end
+
+ if ENV["BUNDLER_SPEC_GEM_SOURCES"]
+ self.sources = [ENV["BUNDLER_SPEC_GEM_SOURCES"]]
+ end
+
+ if ENV["BUNDLER_SPEC_READ_ONLY"]
+ module ReadOnly
+ def open(file, mode)
+ if file != IO::NULL && mode == "wb"
+ raise Errno::EROFS
+ else
+ super
+ end
+ end
+ end
+
+ File.singleton_class.prepend ReadOnly
+ end
+
+ if ENV["BUNDLER_SPEC_FAKE_RESOLVE"]
+ module FakeResolv
+ def getaddrinfo(host, port)
+ if host == ENV["BUNDLER_SPEC_FAKE_RESOLVE"]
+ [["AF_INET", port, "127.0.0.1", "127.0.0.1", 2, 2, 17]]
+ else
+ super
+ end
+ end
+ end
+
+ Socket.singleton_class.prepend FakeResolv
+ end
+end
+
+# mise installed rubygems_plugin.rb to system wide `site_ruby` directory.
+# This empty module avoid to call `mise` command.
+module ReshimInstaller
+ def self.reshim; end
+end
diff --git a/spec/bundler/support/helpers.rb b/spec/bundler/support/helpers.rb
new file mode 100644
index 0000000000..b0d4b5008b
--- /dev/null
+++ b/spec/bundler/support/helpers.rb
@@ -0,0 +1,631 @@
+# frozen_string_literal: true
+
+require_relative "the_bundle"
+require_relative "path"
+require_relative "options"
+require_relative "subprocess"
+
+module Spec
+ module Helpers
+ include Spec::Path
+ include Spec::Options
+ include Spec::Subprocess
+
+ def self.extended(mod)
+ mod.extend Spec::Path
+ mod.extend Spec::Options
+ mod.extend Spec::Subprocess
+ end
+
+ def reset!
+ Dir.glob("#{tmp}/{gems/*,*}", File::FNM_DOTMATCH).each do |dir|
+ next if %w[base base_system remote1 rubocop standard gems rubygems . ..].include?(File.basename(dir))
+ FileUtils.rm_r(dir)
+ end
+ FileUtils.mkdir_p(home)
+ FileUtils.mkdir_p(tmpdir)
+ Bundler.reset!
+ Bundler::Source::Git::GitProxy.reset
+ Gem.clear_paths
+ end
+
+ def the_bundle
+ TheBundle.new
+ end
+
+ MAJOR_DEPRECATION = /^\[DEPRECATED\]\s*/
+
+ def err_without_deprecations
+ err.gsub(/#{MAJOR_DEPRECATION}.+[\n]?/, "")
+ end
+
+ def deprecations
+ err.split("\n").filter_map {|l| l.sub(MAJOR_DEPRECATION, "") if l.match?(MAJOR_DEPRECATION) }
+ end
+
+ def run(cmd, *args)
+ opts = args.last.is_a?(Hash) ? args.pop : {}
+ groups = args.map(&:inspect).join(", ")
+ setup = "require 'bundler' ; Bundler.ui.silence { Bundler.setup(#{groups}) }"
+ ruby([setup, cmd].join(" ; "), opts)
+ end
+
+ def load_error_run(ruby, name, *args)
+ cmd = <<-RUBY
+ begin
+ #{ruby}
+ rescue LoadError => e
+ warn e.message if e.message.include?("-- #{name}")
+ end
+ RUBY
+ opts = args.last.is_a?(Hash) ? args.pop : {}
+ args += [opts]
+ run(cmd, *args)
+ end
+
+ def in_bundled_app(cmd, options = {})
+ sys_exec(cmd, dir: bundled_app, raise_on_error: options[:raise_on_error])
+ end
+
+ def bundle(cmd, options = {}, &block)
+ bundle_bin = options.delete(:bundle_bin)
+ bundle_bin ||= installed_bindir.join("bundle")
+
+ env = options.delete(:env) || {}
+
+ requires = options.delete(:requires) || []
+
+ dir = options.delete(:dir) || bundled_app
+ custom_load_path = options.delete(:load_path)
+
+ load_path = []
+ load_path << custom_load_path if custom_load_path
+
+ build_env_options = { load_path: load_path, requires: requires, env: env }
+ build_env_options.merge!(artifice: options.delete(:artifice)) if options.key?(:artifice) || cmd.start_with?("exec")
+
+ match_source(cmd)
+
+ env = build_env(build_env_options)
+
+ raise_on_error = options.delete(:raise_on_error)
+
+ args = options.map do |k, v|
+ case v
+ when true
+ " --#{k}"
+ when false
+ " --no-#{k}"
+ else
+ " --#{k} #{v}"
+ end
+ end.join
+
+ cmd = "#{Gem.ruby} #{bundle_bin} #{cmd}#{args}"
+ sys_exec(cmd, { env: env, dir: dir, raise_on_error: raise_on_error }, &block)
+ end
+
+ def main_source(dir)
+ gemfile = File.expand_path("Gemfile", dir)
+ return unless File.exist?(gemfile)
+
+ match = File.readlines(gemfile).first.match(/source ["'](?<source>[^"']+)["']/)
+ return unless match
+
+ match[:source]
+ end
+
+ def bundler(cmd, options = {})
+ options[:bundle_bin] = system_gem_path("bin/bundler")
+ bundle(cmd, options)
+ end
+
+ def ruby(ruby, options = {})
+ env = build_env({ artifice: nil }.merge(options))
+ escaped_ruby = ruby.shellescape
+ options[:env] = env if env
+ options[:dir] ||= bundled_app
+ sys_exec(%(#{Gem.ruby} -w -e #{escaped_ruby}), options)
+ end
+
+ def load_error_ruby(ruby, name, opts = {})
+ ruby(<<-R)
+ begin
+ #{ruby}
+ rescue LoadError => e
+ warn e.message if e.message.include?("-- #{name}")
+ end
+ R
+ end
+
+ def build_env(options = {})
+ env = options.delete(:env) || {}
+ libs = options.delete(:load_path) || []
+ env["RUBYOPT"] = opt_add("-I#{libs.join(File::PATH_SEPARATOR)}", env["RUBYOPT"]) if libs.any?
+
+ current_example = RSpec.current_example
+
+ main_source = @gemfile_source if defined?(@gemfile_source)
+ compact_index_main_source = main_source&.start_with?("https://gem.repo", "https://gems.security")
+
+ requires = options.delete(:requires) || []
+ requires << hax
+
+ artifice = options.delete(:artifice) do
+ if current_example && current_example.metadata[:realworld]
+ "vcr"
+ elsif compact_index_main_source
+ env["BUNDLER_SPEC_GEM_REPO"] ||=
+ case main_source
+ when "https://gem.repo1" then gem_repo1.to_s
+ when "https://gem.repo2" then gem_repo2.to_s
+ when "https://gem.repo3" then gem_repo3.to_s
+ when "https://gem.repo4" then gem_repo4.to_s
+ when "https://gems.security" then security_repo.to_s
+ end
+
+ "compact_index"
+ else
+ "fail"
+ end
+ end
+ if artifice
+ requires << "#{Path.spec_dir}/support/artifice/#{artifice}.rb"
+ end
+
+ requires.each {|r| env["RUBYOPT"] = opt_add("-r#{r}", env["RUBYOPT"]) }
+
+ env
+ end
+
+ def gembin(cmd, options = {})
+ cmd = bundled_app("bin/#{cmd}") unless cmd.to_s.include?("/")
+ sys_exec(cmd.to_s, options)
+ end
+
+ def sys_exec(cmd, options = {}, &block)
+ env = options[:env] || {}
+ env["RUBYOPT"] = opt_add(opt_add("-r#{spec_dir}/support/switch_rubygems.rb", env["RUBYOPT"]), ENV["RUBYOPT"])
+ options[:env] = env
+
+ sh(cmd, options, &block)
+ end
+
+ def bundle_config(config = nil, path = bundled_app(".bundle/config"))
+ if config.is_a?(String)
+ key, value = config.split(" ", 2)
+ config = { Bundler::Settings.key_for(key) => value }
+ end
+
+ current = File.exist?(path) ? Psych.load_file(path) : {}
+ return current unless config
+
+ current = {} if current.empty?
+
+ FileUtils.mkdir_p(File.dirname(path))
+
+ new_config = current.merge(config).compact
+
+ File.open(path, "w+") do |f|
+ f.puts new_config.to_yaml
+ end
+
+ new_config
+ end
+
+ def bundle_config_global(config = nil)
+ bundle_config(config, home(".bundle/config"))
+ end
+
+ def create_file(path, contents = "")
+ contents = strip_whitespace(contents)
+ path = Pathname.new(path).expand_path(bundled_app) unless path.is_a?(Pathname)
+ path.dirname.mkpath
+ path.write(contents)
+
+ # if the file is a script, create respective bat file on Windows
+ if contents.start_with?("#!")
+ path.chmod(0o755)
+ if Gem.win_platform?
+ path.sub_ext(".bat").write <<~SCRIPT
+ @ECHO OFF
+ @"ruby.exe" "%~dpn0" %*
+ SCRIPT
+ end
+ end
+ end
+
+ def gemfile(*args)
+ contents = args.pop
+
+ if contents.nil?
+ read_gemfile
+ else
+ match_source(contents)
+ create_file(args.pop || "Gemfile", contents)
+ end
+ end
+
+ def lockfile(*args)
+ contents = args.pop
+
+ if contents.nil?
+ read_lockfile
+ else
+ create_file(args.pop || "Gemfile.lock", contents)
+ end
+ end
+
+ def read_gemfile(file = "Gemfile")
+ read_bundled_app_file(file)
+ end
+
+ def read_lockfile(file = "Gemfile.lock")
+ read_bundled_app_file(file)
+ end
+
+ def read_bundled_app_file(file)
+ bundled_app(file).read
+ end
+
+ def strip_whitespace(str)
+ # Trim the leading spaces
+ spaces = str[/\A\s+/, 0] || ""
+ str.gsub(/^#{spaces}/, "")
+ end
+
+ def install_gemfile(*args)
+ opts = args.last.is_a?(Hash) ? args.pop : {}
+ gemfile(*args)
+ bundle :install, opts
+ end
+
+ def lock_gemfile(*args)
+ gemfile(*args)
+ opts = args.last.is_a?(Hash) ? args.last : {}
+ bundle :lock, opts
+ end
+
+ def base_system_gems(*names, **options)
+ system_gems names.map {|name| find_base_path(name) }, **options
+ end
+
+ def system_gems(*gems)
+ gems = gems.flatten
+ options = gems.last.is_a?(Hash) ? gems.pop : {}
+ install_dir = options.fetch(:path, system_gem_path)
+ default = options.fetch(:default, false)
+ gems.each do |g|
+ gem_name = g.to_s
+ bundler = gem_name.match(/\Abundler-(?<version>.*)\z/)
+
+ if bundler
+ with_built_bundler(bundler[:version], released: options.fetch(:released, false)) {|gem_path| install_gem(gem_path, install_dir, default) }
+ elsif %r{\A(?:[a-zA-Z]:)?/.*\.gem\z}.match?(gem_name)
+ install_gem(gem_name, install_dir, default)
+ else
+ gem_repo = options.fetch(:gem_repo, gem_repo1)
+ install_gem("#{gem_repo}/gems/#{gem_name}.gem", install_dir, default)
+ end
+ end
+ end
+
+ def self.install_dev_bundler
+ extend self
+
+ with_built_bundler(nil, build_path: tmp_root) {|gem_path| install_gem(gem_path, pristine_system_gem_path) }
+ end
+
+ def install_gem(path, install_dir, default = false)
+ raise ArgumentError, "`#{path}` does not exist!" unless File.exist?(path)
+
+ require "rubygems/installer"
+
+ with_simulated_platform do
+ installer = Gem::Installer.at(
+ path.to_s,
+ install_dir: install_dir.to_s,
+ document: [],
+ ignore_dependencies: true,
+ wrappers: true,
+ env_shebang: true,
+ force: true
+ )
+ installer.install
+ end
+
+ if default
+ gem = Pathname.new(path).basename.to_s.match(/(.*)\.gem/)[1]
+
+ # Revert Gem::Installer#write_spec and apply Gem::Installer#write_default_spec
+ FileUtils.mkdir_p File.join(install_dir, "specifications", "default")
+ File.rename File.join(install_dir, "specifications", gem + ".gemspec"),
+ File.join(install_dir, "specifications", "default", gem + ".gemspec")
+
+ # Revert Gem::Installer#write_cache_file
+ File.delete File.join(install_dir, "cache", gem + ".gem")
+ end
+ end
+
+ def uninstall_gem(name, options = {})
+ require "rubygems/uninstaller"
+
+ gem_home = options.dig(:env, "GEM_HOME") || system_gem_path.to_s
+
+ with_env_vars("GEM_HOME" => gem_home) do
+ Gem.clear_paths
+
+ uninstaller = Gem::Uninstaller.new(
+ name,
+ ignore: true,
+ executables: true,
+ all: true
+ )
+ uninstaller.uninstall
+ ensure
+ Gem.clear_paths
+ end
+ end
+
+ def installed_gems_list(options = {})
+ gem_home = options.dig(:env, "GEM_HOME") || system_gem_path.to_s
+
+ # Temporarily set GEM_HOME for the command
+ old_gem_home = ENV["GEM_HOME"]
+ ENV["GEM_HOME"] = gem_home
+ Gem.clear_paths
+
+ begin
+ require "rubygems/commands/list_command"
+
+ # Capture output from the list command
+ require "stringio"
+ output_io = StringIO.new
+ cmd = Gem::Commands::ListCommand.new
+ cmd.ui = Gem::StreamUI.new(StringIO.new, output_io, StringIO.new, false)
+ cmd.invoke
+ output = output_io.string.strip
+ ensure
+ ENV["GEM_HOME"] = old_gem_home
+ Gem.clear_paths
+ end
+
+ # Create a fake command execution so `out` helper works
+ command_execution = Spec::CommandExecution.new("gem list", timeout: 60)
+ command_execution.original_stdout << output
+ command_execution.exitstatus = 0
+ command_executions << command_execution
+
+ output
+ end
+
+ def with_built_bundler(version = nil, opts = {}, &block)
+ require_relative "builders"
+
+ Builders::BundlerBuilder.new(self, "bundler", version)._build(opts, &block)
+ end
+
+ def with_gem_path_as(path)
+ without_env_side_effects do
+ ENV["GEM_HOME"] = path.to_s
+ ENV["GEM_PATH"] = path.to_s
+ ENV["BUNDLER_ORIG_GEM_HOME"] = nil
+ ENV["BUNDLER_ORIG_GEM_PATH"] = nil
+ yield
+ end
+ end
+
+ def with_path_as(path)
+ without_env_side_effects do
+ ENV["PATH"] = path.to_s
+ ENV["BUNDLER_ORIG_PATH"] = nil
+ yield
+ end
+ end
+
+ def without_env_side_effects
+ backup = ENV.to_hash
+ yield
+ ensure
+ ENV.replace(backup)
+ end
+
+ # Simulate the platform set by BUNDLER_SPEC_PLATFORM for in-process
+ # operations, mirroring what hax.rb does for subprocesses.
+ def with_simulated_platform
+ spec_platform = ENV["BUNDLER_SPEC_PLATFORM"]
+ unless spec_platform
+ return yield
+ end
+
+ old_arch = RbConfig::CONFIG["arch"]
+ old_host_os = RbConfig::CONFIG["host_os"]
+
+ if /mingw|mswin/.match?(spec_platform)
+ Gem.class_variable_set(:@@win_platform, nil) # rubocop:disable Style/ClassVars
+ RbConfig::CONFIG["host_os"] = spec_platform.gsub(/^[^-]+-/, "").tr("-", "_")
+ end
+
+ RbConfig::CONFIG["arch"] = spec_platform
+ Gem::Platform.instance_variable_set(:@local, nil)
+ Gem.instance_variable_set(:@platforms, [])
+
+ yield
+ ensure
+ if spec_platform
+ RbConfig::CONFIG["arch"] = old_arch
+ RbConfig::CONFIG["host_os"] = old_host_os
+ Gem::Platform.instance_variable_set(:@local, nil)
+ Gem.instance_variable_set(:@platforms, [])
+ end
+ end
+
+ def with_path_added(path)
+ with_path_as([path.to_s, ENV["PATH"]].join(File::PATH_SEPARATOR)) do
+ yield
+ end
+ end
+
+ def break_git!
+ FileUtils.mkdir_p(tmp("broken_path"))
+ File.open(tmp("broken_path/git"), "w", 0o755) do |f|
+ f.puts "#!/usr/bin/env ruby\nSTDERR.puts 'This is not the git you are looking for'\nexit 1"
+ end
+
+ ENV["PATH"] = "#{tmp("broken_path")}:#{ENV["PATH"]}"
+ end
+
+ def with_fake_man
+ FileUtils.mkdir_p(tmp("fake_man"))
+ create_file(tmp("fake_man/man"), <<~SCRIPT)
+ #!/usr/bin/env ruby
+ puts ARGV.inspect
+ SCRIPT
+ with_path_added(tmp("fake_man")) { yield }
+ end
+
+ def pristine_system_gems(*gems)
+ FileUtils.rm_r(system_gem_path)
+
+ if gems.any?
+ system_gems(*gems)
+ else
+ default_system_gems
+ end
+ end
+
+ def cache_gems(*gems, gem_repo: gem_repo1)
+ gems = gems.flatten
+
+ FileUtils.mkdir_p("#{bundled_app}/vendor/cache")
+
+ gems.each do |g|
+ path = "#{gem_repo}/gems/#{g}.gem"
+ raise ArgumentError, "`#{path}` does not exist!" unless File.exist?(path)
+ FileUtils.cp(path, "#{bundled_app}/vendor/cache")
+ end
+ end
+
+ def simulate_new_machine
+ FileUtils.rm_r bundled_app(".bundle")
+ pristine_system_gems
+ end
+
+ def default_system_gems
+ FileUtils.cp_r pristine_system_gem_path, system_gem_path
+ end
+
+ def simulate_ruby_platform(ruby_platform)
+ old = ENV["BUNDLER_SPEC_RUBY_PLATFORM"]
+ ENV["BUNDLER_SPEC_RUBY_PLATFORM"] = ruby_platform.to_s
+ yield
+ ensure
+ ENV["BUNDLER_SPEC_RUBY_PLATFORM"] = old
+ end
+
+ def simulate_platform(platform)
+ old = ENV["BUNDLER_SPEC_PLATFORM"]
+ ENV["BUNDLER_SPEC_PLATFORM"] = platform.to_s
+ yield
+ ensure
+ ENV["BUNDLER_SPEC_PLATFORM"] = old if block_given?
+ end
+
+ def current_ruby_minor
+ Gem.ruby_version.segments.tap {|s| s.delete_at(2) }.join(".")
+ end
+
+ def next_ruby_minor
+ ruby_major_minor.map.with_index {|s, i| i == 1 ? s + 1 : s }.join(".")
+ end
+
+ def ruby_major_minor
+ Gem.ruby_version.segments[0..1]
+ end
+
+ def revision_for(path)
+ git("rev-parse HEAD", path).strip
+ end
+
+ def with_read_only(pattern)
+ chmod = lambda do |dirmode, filemode|
+ lambda do |f|
+ mode = File.directory?(f) ? dirmode : filemode
+ File.chmod(mode, f)
+ end
+ end
+
+ Dir[pattern].each(&chmod[0o555, 0o444])
+ yield
+ ensure
+ Dir[pattern].each(&chmod[0o755, 0o644])
+ end
+
+ # Simulate replacing TODOs with real values
+ def prepare_gemspec(pathname)
+ process_file(pathname) do |line|
+ case line
+ when /spec\.metadata\["(?:allowed_push_host|homepage_uri|source_code_uri|changelog_uri)"\]/, /spec\.homepage/
+ line.gsub(/\=.*$/, '= "http://example.org"')
+ when /spec\.summary/
+ line.gsub(/\=.*$/, '= "A short summary of my new gem."')
+ when /spec\.description/
+ line.gsub(/\=.*$/, '= "A longer description of my new gem."')
+ else
+ line
+ end
+ end
+ end
+
+ def process_file(pathname)
+ changed_lines = pathname.readlines.map do |line|
+ yield line
+ end
+ File.open(pathname, "w") {|file| file.puts(changed_lines.join) }
+ end
+
+ def with_env_vars(env_hash, &block)
+ current_values = {}
+ env_hash.each do |k, v|
+ current_values[k] = ENV[k]
+ ENV[k] = v
+ end
+ block.call if block_given?
+ env_hash.each do |k, _|
+ ENV[k] = current_values[k]
+ end
+ end
+
+ def require_rack_test
+ # need to hack, so we can require rack for testing
+ old_gem_home = ENV["GEM_HOME"]
+ ENV["GEM_HOME"] = Spec::Path.scoped_base_system_gem_path.to_s
+ require "rack/test"
+ ENV["GEM_HOME"] = old_gem_home
+ end
+
+ def exit_status_for_signal(signal_number)
+ # For details see: https://en.wikipedia.org/wiki/Exit_status#Shell_and_scripts
+ 128 + signal_number
+ end
+
+ def empty_repo4
+ FileUtils.rm_r gem_repo4
+
+ build_repo4 {}
+ end
+
+ private
+
+ def match_source(contents)
+ match = /source ["']?(?<source>http[^"']+)["']?/.match(contents)
+ return unless match
+
+ @gemfile_source = match[:source]
+ end
+
+ def git_root_dir?
+ root.to_s == `git rev-parse --show-toplevel`.chomp
+ end
+ end
+end
diff --git a/spec/bundler/support/indexes.rb b/spec/bundler/support/indexes.rb
new file mode 100644
index 0000000000..1fbdd49abe
--- /dev/null
+++ b/spec/bundler/support/indexes.rb
@@ -0,0 +1,424 @@
+# frozen_string_literal: true
+
+module Spec
+ module Indexes
+ def dep(name, reqs = nil)
+ @deps ||= []
+ @deps << Bundler::Dependency.new(name, reqs)
+ end
+
+ def platform(*args)
+ @platforms ||= []
+ @platforms.concat args.map {|p| Gem::Platform.new(p) }
+ end
+
+ alias_method :platforms, :platform
+
+ def resolve(args = [], dependency_api_available: true)
+ @platforms ||= ["ruby"]
+ default_source = instance_double("Bundler::Source::Rubygems", specs: @index, to_s: "locally install gems", dependency_api_available?: dependency_api_available)
+ source_requirements = { default: default_source }
+ base = args[0] || Bundler::SpecSet.new([])
+ base.each {|ls| ls.source = default_source }
+ gem_version_promoter = args[1] || Bundler::GemVersionPromoter.new
+ originally_locked = args[2] || Bundler::SpecSet.new([])
+ unlock = args[3] || []
+ @deps.each do |d|
+ name = d.name
+ source_requirements[name] = d.source = default_source
+ end
+ packages = Bundler::Resolver::Base.new(source_requirements, @deps, base, @platforms, locked_specs: originally_locked, unlock: unlock)
+ Bundler::Resolver.new(packages, gem_version_promoter).start
+ end
+
+ def should_not_resolve
+ expect { resolve }.to raise_error(Bundler::GemNotFound)
+ end
+
+ def should_resolve_as(specs)
+ got = resolve
+ got = got.map(&:full_name).sort
+ expect(got).to eq(specs.sort)
+ end
+
+ def should_resolve_without_dependency_api(specs)
+ got = resolve(dependency_api_available: false)
+ got = got.map(&:full_name).sort
+ expect(got).to eq(specs.sort)
+ end
+
+ def should_resolve_and_include(specs, args = [])
+ got = resolve(args)
+ got = got.map(&:full_name).sort
+ specs.each do |s|
+ expect(got).to include(s)
+ end
+ end
+
+ def gem(*args, &blk)
+ build_spec(*args, &blk).first
+ end
+
+ def locked(*args)
+ Bundler::SpecSet.new(args.map do |name, version|
+ gem(name, version)
+ end)
+ end
+
+ def should_conservative_resolve_and_include(opts, unlock, specs)
+ opts = Array(opts)
+ search = Bundler::GemVersionPromoter.new.tap do |s|
+ s.level = opts.first
+ s.strict = opts.include?(:strict)
+ end
+ should_resolve_and_include specs, [@base, search, @locked, unlock]
+ end
+
+ def an_awesome_index
+ build_index do
+ gem "myrack", %w[0.8 0.9 0.9.1 0.9.2 1.0 1.1]
+ gem "myrack-mount", %w[0.4 0.5 0.5.1 0.5.2 0.6]
+
+ # --- Pre-release support
+ gem "RubyGems\0", ["1.3.2"]
+
+ # --- Rails
+ versions "1.2.3 2.2.3 2.3.5 3.0.0.beta 3.0.0.beta1" do |version|
+ gem "activesupport", version
+ gem "actionpack", version do
+ dep "activesupport", version
+ if version >= v("3.0.0.beta")
+ dep "myrack", "~> 1.1"
+ dep "myrack-mount", ">= 0.5"
+ elsif version > v("2.3") then dep "myrack", "~> 1.0.0"
+ elsif version > v("2.0.0") then dep "myrack", "~> 0.9.0"
+ end
+ end
+ gem "activerecord", version do
+ dep "activesupport", version
+ dep "arel", ">= 0.2" if version >= v("3.0.0.beta")
+ end
+ gem "actionmailer", version do
+ dep "activesupport", version
+ dep "actionmailer", version
+ end
+ if version < v("3.0.0.beta")
+ gem "railties", version do
+ dep "activerecord", version
+ dep "actionpack", version
+ dep "actionmailer", version
+ dep "activesupport", version
+ end
+ else
+ gem "railties", version
+ gem "rails", version do
+ dep "activerecord", version
+ dep "actionpack", version
+ dep "actionmailer", version
+ dep "activesupport", version
+ dep "railties", version
+ end
+ end
+ end
+
+ versions "1.0 1.2 1.2.1 1.2.2 1.3 1.3.0.1 1.3.5 1.4.0 1.4.2 1.4.2.1" do |version|
+ platforms "ruby java mswin32 mingw32 x64-mingw-ucrt" do |platform|
+ next if version == v("1.4.2.1") && platform != pl("x86-mswin32")
+ next if version == v("1.4.2") && platform == pl("x86-mswin32")
+ gem "nokogiri", version, platform do
+ dep "weakling", ">= 0.0.3" if platform =~ pl("java") # rubocop:disable Performance/RegexpMatch
+ end
+ end
+ end
+
+ versions "0.0.1 0.0.2 0.0.3" do |version|
+ gem "weakling", version
+ end
+
+ # --- Rails related
+ versions "1.2.3 2.2.3 2.3.5" do |version|
+ gem "activemerchant", version do
+ dep "activesupport", ">= #{version}"
+ end
+ end
+
+ gem "reform", ["1.0.0"] do
+ dep "activesupport", ">= 1.0.0.beta1"
+ end
+
+ gem "need-pre", ["1.0.0"] do
+ dep "activesupport", "~> 3.0.0.beta1"
+ end
+ end
+ end
+
+ # Builder 3.1.4 will activate first, but if all
+ # goes well, it should resolve to 3.0.4
+ def a_conflict_index
+ build_index do
+ gem "builder", %w[3.0.4 3.1.4]
+ gem("grape", "0.2.6") do
+ dep "builder", ">= 0"
+ end
+
+ versions "3.2.8 3.2.9 3.2.10 3.2.11" do |version|
+ gem("activemodel", version) do
+ dep "builder", "~> 3.0.0"
+ end
+ end
+
+ gem("my_app", "1.0.0") do
+ dep "activemodel", ">= 0"
+ dep "grape", ">= 0"
+ end
+ end
+ end
+
+ def a_complex_conflict_index
+ build_index do
+ gem("a", %w[1.0.2 1.1.4 1.2.0 1.4.0]) do
+ dep "d", ">= 0"
+ end
+
+ gem("d", %w[1.3.0 1.4.1]) do
+ dep "x", ">= 0"
+ end
+
+ gem "d", "0.9.8"
+
+ gem("b", "0.3.4") do
+ dep "a", ">= 1.5.0"
+ end
+
+ gem("b", "0.3.5") do
+ dep "a", ">= 1.2"
+ end
+
+ gem("b", "0.3.3") do
+ dep "a", "> 1.0"
+ end
+
+ versions "3.2 3.3" do |version|
+ gem("c", version) do
+ dep "a", "~> 1.0"
+ end
+ end
+
+ gem("my_app", "1.3.0") do
+ dep "c", ">= 4.0"
+ dep "b", ">= 0"
+ end
+
+ gem("my_app", "1.2.0") do
+ dep "c", "~> 3.3.0"
+ dep "b", "0.3.4"
+ end
+
+ gem("my_app", "1.1.0") do
+ dep "c", "~> 3.2.0"
+ dep "b", "0.3.5"
+ end
+ end
+ end
+
+ def index_with_conflict_on_child
+ build_index do
+ gem "json", %w[1.6.5 1.7.7 1.8.0]
+
+ gem("chef", "10.26") do
+ dep "json", [">= 1.4.4", "<= 1.7.7"]
+ end
+
+ gem("berkshelf", "2.0.7") do
+ dep "json", ">= 1.7.7"
+ end
+
+ gem("chef_app", "1.0.0") do
+ dep "berkshelf", "~> 2.0"
+ dep "chef", "~> 10.26"
+ end
+ end
+ end
+
+ # Issue #3459
+ def a_complicated_index
+ build_index do
+ gem "foo", %w[3.0.0 3.0.5] do
+ dep "qux", ["~> 3.1"]
+ dep "baz", ["< 9.0", ">= 5.0"]
+ dep "bar", ["~> 1.0"]
+ dep "grault", ["~> 3.1"]
+ end
+
+ gem "foo", "1.2.1" do
+ dep "baz", ["~> 4.2"]
+ dep "bar", ["~> 1.0"]
+ dep "qux", ["~> 3.1"]
+ dep "grault", ["~> 2.0"]
+ end
+
+ gem "bar", "1.0.5" do
+ dep "grault", ["~> 3.1"]
+ dep "baz", ["< 9", ">= 4.2"]
+ end
+
+ gem "bar", "1.0.3" do
+ dep "baz", ["< 9", ">= 4.2"]
+ dep "grault", ["~> 2.0"]
+ end
+
+ gem "baz", "8.2.10" do
+ dep "grault", ["~> 3.0"]
+ dep "garply", [">= 0.5.1", "~> 0.5"]
+ end
+
+ gem "baz", "5.0.2" do
+ dep "grault", ["~> 2.0"]
+ dep "garply", [">= 0.3.1"]
+ end
+
+ gem "baz", "4.2.0" do
+ dep "grault", ["~> 2.0"]
+ dep "garply", [">= 0.3.1"]
+ end
+
+ gem "grault", %w[2.6.3 3.1.1]
+
+ gem "garply", "0.5.1" do
+ dep "waldo", ["~> 0.1.3"]
+ end
+
+ gem "waldo", "0.1.5" do
+ dep "plugh", ["~> 0.6.0"]
+ end
+
+ gem "plugh", %w[0.6.3 0.6.11 0.7.0]
+
+ gem "qux", "3.2.21" do
+ dep "plugh", [">= 0.6.4", "~> 0.6"]
+ dep "corge", ["~> 1.0"]
+ end
+
+ gem "corge", "1.10.1"
+ end
+ end
+
+ def a_unresolvable_child_index
+ build_index do
+ gem "json", %w[1.8.0]
+
+ gem("chef", "10.26") do
+ dep "json", [">= 1.4.4", "<= 1.7.7"]
+ end
+
+ gem("berkshelf", "2.0.7") do
+ dep "json", ">= 1.7.7"
+ end
+
+ gem("chef_app_error", "1.0.0") do
+ dep "berkshelf", "~> 2.0"
+ dep "chef", "~> 10.26"
+ end
+ end
+ end
+
+ def a_index_with_root_conflict_on_child
+ build_index do
+ gem "builder", %w[2.1.2 3.0.1 3.1.3]
+ gem "i18n", %w[0.4.1 0.4.2]
+
+ gem "activesupport", %w[3.0.0 3.0.1 3.0.5 3.1.7]
+
+ gem("activemodel", "3.0.5") do
+ dep "activesupport", "= 3.0.5"
+ dep "builder", "~> 2.1.2"
+ dep "i18n", "~> 0.4"
+ end
+
+ gem("activemodel", "3.0.0") do
+ dep "activesupport", "= 3.0.0"
+ dep "builder", "~> 2.1.2"
+ dep "i18n", "~> 0.4.1"
+ end
+
+ gem("activemodel", "3.1.3") do
+ dep "activesupport", "= 3.1.3"
+ dep "builder", "~> 2.1.2"
+ dep "i18n", "~> 0.5"
+ end
+
+ gem("activerecord", "3.0.0") do
+ dep "activesupport", "= 3.0.0"
+ dep "activemodel", "= 3.0.0"
+ end
+
+ gem("activerecord", "3.0.5") do
+ dep "activesupport", "= 3.0.5"
+ dep "activemodel", "= 3.0.5"
+ end
+
+ gem("activerecord", "3.0.9") do
+ dep "activesupport", "= 3.1.5"
+ dep "activemodel", "= 3.1.5"
+ end
+ end
+ end
+
+ def a_circular_index
+ build_index do
+ gem "myrack", "1.0.1"
+ gem("foo", "0.2.6") do
+ dep "bar", ">= 0"
+ end
+
+ gem("bar", "1.0.0") do
+ dep "foo", ">= 0"
+ end
+
+ gem("circular_app", "1.0.0") do
+ dep "foo", ">= 0"
+ dep "bar", ">= 0"
+ end
+ end
+ end
+
+ def an_ambiguous_index
+ build_index do
+ gem("a", "1.0.0") do
+ dep "c", ">= 0"
+ end
+
+ gem("b", %w[0.5.0 1.0.0])
+
+ gem("b", "2.0.0") do
+ dep "c", "< 2.0.0"
+ end
+
+ gem("c", "1.0.0") do
+ dep "d", "1.0.0"
+ end
+
+ gem("c", "2.0.0") do
+ dep "d", "2.0.0"
+ end
+
+ gem("d", %w[1.0.0 2.0.0])
+ end
+ end
+
+ def optional_prereleases_index
+ build_index do
+ gem("a", %w[1.0.0])
+
+ gem("a", "2.0.0") do
+ dep "b", ">= 2.0.0.pre"
+ end
+
+ gem("b", %w[0.9.0 1.5.0 2.0.0.pre])
+
+ # --- Pre-release support
+ gem "RubyGems\0", ["1.3.2"]
+ end
+ end
+ end
+end
diff --git a/spec/bundler/support/matchers.rb b/spec/bundler/support/matchers.rb
new file mode 100644
index 0000000000..5a3c38a4db
--- /dev/null
+++ b/spec/bundler/support/matchers.rb
@@ -0,0 +1,227 @@
+# frozen_string_literal: true
+
+require "forwardable"
+require_relative "the_bundle"
+
+module Spec
+ module Matchers
+ extend RSpec::Matchers
+
+ class Precondition
+ include RSpec::Matchers::Composable
+ extend Forwardable
+ def_delegators :failing_matcher,
+ :failure_message,
+ :actual,
+ :description,
+ :diffable?,
+ :expected,
+ :failure_message_when_negated
+
+ def initialize(matcher, preconditions)
+ @matcher = with_matchers_cloned(matcher)
+ @preconditions = with_matchers_cloned(preconditions)
+ @failure_index = nil
+ end
+
+ def matches?(target, &blk)
+ return false if @failure_index = @preconditions.index {|pc| !pc.matches?(target, &blk) }
+ @matcher.matches?(target, &blk)
+ end
+
+ def does_not_match?(target, &blk)
+ return false if @failure_index = @preconditions.index {|pc| !pc.matches?(target, &blk) }
+ if @matcher.respond_to?(:does_not_match?)
+ @matcher.does_not_match?(target, &blk)
+ else
+ !@matcher.matches?(target, &blk)
+ end
+ end
+
+ def expects_call_stack_jump?
+ @matcher.expects_call_stack_jump? || @preconditions.any?(&:expects_call_stack_jump)
+ end
+
+ def supports_block_expectations?
+ @matcher.supports_block_expectations? || @preconditions.any?(&:supports_block_expectations)
+ end
+
+ def failing_matcher
+ @failure_index ? @preconditions[@failure_index] : @matcher
+ end
+ end
+
+ def self.define_compound_matcher(matcher, preconditions, &declarations)
+ raise ArgumentError, "Must have preconditions to define a compound matcher" if preconditions.empty?
+ define_method(matcher) do |*expected, &block_arg|
+ Precondition.new(
+ RSpec::Matchers::DSL::Matcher.new(matcher, declarations, self, *expected, &block_arg),
+ preconditions
+ )
+ end
+ end
+
+ RSpec::Matchers.define :have_dep do |*args|
+ dep = Bundler::Dependency.new(*args)
+
+ match do |actual|
+ actual.length == 1 && actual.all? {|d| d == dep }
+ end
+ end
+
+ RSpec::Matchers.define :have_gem do |*args|
+ match do |actual|
+ actual.length == args.length && actual.all? {|a| args.include?(a.full_name) }
+ end
+ end
+
+ RSpec::Matchers.define :be_sorted do
+ diffable
+ attr_reader :expected
+ match do |actual|
+ expected = block_arg ? actual.sort_by(&block_arg) : actual.sort
+ actual.==(expected).tap do
+ # HACK: since rspec won't show a diff when everything is a string
+ differ = RSpec::Support::Differ.new
+ @actual = differ.send(:object_to_string, actual)
+ @expected = differ.send(:object_to_string, expected)
+ end
+ end
+ end
+
+ RSpec::Matchers.define :be_well_formed do
+ match(&:empty?)
+
+ failure_message do |actual|
+ actual.join("\n")
+ end
+ end
+
+ define_compound_matcher :read_as, [exist] do |file_contents|
+ diffable
+
+ match do |actual|
+ @actual = Bundler.read_file(actual)
+ values_match?(file_contents, @actual)
+ end
+ end
+
+ def indent(string, padding = 4, indent_character = " ")
+ string.to_s.gsub(/^/, indent_character * padding).gsub("\t", " ")
+ end
+
+ define_compound_matcher :include_gems, [be_an_instance_of(Spec::TheBundle)] do |*names|
+ match do
+ opts = names.last.is_a?(Hash) ? names.pop : {}
+ source = opts.delete(:source)
+ groups = Array(opts.delete(:groups)).map(&:inspect).join(", ")
+ opts[:raise_on_error] = false
+ @errors = names.filter_map do |full_name|
+ name, version, platform = full_name.split(/\s+/)
+ platform ||= "ruby"
+ require_path = name.tr("-", "/")
+ version_const = name == "bundler" ? "Bundler::VERSION" : Spec::Builders.constantize(name)
+ source_const = "#{Spec::Builders.constantize(name)}_SOURCE"
+ ruby <<~R, opts
+ require 'bundler'
+ Bundler.setup(#{groups})
+
+ require '#{require_path}'
+ actual_version, actual_platform = #{version_const}.split(/\s+/, 2)
+ actual_platform ||= "ruby"
+ unless Gem::Version.new(actual_version) == Gem::Version.new('#{version}')
+ puts actual_version
+ exit 64
+ end
+ unless actual_platform.to_s == '#{platform}'
+ puts actual_platform
+ exit 65
+ end
+ require '#{require_path}/source'
+ exit 0 if #{source.nil?}
+ actual_source = #{source_const}
+ unless actual_source == '#{source}'
+ puts actual_source
+ exit 66
+ end
+ R
+ next if exitstatus == 0
+ if exitstatus == 64
+ actual_version = out.split("\n").last
+ next "#{name} was expected to be at version #{version} but was #{actual_version}"
+ end
+ if exitstatus == 65
+ actual_platform = out.split("\n").last
+ next "#{name} was expected to be of platform #{platform} but was #{actual_platform}"
+ end
+ if exitstatus == 66
+ actual_source = out.split("\n").last
+ next "Expected #{name} (#{version}) to be installed from `#{source}`, was actually from `#{actual_source}`"
+ end
+ next "Command to check for inclusion of gem #{full_name} failed"
+ end
+
+ @errors.empty?
+ end
+
+ match_when_negated do
+ opts = names.last.is_a?(Hash) ? names.pop : {}
+ groups = Array(opts.delete(:groups)).map(&:inspect).join(", ")
+ opts[:raise_on_error] = false
+ @errors = names.filter_map do |name|
+ name, version = name.split(/\s+/, 2)
+ ruby <<-R, opts
+ begin
+ require 'bundler'
+ Bundler.setup(#{groups})
+ rescue Bundler::GemNotFound, Bundler::GitError
+ exit 0
+ end
+
+ begin
+ require '#{name}'
+ name_constant = #{Spec::Builders.constantize(name)}
+ if #{version.nil?} || name_constant == '#{version}'
+ exit 64
+ else
+ exit 0
+ end
+ rescue LoadError, NameError
+ exit 0
+ end
+ R
+ next if exitstatus == 0
+ next "command to check version of #{name} installed failed" unless exitstatus == 64
+ next "expected #{name} to not be installed, but it was" if version.nil?
+ next "expected #{name} (#{version}) not to be installed, but it was"
+ end
+
+ @errors.empty?
+ end
+
+ failure_message do
+ super() + " but:\n" + @errors.map {|e| indent(e) }.join("\n")
+ end
+
+ failure_message_when_negated do
+ super() + " but:\n" + @errors.map {|e| indent(e) }.join("\n")
+ end
+ end
+ RSpec::Matchers.define_negated_matcher :not_include_gems, :include_gems
+ RSpec::Matchers.alias_matcher :include_gem, :include_gems
+
+ def plugin_should_be_installed(*names)
+ names.each do |name|
+ expect(Bundler::Plugin).to be_installed(name)
+ path = Pathname.new(Bundler::Plugin.installed?(name))
+ expect(path + "plugins.rb").to exist
+ end
+ end
+
+ def plugin_should_not_be_installed(*names)
+ names.each do |name|
+ expect(Bundler::Plugin).not_to be_installed(name)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/support/options.rb b/spec/bundler/support/options.rb
new file mode 100644
index 0000000000..551fa1acd8
--- /dev/null
+++ b/spec/bundler/support/options.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Spec
+ module Options
+ def opt_add(option, options)
+ [option.strip, options].compact.reject(&:empty?).join(" ")
+ end
+
+ def opt_remove(option, options)
+ return unless options
+
+ options.split(" ").reject {|opt| opt.strip == option.strip }.join(" ")
+ end
+ end
+end
diff --git a/spec/bundler/support/path.rb b/spec/bundler/support/path.rb
new file mode 100644
index 0000000000..2e6486412f
--- /dev/null
+++ b/spec/bundler/support/path.rb
@@ -0,0 +1,381 @@
+# frozen_string_literal: true
+
+require "pathname" unless defined?(Pathname)
+require "rbconfig"
+
+require_relative "env"
+
+module Spec
+ module Path
+ include Spec::Env
+
+ def source_root
+ @source_root ||= Pathname.new(ruby_core? ? "../../.." : "../../bundler").expand_path(__dir__)
+ end
+
+ def root
+ @root ||= system_gem_path("gems/bundler-#{Bundler::VERSION}")
+ end
+
+ def gemspec
+ @gemspec ||= source_root.join(relative_gemspec)
+ end
+
+ def relative_gemspec
+ @relative_gemspec ||= ruby_core? ? "lib/bundler/bundler.gemspec" : "bundler.gemspec"
+ end
+
+ def loaded_gemspec
+ @loaded_gemspec ||= Dir.chdir(source_root) { Gem::Specification.load(gemspec.to_s) }
+ end
+
+ def test_gemfile
+ @test_gemfile ||= tool_dir.join("test_gems.rb")
+ end
+
+ def rubocop_gemfile
+ @rubocop_gemfile ||= source_root.join(rubocop_gemfile_basename)
+ end
+
+ def standard_gemfile
+ @standard_gemfile ||= source_root.join(standard_gemfile_basename)
+ end
+
+ def dev_gemfile
+ @dev_gemfile ||= tool_dir.join("dev_gems.rb")
+ end
+
+ def dev_binstub
+ @dev_binstub ||= bindir.join("bundle")
+ end
+
+ def bindir
+ @bindir ||= source_root.join(ruby_core? ? "spec/bin" : "../bin")
+ end
+
+ def exedir
+ @exedir ||= source_root.join(ruby_core? ? "libexec" : "exe")
+ end
+
+ def installed_bindir
+ @installed_bindir ||= system_gem_path("bin")
+ end
+
+ def gem_cmd
+ @gem_cmd ||= ruby_core? ? source_root.join("bin/gem") : "gem"
+ end
+
+ def gem_bin
+ @gem_bin ||= ENV["GEM_COMMAND"] || "gem"
+ end
+
+ def path
+ env_path = ENV["PATH"]
+ env_path = env_path.split(File::PATH_SEPARATOR).reject {|path| path == exedir.to_s }.join(File::PATH_SEPARATOR) if ruby_core?
+ env_path
+ end
+
+ def spec_dir
+ @spec_dir ||= source_root.join(ruby_core? ? "spec/bundler" : "../spec")
+ end
+
+ def man_dir
+ @man_dir ||= lib_dir.join("bundler/man")
+ end
+
+ def hax
+ @hax ||= spec_dir.join("support/hax.rb")
+ end
+
+ def tracked_files
+ @tracked_files ||= git_ls_files(tracked_files_glob)
+ end
+
+ def shipped_files
+ @shipped_files ||= if ruby_core_tarball?
+ loaded_gemspec.files.map {|f| f.gsub(%r{^exe/}, "libexec/") }
+ elsif ruby_core?
+ tracked_files
+ else
+ loaded_gemspec.files
+ end
+ end
+
+ def lib_tracked_files
+ @lib_tracked_files ||= git_ls_files(lib_tracked_files_glob)
+ end
+
+ def man_tracked_files
+ @man_tracked_files ||= git_ls_files(man_tracked_files_glob)
+ end
+
+ def tmp(*path)
+ tmp_root.join("#{test_env_version}.#{scope}").join(*path)
+ end
+
+ def tmp_root
+ if ruby_core? && (tmpdir = ENV["TMPDIR"])
+ # Use realpath to resolve any symlinks in TMPDIR (e.g., on macOS /var -> /private/var)
+ real = begin
+ File.realpath(tmpdir)
+ rescue Errno::ENOENT, Errno::EACCES
+ tmpdir
+ end
+ Pathname(real)
+ else
+ (ruby_core? ? source_root : source_root.parent).join("tmp")
+ end
+ end
+
+ # Bump this version whenever you make a breaking change to the spec setup
+ # that requires regenerating tmp/.
+
+ def test_env_version
+ 2
+ end
+
+ def scope
+ test_number = ENV["TEST_ENV_NUMBER"]
+ return "1" if test_number.nil?
+
+ test_number.empty? ? "1" : test_number
+ end
+
+ def home(*path)
+ tmp("home", *path)
+ end
+
+ def default_bundle_path(*path)
+ system_gem_path(*path)
+ end
+
+ def default_cache_path(*path)
+ default_bundle_path("cache/bundler", *path)
+ end
+
+ def compact_index_cache_path
+ home(".bundle/cache/compact_index")
+ end
+
+ def bundled_app(*path)
+ root = tmp("bundled_app")
+ FileUtils.mkdir_p(root)
+ root.join(*path)
+ end
+
+ def bundled_app2(*path)
+ root = tmp("bundled_app2")
+ FileUtils.mkdir_p(root)
+ root.join(*path)
+ end
+
+ def vendored_gems(path = nil)
+ scoped_gem_path(bundled_app("vendor/bundle")).join(*[path].compact)
+ end
+
+ def cached_gem(path)
+ bundled_app("vendor/cache/#{path}.gem")
+ end
+
+ def bundled_app_gemfile
+ bundled_app("Gemfile")
+ end
+
+ def bundled_app_lock
+ bundled_app("Gemfile.lock")
+ end
+
+ def scoped_base_system_gem_path
+ scoped_gem_path(base_system_gem_path)
+ end
+
+ def base_system_gem_path
+ tmp_root.join("gems/base")
+ end
+
+ def rubocop_gem_path
+ tmp_root.join("gems/rubocop")
+ end
+
+ def standard_gem_path
+ tmp_root.join("gems/standard")
+ end
+
+ def file_uri_for(path)
+ protocol = "file://"
+ root = Gem.win_platform? ? "/" : ""
+
+ protocol + root + path.to_s
+ end
+
+ def gem_repo1(*args)
+ gem_path("remote1", *args)
+ end
+
+ def gem_repo_missing(*args)
+ gem_path("missing", *args)
+ end
+
+ def gem_repo2(*args)
+ gem_path("remote2", *args)
+ end
+
+ def gem_repo3(*args)
+ gem_path("remote3", *args)
+ end
+
+ def gem_repo4(*args)
+ gem_path("remote4", *args)
+ end
+
+ def security_repo(*args)
+ gem_path("security_repo", *args)
+ end
+
+ def system_gem_path(*path)
+ gem_path("system", *path)
+ end
+
+ def pristine_system_gem_path
+ tmp_root.join("gems/pristine_system")
+ end
+
+ def local_gem_path(*path, base: bundled_app)
+ scoped_gem_path(base.join(".bundle")).join(*path)
+ end
+
+ def scoped_gem_path(base)
+ base.join(Gem.ruby_engine, RbConfig::CONFIG["ruby_version"])
+ end
+
+ def gem_path(*args)
+ tmp("gems", *args)
+ end
+
+ def lib_path(*args)
+ tmp("libs", *args)
+ end
+
+ def source_lib_dir
+ source_root.join("lib")
+ end
+
+ def lib_dir
+ root.join("lib")
+ end
+
+ def global_plugin_gem(*args)
+ home ".bundle", "plugin", "gems", *args
+ end
+
+ def local_plugin_gem(*args)
+ bundled_app ".bundle", "plugin", "gems", *args
+ end
+
+ def tmpdir(*args)
+ tmp "tmpdir", *args
+ end
+
+ def replace_version_file(version, dir: source_root)
+ version_file = File.expand_path("lib/bundler/version.rb", dir)
+ contents = File.read(version_file)
+ contents.sub!(/(^\s+VERSION\s*=\s*).*$/, %(\\1"#{version}"))
+ File.open(version_file, "w") {|f| f << contents }
+ end
+
+ def replace_required_ruby_version(version, dir:)
+ gemspec_file = File.expand_path("bundler.gemspec", dir)
+ contents = File.read(gemspec_file)
+ contents.sub!(/(^\s+s\.required_ruby_version\s*=\s*)"[^"]+"/, %(\\1"#{version}"))
+ File.open(gemspec_file, "w") {|f| f << contents }
+ end
+
+ def replace_changelog(version, dir:)
+ changelog = File.expand_path("CHANGELOG.md", dir)
+ contents = File.readlines(changelog)
+ contents = [contents[0], contents[1], "## #{version} (2100-01-01)\n", *contents[3..-1]].join
+ File.open(changelog, "w") {|f| f << contents }
+ end
+
+ def git_root
+ ruby_core? ? source_root : source_root.parent
+ end
+
+ def rake_path
+ find_base_path("rake")
+ end
+
+ def rake_version
+ File.basename(rake_path).delete_prefix("rake-").delete_suffix(".gem")
+ end
+
+ def sinatra_dependency_paths
+ deps = %w[
+ mustermann
+ rack
+ rack-protection
+ rack-session
+ tilt
+ sinatra
+ base64
+ logger
+ compact_index
+ ]
+ path = if deps.all? {|dep| !Dir[scoped_base_system_gem_path.join("gems/#{dep}-*")].empty? }
+ scoped_base_system_gem_path
+ elsif ruby_core? && deps.all? {|dep| !Dir[source_root.join(".bundle/gems/#{dep}-*")].empty? }
+ source_root.join(".bundle")
+ else
+ scoped_base_system_gem_path
+ end
+
+ Dir[path.join("gems/{#{deps.join(",")}}-*/lib")].map(&:to_s)
+ end
+
+ private
+
+ def find_base_path(name)
+ Dir["#{scoped_base_system_gem_path}/**/#{name}-*.gem"].first
+ end
+
+ def git_ls_files(glob)
+ skip "Not running on a git context, since running tests from a tarball" if ruby_core_tarball?
+
+ git("ls-files -z -- #{glob}", source_root).split("\x0")
+ end
+
+ def tracked_files_glob
+ ruby_core? ? "libexec/bundle* lib/bundler lib/bundler.rb spec/bundler man/bundle*" : "lib exe CHANGELOG.md LICENSE.md README.md bundler.gemspec"
+ end
+
+ def lib_tracked_files_glob
+ ruby_core? ? "lib/bundler lib/bundler.rb" : "lib"
+ end
+
+ def man_tracked_files_glob
+ "lib/bundler/man/bundle*.1.ronn lib/bundler/man/gemfile*.5.ronn"
+ end
+
+ def ruby_core_tarball?
+ !git_root.join(".git").directory?
+ end
+
+ def rubocop_gemfile_basename
+ tool_dir.join("rubocop_gems.rb")
+ end
+
+ def standard_gemfile_basename
+ tool_dir.join("standard_gems.rb")
+ end
+
+ def tool_dir
+ ruby_core? ? source_root.join("tool/bundler") : source_root.join("../tool/bundler")
+ end
+
+ def templates_dir
+ lib_dir.join("bundler", "templates")
+ end
+
+ extend self
+ end
+end
diff --git a/spec/bundler/support/permissions.rb b/spec/bundler/support/permissions.rb
new file mode 100644
index 0000000000..b21ce3848d
--- /dev/null
+++ b/spec/bundler/support/permissions.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Spec
+ module Permissions
+ def with_umask(new_umask)
+ old_umask = File.umask(new_umask)
+ yield if block_given?
+ ensure
+ File.umask(old_umask)
+ end
+ end
+end
diff --git a/spec/bundler/support/platforms.rb b/spec/bundler/support/platforms.rb
new file mode 100644
index 0000000000..56a0843005
--- /dev/null
+++ b/spec/bundler/support/platforms.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module Spec
+ module Platforms
+ def not_local
+ generic_local_platform == Gem::Platform::RUBY ? "java" : Gem::Platform::RUBY
+ end
+
+ def local_platform
+ Bundler.local_platform
+ end
+
+ def generic_local_platform
+ Gem::Platform.generic(local_platform)
+ end
+
+ def local_tag
+ if Gem.java_platform?
+ :jruby
+ elsif Gem.win_platform?
+ :windows
+ else
+ :ruby
+ end
+ end
+
+ def not_local_tag
+ [:jruby, :windows, :ruby].find {|tag| tag != local_tag }
+ end
+
+ def local_ruby_engine
+ RUBY_ENGINE
+ end
+
+ def local_engine_version
+ RUBY_ENGINE == "ruby" ? Gem.ruby_version : RUBY_ENGINE_VERSION
+ end
+
+ def not_local_engine_version
+ case not_local_tag
+ when :ruby, :windows
+ not_local_ruby_version
+ when :jruby
+ "1.6.1"
+ end
+ end
+
+ def not_local_ruby_version
+ "1.12"
+ end
+
+ def not_local_patchlevel
+ 9999
+ end
+
+ def default_platform_list(*extra, defaults: default_locked_platforms)
+ defaults.concat(extra).map(&:to_s).uniq
+ end
+
+ def lockfile_platforms(*extra, defaults: default_locked_platforms)
+ platforms = default_platform_list(*extra, defaults: defaults)
+ platforms.sort.join("\n ")
+ end
+
+ def default_locked_platforms
+ [local_platform, generic_default_locked_platform].compact
+ end
+
+ def generic_default_locked_platform
+ return unless Bundler::MatchPlatform.generic_local_platform_is_ruby?
+
+ Gem::Platform::RUBY
+ end
+ end
+end
diff --git a/spec/bundler/support/rubygems_ext.rb b/spec/bundler/support/rubygems_ext.rb
new file mode 100644
index 0000000000..812dc4deaa
--- /dev/null
+++ b/spec/bundler/support/rubygems_ext.rb
@@ -0,0 +1,222 @@
+# frozen_string_literal: true
+
+abort "RubyGems only supports Ruby 3.2 or higher" if RUBY_VERSION < "3.2.0"
+
+require_relative "path"
+
+$LOAD_PATH.unshift(Spec::Path.source_lib_dir.to_s)
+
+module Spec
+ module Rubygems
+ extend self
+
+ def gem_load(gem_name, bin_container)
+ require_relative "switch_rubygems"
+
+ gem_load_and_activate(gem_name, bin_container)
+ end
+
+ def gem_load_and_possibly_install(gem_name, bin_container)
+ require_relative "switch_rubygems"
+
+ gem_load_activate_and_possibly_install(gem_name, bin_container)
+ end
+
+ def gem_require(gem_name, entrypoint)
+ gem_activate(gem_name)
+ require entrypoint
+ end
+
+ def test_setup
+ # Install test dependencies unless parallel-rspec is being used, since in that case they should be setup already
+ install_test_deps unless ENV["RSPEC_FORMATTER_OUTPUT_ID"]
+
+ setup_test_paths
+
+ require "fileutils"
+
+ FileUtils.mkdir_p(Path.home)
+ FileUtils.mkdir_p(Path.tmpdir)
+
+ ENV["HOME"] = Path.home.to_s
+ # Remove "RUBY_CODESIGN", which is used by mkmf-generated Makefile to
+ # sign extension bundles on macOS, to avoid trying to find the specified key
+ # from the fake $HOME/Library/Keychains directory.
+ ENV.delete "RUBY_CODESIGN"
+ if Path.ruby_core?
+ if (tmpdir = ENV["TMPDIR"])
+ tmpdir_real = begin
+ File.realpath(tmpdir)
+ rescue Errno::ENOENT, Errno::EACCES
+ tmpdir
+ end
+ ENV["TMPDIR"] = tmpdir_real if tmpdir_real != tmpdir
+ end
+ else
+ ENV["TMPDIR"] = Path.tmpdir.to_s
+ end
+
+ require "rubygems/user_interaction"
+ Gem::DefaultUserInteraction.ui = Gem::SilentUI.new
+ end
+
+ def setup_test_paths
+ ENV["BUNDLE_PATH"] = nil
+ ENV["PATH"] = [Path.system_gem_path("bin"), ENV["PATH"]].join(File::PATH_SEPARATOR)
+ ENV["PATH"] = [Path.exedir, ENV["PATH"]].join(File::PATH_SEPARATOR) if Path.ruby_core?
+ end
+
+ def install_test_deps
+ dev_bundle("install", gemfile: test_gemfile, path: Path.base_system_gem_path.to_s)
+ dev_bundle("install", gemfile: rubocop_gemfile, path: Path.rubocop_gem_path.to_s)
+ dev_bundle("install", gemfile: standard_gemfile, path: Path.standard_gem_path.to_s)
+
+ require_relative "helpers"
+ Helpers.install_dev_bundler
+
+ install_vendored_compact_index
+ end
+
+ # Vendor `rubygems/rubygems.org#lib/compact_index/` under `tmp/compact_index/`
+ # so the artifice can serve compact-index responses without a runtime gem
+ # dependency. Pinned to a reviewed commit; override with COMPACT_INDEX_REF
+ # to refresh against another ref (the existing vendor copy is discarded).
+ def install_vendored_compact_index
+ target_root = Path.tmp_root.join("compact_index")
+ require "fileutils"
+ FileUtils.mkdir_p(Path.tmp_root)
+
+ files = %w[
+ lib/compact_index.rb
+ lib/compact_index/dependency.rb
+ lib/compact_index/gem.rb
+ lib/compact_index/gem_version.rb
+ lib/compact_index/versions_file.rb
+ ]
+
+ # Serialize installs so parallel test setups don't race on the same
+ # vendor tree, and only skip the download when every file is present so
+ # an interrupted run can't leave a partial copy behind.
+ File.open(Path.tmp_root.join("compact_index.lock"), File::CREAT | File::RDWR) do |lock|
+ lock.flock(File::LOCK_EX)
+
+ FileUtils.rm_rf(target_root) if ENV["COMPACT_INDEX_REF"]
+
+ next if files.all? {|path| File.exist?(target_root.join(path)) }
+
+ require "open-uri"
+ ref = ENV["COMPACT_INDEX_REF"] || "7c68a7b39761c61a66f9299f85b889ec39afc02c"
+ files.each do |path|
+ url = "https://raw.githubusercontent.com/rubygems/rubygems.org/#{ref}/#{path}"
+ target = target_root.join(path)
+ FileUtils.mkdir_p(File.dirname(target))
+ tmp = "#{target}.tmp"
+ File.write(tmp, URI.parse(url).open(&:read))
+ File.rename(tmp, target)
+ end
+ end
+ end
+
+ def check_source_control_changes(success_message:, error_message:)
+ require "open3"
+
+ output, status = Open3.capture2e("git status --porcelain")
+
+ if status.success? && output.empty?
+ puts
+ puts success_message
+ puts
+ else
+ system("git diff")
+
+ puts
+ puts error_message
+ puts
+
+ exit(1)
+ end
+ end
+
+ def dev_bundle(*args, gemfile: dev_gemfile, path: nil)
+ old_gemfile = ENV["BUNDLE_GEMFILE"]
+ old_orig_gemfile = ENV["BUNDLER_ORIG_BUNDLE_GEMFILE"]
+ ENV["BUNDLE_GEMFILE"] = gemfile.to_s
+ ENV["BUNDLER_ORIG_BUNDLE_GEMFILE"] = nil
+
+ if path
+ old_path = ENV["BUNDLE_PATH"]
+ ENV["BUNDLE_PATH"] = path
+ else
+ old_path__system = ENV["BUNDLE_PATH__SYSTEM"]
+ ENV["BUNDLE_PATH__SYSTEM"] = "true"
+ end
+
+ require "shellwords"
+ # We don't use `Open3` here because it does not work on JRuby + Windows
+ output = `ruby #{Path.dev_binstub} #{args.shelljoin}`
+ raise output unless $?.success?
+ output
+ ensure
+ if path
+ ENV["BUNDLE_PATH"] = old_path
+ else
+ ENV["BUNDLE_PATH__SYSTEM"] = old_path__system
+ end
+
+ ENV["BUNDLER_ORIG_BUNDLE_GEMFILE"] = old_orig_gemfile
+ ENV["BUNDLE_GEMFILE"] = old_gemfile
+ end
+
+ private
+
+ def gem_load_and_activate(gem_name, bin_container)
+ gem_activate(gem_name)
+ load Gem.bin_path(gem_name, bin_container)
+ rescue Gem::LoadError => e
+ abort "We couldn't activate #{gem_name} (#{e.requirement}). Run `gem install #{gem_name}:'#{e.requirement}'`"
+ end
+
+ def gem_load_activate_and_possibly_install(gem_name, bin_container)
+ gem_activate_and_possibly_install(gem_name)
+ load Gem.bin_path(gem_name, bin_container)
+ end
+
+ def gem_activate_and_possibly_install(gem_name)
+ gem_activate(gem_name)
+ rescue Gem::LoadError => e
+ Gem.install(gem_name, e.requirement)
+ retry
+ end
+
+ def gem_activate(gem_name)
+ require_relative "activate"
+ require "bundler"
+ gem_requirement = Bundler::LockfileParser.new(File.read(dev_lockfile)).specs.find {|spec| spec.name == gem_name }.version
+ gem gem_name, gem_requirement
+ end
+
+ def test_gemfile
+ Path.test_gemfile
+ end
+
+ def rubocop_gemfile
+ Path.rubocop_gemfile
+ end
+
+ def standard_gemfile
+ Path.standard_gemfile
+ end
+
+ def dev_gemfile
+ Path.dev_gemfile
+ end
+
+ def dev_lockfile
+ lockfile_for(dev_gemfile)
+ end
+
+ def lockfile_for(gemfile)
+ Pathname.new("#{gemfile.expand_path}.lock")
+ end
+ end
+end
diff --git a/spec/bundler/support/rubygems_version_manager.rb b/spec/bundler/support/rubygems_version_manager.rb
new file mode 100644
index 0000000000..c174c461f0
--- /dev/null
+++ b/spec/bundler/support/rubygems_version_manager.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require_relative "options"
+require_relative "env"
+require_relative "subprocess"
+
+class RubygemsVersionManager
+ include Spec::Options
+ include Spec::Env
+ include Spec::Subprocess
+
+ def initialize(source)
+ @source = source
+ end
+
+ def switch
+ return if use_system?
+
+ assert_system_features_not_loaded!
+
+ switch_local_copy_if_needed
+
+ reexec_if_needed
+ end
+
+ def assert_system_features_not_loaded!
+ at_exit do
+ rubylibdir = RbConfig::CONFIG["rubylibdir"]
+
+ rubygems_path = rubylibdir + "/rubygems"
+ rubygems_default_path = rubygems_path + "/defaults"
+
+ bundler_path = rubylibdir + "/bundler"
+
+ bad_loaded_features = $LOADED_FEATURES.select do |loaded_feature|
+ (loaded_feature.start_with?(rubygems_path) && !loaded_feature.start_with?(rubygems_default_path)) ||
+ loaded_feature.start_with?(bundler_path)
+ end
+
+ errors = if bad_loaded_features.any?
+ all_commands_output + "the following features were incorrectly loaded:\n#{bad_loaded_features.join("\n")}"
+ end
+
+ raise errors if errors
+ end
+ end
+
+ private
+
+ def use_system?
+ @source.nil?
+ end
+
+ def reexec_if_needed
+ return unless rubygems_unrequire_needed?
+
+ require "rbconfig"
+
+ cmd = [RbConfig.ruby, $0, *ARGV].compact
+
+ ENV["RUBYOPT"] = opt_add("-I#{File.join(local_copy_path, "lib")}", opt_remove("--disable-gems", ENV["RUBYOPT"]))
+
+ exec(ENV, *cmd)
+ end
+
+ def switch_local_copy_if_needed
+ return unless local_copy_switch_needed?
+
+ git("checkout #{target_tag}", local_copy_path)
+
+ ENV["RGV"] = local_copy_path
+ end
+
+ def rubygems_unrequire_needed?
+ require "rubygems"
+ !$LOADED_FEATURES.include?(File.join(local_copy_path, "lib/rubygems.rb"))
+ end
+
+ def local_copy_switch_needed?
+ !source_is_path? && target_tag != local_copy_tag
+ end
+
+ def target_tag
+ @target_tag ||= resolve_target_tag
+ end
+
+ def local_copy_tag
+ git("rev-parse --abbrev-ref HEAD", local_copy_path)
+ end
+
+ def local_copy_path
+ @local_copy_path ||= resolve_local_copy_path
+ end
+
+ def resolve_local_copy_path
+ return expanded_source if source_is_path?
+
+ rubygems_path = File.join(source_root, "tmp/rubygems")
+
+ unless File.directory?(rubygems_path)
+ git("clone .. #{rubygems_path}", source_root)
+ end
+
+ rubygems_path
+ end
+
+ def source_is_path?
+ File.directory?(expanded_source)
+ end
+
+ def expanded_source
+ @expanded_source ||= File.expand_path(@source, source_root)
+ end
+
+ def source_root
+ @source_root ||= File.expand_path(ruby_core? ? "../../.." : "../..", __dir__)
+ end
+
+ def resolve_target_tag
+ return "v#{@source}" if @source.match?(/^\d/)
+
+ @source
+ end
+end
diff --git a/spec/bundler/support/setup.rb b/spec/bundler/support/setup.rb
new file mode 100644
index 0000000000..4ac2e5b472
--- /dev/null
+++ b/spec/bundler/support/setup.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require_relative "switch_rubygems"
+
+require_relative "rubygems_ext"
+Spec::Rubygems.install_test_deps
+
+require_relative "path"
+$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__)) if Spec::Path.ruby_core?
diff --git a/spec/bundler/support/shards.rb b/spec/bundler/support/shards.rb
new file mode 100644
index 0000000000..ce33896539
--- /dev/null
+++ b/spec/bundler/support/shards.rb
@@ -0,0 +1,200 @@
+# frozen_string_literal: true
+
+# This classifies test files into 4 shards by running `bin/rspec --profile 10000`
+# to ensure balanced execution times. When adding new test files, it is recommended to
+# re-aggregate and adjust the shards to keep them balanced.
+# For now, please add new files to shard 'shard_d'.
+
+module Spec
+ module Shards
+ EXAMPLE_MAPPINGS = {
+ shard_a: [
+ "spec/runtime/setup_spec.rb",
+ "spec/commands/install_spec.rb",
+ "spec/commands/add_spec.rb",
+ "spec/install/gems/compact_index_spec.rb",
+ "spec/commands/config_spec.rb",
+ "spec/commands/pristine_spec.rb",
+ "spec/install/gemfile/path_spec.rb",
+ "spec/update/git_spec.rb",
+ "spec/commands/open_spec.rb",
+ "spec/commands/remove_spec.rb",
+ "spec/commands/show_spec.rb",
+ "spec/plugins/source/example_spec.rb",
+ "spec/commands/console_spec.rb",
+ "spec/runtime/require_spec.rb",
+ "spec/runtime/env_helpers_spec.rb",
+ "spec/runtime/gem_tasks_spec.rb",
+ "spec/install/gemfile_spec.rb",
+ "spec/commands/fund_spec.rb",
+ "spec/commands/init_spec.rb",
+ "spec/bundler/ruby_dsl_spec.rb",
+ "spec/bundler/mirror_spec.rb",
+ "spec/bundler/source/git/git_proxy_spec.rb",
+ "spec/bundler/source_list_spec.rb",
+ "spec/bundler/plugin/installer_spec.rb",
+ "spec/bundler/errors_spec.rb",
+ "spec/bundler/friendly_errors_spec.rb",
+ "spec/resolver/platform_spec.rb",
+ "spec/bundler/fetcher/downloader_spec.rb",
+ "spec/update/force_spec.rb",
+ "spec/bundler/env_spec.rb",
+ "spec/install/gems/mirror_spec.rb",
+ "spec/install/failure_spec.rb",
+ "spec/bundler/yaml_serializer_spec.rb",
+ "spec/bundler/environment_preserver_spec.rb",
+ "spec/install/gemfile/install_if_spec.rb",
+ "spec/install/gems/gemfile_source_header_spec.rb",
+ "spec/bundler/fetcher/base_spec.rb",
+ "spec/bundler/rubygems_integration_spec.rb",
+ "spec/bundler/worker_spec.rb",
+ "spec/bundler/dependency_spec.rb",
+ "spec/bundler/ui_spec.rb",
+ "spec/bundler/plugin/source_list_spec.rb",
+ "spec/bundler/source/path_spec.rb",
+ ],
+ shard_b: [
+ "spec/install/gemfile/git_spec.rb",
+ "spec/install/gems/standalone_spec.rb",
+ "spec/commands/lock_spec.rb",
+ "spec/cache/gems_spec.rb",
+ "spec/other/major_deprecation_spec.rb",
+ "spec/install/gems/dependency_api_spec.rb",
+ "spec/install/gemfile/gemspec_spec.rb",
+ "spec/plugins/install_spec.rb",
+ "spec/commands/binstubs_spec.rb",
+ "spec/install/gems/flex_spec.rb",
+ "spec/runtime/inline_spec.rb",
+ "spec/commands/post_bundle_message_spec.rb",
+ "spec/runtime/executable_spec.rb",
+ "spec/lock/git_spec.rb",
+ "spec/plugins/hook_spec.rb",
+ "spec/install/allow_offline_install_spec.rb",
+ "spec/install/gems/post_install_spec.rb",
+ "spec/install/gemfile/ruby_spec.rb",
+ "spec/install/security_policy_spec.rb",
+ "spec/install/yanked_spec.rb",
+ "spec/update/gemfile_spec.rb",
+ "spec/runtime/load_spec.rb",
+ "spec/plugins/command_spec.rb",
+ "spec/commands/version_spec.rb",
+ "spec/install/prereleases_spec.rb",
+ "spec/bundler/uri_credentials_filter_spec.rb",
+ "spec/bundler/plugin_spec.rb",
+ "spec/install/gems/mirror_probe_spec.rb",
+ "spec/plugins/list_spec.rb",
+ "spec/bundler/compact_index_client/parser_spec.rb",
+ "spec/bundler/gem_version_promoter_spec.rb",
+ "spec/other/cli_dispatch_spec.rb",
+ "spec/bundler/source/rubygems_spec.rb",
+ "spec/cache/platform_spec.rb",
+ "spec/update/gems/fund_spec.rb",
+ "spec/bundler/stub_specification_spec.rb",
+ "spec/bundler/retry_spec.rb",
+ "spec/bundler/installer/spec_installation_spec.rb",
+ "spec/bundler/spec_set_spec.rb",
+ "spec/quality_es_spec.rb",
+ "spec/bundler/index_spec.rb",
+ "spec/other/cli_man_pages_spec.rb",
+ ],
+ shard_c: [
+ "spec/commands/newgem_spec.rb",
+ "spec/commands/exec_spec.rb",
+ "spec/commands/clean_spec.rb",
+ "spec/commands/platform_spec.rb",
+ "spec/cache/git_spec.rb",
+ "spec/install/gemfile/groups_spec.rb",
+ "spec/commands/cache_spec.rb",
+ "spec/commands/check_spec.rb",
+ "spec/commands/list_spec.rb",
+ "spec/install/path_spec.rb",
+ "spec/bundler/cli_spec.rb",
+ "spec/install/bundler_spec.rb",
+ "spec/install/git_spec.rb",
+ "spec/commands/doctor_spec.rb",
+ "spec/bundler/dsl_spec.rb",
+ "spec/install/gems/fund_spec.rb",
+ "spec/install/gems/env_spec.rb",
+ "spec/bundler/ruby_version_spec.rb",
+ "spec/bundler/definition_spec.rb",
+ "spec/install/gemfile/eval_gemfile_spec.rb",
+ "spec/plugins/source_spec.rb",
+ "spec/install/gems/dependency_api_fallback_spec.rb",
+ "spec/plugins/uninstall_spec.rb",
+ "spec/bundler/plugin/index_spec.rb",
+ "spec/bundler/bundler_spec.rb",
+ "spec/bundler/fetcher_spec.rb",
+ "spec/bundler/source/rubygems/remote_spec.rb",
+ "spec/bundler/lockfile_parser_spec.rb",
+ "spec/cache/cache_path_spec.rb",
+ "spec/bundler/source/git_spec.rb",
+ "spec/bundler/source_spec.rb",
+ "spec/commands/ssl_spec.rb",
+ "spec/bundler/fetcher/compact_index_spec.rb",
+ "spec/bundler/plugin/api_spec.rb",
+ "spec/bundler/endpoint_specification_spec.rb",
+ "spec/bundler/fetcher/index_spec.rb",
+ "spec/bundler/settings/validator_spec.rb",
+ "spec/bundler/build_metadata_spec.rb",
+ "spec/bundler/current_ruby_spec.rb",
+ "spec/bundler/installer/gem_installer_spec.rb",
+ "spec/bundler/installer/parallel_installer_spec.rb",
+ "spec/bundler/cli_common_spec.rb",
+ "spec/bundler/ci_detector_spec.rb",
+ ],
+ shard_d: [
+ "spec/bundler/rubygems_ext_spec.rb",
+ "spec/bundler/resolver/cooldown_spec.rb",
+ "spec/install/cooldown_spec.rb",
+ "spec/commands/outdated_spec.rb",
+ "spec/commands/update_spec.rb",
+ "spec/lock/lockfile_spec.rb",
+ "spec/install/deploy_spec.rb",
+ "spec/install/gemfile/sources_spec.rb",
+ "spec/runtime/self_management_spec.rb",
+ "spec/install/gemfile/specific_platform_spec.rb",
+ "spec/commands/info_spec.rb",
+ "spec/install/gems/resolving_spec.rb",
+ "spec/install/gemfile/platform_spec.rb",
+ "spec/bundler/gem_helper_spec.rb",
+ "spec/install/global_cache_spec.rb",
+ "spec/runtime/platform_spec.rb",
+ "spec/update/gems/post_install_spec.rb",
+ "spec/install/gems/native_extensions_spec.rb",
+ "spec/install/force_spec.rb",
+ "spec/cache/path_spec.rb",
+ "spec/install/gemspecs_spec.rb",
+ "spec/commands/help_spec.rb",
+ "spec/bundler/shared_helpers_spec.rb",
+ "spec/bundler/settings_spec.rb",
+ "spec/resolver/basic_spec.rb",
+ "spec/install/gemfile/force_ruby_platform_spec.rb",
+ "spec/commands/licenses_spec.rb",
+ "spec/install/gemfile/lockfile_spec.rb",
+ "spec/bundler/fetcher/dependency_spec.rb",
+ "spec/quality_spec.rb",
+ "spec/bundler/remote_specification_spec.rb",
+ "spec/install/process_lock_spec.rb",
+ "spec/install/binstubs_spec.rb",
+ "spec/bundler/compact_index_client/updater_spec.rb",
+ "spec/bundler/ui/shell_spec.rb",
+ "spec/other/ext_spec.rb",
+ "spec/commands/issue_spec.rb",
+ "spec/update/path_spec.rb",
+ "spec/bundler/plugin/api/source_spec.rb",
+ "spec/install/gems/win32_spec.rb",
+ "spec/bundler/plugin/dsl_spec.rb",
+ "spec/runtime/requiring_spec.rb",
+ "spec/bundler/plugin/events_spec.rb",
+ "spec/bundler/resolver/candidate_spec.rb",
+ "spec/bundler/digest_spec.rb",
+ "spec/bundler/fetcher/gem_remote_fetcher_spec.rb",
+ "spec/bundler/uri_normalizer_spec.rb",
+ "spec/install/gems/no_build_extension_spec.rb",
+ "spec/install/gems/no_install_plugin_spec.rb",
+ "spec/bundler/override_spec.rb",
+ "spec/install/gemfile/override_spec.rb",
+ ],
+ }.freeze
+ end
+end
diff --git a/spec/bundler/support/subprocess.rb b/spec/bundler/support/subprocess.rb
new file mode 100644
index 0000000000..91db80da48
--- /dev/null
+++ b/spec/bundler/support/subprocess.rb
@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+require_relative "command_execution"
+
+module Spec
+ module Subprocess
+ class TimeoutExceeded < StandardError; end
+
+ def command_executions
+ @command_executions ||= []
+ end
+
+ def last_command
+ command_executions.last || raise("There is no last command")
+ end
+
+ def out
+ last_command.stdout
+ end
+
+ def err
+ last_command.stderr
+ end
+
+ def stdboth
+ last_command.stdboth
+ end
+
+ def exitstatus
+ last_command.exitstatus
+ end
+
+ def git(cmd, path = Dir.pwd, options = {})
+ sh("git #{cmd}", options.merge(dir: path))
+ end
+
+ def sh(cmd, options = {})
+ dir = options[:dir]
+ env = options[:env] || {}
+
+ command_execution = CommandExecution.new(cmd.to_s, timeout: options[:timeout] || 60)
+
+ open3_opts = {}
+ open3_opts[:chdir] = dir if dir
+
+ require "open3"
+ require "shellwords"
+ Open3.popen3(env, *cmd.shellsplit, **open3_opts) do |stdin, stdout, stderr, wait_thr|
+ yield stdin, stdout, wait_thr if block_given?
+ stdin.close
+
+ stdout_handler = ->(data) { command_execution.original_stdout << data }
+ stderr_handler = ->(data) { command_execution.original_stderr << data }
+
+ stdout_thread = read_stream(stdout, stdout_handler, timeout: command_execution.timeout)
+ stderr_thread = read_stream(stderr, stderr_handler, timeout: command_execution.timeout)
+
+ stdout_thread.join
+ stderr_thread.join
+
+ status = wait_thr.value
+ command_execution.exitstatus = if status.exited?
+ status.exitstatus
+ elsif status.signaled?
+ exit_status_for_signal(status.termsig)
+ end
+ rescue TimeoutExceeded
+ command_execution.failure_reason = :timeout
+ command_execution.exitstatus = exit_status_for_signal(Signal.list["INT"])
+ end
+
+ unless options[:raise_on_error] == false || command_execution.success?
+ command_execution.raise_error!
+ end
+
+ command_executions << command_execution
+
+ command_execution.stdout
+ end
+
+ # Mostly copied from https://github.com/piotrmurach/tty-command/blob/49c37a895ccea107e8b78d20e4cb29de6a1a53c8/lib/tty/command/process_runner.rb#L165-L193
+ def read_stream(stream, handler, timeout:)
+ Thread.new do
+ Thread.current.report_on_exception = false
+ cmd_start = Time.now
+ readers = [stream]
+
+ while readers.any?
+ ready = IO.select(readers, nil, readers, timeout)
+ raise TimeoutExceeded if ready.nil?
+
+ ready[0].each do |reader|
+ chunk = reader.readpartial(16 * 1024)
+ handler.call(chunk)
+
+ # control total time spent reading
+ runtime = Time.now - cmd_start
+ time_left = timeout - runtime
+ raise TimeoutExceeded if time_left < 0.0
+ rescue Errno::EAGAIN, Errno::EINTR
+ rescue EOFError, Errno::EPIPE, Errno::EIO
+ readers.delete(reader)
+ reader.close
+ end
+ end
+ end
+ end
+
+ def all_commands_output
+ return "" if command_executions.empty?
+
+ "\n\nCommands:\n#{command_executions.map(&:to_s_verbose).join("\n\n")}"
+ end
+ end
+end
diff --git a/spec/bundler/support/switch_rubygems.rb b/spec/bundler/support/switch_rubygems.rb
new file mode 100644
index 0000000000..640b9f83b7
--- /dev/null
+++ b/spec/bundler/support/switch_rubygems.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+require_relative "rubygems_version_manager"
+ENV["RGV"] ||= "."
+RubygemsVersionManager.new(ENV["RGV"]).switch
diff --git a/spec/bundler/support/the_bundle.rb b/spec/bundler/support/the_bundle.rb
new file mode 100644
index 0000000000..452abd7d41
--- /dev/null
+++ b/spec/bundler/support/the_bundle.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require_relative "path"
+
+module Spec
+ class TheBundle
+ include Spec::Path
+
+ attr_accessor :bundle_dir
+
+ def initialize
+ @bundle_dir = Pathname.new(bundled_app)
+ end
+
+ def to_s
+ "the bundle"
+ end
+ alias_method :inspect, :to_s
+
+ def locked?
+ lockfile.file?
+ end
+
+ def lockfile
+ bundle_dir.join("Gemfile.lock")
+ end
+
+ def locked_gems
+ raise ArgumentError, "Cannot read lockfile if it doesn't exist" unless locked?
+ Bundler::LockfileParser.new(lockfile.read)
+ end
+
+ def locked_specs
+ locked_gems.specs.map(&:full_name)
+ end
+
+ def locked_platforms
+ locked_gems.platforms.map(&:to_s)
+ end
+ end
+end
diff --git a/spec/bundler/support/vendored_net_http.rb b/spec/bundler/support/vendored_net_http.rb
new file mode 100644
index 0000000000..8ff2ccd1fe
--- /dev/null
+++ b/spec/bundler/support/vendored_net_http.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# This defined? guard can be removed once RubyGems 3.4 support is dropped.
+#
+# Bundler specs load this code from `spec/support/vendored_net_http.rb` to avoid
+# activating the Bundler gem too early. Without this guard, we get redefinition
+# warnings once Bundler is actually activated and
+# `lib/bundler/vendored_net_http.rb` is required. This is not an issue in
+# RubyGems versions including `rubygems/vendored_net_http` since `require` takes
+# care of avoiding the double load.
+#
+unless defined?(Gem::Net)
+ begin
+ require "rubygems/vendored_net_http"
+ rescue LoadError
+ begin
+ require "rubygems/net/http"
+ rescue LoadError
+ require "net/http"
+ Gem::Net = Net
+ end
+ end
+end
diff --git a/spec/bundler/update/force_spec.rb b/spec/bundler/update/force_spec.rb
new file mode 100644
index 0000000000..325f58088a
--- /dev/null
+++ b/spec/bundler/update/force_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle update" do
+ before :each do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "re-installs installed gems with --force" do
+ myrack_lib = default_bundle_path("gems/myrack-1.0.0/lib/myrack.rb")
+ myrack_lib.open("w") {|f| f.write("blah blah blah") }
+ bundle :update, force: true
+
+ expect(out).to include "Installing myrack 1.0.0"
+ expect(myrack_lib.open(&:read)).to eq("MYRACK = '1.0.0'\n")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "re-installs installed gems with --redownload" do
+ myrack_lib = default_bundle_path("gems/myrack-1.0.0/lib/myrack.rb")
+ myrack_lib.open("w") {|f| f.write("blah blah blah") }
+ bundle :update, redownload: true
+
+ expect(out).to include "Installing myrack 1.0.0"
+ expect(myrack_lib.open(&:read)).to eq("MYRACK = '1.0.0'\n")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+end
diff --git a/spec/bundler/update/gemfile_spec.rb b/spec/bundler/update/gemfile_spec.rb
new file mode 100644
index 0000000000..f8849640b6
--- /dev/null
+++ b/spec/bundler/update/gemfile_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle update" do
+ context "with --gemfile" do
+ it "finds the gemfile" do
+ gemfile bundled_app("NotGemfile"), <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle :install, gemfile: bundled_app("NotGemfile")
+ bundle :update, gemfile: bundled_app("NotGemfile"), all: true
+
+ # Specify BUNDLE_GEMFILE for `the_bundle`
+ # to retrieve the proper Gemfile
+ ENV["BUNDLE_GEMFILE"] = "NotGemfile"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ context "with gemfile set via config" do
+ before do
+ gemfile bundled_app("NotGemfile"), <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle_config "gemfile #{bundled_app("NotGemfile")}"
+ bundle :install
+ end
+
+ it "uses the gemfile to update" do
+ bundle "update", all: true
+ bundle "list"
+
+ expect(out).to include("myrack (1.0.0)")
+ end
+
+ it "uses the gemfile while in a subdirectory" do
+ bundled_app("subdir").mkpath
+ bundle "update", all: true, dir: bundled_app("subdir")
+ bundle "list", dir: bundled_app("subdir")
+
+ expect(out).to include("myrack (1.0.0)")
+ end
+ end
+end
diff --git a/spec/bundler/update/gems/fund_spec.rb b/spec/bundler/update/gems/fund_spec.rb
new file mode 100644
index 0000000000..a5624d3e0a
--- /dev/null
+++ b/spec/bundler/update/gems/fund_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle update" do
+ before do
+ build_repo2 do
+ build_gem "has_funding_and_other_metadata" do |s|
+ s.metadata = {
+ "bug_tracker_uri" => "https://example.com/user/bestgemever/issues",
+ "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md",
+ "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1",
+ "homepage_uri" => "https://bestgemever.example.io",
+ "mailing_list_uri" => "https://groups.example.com/bestgemever",
+ "funding_uri" => "https://example.com/has_funding_and_other_metadata/funding",
+ "source_code_uri" => "https://example.com/user/bestgemever",
+ "wiki_uri" => "https://example.com/user/bestgemever/wiki",
+ }
+ end
+
+ build_gem "has_funding", "1.2.3" do |s|
+ s.metadata = {
+ "funding_uri" => "https://example.com/has_funding/funding",
+ }
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem 'has_funding_and_other_metadata'
+ gem 'has_funding', '< 2.0'
+ G
+
+ bundle :install
+ end
+
+ context "when listed gems are updated" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem 'has_funding_and_other_metadata'
+ gem 'has_funding'
+ G
+
+ bundle :update, all: true
+ end
+
+ it "displays fund message" do
+ expect(out).to include("2 installed gems you directly depend on are looking for funding.")
+ end
+ end
+end
diff --git a/spec/bundler/update/gems/post_install_spec.rb b/spec/bundler/update/gems/post_install_spec.rb
new file mode 100644
index 0000000000..9c71f6e0e3
--- /dev/null
+++ b/spec/bundler/update/gems/post_install_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle update" do
+ let(:config) {}
+
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack', "< 1.0"
+ gem 'thin'
+ G
+
+ bundle "config set #{config}" if config
+
+ bundle :install
+ end
+
+ shared_examples "a config observer" do
+ context "when ignore post-install messages for gem is set" do
+ let(:config) { "ignore_messages.myrack true" }
+
+ it "doesn't display gem's post-install message" do
+ expect(out).not_to include("Myrack's post install message")
+ end
+ end
+
+ context "when ignore post-install messages for all gems" do
+ let(:config) { "ignore_messages true" }
+
+ it "doesn't display any post-install messages" do
+ expect(out).not_to include("Post-install message")
+ end
+ end
+ end
+
+ shared_examples "a post-install message outputter" do
+ it "should display post-install messages for updated gems" do
+ expect(out).to include("Post-install message from myrack:")
+ expect(out).to include("Myrack's post install message")
+ end
+
+ it "should not display the post-install message for non-updated gems" do
+ expect(out).not_to include("Thin's post install message")
+ end
+ end
+
+ context "when listed gem is updated" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ gem 'thin'
+ G
+
+ bundle :update, all: true
+ end
+
+ it_behaves_like "a post-install message outputter"
+ it_behaves_like "a config observer"
+ end
+
+ context "when dependency triggers update" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack-obama'
+ gem 'thin'
+ G
+
+ bundle :update, all: true
+ end
+
+ it_behaves_like "a post-install message outputter"
+ it_behaves_like "a config observer"
+ end
+end
diff --git a/spec/bundler/update/git_spec.rb b/spec/bundler/update/git_spec.rb
new file mode 100644
index 0000000000..526e988ab7
--- /dev/null
+++ b/spec/bundler/update/git_spec.rb
@@ -0,0 +1,341 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle update" do
+ describe "git sources" do
+ it "floats on a branch when :branch is used" do
+ build_git "foo", "1.0"
+ update_git "foo", branch: "omg"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}", :branch => "omg" do
+ gem 'foo'
+ end
+ G
+
+ update_git "foo" do |s|
+ s.write "lib/foo.rb", "FOO = '1.1'"
+ end
+
+ bundle "update", all: true
+
+ expect(the_bundle).to include_gems "foo 1.1"
+ end
+
+ it "updates correctly when you have like craziness" do
+ build_lib "activesupport", "3.0", path: lib_path("rails/activesupport")
+ build_git "rails", "3.0", path: lib_path("rails") do |s|
+ s.add_dependency "activesupport", "= 3.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails", :git => "#{lib_path("rails")}"
+ G
+
+ bundle "update rails"
+ expect(the_bundle).to include_gems "rails 3.0", "activesupport 3.0"
+ end
+
+ it "floats on a branch when :branch is used and the source is specified in the update" do
+ build_git "foo", "1.0", path: lib_path("foo")
+ update_git "foo", branch: "omg", path: lib_path("foo")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo")}", :branch => "omg" do
+ gem 'foo'
+ end
+ G
+
+ update_git "foo", path: lib_path("foo") do |s|
+ s.write "lib/foo.rb", "FOO = '1.1'"
+ end
+
+ bundle "update --source foo"
+
+ expect(the_bundle).to include_gems "foo 1.1"
+ end
+
+ it "floats on main when updating all gems that are pinned to the source even if you have child dependencies" do
+ build_git "foo", path: lib_path("foo")
+ build_gem "bar", to_bundle: true do |s|
+ s.add_dependency "foo"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "bar"
+ G
+
+ update_git "foo", path: lib_path("foo") do |s|
+ s.write "lib/foo.rb", "FOO = '1.1'"
+ end
+
+ bundle "update foo"
+
+ expect(the_bundle).to include_gems "foo 1.1"
+ end
+
+ it "notices when you change the repo url in the Gemfile" do
+ build_git "foo", path: lib_path("foo_one")
+ build_git "foo", path: lib_path("foo_two")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", "1.0", :git => "#{lib_path("foo_one")}"
+ G
+
+ FileUtils.rm_r lib_path("foo_one")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", "1.0", :git => "#{lib_path("foo_two")}"
+ G
+
+ expect(err).to be_empty
+ expect(out).to include("Fetching #{lib_path}/foo_two")
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "fetches tags from the remote" do
+ build_git "foo"
+ @remote = build_git("bar", bare: true)
+ update_git "foo", remote: @remote.path
+ update_git "foo", push: "main"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{@remote.path}"
+ G
+
+ # Create a new tag on the remote that needs fetching
+ update_git "foo", tag: "fubar"
+ update_git "foo", push: "fubar"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{@remote.path}", :tag => "fubar"
+ G
+
+ bundle "update", all: true
+ expect(err).to be_empty
+ end
+
+ describe "with submodules" do
+ before :each do
+ # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/
+ system(*%W[git config --global protocol.file.allow always])
+
+ build_repo4 do
+ build_gem "submodule" do |s|
+ s.write "lib/submodule.rb", "puts 'GEM'"
+ end
+ end
+
+ build_git "submodule", "1.0" do |s|
+ s.write "lib/submodule.rb", "puts 'GIT'"
+ end
+
+ build_git "has_submodule", "1.0" do |s|
+ s.add_dependency "submodule"
+ end
+
+ git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0")
+ git "commit -m \"submodulator\"", lib_path("has_submodule-1.0")
+ end
+
+ it "it unlocks the source when submodules are added to a git source" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ git "#{lib_path("has_submodule-1.0")}" do
+ gem "has_submodule"
+ end
+ G
+
+ run "require 'submodule'"
+ expect(out).to eq("GEM")
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ git "#{lib_path("has_submodule-1.0")}", :submodules => true do
+ gem "has_submodule"
+ end
+ G
+
+ run "require 'submodule'"
+ expect(out).to eq("GIT")
+ end
+
+ it "unlocks the source when submodules are removed from git source", git: ">= 2.9.0" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ git "#{lib_path("has_submodule-1.0")}", :submodules => true do
+ gem "has_submodule"
+ end
+ G
+
+ run "require 'submodule'"
+ expect(out).to eq("GIT")
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ git "#{lib_path("has_submodule-1.0")}" do
+ gem "has_submodule"
+ end
+ G
+
+ run "require 'submodule'"
+ expect(out).to eq("GEM")
+ end
+ end
+
+ it "errors with a message when the .git repo is gone" do
+ build_git "foo", "1.0"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ FileUtils.rm_rf lib_path("foo-1.0").join(".git")
+
+ bundle :update, all: true, raise_on_error: false
+ expect(err).to include(lib_path("foo-1.0").to_s).
+ and match(/Git error: command `git fetch.+has failed/)
+ end
+
+ it "should not explode on invalid revision on update of gem by name" do
+ build_git "myrack", "0.8"
+
+ build_git "myrack", "0.8", path: lib_path("local-myrack") do |s|
+ s.write "lib/myrack.rb", "puts :LOCAL"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle "update myrack"
+ expect(out).to include("Bundle updated!")
+ end
+
+ it "shows the previous version of the gem" do
+ build_git "rails", "2.3.2", path: lib_path("rails")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails", :git => "#{lib_path("rails")}"
+ G
+
+ update_git "rails", "3.0", path: lib_path("rails"), gemspec: true
+
+ bundle "update", all: true
+ expect(out).to include("Using rails 3.0 (was 2.3.2) from #{lib_path("rails")} (at main@#{revision_for(lib_path("rails"))[0..6]})")
+ end
+ end
+
+ describe "with --source flag" do
+ before :each do
+ build_repo2
+ @git = build_git "foo", path: lib_path("foo") do |s|
+ s.executables = "foobar"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ git "#{lib_path("foo")}" do
+ gem 'foo'
+ end
+ gem 'myrack'
+ G
+ end
+
+ it "updates the source" do
+ update_git "foo", path: @git.path
+
+ bundle "update --source foo"
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" if defined?(FOO_PREV_REF)
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "unlocks gems that were originally pulled in by the source" do
+ update_git "foo", "2.0", path: @git.path
+
+ bundle "update --source foo"
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+
+ it "leaves all other gems frozen" do
+ update_repo2
+ update_git "foo", path: @git.path
+
+ bundle "update --source foo"
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+
+ context "when the gem and the repository have different names" do
+ before :each do
+ build_repo2
+ @git = build_git "foo", path: lib_path("bar")
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ git "#{lib_path("bar")}" do
+ gem 'foo'
+ end
+ gem 'myrack'
+ G
+ end
+
+ it "the --source flag updates version of gems that were originally pulled in by the source" do
+ spec_lines = lib_path("bar/foo.gemspec").read.split("\n")
+ spec_lines[5] = "s.version = '2.0'"
+
+ update_git "foo", "2.0", path: @git.path do |s|
+ s.write "foo.gemspec", spec_lines.join("\n")
+ end
+
+ ref = @git.ref_for "main"
+
+ bundle "update --source bar"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "2.0"
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GIT
+ remote: #{@git.path}
+ revision: #{ref}
+ specs:
+ foo (2.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+ end
+end
diff --git a/spec/bundler/update/path_spec.rb b/spec/bundler/update/path_spec.rb
new file mode 100644
index 0000000000..8c76c94e1a
--- /dev/null
+++ b/spec/bundler/update/path_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+RSpec.describe "path sources" do
+ describe "bundle update --source" do
+ it "shows the previous version of the gem when updated from path source" do
+ build_lib "activesupport", "2.3.5", path: lib_path("rails/activesupport")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport", :path => "#{lib_path("rails/activesupport")}"
+ G
+
+ build_lib "activesupport", "3.0", path: lib_path("rails/activesupport")
+
+ bundle "update --source activesupport"
+ expect(out).to include("Using activesupport 3.0 (was 2.3.5) from source at `#{lib_path("rails/activesupport")}`")
+ end
+ end
+end