diff --git a/Manifest.txt b/Manifest.txt index 7d9811cf5630..1bbde61965d6 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -308,6 +308,7 @@ lib/bundler/worker.rb lib/rubygems.rb lib/rubygems/available_set.rb lib/rubygems/basic_specification.rb +lib/rubygems/bundler_settings.rb lib/rubygems/bundler_version_finder.rb lib/rubygems/ci_detector.rb lib/rubygems/command.rb @@ -357,6 +358,7 @@ lib/rubygems/compact_index_client/updater.rb lib/rubygems/config_file.rb lib/rubygems/cooldown.rb lib/rubygems/cooldown_option.rb +lib/rubygems/cooldown_settings.rb lib/rubygems/core_ext/kernel_gem.rb lib/rubygems/core_ext/kernel_require.rb lib/rubygems/core_ext/kernel_warn.rb diff --git a/bundler.gemspec b/bundler.gemspec index dcd5fb244492..ca53a0cf3a70 100644 --- a/bundler.gemspec +++ b/bundler.gemspec @@ -40,13 +40,14 @@ Gem::Specification.new do |s| s.files = Dir.glob("lib/bundler{.rb,/**/*}", File::FNM_DOTMATCH).reject {|f| File.directory?(f) } # Bundler reuses RubyGems' vendored URI, SecureRandom and PubGrub, its - # pure-Ruby YAML serializer, its compact index client and its credential - # store. Ship a copy under lib/rubygems so Bundler stays self-contained on - # RubyGems versions that predate them. + # pure-Ruby YAML serializer, its shared cooldown setting rules, its compact + # index client and its credential store. Ship a copy under lib/rubygems so + # Bundler stays self-contained on RubyGems versions that predate them. s.files += Dir.glob("lib/rubygems/vendor/uri/**/*", File::FNM_DOTMATCH).reject {|f| File.directory?(f) } s.files += Dir.glob("lib/rubygems/vendor/securerandom/**/*", File::FNM_DOTMATCH).reject {|f| File.directory?(f) } s.files += Dir.glob("lib/rubygems/vendor/pub_grub/**/*", File::FNM_DOTMATCH).reject {|f| File.directory?(f) } s.files += Dir.glob("lib/rubygems/yaml_serializer.rb") + s.files += Dir.glob("lib/rubygems/cooldown_settings.rb") s.files += Dir.glob("lib/rubygems/compact_index_client{.rb,/**/*}", File::FNM_DOTMATCH).reject {|f| File.directory?(f) } s.files += Dir.glob("lib/rubygems/credential_store{.rb,/**/*}", File::FNM_DOTMATCH).reject {|f| File.directory?(f) } diff --git a/lib/bundler/cli/add.rb b/lib/bundler/cli/add.rb index 20f76b59d10a..385666efe99e 100644 --- a/lib/bundler/cli/add.rb +++ b/lib/bundler/cli/add.rb @@ -14,8 +14,7 @@ def initialize(options, gems) def run Bundler.ui.level = "warn" if options[:quiet] - Bundler::CLI::Common.validate_cooldown!(options[:cooldown]) - Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown] + Bundler::CLI::Common.configure_cooldown(options) validate_options! inject_dependencies diff --git a/lib/bundler/cli/common.rb b/lib/bundler/cli/common.rb index 1db4ade89fc1..1f66a113c2ab 100644 --- a/lib/bundler/cli/common.rb +++ b/lib/bundler/cli/common.rb @@ -2,26 +2,37 @@ module Bundler module CLI::Common + # Validates the `--cooldown` flag and makes it the setting for this + # command. Every command that takes the flag goes through here, so they + # share one reading of the value. + def self.configure_cooldown(options) + value = options[:cooldown] + + validate_cooldown!(value) + Bundler.settings.set_command_option_if_given :cooldown, value + end + def self.validate_cooldown!(value) - # Without the flag the config file and BUNDLE_COOLDOWN decide, and those - # only warn, so a typo left in a config file keeps the command usable. + # Without the flag the config files, BUNDLE_COOLDOWN and the gemrc + # setting decide, and those only warn, so a typo left in a config file + # keeps the command usable. return warn_invalid_cooldown_setting if value.nil? return if value.is_a?(Integer) && value >= 0 raise InvalidOption, "Expected `--cooldown` to be a non-negative integer, got #{value.inspect}" end - # A cooldown value that cannot be read as a non-negative integer disables - # the cooldown for every source, overriding any per-source `cooldown:` in - # the Gemfile, so say so rather than letting the protection lapse quietly. + # A cooldown value that cannot be read as a non-negative integer takes no + # part in the resolution, so say so rather than letting the protection + # lapse quietly. The RubyGems `:cooldown:` setting feeds the same + # resolution and is checked in Bundler::Settings#rubygems_cooldown, where + # reading it is already being paid for. def self.warn_invalid_cooldown_setting - value = Bundler.settings.locations(:cooldown).values.first - return if value.nil? + require "rubygems/cooldown_settings" - days = Integer(value.to_s, exception: false) - return if days && !days.negative? + value = Bundler.settings.locations(:cooldown).values.first + return unless Gem::CooldownSettings.invalid?(value) - Bundler.ui.warn "Invalid cooldown value #{value.inspect}, so the cooldown is disabled for all sources. " \ - "Expected a non-negative integer number of days." + Bundler.ui.warn Gem::CooldownSettings.invalid_message(value, "Bundler's configuration") end def self.output_post_install_messages(messages) diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb index 2cebdff5c8a4..f8e4001ed062 100644 --- a/lib/bundler/cli/install.rb +++ b/lib/bundler/cli/install.rb @@ -115,8 +115,7 @@ def normalize_settings Bundler.settings.set_command_option_if_given :jobs, options["jobs"] - Bundler::CLI::Common.validate_cooldown!(options["cooldown"]) - Bundler.settings.set_command_option_if_given :cooldown, options["cooldown"] + Bundler::CLI::Common.configure_cooldown(options) Bundler.settings.set_command_option_if_given :no_install, options["no-install"] diff --git a/lib/bundler/cli/lock.rb b/lib/bundler/cli/lock.rb index dcb79068557f..328b9e868024 100644 --- a/lib/bundler/cli/lock.rb +++ b/lib/bundler/cli/lock.rb @@ -16,8 +16,7 @@ def run check_for_conflicting_options - Bundler::CLI::Common.validate_cooldown!(options[:cooldown]) - Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown] + Bundler::CLI::Common.configure_cooldown(options) print = options[:print] previous_output_stream = Bundler.ui.output_stream diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb index 637f61c760d5..f355955a5a70 100644 --- a/lib/bundler/cli/outdated.rb +++ b/lib/bundler/cli/outdated.rb @@ -26,8 +26,7 @@ def initialize(options, gems) def run check_for_deployment_mode! - Bundler::CLI::Common.validate_cooldown!(options[:cooldown]) - Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown] + Bundler::CLI::Common.configure_cooldown(options) Bundler.definition.validate_runtime! current_specs = Bundler.ui.silence { Bundler.definition.resolve } diff --git a/lib/bundler/cli/update.rb b/lib/bundler/cli/update.rb index 4763fdf48472..c722474f64d7 100644 --- a/lib/bundler/cli/update.rb +++ b/lib/bundler/cli/update.rb @@ -74,8 +74,7 @@ def run opts["force"] = options[:redownload] if options[:redownload] Bundler.settings.set_command_option_if_given :jobs, opts["jobs"] - Bundler::CLI::Common.validate_cooldown!(options[:cooldown]) - Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown] + Bundler::CLI::Common.configure_cooldown(options) Bundler.definition.validate_runtime! diff --git a/lib/bundler/man/bundle-add.1 b/lib/bundler/man/bundle-add.1 index 53fb68965888..87741dae61b0 100644 --- a/lib/bundler/man/bundle-add.1 +++ b/lib/bundler/man/bundle-add.1 @@ -55,7 +55,7 @@ Adds pessimistic declaration of version\. Adds strict declaration of version\. .TP \fB\-\-cooldown=\fR -Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run\. See \fBcooldown\fR in bundle\-config(1) for precedence rules\. +Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run, overriding every other cooldown setting, RubyGems' own included\. See \fBcooldown\fR in bundle\-config(1) for precedence rules\. .SH "EXAMPLES" .IP "1." 4 You can add the \fBrails\fR gem to the Gemfile without any version restriction\. The source of the gem will be the global source\. diff --git a/lib/bundler/man/bundle-add.1.ronn b/lib/bundler/man/bundle-add.1.ronn index 4b5648e440d4..3f8262299902 100644 --- a/lib/bundler/man/bundle-add.1.ronn +++ b/lib/bundler/man/bundle-add.1.ronn @@ -61,8 +61,9 @@ Adds the named gem to the [`Gemfile(5)`][Gemfile(5)] and run `bundle install`. * `--cooldown=`: Only consider gem versions published at least days ago when - resolving. Pass `0` to disable cooldown for this run. See `cooldown` - in bundle-config(1) for precedence rules. + resolving. Pass `0` to disable cooldown for this run, overriding every + other cooldown setting, RubyGems' own included. See `cooldown` in + bundle-config(1) for precedence rules. ## EXAMPLES diff --git a/lib/bundler/man/bundle-cache.1 b/lib/bundler/man/bundle-cache.1 index 81d4a9e04265..09e52e369b1f 100644 --- a/lib/bundler/man/bundle-cache.1 +++ b/lib/bundler/man/bundle-cache.1 @@ -18,7 +18,7 @@ Include gems for all platforms present in the lockfile, not only the current one Specify a different cache path than the default (vendor/cache)\. .TP \fB\-\-cooldown=\fR -Only consider gem versions published at least \fInumber\fR days ago when resolving before caching\. Pass \fB0\fR to disable cooldown for this run, overriding any per\-source or global configuration\. See \fBcooldown\fR in bundle\-config(1)\. +Only consider gem versions published at least \fInumber\fR days ago when resolving before caching\. Pass \fB0\fR to disable cooldown for this run, overriding every other cooldown setting, RubyGems' own included\. See \fBcooldown\fR in bundle\-config(1)\. .TP \fB\-\-gemfile=GEMFILE\fR Use the specified gemfile instead of Gemfile\. diff --git a/lib/bundler/man/bundle-cache.1.ronn b/lib/bundler/man/bundle-cache.1.ronn index 91e16cde2284..9afae76da4d5 100644 --- a/lib/bundler/man/bundle-cache.1.ronn +++ b/lib/bundler/man/bundle-cache.1.ronn @@ -24,8 +24,8 @@ use the gems in the cache in preference to the ones on `rubygems.org`. * `--cooldown=`: Only consider gem versions published at least days ago when resolving before caching. Pass `0` to disable cooldown for this run, - overriding any per-source or global configuration. See `cooldown` in - bundle-config(1). + overriding every other cooldown setting, RubyGems' own included. See + `cooldown` in bundle-config(1). * `--gemfile=GEMFILE`: Use the specified gemfile instead of Gemfile. diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 index b8f37ca7d809..97f773f42c0b 100644 --- a/lib/bundler/man/bundle-config.1 +++ b/lib/bundler/man/bundle-config.1 @@ -90,16 +90,18 @@ The following is a list of all configuration keys and their purpose\. You can le .IP The effective cooldown for any given gem is resolved from three layers, highest precedence first: .IP "1." 4 -CLI flag \fB\-\-cooldown N\fR on \fBinstall\fR, \fBupdate\fR, \fBadd\fR, and \fBoutdated\fR\. +CLI flag \fB\-\-cooldown N\fR on \fBinstall\fR, \fBupdate\fR, \fBadd\fR, \fBcache\fR, \fBlock\fR, and \fBoutdated\fR\. .IP "2." 4 This setting (\fBbundle config set cooldown N\fR or \fBBUNDLE_COOLDOWN=N\fR)\. .IP "3." 4 The per\-source \fBcooldown:\fR keyword in the Gemfile, such as \fBsource "https://rubygems\.org", cooldown: 7\fR\. .IP "" 0 .IP -The CLI flag and this setting apply uniformly to every source, including ones declared with their own \fBcooldown:\fR value\. To keep a private registry permanently exempt while still cooling down public gems, declare \fBsource "https://internal", cooldown: 0\fR in the Gemfile; remember that \fB\-\-cooldown N\fR on the command line will still override it for that single run\. +The CLI flag and this setting apply uniformly to every source, including ones declared with their own \fBcooldown:\fR value\. To exempt a private registry while still cooling down public gems, declare \fBsource "https://internal", cooldown: 0\fR in the Gemfile\. That exemption is not absolute: \fB\-\-cooldown N\fR on the command line overrides it for a single run, and a gemrc cooldown raises it the same way it raises any other layer, as described below\. Where a gemrc cooldown is configured, \fB\-\-cooldown 0\fR is the only way to get the exemption back\. .IP -The value must be a non\-negative integer\. \fB\-\-cooldown\fR rejects anything else outright, while a value coming from this setting or from \fBBUNDLE_COOLDOWN\fR only warns, because a typo left in a config file should not make every command fail\. Such a value disables the cooldown for every source, including sources that declare their own \fBcooldown:\fR in the Gemfile\. +RubyGems has a cooldown of its own, set with \fBgem\fR's \fB\-\-cooldown\fR flag or the \fB:cooldown:\fR key in the gemrc file, and the two are read together so that a cooldown configured for only one of the two tools still protects both\. Whatever the three layers above resolve to is raised to the gemrc value, and the longer of the two wins; the same happens the other way round, where \fBgem install\fR and \fBgem update\fR read this setting\. A configured \fB0\fR takes part like any other value rather than switching the cooldown off, so \fBbundle config set cooldown 0\fR does not cancel a gemrc cooldown\. The \fB\-\-cooldown\fR flag is what bypasses the other tool's setting: it wins outright, which is why \fB\-\-cooldown 0\fR remains the escape hatch for an urgent update\. +.IP +The value must be a non\-negative integer\. \fB\-\-cooldown\fR rejects anything else outright, while a value coming from this setting, from \fBBUNDLE_COOLDOWN\fR or from the gemrc file only warns, because a typo left in a config file should not make every command fail\. Such a value takes no part in the resolution above, so a per\-source \fBcooldown:\fR in the Gemfile, or a usable value configured for the other tool, still applies\. The warning names where the value came from, since either tool reports the other's setting as well as its own\. .IP Cooldown filtering depends on the gem server providing a per\-version \fBcreated_at\fR timestamp in the v2 compact\-index format\. Versions without that metadata \- older gem servers, historical entries that predate the v2 cutover on \fBrubygems\.org\fR, or private registries that still emit the v1 format \- are treated as outside the cooldown window and remain resolvable\. If you rely on cooldown for supply\-chain protection, confirm that the gem server emits \fBcreated_at\fR in its \fB/info/\fR responses\. .IP diff --git a/lib/bundler/man/bundle-config.1.ronn b/lib/bundler/man/bundle-config.1.ronn index 4e1c67e10d55..ac8891f82b21 100644 --- a/lib/bundler/man/bundle-config.1.ronn +++ b/lib/bundler/man/bundle-config.1.ronn @@ -145,26 +145,44 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). The effective cooldown for any given gem is resolved from three layers, highest precedence first: - 1. CLI flag `--cooldown N` on `install`, `update`, `add`, and - `outdated`. + 1. CLI flag `--cooldown N` on `install`, `update`, `add`, `cache`, + `lock`, and `outdated`. 2. This setting (`bundle config set cooldown N` or `BUNDLE_COOLDOWN=N`). 3. The per-source `cooldown:` keyword in the Gemfile, such as `source "https://rubygems.org", cooldown: 7`. The CLI flag and this setting apply uniformly to every source, - including ones declared with their own `cooldown:` value. To keep a - private registry permanently exempt while still cooling down public - gems, declare `source "https://internal", cooldown: 0` in the - Gemfile; remember that `--cooldown N` on the command line will - still override it for that single run. + including ones declared with their own `cooldown:` value. To exempt a + private registry while still cooling down public gems, declare + `source "https://internal", cooldown: 0` in the Gemfile. That + exemption is not absolute: `--cooldown N` on the command line + overrides it for a single run, and a gemrc cooldown raises it the + same way it raises any other layer, as described below. Where a + gemrc cooldown is configured, `--cooldown 0` is the only way to get + the exemption back. + + RubyGems has a cooldown of its own, set with `gem`'s `--cooldown` + flag or the `:cooldown:` key in the gemrc file, and the two are read + together so that a cooldown configured for only one of the two tools + still protects both. Whatever the three layers above resolve to is + raised to the gemrc value, and the longer of the two wins; the same + happens the other way round, where `gem install` and `gem update` + read this setting. A configured `0` takes part like any other value + rather than switching the cooldown off, so `bundle config set + cooldown 0` does not cancel a gemrc cooldown. The `--cooldown` flag + is what bypasses the other tool's setting: it wins outright, which is + why `--cooldown 0` remains the escape hatch for an urgent update. The value must be a non-negative integer. `--cooldown` rejects - anything else outright, while a value coming from this setting or - from `BUNDLE_COOLDOWN` only warns, because a typo left in a config - file should not make every command fail. Such a value disables the - cooldown for every source, including sources that declare their own - `cooldown:` in the Gemfile. + anything else outright, while a value coming from this setting, from + `BUNDLE_COOLDOWN` or from the gemrc file only warns, because a typo + left in a config file should not make every command fail. Such a + value takes no part in the resolution above, so a per-source + `cooldown:` in the Gemfile, or a usable value configured for the + other tool, still applies. The warning names where the value came + from, since either tool reports the other's setting as well as its + own. Cooldown filtering depends on the gem server providing a per-version `created_at` timestamp in the v2 compact-index format. Versions diff --git a/lib/bundler/man/bundle-install.1 b/lib/bundler/man/bundle-install.1 index 801768c7ecef..be5a210b1a39 100644 --- a/lib/bundler/man/bundle-install.1 +++ b/lib/bundler/man/bundle-install.1 @@ -14,7 +14,7 @@ If a \fBGemfile\.lock\fR does exist, and you have updated your Gemfile(5), Bundl .SH "OPTIONS" .TP \fB\-\-cooldown=\fR -Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run, overriding any per\-source or global configuration\. See \fBcooldown\fR in bundle\-config(1) for details on the precedence between the CLI flag, Bundler config, and Gemfile per\-source settings\. +Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run, overriding every other cooldown setting, RubyGems' own included\. See \fBcooldown\fR in bundle\-config(1) for details on the precedence between the CLI flag, Bundler config, Gemfile per\-source settings, and the gemrc file\. .TP \fB\-\-force\fR, \fB\-\-redownload\fR Force reinstalling every gem, even if already installed\. diff --git a/lib/bundler/man/bundle-install.1.ronn b/lib/bundler/man/bundle-install.1.ronn index 56fd8bdf42a1..5ef990223f65 100644 --- a/lib/bundler/man/bundle-install.1.ronn +++ b/lib/bundler/man/bundle-install.1.ronn @@ -40,10 +40,10 @@ update process below under [CONSERVATIVE UPDATING][]. * `--cooldown=`: Only consider gem versions published at least days ago when - resolving. Pass `0` to disable cooldown for this run, overriding any - per-source or global configuration. See `cooldown` in bundle-config(1) - for details on the precedence between the CLI flag, Bundler config, - and Gemfile per-source settings. + resolving. Pass `0` to disable cooldown for this run, overriding every + other cooldown setting, RubyGems' own included. See `cooldown` in + bundle-config(1) for details on the precedence between the CLI flag, + Bundler config, Gemfile per-source settings, and the gemrc file. * `--force`, `--redownload`: Force reinstalling every gem, even if already installed. diff --git a/lib/bundler/man/bundle-lock.1 b/lib/bundler/man/bundle-lock.1 index 8f6a77e435ee..f060fb1a3ad2 100644 --- a/lib/bundler/man/bundle-lock.1 +++ b/lib/bundler/man/bundle-lock.1 @@ -61,7 +61,7 @@ If updating, do not allow any gem to be updated past latest \-\-patch | \-\-mino If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated\. .TP \fB\-\-cooldown=\fR -Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run, overriding any per\-source or global configuration\. See \fBcooldown\fR in bundle\-config(1)\. +Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run, overriding every other cooldown setting, RubyGems' own included\. See \fBcooldown\fR in bundle\-config(1)\. .SH "UPDATING ALL GEMS" If you run \fBbundle lock\fR with \fB\-\-update\fR option without list of gems, bundler will ignore any previously installed gems and resolve all dependencies again based on the latest versions of all gems available in the sources\. .SH "UPDATING A LIST OF GEMS" diff --git a/lib/bundler/man/bundle-lock.1.ronn b/lib/bundler/man/bundle-lock.1.ronn index 683a429b0e1e..df46ac39fc51 100644 --- a/lib/bundler/man/bundle-lock.1.ronn +++ b/lib/bundler/man/bundle-lock.1.ronn @@ -87,8 +87,9 @@ Lock the gems specified in Gemfile. * `--cooldown=`: Only consider gem versions published at least days ago when - resolving. Pass `0` to disable cooldown for this run, overriding any - per-source or global configuration. See `cooldown` in bundle-config(1). + resolving. Pass `0` to disable cooldown for this run, overriding every + other cooldown setting, RubyGems' own included. See `cooldown` in + bundle-config(1). ## UPDATING ALL GEMS diff --git a/lib/bundler/man/bundle-update.1 b/lib/bundler/man/bundle-update.1 index 94161083fc45..9d5ea89c4d71 100644 --- a/lib/bundler/man/bundle-update.1 +++ b/lib/bundler/man/bundle-update.1 @@ -66,7 +66,7 @@ Do not allow any gem to be updated past latest \fB\-\-patch\fR | \fB\-\-minor\fR Use bundle install conservative update behavior and do not allow indirect dependencies to be updated\. .TP \fB\-\-cooldown=\fR -Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run, overriding any per\-source or global configuration\. Combine with \fB\-\-conservative\fR to minimize transitive churn when bypassing cooldown for an urgent update\. See \fBcooldown\fR in bundle\-config(1)\. +Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run, overriding every other cooldown setting, RubyGems' own included\. Combine with \fB\-\-conservative\fR to minimize transitive churn when bypassing cooldown for an urgent update\. See \fBcooldown\fR in bundle\-config(1)\. .SH "UPDATING ALL GEMS" If you run \fBbundle update \-\-all\fR, bundler will ignore any previously installed gems and resolve all dependencies again based on the latest versions of all gems available in the sources\. .P diff --git a/lib/bundler/man/bundle-update.1.ronn b/lib/bundler/man/bundle-update.1.ronn index 72fbf054d157..3ca4dc730a2f 100644 --- a/lib/bundler/man/bundle-update.1.ronn +++ b/lib/bundler/man/bundle-update.1.ronn @@ -94,8 +94,8 @@ gem. * `--cooldown=`: Only consider gem versions published at least days ago when - resolving. Pass `0` to disable cooldown for this run, overriding any - per-source or global configuration. Combine with `--conservative` to + resolving. Pass `0` to disable cooldown for this run, overriding every + other cooldown setting, RubyGems' own included. Combine with `--conservative` to minimize transitive churn when bypassing cooldown for an urgent update. See `cooldown` in bundle-config(1). diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb index 02780bef730c..db5298cdc4a3 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -258,6 +258,12 @@ def pretty_values_for(exposed_key) locations << "Set for the current user (#{global_config_file}): #{printable_value(value, exposed_key).inspect}" end + # The gemrc cooldown sits outside the priority order too: it is not one + # of the layers, it raises whatever they resolve to. See #cooldown_for. + if key == key_for(:cooldown) && (value = rubygems_cooldown) + locations << "Set in the RubyGems configuration as `:cooldown:`: #{value.inspect}. The longer of that and the value above applies" + end + return ["You have not configured a value for `#{exposed_key}`"] if locations.empty? locations end @@ -369,6 +375,50 @@ def installation_parallelization self[:jobs] || processor_count end + ## + # The cooldown that applies to a source whose Gemfile declaration asks for + # +source_cooldown+ days. + # + # `--cooldown` is set as a command line option, and it wins outright so + # that `--cooldown 0` bypasses the cooldown however the two tools are + # configured. Otherwise this setting, or the per-source value when this + # setting is unset, is raised to RubyGems' own `:cooldown:` setting, so a + # cooldown configured for only one of the two tools covers both. + + def cooldown_for(source_cooldown = nil) + command_line = @temporary[key_for(:cooldown)] + return converted_value(command_line, :cooldown) unless command_line.nil? + + # Read raw rather than through #[], whose `to_i` would turn a value that + # is not a number into a 0 that suppresses `source_cooldown`. + configured = cooldown_settings.days(configured_value(:cooldown)) + + cooldown_settings.combine(configured || source_cooldown, rubygems_cooldown) + end + + ## + # RubyGems' `:cooldown:` gemrc setting, read from the loaded gemrc rather + # than from `Gem.configuration.cooldown`, which only exists on RubyGems + # versions that know the setting. A value assigned to that accessor from + # Ruby after startup is therefore not seen here. + # + # Reading it builds Gem::ConfigFile, which costs a command that never + # resolves against a remote around 30ms it has no use for, so the setting + # is validated here, at the point of use, rather than when the command + # starts. + + def rubygems_cooldown + return @rubygems_cooldown if defined?(@rubygems_cooldown) + + @rubygems_cooldown = gemrc_cooldown + + if cooldown_settings.invalid?(@rubygems_cooldown) + Bundler.ui.warn cooldown_settings.invalid_message(@rubygems_cooldown, "the gemrc file") + end + + @rubygems_cooldown + end + def validate! all.each do |raw_key| [@local_config, @env_config, @global_config].each do |settings| @@ -384,6 +434,19 @@ def key_for(key) private + def cooldown_settings + require "rubygems/cooldown_settings" + Gem::CooldownSettings + end + + # Scanned rather than looked up, because ConfigFile#[] stringifies the key + # on RubyGems 3.4 and a `:cooldown:` gemrc entry is stored under a Symbol. + + def gemrc_cooldown + Gem.configuration.each {|key, value| return value if key.to_s == "cooldown" } + nil + end + def configs @configs ||= { temporary: @temporary, diff --git a/lib/bundler/source/rubygems/remote.rb b/lib/bundler/source/rubygems/remote.rb index ca788f829399..31465a4f9262 100644 --- a/lib/bundler/source/rubygems/remote.rb +++ b/lib/bundler/source/rubygems/remote.rb @@ -18,8 +18,8 @@ def initialize(uri, cooldown: nil) end # Returns the cooldown days that apply to this remote, resolving the - # precedence CLI > config > Gemfile per-source. Returns nil if no - # cooldown applies. + # precedence CLI > config > Gemfile per-source and then raising the + # result to RubyGems' own setting. Returns nil if no cooldown applies. # # The resolver asks once per candidate spec, so the settings lookup is # memoized. That snapshots the value: a Remote created before a @@ -28,7 +28,7 @@ def initialize(uri, cooldown: nil) # before the Definition exists), so build a new Remote if you need to. def effective_cooldown return @effective_cooldown if defined?(@effective_cooldown) - @effective_cooldown = Bundler.settings[:cooldown] || @cooldown + @effective_cooldown = Bundler.settings.cooldown_for(@cooldown) end MAX_CACHE_SLUG_HOST_SIZE = 255 - 1 - 32 # 255 minus dot minus MD5 length diff --git a/lib/rubygems/bundler_settings.rb b/lib/rubygems/bundler_settings.rb new file mode 100644 index 000000000000..f470ca075430 --- /dev/null +++ b/lib/rubygems/bundler_settings.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require_relative "util" + +## +# Reads Bundler's settings without loading Bundler, for the RubyGems commands +# that have to agree with something the user configured for Bundler. +# +# Follows Bundler::Settings: the application config file wins over the +# BUNDLE_ environment variable, which wins over the user config file, +# and BUNDLE_IGNORE_CONFIG drops both files. + +module Gem::BundlerSettings + ## + # The configured value of Bundler setting +name+, or nil when it is unset. + + def self.[](name) + key = key_for name + + value = config_value app_config_file, key + value = env_value key if value.nil? + value = config_value user_config_file, key if value.nil? + value + end + + ## + # The value of Bundler setting +name+ from the config files only, + # application file first. Callers that read the environment variable ahead + # of the files pair this with .env. + + def self.from_config_files(name) + key = key_for name + + value = config_value app_config_file, key + value.nil? ? config_value(user_config_file, key) : value + end + + ## + # The value of Bundler setting +name+ from the environment, or nil when the + # variable is unset or empty. + + def self.env(name) + env_value key_for(name) + end + + ## + # The gem dependencies file above the working directory, or nil when there + # is none. RubyGems recognizes a couple of names Bundler does not, so a + # directory holding only one of those is where the two disagree. + + def self.gemfile_path + gemfile = env_value "BUNDLE_GEMFILE" + return gemfile if gemfile + + Gem::Util.traverse_parents(Dir.pwd) do |directory| + found = Gem::GEM_DEP_FILES.find {|f| File.file?(f) } + + return File.join(directory, found) if found + end + + nil + rescue SystemCallError + # Dir.pwd raises when the working directory has been deleted, and when + # an ancestor denies search to the current uid. + nil + end + + def self.key_for(name) + "BUNDLE_#{name.to_s.gsub(".", "__").gsub("-", "___").upcase}" + end + private_class_method :key_for + + def self.env_value(key) + value = ENV[key] + + value unless value.nil? || value.empty? + end + private_class_method :env_value + + ## + # The config file for the application, honoring BUNDLE_APP_CONFIG the way + # Bundler.app_config_path does: an absolute path is used as given, and a + # relative one is resolved against the directory holding the Gemfile. + + def self.app_config_file + return if ignore_config? + + app_config = env_value("BUNDLE_APP_CONFIG") || ".bundle" + return File.join(app_config, "config") if File.absolute_path?(app_config) + + gemfile = gemfile_path + File.join(File.dirname(gemfile), app_config, "config") if gemfile + end + private_class_method :app_config_file + + def self.user_config_file + return if ignore_config? + + file = env_value("BUNDLE_CONFIG") || env_value("BUNDLE_USER_CONFIG") + return file if file + + home = env_value("BUNDLE_USER_HOME") + return File.join(home, "config") if home + + user_home = Gem.user_home + File.join(user_home, ".bundle", "config") if user_home && !user_home.empty? + end + private_class_method :user_config_file + + def self.ignore_config? + !ENV["BUNDLE_IGNORE_CONFIG"].nil? + end + private_class_method :ignore_config? + + ## + # A config file that cannot be read or parsed leaves the setting + # unconfigured rather than aborting the gem command that only wanted to + # consult it. The rescue is deliberately broad: Gem::YAMLSerializer signals + # a bad document with Psych::DisallowedClass, Psych::BadAlias and + # Psych::SyntaxError, which share no class this method could name, and a + # reader that only ever returns a value or nil gains nothing from letting a + # newly added one through. + + def self.config_value(file, key) + return unless file && File.file?(file) + + require_relative "yaml_serializer" + + config = Gem::YAMLSerializer.load File.read(file) + # A config file whose top level is not a mapping has no settings in it, + # and indexing it would read a substring or raise instead of saying so. + config[key] if config.is_a?(Hash) + rescue StandardError + nil + end + private_class_method :config_value +end diff --git a/lib/rubygems/bundler_version_finder.rb b/lib/rubygems/bundler_version_finder.rb index bbe7bf0ab55e..eae408171b0b 100644 --- a/lib/rubygems/bundler_version_finder.rb +++ b/lib/rubygems/bundler_version_finder.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative "bundler_settings" + module Gem::BundlerVersionFinder def self.bundler_version bcv = bundle_config_version @@ -70,66 +72,16 @@ def self.lockfile_contents end private_class_method :lockfile_contents + # BUNDLE_VERSION is read before the config files here, unlike everywhere + # else in Bundler, so the env var alone is enough to pick the version that + # runs without editing a config file first. def self.bundle_config_version - env_version = ENV["BUNDLE_VERSION"] - return env_version if env_version && !env_version.empty? - - version = nil - - [bundler_local_config_file, bundler_global_config_file].each do |config_file| - next unless config_file && File.file?(config_file) - - contents = File.read(config_file) - contents =~ /^BUNDLE_VERSION:\s*["']?([^"'\s]+)["']?\s*$/ - - version = $1 - break if version - end - - version + Gem::BundlerSettings.env("version") || Gem::BundlerSettings.from_config_files("version") end private_class_method :bundle_config_version - def self.bundler_global_config_file - # see Bundler::Settings#global_config_file - if ENV["BUNDLE_CONFIG"] && !ENV["BUNDLE_CONFIG"].empty? - ENV["BUNDLE_CONFIG"] - elsif ENV["BUNDLE_USER_CONFIG"] && !ENV["BUNDLE_USER_CONFIG"].empty? - ENV["BUNDLE_USER_CONFIG"] - elsif ENV["BUNDLE_USER_HOME"] && !ENV["BUNDLE_USER_HOME"].empty? - ENV["BUNDLE_USER_HOME"] + "config" - elsif Gem.user_home && !Gem.user_home.empty? - Gem.user_home + ".bundle/config" - end - end - private_class_method :bundler_global_config_file - - def self.bundler_local_config_file - gemfile = gemfile_path - return unless gemfile - - File.join(File.dirname(gemfile), ".bundle", "config") - end - private_class_method :bundler_local_config_file - def self.gemfile_path - gemfile = ENV["BUNDLE_GEMFILE"] - gemfile = nil if gemfile&.empty? - - unless gemfile - begin - Gem::Util.traverse_parents(Dir.pwd) do |directory| - next unless gemfile = Gem::GEM_DEP_FILES.find {|f| File.file?(f) } - - gemfile = File.join directory, gemfile - break - end - rescue Errno::ENOENT - return - end - end - - gemfile + Gem::BundlerSettings.gemfile_path end private_class_method :gemfile_path end diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb index 853de9e8ecd1..08aad3fa3190 100644 --- a/lib/rubygems/config_file.rb +++ b/lib/rubygems/config_file.rb @@ -130,9 +130,14 @@ class Gem::ConfigFile ## # Number of days a newly published gem version must wait before it is # considered for installation or update (the cooldown period). 0 - # disables the cooldown. A value that cannot be read as a non-negative - # integer warns and leaves the cooldown disabled, so a typo in the gemrc - # file does not make every command fail. + # disables the cooldown, unless Bundler is configured with a longer one: + # this setting and Bundler's own +cooldown+ setting are both read, and the + # longer of the two applies, so a cooldown configured for only one of the + # two tools still covers both. --cooldown overrides both. + # + # A value that cannot be read as a non-negative integer warns and takes no + # part in that resolution, so a typo in the gemrc file does not make every + # command fail. attr_accessor :cooldown diff --git a/lib/rubygems/cooldown.rb b/lib/rubygems/cooldown.rb index 9af06f38202c..dd4de5e66c18 100644 --- a/lib/rubygems/cooldown.rb +++ b/lib/rubygems/cooldown.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative "cooldown_settings" require_relative "user_interaction" ## @@ -10,8 +11,10 @@ # sources that do not provide publish times keep working. # # The cooldown period comes from the --cooldown DAYS option when -# given, falling back to the :cooldown: setting in the gemrc file. -# A value of 0 disables the cooldown. +# given, and 0 there disables the cooldown. Without the option the +# :cooldown: setting in the gemrc file and Bundler's own cooldown +# setting both apply and the longer of the two wins, so a 0 in either of them +# disables nothing while the other names a period. class Gem::Cooldown ## @@ -20,23 +23,30 @@ class Gem::Cooldown attr_reader :days ## - # Creates a Cooldown from the command line +options+, preferring the - # --cooldown option over the :cooldown: gemrc setting. + # Creates a Cooldown from the command line +options+. The --cooldown + # option wins outright, so --cooldown 0 bypasses the cooldown + # however the two tools are configured. Without it the :cooldown: gemrc + # setting and Bundler's cooldown setting are both read and the longer of + # the two applies, so a cooldown configured for only one of them still + # covers gem commands. def self.from_options(options) - new(options[:cooldown] || Gem.configuration.cooldown) + days = options[:cooldown] + return new(days) unless days.nil? + + require_relative "bundler_settings" + + new Gem::CooldownSettings.combine(warn_unless_valid(Gem.configuration.cooldown, "the gemrc file"), + warn_unless_valid(Gem::BundlerSettings["cooldown"], "Bundler's configuration")) end def initialize(days, now: Time.now) - # A gemrc value is arbitrary YAML, so it can be any type at all. Anything - # that cannot be read as a non-negative integer leaves the cooldown - # disabled rather than raising out of an unrelated command. - valid = valid_days?(days) + invalid = Gem::CooldownSettings.invalid?(days) - @days = valid ? days.to_i : 0 + @days = Gem::CooldownSettings.days(days) || 0 @now = now - Gem::Cooldown.warn_invalid_days(days) unless valid || days.nil? + Gem::Cooldown.warn_invalid_days(days, "the cooldown setting") if invalid end ## @@ -134,29 +144,30 @@ def self.reset_warned_missing_created_at # :nodoc: @warned = nil end - # Warns once per process that a configured cooldown value cannot be read - # as a non-negative integer, which leaves the cooldown disabled. The - # --cooldown option is validated by the option parser; this catches the - # gemrc path. + # Returns +value+, warning first when it is configured but cannot be read + # as a number of days. - def self.warn_invalid_days(value) # :nodoc: - return if @warned_invalid_days - @warned_invalid_days = true + def self.warn_unless_valid(value, source) # :nodoc: + warn_invalid_days(value, source) if Gem::CooldownSettings.invalid?(value) - Gem::DefaultUserInteraction.ui.alert_warning \ - "Invalid cooldown value #{value.inspect}, so the cooldown is disabled. " \ - "Expected a non-negative integer number of days." + value end - def self.reset_warned_invalid_days # :nodoc: - @warned_invalid_days = nil - end + # Warns that a configured cooldown value cannot be read as a non-negative + # integer, so it does not apply. The --cooldown option is validated by the + # option parser; this catches the config file paths. Both the gemrc and + # Bundler settings feed one resolution, so each source gets its own warning + # rather than the first one silencing the other. - private + def self.warn_invalid_days(value, source) # :nodoc: + @warned_invalid_days ||= [] + return if @warned_invalid_days.include?(source) + @warned_invalid_days << source - def valid_days?(value) - days = Integer(value.to_s, exception: false) + Gem::DefaultUserInteraction.ui.alert_warning Gem::CooldownSettings.invalid_message(value, source) + end - !days.nil? && !days.negative? + def self.reset_warned_invalid_days # :nodoc: + @warned_invalid_days = nil end end diff --git a/lib/rubygems/cooldown_option.rb b/lib/rubygems/cooldown_option.rb index b91ae6eab45c..e03dac42a080 100644 --- a/lib/rubygems/cooldown_option.rb +++ b/lib/rubygems/cooldown_option.rb @@ -12,7 +12,8 @@ module Gem::CooldownOption def add_cooldown_option(group = nil) args = [group, "--cooldown DAYS", Integer, "Do not use gem versions published within", - "the last DAYS days (0 disables the cooldown)"].compact + "the last DAYS days (0 disables the cooldown,", + "overriding the gemrc and Bundler settings)"].compact add_option(*args) do |value, options| if value.negative? diff --git a/lib/rubygems/cooldown_settings.rb b/lib/rubygems/cooldown_settings.rb new file mode 100644 index 000000000000..2349ad309a84 --- /dev/null +++ b/lib/rubygems/cooldown_settings.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# Skip reloading when an identical copy (e.g. the one shipped inside the Bundler +# gem) was already required from a different path, to avoid redefinition warnings. +return if defined?(Gem::CooldownSettings) + +## +# The rules RubyGems and Bundler share for reading a configured cooldown +# period. Bundler ships a copy of this file, so it deliberately has no +# requires: none of the rest of RubyGems is guaranteed to be around it. + +module Gem::CooldownSettings + ## + # +value+ read as a number of days, or nil when it is absent or cannot be + # read as a non-negative integer. A gemrc entry and a Bundler config entry + # are both arbitrary YAML, so either can be any type at all. + + def self.days(value) + return if value.nil? + + # Base 10 explicitly: a leading zero is how a user writes a small number + # of days, not a request for octal. + days = Integer(value.to_s, 10, exception: false) + + days if days && !days.negative? + end + + ## + # True when +value+ is configured but cannot be read as a number of days. + + def self.invalid?(value) + !value.nil? && days(value).nil? + end + + ## + # The cooldown that applies when RubyGems and Bundler are configured + # separately: the longest of +values+, so a cooldown configured for only + # one of the two tools protects both. Returns nil when none of them is + # usable. + # + # A configured 0 takes part like any other value rather than switching the + # cooldown off, which leaves --cooldown 0 as the way to bypass a + # cooldown that the other tool configures. + + def self.combine(*values) + values.filter_map {|value| days(value) }.max + end + + # +source+ names where the value came from, since RubyGems and Bundler now + # each read the other's setting and the same complaint from either of them + # is otherwise impossible to trace back to a file. + + def self.invalid_message(value, source) # :nodoc: + "Invalid cooldown value #{value.inspect} in #{source}, so it is ignored. " \ + "Expected a non-negative integer number of days." + end +end diff --git a/spec/bundler/cli_common_spec.rb b/spec/bundler/cli_common_spec.rb index d38dc998c82d..b592da603b67 100644 --- a/spec/bundler/cli_common_spec.rb +++ b/spec/bundler/cli_common_spec.rb @@ -22,7 +22,7 @@ describe "validate_cooldown!" do def warning(inspected) - "Invalid cooldown value #{inspected}, so the cooldown is disabled for all sources. " \ + "Invalid cooldown value #{inspected} in Bundler's configuration, so it is ignored. " \ "Expected a non-negative integer number of days." end @@ -66,6 +66,11 @@ def warning(inspected) Bundler.settings.temporary(cooldown: "0") { subject.validate_cooldown!(nil) } subject.validate_cooldown!(nil) end + + it "leaves the RubyGems setting to Bundler::Settings#rubygems_cooldown" do + expect(Bundler.settings).not_to receive(:rubygems_cooldown) + subject.validate_cooldown!(nil) + end end end end diff --git a/spec/bundler/settings_spec.rb b/spec/bundler/settings_spec.rb index 58027fd0b22c..805c80b9bcf2 100644 --- a/spec/bundler/settings_spec.rb +++ b/spec/bundler/settings_spec.rb @@ -136,6 +136,134 @@ end end + describe "#pretty_values_for" do + it "reports a gemrc cooldown that no Bundler layer configures" do + allow(Gem.configuration).to receive(:each).and_yield(:cooldown, 7) + + expect(settings.pretty_values_for(:cooldown)).to eq( + ["Set in the RubyGems configuration as `:cooldown:`: 7. The longer of that and the value above applies"] + ) + end + + it "says nothing about RubyGems when it configures no cooldown" do + allow(Gem.configuration).to receive(:each) + + expect(settings.pretty_values_for(:cooldown)).to eq(["You have not configured a value for `cooldown`"]) + end + end + + describe "#rubygems_cooldown" do + it "warns once when the gemrc value is not a number" do + allow(Gem.configuration).to receive(:each).and_yield(:cooldown, "abc") + + expect(Bundler.ui).to receive(:warn).with(/Invalid cooldown value "abc"/).once + 2.times { settings.rubygems_cooldown } + end + + it "stays quiet for a usable value" do + allow(Gem.configuration).to receive(:each).and_yield(:cooldown, 7) + + expect(Bundler.ui).not_to receive(:warn) + expect(settings.rubygems_cooldown).to be 7 + end + + it "reads a value that Gem::ConfigFile#[] cannot look up" do + # RubyGems 3.4 behaves this way. See Settings#gemrc_cooldown. + allow(Gem.configuration).to receive(:[]).with(:cooldown).and_return(nil) + allow(Gem.configuration).to receive(:each).and_yield(:cooldown, 7) + + expect(settings.rubygems_cooldown).to be 7 + end + + it "reads a value stored under a string key" do + allow(Gem.configuration).to receive(:each).and_yield("cooldown", 7) + + expect(settings.rubygems_cooldown).to be 7 + end + end + + describe "#cooldown_for" do + before { allow(settings).to receive(:rubygems_cooldown).and_return(nil) } + + it "is nil when nothing is configured" do + expect(settings.cooldown_for).to be_nil + end + + it "returns the per-source value when the setting is unset" do + expect(settings.cooldown_for(7)).to be 7 + end + + it "prefers the setting over the per-source value" do + settings.set_local :cooldown, "14" + + expect(settings.cooldown_for(7)).to be 14 + end + + it "keeps the per-source value when the setting is not a number" do + settings.set_local :cooldown, "abc" + + expect(settings.cooldown_for(7)).to be 7 + end + + context "when RubyGems configures a cooldown too" do + it "uses it when Bundler configures none" do + allow(settings).to receive(:rubygems_cooldown).and_return(7) + + expect(settings.cooldown_for).to be 7 + end + + it "takes the longer of the two" do + allow(settings).to receive(:rubygems_cooldown).and_return(7) + settings.set_local :cooldown, "3" + + expect(settings.cooldown_for).to be 7 + + settings.set_local :cooldown, "14" + + expect(settings.cooldown_for).to be 14 + end + + it "raises a per-source value to it" do + allow(settings).to receive(:rubygems_cooldown).and_return(7) + + expect(settings.cooldown_for(3)).to be 7 + end + + it "leaves a longer per-source value alone" do + allow(settings).to receive(:rubygems_cooldown).and_return(7) + + expect(settings.cooldown_for(14)).to be 14 + end + + it "counts a configured 0 as a value rather than as unset" do + allow(settings).to receive(:rubygems_cooldown).and_return(7) + settings.set_local :cooldown, "0" + + expect(settings.cooldown_for).to be 7 + + allow(settings).to receive(:rubygems_cooldown).and_return(0) + settings.set_local :cooldown, "7" + + expect(settings.cooldown_for).to be 7 + end + + it "ignores a value that is not a number" do + allow(settings).to receive(:rubygems_cooldown).and_return("abc") + settings.set_local :cooldown, "7" + + expect(settings.cooldown_for).to be 7 + end + + it "lets --cooldown win outright so 0 bypasses it" do + allow(settings).to receive(:rubygems_cooldown).and_return(7) + + settings.temporary(cooldown: 0) do + expect(settings.cooldown_for(14)).to be 0 + end + end + end + end + context "when the setting has been renamed" do it "reads the value set under the old name" do settings.set_local :no_prune, "true" diff --git a/spec/bundler/source/rubygems/remote_spec.rb b/spec/bundler/source/rubygems/remote_spec.rb index 0fca8dbd6725..d8b848f9c34b 100644 --- a/spec/bundler/source/rubygems/remote_spec.rb +++ b/spec/bundler/source/rubygems/remote_spec.rb @@ -206,18 +206,18 @@ def remote(uri) it "reads the settings only once, however many candidates ask" do r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7) - expect(Bundler.settings).to receive(:[]).with(:cooldown).once.and_return(14) + expect(Bundler.settings).to receive(:cooldown_for).with(7).once.and_return(14) expect(r.effective_cooldown).to eq(14) expect(r.effective_cooldown).to eq(14) end - it "memoizes an absent override without re-reading the settings" do + it "memoizes an absent cooldown without re-reading the settings" do r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7) - expect(Bundler.settings).to receive(:[]).with(:cooldown).once.and_return(nil) + expect(Bundler.settings).to receive(:cooldown_for).with(7).once.and_return(nil) - expect(r.effective_cooldown).to eq(7) - expect(r.effective_cooldown).to eq(7) + expect(r.effective_cooldown).to be_nil + expect(r.effective_cooldown).to be_nil end end end diff --git a/spec/install/cooldown_spec.rb b/spec/install/cooldown_spec.rb index 6991725aa364..2c0cb7f96ae4 100644 --- a/spec/install/cooldown_spec.rb +++ b/spec/install/cooldown_spec.rb @@ -200,6 +200,83 @@ expect(the_bundle).to include_gems("ripe_gem 2.0.0") end + context "when RubyGems configures a cooldown" do + before do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + end + + after { FileUtils.rm_rf home(".gemrc") } + + def gemrc_cooldown(days) + File.write home(".gemrc"), { cooldown: days }.to_yaml + end + + it "applies it when bundler configures none of its own" do + gemrc_cooldown 7 + + bundle "install", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "takes the longer of the two settings" do + gemrc_cooldown 7 + + bundle "install", env: { "BUNDLE_COOLDOWN" => "1" }, artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "counts a bundler-side 0 as a value rather than as unset" do + gemrc_cooldown 7 + + bundle "install", env: { "BUNDLE_COOLDOWN" => "0" }, artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "counts a gemrc 0 as a value rather than as unset" do + gemrc_cooldown 0 + + bundle "install", env: { "BUNDLE_COOLDOWN" => "7" }, artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "raises a shorter per-source cooldown to it" do + gemrc_cooldown 7 + + gemfile <<-G + source "https://gem.repo3", cooldown: 0 + gem "ripe_gem" + G + + bundle "install", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "lets --cooldown 0 bypass it" do + gemrc_cooldown 7 + + bundle "install --cooldown 0", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end + + it "warns and ignores it when it is not a number" do + gemrc_cooldown "seven" + + bundle "install", artifice: "compact_index_cooldown" + + expect(err).to include('Invalid cooldown value "seven" in the gemrc file, so it is ignored.') + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end + end + it "summarizes skipped versions at the end of bundle install" do gemfile <<-G source "https://gem.repo3" diff --git a/spec/support/path.rb b/spec/support/path.rb index 5df6de58d6cf..a3bf3251c866 100644 --- a/spec/support/path.rb +++ b/spec/support/path.rb @@ -359,7 +359,7 @@ def git_ls_files(glob) end def tracked_files_glob - ruby_core? ? "libexec/bundle* lib/bundler lib/bundler.rb lib/rubygems/vendor/uri lib/rubygems/vendor/securerandom lib/rubygems/vendor/pub_grub lib/rubygems/yaml_serializer.rb lib/rubygems/compact_index_client* lib/rubygems/credential_store* spec/bundler man/bundle*" : "exe/bundle exe/bundler lib/bundler lib/bundler.rb lib/rubygems/vendor/uri lib/rubygems/vendor/securerandom lib/rubygems/vendor/pub_grub lib/rubygems/yaml_serializer.rb lib/rubygems/compact_index_client* lib/rubygems/credential_store* bundler.gemspec CHANGELOG-bundler.md MIT.txt" + ruby_core? ? "libexec/bundle* lib/bundler lib/bundler.rb lib/rubygems/vendor/uri lib/rubygems/vendor/securerandom lib/rubygems/vendor/pub_grub lib/rubygems/yaml_serializer.rb lib/rubygems/cooldown_settings.rb lib/rubygems/compact_index_client* lib/rubygems/credential_store* spec/bundler man/bundle*" : "exe/bundle exe/bundler lib/bundler lib/bundler.rb lib/rubygems/vendor/uri lib/rubygems/vendor/securerandom lib/rubygems/vendor/pub_grub lib/rubygems/yaml_serializer.rb lib/rubygems/cooldown_settings.rb lib/rubygems/compact_index_client* lib/rubygems/credential_store* bundler.gemspec CHANGELOG-bundler.md MIT.txt" end def lib_tracked_files_glob diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index cd50ed5ae1a8..72935437878a 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -397,6 +397,9 @@ def setup ENV["BUNDLE_CONFIG"] = nil ENV["BUNDLE_USER_CONFIG"] = nil ENV["BUNDLE_USER_HOME"] = nil + # Cooldown resolution reads Bundler's settings, so a developer running the + # suite with one configured would otherwise see it applied to the fixtures. + ENV["BUNDLE_COOLDOWN"] = nil ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"] = "true" @current_dir = Dir.pwd diff --git a/test/rubygems/test_gem_bundler_settings.rb b/test/rubygems/test_gem_bundler_settings.rb new file mode 100644 index 000000000000..0a4e22978d82 --- /dev/null +++ b/test/rubygems/test_gem_bundler_settings.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true + +require_relative "helper" +require "rubygems/bundler_settings" + +class TestGemBundlerSettings < Gem::TestCase + def setup + super + + @gemfile = File.join @tempdir, "Gemfile" + FileUtils.touch @gemfile + ENV["BUNDLE_GEMFILE"] = @gemfile + end + + def test_reads_the_environment_variable + ENV["BUNDLE_COOLDOWN"] = "7" + + assert_equal "7", Gem::BundlerSettings["cooldown"] + end + + def test_ignores_an_empty_environment_variable + ENV["BUNDLE_COOLDOWN"] = "" + + assert_nil Gem::BundlerSettings["cooldown"] + end + + def test_reads_the_user_config_file + write_global_config "BUNDLE_COOLDOWN: \"7\"\n" + + assert_equal "7", Gem::BundlerSettings["cooldown"] + end + + def test_reads_the_application_config_file + write_app_config "BUNDLE_COOLDOWN: \"7\"\n" + + assert_equal "7", Gem::BundlerSettings["cooldown"] + end + + def test_application_config_file_wins_over_the_environment_and_the_user_file + write_app_config "BUNDLE_COOLDOWN: \"3\"\n" + ENV["BUNDLE_COOLDOWN"] = "5" + write_global_config "BUNDLE_COOLDOWN: \"7\"\n" + + assert_equal "3", Gem::BundlerSettings["cooldown"] + end + + def test_environment_wins_over_the_user_config_file + ENV["BUNDLE_COOLDOWN"] = "5" + write_global_config "BUNDLE_COOLDOWN: \"7\"\n" + + assert_equal "5", Gem::BundlerSettings["cooldown"] + end + + def test_bundle_app_config_relocates_the_application_config_file + dir = File.join @tempdir, "elsewhere" + FileUtils.mkdir_p dir + File.write File.join(dir, "config"), "BUNDLE_COOLDOWN: \"7\"\n" + + write_app_config "BUNDLE_COOLDOWN: \"3\"\n" + ENV["BUNDLE_APP_CONFIG"] = dir + + assert_equal "7", Gem::BundlerSettings["cooldown"] + end + + def test_relative_bundle_app_config_resolves_against_the_gemfile + dir = File.join @tempdir, "elsewhere" + FileUtils.mkdir_p dir + File.write File.join(dir, "config"), "BUNDLE_COOLDOWN: \"7\"\n" + + ENV["BUNDLE_APP_CONFIG"] = "elsewhere" + + assert_equal "7", Gem::BundlerSettings["cooldown"] + end + + def test_bundle_user_home_joins_the_config_file_name + dir = File.join @tempdir, "user_home" + FileUtils.mkdir_p dir + File.write File.join(dir, "config"), "BUNDLE_COOLDOWN: \"7\"\n" + + ENV["BUNDLE_USER_HOME"] = dir + + assert_equal "7", Gem::BundlerSettings["cooldown"] + end + + def test_falls_back_to_the_dot_bundle_directory_in_the_home_directory + dir = File.join @userhome, ".bundle" + FileUtils.mkdir_p dir + File.write File.join(dir, "config"), "BUNDLE_COOLDOWN: \"7\"\n" + + assert_equal "7", Gem::BundlerSettings["cooldown"] + end + + def test_bundle_ignore_config_drops_the_config_files + write_app_config "BUNDLE_COOLDOWN: \"3\"\n" + write_global_config "BUNDLE_COOLDOWN: \"7\"\n" + ENV["BUNDLE_IGNORE_CONFIG"] = "1" + + assert_nil Gem::BundlerSettings["cooldown"] + + ENV["BUNDLE_COOLDOWN"] = "5" + + assert_equal "5", Gem::BundlerSettings["cooldown"] + end + + def test_unset_key_is_nil + write_global_config "BUNDLE_JOBS: \"8\"\n" + + assert_nil Gem::BundlerSettings["cooldown"] + end + + def test_from_config_files_skips_the_environment + ENV["BUNDLE_COOLDOWN"] = "5" + write_global_config "BUNDLE_COOLDOWN: \"7\"\n" + + assert_equal "7", Gem::BundlerSettings.from_config_files("cooldown") + end + + def test_env_reads_only_the_environment + write_global_config "BUNDLE_COOLDOWN: \"7\"\n" + + assert_nil Gem::BundlerSettings.env("cooldown") + + ENV["BUNDLE_COOLDOWN"] = "5" + + assert_equal "5", Gem::BundlerSettings.env("cooldown") + end + + def test_dotted_and_dashed_names_take_the_key_bundler_writes + write_global_config "BUNDLE_GEM__TEST: \"minitest\"\n" + + assert_equal "minitest", Gem::BundlerSettings["gem.test"] + end + + def test_unreadable_config_file_is_ignored + ENV["BUNDLE_CONFIG"] = File.join @tempdir, "no", "such", "config" + + assert_nil Gem::BundlerSettings["cooldown"] + end + + def test_config_file_without_a_mapping_is_ignored + ["- one\n- two\n", "BUNDLE_COOLDOWN:7\n"].each do |contents| + write_global_config contents + + assert_nil Gem::BundlerSettings["cooldown"], contents + end + end + + def test_unparsable_config_file_is_ignored + ["BUNDLE_COOLDOWN: !ruby/object:Foo {}\n", "BUNDLE_COOLDOWN: *nowhere\n"].each do |contents| + write_global_config contents + + assert_nil Gem::BundlerSettings["cooldown"], contents + end + end + + def test_gemfile_path_from_the_environment + assert_equal @gemfile, Gem::BundlerSettings.gemfile_path + end + + def test_gemfile_path_found_by_walking_up + ENV["BUNDLE_GEMFILE"] = nil + nested = File.join @tempdir, "a", "b" + FileUtils.mkdir_p nested + + Dir.chdir nested do + assert_equal @gemfile, Gem::BundlerSettings.gemfile_path + end + end + + def test_gemfile_path_without_a_gemfile + ENV["BUNDLE_GEMFILE"] = nil + File.delete @gemfile + + Dir.chdir @tempdir do + assert_nil Gem::BundlerSettings.gemfile_path + end + end + + def write_app_config(contents) + dir = File.join @tempdir, ".bundle" + FileUtils.mkdir_p dir + File.write File.join(dir, "config"), contents + end + + def write_global_config(contents) + path = File.join @tempdir, "global_config" + File.write path, contents + ENV["BUNDLE_CONFIG"] = path + end +end diff --git a/test/rubygems/test_gem_bundler_version_finder.rb b/test/rubygems/test_gem_bundler_version_finder.rb index b5ef6293abfd..4ff402db7de4 100644 --- a/test/rubygems/test_gem_bundler_version_finder.rb +++ b/test/rubygems/test_gem_bundler_version_finder.rb @@ -2,7 +2,6 @@ require_relative "helper" require "rubygems/bundler_version_finder" -require "tempfile" class TestGemBundlerVersionFinder < Gem::TestCase def setup @@ -24,6 +23,26 @@ def bvf Gem::BundlerVersionFinder end + # Writes +contents+ to the user config file Bundler would read, the same way + # a user pointing BUNDLE_CONFIG at one does. + def with_global_config(contents) + path = File.join @tempdir, "global_bundle_config" + File.write path, contents + ENV["BUNDLE_CONFIG"] = path + + yield + end + + # Writes +contents+ to the application config file Bundler would read for + # the Gemfile the finder locates. + def with_local_config(contents) + dir = File.join @tempdir, ".bundle" + FileUtils.mkdir_p dir + File.write File.join(dir, "config"), contents + + yield + end + def test_bundler_version_defaults_to_nil assert_nil bvf.bundler_version end @@ -62,13 +81,8 @@ def test_bundler_version_with_bundle_config BUNDLE_VERSION: "system" CONFIG - Tempfile.create("bundle_config") do |f| - f.write(config_content) - f.flush - - bvf.stub(:bundler_global_config_file, f.path) do - assert_nil bvf.bundler_version - end + with_global_config(config_content) do + assert_nil bvf.bundler_version end end @@ -77,13 +91,8 @@ def test_bundler_version_with_bundle_config_single_quoted BUNDLE_VERSION: 'system' CONFIG - Tempfile.create("bundle_config") do |f| - f.write(config_with_single_quoted_version) - f.flush - - bvf.stub(:bundler_global_config_file, f.path) do - assert_nil bvf.bundler_version - end + with_global_config(config_with_single_quoted_version) do + assert_nil bvf.bundler_version end end @@ -94,13 +103,8 @@ def test_bundler_version_with_bundle_config_version BUNDLE_VERSION: "1.2.3" CONFIG - Tempfile.create("bundle_config") do |f| - f.write(config_content) - f.flush - - bvf.stub(:bundler_global_config_file, f.path) do - assert_equal v("1.1.1.1"), bvf.bundler_version - end + with_global_config(config_content) do + assert_equal v("1.1.1.1"), bvf.bundler_version end end @@ -119,13 +123,8 @@ def test_bundler_version_with_bundle_version_env_overrides_config BUNDLE_VERSION: "1.2.3" CONFIG - Tempfile.create("bundle_config") do |f| - f.write(config_content) - f.flush - - bvf.stub(:bundler_global_config_file, f.path) do - assert_equal v("2.3.4"), bvf.bundler_version - end + with_global_config(config_content) do + assert_equal v("2.3.4"), bvf.bundler_version end end @@ -136,13 +135,8 @@ def test_bundler_version_with_empty_bundle_version_env BUNDLE_VERSION: "1.2.3" CONFIG - Tempfile.create("bundle_config") do |f| - f.write(config_content) - f.flush - - bvf.stub(:bundler_global_config_file, f.path) do - assert_equal v("1.2.3"), bvf.bundler_version - end + with_global_config(config_content) do + assert_equal v("1.2.3"), bvf.bundler_version end end @@ -159,22 +153,17 @@ def test_bundler_version_with_bundle_config_version_lockfile BUNDLE_VERSION: "lockfile" CONFIG - Tempfile.create("bundle_config") do |f| - f.write(config_content) - f.flush - - bvf.stub(:bundler_global_config_file, f.path) do - bvf.stub(:lockfile_contents, "\n\nBUNDLED WITH\n 1.1.1.1\n") do - assert_equal v("1.1.1.1"), bvf.bundler_version - end + with_global_config(config_content) do + bvf.stub(:lockfile_contents, "\n\nBUNDLED WITH\n 1.1.1.1\n") do + assert_equal v("1.1.1.1"), bvf.bundler_version end end end def test_bundler_version_with_bundle_config_non_existent_file - bvf.stub(:bundler_global_config_file, "/non/existent/path") do - assert_nil bvf.bundler_version - end + ENV["BUNDLE_CONFIG"] = "/non/existent/path" + + assert_nil bvf.bundler_version end def test_bundler_version_set_on_local_config @@ -182,13 +171,8 @@ def test_bundler_version_set_on_local_config BUNDLE_VERSION: "1.2.3" CONFIG - Tempfile.create("bundle_config") do |f| - f.write(config_content) - f.flush - - bvf.stub(:bundler_local_config_file, f.path) do - assert_equal v("1.2.3"), bvf.bundler_version - end + with_local_config(config_content) do + assert_equal v("1.2.3"), bvf.bundler_version end end @@ -198,13 +182,8 @@ def test_bundler_version_with_bundle_config_without_version BUNDLE_GEM__TEST: "minitest" CONFIG - Tempfile.create("bundle_config") do |f| - f.write(config_without_version) - f.flush - - bvf.stub(:bundler_global_config_file, f.path) do - assert_nil bvf.bundler_version - end + with_global_config(config_without_version) do + assert_nil bvf.bundler_version end end diff --git a/test/rubygems/test_gem_commands_outdated_command.rb b/test/rubygems/test_gem_commands_outdated_command.rb index 82e2b6c41e00..505d42ad66fd 100644 --- a/test/rubygems/test_gem_commands_outdated_command.rb +++ b/test/rubygems/test_gem_commands_outdated_command.rb @@ -7,9 +7,18 @@ class TestGemCommandsOutdatedCommand < Gem::TestCase def setup super + without_any_upwards_gemfiles + + @orig_cooldown = Gem.configuration.cooldown @cmd = Gem::Commands::OutdatedCommand.new end + def teardown + Gem.configuration.cooldown = @orig_cooldown + + super + end + def test_initialize assert @cmd.handles?(%W[--platform #{Gem::Platform.local}]) end @@ -121,6 +130,48 @@ def test_execute_cooldown_unparsable_created_at_fails_open assert_equal "", @ui.error end + def test_execute_cooldown_from_the_bundler_setting + util_setup_cooldown_repo "foo-0.2" => util_cooldown_time(30), + "foo-0.3" => util_cooldown_time(1) + + ENV["BUNDLE_COOLDOWN"] = "7" + + use_ui @ui do + @cmd.execute + end + + assert_equal "foo (0.1 < 0.2, 0.3 (cooldown 7d))\n", @ui.output + assert_equal "", @ui.error + end + + def test_execute_cooldown_takes_the_longer_of_the_two_settings + util_setup_cooldown_repo "foo-0.2" => util_cooldown_time(30), + "foo-0.3" => util_cooldown_time(1) + + ENV["BUNDLE_COOLDOWN"] = "7" + Gem.configuration.cooldown = 3 + + use_ui @ui do + @cmd.execute + end + + assert_equal "foo (0.1 < 0.2, 0.3 (cooldown 7d))\n", @ui.output + end + + def test_execute_cooldown_option_bypasses_the_bundler_setting + util_setup_cooldown_repo "foo-0.2" => util_cooldown_time(30), + "foo-0.3" => util_cooldown_time(1) + + ENV["BUNDLE_COOLDOWN"] = "7" + @cmd.options[:cooldown] = 0 + + use_ui @ui do + @cmd.execute + end + + assert_equal "foo (0.1 < 0.3)\n", @ui.output + end + def test_cooldown_option @cmd.handle_options %w[--cooldown 7] diff --git a/test/rubygems/test_gem_cooldown.rb b/test/rubygems/test_gem_cooldown.rb index 6950dcecc2c5..b3ad8c40ef7b 100644 --- a/test/rubygems/test_gem_cooldown.rb +++ b/test/rubygems/test_gem_cooldown.rb @@ -4,6 +4,19 @@ require "rubygems/cooldown" class TestGemCooldown < Gem::TestCase + def setup + super + + without_any_upwards_gemfiles + @orig_cooldown = Gem.configuration.cooldown + end + + def teardown + Gem.configuration.cooldown = @orig_cooldown + + super + end + def test_skip_eh now = Time.now cooldown = Gem::Cooldown.new 7, now: now @@ -32,14 +45,86 @@ def test_skip_eh_inactive end def test_from_options - orig_cooldown = Gem.configuration.cooldown Gem.configuration.cooldown = 5 assert_equal 5, Gem::Cooldown.from_options({}).days assert_equal 7, Gem::Cooldown.from_options(cooldown: 7).days refute Gem::Cooldown.from_options(cooldown: 0).active? - ensure - Gem.configuration.cooldown = orig_cooldown + end + + def test_from_options_uses_the_bundler_setting_when_the_gemrc_has_none + ENV["BUNDLE_COOLDOWN"] = "7" + + assert_equal 7, Gem::Cooldown.from_options({}).days + end + + def test_from_options_takes_the_longer_of_the_two_settings + ENV["BUNDLE_COOLDOWN"] = "7" + Gem.configuration.cooldown = 3 + + assert_equal 7, Gem::Cooldown.from_options({}).days + + Gem.configuration.cooldown = 14 + + assert_equal 14, Gem::Cooldown.from_options({}).days + end + + def test_from_options_takes_a_configured_zero_as_a_value_not_as_unset + ENV["BUNDLE_COOLDOWN"] = "7" + Gem.configuration.cooldown = 0 + + assert_equal 7, Gem::Cooldown.from_options({}).days + + ENV["BUNDLE_COOLDOWN"] = "0" + Gem.configuration.cooldown = 7 + + assert_equal 7, Gem::Cooldown.from_options({}).days + end + + def test_from_options_lets_the_command_line_bypass_the_bundler_setting + ENV["BUNDLE_COOLDOWN"] = "7" + Gem.configuration.cooldown = 3 + + refute Gem::Cooldown.from_options(cooldown: 0).active? + assert_equal 1, Gem::Cooldown.from_options(cooldown: 1).days + end + + def test_from_options_reads_the_bundler_config_file + File.write File.join(@tempdir, "Gemfile"), "" + FileUtils.mkdir_p File.join(@tempdir, ".bundle") + File.write File.join(@tempdir, ".bundle", "config"), "BUNDLE_COOLDOWN: \"7\"\n" + + assert_equal 7, Gem::Cooldown.from_options({}).days + end + + def test_from_options_warns_about_an_invalid_bundler_setting + ENV["BUNDLE_COOLDOWN"] = "seven" + Gem.configuration.cooldown = 3 + + cooldown = use_ui(@ui) { Gem::Cooldown.from_options({}) } + + assert_equal 3, cooldown.days + assert_match %q(Invalid cooldown value "seven" in Bundler's configuration), @ui.error + end + + def test_from_options_names_each_invalid_source_separately + ENV["BUNDLE_COOLDOWN"] = "seven" + Gem.configuration.cooldown = "three" + + refute use_ui(@ui) { Gem::Cooldown.from_options({}) }.active? + + assert_match %q(Invalid cooldown value "three" in the gemrc file), @ui.error + assert_match %q(Invalid cooldown value "seven" in Bundler's configuration), @ui.error + end + + def test_from_options_warns_about_an_invalid_gemrc_setting + Gem.configuration.cooldown = "seven" + ENV["BUNDLE_COOLDOWN"] = "3" + + cooldown = use_ui(@ui) { Gem::Cooldown.from_options({}) } + + assert_equal 3, cooldown.days + assert_match %q(Invalid cooldown value "seven" in the gemrc file), @ui.error end def test_invalid_days_warns_once_and_fails_open @@ -49,7 +134,7 @@ def test_invalid_days_warns_once_and_fails_open end assert_equal 1, @ui.error.scan("Invalid cooldown value").size - assert_match 'Invalid cooldown value "abc", so the cooldown is disabled.', @ui.error + assert_match 'Invalid cooldown value "abc" in the cooldown setting, so it is ignored.', @ui.error assert_match "Expected a non-negative integer number of days.", @ui.error end diff --git a/test/rubygems/test_gem_cooldown_settings.rb b/test/rubygems/test_gem_cooldown_settings.rb new file mode 100644 index 000000000000..3fa07d8f69f0 --- /dev/null +++ b/test/rubygems/test_gem_cooldown_settings.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require_relative "helper" +require "rubygems/cooldown_settings" + +class TestGemCooldownSettings < Gem::TestCase + def test_days_reads_non_negative_integers + assert_equal 7, Gem::CooldownSettings.days(7) + assert_equal 7, Gem::CooldownSettings.days("7") + assert_equal 0, Gem::CooldownSettings.days(0) + end + + def test_days_reads_a_leading_zero_as_base_ten + assert_equal 10, Gem::CooldownSettings.days("010") + assert_equal 8, Gem::CooldownSettings.days("08") + assert_nil Gem::CooldownSettings.days("0x10") + end + + def test_days_rejects_everything_else + ["seven", "7days", -1, true, [7], :sym, ""].each do |value| + assert_nil Gem::CooldownSettings.days(value), value.inspect + end + end + + def test_days_of_an_unset_value_is_nil + assert_nil Gem::CooldownSettings.days(nil) + end + + def test_invalid_eh + assert Gem::CooldownSettings.invalid?("seven") + refute Gem::CooldownSettings.invalid?("7") + refute Gem::CooldownSettings.invalid?(0) + refute Gem::CooldownSettings.invalid?(nil) + end + + def test_combine_takes_the_longest_configured_value + assert_equal 7, Gem::CooldownSettings.combine(3, 7) + assert_equal 7, Gem::CooldownSettings.combine(7, 3) + end + + def test_combine_ignores_unset_and_unusable_values + assert_equal 7, Gem::CooldownSettings.combine(nil, 7) + assert_equal 7, Gem::CooldownSettings.combine("seven", 7) + assert_nil Gem::CooldownSettings.combine(nil, nil) + assert_nil Gem::CooldownSettings.combine("seven", nil) + end + + # A configured 0 means "no cooldown here", not "nothing configured here", so + # it must not be read as unset. The longer value still wins; --cooldown 0 is + # what bypasses a cooldown the other tool configures. + def test_combine_treats_a_configured_zero_as_a_value + assert_equal 7, Gem::CooldownSettings.combine(0, 7) + assert_equal 0, Gem::CooldownSettings.combine(0, nil) + assert_equal 0, Gem::CooldownSettings.combine(0, "seven") + end +end diff --git a/tool/quality_check.rb b/tool/quality_check.rb index 5cc44b296191..acb67a627c3b 100644 --- a/tool/quality_check.rb +++ b/tool/quality_check.rb @@ -260,7 +260,7 @@ def check_for_specific_pronouns(filename) end def tracked_files - @tracked_files ||= git_ls_files("exe/bundle exe/bundler lib/bundler lib/bundler.rb lib/rubygems/vendor/uri lib/rubygems/vendor/securerandom lib/rubygems/vendor/pub_grub lib/rubygems/yaml_serializer.rb lib/rubygems/compact_index_client* lib/rubygems/credential_store* bundler.gemspec CHANGELOG-bundler.md MIT.txt") + @tracked_files ||= git_ls_files("exe/bundle exe/bundler lib/bundler lib/bundler.rb lib/rubygems/vendor/uri lib/rubygems/vendor/securerandom lib/rubygems/vendor/pub_grub lib/rubygems/yaml_serializer.rb lib/rubygems/cooldown_settings.rb lib/rubygems/compact_index_client* lib/rubygems/credential_store* bundler.gemspec CHANGELOG-bundler.md MIT.txt") end def lib_tracked_files