diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..d31906b2f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,5 @@ +# This file was generated on 2019-12-05T21:32:24+00:00 from the rspec-dev repo. +# DO NOT modify it by hand as your changes will get lost the next time it is generated. + +github: [JonRowe] +open_collective: rspec diff --git a/.rubocop.yml b/.rubocop.yml index e51d1ea46..e6d4f3a13 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -23,3 +23,11 @@ Lint/AssignmentInCondition: Exclude: # The pattern makes sense here - 'lib/rspec/support/mutex.rb' + +# Over time we'd like to get this down, but this is what we're at now. +Metrics/AbcSize: + Max: 22 + +# Over time we'd like to get this down, but this is what we're at now. +Metrics/PerceivedComplexity: + Max: 9 diff --git a/.rubocop_rspec_base.yml b/.rubocop_rspec_base.yml index db7028464..27144799e 100644 --- a/.rubocop_rspec_base.yml +++ b/.rubocop_rspec_base.yml @@ -1,4 +1,4 @@ -# This file was generated on 2018-04-05T18:41:16+10:00 from the rspec-dev repo. +# This file was generated on 2020-04-03T18:53:24+03:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. # This file contains defaults for RSpec projects. Individual projects diff --git a/.travis.yml b/.travis.yml index ebb749925..3a33c6c15 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,15 @@ -# This file was generated on 2018-04-05T18:41:16+10:00 from the rspec-dev repo. +# This file was generated on 2020-04-03T18:53:24+03:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. +# In order to install old Rubies, we need to use old Ubuntu distibution. +dist: trusty language: ruby -sudo: false email: false cache: directories: - ../bundle before_install: - - gem update --system # https://github.com/travis-ci/travis-ci/issues/8978#issuecomment-354036443 - - gem install bundler + - "script/update_rubygems_and_install_bundler" - unset _JAVA_OPTIONS - "script/clone_all_rspec_repos" bundler_args: "--binstubs --standalone --without documentation --path ../bundle" @@ -21,9 +21,11 @@ rvm: - 2.0.0 - 2.1 - 2.2.10 - - 2.3.7 - - 2.4.4 - - 2.5.1 + - 2.3.8 + - 2.4.10 + - 2.5.8 + - 2.6.6 + - 2.7.1 - ruby-head - ree - rbx-3 @@ -36,11 +38,14 @@ matrix: include: - rvm: jruby-1.7 env: JRUBY_OPTS='--dev --1.8' + - rvm: 2.7.1 + env: DIFF_LCS_VERSION="~> 1.3.0" + - rvm: 2.7.1 + env: DIFF_LCS_VERSION="1.4.3" allow_failures: - rvm: jruby-head - rvm: ruby-head - rvm: rbx-3 - - rvm: ruby-2.6.0-preview1 fast_finish: true branches: only: diff --git a/BUILD_DETAIL.md b/BUILD_DETAIL.md index 37ebb3a28..cdf1c0566 100644 --- a/BUILD_DETAIL.md +++ b/BUILD_DETAIL.md @@ -1,5 +1,5 @@ @@ -40,7 +40,7 @@ The spec suite performs a couple extra checks that are worth noting: available for use in any context, we want to minimize how many bits of the standard library we load and use. Otherwise, RSpec's use of part of the standard library could mask a problem where a gem author forgets to load a part of the standard library they - rely on. The spec suite contains a spec that defines a whitelist of allowed loaded + rely on. The spec suite contains a spec that defines a list of allowed loaded stdlibs. In addition, we use [SimpleCov](https://github.com/colszowka/simplecov) @@ -68,8 +68,8 @@ $ bin/cucumber ## YARD documentation -RSpec uses [YARD](http://yardoc.org/) for API documentation on the [rspec.info site](http://rspec.info/). -Our commitment to [SemVer](htp://semver.org) requires that we explicitly +RSpec uses [YARD](https://yardoc.org/) for API documentation on the [rspec.info site](https://rspec.info/). +Our commitment to [SemVer](https://semver.org) requires that we explicitly declare our public API, and our build uses YARD to ensure that every class, module and method has either been labeled `@private` or has at least some level of documentation. For new APIs, this forces us to make diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 9b73017c4..e36c3b110 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,5 +1,5 @@ @@ -46,14 +46,14 @@ when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting one of the project maintainers listed at -http://rspec.info/about/. All complaints will be reviewed and investigated +https://rspec.info/about/. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.3.0, available at -[http://contributor-covenant.org/version/1/3/0/][version] +[https://contributor-covenant.org/version/1/3/0/][version] -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/3/0/ +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/3/0/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dbb9e7560..0b073f037 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,5 @@ @@ -13,8 +13,8 @@ If you'd like to help make RSpec better, here are some ways you can contribute: - by running RSpec HEAD to help us catch bugs before new releases - by [reporting bugs you encounter](https://github.com/rspec/rspec-support/issues/new) with [report template](#report-template) - by [suggesting new features](https://github.com/rspec/rspec-support/issues/new) - - by improving RSpec's [Relish](https://relishapp.com/rspec) or [API](http://rspec.info/documentation/) documentation - - by improving [RSpec's website](http://rspec.info/) ([source](https://github.com/rspec/rspec.github.io)) + - by improving RSpec's [Relish](https://relishapp.com/rspec) or [API](https://rspec.info/documentation/) documentation + - by improving [RSpec's website](https://rspec.info/) ([source](https://github.com/rspec/rspec.github.io)) - by taking part in [feature and issue discussions](https://github.com/rspec/rspec-support/issues) - by adding a failing test for reproducible [reported bugs](https://github.com/rspec/rspec-support/issues) - by reviewing [pull requests](https://github.com/rspec/rspec-support/pulls) and suggesting improvements diff --git a/Changelog.md b/Changelog.md index 945930415..094197b34 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,63 @@ +### 3.9.3 / 2020-05-02 +[Full Changelog](http://github.com/rspec/rspec-support/compare/v3.9.2...v3.9.3) + +Bug Fixes: + +* Mark ripper as unsupported on Truffle Ruby. (Brandon Fish, #395) +* Mark ripper as unsupported on JRuby 9.2.0.0. (Brian Hawley, #400) +* Capture `Mutex.new` for our `RSpec::Support:Mutex` in order to + allow stubbing `Mutex.new`. (Jon Rowe, #411) + +### 3.9.2 / 2019-12-30 +[Full Changelog](http://github.com/rspec/rspec-support/compare/v3.9.1...v3.9.2) + +Bug Fixes: + +* Remove unneeded eval. (Matijs van Zuijlen, #394) + +### 3.9.1 / 2019-12-28 +[Full Changelog](http://github.com/rspec/rspec-support/compare/v3.9.0...v3.9.1) + +Bug Fixes: + +* Remove warning caused by keyword arguments on Ruby 2.7.0. + (Jon Rowe, #392) + +### 3.9.0 / 2019-10-07 +[Full Changelog](http://github.com/rspec/rspec-support/compare/v3.8.3...v3.9.0) + +*NO CHANGES* + +Version 3.9.0 was released to allow other RSpec gems to release 3.9.0. + +### 3.8.3 / 2019-10-02 +[Full Changelog](http://github.com/rspec/rspec-support/compare/v3.8.2...v3.8.3) + +Bug Fixes: + +* Escape \r when outputting strings inside arrays. + (Tomita Masahiro, Jon Rowe, #378) +* Ensure that optional hash arguments are recognised correctly vs keyword + arguments. (Evgeni Dzhelyov, #366) + +### 3.8.2 / 2019-06-10 +[Full Changelog](http://github.com/rspec/rspec-support/compare/v3.8.1...v3.8.2) + +Bug Fixes: + +* Ensure that an empty hash is recognised as empty keyword arguments when + applicable. (Thomas Walpole, #375) +* Ensure that diffing truthy values produce diffs consistently. + (Lucas Nestor, #377) + +### 3.8.1 / 2019-03-03 +[Full Changelog](http://github.com/rspec/rspec-support/compare/v3.8.0...v3.8.1) + +Bug Fixes: + +* Ensure that inspecting a `SimpleDelegator` based object works regardless of + visibilty of the `__getobj__` method. (Jon Rowe, #369) + ### 3.8.0 / 2018-08-04 [Full Changelog](http://github.com/rspec/rspec-support/compare/v3.7.1...v3.8.0) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index a31c1c796..d9a596d22 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,12 +1,12 @@ # Development Setup Generally speaking, you only need to clone the project and install -the dependencies with [Bundler](http://bundler.io/). You can either +the dependencies with [Bundler](https://bundler.io/). You can either get a full RSpec development environment using [rspec-dev](https://github.com/rspec/rspec-dev#README) or you can set this project up individually. @@ -22,7 +22,7 @@ Clone the repo: $ git clone git@github.com:rspec/rspec-support.git ``` -Install the dependencies using [Bundler](http://bundler.io/): +Install the dependencies using [Bundler](https://bundler.io/): ``` $ cd rspec-support @@ -31,7 +31,7 @@ $ bundle install To minimize boot time and to ensure we don't depend upon any extra dependencies loaded by Bundler, our CI builds avoid loading Bundler at runtime -by using Bundler's [`--standalone option`](http://myronmars.to/n/dev-blog/2012/03/faster-test-boot-times-with-bundler-standalone). +by using Bundler's [`--standalone option`](https://myronmars.to/n/dev-blog/2012/03/faster-test-boot-times-with-bundler-standalone). While not strictly necessary (many/most of our contributors do not do this!), if you want to exactly reproduce our CI builds you'll want to do the same: @@ -114,7 +114,7 @@ Here's a short, non-exhaustive checklist of things we typically ask contributors # Adding Docs -RSpec uses [YARD](http://yardoc.org/) for its API documentation. To +RSpec uses [YARD](https://yardoc.org/) for its API documentation. To ensure the docs render well, we recommend running a YARD server and viewing your edits in a browser. diff --git a/Gemfile b/Gemfile index 2cf43169f..198a555ce 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,28 @@ branch = File.read(File.expand_path("../maintenance-branch", __FILE__)).chomp end end +if RUBY_VERSION < '1.9.3' + gem 'rake', '< 11.0.0' # rake 11 requires Ruby 1.9.3 or later +elsif RUBY_VERSION < '2.0.0' + gem 'rake', '< 12.0.0' # rake 12 requires Ruby 2.0.0 or later +else + gem 'rake', '>= 12.3.3' +end + +if ENV['DIFF_LCS_VERSION'] + gem 'diff-lcs', ENV['DIFF_LCS_VERSION'] +else + gem 'diff-lcs', '~> 1.4', '>= 1.4.3' +end + +if RUBY_VERSION < '2.2.0' && !!(RbConfig::CONFIG['host_os'] =~ /cygwin|mswin|mingw|bccwin|wince|emx/) + gem "childprocess", "< 1.0.0" +elsif RUBY_VERSION < '2.3.0' + gem "childprocess", "< 3.0.0" +else + gem "childprocess", ">= 3.0.0" +end + ### dep for ci/coverage gem 'simplecov', '~> 0.8' @@ -20,10 +42,13 @@ if RUBY_VERSION < '2.0.0' || RUBY_ENGINE == 'java' gem 'json', '< 2.0.0' # is a dependency of simplecov end -if RUBY_VERSION < '2.0.0' && !!(RbConfig::CONFIG['host_os'] =~ /cygwin|mswin|mingw|bccwin|wince|emx/) - gem 'ffi', '< 1.9.15' # allow ffi to be installed on older rubies on windows -elsif RUBY_VERSION < '1.9' - gem 'ffi', '< 1.9.19' # ffi dropped Ruby 1.8 support in 1.9.19 +if RUBY_VERSION < '2.2.0' && !!(RbConfig::CONFIG['host_os'] =~ /cygwin|mswin|mingw|bccwin|wince|emx/) + gem 'ffi', '< 1.10' +elsif RUBY_VERSION < '2.0' + # ffi dropped Ruby 1.8 support in 1.9.19 and Ruby 1.9 support in 1.11.0 + gem 'ffi', '< 1.9.19' +else + gem 'ffi', '~> 1.11.0' end # No need to run rubocop on earlier versions diff --git a/README.md b/README.md index 7bdde9c97..4a066618e 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,3 @@ # RSpec::Support -`RSpec::Support` provides common functionality to `RSpec::Core`, -`RSpec::Expectations` and `RSpec::Mocks`. It is considered -suitable for internal use only at this time. - -## Installation / Usage - -Install one or more of the `RSpec` gems. - -Want to run against the `master` branch? You'll need to include the dependent -RSpec repos as well. Add the following to your `Gemfile`: - -```ruby -%w[rspec-core rspec-expectations rspec-mocks rspec-support].each do |lib| - gem lib, :git => "https://github.com/rspec/#{lib}.git", :branch => 'master' -end -``` - -## Contributing - -Once you've set up the environment, you'll need to cd into the working -directory of whichever repo you want to work in. From there you can run the -specs and cucumber features, and make patches. - -NOTE: You do not need to use rspec-dev to work on a specific RSpec repo. You -can treat each RSpec repo as an independent project. - -- [Build details](BUILD_DETAIL.md) -- [Code of Conduct](CODE_OF_CONDUCT.md) -- [Detailed contributing guide](CONTRIBUTING.md) -- [Development setup guide](DEVELOPMENT.md) - -## Patches - -Please submit a pull request or a github issue. If you submit an issue, please -include a link to either of: - -* a gist (or equivalent) of the patch -* a branch or commit in your github fork of the repo +This branch is deprecated, please use main`. diff --git a/REPORT_TEMPLATE.md b/REPORT_TEMPLATE.md index eff8136ce..d07c0d46c 100644 --- a/REPORT_TEMPLATE.md +++ b/REPORT_TEMPLATE.md @@ -1,5 +1,5 @@ diff --git a/Rakefile b/Rakefile index 6756730f1..b240b2fb3 100644 --- a/Rakefile +++ b/Rakefile @@ -17,14 +17,18 @@ task :default => [:spec] task :verify_private_key_present do private_key = File.expand_path('~/.gem/rspec-gem-private_key.pem') unless File.exist?(private_key) - raise "Your private key is not present. This gem should not be built without that." + raise "Your private key is not present. This gem should not be built without it." end end task :build => :verify_private_key_present -require 'rubocop/rake_task' -desc 'Run RuboCop on the lib directory' -RuboCop::RakeTask.new(:rubocop) do |task| - task.patterns = ['lib/**/*.rb'] +begin + require 'rubocop/rake_task' + desc 'Run RuboCop on the lib directory' + RuboCop::RakeTask.new(:rubocop) do |task| + task.patterns = ['lib/**/*.rb'] + end +rescue LoadError + # No rubocop means no rubocop rake task end diff --git a/appveyor.yml b/appveyor.yml index 469175b41..6fc48f7db 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -# This file was generated on 2018-04-05T18:41:16+10:00 from the rspec-dev repo. +# This file was generated on 2020-04-03T18:53:24+03:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. version: "{build}" @@ -33,9 +33,10 @@ test_script: environment: matrix: - - ruby_version: 193 - ruby_version: 200 - ruby_version: 21 - ruby_version: 22 - ruby_version: 23-x64 - ruby_version: 24-x64 + - ruby_version: 25-x64 + - ruby_version: 26-x64 diff --git a/benchmarks/class_exec_vs_klass_exec.rb b/benchmarks/class_exec_vs_klass_exec.rb new file mode 100644 index 000000000..9cb594d28 --- /dev/null +++ b/benchmarks/class_exec_vs_klass_exec.rb @@ -0,0 +1,43 @@ +require 'benchmark/ips' +require 'rspec/support' +require 'rspec/support/with_keywords_when_needed' + +Klass = Class.new do + def test(*args, **kwargs) + end +end + +def class_exec_args + Klass.class_exec(:a, :b) { } +end + +def klass_exec_args + RSpec::Support::WithKeywordsWhenNeeded.class_exec(Klass, :a, :b) { } +end + +def class_exec_kw_args + Klass.class_exec(a: :b) { |a:| } +end + +def klass_exec_kw_args + RSpec::Support::WithKeywordsWhenNeeded.class_exec(Klass, a: :b) { |a:| } +end + +Benchmark.ips do |x| + x.report("class_exec(*args) ") { class_exec_args } + x.report("klass_exec(*args) ") { klass_exec_args } + x.report("class_exec(*args, **kwargs)") { class_exec_kw_args } + x.report("klass_exec(*args, **kwargs)") { klass_exec_kw_args } +end + +__END__ + +Calculating ------------------------------------- +class_exec(*args) + 5.555M (± 1.6%) i/s - 27.864M in 5.017682s +klass_exec(*args) + 657.945k (± 4.6%) i/s - 3.315M in 5.051511s +class_exec(*args, **kwargs) + 2.882M (± 3.3%) i/s - 14.555M in 5.056905s +klass_exec(*args, **kwargs) + 52.710k (± 4.1%) i/s - 265.188k in 5.041218s diff --git a/lib/rspec/support.rb b/lib/rspec/support.rb index 549a53bc8..6a66eb180 100644 --- a/lib/rspec/support.rb +++ b/lib/rspec/support.rb @@ -1,3 +1,5 @@ +warn "This branch is deprecated. Please use `main`." + module RSpec module Support # @api private @@ -139,7 +141,7 @@ def self.===(exception) end end - # The Differ is only needed when a a spec fails with a diffable failure. + # The Differ is only needed when a spec fails with a diffable failure. # In the more common case of all specs passing or the only failures being # non-diffable, we can avoid the extra cost of loading the differ, diff-lcs, # pp, etc by avoiding an unnecessary require. Instead, autoload will take diff --git a/lib/rspec/support/caller_filter.rb b/lib/rspec/support/caller_filter.rb index eb6f4dca9..cd59a30f8 100644 --- a/lib/rspec/support/caller_filter.rb +++ b/lib/rspec/support/caller_filter.rb @@ -69,7 +69,7 @@ def self.first_non_rspec_line(skip_frames=3, increment=5) return line.to_s if line skip_frames += increment - increment *= 2 # The choice of two here is arbitrary. + increment *= 2 # The choice of two here is arbitrary. end end else diff --git a/lib/rspec/support/comparable_version.rb b/lib/rspec/support/comparable_version.rb index d17b713f9..2afa0a4f2 100644 --- a/lib/rspec/support/comparable_version.rb +++ b/lib/rspec/support/comparable_version.rb @@ -10,7 +10,7 @@ def initialize(string) @string = string end - def <=>(other) + def <=>(other) # rubocop:disable Metrics/AbcSize other = self.class.new(other) unless other.is_a?(self.class) return 0 if string == other.string diff --git a/lib/rspec/support/differ.rb b/lib/rspec/support/differ.rb index 442c892ce..b81184239 100644 --- a/lib/rspec/support/differ.rb +++ b/lib/rspec/support/differ.rb @@ -11,7 +11,7 @@ class Differ def diff(actual, expected) diff = "" - if actual && expected + unless actual.nil? || expected.nil? if all_strings?(actual, expected) if any_multiline_strings?(actual, expected) diff = diff_as_string(coerce_to_string(actual), coerce_to_string(expected)) @@ -97,7 +97,7 @@ def diffably_stringify(array) if Array === entry entry.inspect else - entry.to_s.gsub("\n", "\\n") + entry.to_s.gsub("\n", "\\n").gsub("\r", "\\r") end end end diff --git a/lib/rspec/support/encoded_string.rb b/lib/rspec/support/encoded_string.rb index a6ca0cb64..13c323503 100644 --- a/lib/rspec/support/encoded_string.rb +++ b/lib/rspec/support/encoded_string.rb @@ -5,31 +5,11 @@ class EncodedString # Reduce allocations by storing constants. UTF_8 = "UTF-8" US_ASCII = "US-ASCII" - # - # In MRI 2.1 'invalid: :replace' changed to also replace an invalid byte sequence - # see https://github.com/ruby/ruby/blob/v2_1_0/NEWS#L176 - # https://www.ruby-forum.com/topic/6861247 - # https://twitter.com/nalsh/status/553413844685438976 - # - # For example, given: - # "\x80".force_encoding("Emacs-Mule").encode(:invalid => :replace).bytes.to_a - # - # On MRI 2.1 or above: 63 # '?' - # else : 128 # "\x80" - # + # Ruby's default replacement string is: # U+FFFD ("\xEF\xBF\xBD"), for Unicode encoding forms, else # ? ("\x3F") REPLACE = "?" - ENCODE_UNCONVERTABLE_BYTES = { - :invalid => :replace, - :undef => :replace, - :replace => REPLACE - } - ENCODE_NO_CONVERTER = { - :invalid => :replace, - :replace => REPLACE - } def initialize(string, encoding=nil) @encoding = encoding @@ -112,9 +92,25 @@ def matching_encoding(string) string = remove_invalid_bytes(string) string.encode(@encoding) rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError - string.encode(@encoding, ENCODE_UNCONVERTABLE_BYTES) + # Originally defined as a constant to avoid uneeded allocations, this hash must + # be defined inline (without {}) to avoid warnings on Ruby 2.7 + # + # In MRI 2.1 'invalid: :replace' changed to also replace an invalid byte sequence + # see https://github.com/ruby/ruby/blob/v2_1_0/NEWS#L176 + # https://www.ruby-forum.com/topic/6861247 + # https://twitter.com/nalsh/status/553413844685438976 + # + # For example, given: + # "\x80".force_encoding("Emacs-Mule").encode(:invalid => :replace).bytes.to_a + # + # On MRI 2.1 or above: 63 # '?' + # else : 128 # "\x80" + # + string.encode(@encoding, :invalid => :replace, :undef => :replace, :replace => REPLACE) rescue Encoding::ConverterNotFoundError - string.dup.force_encoding(@encoding).encode(ENCODE_NO_CONVERTER) + # Originally defined as a constant to avoid uneeded allocations, this hash must + # be defined inline (without {}) to avoid warnings on Ruby 2.7 + string.dup.force_encoding(@encoding).encode(:invalid => :replace, :replace => REPLACE) end # Prevents raising ArgumentError diff --git a/lib/rspec/support/method_signature_verifier.rb b/lib/rspec/support/method_signature_verifier.rb index ceffceffa..16736224e 100644 --- a/lib/rspec/support/method_signature_verifier.rb +++ b/lib/rspec/support/method_signature_verifier.rb @@ -77,14 +77,19 @@ def invalid_kw_args_from(given_kw_args) given_kw_args - @allowed_kw_args end + # If the last argument is Hash, Ruby will treat only symbol keys as keyword arguments + # the rest will be grouped in another Hash and passed as positional argument. def has_kw_args_in?(args) - Hash === args.last && could_contain_kw_args?(args) + Hash === args.last && + could_contain_kw_args?(args) && + (args.last.empty? || args.last.keys.any? { |x| x.is_a?(Symbol) }) end # Without considering what the last arg is, could it # contain keyword arguments? def could_contain_kw_args?(args) return false if args.count <= min_non_kw_args + @allows_any_kw_args || @allowed_kw_args.any? end @@ -117,7 +122,7 @@ def classify_parameters end end - @max_non_kw_args = @min_non_kw_args + optional_non_kw_args + @max_non_kw_args = @min_non_kw_args + optional_non_kw_args @allowed_kw_args = @required_kw_args + @optional_kw_args end else @@ -280,7 +285,7 @@ def initialize(signature, args=[]) @arbitrary_kw_args = @unlimited_args = false end - def with_expectation(expectation) # rubocop:disable MethodLength + def with_expectation(expectation) # rubocop:disable MethodLength, Metrics/PerceivedComplexity return self unless MethodSignatureExpectation === expectation if expectation.empty? @@ -357,7 +362,14 @@ def unlimited_args? def split_args(*args) kw_args = if @signature.has_kw_args_in?(args) - args.pop.keys + last = args.pop + non_kw_args = last.reject { |k, _| k.is_a?(Symbol) } + if non_kw_args.empty? + last.keys + else + args << non_kw_args + last.select { |k, _| k.is_a?(Symbol) }.keys + end else [] end diff --git a/lib/rspec/support/object_formatter.rb b/lib/rspec/support/object_formatter.rb index 834309a2c..2798a57b7 100644 --- a/lib/rspec/support/object_formatter.rb +++ b/lib/rspec/support/object_formatter.rb @@ -225,7 +225,7 @@ def self.can_inspect?(object) end def inspect - "#<#{object.class}(#{formatter.format(object.__getobj__)})>" + "#<#{object.class}(#{formatter.format(object.send(:__getobj__))})>" end end diff --git a/lib/rspec/support/reentrant_mutex.rb b/lib/rspec/support/reentrant_mutex.rb index 13ee22bd5..361135942 100644 --- a/lib/rspec/support/reentrant_mutex.rb +++ b/lib/rspec/support/reentrant_mutex.rb @@ -43,7 +43,15 @@ def exit if defined? ::Mutex # On 1.9 and up, this is in core, so we just use the real one - Mutex = ::Mutex + class Mutex < ::Mutex + # If you mock Mutex.new you break our usage of Mutex, so + # instead we capture the original method to return Mutexs. + NEW_MUTEX_METHOD = Mutex.method(:new) + + def self.new + NEW_MUTEX_METHOD.call + end + end else # For 1.8.7 # :nocov: RSpec::Support.require_rspec_support "mutex" diff --git a/lib/rspec/support/ruby_features.rb b/lib/rspec/support/ruby_features.rb index 52aa37ec5..6f784e36d 100644 --- a/lib/rspec/support/ruby_features.rb +++ b/lib/rspec/support/ruby_features.rb @@ -7,7 +7,7 @@ module Support # # Provides query methods for different OS or OS features. module OS - module_function + module_function def windows? !!(RbConfig::CONFIG['host_os'] =~ /cygwin|mswin|mingw|bccwin|wince|emx/) @@ -22,7 +22,7 @@ def windows_file_path? # # Provides query methods for different rubies module Ruby - module_function + module_function def jruby? RUBY_PLATFORM == 'java' @@ -47,6 +47,10 @@ def non_mri? def mri? !defined?(RUBY_ENGINE) || RUBY_ENGINE == 'ruby' end + + def truffleruby? + defined?(RUBY_ENGINE) && RUBY_ENGINE == 'truffleruby' + end end # @api private @@ -54,7 +58,7 @@ def mri? # Provides query methods for ruby features that differ among # implementations. module RubyFeatures - module_function + module_function if Ruby.jruby? # On JRuby 1.7 `--1.8` mode, `Process.respond_to?(:fork)` returns true, @@ -90,15 +94,25 @@ def supports_exception_cause? end end + if RUBY_VERSION.to_f >= 2.7 + def supports_taint? + false + end + else + def supports_taint? + true + end + end ripper_requirements = [ComparableVersion.new(RUBY_VERSION) >= '1.9.2'] - ripper_requirements.push(false) if Ruby.rbx? + ripper_requirements.push(false) if Ruby.rbx? || Ruby.truffleruby? if Ruby.jruby? ripper_requirements.push(Ruby.jruby_version >= '1.7.5') # Ripper on JRuby 9.0.0.0.rc1 - 9.1.8.0 reports wrong line number # or cannot parse source including `:if`. - ripper_requirements.push(!Ruby.jruby_version.between?('9.0.0.0.rc1', '9.1.8.0')) + # Ripper on JRuby 9.x.x.x < 9.2.1.0 can't handle keyword arguments. + ripper_requirements.push(!Ruby.jruby_version.between?('9.0.0.0.rc1', '9.2.0.0')) end if ripper_requirements.all? diff --git a/lib/rspec/support/spec.rb b/lib/rspec/support/spec.rb index 6db588b2a..bd998903e 100644 --- a/lib/rspec/support/spec.rb +++ b/lib/rspec/support/spec.rb @@ -2,6 +2,7 @@ require 'rspec/support/spec/in_sub_process' RSpec::Support.require_rspec_support "spec/deprecation_helpers" +RSpec::Support.require_rspec_support "spec/diff_helpers" RSpec::Support.require_rspec_support "spec/with_isolated_stderr" RSpec::Support.require_rspec_support "spec/stderr_splitter" RSpec::Support.require_rspec_support "spec/formatting_support" diff --git a/lib/rspec/support/spec/diff_helpers.rb b/lib/rspec/support/spec/diff_helpers.rb new file mode 100644 index 000000000..76ef10a67 --- /dev/null +++ b/lib/rspec/support/spec/diff_helpers.rb @@ -0,0 +1,31 @@ +require 'diff/lcs' + +module RSpec + module Support + module Spec + module DiffHelpers + # In the updated version of diff-lcs several diff headers change format slightly + # compensate for this and change minimum version in RSpec 4 + if ::Diff::LCS::VERSION.to_f < 1.4 + def one_line_header(line_number=2) + "-1,#{line_number} +1,#{line_number}" + end + else + def one_line_header(_=2) + "-1 +1" + end + end + + if Diff::LCS::VERSION.to_f < 1.4 || Diff::LCS::VERSION >= "1.4.4" + def removing_two_line_header + "-1,3 +1" + end + else + def removing_two_line_header + "-1,3 +1,5" + end + end + end + end + end +end diff --git a/lib/rspec/support/spec/in_sub_process.rb b/lib/rspec/support/spec/in_sub_process.rb index aadb3de55..85196997c 100644 --- a/lib/rspec/support/spec/in_sub_process.rb +++ b/lib/rspec/support/spec/in_sub_process.rb @@ -7,8 +7,7 @@ module InSubProcess # Useful as a way to isolate a global change to a subprocess. - # rubocop:disable MethodLength - def in_sub_process(prevent_warnings=true) + def in_sub_process(prevent_warnings=true) # rubocop:disable MethodLength, Metrics/AbcSize exception_reader, exception_writer = IO.pipe result_reader, result_writer = IO.pipe @@ -46,7 +45,6 @@ def in_sub_process(prevent_warnings=true) result_reader.close result end - # rubocop:enable MethodLength alias :in_sub_process_if_possible :in_sub_process def marshal_dump_with_unmarshable_object_handling(object) diff --git a/lib/rspec/support/spec/library_wide_checks.rb b/lib/rspec/support/spec/library_wide_checks.rb index 56b059364..a70332ba8 100644 --- a/lib/rspec/support/spec/library_wide_checks.rb +++ b/lib/rspec/support/spec/library_wide_checks.rb @@ -68,7 +68,7 @@ def load_all_files(files, preamble, postamble=nil) run_ruby_with_current_load_path(command, *options) end - [stdout, strip_known_warnings(stderr), status.exitstatus] + [stdout, stderr, status.exitstatus] end define_method :load_all_lib_files do diff --git a/lib/rspec/support/spec/shell_out.rb b/lib/rspec/support/spec/shell_out.rb index f8268cdf1..70ed6bcf6 100644 --- a/lib/rspec/support/spec/shell_out.rb +++ b/lib/rspec/support/spec/shell_out.rb @@ -47,22 +47,41 @@ def run_ruby_with_current_load_path(ruby_command, *flags) # Unset these env vars because `ruby -w` will issue warnings whenever # they are set to non-default values. - with_env 'RUBY_GC_HEAP_FREE_SLOTS' => nil, 'RUBY_GC_MALLOC_LIMIT' => nil, - 'RUBY_FREE_MIN' => nil do + stdout, stderr, status = with_env 'RUBY_GC_HEAP_FREE_SLOTS' => nil, + 'RUBY_GC_MALLOC_LIMIT' => nil, + 'RUBY_FREE_MIN' => nil do shell_out(*command) end + [stdout, strip_known_warnings(stderr), status] end - def strip_known_warnings(input) - input.split("\n").reject do |l| + LINES_TO_IGNORE = + [ # Ignore bundler warning. - l =~ %r{bundler/source/rubygems} || + %r{bundler/source/rubygems}, # Ignore bundler + rubygems warning. - l =~ %r{site_ruby/\d\.\d\.\d/rubygems} || + %r{site_ruby/\d\.\d\.\d/rubygems}, + %r{jruby-\d\.\d\.\d\.\d/lib/ruby/stdlib/rubygems}, # This is required for windows for some reason - l =~ %r{lib/bundler/rubygems} || + %r{lib/bundler/rubygems}, # This is a JRuby file that generates warnings on 9.0.3.0 - l =~ %r{lib/ruby/stdlib/jar} + %r{lib/ruby/stdlib/jar}, + # This is a JRuby file that generates warnings on 9.1.7.0 + %r{org/jruby/RubyKernel\.java}, + # This is a JRuby gem that generates warnings on 9.1.7.0 + %r{ffi-1\.13\.\d+-java}, + %r{uninitialized constant FFI}, + # These are related to the above, there is a warning about io from FFI + %r{jruby-\d\.\d\.\d\.\d/lib/ruby/stdlib/io}, + %r{io/console on JRuby shells out to stty for most operations}, + %r{This branch is deprecated\. Please use `main`\.}, + ] + + def strip_known_warnings(input) + input.split("\n").reject do |l| + LINES_TO_IGNORE.any? { |to_ignore| l =~ to_ignore } || + # Remove blank lines + l == "" || l.nil? end.join("\n") end diff --git a/lib/rspec/support/spec/stderr_splitter.rb b/lib/rspec/support/spec/stderr_splitter.rb index f797a2362..03b50e939 100644 --- a/lib/rspec/support/spec/stderr_splitter.rb +++ b/lib/rspec/support/spec/stderr_splitter.rb @@ -6,6 +6,7 @@ class StdErrSplitter def initialize(original) @orig_stderr = original @output_tracker = ::StringIO.new + @last_line = nil end respond_to_name = (::RUBY_VERSION.to_f < 1.9) ? :respond_to? : :respond_to_missing? @@ -38,8 +39,19 @@ def to_io def write(line) return if line =~ %r{^\S+/gems/\S+:\d+: warning:} # http://rubular.com/r/kqeUIZOfPG + # Ruby 2.7.0 warnings from keyword argments span multiple lines, extend check above + # to look for the next line. + return if @last_line =~ %r{^\S+/gems/\S+:\d+: warning:} && + line =~ %r{warning: The called method .* is defined here} + + # Ruby 2.7.0 complains about hashes used in place of keyword arguments + # Aruba 0.14.2 uses this internally triggering that here + return if line =~ %r{lib/ruby/2\.7\.0/fileutils\.rb:622: warning:} + @orig_stderr.write(line) @output_tracker.write(line) + ensure + @last_line = line end def has_output? diff --git a/lib/rspec/support/spec/string_matcher.rb b/lib/rspec/support/spec/string_matcher.rb index 9e1623178..7df319918 100644 --- a/lib/rspec/support/spec/string_matcher.rb +++ b/lib/rspec/support/spec/string_matcher.rb @@ -4,7 +4,6 @@ # which also relies on EncodedString. Instead, confirm the # strings have the same bytes. RSpec::Matchers.define :be_identical_string do |expected| - if String.method_defined?(:encoding) match do expected_encoding? && diff --git a/lib/rspec/support/version.rb b/lib/rspec/support/version.rb index 0b865ee6e..585c205ad 100644 --- a/lib/rspec/support/version.rb +++ b/lib/rspec/support/version.rb @@ -1,7 +1,7 @@ module RSpec module Support module Version - STRING = '3.8.0' + STRING = '3.10.0.pre' end end end diff --git a/lib/rspec/support/with_keywords_when_needed.rb b/lib/rspec/support/with_keywords_when_needed.rb new file mode 100644 index 000000000..56b67e70b --- /dev/null +++ b/lib/rspec/support/with_keywords_when_needed.rb @@ -0,0 +1,33 @@ +RSpec::Support.require_rspec_support("method_signature_verifier") + +module RSpec + module Support + module WithKeywordsWhenNeeded + # This module adds keyword sensitive support for core ruby methods + # where we cannot use `ruby2_keywords` directly. + + module_function + + if RSpec::Support::RubyFeatures.kw_args_supported? + # Remove this in RSpec 4 in favour of explictly passed in kwargs where + # this is used. Works around a warning in Ruby 2.7 + + def class_exec(klass, *args, &block) + if MethodSignature.new(block).has_kw_args_in?(args) + binding.eval(<<-CODE, __FILE__, __LINE__) + kwargs = args.pop + klass.class_exec(*args, **kwargs, &block) + CODE + else + klass.class_exec(*args, &block) + end + end + ruby2_keywords :class_exec if respond_to?(:ruby2_keywords, true) + else + def class_exec(klass, *args, &block) + klass.class_exec(*args, &block) + end + end + end + end +end diff --git a/rspec-support.gemspec b/rspec-support.gemspec index 6699688fa..c32cd57d4 100644 --- a/rspec-support.gemspec +++ b/rspec-support.gemspec @@ -13,6 +13,14 @@ Gem::Specification.new do |spec| spec.description = "Support utilities for RSpec gems" spec.license = "MIT" + spec.metadata = { + 'bug_tracker_uri' => 'https://github.com/rspec/rspec-support/issues', + 'changelog_uri' => "https://github.com/rspec/rspec-support/blob/v#{spec.version}/Changelog.md", + 'documentation_uri' => 'https://rspec.info/documentation/', + 'mailing_list_uri' => 'https://groups.google.com/forum/#!forum/rspec', + 'source_code_uri' => 'https://github.com/rspec/rspec-support', + } + spec.files = `git ls-files -- lib/*`.split("\n") spec.files += %w[README.md LICENSE.md Changelog.md] spec.test_files = [] @@ -27,7 +35,6 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 1.8.7' - spec.add_development_dependency "bundler", "~> 1.3" - spec.add_development_dependency "rake", "~> 10.0.0" + spec.add_development_dependency "rake", "> 10.0.0" spec.add_development_dependency "thread_order", "~> 1.1.0" end diff --git a/script/clone_all_rspec_repos b/script/clone_all_rspec_repos index cbbef3743..2e97bc8f1 100755 --- a/script/clone_all_rspec_repos +++ b/script/clone_all_rspec_repos @@ -1,5 +1,5 @@ #!/bin/bash -# This file was generated on 2018-04-05T18:41:16+10:00 from the rspec-dev repo. +# This file was generated on 2020-04-03T18:53:24+03:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. set -e diff --git a/script/functions.sh b/script/functions.sh index 7a769e257..0242a15c4 100644 --- a/script/functions.sh +++ b/script/functions.sh @@ -1,4 +1,4 @@ -# This file was generated on 2018-04-05T18:41:16+10:00 from the rspec-dev repo. +# This file was generated on 2020-04-03T18:53:24+03:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" @@ -6,7 +6,7 @@ source $SCRIPT_DIR/travis_functions.sh source $SCRIPT_DIR/predicate_functions.sh # If JRUBY_OPTS isn't set, use these. -# see http://docs.travis-ci.com/user/ci-environment/ +# see https://docs.travis-ci.com/user/ci-environment/ export JRUBY_OPTS=${JRUBY_OPTS:-"--server -Xcompile.invokedynamic=false"} SPECS_HAVE_RUN_FILE=specs.out MAINTENANCE_BRANCH=`cat maintenance-branch` @@ -19,7 +19,7 @@ fi function clone_repo { if [ ! -d $1 ]; then # don't clone if the dir is already there - travis_retry eval "git clone git://github.com/rspec/$1 --depth 1 --branch $MAINTENANCE_BRANCH" + travis_retry eval "git clone https://github.com/rspec/$1 --depth 1 --branch $MAINTENANCE_BRANCH" fi; } @@ -187,7 +187,9 @@ function run_all_spec_suites { fold "rspec-core specs" run_spec_suite_for "rspec-core" fold "rspec-expectations specs" run_spec_suite_for "rspec-expectations" fold "rspec-mocks specs" run_spec_suite_for "rspec-mocks" - fold "rspec-rails specs" run_spec_suite_for "rspec-rails" + if rspec_rails_compatible; then + fold "rspec-rails specs" run_spec_suite_for "rspec-rails" + fi if rspec_support_compatible; then fold "rspec-support specs" run_spec_suite_for "rspec-support" diff --git a/script/predicate_functions.sh b/script/predicate_functions.sh index 4d592e273..82ef9baaf 100644 --- a/script/predicate_functions.sh +++ b/script/predicate_functions.sh @@ -1,4 +1,4 @@ -# This file was generated on 2018-04-05T18:41:16+10:00 from the rspec-dev repo. +# This file was generated on 2020-04-03T18:53:24+03:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. function is_mri { @@ -57,6 +57,30 @@ function is_mri_2plus { fi } +function is_ruby_23_plus { + if ruby -e "exit(RUBY_VERSION.to_f >= 2.3)"; then + return 0 + else + return 1 + fi +} + +function is_ruby_25_plus { + if ruby -e "exit(RUBY_VERSION.to_f >= 2.5)"; then + return 0 + else + return 1 + fi +} + +function rspec_rails_compatible { + if is_ruby_25_plus; then + return 0 + else + return 1 + fi +} + function rspec_support_compatible { if [ "$MAINTENANCE_BRANCH" != "2-99-maintenance" ] && [ "$MAINTENANCE_BRANCH" != "2-14-maintenance" ]; then return 0 diff --git a/script/run_build b/script/run_build index 4b42ed37d..8d4e9fbc0 100755 --- a/script/run_build +++ b/script/run_build @@ -1,5 +1,5 @@ #!/bin/bash -# This file was generated on 2018-04-05T18:41:16+10:00 from the rspec-dev repo. +# This file was generated on 2020-04-03T18:53:24+03:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. set -e diff --git a/script/travis_functions.sh b/script/travis_functions.sh index 7473ae719..e34a04ecd 100644 --- a/script/travis_functions.sh +++ b/script/travis_functions.sh @@ -1,4 +1,4 @@ -# This file was generated on 2018-04-05T18:41:16+10:00 from the rspec-dev repo. +# This file was generated on 2020-04-03T18:53:24+03:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. # Taken from: diff --git a/script/update_rubygems_and_install_bundler b/script/update_rubygems_and_install_bundler new file mode 100755 index 000000000..bf376353d --- /dev/null +++ b/script/update_rubygems_and_install_bundler @@ -0,0 +1,15 @@ +#!/bin/bash +# This file was generated on 2020-04-03T18:53:24+03:00 from the rspec-dev repo. +# DO NOT modify it by hand as your changes will get lost the next time it is generated. + +set -e +source script/functions.sh + +if is_ruby_23_plus; then + yes | gem update --system + yes | gem install bundler +else + echo "Warning installing older versions of Rubygems / Bundler" + gem update --system '2.7.8' + gem install bundler -v '1.17.3' +fi diff --git a/spec/rspec/support/differ_spec.rb b/spec/rspec/support/differ_spec.rb index 921cf9b1b..f27c169e9 100644 --- a/spec/rspec/support/differ_spec.rb +++ b/spec/rspec/support/differ_spec.rb @@ -6,7 +6,9 @@ module RSpec module Support - describe Differ do + RSpec.describe Differ do + include Spec::DiffHelpers + describe '#diff' do let(:differ) { RSpec::Support::Differ.new } @@ -14,24 +16,46 @@ module Support expected = "foo\nzap\nbar\nthis\nis\nsoo\nvery\nvery\nequal\ninsert\na\nanother\nline\n" actual = "foo\nbar\nzap\nthis\nis\nsoo\nvery\nvery\nequal\ninsert\na\nline\n" - expected_diff = <<-'EOD' - - -@@ -1,6 +1,6 @@ - foo --zap - bar -+zap - this - is - soo -@@ -9,6 +9,5 @@ - equal - insert - a --another - line -EOD + if Diff::LCS::VERSION.to_f < 1.4 || Diff::LCS::VERSION >= "1.4.4" + expected_diff = dedent(<<-'EOD') + | + | + |@@ -1,6 +1,6 @@ + | foo + |-zap + | bar + |+zap + | this + | is + | soo + |@@ -9,6 +9,5 @@ + | equal + | insert + | a + |-another + | line + | + EOD + else + expected_diff = dedent(<<-'EOD') + | + | + |@@ -1,4 +1,6 @@ + | foo + |-zap + | bar + |+zap + | this + |@@ -9,6 +11,7 @@ + | equal + | insert + | a + |-another + | line + | + EOD + end + diff = differ.diff(actual, expected) expect(diff).to be_diffed_as(expected_diff) @@ -41,25 +65,45 @@ module Support expected = "foo\nzap\nbar\nthis\nis\nsoo\nvery\nvery\nequal\ninsert\na\nanother\nline\n" actual = "foo\nbar\nzap\nthis\nis\nsoo\nvery\nvery\nequal\ninsert\na\nline\n" - expected_diff = dedent(<<-'EOS') - | - | - |@@ -1,6 +1,6 @@ - | foo - |-zap - | bar - |+zap - | this - | is - | soo - |@@ -9,6 +9,5 @@ - | equal - | insert - | a - |-another - | line - | - EOS + if Diff::LCS::VERSION.to_f < 1.4 || Diff::LCS::VERSION >= "1.4.4" + expected_diff = dedent(<<-'EOS') + | + | + |@@ -1,6 +1,6 @@ + | foo + |-zap + | bar + |+zap + | this + | is + | soo + |@@ -9,6 +9,5 @@ + | equal + | insert + | a + |-another + | line + | + EOS + else + expected_diff = dedent(<<-'EOS') + | + | + |@@ -1,4 +1,6 @@ + | foo + |-zap + | bar + |+zap + | this + |@@ -9,6 +11,7 @@ + | equal + | insert + | a + |-another + | line + | + EOS + end diff = differ.diff(actual, expected) expect(diff).to be_diffed_as(expected_diff) @@ -79,7 +123,6 @@ def differ_ivars end if String.method_defined?(:encoding) - it "returns an empty string if strings are not multiline" do expected = "Tu avec carte {count} item has".encode('UTF-16LE') actual = "Tu avec carté {count} itém has".encode('UTF-16LE') @@ -92,12 +135,13 @@ def differ_ivars it 'copes with encoded strings', :skip => RSpec::Support::OS.windows? do expected = "Tu avec carte {count} item has\n".encode('UTF-16LE') actual = "Tu avec carté {count} itém has\n".encode('UTF-16LE') - expected_diff = <<-EOD.encode('UTF-16LE') - -@@ -1,2 +1,2 @@ --Tu avec carte {count} item has -+Tu avec carté {count} itém has -EOD + expected_diff = dedent(<<-EOD).encode('UTF-16LE') + | + |@@ #{one_line_header} @@ + |-Tu avec carte {count} item has + |+Tu avec carté {count} itém has + | + EOD diff = differ.diff(actual, expected) expect(diff).to be_diffed_as(expected_diff) @@ -106,7 +150,7 @@ def differ_ivars it 'handles differently encoded strings that are compatible' do expected = "abc\n".encode('us-ascii') actual = "강인철\n".encode('UTF-8') - expected_diff = "\n@@ -1,2 +1,2 @@\n-abc\n+강인철\n" + expected_diff = "\n@@ #{one_line_header} @@\n-abc\n+강인철\n" diff = differ.diff(actual, expected) expect(diff).to be_diffed_as(expected_diff) end @@ -114,7 +158,7 @@ def differ_ivars it 'uses the default external encoding when the two strings have incompatible encodings', :failing_on_appveyor do expected = "Tu avec carte {count} item has\n" actual = "Tu avec carté {count} itém has\n".encode('UTF-16LE') - expected_diff = "\n@@ -1,2 +1,2 @@\n-Tu avec carte {count} item has\n+Tu avec carté {count} itém has\n" + expected_diff = "\n@@ #{one_line_header} @@\n-Tu avec carte {count} item has\n+Tu avec carté {count} itém has\n" diff = differ.diff(actual, expected) expect(diff).to be_diffed_as(expected_diff) @@ -135,16 +179,18 @@ def differ_ivars it "outputs unified diff message of two objects" do animal_class = Class.new do + include RSpec::Support::FormattingSupport + def initialize(name, species) @name, @species = name, species end def inspect - <<-EOA - + dedent(<<-EOA) + | EOA end end @@ -152,15 +198,16 @@ def inspect expected = animal_class.new "bob", "giraffe" actual = animal_class.new "bob", "tortoise" - expected_diff = <<'EOD' - -@@ -1,5 +1,5 @@ - -EOD + expected_diff = dedent(<<-'EOD') + | + |@@ -1,5 +1,5 @@ + | + | + EOD diff = differ.diff(expected,actual) expect(diff).to be_diffed_as(expected_diff) @@ -170,19 +217,20 @@ def inspect expected = [ :foo, 'bar', :baz, 'quux', :metasyntactic, 'variable', :delta, 'charlie', :width, 'quite wide' ] actual = [ :foo, 'bar', :baz, 'quux', :metasyntactic, 'variable', :delta, 'tango' , :width, 'very wide' ] - expected_diff = <<'EOD' - - -@@ -5,7 +5,7 @@ - :metasyntactic, - "variable", - :delta, -- "tango", -+ "charlie", - :width, -- "very wide"] -+ "quite wide"] -EOD + expected_diff = dedent(<<-'EOD') + | + | + |@@ -5,7 +5,7 @@ + | :metasyntactic, + | "variable", + | :delta, + |- "tango", + |+ "charlie", + | :width, + |- "very wide"] + |+ "quite wide"] + | + EOD diff = differ.diff(expected,actual) expect(diff).to be_diffed_as(expected_diff) @@ -200,12 +248,25 @@ def inspect; ""; end diff = differ.diff [obj], [] end - expected_diff = <<-EOD + expected_diff = dedent(<<-EOD) + | + |@@ #{one_line_header} @@ + |-[] + |+[] + | + EOD + expect(diff).to be_diffed_as(expected_diff) + end -@@ -1,2 +1,2 @@ --[] -+[] -EOD + it 'outputs unified diff message of strings in arrays' do + diff = differ.diff(["a\r\nb"], ["a\r\nc"]) + expected_diff = dedent(<<-EOD) + | + |@@ #{one_line_header} @@ + |-a\\r\\nc + |+a\\r\\nb + | + EOD expect(diff).to be_diffed_as(expected_diff) end @@ -213,16 +274,17 @@ def inspect; ""; end expected = { :foo => 'bar', :baz => 'quux', :metasyntactic => 'variable', :delta => 'charlie', :width =>'quite wide' } actual = { :foo => 'bar', :metasyntactic => 'variable', :delta => 'charlotte', :width =>'quite wide' } - expected_diff = <<'EOD' - -@@ -1,4 +1,5 @@ --:delta => "charlotte", -+:baz => "quux", -+:delta => "charlie", - :foo => "bar", - :metasyntactic => "variable", - :width => "quite wide", -EOD + expected_diff = dedent(<<-'EOD') + | + |@@ -1,4 +1,5 @@ + |-:delta => "charlotte", + |+:baz => "quux", + |+:delta => "charlie", + | :foo => "bar", + | :metasyntactic => "variable", + | :width => "quite wide", + | + EOD diff = differ.diff(expected,actual) expect(diff).to be_diffed_as(expected_diff) @@ -233,16 +295,17 @@ def inspect; ""; end expected = [{ :foo => 'bar', :baz => 'quux', :metasyntactic => 'variable', :delta => 'charlie', :width =>'quite wide' }] actual = [{ :metasyntactic => 'variable', :delta => 'charlotte', :width =>'quite wide', :foo => 'bar' }] - expected_diff = <<'EOD' - -@@ -1,4 +1,5 @@ --[{:delta=>"charlotte", -+[{:baz=>"quux", -+ :delta=>"charlie", - :foo=>"bar", - :metasyntactic=>"variable", - :width=>"quite wide"}] -EOD + expected_diff = dedent(<<-'EOD') + | + |@@ -1,4 +1,5 @@ + |-[{:delta=>"charlotte", + |+[{:baz=>"quux", + |+ :delta=>"charlie", + | :foo=>"bar", + | :metasyntactic=>"variable", + | :width=>"quite wide"}] + | + EOD diff = differ.diff(expected,actual) expect(diff).to be_diffed_as(expected_diff) @@ -250,33 +313,39 @@ def inspect; ""; end end it 'outputs unified diff message of two hashes with differing encoding', :failing_on_appveyor do - expected_diff = %Q{ -@@ -1,2 +1,2 @@ --"a" => "a", -#{ (RUBY_VERSION.to_f > 1.8) ? %Q{+"ö" => "ö"} : '+"\303\266" => "\303\266"' }, -} + expected_diff = dedent(<<-"EOD") + | + |@@ #{one_line_header} @@ + |-"a" => "a", + |#{ (RUBY_VERSION.to_f > 1.8) ? %Q{+"ö" => "ö"} : '+"\303\266" => "\303\266"' }, + | + EOD diff = differ.diff({'ö' => 'ö'}, {'a' => 'a'}) expect(diff).to be_diffed_as(expected_diff) end it 'outputs unified diff message of two hashes with encoding different to key encoding', :failing_on_appveyor do - expected_diff = %Q{ -@@ -1,2 +1,2 @@ --:a => "a", -#{ (RUBY_VERSION.to_f > 1.8) ? %Q{+\"한글\" => \"한글2\"} : '+"\355\225\234\352\270\200" => "\355\225\234\352\270\2002"' }, -} + expected_diff = dedent(<<-"EOD") + | + |@@ #{one_line_header} @@ + |-:a => "a", + |#{ (RUBY_VERSION.to_f > 1.8) ? %Q{+\"한글\" => \"한글2\"} : '+"\355\225\234\352\270\200" => "\355\225\234\352\270\2002"' }, + | + EOD diff = differ.diff({ "한글" => "한글2"}, { :a => "a"}) expect(diff).to be_diffed_as(expected_diff) end it "outputs unified diff message of two hashes with object keys" do - expected_diff = %Q{ -@@ -1,2 +1,2 @@ --["a", "c"] => "b", -+["d", "c"] => "b", -} + expected_diff = dedent(<<-"EOD") + | + |@@ #{one_line_header} @@ + |-["a", "c"] => "b", + |+["d", "c"] => "b", + | + EOD diff = differ.diff({ ['d','c'] => 'b'}, { ['a','c'] => 'b' }) expect(diff).to be_diffed_as(expected_diff) @@ -287,22 +356,26 @@ def inspect; ""; end let(:formatted_time) { ObjectFormatter.format(time) } it "outputs unified diff message of two hashes with Time object keys" do - expected_diff = %Q{ -@@ -1,2 +1,2 @@ --#{formatted_time} => "b", -+#{formatted_time} => "c", -} + expected_diff = dedent(<<-"EOD") + | + |@@ #{one_line_header} @@ + |-#{formatted_time} => "b", + |+#{formatted_time} => "c", + | + EOD diff = differ.diff({ time => 'c'}, { time => 'b' }) expect(diff).to be_diffed_as(expected_diff) end it "outputs unified diff message of two hashes with hashes inside them" do - expected_diff = %Q{ -@@ -1,2 +1,2 @@ --"b" => {"key_1"=>#{formatted_time}}, -+"c" => {"key_1"=>#{formatted_time}}, -} + expected_diff = dedent(<<-"EOD") + | + |@@ #{one_line_header} @@ + |-"b" => {"key_1"=>#{formatted_time}}, + |+"c" => {"key_1"=>#{formatted_time}}, + | + EOD left_side_hash = {'c' => {'key_1' => time}} right_side_hash = {'b' => {'key_1' => time}} @@ -316,22 +389,26 @@ def inspect; ""; end let(:formatted_time) { ObjectFormatter.format(time) } it "outputs unified diff message of two arrays with Time object keys" do - expected_diff = %Q{ -@@ -1,2 +1,2 @@ --[#{formatted_time}, "b"] -+[#{formatted_time}, "c"] -} + expected_diff = dedent(<<-"EOD") + | + |@@ #{one_line_header} @@ + |-[#{formatted_time}, "b"] + |+[#{formatted_time}, "c"] + | + EOD diff = differ.diff([time, 'c'], [time, 'b']) expect(diff).to be_diffed_as(expected_diff) end it "outputs unified diff message of two arrays with hashes inside them" do - expected_diff = %Q{ -@@ -1,2 +1,2 @@ --[{"b"=>#{formatted_time}}, "c"] -+[{"a"=>#{formatted_time}}, "c"] -} + expected_diff = dedent(<<-"EOD") + | + |@@ #{one_line_header} @@ + |-[{"b"=>#{formatted_time}}, "c"] + |+[{"a"=>#{formatted_time}}, "c"] + | + EOD left_side_array = [{'a' => time}, 'c'] right_side_array = [{'b' => time}, 'c'] @@ -344,37 +421,40 @@ def inspect; ""; end expected = "this is:\n one string" actual = "this is:\n another string" - expected_diff = <<'EOD' - -@@ -1,3 +1,3 @@ - this is: -- another string -+ one string -EOD + expected_diff = dedent(<<-'EOD') + | + |@@ -1,3 +1,3 @@ + | this is: + |- another string + |+ one string + | + EOD diff = differ.diff(expected,actual) expect(diff).to be_diffed_as(expected_diff) end it "splits items with newlines" do - expected_diff = <<'EOD' - -@@ -1,3 +1 @@ --a\nb --c\nd -EOD + expected_diff = dedent(<<-"EOD") + | + |@@ #{removing_two_line_header} @@ + |-a\\nb + |-c\\nd + | + EOD diff = differ.diff [], ["a\nb", "c\nd"] expect(diff).to be_diffed_as(expected_diff) end it "shows inner arrays on a single line" do - expected_diff = <<'EOD' - -@@ -1,3 +1 @@ --a\nb --["c\nd"] -EOD + expected_diff = dedent(<<-"EOD") + | + |@@ #{removing_two_line_header} @@ + |-a\\nb + |-["c\\nd"] + | + EOD diff = differ.diff [], ["a\nb", ["c\nd"]] expect(diff).to be_diffed_as(expected_diff) @@ -423,7 +503,7 @@ def inspect; ""; end expected_diff = dedent(<<-EOS) | - |@@ -1,2 +1,2 @@ + |@@ #{one_line_header} @@ |-[#] |+[#{object.inspect}] | @@ -445,7 +525,7 @@ def inspect; ""; end expected_diff = dedent(<<-EOS) | - |@@ -1,2 +1,2 @@ + |@@ #{one_line_header} @@ |-"oop" |+"oof" | @@ -462,12 +542,19 @@ def inspect; ""; end it "outputs colored diffs" do expected = "foo bar baz\n" actual = "foo bang baz\n" - expected_diff = "\e[0m\n\e[0m\e[34m@@ -1,2 +1,2 @@\n\e[0m\e[31m-foo bang baz\n\e[0m\e[32m+foo bar baz\n\e[0m" + expected_diff = "\e[0m\n\e[0m\e[34m@@ #{one_line_header} @@\n\e[0m\e[31m-foo bang baz\n\e[0m\e[32m+foo bar baz\n\e[0m" diff = differ.diff(expected,actual) expect(diff).to be_diffed_as(expected_diff) end end + + context 'when expected or actual is false' do + it 'generates a diff' do + expect(differ.diff(true, false)).to_not be_empty + expect(differ.diff(false, true)).to_not be_empty + end + end end end end diff --git a/spec/rspec/support/method_signature_verifier_spec.rb b/spec/rspec/support/method_signature_verifier_spec.rb index 8d8af8c43..dd9b3c6ea 100644 --- a/spec/rspec/support/method_signature_verifier_spec.rb +++ b/spec/rspec/support/method_signature_verifier_spec.rb @@ -366,6 +366,102 @@ def arity_kw(x, y:1, z:2); end end end + if RubyFeatures.kw_args_supported? + describe 'a method with optional argument and keyword arguments' do + eval <<-RUBY + def arity_kw(x, y = {}, z:2); end + RUBY + + let(:test_method) { method(:arity_kw) } + + it 'does not require any of the arguments' do + expect(valid?(nil)).to eq(true) + expect(valid?(nil, nil)).to eq(true) + end + + it 'does not allow an invalid keyword arguments' do + expect(valid?(nil, nil, :a => 1)).to eq(false) + expect(valid?(nil, :a => 1)).to eq(false) + end + + it 'treats symbols as keyword arguments and the rest as optional argument' do + expect(valid?(nil, 'a' => 1)).to eq(true) + expect(valid?(nil, 'a' => 1, :z => 3)).to eq(true) + expect(valid?(nil, 'a' => 1, :b => 3)).to eq(false) + expect(valid?(nil, 'a' => 1, :b => 2, :z => 3)).to eq(false) + end + + it 'mentions the invalid keyword args in the error', :pending => RSpec::Support::Ruby.jruby? && !RSpec::Support::Ruby.jruby_9000? do + expect(error_for(1, 2, :a => 0)).to eq("Invalid keyword arguments provided: a") + expect(error_for(1, :a => 0)).to eq("Invalid keyword arguments provided: a") + expect(error_for(1, 'a' => 0, :b => 0)).to eq("Invalid keyword arguments provided: b") + end + + it 'describes invalid arity precisely' do + expect(error_for()).to \ + eq("Wrong number of arguments. Expected 1 to 2, got 0.") + end + + it 'does not blow up when given a BasicObject as the last arg' do + expect(valid?(BasicObject.new)).to eq(true) + end + + it 'does not mutate the provided args array' do + args = [nil, nil, { :y => 1 }] + described_class.new(signature, args).valid? + expect(args).to eq([nil, nil, { :y => 1 }]) + end + + it 'mentions the arity and optional kw args in the description', :pending => RSpec::Support::Ruby.jruby? && !RSpec::Support::Ruby.jruby_9000? do + expect(signature_description).to eq("arity of 1 to 2 and optional keyword args (:z)") + end + + it "indicates the optional keyword args" do + expect(signature.optional_kw_args).to contain_exactly(:z) + end + + it "indicates it has no required keyword args" do + expect(signature.required_kw_args).to eq([]) + end + + describe 'with an expectation object' do + it 'matches the exact arity' do + expect(validate_expectation 0).to eq(false) + expect(validate_expectation 1).to eq(true) + expect(validate_expectation 2).to eq(true) + end + + it 'matches the exact range' do + expect(validate_expectation 0, 1).to eq(false) + expect(validate_expectation 1, 1).to eq(true) + expect(validate_expectation 1, 2).to eq(true) + expect(validate_expectation 1, 3).to eq(false) + end + + it 'does not match unlimited arguments' do + expect(validate_expectation :unlimited_args).to eq(false) + end + + it 'matches optional keywords with the correct arity' do + expect(validate_expectation :z).to eq(false) + expect(validate_expectation 1, :z).to eq(true) # Are we OK with that? + expect(validate_expectation 1, 2, :z).to eq(true) + expect(validate_expectation 1, 2, :y).to eq(false) + end + + it 'does not match invalid keywords' do + expect(validate_expectation :w).to eq(false) + + expect(validate_expectation 2, :w).to eq(false) + end + + it 'does not match arbitrary keywords' do + expect(validate_expectation :arbitrary_kw_args).to eq(false) + end + end + end + end + if RubyFeatures.required_kw_args_supported? describe 'a method with required keyword arguments' do eval <<-RUBY @@ -636,6 +732,7 @@ def arity_kw_arg_splat(x, **rest); end expect(valid?(nil, :x => 1)).to eq(true) expect(valid?(nil, :x => 1, :y => 2)).to eq(true) expect(valid?(:x => 1)).to eq(true) + expect(valid?(nil, {})).to eq(true) expect(valid?).to eq(false) expect(valid?(nil, nil)).to eq(false) diff --git a/spec/rspec/support/mutex_spec.rb b/spec/rspec/support/mutex_spec.rb new file mode 100644 index 000000000..a600ed695 --- /dev/null +++ b/spec/rspec/support/mutex_spec.rb @@ -0,0 +1,8 @@ +require 'rspec/support/mutex' + +RSpec.describe RSpec::Support::Mutex do + it "allows ::Mutex to be mocked", :if => defined?(::Mutex) do + expect(Mutex).to receive(:new) + ::Mutex.new + end +end diff --git a/spec/rspec/support/object_formatter_spec.rb b/spec/rspec/support/object_formatter_spec.rb index 980363cf8..033e34712 100644 --- a/spec/rspec/support/object_formatter_spec.rb +++ b/spec/rspec/support/object_formatter_spec.rb @@ -140,6 +140,15 @@ def with_delegate_loaded end end + it 'includes the delegator class in the description even when protected' do + with_delegate_loaded do + protected_delegator = Class.new(SimpleDelegator) { protected :__getobj__ } + expect( + ObjectFormatter.format(protected_delegator.new(object)) + ).to eq "#<#{protected_delegator.inspect}(#{object.inspect})>" + end + end + it 'does not require Delegator to be defined' do hide_const("Delegator") expect(ObjectFormatter.format(object)).to eq object.inspect diff --git a/spec/rspec/support/ruby_features_spec.rb b/spec/rspec/support/ruby_features_spec.rb index 0dcb1d2f1..0f368cbcf 100644 --- a/spec/rspec/support/ruby_features_spec.rb +++ b/spec/rspec/support/ruby_features_spec.rb @@ -95,6 +95,10 @@ module Support RubyFeatures.supports_rebinding_module_methods? end + specify "#supports_taint?" do + RubyFeatures.supports_taint? + end + specify "#caller_locations_supported? exists" do RubyFeatures.caller_locations_supported? if Ruby.mri? @@ -107,7 +111,7 @@ def ripper_is_implemented? in_sub_process_if_possible do begin require 'ripper' - !!defined?(::Ripper) + !!defined?(::Ripper) && Ripper.respond_to?(:lex) rescue LoadError false end @@ -116,7 +120,8 @@ def ripper_is_implemented? def ripper_works_correctly? ripper_reports_correct_line_number? && - ripper_can_parse_source_including_keywordish_symbol? + ripper_can_parse_source_including_keywordish_symbol? && + ripper_can_parse_source_referencing_keyword_arguments? end # https://github.com/jruby/jruby/issues/3386 @@ -140,6 +145,25 @@ def ripper_can_parse_source_including_keywordish_symbol? end end + # https://github.com/jruby/jruby/issues/5209 + def ripper_can_parse_source_referencing_keyword_arguments? + in_sub_process_if_possible do + require 'ripper' + # It doesn't matter if keyword arguments don't exist. + if Ruby.mri? || Ruby.jruby? + if RUBY_VERSION < '2.0' + true + else + begin + !::Ripper.sexp('def a(**kw_args); end').nil? + rescue NoMethodError + false + end + end + end + end + end + it 'returns whether Ripper is correctly implemented in the current environment' do expect(RubyFeatures.ripper_supported?).to eq(ripper_is_implemented? && ripper_works_correctly?) end diff --git a/spec/rspec/support/source_spec.rb b/spec/rspec/support/source_spec.rb index 0e3d56915..56532ced4 100644 --- a/spec/rspec/support/source_spec.rb +++ b/spec/rspec/support/source_spec.rb @@ -79,10 +79,19 @@ module RSpec::Support describe '#nodes_by_line_number' do it 'returns a hash containing nodes for each line number' do expect(source.nodes_by_line_number).to match( - 1 => [ - an_object_having_attributes(:type => :@int), - an_object_having_attributes(:type => :@ident) - ], + 1 => + if RUBY_VERSION >= '2.6.0' + [ + an_object_having_attributes(:type => :@int), + an_object_having_attributes(:type => :@period), + an_object_having_attributes(:type => :@ident) + ] + else + [ + an_object_having_attributes(:type => :@int), + an_object_having_attributes(:type => :@ident) + ] + end, 2 => [ an_object_having_attributes(:type => :@ident), an_object_having_attributes(:type => :@ident) diff --git a/spec/rspec/support/warnings_spec.rb b/spec/rspec/support/warnings_spec.rb index 5972819e5..db44ceb53 100644 --- a/spec/rspec/support/warnings_spec.rb +++ b/spec/rspec/support/warnings_spec.rb @@ -11,7 +11,7 @@ it 'works when required in isolation' do out, err, status = run_ruby_with_current_load_path("RSpec.deprecate('foo')", "-rrspec/support/warnings") expect(out).to eq("") - expect(err).to start_with("DEPRECATION: foo is deprecated") + expect(err).to include("DEPRECATION: foo is deprecated") expect(status.exitstatus).to eq(0) end diff --git a/spec/rspec/support/with_keywords_when_needed_spec.rb b/spec/rspec/support/with_keywords_when_needed_spec.rb new file mode 100644 index 000000000..588bf385c --- /dev/null +++ b/spec/rspec/support/with_keywords_when_needed_spec.rb @@ -0,0 +1,48 @@ +require 'rspec/support/with_keywords_when_needed' + +module RSpec::Support + RSpec.describe "WithKeywordsWhenNeeded" do + + describe ".class_exec" do + extend RubyFeatures + + let(:klass) do + Class.new do + def self.check_argument(argument) + raise ArgumentError unless argument == 42 + end + end + end + + def run(klass, *args, &block) + WithKeywordsWhenNeeded.class_exec(klass, *args, &block) + end + + it "will run a block without keyword arguments" do + run(klass, 42) { |arg| check_argument(arg) } + end + + it "will run a block with a hash without keyword arguments" do + run(klass, "value" => 42) { |arg| check_argument(arg["value"]) } + end + + it "will run a block with optional keyword arguments when none are provided", :if => kw_args_supported? do + binding.eval(<<-CODE, __FILE__, __LINE__) + run(klass, 42) { |arg, val: nil| check_argument(arg) } + CODE + end + + it "will run a block with optional keyword arguments when they are provided", :if => required_kw_args_supported? do + binding.eval(<<-CODE, __FILE__, __LINE__) + run(klass, val: 42) { |val: nil| check_argument(val) } + CODE + end + + it "will run a block with required keyword arguments", :if => required_kw_args_supported? do + binding.eval(<<-CODE, __FILE__, __LINE__) + run(klass, val: 42) { |val:| check_argument(val) } + CODE + end + end + end +end