diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94e8dc5..852882a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,6 @@ on: branches: - '*' env: - BRANCH: '3-12-maintenance' RSPEC_CI: true jobs: test: @@ -18,77 +17,30 @@ jobs: strategy: matrix: ruby: + - '3.3' - '3.2' - '3.1' - '3.0' - - 2.7 - - 2.6 - - 2.5 - - 2.4 - - 2.3 - - 2.2 - - 2.1.9 env: - BRANCH: "main" include: - - ruby: jruby-9.2.13.0 + - ruby: '3.3' + name_extra: "against RSpec 3.13" env: - JRUBY_OPTS: "--dev" - # Compatibility builds + BRANCH: "3-13-maintenance" - ruby: '3.2' - name_extra: "against RSpec 3.12" + name_extra: "against RSpec 3.13" env: - BRANCH: "3-12-maintenance" + BRANCH: "3-13-maintenance" - ruby: '3.1' - name_extra: "against RSpec 3.11" + name_extra: "against RSpec 3.13" env: - BRANCH: "3-11-maintenance" - - ruby: '3.1' - name_extra: "against RSpec 3.10" - env: - BRANCH: "3-10-maintenance" - - ruby: '3.0' - name_extra: "against RSpec 3.9" - env: - BRANCH: "3-9-maintenance" - - ruby: '3.0' - name_extra: "against RSpec 3.8" - env: - BRANCH: "3-8-maintenance" - - ruby: '3.0' - name_extra: "against RSpec 3.7" - env: - BRANCH: "3-7-maintenance" - - ruby: '3.0' - name_extra: "against RSpec 3.6" - env: - BRANCH: "3-6-maintenance" - - ruby: '3.0' - name_extra: "against RSpec 3.5" - env: - BRANCH: "3-5-maintenance" - - ruby: '3.0' - name_extra: "against RSpec 3.4" - env: - BRANCH: "3-4-maintenance" - - ruby: '3.0' - name_extra: "against RSpec 3.3" - env: - BRANCH: "3-3-maintenance" - - ruby: '3.0' - name_extra: "against RSpec 3.2" - env: - BRANCH: "3-2-maintenance" - - ruby: '3.0' - name_extra: "against RSpec 3.1" - env: - BRANCH: "3-1-maintenance" + BRANCH: "3-13-maintenance" - ruby: '3.0' - name_extra: "against RSpec 3.0" + name_extra: "against RSpec 3.13" env: - BRANCH: "3-0-maintenance" - + BRANCH: "3-13-maintenance" fail-fast: false continue-on-error: ${{ matrix.allow_failure || endsWith(matrix.ruby, 'head') }} env: ${{ matrix.env }} @@ -101,31 +53,3 @@ jobs: - run: script/update_rubygems_and_install_bundler - run: bundle install --binstubs --standalone - run: script/test_all - - legacy: - name: Legacy Ruby Builds (${{ matrix.container.version }}) - runs-on: ubuntu-20.04 - container: - image: ${{ matrix.container.tag }} - options: ${{ matrix.container.options || '--add-host github-complains-if-this-is-empty.com:127.0.0.1' }} - strategy: - fail-fast: false - matrix: - container: - - version: "2.0" - tag: ghcr.io/rspec/docker-ci:2.0.0 - - version: "1.9.3" - tag: ghcr.io/rspec/docker-ci:1.9.3 - - version: "JRuby 1.7" - tag: ghcr.io/rspec/docker-ci:jruby-1.7 - - version: "JRuby 9.1.17.0" - tag: ghcr.io/rspec/docker-ci:jruby-9.1.17.0 - options: "--add-host rubygems.org:151.101.129.227 --add-host api.rubygems.org:151.101.129.227" - env: - ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true - LEGACY_CI: true - JRUBY_OPTS: ${{ matrix.container.jruby_opts || '--dev' }} - steps: - - uses: actions/checkout@v3 - - run: script/legacy_setup.sh - - run: script/test_all diff --git a/.gitignore b/.gitignore index 4279c1c..ed06470 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ spec/reports test/tmp test/version_tmp tmp +bin/* diff --git a/Changelog.md b/Changelog.md index 95ef607..ef8abc2 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,10 @@ +### 2.0.0.pre + +Version 2.0.0 is a maintenance release, it drops support for Ruby below 3, and +changes the supported RSpec version to "main" and current release series. +(At the time of writing this is 3.13.x, but it means the current supported +release only). + ### 1.3.1 / 2024-10-23 [full changelog](http://github.com/rspec/rspec-its/compare/v1.3.0...v1.3.1) diff --git a/Gemfile b/Gemfile index b9feb99..36a5694 100644 --- a/Gemfile +++ b/Gemfile @@ -1,38 +1,26 @@ +# frozen_string_literal: true + source 'https://rubygems.org' -# Specify your gem's dependencies in rspec-its.gemspec gemspec %w[rspec rspec-core rspec-expectations rspec-mocks rspec-support].each do |lib| - branch = ENV.fetch('BRANCH','main') + branch = ENV.fetch('BRANCH', 'main') library_path = File.expand_path("../../#{lib}", __FILE__) if File.exist?(library_path) && !ENV['USE_GIT_REPOS'] - gem lib, :path => library_path + gem lib, path: library_path elsif lib == 'rspec' - gem 'rspec', :git => "https://github.com/rspec/rspec-metagem.git", :branch => branch + gem 'rspec', git: "https://github.com/rspec/rspec-metagem.git", branch: branch else - gem lib, :git => "https://github.com/rspec/#{lib}.git", :branch => branch + gem lib, git: "https://github.com/rspec/#{lib}.git", branch: branch end end -if RUBY_VERSION < '2.2.0' && !!(RbConfig::CONFIG['host_os'] =~ /cygwin|mswin|mingw|bccwin|wince|emx/) - gem 'ffi', '< 1.10' -elsif RUBY_VERSION < '2.4.0' && !!(RbConfig::CONFIG['host_os'] =~ /cygwin|mswin|mingw|bccwin|wince|emx/) - gem 'ffi', '< 1.15' -elsif RUBY_VERSION < '2.0' - gem 'ffi', '< 1.9.19' # ffi dropped Ruby 1.8 support in 1.9.19 -elsif RUBY_VERSION < '2.3.0' - gem 'ffi', '~> 1.12.0' -else - gem 'ffi', '~> 1.15.0' -end - -# test coverage -# gem 'simplecov', :require => false - -gem 'contracts', '< 0.16' if RUBY_VERSION < '1.9.0' - -gem 'coveralls', :require => false, :platform => :mri_20 - -eval File.read('Gemfile-custom') if File.exist?('Gemfile-custom') +gem 'aruba', '~> 2.2.0' +gem 'bundler', '> 2.0.0' +gem 'coveralls', require: false +gem 'cucumber', '>= 1.3.8' +gem 'ffi', '~> 1.17.0' +gem 'matrix', '~> 0.4.2' +gem 'rake', '~> 13.2.0' diff --git a/LICENSE.txt b/LICENSE.txt index ac5445c..e7d3ca1 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,9 +1,9 @@ (The MIT License) +Copyright (c) 2024 The RSpec Development Team Copyright (c) 2013 Peter Alfvin Copyright (c) 2012 David Chelimsky, Myron Marston Copyright (c) 2006 David Chelimsky, The RSpec Development Team -Copyright (c) 2005 Steven Baker MIT License diff --git a/README.md b/README.md index 5833921..9feecca 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# RSpec::Its [![Build Status](https://travis-ci.org/rspec/rspec-its.svg)](https://travis-ci.org/rspec/rspec-its) +# RSpec::Its [![Build Status](https://github.com/rspec/rspec-its/actions/workflows/ci.yml/badge.svg)](https://github.com/rspec/rspec-its/actions/workflows/ci.yml/badge.svg) RSpec::Its provides the `its` method as a short-hand to specify the expected value of an attribute. @@ -26,33 +26,31 @@ require 'rspec/its' ## Usage -Use the `its` method to generate a nested example group with -a single example that specifies the expected value of an attribute of the -subject using `should`, `should_not` or `is_expected`. -The `its` method can also specify the block expectations of an attribute of the -subject using `will` or `will_not`. +Use the `its` method to generate a nested example group with a single example that specifies the expected value +of an attribute of the subject using `is_expected`. The `its` method can also specify the block expectations of +an attribute of the subject using `will` or `will_not`. `its` accepts a symbol or a string, and a block representing the example. ```ruby -its(:size) { should eq(1) } -its("length") { should eq(1) } +its(:size) { is_expected.to eq(1) } +its("length") { is_expected.to eq(1) } ``` You can use a string with dots to specify a nested attribute (i.e. an attribute of the attribute of the subject). ```ruby -its("phone_numbers.size") { should_not eq(0) } +its("phone_numbers.size") { is_expected.to_not eq(0) } ``` -The following expect-style method is also available: +The following should-style method is also available: ```ruby -its(:size) { is_expected.to eq(1) } +its(:size) { should eq(1) } ``` -as is this alias for pluralized use: +as is an alias of `is_expected` for pluralized use: ```ruby its(:keys) { are_expected.to eq([:key1, :key2]) } @@ -88,13 +86,13 @@ For other objects, multiple keys within the array will be passed as separate arg ```ruby subject { Matrix[ [:a, :b], [:c, :d] ] } -its([1,1]) { should eq(:d) } +its([1,1]) { is_expected.to eq(:d) } ``` Metadata arguments are supported. ```ruby -its(:size, focus: true) { should eq(1) } +its(:size, focus: true) { is_expected.to eq(1) } ``` ## Contributing diff --git a/Rakefile b/Rakefile index 7a954d2..a4f4ca1 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "bundler" Bundler.setup Bundler::GemHelper.install_tasks @@ -13,4 +15,4 @@ RSpec::Core::RakeTask.new(:spec) do |t| t.ruby_opts = %w[-w] end -task :default => [:spec, :cucumber] +task default: %i[spec cucumber] diff --git a/features/its.feature b/features/its.feature index d83f01f..e650b1d 100644 --- a/features/its.feature +++ b/features/its.feature @@ -1,57 +1,25 @@ Feature: attribute of subject - @pre-3-9 Scenario: specify value of a nested attribute Given a file named "example_spec.rb" with: """ruby class Person attr_reader :phone_numbers - def initialize - @phone_numbers = [] - end - end - - describe Person do - context "with one phone number (555-1212)"do - subject do - person = Person.new - person.phone_numbers << "555-1212" - person - end - - its("phone_numbers.first") { should eq("555-1212") } - end - end - """ - When I run rspec with the documentation option - Then the output should contain: - """ - Person - with one phone number (555-1212) - phone_numbers.first - should eq "555-1212" - """ - @post-3-9 - Scenario: specify value of a nested attribute - Given a file named "example_spec.rb" with: - """ruby - class Person - attr_reader :phone_numbers def initialize @phone_numbers = [] end end - describe Person do + RSpec.describe Person do context "with one phone number (555-1212)"do - subject do + subject(:person) do person = Person.new person.phone_numbers << "555-1212" person end - its("phone_numbers.first") { should eq("555-1212") } + its("phone_numbers.first") { is_expected.to eq("555-1212") } end end """ @@ -67,13 +35,13 @@ Feature: attribute of subject Scenario: specify value of an attribute of a hash Given a file named "example_spec.rb" with: """ruby - describe Hash do + RSpec.describe Hash do context "with two items" do subject do {:one => 'one', :two => 'two'} end - its(:size) { should eq(2) } + its(:size) { is_expected.to eq(2) } end end """ @@ -83,14 +51,14 @@ Feature: attribute of subject Scenario: specify value for key in a hash Given a file named "example_spec.rb" with: """ruby - describe Hash do + RSpec.describe Hash do context "with keys :one and 'two'" do subject do {:one => 1, "two" => 2} end - its([:one]) { should eq(1) } - its(["two"]) { should eq(2) } + its([:one]) { is_expected.to eq(1) } + its(["two"]) { is_expected.to eq(2) } end end """ @@ -102,15 +70,15 @@ Feature: attribute of subject """ruby require 'matrix' - describe Matrix do + RSpec.describe Matrix do context "with values [[1, 2], [3, 4]]" do subject do Matrix[[1, 2], [3, 4]] end - its([0, 1]) { should eq(2) } - its([1, 0]) { should eq(3) } - its([1, 2]) { should be_nil } + its([0, 1]) { are_expected.to eq(2) } + its([1, 0]) { are_expected.to eq(3) } + its([1, 2]) { are_expected.to be_nil } end end """ @@ -120,22 +88,22 @@ Feature: attribute of subject Scenario: failures are correctly reported as coming from the `its` line Given a file named "example_spec.rb" with: """ruby - describe Array do + RSpec.describe Array do context "when first created" do - its(:size) { should_not eq(0) } + its(:size) { is_expected.to_not eq(0) } end end """ When I run rspec - Then the output should contain "Failure/Error: its(:size) { should_not eq(0) }" - And the output should not match /#[^\n]*rspec[\x2f]its/ + Then the output should contain "Failure/Error: its(:size) { is_expected.to_not eq(0) }" + And the output should not match /#[^\n]*rspec[\x2f]its/ Scenario: examples can be specified by exact line number Given a file named "example_spec.rb" with: """ruby - describe Array do + RSpec.describe Array do context "when first created" do - its(:size) { should eq(0) } + its(:size) { is_expected.to eq(0) } end end """ @@ -145,10 +113,11 @@ Feature: attribute of subject Scenario: examples can be specified by line number within containing block Given a file named "example_spec.rb" with: """ruby - describe Array do + RSpec.describe Array do context "when first created" do - its(:size) { should eq(0) } + its(:size) { is_expected.to eq(0) } end + it "should never execute this" do expect(true).to be(false) end @@ -166,8 +135,9 @@ Feature: attribute of subject end end - describe Klass do - subject { Klass.new } + RSpec.describe Klass do + subject(:klass) { Klass.new } + its(:foo) { will_not raise_error } its(:bar) { will raise_error(NoMethodError) } end @@ -180,8 +150,9 @@ Feature: attribute of subject """ruby class Klass; end - describe Klass do - subject { Klass.new } + RSpec.describe Klass do + subject(:klass) { Klass.new } + its(:foo) { will_not raise_error } end """ @@ -199,8 +170,9 @@ Feature: attribute of subject end end - describe Klass do - subject { Klass.new } + RSpec.describe Klass do + subject(:arnie) { Klass.new } + its(:terminator) { will be("back") } end """ diff --git a/features/step_definitions/additional_cli_steps.rb b/features/step_definitions/additional_cli_steps.rb index 25a1476..391ca33 100644 --- a/features/step_definitions/additional_cli_steps.rb +++ b/features/step_definitions/additional_cli_steps.rb @@ -1,32 +1,34 @@ -When /^I run rspec( with the documentation option)?$/ do |documentation| - rspec_its_gem_location = File.expand_path('../../../lib/rspec/its', __FILE__) +# frozen_string_literal: true + +When(/^I run rspec( with the documentation option)?$/) do |documentation| + rspec_its_gem_location = File.expand_path('../../lib/rspec/its', __dir__) require_option = "--require #{rspec_its_gem_location}" format_option = documentation ? "--format documentation" : "" rspec_command = ['rspec', require_option, format_option, 'example_spec.rb'].join(' ') step "I run `#{rspec_command}`" end -When /^I run rspec specifying line number (\d+)$/ do |line_number| - rspec_its_gem_location = File.expand_path('../../../lib/rspec/its', __FILE__) +When(/^I run rspec specifying line number (\d+)$/) do |line_number| + rspec_its_gem_location = File.expand_path('../../lib/rspec/its', __dir__) require_option = "--require #{rspec_its_gem_location}" file_specification = "example_spec.rb:#{line_number}" rspec_command = ['rspec', require_option, file_specification].join(' ') step "I run `#{rspec_command}`" end -Then /^the example(?:s)? should(?: all)? pass$/ do - step %q{the output should contain "0 failures"} - step %q{the output should not contain "0 examples"} - step %q{the exit status should be 0} +Then(/^the example(?:s)? should(?: all)? pass$/) do + step 'the output should contain "0 failures"' + step 'the output should not contain "0 examples"' + step 'the exit status should be 0' end -Then(/^the example should fail$/) do - step %q{the output should contain "1 failure"} - step %q{the exit status should not be 0} +Then("the example should fail") do + step 'the output should contain "1 failure"' + step 'the exit status should not be 0' end Then(/^the output should contain "(.*?)" and "(.*?)"$/) do |string1, string2| unless [string1, string2].all? { |s| all_output.include?(s) } - fail %Q{Both "#{string1}" and "#{string2}" were found in:\n#{all_output}} + fail %(Both "#{string1}" and "#{string2}" were found in:\n#{all_output}) end end diff --git a/features/support/env.rb b/features/support/env.rb index 3d09d6c..fb57b75 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -1,15 +1,14 @@ +# frozen_string_literal: true + require 'aruba/cucumber' require 'rspec/core' require 'rspec/its' -Aruba.configure do |config| - config.before(:command) do |cmd| - cmd.environment['JRUBY_OPTS'] = "-X-C #{ENV['JRUBY_OPTS']}" # disable JIT since these processes are so short lived - end -end if RUBY_PLATFORM == 'java' - -Aruba.configure do |config| - config.before(:command) do |cmd| - cmd.environment['RBXOPT'] = "-Xint=true #{ENV['RBXOPT']}" # disable JIT since these processes are so short lived +if RUBY_PLATFORM == 'java' + Aruba.configure do |config| + config.before(:command) do |cmd| + # disable JIT since these processes are so short lived + cmd.environment['JRUBY_OPTS'] = "-X-C #{ENV.fetch('JRUBY_OPTS', '')}" + end end -end if defined?(Rubinius) +end diff --git a/lib/rspec/its.rb b/lib/rspec/its.rb index 6d357a1..8e67d11 100644 --- a/lib/rspec/its.rb +++ b/lib/rspec/its.rb @@ -1,26 +1,29 @@ +# frozen_string_literal: true + +require 'rspec/its/subject' require 'rspec/its/version' require 'rspec/core' RSpec::Core::ExampleGroup.define_example_method :__its_example module RSpec + # Adds the `its` to RSpec Example Groups, included by default. module Its - # Creates a nested example group named by the submitted `attribute`, # and then generates an example using the submitted block. # # @example # # # This ... - # describe Array do - # its(:size) { should eq(0) } + # RSpec.describe Array do + # its(:size) { is_expected.to eq(0) } # end # # # ... generates the same runtime structure as this: - # describe Array do + # RSpec.describe Array do # describe "size" do - # it "should eq(0)" do - # subject.size.should eq(0) + # it "is_expected.to eq(0)" do + # expect(subject.size).to eq(0) # end # end # end @@ -31,14 +34,14 @@ module Its # # @example # - # describe Person do - # subject do + # RSpec.describe Person do + # subject(:person) do # Person.new.tap do |person| # person.phone_numbers << "555-1212" # end # end # - # its("phone_numbers.first") { should eq("555-1212") } + # its("phone_numbers.first") { is_expected.to eq("555-1212") } # end # # When the subject is a `Hash`, you can refer to the Hash keys by @@ -46,30 +49,30 @@ module Its # # @example # - # describe "a configuration Hash" do + # RSpec.describe "a configuration Hash" do # subject do # { :max_users => 3, # 'admin' => :all_permissions. # 'john_doe' => {:permissions => [:read, :write]}} # end # - # its([:max_users]) { should eq(3) } - # its(['admin']) { should eq(:all_permissions) } - # its(['john_doe', :permissions]) { should eq([:read, :write]) } + # its([:max_users]) { is_expected.to eq(3) } + # its(['admin']) { is_expected.to eq(:all_permissions) } + # its(['john_doe', :permissions]) { are_expected.to eq([:read, :write]) } # # # You can still access its regular methods this way: - # its(:keys) { should include(:max_users) } - # its(:count) { should eq(2) } + # its(:keys) { is_expected.to include(:max_users) } + # its(:count) { is_expected.to eq(2) } # end # - # With an implicit subject, `is_expected` can be used as an alternative - # to `should` (e.g. for one-liner use). An `are_expected` alias is also + # With an implicit subject, `should` can be used as an alternative + # to `is_expected` (e.g. for one-liner use). An `are_expected` alias is also # supplied. # # @example # - # describe Array do - # its(:size) { is_expected.to eq(0) } + # RSpec.describe Array do + # its(:size) { should eq(0) } # end # # With an implicit subject, `will` can be used as an alternative @@ -77,7 +80,7 @@ module Its # # @example # - # describe Array do + # RSpec.describe Array do # its(:foo) { will raise_error(NoMethodError) } # end # @@ -86,7 +89,7 @@ module Its # # @example # - # describe Array do + # RSpec.describe Array do # its(:size) { will_not raise_error } # end # @@ -96,15 +99,15 @@ module Its # @example # # # This ... - # describe Array do - # its(:size, :focus) { should eq(0) } + # RSpec.describe Array do + # its(:size, :focus) { is_expected.to eq(0) } # end # # # ... generates the same runtime structure as this: - # describe Array do + # RSpec.describe Array do # describe "size" do - # it "should eq(0)", :focus do - # subject.size.should eq(0) + # it "is expected to eq(0)", :focus do + # expect(subject.size).to eq(0) # end # end # end @@ -115,70 +118,56 @@ module Its # # @example # - # describe Person do + # RSpec.describe Person do # subject { Person.new } + # # before { subject.age = 25 } - # its(:age) { should eq(25) } + # + # its(:age) { is_expected.to eq(25) } # end def its(attribute, *options, &block) - its_caller = caller.select {|file_line| file_line !~ %r(/lib/rspec/its) } - describe(attribute.to_s, :caller => its_caller) do - let(:__its_subject) do - if Array === attribute - if Hash === subject - attribute.inject(subject) {|inner, attr| inner[attr] } - else - subject[*attribute] - end - else - attribute_chain = attribute.to_s.split('.') - attribute_chain.inject(subject) do |inner_subject, attr| - inner_subject.send(attr) - end - end - end + its_caller = caller.grep_v(%r{/lib/rspec/its}) + + describe(attribute.to_s, caller: its_caller) do + let(:__its_subject) { RSpec::Its::Subject.for(attribute, subject) } def is_expected expect(__its_subject) end alias_method :are_expected, :is_expected - def will(matcher=nil, message=nil) - unless matcher.supports_block_expectations? - raise ArgumentError, "`will` only supports block expectations" - end + def will(matcher = nil, message = nil) + raise ArgumentError, "`will` only supports block expectations" unless matcher.supports_block_expectations? + expect { __its_subject }.to matcher, message end - def will_not(matcher=nil, message=nil) - unless matcher.supports_block_expectations? - raise ArgumentError, "`will_not` only supports block expectations" - end + def will_not(matcher = nil, message = nil) + raise ArgumentError, "`will_not` only supports block expectations" unless matcher.supports_block_expectations? + expect { __its_subject }.to_not matcher, message end - def should(matcher=nil, message=nil) + def should(matcher = nil, message = nil) RSpec::Expectations::PositiveExpectationHandler.handle_matcher(__its_subject, matcher, message) end - def should_not(matcher=nil, message=nil) + def should_not(matcher = nil, message = nil) RSpec::Expectations::NegativeExpectationHandler.handle_matcher(__its_subject, matcher, message) end - options << {} unless options.last.kind_of?(Hash) - options.last.merge!(:caller => its_caller) + options << {} unless options.last.is_a?(Hash) + options.last.merge!(caller: its_caller) __its_example(nil, *options, &block) - end end - end end RSpec.configure do |rspec| rspec.extend RSpec::Its - rspec.backtrace_exclusion_patterns << %r(/lib/rspec/its) + rspec.backtrace_exclusion_patterns << %r{/lib/rspec/its} end RSpec::SharedContext.send(:include, RSpec::Its) diff --git a/lib/rspec/its/subject.rb b/lib/rspec/its/subject.rb new file mode 100644 index 0000000..0efd63d --- /dev/null +++ b/lib/rspec/its/subject.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module RSpec + module Its + # @api private + # Handles turning subject into an expectation target + module Subject + def for(attribute, subject) + if Array === attribute + if Hash === subject + attribute.inject(subject) { |inner, attr| inner[attr] } + else + subject[*attribute] + end + else + attribute_chain = attribute.to_s.split('.') + attribute_chain.inject(subject) do |inner_subject, attr| + inner_subject.send(attr) + end + end + end + + module_function :for + end + end +end diff --git a/lib/rspec/its/version.rb b/lib/rspec/its/version.rb index 88da640..50ec6d0 100644 --- a/lib/rspec/its/version.rb +++ b/lib/rspec/its/version.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module RSpec module Its - VERSION = "1.3.1" + VERSION = '2.0.0.pre' end end diff --git a/rspec-its.gemspec b/rspec-its.gemspec index be6e032..7c6bc7c 100644 --- a/rspec-its.gemspec +++ b/rspec-its.gemspec @@ -1,49 +1,31 @@ -# coding: utf-8 -lib = File.expand_path('../lib', __FILE__) +# frozen_string_literal: true + +lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'rspec/its/version' Gem::Specification.new do |spec| spec.name = "rspec-its" spec.version = RSpec::Its::VERSION - spec.authors = ["Peter Alfvin"] - spec.email = ["palfvin@gmail.com"] - spec.description = %q{RSpec extension gem for attribute matching} - spec.summary = %q{Provides "its" method formerly part of rspec-core} + spec.authors = ["The RSpec Development Team"] + spec.email = ["maintainers@rspec.info"] + spec.description = 'RSpec extension gem for attribute matching' + spec.summary = 'Provides "its" method formerly part of rspec-core' spec.homepage = "https://github.com/rspec/rspec-its" spec.license = "MIT" + spec.required_ruby_version = '> 3.0.0' - spec.metadata = { - 'bug_tracker_uri' => 'https://github.com/rspec/rspec-its/issues', - 'changelog_uri' => "https://github.com/rspec/rspec-its/blob/v#{spec.version}/Changelog.md", - 'documentation_uri' => "https://www.rubydoc.info/gems/rspec-its/#{spec.version}", - 'mailing_list_uri' => 'https://groups.google.com/forum/#!forum/rspec', - 'source_code_uri' => 'https://github.com/rspec/rspec-its', - } + spec.metadata['bug_tracker_uri'] = 'https://github.com/rspec/rspec-its/issues' + spec.metadata['changelog_uri'] = "https://github.com/rspec/rspec-its/blob/v#{spec.version}/Changelog.md" + spec.metadata['documentation_uri'] = "https://www.rubydoc.info/gems/rspec-its/#{spec.version}" + spec.metadata['mailing_list_uri'] = 'https://groups.google.com/forum/#!forum/rspec' + spec.metadata['rubygems_mfa_required'] = 'true' + spec.metadata['source_code_uri'] = 'https://github.com/rspec/rspec-its' spec.files = `git ls-files`.split($/) - %w[cucumber.yml] spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } - spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] - spec.add_runtime_dependency 'rspec-core', '>= 3.0.0' - spec.add_runtime_dependency 'rspec-expectations', '>= 3.0.0' - - if RUBY_VERSION.to_f < 1.9 || RUBY_VERSION == '1.9.2' - spec.add_development_dependency "rake", "~> 10.0.0" - elsif RUBY_VERSION.to_f < 2 - spec.add_development_dependency "rake", "~> 11.0.0" - elsif RUBY_VERSION.to_f < 2.3 - spec.add_development_dependency "rake", "~> 12.3.2" - else - spec.add_development_dependency "rake", "~> 13.0.0" - end - - spec.add_development_dependency 'bundler', '> 1.3.0' - if RUBY_VERSION.to_f < 2 - spec.add_development_dependency 'cucumber', '< 3.0.0' - else - spec.add_development_dependency 'cucumber', '>= 1.3.8' - end - spec.add_development_dependency "aruba", "~> 0.14.12" + spec.add_dependency 'rspec-core', '>= 3.13.0' + spec.add_dependency 'rspec-expectations', '>= 3.13.0' end diff --git a/script/legacy_setup.sh b/script/legacy_setup.sh deleted file mode 100755 index fd83a87..0000000 --- a/script/legacy_setup.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -set -e - -bundle install --standalone --binstubs - -if [ -x ./bin/rspec ]; then - echo "RSpec bin detected" -else - if [ -x ./exe/rspec ]; then - cp ./exe/rspec ./bin/rspec - echo "RSpec restored from exe" - else - echo "No RSpec bin available" - exit 1 - fi -fi diff --git a/script/test_all b/script/test_all index 43ac721..0ba5be3 100755 --- a/script/test_all +++ b/script/test_all @@ -21,16 +21,10 @@ else echo "Using $BRANCH" fi -if ruby -e 'exit(ENV.fetch("BRANCH") =~ /3-[0-8]-maintenance/ ? 0 : 1)'; then - TAGS="--tags @pre-3-9" -else - TAGS="--tags @post-3-9" -fi; - if ruby -e "exit(defined?(RUBY_PLATFORM) && RUBY_PLATFORM == 'java')"; then # This is JRUBY which requires this one weird path trick... PATH="${PWD}/bin:$PATH" \ - bundle exec cucumber --strict $TAGS + bundle exec cucumber --strict else - bundle exec cucumber --strict $TAGS + bundle exec cucumber --strict fi; diff --git a/script/update_rubygems_and_install_bundler b/script/update_rubygems_and_install_bundler index c1be703..264c8d7 100755 --- a/script/update_rubygems_and_install_bundler +++ b/script/update_rubygems_and_install_bundler @@ -1,35 +1,7 @@ #!/bin/bash -# This file was generated on 2019-01-03T20:34:23+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. set -e -function is_ruby_31_plus { - if ruby -e "exit(RUBY_VERSION.to_f >= 3.1)"; then - return 0 - else - return 1 - fi -} - -function is_ruby_23_plus { - if ruby -e "exit(RUBY_VERSION.to_f >= 2.3)"; then - return 0 - else - return 1 - fi -} - -if is_ruby_31_plus; then - echo "Installing rubygems 3.3.6 / bundler 2.3.6" - yes | gem update --system '3.3.6' - yes | gem install bundler -v '2.3.6' -elif is_ruby_23_plus; then - echo "Installing rubygems 3.2.22 / bundler 2.2.22" - yes | gem update --system '3.2.22' - yes | gem install bundler -v '2.2.22' -else - echo "Warning installing older versions of Rubygems / Bundler" - gem update --system '2.7.8' - gem install bundler -v '1.17.3' -fi +echo "Installing latest rubygems / bundler" +yes | gem update --system +yes | gem install bundler diff --git a/spec/rspec/its_spec.rb b/spec/rspec/its_spec.rb index 9f894df..b286baf 100644 --- a/spec/rspec/its_spec.rb +++ b/spec/rspec/its_spec.rb @@ -1,394 +1,396 @@ -require 'spec_helper' +# frozen_string_literal: true -module RSpec - describe Its do - describe "#its" do - context "with implicit subject" do - context "preserves described_class" do - its(:symbol) { expect(described_class).to be Its } - its([]) { expect(described_class).to be Its } - end - end +require 'spec_helper' - context "with explicit subject" do - subject do - Class.new do - def initialize - @call_count = 0 - end +RSpec.describe RSpec::Its do + context "with implicit subject" do + context "preserves described_class" do + its(:symbol) { expect(described_class).to be RSpec::Its } + its([]) { expect(described_class).to be RSpec::Its } + end + end - def call_count - @call_count += 1 - end - end.new + context "with explicit subject" do + subject do + Class.new do + def initialize + @call_count = 0 end - before(:each, :meta) do - subject.call_count + def call_count + @call_count += 1 end + end.new + end - context "with some metadata" do - its(:call_count, :meta) { should eq(2) } - end + before(:each, :meta) do + subject.call_count + end - context "with a call counter" do - its(:call_count) { should eq(1) } - end + context "with some metadata" do + its(:call_count, :meta) { is_expected.to eq(2) } + end - context "with nil value" do - subject do - Class.new do - def nil_value - nil - end - end.new - end - its(:nil_value) { should be_nil } - end + context "with a call counter" do + its(:call_count) { is_expected.to eq(1) } + end - context "with nested attributes" do - subject do - Class.new do - def name - "John" - end - end.new - end - its("name") { should eq("John") } - its("name.size") { should eq(4) } - if RUBY_VERSION >= "2.4.0" - its("name.size.class") { should eq(Integer) } - else - its("name.size.class") { should eq(Fixnum) } + context "with nil value" do + subject do + Class.new do + def nil_value + nil end + end.new + end - context "using should_not" do - its("name") { should_not eq("Paul") } - end + its(:nil_value) { is_expected.to be_nil } + end - context "using is_expected" do - its("name") { is_expected.to eq("John") } + context "with nested attributes" do + subject do + Class.new do + def name + "John" end + end.new + end - context "using will_not" do - its("name") { will_not raise_error } - end + its("name") { is_expected.to eq("John") } + its("name.size") { is_expected.to eq(4) } + its("name.size.class") { is_expected.to eq(Integer) } + + context "using are_expected" do + its("name.chars.to_a") { are_expected.to eq(%w[J o h n]) } + end + + context "using will_not" do + its("name") { will_not raise_error } + end + + context "using should" do + its("name") { should eq("John") } + end + + context "using should_not" do + its("name") { should_not eq("Paul") } + end + end - context "using are_expected" do - its("name.chars.to_a") { are_expected.to eq(%w[J o h n]) } + context "when it responds to #[]" do + subject do + Class.new do + def [](*objects) + objects.map do |object| + "#{object.class}: #{object}" + end.join("; ") end - end - context "when it responds to #[]" do - subject do - Class.new do - def [](*objects) - objects.map do |object| - "#{object.class}: #{object.to_s}" - end.join("; ") - end - - def name - "George" - end - end.new + def name + "George" end - its([:a]) { should eq("Symbol: a") } - its(['a']) { should eq("String: a") } - if RUBY_VERSION >= "2.4.0" - its([:b, 'c', 4]) { should eq("Symbol: b; String: c; Integer: 4") } - else - its([:b, 'c', 4]) { should eq("Symbol: b; String: c; Fixnum: 4") } + end.new + end + + its([:a]) { is_expected.to eq("Symbol: a") } + its(['a']) { is_expected.to eq("String: a") } + its([:b, 'c', 4]) { is_expected.to eq("Symbol: b; String: c; Integer: 4") } + its(:name) { is_expected.to eq("George") } + + context "when referring to an attribute that doesn't exist" do + context "it raises an error" do + its(:age) do + expect do + is_expected.to eq(64) + end.to raise_error(NoMethodError) end - its(:name) { should eq("George") } - context "when referring to an attribute that doesn't exist" do - context "it raises an error" do - its(:age) do - expect do - should eq(64) - end.to raise_error(NoMethodError) - end - - context "using will" do - its(:age) { will raise_error(NoMethodError) } - end - end + + context "using will" do + its(:age) { will raise_error(NoMethodError) } end + end + end - context "when it's a hash" do - subject { {:a => {:deep => {:key => "value"}}} } + context "when it's a hash" do + subject { { a: { deep: { key: "value" } } } } - its([:a]) { should eq({:deep => {:key => "value"}}) } - its([:a, :deep]) { should eq({:key => "value"}) } - its([:a, :deep, :key]) { should eq("value") } + its([:a]) { is_expected.to eq({ deep: { key: "value" } }) } + its(%i[a deep]) { is_expected.to eq({ key: "value" }) } + its(%i[a deep key]) { is_expected.to eq("value") } - context "when referring to a key that doesn't exist" do - its([:not_here]) { should be_nil } - its([:a, :ghost]) { should be_nil } - its([:deep, :ghost]) { expect { should eq("missing") }.to raise_error(NoMethodError) } + context "when referring to a key that doesn't exist" do + its([:not_here]) { is_expected.to be_nil } + its(%i[a ghost]) { are_expected.to be_nil } + its(%i[deep ghost]) { expect { is_expected.to eq("missing") }.to raise_error(NoMethodError) } - context "using will" do - its([:deep, :ghost]) { will raise_error(NoMethodError) } - end - end + context "using will" do + its(%i[deep ghost]) { will raise_error(NoMethodError) } end end + end + end - context "when it does not respond to #[]" do - subject { Object.new } - - context "it raises an error" do - its([:a]) do - expect do - should eq("Symbol: a") - end.to raise_error(NoMethodError) - end + context "when it does not respond to #[]" do + subject { Object.new } - context "using will" do - its([:a]) { will raise_error(NoMethodError) } - end - end + context "it raises an error" do + its([:a]) do + expect { is_expected.to eq("Symbol: a") }.to raise_error(NoMethodError) end - context "calling and overriding super" do - it "calls to the subject defined in the parent group" do - group = RSpec::Core::ExampleGroup.describe(Array) do - subject { [1, 'a'] } + context "using will" do + its([:a]) { will raise_error(NoMethodError) } + end + end + end - its(:last) { should eq("a") } + context "calling and overriding super" do + it "calls to the subject defined in the parent group" do + group = RSpec::Core::ExampleGroup.describe(Array) do + subject { [1, 'a'] } - describe '.first' do - def subject; - super().first; - end + its(:last) { is_expected.to eq("a") } - its(:next) { should eq(2) } - end + describe '.first' do + def subject + super.first end - expect(group.run(NullFormatter.new)).to be_truthy + its(:next) { is_expected.to eq(2) } end end - context "with nil subject" do - subject do - Class.new do - def initialize - @counter = -1 - end - - def nil_if_first_time - @counter += 1 - @counter == 0 ? nil : true - end - end.new + expect(group.run(NullFormatter.new)).to be_truthy + end + end + + context "with nil subject" do + subject do + Class.new do + def initialize + @counter = -1 end - its(:nil_if_first_time) { should be(nil) } - end - context "with false subject" do - subject do - Class.new do - def initialize - @counter = -1 - end - - def false_if_first_time - @counter += 1 - @counter > 0 - end - end.new + def nil_if_first_time + @counter += 1 + @counter == 0 ? nil : true end - its(:false_if_first_time) { should be(false) } - end + end.new + end - describe 'accessing `subject` in `before` and `let`' do - subject { 'my subject' } - before { @subject_in_before = subject } - let(:subject_in_let) { subject } - let!(:eager_loaded_subject_in_let) { subject } - - # These examples read weird, because we're actually - # specifying the behaviour of `its` itself - its(nil) { expect(subject).to eq('my subject') } - its(nil) { expect(@subject_in_before).to eq('my subject') } - its(nil) { expect(subject_in_let).to eq('my subject') } - its(nil) { expect(eager_loaded_subject_in_let).to eq('my subject') } - end + its(:nil_if_first_time) { is_expected.to be(nil) } + end - describe "in shared_context" do - shared_context "shared stuff" do - subject { Array } - its(:name) { should eq "Array" } + context "with false subject" do + subject do + Class.new do + def initialize + @counter = -1 end - include_context "shared stuff" - end + def false_if_first_time + @counter += 1 + @counter > 0 + end + end.new + end - describe "when extending SharedContext" do - it 'works with an implicit subject' do - shared = Module.new do - extend RSpec::SharedContext - its(:size) { should eq 0 } - end - group = RSpec::Core::ExampleGroup.describe(Array) do - include shared - end + its(:false_if_first_time) { is_expected.to be(false) } + end - group.run(NullFormatter.new) + describe 'accessing `subject` in `before` and `let`' do + subject { 'my subject' } - result = group.children.first.examples.first.execution_result - # Following conditional needed to work across mix of RSpec and ruby versions without warning - status = result.respond_to?(:status) ? result.status : result[:status].to_sym - expect(status).to eq(:passed) - end - end - end - context "with metadata" do - context "preserves access to metadata that doesn't end in hash" do - its([], :foo) do |example| - expect(example.metadata[:foo]).to be(true) - end - end - context "preserves access to metadata that ends in hash" do - its([], :foo, :bar => 17) do |example| - expect(example.metadata[:foo]).to be(true) - expect(example.metadata[:bar]).to be(17) - end - end - end + before { @subject_in_before = subject } - context "when expecting errors" do - subject do - Class.new do - def good; end + let(:subject_in_let) { subject } + let!(:eager_loaded_subject_in_let) { subject } - def bad - raise ArgumentError, "message" - end - end.new - end + # These examples read weird, because we're actually + # specifying the behaviour of `its` itself + its(nil) { expect(subject).to eq('my subject') } + its(nil) { expect(@subject_in_before).to eq('my subject') } + its(nil) { expect(subject_in_let).to eq('my subject') } + its(nil) { expect(eager_loaded_subject_in_let).to eq('my subject') } + end - its(:good) { will_not raise_error } - its(:bad) { will raise_error(ArgumentError) } - its(:bad) { will raise_error("message") } - its(:bad) { will raise_error(ArgumentError, "message") } + describe "in shared_context" do + shared_context "shared stuff" do + subject { Array } + + its(:name) { is_expected.to eq "Array" } end - context "when expecting throws" do - subject do - Class.new do - def good; end + include_context "shared stuff" + end - def bad - throw :abort, "message" - end - end.new + describe "when extending SharedContext" do + it 'works with an implicit subject' do + shared = Module.new do + extend RSpec::SharedContext + its(:size) { is_expected.to eq 0 } end - its(:good) { will_not throw_symbol } - its(:bad) { will throw_symbol } - its(:bad) { will throw_symbol(:abort) } - its(:bad) { will throw_symbol(:abort, "message") } + group = RSpec::Core::ExampleGroup.describe(Array) do + include shared + end + + group.run(NullFormatter.new) + + result = group.children.first.examples.first.execution_result + # Following conditional needed to work across mix of RSpec and ruby versions without warning + status = result.respond_to?(:status) ? result.status : result[:status].to_sym + expect(status).to eq(:passed) end + end + end - context "with change observation" do - subject do - Class.new do - attr_reader :count + context "with metadata" do + context "preserves access to metadata that doesn't end in hash" do + its([], :foo) do |example| + expect(example.metadata[:foo]).to be(true) + end + end - def initialize - @count = 0 - end + context "preserves access to metadata that ends in hash" do + its([], :foo, bar: 17) do |example| + expect(example.metadata[:foo]).to be(true) + expect(example.metadata[:bar]).to be(17) + end + end + end - def increment - @count += 1 - end + context "when expecting errors" do + subject do + Class.new do + def good; end - def noop; end - end.new + def bad + raise ArgumentError, "message" end + end.new + end - its(:increment) { will change { subject.count }.by(1) } - its(:increment) { will change { subject.count }.from(0) } - its(:increment) { will change { subject.count }.from(0).to(1) } - its(:increment) { will change { subject.count }.by_at_least(1) } - its(:increment) { will change { subject.count }.by_at_most(1) } + its(:good) { will_not raise_error } + its(:bad) { will raise_error(ArgumentError) } + its(:bad) { will raise_error("message") } + its(:bad) { will raise_error(ArgumentError, "message") } + end - its(:noop) { will_not change { subject.count } } - its(:noop) { will_not change { subject.count }.from(0) } + context "when expecting throws" do + subject do + Class.new do + def good; end - its(:increment) do - expect { will_not change { subject.count }.by(0) }.to \ - raise_error(NotImplementedError, '`expect { }.not_to change { }.by()` is not supported') + def bad + throw :abort, "message" end + end.new + end - its(:increment) do - expect { will_not change { subject.count }.by_at_least(2) }.to \ - raise_error(NotImplementedError, '`expect { }.not_to change { }.by_at_least()` is not supported') + its(:good) { will_not throw_symbol } + its(:bad) { will throw_symbol } + its(:bad) { will throw_symbol(:abort) } + its(:bad) { will throw_symbol(:abort, "message") } + end + + context "with change observation" do + subject do + Class.new do + attr_reader :count + + def initialize + @count = 0 end - its(:increment) do - expect { will_not change { subject.count }.by_at_most(3) }.to \ - raise_error(NotImplementedError, '`expect { }.not_to change { }.by_at_most()` is not supported') + def increment + @count += 1 end - end - context "with output capture" do - subject do - Class.new do - def stdout - print "some output" - end + def noop; end + end.new + end - def stderr - $stderr.print "some error" - end + its(:increment) { will change { subject.count }.by(1) } + its(:increment) { will change { subject.count }.from(0) } + its(:increment) { will change { subject.count }.from(0).to(1) } + its(:increment) { will change { subject.count }.by_at_least(1) } + its(:increment) { will change { subject.count }.by_at_most(1) } - def noop; end - end.new - end + its(:noop) { will_not(change { subject.count }) } + its(:noop) { will_not change { subject.count }.from(0) } - its(:stdout) { will output("some output").to_stdout } - its(:stderr) { will output("some error").to_stderr } + its(:increment) do + expect { will_not change { subject.count }.by(0) }.to \ + raise_error(NotImplementedError, '`expect { }.not_to change { }.by()` is not supported') + end - its(:noop) { will_not output("some error").to_stderr } - its(:noop) { will_not output("some output").to_stdout } - end + its(:increment) do + expect { will_not change { subject.count }.by_at_least(2) }.to \ + raise_error(NotImplementedError, '`expect { }.not_to change { }.by_at_least()` is not supported') + end - context "#will with non block expectations" do - subject do - Class.new do - def terminator - "back" - end - end.new - end + its(:increment) do + expect { will_not change { subject.count }.by_at_most(3) }.to \ + raise_error(NotImplementedError, '`expect { }.not_to change { }.by_at_most()` is not supported') + end + end - its(:terminator) do - expect { will be("back") }.to \ - raise_error(ArgumentError, '`will` only supports block expectations') + context "with output capture" do + subject do + Class.new do + def stdout + print "some output" end - its(:terminator) do - expect { will_not be("back") }.to \ - raise_error(ArgumentError, '`will_not` only supports block expectations') + def stderr + $stderr.print "some error" end - end - context "when example is redefined" do - subject do - Class.new do - def will_still_work; true; end - end.new + def noop; end + end.new + end + + its(:stdout) { will output("some output").to_stdout } + its(:stderr) { will output("some error").to_stderr } + + its(:noop) { will_not output("some error").to_stderr } + its(:noop) { will_not output("some output").to_stdout } + end + + context "#will with non block expectations" do + subject do + Class.new do + def terminator + "back" end + end.new + end - def self.example(*_args) - raise + its(:terminator) do + expect { will be("back") }.to \ + raise_error(ArgumentError, '`will` only supports block expectations') + end + + its(:terminator) do + expect { will_not be("back") }.to \ + raise_error(ArgumentError, '`will_not` only supports block expectations') + end + end + + context "when example is redefined" do + subject do + Class.new do + def will_still_work + true end + end.new + end - its(:will_still_work) { is_expected.to be true } - end + def self.example(*_args) + raise end + + its(:will_still_work) { is_expected.to be true } end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3433ee9..20e85f8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,12 +1,19 @@ +# frozen_string_literal: true + require 'rspec/its' -Dir['./spec/support/**/*'].each {|f| require f} +Dir['./spec/support/**/*'].each { |f| require f } class NullFormatter private + def method_missing(method, *args, &block) # ignore end + + def respond_to_missing?(method, *args, &block) + # ignore + end end RSpec.configure do |config|