diff --git a/.rspec b/.rspec index 32fe547d9d..eb8599d92e 100644 --- a/.rspec +++ b/.rspec @@ -1,2 +1,3 @@ --order rand --warnings +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml index 4dc0e01e8a..368bdfb4d4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -11,7 +11,7 @@ ClassLength: # This should go down over time. LineLength: - Max: 180 + Max: 130 Lint/HandleExceptions: Exclude: @@ -25,7 +25,7 @@ Lint/LiteralInInterpolation: # This should go down over time. MethodLength: - Max: 152 + Max: 155 # Exclude the default spec_helper to make it easier to uncomment out # default settings (for both users and the Cucumber suite). diff --git a/.rubocop_rspec_base.yml b/.rubocop_rspec_base.yml index 5a03e132a1..f7bea1c203 100644 --- a/.rubocop_rspec_base.yml +++ b/.rubocop_rspec_base.yml @@ -1,4 +1,4 @@ -# This file was generated on 2014-08-23T21:27:12-07:00 from the rspec-dev repo. +# This file was generated on 2015-01-07T22:08:46-08: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 371495bd64..ea4f1b0fcc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,11 @@ -# This file was generated on 2014-08-23T21:27:12-07:00 from the rspec-dev repo. +# This file was generated on 2015-01-07T22:08:46-08:00 from the rspec-dev repo. # DO NOT modify it by hand as your changes will get lost the next time it is generated. language: ruby sudo: false +cache: + directories: + - ../bundle before_install: - "script/clone_all_rspec_repos" # Downgrade bundler to work around https://github.com/bundler/bundler/issues/3004 @@ -15,9 +18,8 @@ rvm: - 1.9.2 - 1.9.3 - 2.0.0 - - 2.1.0 - - 2.1.1 - - 2.1.2 + - 2.1 + - 2.2 - ruby-head - ree - jruby-18mode diff --git a/.yardopts b/.yardopts index 0feceb4a30..0ab6943666 100644 --- a/.yardopts +++ b/.yardopts @@ -3,5 +3,6 @@ --markup markdown --default-return void - +Filtering.md Changelog.md License.txt diff --git a/Changelog.md b/Changelog.md index e64fd88cf6..68be1747e3 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,169 @@ +### 3.2.0 / 2015-02-03 +[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.1.7...v3.2.0) + +Enhancements: + +* Improve the `inspect` output of example groups. (Mike Dalton, #1687) +* When rake task fails, only output the command if `verbose` flag is + set. (Ben Snape, #1704) +* Add `RSpec.clear_examples` as a clear way to reset examples in between + spec runs, whilst retaining user configuration. (Alexey Fedorov, #1706) +* Reduce string allocations when defining and running examples by 70% + and 50% respectively. (Myron Marston, #1738) +* Removed dependency on pathname from stdlib. (Sam Phippen, #1703) +* Improve the message presented when a user hits Ctrl-C. + (Alex Chaffee #1717, #1742) +* Improve shared example group inclusion backtrace displayed + in failed example output so that it works for all methods + of including shared example groups and shows all inclusion + locations. (Myron Marston, #1763) +* Issue seed notification at start (as well as the end) of the reporter + run. (Arlandis Word, #1761) +* Improve the documentation of around hooks. (Jim Kingdon, #1772) +* Support prepending of modules into example groups from config and allow + filtering based on metadata. (Arlandis Word, #1806) +* Emit warnings when `:suite` hooks are registered on an example group + (where it has always been ignored) or are registered with metadata + (which has always been ignored). (Myron Marston, #1805) +* Provide a friendly error message when users call RSpec example group + APIs (e.g. `context`, `describe`, `it`, `let`, `before`, etc) from + within an example where those APIs are unavailable. (Myron Marston, #1819) +* Provide a friendly error message when users call RSpec example + APIs (e.g. `expect`, `double`, `stub_const`, etc) from + within an example group where those APIs are unavailable. + (Myron Marston, #1819) +* Add new `RSpec::Core::Sandbox.sandboxed { }` API that facilitates + testing RSpec with RSpec, allowing you to define example groups + and example from within an example without affecting the global + `RSpec.world` state. (Tyler Ball, 1808) +* Apply line-number filters only to the files they are scoped to, + allowing you to mix filtered and unfiltered files. (Myron Marston, #1839) +* When dumping pending examples, include the failure details so that you + don't have to un-pend the example to see it. (Myron Marston, #1844) +* Make `-I` option support multiple values when separated by + `File::PATH_SEPARATOR`, such as `rspec -I foo:bar`. This matches + the behavior of Ruby's `-I` option. (Fumiaki Matsushima, #1855). + +Bug Fixes: + +* When assigning generated example descriptions, surface errors + raised by `matcher.description` in the example description. + (Myron Marston, #1771) +* Don't consider expectations from `after` hooks when generating + example descriptions. (Myron Marston, #1771) +* Don't apply metadata-filtered config hooks to examples in groups + with matching metadata when those examples override the parent + metadata value to not match. (Myron Marston, #1796) +* Fix `config.expect_with :minitest` so that `skip` uses RSpec's + implementation rather than Minitest's. (Jonathan Rochkind, #1822) +* Fix `NameError` caused when duplicate example group aliases are defined and + the DSL is not globally exposed. (Aaron Kromer, #1825) +* When a shared example defined in an external file fails, use the host + example group (from a loaded spec file) for the re-run command to + ensure the command will actually work. (Myron Marston, #1835) +* Fix location filtering to work properly for examples defined in + a nested example group within a shared example group defined in + an external file. (Bradley Schaefer, Xavier Shay, Myron Marston, #1837) +* When a pending example fails (as expected) due to a mock expectation, + set `RSpec::Core::Example::ExecutionResult#pending_exception` -- + previously it was not being set but should have been. (Myron Marston, #1844) +* Fix rake task to work when `rspec-core` is installed in a directory + containing a space. (Guido Günther, #1845) +* Fix regression in 3.1 that caused `describe Regexp` to raise errors. + (Durran Jordan, #1853) +* Fix regression in 3.x that caused the profile information to be printed + after the summary. (Max Lincoln, #1857) +* Apply `--seed` before loading `--require` files so that required files + can access the provided seed. (Myron Marston, #1745) +* Handle `RSpec::Core::Formatters::DeprecationFormatter::FileStream` being + reopened with an IO stream, which sometimes happens with spring. + (Kevin Mook, #1757) + +### 3.1.7 / 2014-10-11 +[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.1.6...v3.1.7) + +Bug Fixes: + +* Fix `Metadata.relative_path` so that for a current directory of + `/foo/bar`, `/foo/bar_1` is not wrongly converted to `._1`. + (Akos Vandra, #1730) +* Prevent constant lookup mistakenly finding `RSpec::ExampleGroups` generated + constants on 1.9.2 by appending a trailing `_` to the generated names. + (Jon Rowe, #1737) +* Fix bug in `:pending` metadata. If it got set in any way besides passing + it as part of the metadata literal passed to `it` (such as by using + `define_derived_metadata`), it did not have the desired effect, + instead marking the example as `:passed`. (Myron Marston, #1739) + +### 3.1.6 / 2014-10-08 +[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.1.5...v3.1.6) + +Bug Fixes: + +* Fix regression in rake task pattern handling, that prevented patterns + that were relative from the current directory rather than from `spec` + from working properly. (Myron Marston, #1734) +* Prevent rake task from generating duplicate load path entries. + (Myron Marston, #1735) + +### 3.1.5 / 2014-09-29 +[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.1.4...v3.1.5) + +Bug Fixes: + +* Fix issue with the rake task incorrectly escaping strings on Windows. + (Jon Rowe #1718) +* Support absolute path patterns. While this wasn't officially supported + previously, setting `rake_task.pattern` to an absolute path pattern in + RSpec 3.0 and before worked since it delegated to `FileList` internally + (but now just forwards the pattern on to the `rspec` command). + (Myron Marston, #1726) + +### 3.1.4 / 2014-09-18 +[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.1.3...v3.1.4) + +Bug Fixes: + +* Fix implicit `subject` when using `describe false` or `describe nil` + so that it returns the provided primitive rather than the string + representation. (Myron Marston, #1710) +* Fix backtrace filtering to allow code in subdirectories of your + current working directory (such as vendor/bundle/...) to be filtered + from backtraces. (Myron Marston, #1708) + +### 3.1.3 / 2014-09-15 +[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.1.2...v3.1.3) + +Bug Fixes: + +* Fix yet another regression in rake task pattern handling, to allow + `task.pattern = FileList["..."]` to work. That was never intended + to be supported but accidentally worked in 3.0 and earlier. + (Myron Marston, #1701) +* Fix pattern handling so that files are normalized to absolute paths + before subtracting the `--exclude-pattern` matched files from the + `--pattern` matched files so that it still works even if the patterns + are in slightly different forms (e.g. one starting with `./`). + (Christian Nelson, #1698) + +### 3.1.2 / 2014-09-08 +[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.1.1...v3.1.2) + +Bug Fixes: + +* Fix another regression in rake task pattern handling, so that patterns + that start with `./` still work. (Christian Nelson, #1696) + +### 3.1.1 / 2014-09-05 +[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.1.0...v3.1.1) + +Bug Fixes: + +* Fix a regression in rake task pattern handling, so that `rake_task.pattern = array` + works again. While we never intended to support array values (or even knew that worked!), + the implementation from 3.0 and earlier used `FileList` internally, which allows arrays. + The fix restores the old behavior. (Myron Marston, #1694) + ### 3.1.0 / 2014-09-04 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.0.4...v3.1.0) @@ -371,6 +537,9 @@ Bug Fixes: directory (was broken in beta1). (Jon Rowe) * Prevent RSpec mangling file names that have substrings containing `line_number` or `default_path`. (Matijs van Zuijlen) +* Fix failure line detection so that it handles relative file paths + (which can happen when running specs through `ruby` using `rspec/autorun`). + (Myron Marston, #1829) ### 3.0.0.beta1 / 2013-11-07 [Full Changelog](http://github.com/rspec/rspec-core/compare/v2.99.1...v3.0.0.beta1) diff --git a/Filtering.md b/Filtering.md new file mode 100644 index 0000000000..5c00bdd18a --- /dev/null +++ b/Filtering.md @@ -0,0 +1,161 @@ +# Filtering + +RSpec supports filtering examples and example groups in multiple ways, +allowing you to run a targeted subset of your suite that you are +currently interested in. + +## Filtering by Tag + +Examples and groups can be filtered by matching tags declared on +the command line or options files, or filters declared via +`RSpec.configure`, with hash key/values submitted within example group +and/or example declarations. For example, given this declaration: + +``` ruby +RSpec.describe Thing, :awesome => true do + it "does something" do + # ... + end +end +``` + +That group (or any other with `:awesome => true`) would be filtered in +with any of the following commands: + + rspec --tag awesome:true + rspec --tag awesome + rspec -t awesome:true + rspec -t awesome + +Prefixing the tag names with `~` negates the tags, thus excluding this +group with any of: + + rspec --tag ~awesome:true + rspec --tag ~awesome + rspec -t ~awesome:true + rspec -t ~awesome + +## Filtering by Example description + +RSpec provides the `--example` (short form: `-e`) option to allow you to +select examples or groups by their description. All loaded examples +whose full description (computed based on the description of the example +plus that of all ancestor groups) contains the provided argument will be +executed. + + rspec --example "Homepage when logged in" + rspec -e "Homepage when logged in" + +You can specify this option multiple times to select multiple sets of examples: + + rspec -e "Homepage when logged in" -e "User" + +Note that RSpec will load all spec files in these situations, which can +incur considerable start-up costs (particularly for Rails apps). If you +know that the examples you are targeting are in particular files, you can +also pass the file or directory name so that RSpec loads only those spec +files, speeding things up: + + rspec spec/homepage_spec.rb -e "Homepage when logged in" + rspec -e "Homepage when logged in" spec/homepage_spec.rb + +Note also that description-less examples that have generated descriptions +(typical when using the one-liner syntax) cannot be directly filtered with +this option, because it is necessary to execute the example to generate the +description, so RSpec is unable to use the not-yet-generated description to +decide whether or not to execute an example. You can, of course, pass part +of a group's description to select all examples defined in the group +(including those that have no description). + +## Filtering by Example Location + +Examples and groups can be selected from the command line by passing the +file and line number where they are defined, separated by a colon: + + rspec spec/homepage_spec.rb:14 spec/widgets_spec.rb:40 spec/users_spec.rb + +This command would run the example or group defined on line 14 of +`spec/homepage_spec.rb`, the example or group defined on line 40 of +`spec/widgets_spec.rb`, and all examples and groups defined in +`spec/users_spec.rb`. + +If there is no example or group defined at the specified line, RSpec +will run the last example or group defined before the line. + +## Focusing + +RSpec supports configuration options that make it easy to select +examples by temporarily tweaking them. In your `spec_helper.rb` (or +a similar file), put this configuration: + +``` ruby +RSpec.configure do |config| + config.filter_run :focus + config.run_all_when_everything_filtered = true +end +``` + +This configuration is generated for you by `rspec --init` in the +commented-out section of recommendations. With that in place, you +can tag any example group or example with `:focus` metadata to +select it: + +``` ruby +it "does something" do +# becomes... +it "does something", :focus do +``` + +RSpec also ships with aliases of the common example group definition +methods (`describe`, `context`) and example methods (`it`, `specify`, +`example`) with an `f` prefix that automatically includes `:focus => +true` metadata, allowing you to easily change `it` to `fit` (think +"focused it"), `describe` to `fdescribe`, etc in order to temporarily +focus them. + +## Options files and command line overrides + +Command line option declarations can be stored in `.rspec`, `~/.rspec`, or a custom +options file. This is useful for storing defaults. For example, let's +say you've got some slow specs that you want to suppress most of the +time. You can tag them like this: + +``` ruby +RSpec.describe Something, :slow => true do +``` + +And then store this in `.rspec`: + + --tag ~slow:true + +Now when you run `rspec`, that group will be excluded. + +## Overriding + +Of course, you probably want to run them sometimes, so you can override +this tag on the command line like this: + + rspec --tag slow:true + +## Precedence + +Location and description filters have priority over tag filters since +they express a desire by the user to run specific examples. Thus, you +could specify a location or description at the command line to run an +example or example group that would normally be excluded due to a +`:slow` tag if you were using the above configuration. + +## RSpec.configure + +You can also store default tags with `RSpec.configure`. We use `tag` on +the command line (and in options files like `.rspec`), but for historical +reasons we use the term `filter` in `RSpec.configure`: + +``` ruby +RSpec.configure do |c| + c.filter_run_including :foo => :bar + c.filter_run_excluding :foo => :bar +end +``` + +These declarations can also be overridden from the command line. diff --git a/Gemfile b/Gemfile index 6e7ae838b0..7726d771cc 100644 --- a/Gemfile +++ b/Gemfile @@ -30,5 +30,6 @@ end gem 'simplecov', '~> 0.8' gem 'rubocop', "~> 0.23.0", :platform => [:ruby_19, :ruby_20, :ruby_21] +gem 'test-unit', '~> 3.0' if RUBY_VERSION.to_f >= 2.2 eval File.read('Gemfile-custom') if File.exist?('Gemfile-custom') diff --git a/README.md b/README.md index 3cd7e7290f..d47f17b39c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# rspec-core [![Build Status](https://secure.travis-ci.org/rspec/rspec-core.png?branch=master)](http://travis-ci.org/rspec/rspec-core) [![Code Climate](https://codeclimate.com/github/rspec/rspec-core.png)](https://codeclimate.com/github/rspec/rspec-core) +# rspec-core [![Build Status](https://secure.travis-ci.org/rspec/rspec-core.svg?branch=master)](http://travis-ci.org/rspec/rspec-core) [![Code Climate](https://codeclimate.com/github/rspec/rspec-core.svg)](https://codeclimate.com/github/rspec/rspec-core) rspec-core provides the structure for writing executable examples of how your code should behave, and an `rspec` command with tools to constrain which @@ -10,6 +10,15 @@ examples get run and tailor the output. gem install rspec-core # for rspec-core only rspec --help +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 => "git://github.com/rspec/#{lib}.git", :branch => 'master' +end +``` + ## basic structure RSpec uses the words "describe" and "it" so we can express concepts like a conversation: diff --git a/Rakefile b/Rakefile index 1c9210b9d0..550f874261 100644 --- a/Rakefile +++ b/Rakefile @@ -43,17 +43,34 @@ task :rdoc do sh "yardoc" end +with_changelog_in_features = lambda do |&block| + begin + sh "cp Changelog.md features/" + block.call + ensure + sh "rm features/Changelog.md" + end +end + desc "Push docs/cukes to relishapp using the relish-client-gem" task :relish, :version do |_t, args| raise "rake relish[VERSION]" unless args[:version] - sh "cp Changelog.md features/" - if `relish versions rspec/rspec-core`.split.map(&:strip).include? args[:version] - puts "Version #{args[:version]} already exists" - else - sh "relish versions:add rspec/rspec-core:#{args[:version]}" + + with_changelog_in_features.call do + if `relish versions rspec/rspec-core`.split.map(&:strip).include? args[:version] + puts "Version #{args[:version]} already exists" + else + sh "relish versions:add rspec/rspec-core:#{args[:version]}" + end + sh "relish push rspec/rspec-core:#{args[:version]}" + end +end + +desc "Push to relish staging environment" +task :relish_staging do + with_changelog_in_features.call do + sh "relish push rspec-staging/rspec-core" end - sh "relish push rspec/rspec-core:#{args[:version]}" - sh "rm features/Changelog.md" end task :default => [:spec, :cucumber, :rubocop] diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000000..9e4f457e30 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,34 @@ +# This file was generated on 2015-01-07T22:08:46-08: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}" + +# This will build all PRs targetting matching branches. +# Without this, each PR builds twice -- once for the PR branch HEAD, +# and once for the merge commit that github creates for each mergable PR. +branches: + only: + - master + - /.*-maintenance$/ + +# Disable normal Windows builds in favor of our test script. +build: off + +install: + - SET PATH=C:\Ruby%ruby_version%\bin;%PATH% + - ruby --version + - gem --version + # We lock to 1.7.7 to avoid warnings from 1.7.8 + - gem install bundler -v=1.7.7 + - bundler --version + - bundle install + - cinst ansicon + +test_script: + - bundle exec rspec + +environment: + matrix: + # ruby_version: '20' doesn't work for some reason + - ruby_version: '193' + - ruby_version: '21' diff --git a/benchmarks/allocations/1000_groups_1_example.rb b/benchmarks/allocations/1000_groups_1_example.rb new file mode 100644 index 0000000000..756a2e0599 --- /dev/null +++ b/benchmarks/allocations/1000_groups_1_example.rb @@ -0,0 +1,124 @@ +require_relative "helper" + +benchmark_allocations do + 1000.times do |i| + RSpec.describe "group #{i}" do + it "has one example" do + end + end + end +end + +__END__ + +Original allocations: + + class_plus count +---------------------------------------- ----- +String 28000 +Array 15000 +RubyVM::Env 9000 +Proc 9000 +Hash 9000 +RSpec::Core::Hooks::HookCollection 6000 +Array 5000 +MatchData 3000 +Array 2000 +Array 2000 +Module 2000 +RSpec::Core::Example::ExecutionResult 2000 +RSpec::Core::Metadata::ExampleGroupHash 1000 +Class 1000 +Array 1000 +RSpec::Core::Hooks::AroundHookCollection 1000 +RSpec::Core::Hooks::HookCollections 1000 +RSpec::Core::Metadata::ExampleHash 1000 +RSpec::Core::Example 1000 +Array 1000 + + +After removing `:suite` support from `Hooks` module, +it cut Array and RSpec::Core::Hooks::HookCollection +allocations by 2000 each: + + class_plus count +---------------------------------------- ----- +String 28000 +Array 13000 +Proc 9000 +RubyVM::Env 9000 +Hash 9000 +Array 5000 +RSpec::Core::Hooks::HookCollection 4000 +MatchData 3000 +Array 2000 +RSpec::Core::Example::ExecutionResult 2000 +Module 2000 +Array 2000 +RSpec::Core::Hooks::HookCollections 1000 +RSpec::Core::Example 1000 +Array 1000 +RSpec::Core::Metadata::ExampleHash 1000 +RSpec::Core::Hooks::AroundHookCollection 1000 +RSpec::Core::Metadata::ExampleGroupHash 1000 +Class 1000 +Array 1000 + +.... + +Later, our allocations were: + + class_plus count +--------------------------------------- ----- +String 26000 +Hash 19000 +Array 18000 +Set 10000 +Proc 9000 +RubyVM::Env 9000 +RSpec::Core::Hooks::HookCollection 5000 +RSpec::Core::FilterableItemRepository 5000 +Array 5000 +MatchData 3000 +RSpec::Core::Example::ExecutionResult 2000 +Array 2000 +Module 2000 +Array 2000 +RSpec::Core::Metadata::ExampleGroupHash 1000 +Class 1000 +RSpec::Core::Metadata::ExampleHash 1000 +RSpec::Core::Example 1000 +Array 1000 +Array 1000 +RSpec::Core::Hooks::HookCollections 1000 + + +After changing the hooks implementation to lazily +instantiate `HookCollection` instances, it dropped +our allocations by: + - 8K hashes + - 10K arrays + - 10K sets + - 5K FilterableItemRepository + - 5K HookCollecion + + class_plus count +--------------------------------------- ----- +String 26000 +Hash 11000 +Array 8000 +Proc 5000 +RubyVM::Env 5000 +Array 5000 +MatchData 3000 +Array 2000 +Array 2000 +RSpec::Core::Example::ExecutionResult 2000 +Module 2000 +Array 1000 +RSpec::Core::Metadata::ExampleGroupHash 1000 +Class 1000 +RSpec::Core::Metadata::ExampleHash 1000 +RSpec::Core::Example 1000 +Array 1000 +RSpec::Core::Hooks::HookCollections 1000 diff --git a/benchmarks/allocations/1_group_1000_examples.rb b/benchmarks/allocations/1_group_1000_examples.rb new file mode 100644 index 0000000000..c058abd86a --- /dev/null +++ b/benchmarks/allocations/1_group_1000_examples.rb @@ -0,0 +1,63 @@ +require_relative "helper" + +benchmark_allocations do + RSpec.describe "one example group" do + 1000.times do |i| + example "example #{i}" do + end + end + end +end + +__END__ + +Original stats: + + class_plus count +---------------------------------------- ----- +String 22046 +Hash 3006 +Array 3002 +Proc 2007 +RubyVM::Env 2007 +Array 1013 +Regexp 1001 +RSpec::Core::Example::ExecutionResult 1001 +Array 1001 +RSpec::Core::Example 1000 +RSpec::Core::Metadata::ExampleHash 1000 +RSpec::Core::Hooks::HookCollection 6 +MatchData 4 +Array 2 +Module 2 +RSpec::Core::Metadata::ExampleGroupHash 1 +RSpec::Core::Hooks::AroundHookCollection 1 +Class 1 +Array 1 +RSpec::Core::Hooks::HookCollections 1 +Array 1 + +After my fixes: + + class_plus count +---------------------------------------- ----- +String 6030 +Hash 3006 +Array 3002 +RubyVM::Env 2007 +Proc 2007 +Array 1013 +RSpec::Core::Example::ExecutionResult 1001 +Array 1001 +RSpec::Core::Metadata::ExampleHash 1000 +RSpec::Core::Example 1000 +RSpec::Core::Hooks::HookCollection 6 +MatchData 4 +Module 2 +Array 2 +RSpec::Core::Hooks::HookCollections 1 +Array 1 +RSpec::Core::Hooks::AroundHookCollection 1 +RSpec::Core::Metadata::ExampleGroupHash 1 +Class 1 +Array 1 diff --git a/benchmarks/allocations/helper.rb b/benchmarks/allocations/helper.rb new file mode 100644 index 0000000000..644f233f33 --- /dev/null +++ b/benchmarks/allocations/helper.rb @@ -0,0 +1,30 @@ +$LOAD_PATH.unshift File.expand_path("../../../lib", __FILE__) +require 'rspec/core' +require 'allocation_stats' + +def benchmark_allocations(burn: 1, min_allocations: 0) + stats = AllocationStats.new(burn: burn).trace do + yield + end + + columns = if ENV['DETAIL'] + [:sourcefile, :sourceline, :class_plus] + else + [:class_plus] + end + + results = stats.allocations(alias_paths: true).group_by(*columns).from_pwd.sort_by_size.to_text + count_regex = /\s+(\d+)\z/ + + total_objects = results.split("\n").map { |line| line[count_regex, 1] }.compact.map { |c| Integer(c) }.inject(0, :+) + + filtered = results.split("\n").select do |line| + count = line[count_regex, 1] + count.nil? || Integer(count) >= min_allocations + end + + puts filtered.join("\n") + line_length = filtered.last.length + puts "-" * line_length + puts "Total:#{total_objects.to_s.rjust(line_length - "Total:".length)}" +end diff --git a/benchmarks/allocations/running_1000_groups_1_example.rb b/benchmarks/allocations/running_1000_groups_1_example.rb new file mode 100644 index 0000000000..b340f6d1f0 --- /dev/null +++ b/benchmarks/allocations/running_1000_groups_1_example.rb @@ -0,0 +1,100 @@ +require_relative "helper" + +1000.times do |i| + RSpec.describe "group #{i}" do + it "has one example" do + end + end +end + +benchmark_allocations(burn: 0, min_allocations: 50) do + RSpec::Core::Runner.run([]) +end + +__END__ + +Before optimization: + + class_plus count +----------------------------------------------------------------------------------------------- ----- +Array 26021 +String 21331 +Array 19402 +Array 6001 +Array 6001 +RSpec::Core::Hooks::HookCollection 4004 +Array 4004 +Hash 3098 +Proc 3096 +RubyVM::Env 3056 +Time 2002 +Random 2001 +RSpec::Core::Hooks::AroundHookCollection 2000 +RSpec::Core::Notifications::GroupNotification 2000 +RSpec::Core::Notifications::ExampleNotification 2000 +RSpec::Core::Hooks::GroupHookCollection 2000 +Array 1003 +Array 1002 +Array 1002 +RSpec::Core::Example::Procsy 1000 +RubyVM::InstructionSequence 506 +Array 391 +Array 205 +Array 52 + + +After optimization, we allocate 2000 less arrays and 2000 less RSpec::Core::Hooks::HookCollection +instances. That's 2 less of each per example group. + + class_plus count +----------------------------------------------------------------------------------------------- ----- +Array 26021 +String 21331 +Array 17400 +Array 6001 +Array 6001 +Array 4004 +Hash 3098 +Proc 3096 +RubyVM::Env 3056 +RSpec::Core::Hooks::HookCollection 2002 +Time 2002 +Random 2001 +RSpec::Core::Notifications::ExampleNotification 2000 +RSpec::Core::Notifications::GroupNotification 2000 +RSpec::Core::Hooks::GroupHookCollection 2000 +Array 1003 +Array 1002 +Array 1002 +RSpec::Core::Example::Procsy 1000 +RSpec::Core::Hooks::AroundHookCollection 1000 +RubyVM::InstructionSequence 506 +Array 391 +Array 205 +Array 52 + +After yet further optimization (where HookCollection instances are only created when hooks are added), +we've reduced allocations significantly further: + + class_plus count +----------------------------------------------------------------------------------------------- ----- +String 21332 +Array 13412 +Array 6021 +Array 6001 +Array 6001 +Hash 3105 +Array 3004 +Proc 2101 +RubyVM::Env 2061 +Time 2002 +Random 2001 +RSpec::Core::Notifications::GroupNotification 2000 +RSpec::Core::Notifications::ExampleNotification 2000 +Array 1003 +Array 1002 +Array 1002 +RubyVM::InstructionSequence 506 +Array 391 +Array 208 +Array 52 diff --git a/benchmarks/allocations/running_1_group_1000_examples.rb b/benchmarks/allocations/running_1_group_1000_examples.rb new file mode 100644 index 0000000000..506394a3be --- /dev/null +++ b/benchmarks/allocations/running_1_group_1000_examples.rb @@ -0,0 +1,60 @@ +require_relative "helper" + +RSpec.describe "one example group" do + 1000.times do |i| + example "example #{i}" do + end + end +end + +benchmark_allocations(burn: 0) do + RSpec::Core::Runner.run([]) +end + +__END__ + +Original allocations: + + class_plus count +----------------------------------------------------------------------------------------------- ----- +String 35018 +Array 14030 +Array 12075 +RSpec::Core::Hooks::HookCollection 4000 +Time 2002 +Array 2000 +RSpec::Core::Hooks::AroundHookCollection 2000 +RSpec::Core::Notifications::ExampleNotification 2000 +Proc 1065 +RubyVM::Env 1018 +Array 1006 +Array 1005 +RSpec::ExampleGroups::OneExampleGroup 1002 +Array 67 +RubyVM::InstructionSequence 41 +Hash 35 +Set 30 +File 6 + +After my change: + + class_plus count +----------------------------------------------------------------------------------------------- ----- +Array 14030 +String 12967 +Array 12075 +RSpec::Core::Hooks::HookCollection 4000 +Time 2002 +RSpec::Core::Notifications::ExampleNotification 2000 +Array 2000 +RSpec::Core::Hooks::AroundHookCollection 2000 +Proc 1065 +RubyVM::Env 1018 +Array 1006 +Array 1005 +RSpec::ExampleGroups::OneExampleGroup 1002 +Array 67 +RubyVM::InstructionSequence 41 +Hash 35 +Set 30 +File 6 diff --git a/benchmarks/capture_block_vs_yield.rb b/benchmarks/capture_block_vs_yield.rb new file mode 100644 index 0000000000..2705527678 --- /dev/null +++ b/benchmarks/capture_block_vs_yield.rb @@ -0,0 +1,208 @@ +require 'benchmark/ips' + +def yield_control + yield +end + +def capture_block_and_yield(&block) + yield +end + +def capture_block_and_call(&block) + block.call +end + +puts "Using the block directly" + +Benchmark.ips do |x| + x.report("yield ") do + yield_control { } + end + + x.report("capture block and yield") do + capture_block_and_yield { } + end + + x.report("capture block and call ") do + capture_block_and_call { } + end +end + +puts "Forwarding the block to another method" + +def tap_with_yield + 5.tap { |i| yield i } +end + +def tap_with_forwarded_block(&block) + 5.tap(&block) +end + +Benchmark.ips do |x| + x.report("tap { |i| yield i }") do + tap_with_yield { |i| } + end + + x.report("tap(&block) ") do + tap_with_forwarded_block { |i| } + end +end + +def yield_n_times(n) + n.times { yield } +end + +def forward_block_to_n_times(n, &block) + n.times(&block) +end + +def call_block_n_times(n, &block) + n.times { block.call } +end + +[10, 25, 50, 100, 1000, 10000].each do |count| + puts "Invoking the block #{count} times" + + Benchmark.ips do |x| + x.report("#{count}.times { yield } ") do + yield_n_times(count) { } + end + + x.report("#{count}.times(&block) ") do + forward_block_to_n_times(count) { } + end + + x.report("#{count}.times { block.call }") do + call_block_n_times(count) { } + end + end +end + +__END__ + +This benchmark demonstrates that capturing a block (e.g. `&block`) has +a high constant cost, taking about 5x longer than a single `yield` +(even if the block is never used!). + +However, fowarding a captured block can be faster than using `yield` +if the block is used many times (the breakeven point is at about 20-25 +invocations), so it appears that he per-invocation cost of `yield` +is higher than that of a captured-and-forwarded block. + +Note that there is no circumstance where using `block.call` is faster. + +See also `flat_map_vs_inject.rb`, which appears to contradict these +results a little bit. + +Using the block directly +Calculating ------------------------------------- +yield + 91.539k i/100ms +capture block and yield + 50.945k i/100ms +capture block and call + 50.923k i/100ms +------------------------------------------------- +yield + 4.757M (± 6.0%) i/s - 23.709M +capture block and yield + 1.112M (±20.7%) i/s - 5.349M +capture block and call + 964.475k (±20.3%) i/s - 4.634M +Forwarding the block to another method +Calculating ------------------------------------- + tap { |i| yield i } 74.620k i/100ms + tap(&block) 51.382k i/100ms +------------------------------------------------- + tap { |i| yield i } 3.213M (± 6.3%) i/s - 16.043M + tap(&block) 970.418k (±18.6%) i/s - 4.727M +Invoking the block 10 times +Calculating ------------------------------------- +10.times { yield } + 49.151k i/100ms +10.times(&block) + 40.682k i/100ms +10.times { block.call } + 27.576k i/100ms +------------------------------------------------- +10.times { yield } + 908.673k (± 4.9%) i/s - 4.571M +10.times(&block) + 674.565k (±16.1%) i/s - 3.336M +10.times { block.call } + 385.056k (±10.3%) i/s - 1.930M +Invoking the block 25 times +Calculating ------------------------------------- +25.times { yield } + 29.874k i/100ms +25.times(&block) + 30.934k i/100ms +25.times { block.call } + 17.119k i/100ms +------------------------------------------------- +25.times { yield } + 416.342k (± 3.6%) i/s - 2.091M +25.times(&block) + 446.108k (±10.6%) i/s - 2.227M +25.times { block.call } + 201.264k (± 7.2%) i/s - 1.010M +Invoking the block 50 times +Calculating ------------------------------------- +50.times { yield } + 17.690k i/100ms +50.times(&block) + 21.760k i/100ms +50.times { block.call } + 9.961k i/100ms +------------------------------------------------- +50.times { yield } + 216.195k (± 5.7%) i/s - 1.079M +50.times(&block) + 280.217k (± 9.9%) i/s - 1.393M +50.times { block.call } + 112.754k (± 5.6%) i/s - 567.777k +Invoking the block 100 times +Calculating ------------------------------------- +100.times { yield } + 10.143k i/100ms +100.times(&block) + 13.688k i/100ms +100.times { block.call } + 5.551k i/100ms +------------------------------------------------- +100.times { yield } + 111.700k (± 3.6%) i/s - 568.008k +100.times(&block) + 163.638k (± 7.7%) i/s - 821.280k +100.times { block.call } + 58.472k (± 5.6%) i/s - 294.203k +Invoking the block 1000 times +Calculating ------------------------------------- +1000.times { yield } + 1.113k i/100ms +1000.times(&block) + 1.817k i/100ms +1000.times { block.call } + 603.000 i/100ms +------------------------------------------------- +1000.times { yield } + 11.156k (± 8.4%) i/s - 56.763k +1000.times(&block) + 18.551k (±10.1%) i/s - 92.667k +1000.times { block.call } + 6.206k (± 3.5%) i/s - 31.356k +Invoking the block 10000 times +Calculating ------------------------------------- +10000.times { yield } + 113.000 i/100ms +10000.times(&block) + 189.000 i/100ms +10000.times { block.call } + 61.000 i/100ms +------------------------------------------------- +10000.times { yield } + 1.150k (± 3.6%) i/s - 5.763k +10000.times(&block) + 1.896k (± 6.9%) i/s - 9.450k +10000.times { block.call } + 624.401 (± 3.0%) i/s - 3.172k diff --git a/benchmarks/flat_map_vs_inject.rb b/benchmarks/flat_map_vs_inject.rb new file mode 100644 index 0000000000..6a99d190f1 --- /dev/null +++ b/benchmarks/flat_map_vs_inject.rb @@ -0,0 +1,55 @@ +require 'benchmark/ips' + +words = %w[ foo bar bazz big small medium large tiny less more good bad mediocre ] + +def flat_map_using_yield(array) + array.flat_map { |item| yield item } +end + +def flat_map_using_block(array, &block) + array.flat_map(&block) +end + +Benchmark.ips do |x| + x.report("flat_map") do + words.flat_map(&:codepoints) + end + + x.report("inject (+)") do + words.inject([]) { |a, w| a + w.codepoints } + end + + x.report("inject (concat)") do + words.inject([]) { |a, w| a.concat w.codepoints } + end + + x.report("flat_map_using_yield") do + flat_map_using_yield(words, &:codepoints) + end + + x.report("flat_map_using_block") do + flat_map_using_block(words, &:codepoints) + end +end + +__END__ + +Surprisingly, `flat_map(&block)` appears to be faster than +`flat_map { yield }` in spite of the fact that our array here +is smaller than the break-even point of 20-25 measured in the +`capture_block_vs_yield.rb` benchmark. In fact, the forwaded-block +version remains faster in my benchmarks here no matter how small +I shrink the `words` array. I'm not sure why! + +Calculating ------------------------------------- + flat_map 10.594k i/100ms + inject (+) 8.357k i/100ms + inject (concat) 10.404k i/100ms +flat_map_using_yield 10.081k i/100ms +flat_map_using_block 11.683k i/100ms +------------------------------------------------- + flat_map 136.442k (±10.4%) i/s - 678.016k + inject (+) 98.024k (± 9.7%) i/s - 493.063k + inject (concat) 119.822k (±10.5%) i/s - 593.028k +flat_map_using_yield 112.284k (± 9.7%) i/s - 564.536k +flat_map_using_block 134.533k (± 6.3%) i/s - 677.614k diff --git a/benchmarks/module_inclusion_filtering.rb b/benchmarks/module_inclusion_filtering.rb new file mode 100644 index 0000000000..eaeb5f01c6 --- /dev/null +++ b/benchmarks/module_inclusion_filtering.rb @@ -0,0 +1,89 @@ +require_relative "../bundle/bundler/setup" # configures load paths +require 'rspec/core' + +class << RSpec + attr_writer :world +end + +# Here we are restoring the old implementation of `configure_group`, so that +# we can toggle the new vs old implementation in the benchmark by aliasing it. +module RSpecConfigurationOverrides + def initialize(*args) + super + @include_extend_or_prepend_modules = [] + end + + def include(mod, *filters) + meta = RSpec::Core::Metadata.build_hash_from(filters, :warn_about_example_group_filtering) + @include_extend_or_prepend_modules << [:include, mod, meta] + super + end + + def old_configure_group(group) + @include_extend_or_prepend_modules.each do |include_extend_or_prepend, mod, filters| + next unless filters.empty? || RSpec::Core::MetadataFilter.apply?(:any?, filters, group.metadata) + __send__("safe_#{include_extend_or_prepend}", mod, group) + end + end + + def self.prepare_implementation(prefix) + RSpec.world = RSpec::Core::World.new # clear our state + RSpec::Core::Configuration.class_eval do + alias_method :configure_group, :"#{prefix}_configure_group" + end + end +end + +RSpec::Core::Configuration.class_eval do + prepend RSpecConfigurationOverrides + alias new_configure_group configure_group +end + +RSpec.configure do |c| + 50.times { c.include Module.new, :include_it } +end + +require 'benchmark/ips' + +Benchmark.ips do |x| + x.report("Old linear search: non-matching metadata") do |times| + RSpecConfigurationOverrides.prepare_implementation(:old) + times.times { |i| RSpec.describe "Old linear search: non-matching metadata #{i}" } + end + + x.report("New memoized search: non-matching metadata") do |times| + RSpecConfigurationOverrides.prepare_implementation(:new) + times.times { |i| RSpec.describe "New memoized search: non-matching metadata #{i}" } + end + + x.report("Old linear search: matching metadata") do |times| + RSpecConfigurationOverrides.prepare_implementation(:old) + times.times { |i| RSpec.describe "Old linear search: matching metadata #{i}", :include_it } + end + + x.report("New memoized search: matching metadata") do |times| + RSpecConfigurationOverrides.prepare_implementation(:new) + times.times { |i| RSpec.describe "New memoized search: matching metadata #{i}", :include_it } + end +end + +__END__ + +Calculating ------------------------------------- +Old linear search: non-matching metadata + 86.000 i/100ms +New memoized search: non-matching metadata + 93.000 i/100ms +Old linear search: matching metadata + 79.000 i/100ms +New memoized search: matching metadata + 90.000 i/100ms +------------------------------------------------- +Old linear search: non-matching metadata + 884.109 (±61.9%) i/s - 3.268k +New memoized search: non-matching metadata + 1.099k (±81.2%) i/s - 3.441k +Old linear search: matching metadata + 822.348 (±57.5%) i/s - 3.081k +New memoized search: matching metadata + 1.116k (±76.6%) i/s - 3.510k diff --git a/benchmarks/precalculate_absolute_file_path_or_not.rb b/benchmarks/precalculate_absolute_file_path_or_not.rb new file mode 100644 index 0000000000..3810bce4a2 --- /dev/null +++ b/benchmarks/precalculate_absolute_file_path_or_not.rb @@ -0,0 +1,29 @@ +require 'benchmark/ips' + +metadata = { :file_path => "some/path.rb" } +meta_with_absolute = metadata.merge(:absolute_file_path => File.expand_path(metadata[:file_path])) + +Benchmark.ips do |x| + x.report("fetch absolute path from hash") do + meta_with_absolute[:absolute_file_path] + end + + x.report("calculate absolute path") do + File.expand_path(metadata[:file_path]) + end +end + +__END__ + +Precalculating the absolute file path is much, much faster! + +Calculating ------------------------------------- +fetch absolute path from hash + 102.164k i/100ms +calculate absolute path + 9.331k i/100ms +------------------------------------------------- +fetch absolute path from hash + 7.091M (±11.6%) i/s - 34.736M +calculate absolute path + 113.141k (± 8.6%) i/s - 569.191k diff --git a/benchmarks/singleton_example_groups/helper.rb b/benchmarks/singleton_example_groups/helper.rb new file mode 100644 index 0000000000..8f543c1e25 --- /dev/null +++ b/benchmarks/singleton_example_groups/helper.rb @@ -0,0 +1,120 @@ +require_relative "../../bundle/bundler/setup" # configures load paths +require 'rspec/core' +require 'stackprof' + +class << RSpec + attr_writer :world +end + +RSpec::Core::Example.class_eval do + alias_method :new_with_around_and_singleton_context_hooks, :with_around_and_singleton_context_hooks + alias_method :old_with_around_and_singleton_context_hooks, :with_around_example_hooks +end + +RSpec::Core::Hooks::HookCollections.class_eval do + def old_register_global_singleton_context_hooks(*) + # no-op: this method didn't exist before + end + alias_method :new_register_global_singleton_context_hooks, :register_global_singleton_context_hooks +end + +RSpec::Core::Configuration.class_eval do + def old_configure_example(*) + # no-op: this method didn't exist before + end + alias_method :new_configure_example, :configure_example +end + +RSpec.configure do |c| + c.output_stream = StringIO.new +end + +require 'benchmark/ips' + +class BenchmarkHelpers + def self.prepare_implementation(prefix) + RSpec.world = RSpec::Core::World.new # clear our state + RSpec::Core::Example.__send__ :alias_method, :with_around_and_singleton_context_hooks, :"#{prefix}_with_around_and_singleton_context_hooks" + RSpec::Core::Hooks::HookCollections.__send__ :alias_method, :register_global_singleton_context_hooks, :"#{prefix}_register_global_singleton_context_hooks" + RSpec::Core::Configuration.__send__ :alias_method, :configure_example, :"#{prefix}_configure_example" + end + + @@runner = RSpec::Core::Runner.new(RSpec::Core::ConfigurationOptions.new([])) + def self.define_and_run_examples(desc, count, group_meta: {}, example_meta: {}) + groups = count.times.map do |i| + RSpec.describe "Group #{desc} #{i}", group_meta do + 10.times { |j| example("ex #{j}", example_meta) { } } + end + end + + @@runner.run_specs(groups) + end + + def self.profile(count, meta = { example_meta: { apply_it: true } }) + [:new, :old].map do |prefix| + prepare_implementation(prefix) + + results = StackProf.run(mode: :cpu) do + define_and_run_examples("No match/#{prefix}", count, meta) + end + + format_profile_results(results, prefix) + end + end + + def self.format_profile_results(results, prefix) + File.open("tmp/#{prefix}_stack_prof_results.txt", "w") do |f| + StackProf::Report.new(results).print_text(false, nil, f) + end + system "open tmp/#{prefix}_stack_prof_results.txt" + + File.open("tmp/#{prefix}_stack_prof_results.graphviz", "w") do |f| + StackProf::Report.new(results).print_graphviz(nil, f) + end + + system "dot tmp/#{prefix}_stack_prof_results.graphviz -Tpdf > tmp/#{prefix}_stack_prof_results.pdf" + system "open tmp/#{prefix}_stack_prof_results.pdf" + end + + def self.run_benchmarks + Benchmark.ips do |x| + implementations = { :old => "without", :new => "with" } + # Historically, many of our benchmarks have initially been order-sensitive, + # where whichever implementation went first got favored because defining + # more groups (or whatever) would cause things to slow down. To easily + # check if we're having those problems, you can pass REVERSE=1 to try + # it out in the opposite order. + implementations = implementations.to_a.reverse.to_h if ENV['REVERSE'] + + implementations.each do |prefix, description| + x.report("No match -- #{description} singleton group support") do |times| + prepare_implementation(prefix) + define_and_run_examples("No match/#{description}", times) + end + end + + implementations.each do |prefix, description| + x.report("Example match -- #{description} singleton group support") do |times| + prepare_implementation(prefix) + define_and_run_examples("Example match/#{description}", times, example_meta: { apply_it: true }) + end + end + + implementations.each do |prefix, description| + x.report("Group match -- #{description} singleton group support") do |times| + prepare_implementation(prefix) + define_and_run_examples("Group match/#{description}", times, group_meta: { apply_it: true }) + end + end + + implementations.each do |prefix, description| + x.report("Both match -- #{description} singleton group support") do |times| + prepare_implementation(prefix) + define_and_run_examples("Both match/#{description}", times, + example_meta: { apply_it: true }, + group_meta: { apply_it: true }) + end + end + end + end +end diff --git a/benchmarks/singleton_example_groups/with_config_hooks.rb b/benchmarks/singleton_example_groups/with_config_hooks.rb new file mode 100644 index 0000000000..08f49265fd --- /dev/null +++ b/benchmarks/singleton_example_groups/with_config_hooks.rb @@ -0,0 +1,28 @@ +require_relative "helper" + +RSpec.configure do |c| + 10.times do + c.before(:context, :apply_it) { } + c.after(:context, :apply_it) { } + end +end + +BenchmarkHelpers.run_benchmarks + +__END__ +No match -- without singleton group support + 575.250 (±29.0%) i/s - 2.484k +No match -- with singleton group support + 503.671 (±21.8%) i/s - 2.250k +Example match -- without singleton group support + 544.191 (±25.7%) i/s - 2.160k +Example match -- with singleton group support + 413.538 (±22.2%) i/s - 1.715k +Group match -- without singleton group support + 517.998 (±28.2%) i/s - 2.058k +Group match -- with singleton group support + 431.554 (±15.3%) i/s - 1.960k +Both match -- without singleton group support + 525.306 (±25.1%) i/s - 2.107k in 5.556760s +Both match -- with singleton group support + 440.288 (±16.6%) i/s - 1.848k diff --git a/benchmarks/singleton_example_groups/with_config_hooks_module_inclusions_and_shared_context_inclusions.rb b/benchmarks/singleton_example_groups/with_config_hooks_module_inclusions_and_shared_context_inclusions.rb new file mode 100644 index 0000000000..9eee773b14 --- /dev/null +++ b/benchmarks/singleton_example_groups/with_config_hooks_module_inclusions_and_shared_context_inclusions.rb @@ -0,0 +1,35 @@ +require_relative "helper" + +RSpec.configure do |c| + 10.times do + c.before(:context, :apply_it) { } + c.after(:context, :apply_it) { } + c.include Module.new, :apply_it + end +end + +1.upto(10) do |i| + RSpec.shared_context "context #{i}", :apply_it do + end +end + +BenchmarkHelpers.run_benchmarks + +__END__ + +No match -- without singleton group support + 544.396 (±34.0%) i/s - 2.340k +No match -- with singleton group support + 451.635 (±31.0%) i/s - 1.935k +Example match -- without singleton group support + 538.788 (±23.8%) i/s - 2.450k +Example match -- with singleton group support + 342.990 (±22.4%) i/s - 1.440k +Group match -- without singleton group support + 509.969 (±26.7%) i/s - 2.070k +Group match -- with singleton group support + 405.284 (±20.5%) i/s - 1.518k +Both match -- without singleton group support + 513.344 (±24.0%) i/s - 1.927k +Both match -- with singleton group support + 406.111 (±18.5%) i/s - 1.760k diff --git a/benchmarks/singleton_example_groups/with_module_inclusions.rb b/benchmarks/singleton_example_groups/with_module_inclusions.rb new file mode 100644 index 0000000000..e3a8c43fe2 --- /dev/null +++ b/benchmarks/singleton_example_groups/with_module_inclusions.rb @@ -0,0 +1,28 @@ +require_relative "helper" + +RSpec.configure do |c| + 1.upto(10) do + c.include Module.new, :apply_it + end +end + +BenchmarkHelpers.run_benchmarks + +__END__ + +No match -- without singleton group support + 555.498 (±27.0%) i/s - 2.496k +No match -- with singleton group support + 529.826 (±23.0%) i/s - 2.397k in 5.402305s +Example match -- without singleton group support + 541.845 (±29.0%) i/s - 2.208k +Example match -- with singleton group support + 465.440 (±20.4%) i/s - 2.091k +Group match -- without singleton group support + 530.976 (±24.1%) i/s - 2.303k +Group match -- with singleton group support + 505.291 (±18.8%) i/s - 2.226k +Both match -- without singleton group support + 542.168 (±28.4%) i/s - 2.067k in 5.414905s +Both match -- with singleton group support + 503.226 (±27.2%) i/s - 1.880k in 5.621210s diff --git a/benchmarks/singleton_example_groups/with_no_config_hooks_or_inclusions.rb b/benchmarks/singleton_example_groups/with_no_config_hooks_or_inclusions.rb new file mode 100644 index 0000000000..ec7849d2b1 --- /dev/null +++ b/benchmarks/singleton_example_groups/with_no_config_hooks_or_inclusions.rb @@ -0,0 +1,22 @@ +require_relative "helper" + +BenchmarkHelpers.run_benchmarks + +__END__ + +No match -- without singleton group support + 565.198 (±28.8%) i/s - 2.438k +No match -- with singleton group support + 539.781 (±18.9%) i/s - 2.496k +Example match -- without singleton group support + 539.287 (±28.2%) i/s - 2.450k in 5.555471s +Example match -- with singleton group support + 511.576 (±28.1%) i/s - 2.058k +Group match -- without singleton group support + 535.298 (±23.2%) i/s - 2.352k +Group match -- with singleton group support + 539.454 (±19.1%) i/s - 2.350k +Both match -- without singleton group support + 550.932 (±32.1%) i/s - 2.145k in 5.930432s +Both match -- with singleton group support + 540.183 (±19.6%) i/s - 2.300k diff --git a/benchmarks/singleton_example_groups/with_shared_context_inclusions.rb b/benchmarks/singleton_example_groups/with_shared_context_inclusions.rb new file mode 100644 index 0000000000..2d99ec6d2a --- /dev/null +++ b/benchmarks/singleton_example_groups/with_shared_context_inclusions.rb @@ -0,0 +1,28 @@ +require_relative "helper" + +1.upto(10) do |i| + RSpec.shared_context "context #{i}", :apply_it do + end +end + +BenchmarkHelpers.run_benchmarks +# BenchmarkHelpers.profile(1000) + +__END__ + +No match -- without singleton group support + 563.304 (±29.6%) i/s - 2.385k +No match -- with singleton group support + 538.738 (±22.3%) i/s - 2.209k +Example match -- without singleton group support + 546.605 (±25.6%) i/s - 2.450k +Example match -- with singleton group support + 421.111 (±23.5%) i/s - 1.845k +Group match -- without singleton group support + 536.267 (±27.4%) i/s - 2.050k +Group match -- with singleton group support + 508.644 (±17.7%) i/s - 2.268k +Both match -- without singleton group support + 538.047 (±27.7%) i/s - 2.067k in 5.431649s +Both match -- with singleton group support + 505.388 (±26.7%) i/s - 1.880k in 5.578614s diff --git a/features/clear_examples.feature b/features/clear_examples.feature new file mode 100644 index 0000000000..139879041a --- /dev/null +++ b/features/clear_examples.feature @@ -0,0 +1,107 @@ +Feature: Running specs multiple times with different runner options in the same process + + Use `clear_examples` command to clear all example groups between different + runs in the same process. It: + + - clears all example groups + - restores inclusion and exclusion filters set by configuration + - clears inclusion and exclusion filters set by previous spec run (via runner) + - resets all time counters (start time, load time, duration, etc.) + - resets different counts of examples (all examples, pending and failed) + + ```ruby + require "spec_helper" + + RSpec::Core::Runner.run([... some parameters ...]) + + RSpec.clear_examples + + RSpec::Core::Runner.run([... different parameters ...]) + ``` + + Background: + Given a file named "spec/spec_helper.rb" with: + """ruby + RSpec.configure do |config| + config.filter_run_including :focus => true + config.filter_run_excluding :slow => true + config.run_all_when_everything_filtered = true + end + """ + Given a file named "spec/truth_spec.rb" with: + """ruby + require 'spec_helper' + + RSpec.describe "truth" do + describe true do + it "is truthy" do + expect(true).to be_truthy + end + + it "is not falsy" do + expect(true).not_to be_falsy + end + end + + describe false do + it "is falsy" do + expect(false).to be_falsy + end + + it "is truthy" do + expect(false).not_to be_truthy + end + end + end + """ + + Scenario: Running specs multiple times in the same process + Given a file named "scripts/multiple_runs.rb" with: + """ruby + require 'rspec/core' + + RSpec::Core::Runner.run(['spec']) + RSpec.clear_examples + RSpec::Core::Runner.run(['spec']) + """ + When I run `ruby scripts/multiple_runs.rb` + Then the output should match: + """ + 4 examples, 0 failures + .* + 4 examples, 0 failures + """ + + Scenario: Running specs multiple times in the same process with different parameters + Given a file named "spec/bar_spec.rb" with: + """ruby + require 'spec_helper' + + RSpec.describe 'bar' do + subject(:bar) { :focused } + + it 'is focused', :focus do + expect(bar).to be(:focused) + end + end + """ + Given a file named "scripts/different_parameters.rb" with: + """ruby + require 'rspec/core' + + RSpec::Core::Runner.run(['spec']) + RSpec.clear_examples + RSpec::Core::Runner.run(['spec/truth_spec.rb:4']) + RSpec.clear_examples + RSpec::Core::Runner.run(['spec', '-e', 'fals']) + """ + When I run `ruby scripts/different_parameters.rb` + Then the output should match: + """ + 1 example, 0 failures + .* + 2 examples, 0 failures + .* + 3 examples, 0 failures + """ + diff --git a/features/command_line/example_name_option.feature b/features/command_line/example_name_option.feature index b6297aeae3..f41a9c92c2 100644 --- a/features/command_line/example_name_option.feature +++ b/features/command_line/example_name_option.feature @@ -12,6 +12,8 @@ Feature: `--example` option You can also use the option more than once to specify multiple example matches. + Note: description-less examples that have generated descriptions (typical when using the [one-liner syntax](../subject/one-liner-syntax)) cannot be directly filtered with this option, because it is necessary to execute the example to generate the description, so RSpec is unable to use the not-yet-generated description to decide whether or not to execute an example. You can, of course, pass part of a group's description to select all examples defined in the group (including those that have no description). + Background: Given a file named "first_spec.rb" with: """ruby diff --git a/features/command_line/order.md b/features/command_line/order.md index 0ffc6f5467..92eeb28251 100644 --- a/features/command_line/order.md +++ b/features/command_line/order.md @@ -2,7 +2,9 @@ Use the `--order` option to tell RSpec how to order the files, groups, and examples. The available ordering schemes are `defined` and `rand`. `defined` is the default, which executes groups and examples in the order they -are defined as the spec files are loaded. +are defined as the spec files are loaded, with the caveat that each group +runs its examples before running its nested example groups, even if the +nested groups are defined before the examples. Use `rand` to randomize the order of groups and examples within the groups. Nested groups are always run from top-level to bottom-level in order to avoid diff --git a/features/command_line/randomization.feature b/features/command_line/randomization.feature index f71cc8bd8c..f975334b26 100644 --- a/features/command_line/randomization.feature +++ b/features/command_line/randomization.feature @@ -34,10 +34,13 @@ Feature: Randomization can be reproduced across test runs is necessary to replicate a given test run's randomness. Background: + Given a file named ".rspec" with: + """ + --require spec_helper + """ + Given a file named "spec/random_spec.rb" with: """ruby - require 'spec_helper' - RSpec.describe 'randomized example' do it 'prints random numbers' do puts 5.times.map { rand(99) }.join("-") diff --git a/features/command_line/ruby.feature b/features/command_line/ruby.feature index a9026918a6..d0e50ba006 100644 --- a/features/command_line/ruby.feature +++ b/features/command_line/ruby.feature @@ -16,8 +16,12 @@ Feature: run with `ruby` command it "is < 2" do expect(1).to be < 2 end + + it "has an intentional failure" do + expect(1).to be > 2 + end end """ When I run `ruby example_spec.rb` - Then the output should contain "1 example, 0 failures" - + Then the output should contain "2 examples, 1 failure" + And the output should contain "expect(1).to be > 2" diff --git a/features/configuration/backtrace_exclusion_patterns.feature b/features/configuration/backtrace_exclusion_patterns.feature index be4b8f9f48..4a87469d0d 100644 --- a/features/configuration/backtrace_exclusion_patterns.feature +++ b/features/configuration/backtrace_exclusion_patterns.feature @@ -1,7 +1,21 @@ Feature: Excluding lines from the backtrace - To reduce the noise when diagnosing failures, RSpec excludes matching lines - from backtraces. The default exclusion patterns are: + To reduce the noise when diagnosing failures, RSpec can exclude lines belonging to certain gems or matching given patterns. + + If you want to filter out backtrace lines belonging to specific gems, you can use `config.filter_gems_from_backtrace` like so: + + ```ruby + config.filter_gems_from_backtrace "ignored_gem", "another_ignored_gem", + ``` + + For more control over which lines to ignore, you can use the the `backtrace_exclusion_patterns` option to either replace the default exclusion patterns, or append your own, e.g. + + ```ruby + config.backtrace_exclusion_patterns = [/first pattern/, /second pattern/] + config.backtrace_exclusion_patterns << /another pattern/ + ``` + + The default exclusion patterns are: ```ruby /\/lib\d*\/ruby\//, @@ -10,12 +24,8 @@ Feature: Excluding lines from the backtrace /lib\/rspec\/(core|expectations|matchers|mocks)/ ``` - This list can be modified or replaced with the `backtrace_exclusion_patterns` - option. Additionally, `rspec` can be run with the `--backtrace` option to skip - backtrace cleaning entirely. + Additionally, `rspec` can be run with the `--backtrace` option to skip backtrace cleaning entirely. - In addition, if you want to filter out backtrace lines from specific gems, you - can use `config.filter_gems_from_backtrace`. Scenario: Using default `backtrace_exclusion_patterns` Given a file named "spec/failing_spec.rb" with: @@ -38,17 +48,13 @@ Feature: Excluding lines from the backtrace /spec_helper/ ] end - - def foo - "bar" - end """ And a file named "spec/example_spec.rb" with: """ruby require 'spec_helper' RSpec.describe "foo" do it "returns baz" do - expect(foo).to eq("baz") + expect("foo").to eq("baz") end end """ @@ -57,55 +63,65 @@ Feature: Excluding lines from the backtrace And the output should contain "lib/rspec/expectations" Scenario: Appending to `backtrace_exclusion_patterns` - Given a file named "spec/matchers/be_baz_matcher.rb" with: + Given a file named "spec/support/assert_baz.rb" with: """ruby - RSpec::Matchers.define :be_baz do |_| - match do |actual| - actual == "baz" - end + require "support/really_assert_baz" + + def assert_baz(arg) + really_assert_baz(arg) + end + """ + And a file named "spec/support/really_assert_baz.rb" with: + """ruby + def really_assert_baz(arg) + expect(arg).to eq("baz") end """ And a file named "spec/example_spec.rb" with: """ruby + require "support/assert_baz" RSpec.configure do |config| - config.backtrace_exclusion_patterns << /be_baz_matcher/ + config.backtrace_exclusion_patterns << /really/ end RSpec.describe "bar" do it "is baz" do - expect("bar").to be_baz + assert_baz("bar") end end """ When I run `rspec` Then the output should contain "1 example, 1 failure" - But the output should not contain "be_baz_matcher" + And the output should contain "assert_baz" + But the output should not contain "really_assert_baz" And the output should not contain "lib/rspec/expectations" - Scenario: Running `rspec` with the `--backtrace` option - Given a file named "spec/matchers/be_baz_matcher.rb" with: + Scenario: Running `rspec` with `--backtrace` prints unfiltered backtraces + Given a file named "spec/support/custom_helper.rb" with: """ruby - RSpec::Matchers.define :be_baz do |_| - match do |actual| - actual == "baz" - end + def assert_baz(arg) + expect(arg).to eq("baz") end """ And a file named "spec/example_spec.rb" with: """ruby + require "support/custom_helper" + RSpec.configure do |config| - config.backtrace_exclusion_patterns << /be_baz_matcher/ + config.backtrace_exclusion_patterns << /custom_helper/ end RSpec.describe "bar" do it "is baz" do - expect("bar").to be_baz + assert_baz("bar") end end """ When I run `rspec --backtrace` Then the output should contain "1 example, 1 failure" - And the output should not contain "be_baz_matcher" + And the output should contain "spec/support/custom_helper.rb:2:in `assert_baz'" + And the output should contain "lib/rspec/expectations" + And the output should contain "lib/rspec/core" Scenario: Using `filter_gems_from_backtrace` to filter the named gem Given a vendored gem named "my_gem" containing a file named "lib/my_gem.rb" with: @@ -134,5 +150,5 @@ Feature: Excluding lines from the backtrace config.filter_gems_from_backtrace "my_gem" end """ - Then the output from `rspec` should contain "# ./vendor/my_gem-1.2.3/lib/my_gem.rb:4:in `do_amazing_things!'" - But the output from `rspec --require spec_helper` should not contain "# ./vendor/my_gem-1.2.3/lib/my_gem.rb:4:in `do_amazing_things!'" + Then the output from `rspec` should contain "vendor/my_gem-1.2.3/lib/my_gem.rb:4:in `do_amazing_things!'" + But the output from `rspec --require spec_helper` should not contain "vendor/my_gem-1.2.3/lib/my_gem.rb:4:in `do_amazing_things!'" diff --git a/features/configuration/overriding_global_ordering.feature b/features/configuration/overriding_global_ordering.feature index aa88fb78c8..aafb4d2756 100644 --- a/features/configuration/overriding_global_ordering.feature +++ b/features/configuration/overriding_global_ordering.feature @@ -11,7 +11,7 @@ Feature: Overriding global ordering `:global`, it will be the global default, used by all groups that do not have `:order` metadata (and by RSpec to order the top-level groups). - Scenario: Running a specific examples group in order + Scenario: Running a specific example group in order Given a file named "order_dependent_spec.rb" with: """ruby RSpec.describe "examples only pass when they are run in order", :order => :defined do diff --git a/features/example_groups/shared_context.feature b/features/example_groups/shared_context.feature index a156d320e0..c21e23861e 100644 --- a/features/example_groups/shared_context.feature +++ b/features/example_groups/shared_context.feature @@ -4,6 +4,8 @@ Feature: shared context of example groups either explicitly, using `include_context`, or implicitly by matching metadata. + When implicitly including shared contexts via matching metadata, the normal way is to define matching metadata on an example group (in which case the ontext is included in the entire group), but you can also include it in an individual example. RSpec treats every example as having a singleton example group (analogous to Ruby's singleton classes) containing just the one example. + Background: Given a file named "shared_stuff.rb" with: """ruby @@ -90,3 +92,21 @@ Feature: shared context """ When I run `rspec shared_context_example.rb` Then the examples should all pass + + Scenario: Declare a shared context and include it with metadata of an individual example + Given a file named "shared_context_example.rb" with: + """ruby + require "./shared_stuff.rb" + + RSpec.describe "group that does not include the shared context" do + it "does not have access to shared methods normally" do + expect(self).not_to respond_to(:shared_method) + end + + it "has access to shared methods from examples with matching metadata", :a => :b do + expect(shared_method).to eq("it works") + end + end + """ + When I run `rspec shared_context_example.rb` + Then the examples should all pass diff --git a/features/example_groups/shared_examples.feature b/features/example_groups/shared_examples.feature index b021bff50a..27563be13a 100644 --- a/features/example_groups/shared_examples.feature +++ b/features/example_groups/shared_examples.feature @@ -1,6 +1,6 @@ Feature: shared examples - Shared examples let you describe behaviour of types or modules. When declared, + Shared examples let you describe behaviour of classes or modules. When declared, a shared group's content is stored. It is only realized in the context of another example group, which provides any context the shared group needs to run. @@ -28,18 +28,22 @@ Feature: shared examples 'shared_examples_for_widgets'` to require a file at `#{PROJECT_ROOT}/spec/shared_examples_for_widgets.rb`. - 2. Put files containing shared examples in `spec/support/` and require files - in that directory from `spec/spec_helper.rb`: + 2. One convention is to put files containing shared examples in `spec/support/` + and require files in that directory from `spec/spec_helper.rb`: ```ruby Dir["./spec/support/**/*.rb"].sort.each { |f| require f} ``` - This is included in the generated `spec/spec_helper.rb` file in - `rspec-rails` + Historically, this was included in the generated `spec/spec_helper.rb` file in + `rspec-rails`. However, in order to keep your test suite boot time down, + it's a good idea to not autorequire all files in a directory like this. + When running only one spec file, loading unneeded dependencies or performing + unneeded setup can have a significant, noticable effect on how long it takes + before the first example runs. - 3. When all of the groups that include the shared group, just declare the - shared group in the same file. + 3. When all of the groups that include the shared group reside in the same file, + just declare the shared group in that file. Scenario: Shared examples group included in two groups in one file Given a file named "collection_spec.rb" with: @@ -220,11 +224,11 @@ Feature: shared examples RSpec.describe String, :a => :b do end """ - When I run `rspec shared_example_metadata_spec.rb` - Then the output should contain: - """ - 1 example, 0 failures - """ + When I run `rspec shared_example_metadata_spec.rb` + Then the output should contain: + """ + 1 example, 0 failures + """ Scenario: Shared examples are nestable by context Given a file named "context_specific_examples_spec.rb" with: diff --git a/features/expectation_framework_integration/configure_expectation_framework.feature b/features/expectation_framework_integration/configure_expectation_framework.feature index a7852fb73f..6a28436aca 100644 --- a/features/expectation_framework_integration/configure_expectation_framework.feature +++ b/features/expectation_framework_integration/configure_expectation_framework.feature @@ -89,6 +89,10 @@ Feature: configure expectation framework it "is empty (intentional failure)" do assert_empty [1], "errantly expected [1] to be empty" end + + it "marks pending for skip method" do + skip "intentionally" + end end """ When I run `rspec -b example_spec.rb` @@ -97,7 +101,7 @@ Feature: configure expectation framework MiniT|test::Assertion: errantly expected \[1\] to be empty """ - And the output should contain "3 examples, 1 failure" + And the output should contain "4 examples, 1 failure, 1 pending" And the output should not contain "Warning: you should require 'minitest/autorun' instead." Scenario: Configure rspec/expectations AND test/unit assertions @@ -120,7 +124,7 @@ Feature: configure expectation framework When I run `rspec example_spec.rb` Then the examples should all pass - Scenario: Configure rspec/expecations AND minitest assertions + Scenario: Configure rspec/expectations AND minitest assertions Given a file named "example_spec.rb" with: """ruby RSpec.configure do |config| diff --git a/features/helper_methods/modules.feature b/features/helper_methods/modules.feature index 200765a1ff..b92070e497 100644 --- a/features/helper_methods/modules.feature +++ b/features/helper_methods/modules.feature @@ -11,6 +11,8 @@ Feature: Define helper methods in a module given metadata will `include` or `extend` the module. You can also specify metadata using only symbols. + Note that examples that match a `config.include` module's metadata will also have the module included. RSpec treats every example as having a singleton example group (analogous to Ruby's singleton classes) containing just the one example. + Background: Given a file named "helpers.rb" with: """ruby @@ -79,6 +81,10 @@ Feature: Define helper methods in a module it "does not have access to the helper methods defined in the module" do expect { help }.to raise_error(NameError) end + + it "does have access when the example has matching metadata", :foo => :bar do + expect(help).to be(:available) + end end """ When I run `rspec include_module_in_some_groups_spec.rb` diff --git a/features/hooks/around_hooks.feature b/features/hooks/around_hooks.feature index 47a71e0197..99a76d9224 100644 --- a/features/hooks/around_hooks.feature +++ b/features/hooks/around_hooks.feature @@ -85,6 +85,29 @@ Feature: `around` hooks When I run `rspec example_spec.rb` Then the output should contain "this should show up in the output" + Scenario: An around hook continues to run even if the example throws an exception + Given a file named "example_spec.rb" with: + """ruby + RSpec.describe "something" do + around(:example) do |example| + puts "around example setup" + example.run + puts "around example cleanup" + end + + it "still executes the entire around hook" do + fail "the example blows up" + end + end + """ + When I run `rspec example_spec.rb` + Then the output should contain "1 example, 1 failure" + And the output should contain: + """ + around example setup + around example cleanup + """ + Scenario: Define a global `around` hook Given a file named "example_spec.rb" with: """ruby @@ -215,9 +238,11 @@ Feature: `around` hooks Then the output should contain "1 example, 0 failures, 1 pending" And the output should contain: """ - Pending: - implicit pending example should be detected as Not yet implemented - # Not yet implemented + Pending: (Failures listed here are expected and do not affect your suite's status) + + 1) implicit pending example should be detected as Not yet implemented + # Not yet implemented + # ./example_spec.rb:6 """ @@ -239,8 +264,10 @@ Feature: `around` hooks Then the output should contain "1 example, 0 failures, 1 pending" And the output should contain: """ - explicit pending example should be detected as pending - # No reason given + Pending: (Failures listed here are expected and do not affect your suite's status) + + 1) explicit pending example should be detected as pending + # No reason given """ Scenario: Multiple `around` hooks in the same scope diff --git a/features/hooks/filtering.feature b/features/hooks/filtering.feature index 30af1a4d8d..abdb12be69 100644 --- a/features/hooks/filtering.feature +++ b/features/hooks/filtering.feature @@ -14,6 +14,8 @@ Feature: filters end ``` + Note that filtered `:context` hooks will still be applied to individual examples with matching metadata -- in effect, every example has a singleton example group containing just the one example (analogous to Ruby's singleton classes). + You can also specify metadata using only symbols. Scenario: Filter `before(:example)` hooks using arbitrary metadata @@ -127,6 +129,10 @@ Feature: filters expect(@hook).to be_nil end + it "runs the hook for a single example with matching metadata", :foo => :bar do + expect(@hook).to eq(:before_context_foo_bar) + end + describe "a nested subgroup with matching metadata", :foo => :bar do it "runs the hook" do expect(@hook).to eq(:before_context_foo_bar) @@ -166,31 +172,37 @@ Feature: filters it "does not run the hook" do puts "unfiltered" end + + it "runs the hook for a single example with matching metadata", :foo => :bar do + puts "filtered 1" + end end describe "a group with matching metadata", :foo => :bar do it "runs the hook" do - puts "filtered 1" + puts "filtered 2" end end describe "another group without matching metadata" do describe "a nested subgroup with matching metadata", :foo => :bar do it "runs the hook" do - puts "filtered 2" + puts "filtered 3" end end end end """ - When I run `rspec --format progress filter_after_context_hooks_spec.rb` + When I run `rspec --format progress filter_after_context_hooks_spec.rb --order defined` Then the examples should all pass And the output should contain: """ unfiltered .filtered 1 + after :context + .filtered 2 .after :context - filtered 2 + filtered 3 .after :context """ diff --git a/features/metadata/user_defined.feature b/features/metadata/user_defined.feature index 2f00f5dc5d..b235035e0b 100644 --- a/features/metadata/user_defined.feature +++ b/features/metadata/user_defined.feature @@ -26,11 +26,11 @@ Feature: User-defined metadata describe 'a sub-group with user-defined metadata', :bar => 12 do it 'has access to the sub-group metadata' do |example| - expect(example.metadata[:foo]).to eq(17) + expect(example.metadata[:bar]).to eq(12) end it 'also has access to metadata defined on parent groups' do |example| - expect(example.metadata[:bar]).to eq(12) + expect(example.metadata[:foo]).to eq(17) end end end diff --git a/features/pending_and_skipped_examples/pending_examples.feature b/features/pending_and_skipped_examples/pending_examples.feature index c8bf26b366..7a5da21b82 100644 --- a/features/pending_and_skipped_examples/pending_examples.feature +++ b/features/pending_and_skipped_examples/pending_examples.feature @@ -18,11 +18,12 @@ Feature: `pending` examples And the output should contain "1 example, 0 failures, 1 pending" And the output should contain: """ - Pending: - an example is implemented but waiting - # something else getting finished - # ./pending_without_block_spec.rb:2 + Pending: (Failures listed here are expected and do not affect your suite's status) + + 1) an example is implemented but waiting + # something else getting finished """ + Scenario: `pending` any arbitrary reason with a passing example Given a file named "pending_with_passing_example_spec.rb" with: """ruby diff --git a/features/pending_and_skipped_examples/skipped_examples.feature b/features/pending_and_skipped_examples/skipped_examples.feature index b2ceb43c25..4ff3c82b28 100644 --- a/features/pending_and_skipped_examples/skipped_examples.feature +++ b/features/pending_and_skipped_examples/skipped_examples.feature @@ -29,10 +29,11 @@ Feature: `skip` examples And the output should contain "1 example, 0 failures, 1 pending" And the output should contain: """ - Pending: - an example is skipped - # No reason given - # ./skipped_spec.rb:2 + Pending: (Failures listed here are expected and do not affect your suite's status) + + 1) an example is skipped + # No reason given + # ./skipped_spec.rb:2 """ Scenario: Skipping using `skip` inside an example @@ -49,10 +50,11 @@ Feature: `skip` examples And the output should contain "1 example, 0 failures, 1 pending" And the output should contain: """ - Pending: - an example is skipped - # No reason given - # ./skipped_spec.rb:2 + Pending: (Failures listed here are expected and do not affect your suite's status) + + 1) an example is skipped + # No reason given + # ./skipped_spec.rb:2 """ Scenario: Temporarily skipping by prefixing `it`, `specify`, or `example` with an x @@ -74,16 +76,19 @@ Feature: `skip` examples And the output should contain "3 examples, 0 failures, 3 pending" And the output should contain: """ - Pending: - an example is skipped using xit - # Temporarily skipped with xit - # ./temporarily_skipped_spec.rb:2 - an example is skipped using xspecify - # Temporarily skipped with xspecify - # ./temporarily_skipped_spec.rb:5 - an example is skipped using xexample - # Temporarily skipped with xexample - # ./temporarily_skipped_spec.rb:8 + Pending: (Failures listed here are expected and do not affect your suite's status) + + 1) an example is skipped using xit + # Temporarily skipped with xit + # ./temporarily_skipped_spec.rb:2 + + 2) an example is skipped using xspecify + # Temporarily skipped with xspecify + # ./temporarily_skipped_spec.rb:5 + + 3) an example is skipped using xexample + # Temporarily skipped with xexample + # ./temporarily_skipped_spec.rb:8 """ Scenario: Skipping using metadata @@ -99,8 +104,9 @@ Feature: `skip` examples And the output should contain "1 example, 0 failures, 1 pending" And the output should contain: """ - Pending: - an example is skipped - # No reason given - # ./skipped_spec.rb:2 + Pending: (Failures listed here are expected and do not affect your suite's status) + + 1) an example is skipped + # No reason given + # ./skipped_spec.rb:2 """ diff --git a/features/step_definitions/additional_cli_steps.rb b/features/step_definitions/additional_cli_steps.rb index dc792608dd..5ecfcadea7 100644 --- a/features/step_definitions/additional_cli_steps.rb +++ b/features/step_definitions/additional_cli_steps.rb @@ -8,13 +8,13 @@ Then /^the output should not contain any of these:$/ do |table| table.raw.flatten.each do |string| - expect(all_output).not_to match(regexp(string)) + expect(all_output).not_to include(string) end end Then /^the output should contain one of the following:$/ do |table| matching_output = table.raw.flatten.select do |string| - all_output =~ regexp(string) + all_output.include?(string) end expect(matching_output.count).to eq(1) @@ -66,7 +66,7 @@ line =~ /(^\s+# [^:]+:\d+)/ ? $1 : line # http://rubular.com/r/zDD7DdWyzF end.join("\n") - expect(normalized_output).to match(regexp(partial_output)) + expect(normalized_output).to include(partial_output) end Then /^the output should not contain any error backtraces$/ do diff --git a/features/subject/one_liner_syntax.feature b/features/subject/one_liner_syntax.feature index 94af6e5cac..07860fd062 100644 --- a/features/subject/one_liner_syntax.feature +++ b/features/subject/one_liner_syntax.feature @@ -18,7 +18,10 @@ Feature: One-liner syntax `:should` syntax is disabled (since that merely removes `Object#should` but this is `RSpec::Core::ExampleGroup#should`). - Note: this feature is only available when using rspec-expectations. + Notes: + + * This feature is only available when using rspec-expectations. + * Examples defined using this one-liner syntax cannot be directly selected from the command line using the [`--example` option](../command-line/example-option). Scenario: Implicit subject Given a file named "example_spec.rb" with: diff --git a/lib/rspec/core.rb b/lib/rspec/core.rb index 5283587401..77d93705e8 100644 --- a/lib/rspec/core.rb +++ b/lib/rspec/core.rb @@ -43,18 +43,38 @@ module RSpec extend RSpec::Core::Warnings - # Used to ensure examples get reloaded between multiple runs in - # the same process. + class << self + # Setters for shared global objects + # @api private + attr_writer :configuration, :world + end + + # Used to ensure examples get reloaded and user configuration gets reset to + # defaults between multiple runs in the same process. # # Users must invoke this if they want to have the configuration reset when - # they use runner multiple times within the same process. + # they use the runner multiple times within the same process. Users must deal + # themselves with re-configuration of RSpec before run. def self.reset @world = nil @configuration = nil end - # Returns the global [Configuration](RSpec/Core/Configuration) object. While you - # _can_ use this method to access the configuration, the more common + # Used to ensure examples get reloaded between multiple runs in the same + # process and ensures user configuration is persisted. + # + # Users must invoke this if they want to clear all examples but preserve + # current configuration when they use the runner multiple times within the + # same process. + def self.clear_examples + world.reset + configuration.reporter.reset + configuration.start_time = ::RSpec::Core::Time.now + configuration.reset_filters + end + + # Returns the global [Configuration](RSpec/Core/Configuration) object. While + # you _can_ use this method to access the configuration, the more common # convention is to use [RSpec.configure](RSpec#configure-class_method). # # @example @@ -116,11 +136,11 @@ def self.current_example=(example) # A single thread local variable so we don't excessively pollute that # namespace. def self.thread_local_metadata - Thread.current[:_rspec] ||= {} + Thread.current[:_rspec] ||= { :shared_example_group_inclusions => [] } end # @private - # Internal container for global non-configuration data + # Internal container for global non-configuration data. def self.world @world ||= RSpec::Core::World.new end @@ -137,7 +157,7 @@ class << self end end - # @private path to executable file + # @private path to executable file. def self.path_to_executable @path_to_executable ||= File.expand_path('../../../exe/rspec', __FILE__) end diff --git a/lib/rspec/core/backport_random.rb b/lib/rspec/core/backport_random.rb index 90046d54e4..1b8afaf56a 100644 --- a/lib/rspec/core/backport_random.rb +++ b/lib/rspec/core/backport_random.rb @@ -60,14 +60,14 @@ def self.coerce_to_int(obj) coerce_to(obj, Integer, :to_int) end - # Used internally to make it easy to deal with optional arguments + # Used internally to make it easy to deal with optional arguments. # (from Rubinius) Undefined = Object.new # @private class Random # @private - # An implementation of Mersenne Twister MT19937 in Ruby + # An implementation of Mersenne Twister MT19937 in Ruby. class MT19937 STATE_SIZE = 624 LAST_STATE = STATE_SIZE - 1 @@ -92,9 +92,10 @@ def next_state end # Seed must be either an Integer (only the first 32 bits will be used) - # or an Array of Integers (of which only the first 32 bits will be used) + # or an Array of Integers (of which only the first 32 bits will be + # used). # - # No conversion or type checking is done at this level + # No conversion or type checking is done at this level. def seed=(seed) case seed when Integer @@ -129,7 +130,7 @@ def seed=(seed) end end - # Returns a random Integer from the range 0 ... (1 << 32) + # Returns a random Integer from the range 0 ... (1 << 32). def random_32_bits next_state if @last_read >= LAST_STATE @last_read += 1 @@ -146,12 +147,12 @@ def random_32_bits # No argument checking is done here either. FLOAT_FACTOR = 1.0/9007199254740992.0 - # generates a random number on [0,1) with 53-bit resolution + # Generates a random number on [0, 1) with 53-bit resolution. def random_float ((random_32_bits >> 5) * 67108864.0 + (random_32_bits >> 6)) * FLOAT_FACTOR; end - # Returns an integer within 0...upto + # Returns an integer within 0...upto. def random_integer(upto) n = upto - 1 nb_full_32 = 0 @@ -202,7 +203,8 @@ def marshal_load(ary) end end - # Convert an Integer seed of arbitrary size to either a single 32 bit integer, or an Array of 32 bit integers + # Convert an Integer seed of arbitrary size to either a single 32 bit + # integer, or an Array of 32 bit integers. def self.convert_seed(seed) seed = seed.abs long_values = [] @@ -211,7 +213,8 @@ def self.convert_seed(seed) seed >>= 32 end until seed == 0 - long_values.pop if long_values[-1] == 1 && long_values.size > 1 # Done to allow any kind of sequence of integers + # Done to allow any kind of sequence of integers. + long_values.pop if long_values[-1] == 1 && long_values.size > 1 long_values.size > 1 ? long_values : long_values.first end diff --git a/lib/rspec/core/backtrace_formatter.rb b/lib/rspec/core/backtrace_formatter.rb index d8dde2893e..b1dff2f1c7 100644 --- a/lib/rspec/core/backtrace_formatter.rb +++ b/lib/rspec/core/backtrace_formatter.rb @@ -12,23 +12,22 @@ def initialize patterns << "org/jruby/" if RUBY_PLATFORM == 'java' patterns.map! { |s| Regexp.new(s.gsub("/", File::SEPARATOR)) } - @system_exclusion_patterns = [Regexp.union(RSpec::CallerFilter::IGNORE_REGEX, *patterns)] - @exclusion_patterns = [] + @system_exclusion_patterns - @inclusion_patterns = [Regexp.new(Dir.getwd)] + @exclusion_patterns = [Regexp.union(RSpec::CallerFilter::IGNORE_REGEX, *patterns)] + @inclusion_patterns = [] + + return unless matches?(@exclusion_patterns, File.join(Dir.getwd, "lib", "foo.rb:13")) + inclusion_patterns << Regexp.new(Dir.getwd) end attr_writer :full_backtrace def full_backtrace? - @full_backtrace || @exclusion_patterns.empty? + @full_backtrace || exclusion_patterns.empty? end def filter_gem(gem_name) sep = File::SEPARATOR - pattern = /#{sep}#{gem_name}(-[^#{sep}]+)?#{sep}/ - - @exclusion_patterns << pattern - @system_exclusion_patterns << pattern + exclusion_patterns << /#{sep}#{gem_name}(-[^#{sep}]+)?#{sep}/ end def format_backtrace(backtrace, options={}) @@ -54,9 +53,7 @@ def backtrace_line(line) def exclude?(line) return false if @full_backtrace - relative_line = Metadata.relative_path(line) - return false unless matches?(@exclusion_patterns, relative_line) - matches?(@system_exclusion_patterns, relative_line) || !matches?(@inclusion_patterns, line) + matches?(exclusion_patterns, line) && !matches?(inclusion_patterns, line) end private diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 6704fd66b5..f5dd8f9543 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -32,15 +32,23 @@ module Core class Configuration include RSpec::Core::Hooks + # Module that holds `attr_reader` declarations. It's in a separate + # module to allow us to override those methods and use `super`. + # @private + Readers = Module.new + include Readers + # @private class MustBeConfiguredBeforeExampleGroupsError < StandardError; end # @private def self.define_reader(name) - define_method(name) do - variable = instance_variable_defined?("@#{name}") ? instance_variable_get("@#{name}") : nil - value_for(name, variable) + Readers.class_eval do + remove_method name if method_defined?(name) + attr_reader name end + + define_method(name) { value_for(name) { super() } } end # @private @@ -71,7 +79,7 @@ def self.add_setting(name, opts={}) # @private # - # As `add_setting` but only add the reader + # As `add_setting` but only add the reader. def self.add_read_only_setting(name, opts={}) raise "Use the instance add_setting method if you want to set a default" if opts.key?(:default) define_reader name @@ -90,8 +98,8 @@ def self.add_read_only_setting(name, opts={}) # `"spec"`). Allows you to just type `rspec` instead of `rspec spec` to # run all the examples in the `spec` directory. # - # Note: Other scripts invoking `rspec` indirectly will ignore this - # setting. + # @note Other scripts invoking `rspec` indirectly will ignore this + # setting. add_setting :default_path # @macro add_setting @@ -160,11 +168,12 @@ def deprecation_stream=(value) add_setting :failure_exit_code # @macro define_reader - # Indicates files configured to be required + # Indicates files configured to be required. define_reader :requires # @macro define_reader - # Returns dirs that have been prepended to the load path by the `-I` command line option + # Returns dirs that have been prepended to the load path by the `-I` + # command line option. define_reader :libs # @macro add_setting @@ -172,7 +181,7 @@ def deprecation_stream=(value) # Default: `$stdout`. define_reader :output_stream - # Set the output stream for reporter + # Set the output stream for reporter. # @attr value [IO] value for output, defaults to $stdout def output_stream=(value) if @reporter && !value.equal?(@output_stream) @@ -186,20 +195,20 @@ def output_stream=(value) end # @macro define_reader - # Load files matching this pattern (default: `'**{,/*/**}/*_spec.rb'`) + # Load files matching this pattern (default: `'**{,/*/**}/*_spec.rb'`). define_reader :pattern - # Set pattern to match files to load + # Set pattern to match files to load. # @attr value [String] the filename pattern to filter spec files by def pattern=(value) update_pattern_attr :pattern, value end # @macro define_reader - # Exclude files matching this pattern + # Exclude files matching this pattern. define_reader :exclude_pattern - # Set pattern to match files to exclude + # Set pattern to match files to exclude. # @attr value [String] the filename pattern to exclude spec files by def exclude_pattern=(value) update_pattern_attr :exclude_pattern, value @@ -211,84 +220,93 @@ def exclude_pattern=(value) add_setting :profile_examples # @macro add_setting - # Run all examples if none match the configured filters (default: `false`). + # Run all examples if none match the configured filters + # (default: `false`). add_setting :run_all_when_everything_filtered # @macro add_setting # Color to use to indicate success. # @param color [Symbol] defaults to `:green` but can be set to one of the - # following: `[:black, :white, :red, :green, :yellow, - # :blue, :magenta, :cyan]` + # following: `[:black, :white, :red, :green, :yellow, :blue, :magenta, + # :cyan]` add_setting :success_color # @macro add_setting # Color to use to print pending examples. # @param color [Symbol] defaults to `:yellow` but can be set to one of the - # following: `[:black, :white, :red, :green, :yellow, - # :blue, :magenta, :cyan]` + # following: `[:black, :white, :red, :green, :yellow, :blue, :magenta, + # :cyan]` add_setting :pending_color # @macro add_setting # Color to use to indicate failure. # @param color [Symbol] defaults to `:red` but can be set to one of the - # following: `[:black, :white, :red, :green, :yellow, - # :blue, :magenta, :cyan]` + # following: `[:black, :white, :red, :green, :yellow, :blue, :magenta, + # :cyan]` add_setting :failure_color # @macro add_setting # The default output color. # @param color [Symbol] defaults to `:white` but can be set to one of the - # following:`[:black, :white, :red, :green, :yellow, - # :blue, :magenta, :cyan]` + # following: `[:black, :white, :red, :green, :yellow, :blue, :magenta, + # :cyan]` add_setting :default_color # @macro add_setting # Color used when a pending example is fixed. # @param color [Symbol] defaults to `:blue` but can be set to one of the - # following: `[:black, :white, :red, :green, :yellow, - # :blue, :magenta, :cyan]` + # following: `[:black, :white, :red, :green, :yellow, :blue, :magenta, + # :cyan]` add_setting :fixed_color # @macro add_setting # Color used to print details. # @param color [Symbol] defaults to `:cyan` but can be set to one of the - # following: `[:black, :white, :red, :green, :yellow, - # :blue, :magenta, :cyan]` + # following: `[:black, :white, :red, :green, :yellow, :blue, :magenta, + # :cyan]` add_setting :detail_color # Deprecated. This config option was added in RSpec 2 to pave the way # for this being the default behavior in RSpec 3. Now this option is # a no-op. def treat_symbols_as_metadata_keys_with_true_values=(_value) - RSpec.deprecate("RSpec::Core::Configuration#treat_symbols_as_metadata_keys_with_true_values=", - :message => "RSpec::Core::Configuration#treat_symbols_as_metadata_keys_with_true_values= " \ - "is deprecated, it is now set to true as default and setting it to false has no effect.") + RSpec.deprecate( + "RSpec::Core::Configuration#treat_symbols_as_metadata_keys_with_true_values=", + :message => "RSpec::Core::Configuration#treat_symbols_as_metadata_keys_with_true_values= " \ + "is deprecated, it is now set to true as default and " \ + "setting it to false has no effect." + ) end - # Record the start time of the spec suite to measure load time + # Record the start time of the spec suite to measure load time. add_setting :start_time # @private add_setting :tty # @private - add_setting :include_or_extend_modules - # @private attr_writer :files_to_run # @private - add_setting :expecting_with_rspec - # @private attr_accessor :filter_manager # @private - attr_reader :backtrace_formatter, :ordering_manager + attr_accessor :static_config_filter_manager + # @private + attr_reader :backtrace_formatter, :ordering_manager, :loaded_spec_files def initialize # rubocop:disable Style/GlobalVars @start_time = $_rspec_core_load_started_at || ::RSpec::Core::Time.now # rubocop:enable Style/GlobalVars @expectation_frameworks = [] - @include_or_extend_modules = [] + @include_modules = FilterableItemRepository::QueryOptimized.new(:any?) + @extend_modules = FilterableItemRepository::QueryOptimized.new(:any?) + @prepend_modules = FilterableItemRepository::QueryOptimized.new(:any?) + + @before_suite_hooks = [] + @after_suite_hooks = [] + @mock_framework = nil @files_or_directories_to_run = [] + @loaded_spec_files = Set.new @color = false @pattern = '**{,/*/**}/*_spec.rb' @exclude_pattern = '' @@ -303,6 +321,7 @@ def initialize @reporter = nil @reporter_buffer = nil @filter_manager = FilterManager.new + @static_config_filter_manager = FilterManager.new @ordering_manager = Ordering::ConfigurationManager.new @preferred_options = {} @failure_color = :red @@ -314,7 +333,7 @@ def initialize @profile_examples = false @requires = [] @libs = [] - @derived_metadata_blocks = [] + @derived_metadata_blocks = FilterableItemRepository::QueryOptimized.new(:any?) end # @private @@ -332,18 +351,29 @@ def reset @formatter_loader = nil end + # @private + def reset_filters + self.filter_manager = FilterManager.new + filter_manager.include_only( + Metadata.deep_hash_dup(static_config_filter_manager.inclusions.rules) + ) + filter_manager.exclude_only( + Metadata.deep_hash_dup(static_config_filter_manager.exclusions.rules) + ) + end + # @overload add_setting(name) # @overload add_setting(name, opts) # @option opts [Symbol] :default # - # set a default value for the generated getter and predicate methods: + # Set a default value for the generated getter and predicate methods: # # add_setting(:foo, :default => "default value") # # @option opts [Symbol] :alias_with # - # Use `:alias_with` to alias the setter, getter, and predicate to another - # name, or names: + # Use `:alias_with` to alias the setter, getter, and predicate to + # another name, or names: # # add_setting(:foo, :alias_with => :bar) # add_setting(:foo, :alias_with => [:bar, :baz]) @@ -366,7 +396,7 @@ def reset # # RSpec.configuration.foo=(value) # RSpec.configuration.foo - # RSpec.configuration.foo? # returns true if foo returns anything but nil or false + # RSpec.configuration.foo? # Returns true if foo returns anything but nil or false. def add_setting(name, opts={}) default = opts.delete(:default) (class << self; self; end).class_exec do @@ -375,7 +405,7 @@ def add_setting(name, opts={}) __send__("#{name}=", default) if default end - # Returns the configured mock framework adapter module + # Returns the configured mock framework adapter module. def mock_framework if @mock_framework.nil? begin @@ -387,7 +417,7 @@ def mock_framework @mock_framework end - # Delegates to mock_framework=(framework) + # Delegates to mock_framework=(framework). def mock_framework=(framework) mock_with framework end @@ -395,19 +425,19 @@ def mock_framework=(framework) # Regexps used to exclude lines from backtraces. # # Excludes lines from ruby (and jruby) source, installed gems, anything - # in any "bin" directory, and any of the rspec libs (outside gem + # in any "bin" directory, and any of the RSpec libs (outside gem # installs) by default. # # You can modify the list via the getter, or replace it with the setter. # # To override this behaviour and display a full backtrace, use - # `--backtrace`on the command line, in a `.rspec` file, or in the + # `--backtrace` on the command line, in a `.rspec` file, or in the # `rspec_options` attribute of RSpec's rake task. def backtrace_exclusion_patterns @backtrace_formatter.exclusion_patterns end - # Set regular expressions used to exclude lines in backtrace + # Set regular expressions used to exclude lines in backtrace. # @param patterns [Regexp] set the backtrace exlusion pattern def backtrace_exclusion_patterns=(patterns) @backtrace_formatter.exclusion_patterns = patterns @@ -425,7 +455,7 @@ def backtrace_inclusion_patterns @backtrace_formatter.inclusion_patterns end - # Set regular expressions used to include lines in backtrace + # Set regular expressions used to include lines in backtrace. # @attr patterns [Regexp] set backtrace_formatter inclusion_patterns def backtrace_inclusion_patterns=(patterns) @backtrace_formatter.inclusion_patterns = patterns @@ -485,8 +515,8 @@ def filter_gems_from_backtrace(*gem_names) # teardown_mocks_for_rspec # - called after verify_mocks_for_rspec (even if there are errors) # - # If the module responds to `configuration` and `mock_with` receives a block, - # it will yield the configuration object to the block e.g. + # If the module responds to `configuration` and `mock_with` receives a + # block, it will yield the configuration object to the block e.g. # # config.mock_with OtherMockFrameworkAdapter do |mod_config| # mod_config.custom_setting = true @@ -515,7 +545,8 @@ def mock_with(framework) end if block_given? - raise "#{framework_module} must respond to `configuration` so that mock_with can yield it." unless framework_module.respond_to?(:configuration) + raise "#{framework_module} must respond to `configuration` so that " \ + "mock_with can yield it." unless framework_module.respond_to?(:configuration) yield framework_module.configuration end @@ -534,7 +565,7 @@ def expectation_frameworks @expectation_frameworks end - # Delegates to expect_with(framework) + # Delegates to expect_with(framework). def expectation_framework=(framework) expect_with(framework) end @@ -569,7 +600,6 @@ def expect_with(*frameworks) framework when :rspec require 'rspec/expectations' - self.expecting_with_rspec = true ::RSpec::Matchers when :test_unit require 'rspec/core/test_unit_assertions_adapter' @@ -587,21 +617,24 @@ def expect_with(*frameworks) end if block_given? - raise "expect_with only accepts a block with a single argument. Call expect_with #{modules.length} times, once with each argument, instead." if modules.length > 1 - raise "#{modules.first} must respond to `configuration` so that expect_with can yield it." unless modules.first.respond_to?(:configuration) + raise "expect_with only accepts a block with a single argument. " \ + "Call expect_with #{modules.length} times, " \ + "once with each argument, instead." if modules.length > 1 + raise "#{modules.first} must respond to `configuration` so that " \ + "expect_with can yield it." unless modules.first.respond_to?(:configuration) yield modules.first.configuration end @expectation_frameworks.push(*modules) end - # Check if full backtrace is enabled + # Check if full backtrace is enabled. # @return [Boolean] is full backtrace enabled def full_backtrace? @backtrace_formatter.full_backtrace? end - # Toggle full backtrace + # Toggle full backtrace. # @attr true_or_false [Boolean] toggle full backtrace display def full_backtrace=(true_or_false) @backtrace_formatter.full_backtrace = true_or_false @@ -613,10 +646,10 @@ def full_backtrace=(true_or_false) # @see color_enabled? # @return [Boolean] def color - value_for(:color, @color) + value_for(:color) { @color } end - # Check if color is enabled for a particular output + # Check if color is enabled for a particular output. # @param output [IO] an output stream to use, defaults to the current # `output_stream` # @return [Boolean] @@ -624,13 +657,15 @@ def color_enabled?(output=output_stream) output_to_tty?(output) && color end - # Toggle output color + # Toggle output color. # @attr true_or_false [Boolean] toggle color enabled def color=(true_or_false) return unless true_or_false - if RSpec.world.windows_os? && !ENV['ANSICON'] - RSpec.warning "You must use ANSICON 1.31 or later (http://adoxa.3eeweb.com/ansicon/) to use colour on Windows" + if RSpec::Support::OS.windows? && !ENV['ANSICON'] + RSpec.warning "You must use ANSICON 1.31 or later " \ + "(http://adoxa.3eeweb.com/ansicon/) to use colour " \ + "on Windows" @color = false else @color = true @@ -743,10 +778,10 @@ def reporter # @api private # - # Defaults `profile_examples` to 10 examples when `@profile_examples` is `true`. - # + # Defaults `profile_examples` to 10 examples when `@profile_examples` is + # `true`. def profile_examples - profile = value_for(:profile_examples, @profile_examples) + profile = value_for(:profile_examples) { @profile_examples } if profile && !profile.is_a?(Integer) 10 else @@ -762,7 +797,7 @@ def files_or_directories_to_run=(*files) @files_to_run = nil end - # The spec files RSpec will run + # The spec files RSpec will run. # @return [Array] specified files about to run def files_to_run @files_to_run ||= get_files_to_run(@files_or_directories_to_run) @@ -776,8 +811,8 @@ def files_to_run # @note The specific example alias below (`pending`) is already # defined for you. # @note Use with caution. This extends the language used in your - # specs, but does not add any additional documentation. We use this - # in rspec to define methods like `focus` and `xit`, but we also add + # specs, but does not add any additional documentation. We use this + # in RSpec to define methods like `focus` and `xit`, but we also add # docs for those methods. # # @example @@ -860,8 +895,8 @@ def alias_example_group_to(new_name, *args) # # ...sortability examples here # # @note Use with caution. This extends the language used in your - # specs, but does not add any additional documentation. We use this - # in rspec to define `it_should_behave_like` (for backward + # specs, but does not add any additional documentation. We use this + # in RSpec to define `it_should_behave_like` (for backward # compatibility), but we also add docs for that method. def alias_it_behaves_like_to(new_name, report_label='') RSpec::Core::ExampleGroup.define_nested_shared_group_method(new_name, report_label) @@ -878,28 +913,30 @@ def alias_it_behaves_like_to(new_name, report_label='') # or config files (e.g. `.rspec`). # # @example - # # given this declaration + # # Given this declaration. # describe "something", :foo => 'bar' do # # ... # end # - # # any of the following will include that group + # # Any of the following will include that group. # config.filter_run_including :foo => 'bar' # config.filter_run_including :foo => /^ba/ # config.filter_run_including :foo => lambda {|v| v == 'bar'} # config.filter_run_including :foo => lambda {|v,m| m[:foo] == 'bar'} # - # # given a proc with an arity of 1, the lambda is passed the value related to the key, e.g. + # # Given a proc with an arity of 1, the lambda is passed the value + # # related to the key, e.g. # config.filter_run_including :foo => lambda {|v| v == 'bar'} # - # # given a proc with an arity of 2, the lambda is passed the value related to the key, - # # and the metadata itself e.g. + # # Given a proc with an arity of 2, the lambda is passed the value + # # related to the key, and the metadata itself e.g. # config.filter_run_including :foo => lambda {|v,m| m[:foo] == 'bar'} # # filter_run_including :foo # same as filter_run_including :foo => true def filter_run_including(*args) meta = Metadata.build_hash_from(args, :warn_about_example_group_filtering) filter_manager.include_with_low_priority meta + static_config_filter_manager.include_with_low_priority Metadata.deep_hash_dup(meta) end alias_method :filter_run, :filter_run_including @@ -936,28 +973,30 @@ def inclusion_filter # or config files (e.g. `.rspec`). # # @example - # # given this declaration + # # Given this declaration. # describe "something", :foo => 'bar' do # # ... # end # - # # any of the following will exclude that group + # # Any of the following will exclude that group. # config.filter_run_excluding :foo => 'bar' # config.filter_run_excluding :foo => /^ba/ # config.filter_run_excluding :foo => lambda {|v| v == 'bar'} # config.filter_run_excluding :foo => lambda {|v,m| m[:foo] == 'bar'} # - # # given a proc with an arity of 1, the lambda is passed the value related to the key, e.g. + # # Given a proc with an arity of 1, the lambda is passed the value + # # related to the key, e.g. # config.filter_run_excluding :foo => lambda {|v| v == 'bar'} # - # # given a proc with an arity of 2, the lambda is passed the value related to the key, - # # and the metadata itself e.g. + # # Given a proc with an arity of 2, the lambda is passed the value + # # related to the key, and the metadata itself e.g. # config.filter_run_excluding :foo => lambda {|v,m| m[:foo] == 'bar'} # # filter_run_excluding :foo # same as filter_run_excluding :foo => true def filter_run_excluding(*args) meta = Metadata.build_hash_from(args, :warn_about_example_group_filtering) filter_manager.exclude_with_low_priority meta + static_config_filter_manager.exclude_with_low_priority Metadata.deep_hash_dup(meta) end # Clears and reassigns the `exclusion_filter`. Set to `nil` if you don't @@ -979,8 +1018,8 @@ def exclusion_filter end # Tells RSpec to include `mod` in example groups. Methods defined in - # `mod` are exposed to examples (not example groups). Use `filters` to - # constrain the groups in which to include the module. + # `mod` are exposed to examples (not example groups). Use `filters` to + # constrain the groups or examples in which to include the module. # # @example # @@ -1009,14 +1048,22 @@ def exclusion_filter # end # end # + # @note Filtered module inclusions can also be applied to + # individual examples that have matching metadata. Just like + # Ruby's object model is that every object has a singleton class + # which has only a single instance, RSpec's model is that every + # example has a singleton example group containing just the one + # example. + # # @see #extend + # @see #prepend def include(mod, *filters) meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering) - include_or_extend_modules << [:include, mod, meta] + @include_modules.append(mod, meta) end - # Tells RSpec to extend example groups with `mod`. Methods defined in - # `mod` are exposed to example groups (not examples). Use `filters` to + # Tells RSpec to extend example groups with `mod`. Methods defined in + # `mod` are exposed to example groups (not examples). Use `filters` to # constrain the groups to extend. # # Similar to `include`, but behavior is added to example groups, which @@ -1044,25 +1091,87 @@ def include(mod, *filters) # end # # @see #include + # @see #prepend def extend(mod, *filters) meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering) - include_or_extend_modules << [:extend, mod, meta] + @extend_modules.append(mod, meta) + end + + if RSpec::Support::RubyFeatures.module_prepends_supported? + # Tells RSpec to prepend example groups with `mod`. Methods defined in + # `mod` are exposed to examples (not example groups). Use `filters` to + # constrain the groups in which to prepend the module. + # + # Similar to `include`, but module is included before the example group's class + # in the ancestor chain. + # + # @example + # + # module OverrideMod + # def override_me + # "overridden" + # end + # end + # + # RSpec.configure do |config| + # config.prepend(OverrideMod, :method => :prepend) + # end + # + # describe "overriding example's class", :method => :prepend do + # it "finds the user" do + # self.class.class_eval do + # def override_me + # end + # end + # override_me # => "overridden" + # # ... + # end + # end + # + # @see #include + # @see #extend + def prepend(mod, *filters) + meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering) + @prepend_modules.append(mod, meta) + end end # @private # - # Used internally to extend a group with modules using `include` and/or + # Used internally to extend a group with modules using `include`, `prepend` and/or # `extend`. def configure_group(group) - include_or_extend_modules.each do |include_or_extend, mod, filters| - next unless filters.empty? || group.any_apply?(filters) - __send__("safe_#{include_or_extend}", mod, group) + configure_group_with group, @include_modules, :safe_include + configure_group_with group, @extend_modules, :safe_extend + configure_group_with group, @prepend_modules, :safe_prepend + end + + # @private + def configure_group_with(group, module_list, application_method) + module_list.items_for(group.metadata).each do |mod| + __send__(application_method, mod, group) end end # @private - def safe_include(mod, host) - host.__send__(:include, mod) unless host < mod + # + # Used internally to extend the singleton class of a single example's + # example group instance with modules using `include` and/or `extend`. + def configure_example(example) + # We replace the metadata so that SharedExampleGroupModule#included + # has access to the example's metadata[:location]. + example.example_group_instance.singleton_class.with_replaced_metadata(example.metadata) do + @include_modules.items_for(example.metadata).each do |mod| + safe_include(mod, example.example_group_instance.singleton_class) + end + end + end + + if RSpec::Support::RubyFeatures.module_prepends_supported? + # @private + def safe_prepend(mod, host) + host.__send__(:prepend, mod) unless host < mod + end end # @private @@ -1075,11 +1184,21 @@ def requires=(paths) # @private if RUBY_VERSION.to_f >= 1.9 + # @private + def safe_include(mod, host) + host.__send__(:include, mod) unless host < mod + end + # @private def safe_extend(mod, host) host.extend(mod) unless host.singleton_class < mod end else + # @private + def safe_include(mod, host) + host.__send__(:include, mod) unless host.included_modules.include?(mod) + end + # @private def safe_extend(mod, host) host.extend(mod) unless (class << host; self; end).included_modules.include?(mod) @@ -1102,7 +1221,12 @@ def configure_expectation_framework # @private def load_spec_files - files_to_run.uniq.each { |f| load File.expand_path(f) } + files_to_run.uniq.each do |f| + file = File.expand_path(f) + load file + loaded_spec_files << file + end + @spec_files_loaded = true end @@ -1112,7 +1236,8 @@ def load_spec_files # Formats the docstring output using the block provided. # # @example - # # This will strip the descriptions of both examples and example groups. + # # This will strip the descriptions of both examples and example + # # groups. # RSpec.configure do |config| # config.format_docstrings { |s| s.strip } # end @@ -1157,7 +1282,8 @@ def self.delegate_to_ordering_manager(*methods) # @macro delegate_to_ordering_manager # - # Sets the default global order and, if order is `'rand:'`, also sets the seed. + # Sets the default global order and, if order is `'rand:'`, also + # sets the seed. delegate_to_ordering_manager :order= # @macro delegate_to_ordering_manager @@ -1167,8 +1293,10 @@ def self.delegate_to_ordering_manager(*methods) # # @param name [Symbol] The name of the ordering. # @yield Block that will order the given examples or example groups - # @yieldparam list [Array, Array] The examples or groups to order - # @yieldreturn [Array, Array] The re-ordered examples or groups + # @yieldparam list [Array, + # Array] The examples or groups to order + # @yieldreturn [Array, + # Array] The re-ordered examples or groups # # @example # RSpec.configure do |rspec| @@ -1189,7 +1317,7 @@ def self.delegate_to_ordering_manager(*methods) # @private delegate_to_ordering_manager :seed_used?, :ordering_registry - # Set Ruby warnings on or off + # Set Ruby warnings on or off. def warnings=(value) $VERBOSE = !!value end @@ -1252,7 +1380,7 @@ def raise_errors_for_deprecations! # `shared_examples_for`, etc) onto `main` and `Module`, instead # requiring you to prefix these methods with `RSpec.`. It enables # expect-only syntax for rspec-mocks and rspec-expectations. It - # simply disables monkey patching on whatever pieces of rspec + # simply disables monkey patching on whatever pieces of RSpec # the user is using. # # @note It configures rspec-mocks and rspec-expectations only @@ -1265,7 +1393,7 @@ def raise_errors_for_deprecations! # # @example # - # # It disables all monkey patching + # # It disables all monkey patching. # RSpec.configure do |config| # config.disable_monkey_patching! # end @@ -1295,38 +1423,156 @@ def disable_monkey_patching! # Defines a callback that can assign derived metadata values. # - # @param filters [Array, Hash] metadata filters that determine which example - # or group metadata hashes the callback will be triggered for. If none are given, - # the callback will be run against the metadata hashes of all groups and examples. - # @yieldparam metadata [Hash] original metadata hash from an example or group. Mutate this in - # your block as needed. + # @param filters [Array, Hash] metadata filters that determine + # which example or group metadata hashes the callback will be triggered + # for. If none are given, the callback will be run against the metadata + # hashes of all groups and examples. + # @yieldparam metadata [Hash] original metadata hash from an example or + # group. Mutate this in your block as needed. # # @example # RSpec.configure do |config| - # # Tag all groups and examples in the spec/unit directory with :type => :unit + # # Tag all groups and examples in the spec/unit directory with + # # :type => :unit # config.define_derived_metadata(:file_path => %r{/spec/unit/}) do |metadata| # metadata[:type] = :unit # end # end def define_derived_metadata(*filters, &block) meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering) - @derived_metadata_blocks << [meta, block] + @derived_metadata_blocks.append(block, meta) end # @private def apply_derived_metadata_to(metadata) - @derived_metadata_blocks.each do |filter, block| - block.call(metadata) if filter.empty? || MetadataFilter.any_apply?(filter, metadata) + @derived_metadata_blocks.items_for(metadata).each do |block| + block.call(metadata) end end + # Defines a `before` hook. See {Hooks#before} for full docs. + # + # This method differs from {Hooks#before} in only one way: it supports + # the `:suite` scope. Hooks with the `:suite` scope will be run once before + # the first example of the entire suite is executed. + # + # @see #prepend_before + # @see #after + # @see #append_after + def before(*args, &block) + handle_suite_hook(args, @before_suite_hooks, :push, + Hooks::BeforeHook, block) || super(*args, &block) + end + alias_method :append_before, :before + + # Adds `block` to the start of the list of `before` blocks in the same + # scope (`:example`, `:context`, or `:suite`), in contrast to {#before}, + # which adds the hook to the end of the list. + # + # See {Hooks#before} for full `before` hook docs. + # + # This method differs from {Hooks#prepend_before} in only one way: it supports + # the `:suite` scope. Hooks with the `:suite` scope will be run once before + # the first example of the entire suite is executed. + # + # @see #before + # @see #after + # @see #append_after + def prepend_before(*args, &block) + handle_suite_hook(args, @before_suite_hooks, :unshift, + Hooks::BeforeHook, block) || super(*args, &block) + end + + # Defines a `after` hook. See {Hooks#after} for full docs. + # + # This method differs from {Hooks#after} in only one way: it supports + # the `:suite` scope. Hooks with the `:suite` scope will be run once after + # the last example of the entire suite is executed. + # + # @see #append_after + # @see #before + # @see #prepend_before + def after(*args, &block) + handle_suite_hook(args, @after_suite_hooks, :unshift, + Hooks::AfterHook, block) || super(*args, &block) + end + alias_method :prepend_after, :after + + # Adds `block` to the end of the list of `after` blocks in the same + # scope (`:example`, `:context`, or `:suite`), in contrast to {#after}, + # which adds the hook to the start of the list. + # + # See {Hooks#after} for full `after` hook docs. + # + # This method differs from {Hooks#append_after} in only one way: it supports + # the `:suite` scope. Hooks with the `:suite` scope will be run once after + # the last example of the entire suite is executed. + # + # @see #append_after + # @see #before + # @see #prepend_before + def append_after(*args, &block) + handle_suite_hook(args, @after_suite_hooks, :push, + Hooks::AfterHook, block) || super(*args, &block) + end + + # @private + def with_suite_hooks + return yield if dry_run? + + hook_context = SuiteHookContext.new + begin + run_hooks_with(@before_suite_hooks, hook_context) + yield + ensure + run_hooks_with(@after_suite_hooks, hook_context) + end + end + + # @private + # Holds the various registered hooks. Here we use a FilterableItemRepository + # implementation that is specifically optimized for the read/write patterns + # of the config object. + def hooks + @hooks ||= HookCollections.new(self, FilterableItemRepository::QueryOptimized) + end + private + def handle_suite_hook(args, collection, append_or_prepend, hook_type, block) + scope, meta = *args + return nil unless scope == :suite + + if meta + # TODO: in RSpec 4, consider raising an error here. + # We warn only for backwards compatibility. + RSpec.warn_with "WARNING: `:suite` hooks do not support metadata since " \ + "they apply to the suite as a whole rather than " \ + "any individual example or example group that has metadata. " \ + "The metadata you have provided (#{meta.inspect}) will be ignored." + end + + collection.__send__(append_or_prepend, hook_type.new(block, {})) + end + + def run_hooks_with(hooks, hook_context) + hooks.each { |h| h.run(hook_context) } + end + def get_files_to_run(paths) - FlatMap.flat_map(paths) do |path| + FlatMap.flat_map(paths_to_check(paths)) do |path| path = path.gsub(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR File.directory?(path) ? gather_directories(path) : extract_location(path) - end.sort + end.sort.uniq + end + + def paths_to_check(paths) + return paths if pattern_might_load_specs_from_vendored_dirs? + paths + [Dir.getwd] + end + + def pattern_might_load_specs_from_vendored_dirs? + pattern.split(File::SEPARATOR).first.include?('**') end def gather_directories(path) @@ -1336,8 +1582,28 @@ def gather_directories(path) end def get_matching_files(path, pattern) + Dir[file_glob_from(path, pattern)].map { |file| File.expand_path(file) } + end + + def file_glob_from(path, pattern) stripped = "{#{pattern.gsub(/\s*,\s*/, ',')}}" - pattern =~ /^#{Regexp.escape path}/ ? Dir[stripped] : Dir["#{path}/#{stripped}"] + return stripped if pattern =~ /^(\.\/)?#{Regexp.escape path}/ || absolute_pattern?(pattern) + File.join(path, stripped) + end + + if RSpec::Support::OS.windows? + def absolute_pattern?(pattern) + pattern =~ /\A[A-Z]:\\/ || windows_absolute_network_path?(pattern) + end + + def windows_absolute_network_path?(pattern) + return false unless ::File::ALT_SEPARATOR + pattern.start_with?(::File::ALT_SEPARATOR + ::File::ALT_SEPARATOR) + end + else + def absolute_pattern?(pattern) + pattern.start_with?(File::Separator) + end end def extract_location(path) @@ -1349,6 +1615,7 @@ def extract_location(path) filter_manager.add_location path, lines end + return [] if path == default_path path end @@ -1356,8 +1623,8 @@ def command $0.split(File::SEPARATOR).last end - def value_for(key, default=nil) - @preferred_options.key?(key) ? @preferred_options[key] : default + def value_for(key) + @preferred_options.fetch(key) { yield } end def assert_no_example_groups_defined(config_option) @@ -1398,7 +1665,8 @@ def rspec_expectations_loaded? def update_pattern_attr(name, value) if @spec_files_loaded - RSpec.warning "Configuring `#{name}` to #{value} has no effect since RSpec has already loaded the spec files." + RSpec.warning "Configuring `#{name}` to #{value} has no effect since " \ + "RSpec has already loaded the spec files." end instance_variable_set(:"@#{name}", value) diff --git a/lib/rspec/core/configuration_options.rb b/lib/rspec/core/configuration_options.rb index d6c1c0ecdd..36b9f5d05b 100644 --- a/lib/rspec/core/configuration_options.rb +++ b/lib/rspec/core/configuration_options.rb @@ -84,15 +84,22 @@ def order(keys) # set before it. :default_path, - # These must be set before `requires` to support checking `config.files_to_run` - # from within `spec_helper.rb` when a `-rspec_helper` option is used. + # These must be set before `requires` to support checking + # `config.files_to_run` from within `spec_helper.rb` when a + # `-rspec_helper` option is used. :files_or_directories_to_run, :pattern, :exclude_pattern, - # In general, we want to require the specified files as early as possible. - # The `--require` option is specifically intended to allow early requires. - # For later requires, they can just put the require in their spec files, but - # `--require` provides a unique opportunity for users to instruct RSpec to - # load an extension file early for maximum flexibility. + # Necessary so that the `--seed` option is applied before requires, + # in case required files do something with the provided seed. + # (such as seed global randomization with it). + :order, + + # In general, we want to require the specified files as early as + # possible. The `--require` option is specifically intended to allow + # early requires. For later requires, they can just put the require in + # their spec files, but `--require` provides a unique opportunity for + # users to instruct RSpec to load an extension file early for maximum + # flexibility. :requires ] diff --git a/lib/rspec/core/dsl.rb b/lib/rspec/core/dsl.rb index bd78cc610e..9f290b997d 100644 --- a/lib/rspec/core/dsl.rb +++ b/lib/rspec/core/dsl.rb @@ -8,7 +8,7 @@ module Core # By default the methods `describe`, `context` and `example_group` # are exposed. These methods define a named context for one or # more examples. The given block is evaluated in the context of - # a generated subclass of {RSpec::Core::ExampleGroup} + # a generated subclass of {RSpec::Core::ExampleGroup}. # # ## Examples: # @@ -35,6 +35,8 @@ def self.exposed_globally? # @private def self.expose_example_group_alias(name) + return if example_group_aliases.include?(name) + example_group_aliases << name (class << RSpec; self; end).__send__(:define_method, name) do |*args, &example_group_block| @@ -49,7 +51,7 @@ class << self attr_accessor :top_level end - # Adds the describe method to Module and the top level binding + # Adds the describe method to Module and the top level binding. # @api private def self.expose_globally! return if exposed_globally? @@ -61,7 +63,7 @@ def self.expose_globally! @exposed_globally = true end - # Removes the describe method from Module and the top level binding + # Removes the describe method from Module and the top level binding. # @api private def self.remove_globally! return unless exposed_globally? @@ -76,6 +78,7 @@ def self.remove_globally! # @private def self.expose_example_group_alias_globally(method_name) change_global_dsl do + remove_method(method_name) if method_defined?(method_name) define_method(method_name) { |*a, &b| ::RSpec.__send__(method_name, *a, &b) } end end @@ -89,5 +92,5 @@ def self.change_global_dsl(&changes) end end -# capture main without an eval +# Capture main without an eval. ::RSpec::Core::DSL.top_level = self diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb index 0304da46f8..8d02d6ef72 100644 --- a/lib/rspec/core/example.rb +++ b/lib/rspec/core/example.rb @@ -44,14 +44,15 @@ module Core class Example # @private # - # Used to define methods that delegate to this example's metadata + # Used to define methods that delegate to this example's metadata. def self.delegate_to_metadata(key) define_method(key) { @metadata[key] } end # @return [ExecutionResult] represents the result of running this example. delegate_to_metadata :execution_result - # @return [String] the relative path to the file where this example was defined. + # @return [String] the relative path to the file where this example was + # defined. delegate_to_metadata :file_path # @return [String] the full description (including the docstrings of # all parent example groups). @@ -59,22 +60,22 @@ def self.delegate_to_metadata(key) # @return [String] the exact source location of this example in a form # like `./path/to/spec.rb:17` delegate_to_metadata :location - # @return [Boolean] flag that indicates that the example is not expected to pass. - # It will be run and will either have a pending result (if a failure occurs) - # or a failed result (if no failure occurs). + # @return [Boolean] flag that indicates that the example is not expected + # to pass. It will be run and will either have a pending result (if a + # failure occurs) or a failed result (if no failure occurs). delegate_to_metadata :pending # @return [Boolean] flag that will cause the example to not run. # The {ExecutionResult} status will be `:pending`. delegate_to_metadata :skip # Returns the string submitted to `example` or its aliases (e.g. - # `specify`, `it`, etc). If no string is submitted (e.g. `it { is_expected.to - # do_something }`) it returns the message generated by the matcher if - # there is one, otherwise returns a message including the location of the - # example. + # `specify`, `it`, etc). If no string is submitted (e.g. + # `it { is_expected.to do_something }`) it returns the message generated + # by the matcher if there is one, otherwise returns a message including + # the location of the example. def description description = if metadata[:description].to_s.empty? - "example at #{location}" + location_description else metadata[:description] end @@ -82,10 +83,28 @@ def description RSpec.configuration.format_docstrings_block.call(description) end + # Returns a description of the example that always includes the location. + def inspect_output + inspect_output = "\"#{description}\"" + unless metadata[:description].to_s.empty? + inspect_output << " (#{location})" + end + inspect_output + end + + # Returns the argument that can be passed to the `rspec` command to rerun this example. + def rerun_argument + loaded_spec_files = RSpec.configuration.loaded_spec_files + + Metadata.ascending(metadata) do |meta| + return meta[:location] if loaded_spec_files.include?(meta[:absolute_file_path]) + end + end + # @attr_reader # # Returns the first exception raised in the context of running this - # example (nil if no exception is raised) + # example (nil if no exception is raised). attr_reader :exception # @attr_reader @@ -105,10 +124,14 @@ def description attr_accessor :clock # Creates a new instance of Example. - # @param example_group_class [Class] the subclass of ExampleGroup in which this Example is declared - # @param description [String] the String passed to the `it` method (or alias) - # @param user_metadata [Hash] additional args passed to `it` to be used as metadata - # @param example_block [Proc] the block of code that represents the example + # @param example_group_class [Class] the subclass of ExampleGroup in which + # this Example is declared + # @param description [String] the String passed to the `it` method (or + # alias) + # @param user_metadata [Hash] additional args passed to `it` to be used as + # metadata + # @param example_block [Proc] the block of code that represents the + # example # @api private def initialize(example_group_class, description, user_metadata, example_block=nil) @example_group_class = example_group_class @@ -137,15 +160,18 @@ def example_group # @param example_group_instance the instance of an ExampleGroup subclass def run(example_group_instance, reporter) @example_group_instance = example_group_instance + hooks.register_global_singleton_context_hooks(self, RSpec.configuration.hooks) + RSpec.configuration.configure_example(self) RSpec.current_example = self start(reporter) + Pending.mark_pending!(self, pending) if pending? begin if skipped? Pending.mark_pending! self, skip elsif !RSpec.configuration.dry_run? - with_around_example_hooks do + with_around_and_singleton_context_hooks do begin run_before_example @example_group_instance.instance_exec(self, &@example_block) @@ -170,7 +196,7 @@ def run(example_group_instance, reporter) rescue Exception => e set_exception(e) ensure - @example_group_instance.instance_variables.each do |ivar| + ExampleGroup.each_instance_variable_for_example(@example_group_instance) do |ivar| @example_group_instance.instance_variable_set(ivar, nil) end @example_group_instance = nil @@ -193,7 +219,7 @@ def run(example_group_instance, reporter) # if ex.metadata[:key] == :some_value && some_global_condition # raise "some message" # end - # ex.run # run delegates to ex.call + # ex.run # run delegates to ex.call. # end # end # @@ -222,7 +248,8 @@ def call(*args, &block) end alias run call - # Provides a wrapped proc that will update our `executed?` state when executed. + # Provides a wrapped proc that will update our `executed?` state when + # executed. def to_proc method(:call).to_proc end @@ -249,21 +276,6 @@ def inspect end end - # @private - def any_apply?(filters) - MetadataFilter.any_apply?(filters, metadata) - end - - # @private - def all_apply?(filters) - MetadataFilter.all_apply?(filters, metadata) || @example_group_class.all_apply?(filters) - end - - # @private - def around_example_hooks - @around_example_hooks ||= example_group.hooks.around_example_hooks_for(self) - end - # @private # # Used internally to set an exception in an after hook, which @@ -302,7 +314,7 @@ def fail_with_exception(reporter, exception) # @private # # Used internally to skip without actually executing the example when - # skip is used in before(:context) + # skip is used in before(:context). def skip_with_exception(reporter, exception) start(reporter) Pending.mark_skipped! self, exception.argument @@ -323,12 +335,12 @@ def instance_exec(*args, &block) private - def with_around_example_hooks(&block) - if around_example_hooks.empty? - yield - else - @example_group_class.hooks.run(:around, :example, self, Procsy.new(self, &block)) - end + def hooks + example_group_instance.singleton_class.hooks + end + + def with_around_example_hooks + hooks.run(:around, :example, self) { yield } rescue Exception => e set_exception(e, "in an `around(:example)` hook") end @@ -364,15 +376,21 @@ def record_finished(status) def run_before_example @example_group_instance.setup_mocks_for_rspec - @example_group_class.hooks.run(:before, :example, self) + hooks.run(:before, :example, self) + end + + def with_around_and_singleton_context_hooks + singleton_context_hooks_host = example_group_instance.singleton_class + singleton_context_hooks_host.run_before_context_hooks(example_group_instance) + with_around_example_hooks { yield } + ensure + singleton_context_hooks_host.run_after_context_hooks(example_group_instance) end def run_after_example - @example_group_class.hooks.run(:after, :example, self) + assign_generated_description if defined?(::RSpec::Matchers) + hooks.run(:after, :example, self) verify_mocks - assign_generated_description if RSpec.configuration.expecting_with_rspec? - rescue Exception => e - set_exception(e, "in an `after(:example)` hook") ensure @example_group_instance.teardown_mocks_for_rspec end @@ -382,6 +400,7 @@ def verify_mocks rescue Exception => e if pending? execution_result.pending_fixed = false + execution_result.pending_exception = e @exception = nil else set_exception(e) @@ -393,16 +412,25 @@ def mocks_need_verification? end def assign_generated_description - if metadata[:description].empty? && (description = RSpec::Matchers.generated_description) + if metadata[:description].empty? && (description = generate_description) metadata[:description] = description metadata[:full_description] << description end - rescue Exception => e - set_exception(e, "while assigning the example description") ensure RSpec::Matchers.clear_generated_description end + def generate_description + RSpec::Matchers.generated_description + rescue Exception => e + location_description + " (Got an error when generating description " \ + "from matcher: #{e.class}: #{e.message} -- #{e.backtrace.first})" + end + + def location_description + "example at #{location}" + end + def skip_message if String === skip skip @@ -447,6 +475,15 @@ class ExecutionResult alias pending_fixed? pending_fixed + # @return [Boolean] Indicates if the example was completely skipped + # (typically done via `:skip` metadata or the `skip` method). Skipped examples + # will have a `:pending` result. A `:pending` result can also come from examples + # that were marked as `:pending`, which causes them to be run, and produces a + # `:failed` result if the example passes. + def example_skipped? + status == :pending && !pending_exception + end + # @api private # Records the finished status of the example. def record_finished(status, finished_at) @@ -492,7 +529,7 @@ def initialize super(AnonymousExampleGroup, "", {}) end - # To ensure we don't silence errors... + # To ensure we don't silence errors. def set_exception(exception, _context=nil) raise exception end diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index f12fedfc58..1738de33a9 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -3,7 +3,7 @@ module RSpec module Core # ExampleGroup and {Example} are the main structural elements of - # rspec-core. Consider this example: + # rspec-core. Consider this example: # # describe Thing do # it "does something" do @@ -17,13 +17,13 @@ module Core # # Example group bodies (e.g. `describe` or `context` blocks) are evaluated # in the context of a new subclass of ExampleGroup. Individual examples are - # evaluated in the context of an instance of the specific ExampleGroup subclass - # to which they belong. + # evaluated in the context of an instance of the specific ExampleGroup + # subclass to which they belong. # # Besides the class methods defined here, there are other interesting macros - # defined in {Hooks}, {MemoizedHelpers::ClassMethods} and {SharedExampleGroup}. - # There are additional instance methods available to your examples defined in - # {MemoizedHelpers} and {Pending}. + # defined in {Hooks}, {MemoizedHelpers::ClassMethods} and + # {SharedExampleGroup}. There are additional instance methods available to + # your examples defined in {MemoizedHelpers} and {Pending}. class ExampleGroup extend Hooks @@ -32,10 +32,11 @@ class ExampleGroup include Pending extend SharedExampleGroup - unless respond_to?(:define_singleton_method) - # @private - def self.define_singleton_method(*a, &b) - (class << self; self; end).__send__(:define_method, *a, &b) + # @private + def self.idempotently_define_singleton_method(name, &definition) + (class << self; self; end).module_exec do + remove_method(name) if method_defined?(name) + define_method(name, &definition) end end @@ -44,7 +45,21 @@ def self.define_singleton_method(*a, &b) # The [Metadata](Metadata) object associated with this group. # @see Metadata def self.metadata - @metadata if defined?(@metadata) + @metadata ||= nil + end + + # Temporarily replace the provided metadata. + # Intended primarily to allow an example group's singleton class + # to return the metadata of the example that it exists for. This + # is necessary for shared example group inclusion to work properly + # with singleton example groups. + # @private + def self.with_replaced_metadata(meta) + orig_metadata = metadata + @metadata = meta + yield + ensure + @metadata = orig_metadata end # @private @@ -56,7 +71,7 @@ def self.superclass_metadata # @private def self.delegate_to_metadata(*names) names.each do |name| - define_singleton_method(name) { metadata.fetch(name) } + idempotently_define_singleton_method(name) { metadata.fetch(name) } end end @@ -77,7 +92,6 @@ def self.description # end # end # - # def described_class self.class.described_class end @@ -89,9 +103,20 @@ def described_class # @private # @macro [attach] define_example_method # @!scope class - # @param name [String] - # @param extra_options [Hash] - # @param implementation [Block] + # @overload $1 + # @overload $1(&example_implementation) + # @param example_implementation [Block] The implementation of the example. + # @overload $1(doc_string, *metadata_keys, metadata={}) + # @param doc_string [String] The example's doc string. + # @param metadata [Hash] Metadata for the example. + # @param metadata_keys [Array] Metadata tags for the example. + # Will be transformed into hash entries with `true` values. + # @overload $1(doc_string, *metadata_keys, metadata={}, &example_implementation) + # @param doc_string [String] The example's doc string. + # @param metadata [Hash] Metadata for the example. + # @param metadata_keys [Array] Metadata tags for the example. + # Will be transformed into hash entries with `true` values. + # @param example_implementation [Block] The implementation of the example. # @yield [Example] the example object # @example # $1 do @@ -100,6 +125,9 @@ def described_class # $1 "does something" do # end # + # $1 "does something", :slow, :uses_js do + # end + # # $1 "does something", :with => 'additional metadata' do # end # @@ -107,23 +135,13 @@ def described_class # # ex is the Example object that contains metadata about the example # end def self.define_example_method(name, extra_options={}) - define_singleton_method(name) do |*all_args, &block| + idempotently_define_singleton_method(name) do |*all_args, &block| desc, *args = *all_args + options = Metadata.build_hash_from(args) options.update(:skip => RSpec::Core::Pending::NOT_YET_IMPLEMENTED) unless block options.update(extra_options) - # Metadata inheritance normally happens in `Example#initialize`, - # but for `:pending` specifically we need it earlier. - pending_metadata = options[:pending] || metadata[:pending] - - if pending_metadata - options, block = ExampleGroup.pending_metadata_and_block_for( - options.merge(:pending => pending_metadata), - block - ) - end - examples << RSpec::Core::Example.new(self, desc, options, block) examples.last end @@ -144,25 +162,25 @@ def self.define_example_method(name, extra_options={}) # end define_example_method :specify - # Shortcut to define an example with `:focus => true` + # Shortcut to define an example with `:focus => true`. # @see example define_example_method :focus, :focus => true - # Shortcut to define an example with `:focus => true` + # Shortcut to define an example with `:focus => true`. # @see example define_example_method :fexample, :focus => true - # Shortcut to define an example with `:focus => true` + # Shortcut to define an example with `:focus => true`. # @see example define_example_method :fit, :focus => true - # Shortcut to define an example with `:focus => true` + # Shortcut to define an example with `:focus => true`. # @see example define_example_method :fspecify, :focus => true - # Shortcut to define an example with `:skip => 'Temporarily skipped with xexample'` + # Shortcut to define an example with `:skip => 'Temporarily skipped with xexample'`. # @see example define_example_method :xexample, :skip => 'Temporarily skipped with xexample' - # Shortcut to define an example with `:skip => 'Temporarily skipped with xit'` + # Shortcut to define an example with `:skip => 'Temporarily skipped with xit'`. # @see example define_example_method :xit, :skip => 'Temporarily skipped with xit' - # Shortcut to define an example with `:skip => 'Temporarily skipped with xspecify'` + # Shortcut to define an example with `:skip => 'Temporarily skipped with xspecify'`. # @see example define_example_method :xspecify, :skip => 'Temporarily skipped with xspecify' # Shortcut to define an example with `:skip => true` @@ -177,11 +195,17 @@ def self.define_example_method(name, extra_options={}) # @!group Defining Example Groups # @private - # @macro [attach] alias_example_group_to + # @macro [attach] define_example_group_method # @!scope class - # @param name [String] The example group doc string - # @param metadata [Hash] Additional metadata to attach to the example group - # @yield The example group definition + # @overload $1 + # @overload $1(&example_group_definition) + # @param example_group_definition [Block] The definition of the example group. + # @overload $1(doc_string, *metadata_keys, metadata={}, &example_implementation) + # @param doc_string [String] The group's doc string. + # @param metadata [Hash] Metadata for the group. + # @param metadata_keys [Array] Metadata tags for the group. + # Will be transformed into hash entries with `true` values. + # @param example_group_definition [Block] The definition of the example group. # # Generates a subclass of this example group which inherits # everything except the examples themselves. @@ -205,7 +229,7 @@ def self.define_example_method(name, extra_options={}) # # @see DSL#describe def self.define_example_group_method(name, metadata={}) - define_singleton_method(name) do |*args, &example_group_block| + idempotently_define_singleton_method(name) do |*args, &example_group_block| thread_data = RSpec.thread_local_metadata top_level = self == ExampleGroup @@ -240,8 +264,8 @@ def self.define_example_group_method(name, metadata={}) define_example_group_method :example_group - # An alias of `example_group`. Generally used when grouping - # examples by a thing you are describing (e.g. an object, class or method). + # An alias of `example_group`. Generally used when grouping examples by a + # thing you are describing (e.g. an object, class or method). # @see example_group define_example_group_method :describe @@ -276,12 +300,12 @@ def self.define_example_group_method(name, metadata={}) # # @see SharedExampleGroup def self.define_nested_shared_group_method(new_name, report_label="it should behave like") - define_singleton_method(new_name) do |name, *args, &customization_block| - # Pass :caller so the :location metadata is set properly... - # otherwise, it'll be set to the next line because that's + idempotently_define_singleton_method(new_name) do |name, *args, &customization_block| + # Pass :caller so the :location metadata is set properly. + # Otherwise, it'll be set to the next line because that's # the block's source_location. - group = example_group("#{report_label} #{name}", :caller => caller) do - find_and_eval_shared("examples", name, *args, &customization_block) + group = example_group("#{report_label} #{name}", :caller => (the_caller = caller)) do + find_and_eval_shared("examples", name, the_caller.first, *args, &customization_block) end group.metadata[:shared_group_name] = name group @@ -297,32 +321,36 @@ def self.define_nested_shared_group_method(new_name, report_label="it should beh # Includes shared content mapped to `name` directly in the group in which # it is declared, as opposed to `it_behaves_like`, which creates a nested - # group. If given a block, that block is also eval'd in the current context. + # group. If given a block, that block is also eval'd in the current + # context. # # @see SharedExampleGroup def self.include_context(name, *args, &block) - find_and_eval_shared("context", name, *args, &block) + find_and_eval_shared("context", name, caller.first, *args, &block) end # Includes shared content mapped to `name` directly in the group in which # it is declared, as opposed to `it_behaves_like`, which creates a nested - # group. If given a block, that block is also eval'd in the current context. + # group. If given a block, that block is also eval'd in the current + # context. # # @see SharedExampleGroup def self.include_examples(name, *args, &block) - find_and_eval_shared("examples", name, *args, &block) + find_and_eval_shared("examples", name, caller.first, *args, &block) end # @private - def self.find_and_eval_shared(label, name, *args, &customization_block) + def self.find_and_eval_shared(label, name, inclusion_location, *args, &customization_block) shared_block = RSpec.world.shared_example_group_registry.find(parent_groups, name) unless shared_block raise ArgumentError, "Could not find shared #{label} #{name.inspect}" end - module_exec(*args, &shared_block) - module_exec(&customization_block) if customization_block + SharedExampleGroupInclusionStackFrame.with_frame(name, Metadata.relative_path(inclusion_location)) do + module_exec(*args, &shared_block) + module_exec(&customization_block) if customization_block + end end # @!endgroup @@ -348,10 +376,11 @@ def self.set_it_up(*args, &example_group_block) # Ruby 1.9 has a bug that can lead to infinite recursion and a # SystemStackError if you include a module in a superclass after # including it in a subclass: https://gist.github.com/845896 - # To prevent this, we must include any modules in RSpec::Core::ExampleGroup - # before users create example groups and have a chance to include - # the same module in a subclass of RSpec::Core::ExampleGroup. - # So we need to configure example groups here. + # To prevent this, we must include any modules in + # RSpec::Core::ExampleGroup before users create example groups and have + # a chance to include the same module in a subclass of + # RSpec::Core::ExampleGroup. So we need to configure example groups + # here. ensure_example_groups_are_configured description = args.shift @@ -363,7 +392,7 @@ def self.set_it_up(*args, &example_group_block) ) hooks.register_globals(self, RSpec.configuration.hooks) - RSpec.world.configure_group(self) + RSpec.configuration.configure_group(self) end # @private @@ -378,7 +407,8 @@ def self.filtered_examples # @private def self.descendant_filtered_examples - @descendant_filtered_examples ||= filtered_examples + children.inject([]) { |a, e| a + e.descendant_filtered_examples } + @descendant_filtered_examples ||= filtered_examples + + FlatMap.flat_map(children, &:descendant_filtered_examples) end # @private @@ -388,7 +418,7 @@ def self.children # @private def self.descendants - @_descendants ||= [self] + children.inject([]) { |a, e| a + e.descendants } + @_descendants ||= [self] + FlatMap.flat_map(children, &:descendants) end ## @private @@ -419,47 +449,66 @@ def self.before_context_ivars # @private def self.store_before_context_ivars(example_group_instance) - return if example_group_instance.instance_variables.empty? - - example_group_instance.instance_variables.each do |ivar| + each_instance_variable_for_example(example_group_instance) do |ivar| before_context_ivars[ivar] = example_group_instance.instance_variable_get(ivar) end end # @private def self.run_before_context_hooks(example_group_instance) - return if descendant_filtered_examples.empty? - begin - set_ivars(example_group_instance, superclass.before_context_ivars) + set_ivars(example_group_instance, superclass_before_context_ivars) + + ContextHookMemoizedHash::Before.isolate_for_context_hook(example_group_instance) do + hooks.run(:before, :context, example_group_instance) + end + ensure + store_before_context_ivars(example_group_instance) + end - ContextHookMemoizedHash::Before.isolate_for_context_hook(example_group_instance) do - hooks.run(:before, :context, example_group_instance) + if RUBY_VERSION.to_f >= 1.9 + # @private + def self.superclass_before_context_ivars + superclass.before_context_ivars + end + else # 1.8.7 + # @private + def self.superclass_before_context_ivars + if superclass.respond_to?(:before_context_ivars) + superclass.before_context_ivars + else + # `self` must be the singleton class of an ExampleGroup instance. + # On 1.8.7, the superclass of a singleton class of an instance of A + # is A's singleton class. On 1.9+, it's A. On 1.8.7, the first ancestor + # is A, so we can mirror 1.8.7's behavior here. Note that we have to + # search for the first that responds to `before_context_ivars` + # in case a module has been included in the singleton class. + ancestors.find { |a| a.respond_to?(:before_context_ivars) }.before_context_ivars end - ensure - store_before_context_ivars(example_group_instance) end end # @private def self.run_after_context_hooks(example_group_instance) - return if descendant_filtered_examples.empty? set_ivars(example_group_instance, before_context_ivars) ContextHookMemoizedHash::After.isolate_for_context_hook(example_group_instance) do hooks.run(:after, :context, example_group_instance) end + ensure + before_context_ivars.clear end - # Runs all the examples in this group - def self.run(reporter) + # Runs all the examples in this group. + def self.run(reporter=RSpec::Core::NullReporter.new) if RSpec.world.wants_to_quit RSpec.world.clear_remaining_example_groups if top_level? return end reporter.example_group_started(self) + should_run_context_hooks = descendant_filtered_examples.any? begin - run_before_context_hooks(new) + run_before_context_hooks(new('before(:context) hook')) if should_run_context_hooks result_for_this_group = run_examples(reporter) results_for_descendants = ordering_strategy.order(children).map { |child| child.run(reporter) }.all? result_for_this_group && results_for_descendants @@ -469,8 +518,7 @@ def self.run(reporter) RSpec.world.wants_to_quit = true if fail_fast? for_filtered_examples(reporter) { |example| example.fail_with_exception(reporter, ex) } ensure - run_after_context_hooks(new) - before_context_ivars.clear + run_after_context_hooks(new('after(:context) hook')) if should_run_context_hooks reporter.example_group_finished(self) end end @@ -495,7 +543,7 @@ def self.ordering_strategy def self.run_examples(reporter) ordering_strategy.order(filtered_examples).map do |example| next if RSpec.world.wants_to_quit - instance = new + instance = new(example.inspect_output) set_ivars(instance, before_context_ivars) succeeded = example.run(instance, reporter) RSpec.world.wants_to_quit = true if fail_fast? && !succeeded @@ -520,21 +568,11 @@ def self.fail_fast? RSpec.configuration.fail_fast? end - # @private - def self.any_apply?(filters) - MetadataFilter.any_apply?(filters, metadata) - end - - # @private - def self.all_apply?(filters) - MetadataFilter.all_apply?(filters, metadata) - end - # @private def self.declaration_line_numbers @declaration_line_numbers ||= [metadata[:line_number]] + examples.map { |e| e.metadata[:line_number] } + - children.inject([]) { |a, e| a + e.declaration_line_numbers } + FlatMap.flat_map(children, &:declaration_line_numbers) end # @private @@ -547,28 +585,68 @@ def self.set_ivars(instance, ivars) ivars.each { |name, value| instance.instance_variable_set(name, value) } end + if RUBY_VERSION.to_f < 1.9 + # @private + INSTANCE_VARIABLE_TO_IGNORE = '@__inspect_output'.freeze + else + # @private + INSTANCE_VARIABLE_TO_IGNORE = :@__inspect_output + end + # @private - def self.pending_metadata_and_block_for(options, block) - if String === options[:pending] - reason = options[:pending] - else - options[:pending] = true - reason = RSpec::Core::Pending::NO_REASON_GIVEN + def self.each_instance_variable_for_example(group) + group.instance_variables.each do |ivar| + yield ivar unless ivar == INSTANCE_VARIABLE_TO_IGNORE end + end - # Assign :caller so that the callback's source_location isn't used - # as the example location. - options[:caller] ||= Metadata.backtrace_from(block) + def initialize(inspect_output=nil) + @__inspect_output = inspect_output || '(no description provided)' + end - # This will fail if no block is provided, which is effectively the - # same as failing the example so it will be marked correctly as - # pending. - callback = Proc.new do - pending(reason) - instance_exec(&block) + # @private + def inspect + "#<#{self.class} #{@__inspect_output}>" + end + + unless method_defined?(:singleton_class) # for 1.8.7 + # @private + def singleton_class + class << self; self; end end + end - return options, callback + # Raised when an RSpec API is called in the wrong scope, such as `before` + # being called from within an example rather than from within an example + # group block. + WrongScopeError = Class.new(NoMethodError) + + def self.method_missing(name, *args) + if method_defined?(name) + raise WrongScopeError, + "`#{name}` is not available on an example group (e.g. a " \ + "`describe` or `context` block). It is only available from " \ + "within individual examples (e.g. `it` blocks) or from " \ + "constructs that run in the scope of an example (e.g. " \ + "`before`, `let`, etc)." + end + + super + end + private_class_method :method_missing + + private + + def method_missing(name, *args) + if self.class.respond_to?(name) + raise WrongScopeError, + "`#{name}` is not available from within an example (e.g. an " \ + "`it` block) or from constructs that run in the scope of an " \ + "example (e.g. `before`, `let`, etc). It is only available " \ + "on an example group (e.g. a `describe` or `context` block)." + end + + super end end @@ -579,11 +657,55 @@ def self.metadata {} end end + + # Contains information about the inclusion site of a shared example group. + class SharedExampleGroupInclusionStackFrame + # @return [String] the name of the shared example group + attr_reader :shared_group_name + # @return [String] the location where the shared example was included + attr_reader :inclusion_location + + def initialize(shared_group_name, inclusion_location) + @shared_group_name = shared_group_name + @inclusion_location = inclusion_location + end + + # @return [String] The {#inclusion_location}, formatted for display by a formatter. + def formatted_inclusion_location + @formatted_inclusion_location ||= begin + RSpec.configuration.backtrace_formatter.backtrace_line( + inclusion_location.sub(/(:\d+):in .+$/, '\1') + ) + end + end + + # @return [String] Description of this stack frame, in the form used by + # RSpec's built-in formatters. + def description + @description ||= "Shared Example Group: #{shared_group_name.inspect} " \ + "called from #{formatted_inclusion_location}" + end + + # @private + def self.current_backtrace + RSpec.thread_local_metadata[:shared_example_group_inclusions].reverse + end + + # @private + def self.with_frame(name, location) + current_stack = RSpec.thread_local_metadata[:shared_example_group_inclusions] + current_stack << new(name, location) + yield + ensure + current_stack.pop + end + end end # @private # - # Namespace for the example group subclasses generated by top-level `describe`. + # Namespace for the example group subclasses generated by top-level + # `describe`. module ExampleGroups extend Support::RecursiveConstMethods @@ -604,24 +726,39 @@ def self.constant_scope_for(group) def self.base_name_for(group) return "Anonymous" if group.description.empty? - # convert to CamelCase - name = ' ' + group.description - name.gsub!(/[^0-9a-zA-Z]+([0-9a-zA-Z])/) { Regexp.last_match[1].upcase } + # Convert to CamelCase. + name = ' ' << group.description + name.gsub!(/[^0-9a-zA-Z]+([0-9a-zA-Z])/) do + match = ::Regexp.last_match[1] + match.upcase! + match + end - name.lstrip! # Remove leading whitespace - name.gsub!(/\W/, '') # JRuby, RBX and others don't like non-ascii in const names + name.lstrip! # Remove leading whitespace + name.gsub!(/\W/, ''.freeze) # JRuby, RBX and others don't like non-ascii in const names # Ruby requires first const letter to be A-Z. Use `Nested` # as necessary to enforce that. - name.gsub!(/\A([^A-Z]|\z)/, 'Nested\1') + name.gsub!(/\A([^A-Z]|\z)/, 'Nested\1'.freeze) name end + if RUBY_VERSION == '1.9.2' + class << self + alias _base_name_for base_name_for + def base_name_for(group) + _base_name_for(group) + '_' + end + end + private_class_method :_base_name_for + end + def self.disambiguate(name, const_scope) return name unless const_defined_on?(const_scope, name) - # Add a trailing number if needed to disambiguate from an existing constant. + # Add a trailing number if needed to disambiguate from an existing + # constant. name << "_2" name.next! while const_defined_on?(const_scope, name) name diff --git a/lib/rspec/core/filter_manager.rb b/lib/rspec/core/filter_manager.rb index 406369b214..b3e3217e20 100644 --- a/lib/rspec/core/filter_manager.rb +++ b/lib/rspec/core/filter_manager.rb @@ -1,71 +1,6 @@ module RSpec module Core # @private - # Manages the filtering of examples and groups by matching tags declared on - # the command line or options files, or filters declared via - # `RSpec.configure`, with hash key/values submitted within example group - # and/or example declarations. For example, given this declaration: - # - # describe Thing, :awesome => true do - # it "does something" do - # # ... - # end - # end - # - # That group (or any other with `:awesome => true`) would be filtered in - # with any of the following commands: - # - # rspec --tag awesome:true - # rspec --tag awesome - # rspec -t awesome:true - # rspec -t awesome - # - # Prefixing the tag names with `~` negates the tags, thus excluding this group with - # any of: - # - # rspec --tag ~awesome:true - # rspec --tag ~awesome - # rspec -t ~awesome:true - # rspec -t ~awesome - # - # ## Options files and command line overrides - # - # Tag declarations can be stored in `.rspec`, `~/.rspec`, or a custom - # options file. This is useful for storing defaults. For example, let's - # say you've got some slow specs that you want to suppress most of the - # time. You can tag them like this: - # - # describe Something, :slow => true do - # - # And then store this in `.rspec`: - # - # --tag ~slow:true - # - # Now when you run `rspec`, that group will be excluded. - # - # ## Overriding - # - # Of course, you probably want to run them sometimes, so you can override - # this tag on the command line like this: - # - # rspec --tag slow:true - # - # ## RSpec.configure - # - # You can also store default tags with `RSpec.configure`. We use `tag` on - # the command line (and in options files like `.rspec`), but for historical - # reasons we use the term `filter` in `RSpec.configure: - # - # RSpec.configure do |c| - # c.filter_run_including :foo => :bar - # c.filter_run_excluding :foo => :bar - # end - # - # These declarations can also be overridden from the command line. - # - # @see RSpec.configure - # @see Configuration#filter_run_including - # @see Configuration#filter_run_excluding class FilterManager attr_reader :exclusions, :inclusions @@ -83,7 +18,7 @@ def add_location(file_path, line_numbers) # { "path/to/file.rb" => [37, 42] } locations = inclusions.delete(:locations) || Hash.new { |h, k| h[k] = [] } locations[File.expand_path(file_path)].push(*line_numbers) - inclusions.add_location(locations) + inclusions.add(:locations => locations) end def empty? @@ -91,11 +26,13 @@ def empty? end def prune(examples) + examples = prune_conditionally_filtered_examples(examples) + if inclusions.standalone? - base_exclusions = ExclusionRules.new - examples.select { |e| !base_exclusions.include_example?(e) && include?(e) } + examples.select { |e| include?(e) } else - examples.select { |e| !exclude?(e) && include?(e) } + locations = inclusions.fetch(:locations) { Hash.new([]) } + examples.select { |e| priority_include?(e, locations) || (!exclude?(e) && include?(e)) } end end @@ -111,10 +48,6 @@ def exclude_with_low_priority(*args) exclusions.add_with_low_priority(args.last) end - def exclude?(example) - exclusions.include_example?(example) - end - def include(*args) inclusions.add(args.last) end @@ -127,9 +60,32 @@ def include_with_low_priority(*args) inclusions.add_with_low_priority(args.last) end + private + + def exclude?(example) + exclusions.include_example?(example) + end + def include?(example) inclusions.include_example?(example) end + + def prune_conditionally_filtered_examples(examples) + examples.reject do |ex| + meta = ex.metadata + !meta.fetch(:if, true) || meta[:unless] + end + end + + # When a user specifies a particular spec location, that takes priority + # over any exclusion filters (such as if the spec is tagged with `:slow` + # and there is a `:slow => true` exclusion filter), but only for specs + # defined in the same file as the location filters. Excluded specs in + # other files should still be excluded. + def priority_include?(example, locations) + return false if locations[example.metadata[:absolute_file_path]].empty? + MetadataFilter.filter_applies?(:locations, locations, example.metadata) + end end # @private @@ -194,16 +150,17 @@ def each_pair(&block) def description rules.inspect.gsub(PROC_HEX_NUMBER, '').gsub(PROJECT_DIR, '.').gsub(' (lambda)', '') end + + def include_example?(example) + MetadataFilter.apply?(:any?, @rules, example.metadata) + end end # @private - class InclusionRules < FilterRules - STANDALONE_FILTERS = [:locations, :full_description] - - def add_location(locations) - replace_filters(:locations => locations) - end + ExclusionRules = FilterRules + # @private + class InclusionRules < FilterRules def add(*args) apply_standalone_filter(*args) || super end @@ -217,7 +174,7 @@ def use(*args) end def include_example?(example) - @rules.empty? ? true : example.any_apply?(@rules) + @rules.empty? || super end def standalone? @@ -240,19 +197,7 @@ def replace_filters(new_rules) end def is_standalone_filter?(rules) - STANDALONE_FILTERS.any? { |key| rules.key?(key) } - end - end - - # @private - class ExclusionRules < FilterRules - CONDITIONAL_FILTERS = { - :if => lambda { |value| !value }, - :unless => lambda { |value| value } - }.freeze - - def include_example?(example) - example.any_apply?(@rules) || example.any_apply?(CONDITIONAL_FILTERS) + rules.key?(:full_description) end end end diff --git a/lib/rspec/core/flat_map.rb b/lib/rspec/core/flat_map.rb index cc4e5a4d30..71093ac832 100644 --- a/lib/rspec/core/flat_map.rb +++ b/lib/rspec/core/flat_map.rb @@ -3,12 +3,12 @@ module Core # @private module FlatMap if [].respond_to?(:flat_map) - def flat_map(array) - array.flat_map { |item| yield item } + def flat_map(array, &block) + array.flat_map(&block) end else # for 1.8.7 - def flat_map(array) - array.map { |item| yield item }.flatten + def flat_map(array, &block) + array.map(&block).flatten(1) end end diff --git a/lib/rspec/core/formatters.rb b/lib/rspec/core/formatters.rb index bc46a990e0..765c2e1f45 100644 --- a/lib/rspec/core/formatters.rb +++ b/lib/rspec/core/formatters.rb @@ -1,10 +1,12 @@ RSpec::Support.require_rspec_support "directory_maker" # ## Built-in Formatters # -# * progress (default) - prints dots for passing examples, `F` for failures, `*` for pending -# * documentation - prints the docstrings passed to `describe` and `it` methods (and their aliases) +# * progress (default) - Prints dots for passing examples, `F` for failures, `*` +# for pending. +# * documentation - Prints the docstrings passed to `describe` and `it` methods +# (and their aliases). # * html -# * json - useful for archiving data for subsequent analysis +# * json - Useful for archiving data for subsequent analysis. # # The progress formatter is the default, but you can choose any one or more of # the other formatters by passing with the `--format` (or `-f` for short) @@ -72,7 +74,8 @@ module RSpec::Core::Formatters # Register the formatter class # @param formatter_class [Class] formatter class to register - # @param notifications [Symbol, ...] one or more notifications to be registered to the specified formatter + # @param notifications [Symbol, ...] one or more notifications to be + # registered to the specified formatter # # @see RSpec::Core::Formatters::BaseFormatter def self.register(formatter_class, *notifications) @@ -88,7 +91,7 @@ def self.register(formatter_class, *notifications) class Loader # @api private # - # Internal formatters are stored here when loaded + # Internal formatters are stored here when loaded. def self.formatters @formatters ||= {} end @@ -165,7 +168,8 @@ def add(formatter_to_use, *paths) def find_formatter(formatter_to_use) built_in_formatter(formatter_to_use) || custom_formatter(formatter_to_use) || - (raise ArgumentError, "Formatter '#{formatter_to_use}' unknown - maybe you meant 'documentation' or 'progress'?.") + (raise ArgumentError, "Formatter '#{formatter_to_use}' unknown - " \ + "maybe you meant 'documentation' or 'progress'?.") end def duplicate_formatter_exists?(new_formatter) diff --git a/lib/rspec/core/formatters/base_formatter.rb b/lib/rspec/core/formatters/base_formatter.rb index 55c52b110e..84248aa174 100644 --- a/lib/rspec/core/formatters/base_formatter.rb +++ b/lib/rspec/core/formatters/base_formatter.rb @@ -4,13 +4,15 @@ module RSpec module Core module Formatters - # RSpec's built-in formatters are all subclasses of RSpec::Core::Formatters::BaseTextFormatter. + # RSpec's built-in formatters are all subclasses of + # RSpec::Core::Formatters::BaseTextFormatter. # # @see RSpec::Core::Formatters::BaseTextFormatter # @see RSpec::Core::Reporter # @see RSpec::Core::Formatters::Protocol class BaseFormatter - # all formatters inheriting from this formatter will receive these notifications + # All formatters inheriting from this formatter will receive these + # notifications. Formatters.register self, :start, :example_group_started, :close attr_accessor :example_group attr_reader :output @@ -34,7 +36,8 @@ def start(notification) # @api public # - # @param notification [GroupNotification] containing example_group subclass of `RSpec::Core::ExampleGroup` + # @param notification [GroupNotification] containing example_group + # subclass of `RSpec::Core::ExampleGroup` # @see RSpec::Core::Formatters::Protocol#example_group_started def example_group_started(notification) @example_group = notification.group @@ -42,7 +45,7 @@ def example_group_started(notification) # @api public # - # @param notification [NullNotification] + # @param _notification [NullNotification] (Ignored) # @see RSpec::Core::Formatters::Protocol#close def close(_notification) restore_sync_output diff --git a/lib/rspec/core/formatters/base_text_formatter.rb b/lib/rspec/core/formatters/base_text_formatter.rb index 6a858b1e44..03b79fb18a 100644 --- a/lib/rspec/core/formatters/base_text_formatter.rb +++ b/lib/rspec/core/formatters/base_text_formatter.rb @@ -4,8 +4,9 @@ module RSpec module Core module Formatters - # Base for all of RSpec's built-in formatters. See RSpec::Core::Formatters::BaseFormatter - # to learn more about all of the methods called by the reporter. + # Base for all of RSpec's built-in formatters. See + # RSpec::Core::Formatters::BaseFormatter to learn more about all of the + # methods called by the reporter. # # @see RSpec::Core::Formatters::BaseFormatter # @see RSpec::Core::Reporter @@ -13,7 +14,6 @@ class BaseTextFormatter < BaseFormatter Formatters.register self, :message, :dump_summary, :dump_failures, :dump_pending, :seed - # @method message # @api public # # Used by the reporter to send messages to the output stream. @@ -23,7 +23,6 @@ def message(notification) output.puts notification.message end - # @method dump_failures # @api public # # Dumps detailed information about each example failure. @@ -34,14 +33,13 @@ def dump_failures(notification) output.puts notification.fully_formatted_failed_examples end - # @method dump_summary # @api public # - # This method is invoked after the dumping of examples and failures. Each parameter - # is assigned to a corresponding attribute. + # This method is invoked after the dumping of examples and failures. + # Each parameter is assigned to a corresponding attribute. # - # @param summary [SummaryNotification] containing duration, example_count, - # failure_count and pending_count + # @param summary [SummaryNotification] containing duration, + # example_count, failure_count and pending_count def dump_summary(summary) output.puts summary.fully_formatted end @@ -63,12 +61,14 @@ def seed(notification) # Invoked at the very end, `close` allows the formatter to clean # up resources, e.g. open streams, etc. # - # @param notification [NullNotification] + # @param _notification [NullNotification] (Ignored) def close(_notification) return unless IO === output - return if output.closed? || output == $stdout + return if output.closed? + + output.puts - output.close + output.close unless output == $stdout end end end diff --git a/lib/rspec/core/formatters/console_codes.rb b/lib/rspec/core/formatters/console_codes.rb index 9c2a11a7cc..1419dbc075 100644 --- a/lib/rspec/core/formatters/console_codes.rb +++ b/lib/rspec/core/formatters/console_codes.rb @@ -22,14 +22,20 @@ module ConsoleCodes module_function + # @private + CONFIG_COLORS_TO_METHODS = Configuration.instance_methods.grep(/_color\z/).inject({}) do |hash, method| + hash[method.to_s.sub(/_color\z/, '').to_sym] = method + hash + end + # Fetches the correct code for the supplied symbol, or checks # that a code is valid. Defaults to white (37). # # @param code_or_symbol [Symbol, Fixnum] Symbol or code to check # @return [Fixnum] a console code def console_code_for(code_or_symbol) - if RSpec.configuration.respond_to?(:"#{code_or_symbol}_color") - console_code_for configuration_color(code_or_symbol) + if (config_method = CONFIG_COLORS_TO_METHODS[code_or_symbol]) + console_code_for RSpec.configuration.__send__(config_method) elsif VT100_CODE_VALUES.key?(code_or_symbol) code_or_symbol else @@ -53,11 +59,6 @@ def wrap(text, code_or_symbol) text end end - - # @private - def configuration_color(code) - RSpec.configuration.__send__(:"#{code}_color") - end end end end diff --git a/lib/rspec/core/formatters/deprecation_formatter.rb b/lib/rspec/core/formatters/deprecation_formatter.rb index 231fee830e..118fe88761 100644 --- a/lib/rspec/core/formatters/deprecation_formatter.rb +++ b/lib/rspec/core/formatters/deprecation_formatter.rb @@ -21,7 +21,8 @@ def initialize(deprecation_stream, summary_stream) def printer @printer ||= case deprecation_stream when File - ImmediatePrinter.new(FileStream.new(deprecation_stream), summary_stream, self) + ImmediatePrinter.new(FileStream.new(deprecation_stream), + summary_stream, self) when RaiseErrorStream ImmediatePrinter.new(deprecation_stream, summary_stream, self) else @@ -209,14 +210,15 @@ def puts(*args) end def summarize(summary_stream, deprecation_count) - summary_stream.puts "\n#{Helpers.pluralize(deprecation_count, 'deprecation')} logged to #{@file.path}" + path = @file.respond_to?(:path) ? @file.path : @file.inspect + summary_stream.puts "\n#{Helpers.pluralize(deprecation_count, 'deprecation')} logged to #{path}" puts RAISE_ERROR_CONFIG_NOTICE end end end end - # Deprecation Error + # Deprecation Error. DeprecationError = Class.new(StandardError) end end diff --git a/lib/rspec/core/formatters/documentation_formatter.rb b/lib/rspec/core/formatters/documentation_formatter.rb index 5cf6e178e3..5deb4a754f 100644 --- a/lib/rspec/core/formatters/documentation_formatter.rb +++ b/lib/rspec/core/formatters/documentation_formatter.rb @@ -29,11 +29,13 @@ def example_passed(passed) end def example_pending(pending) - output.puts pending_output(pending.example, pending.example.execution_result.pending_message) + output.puts pending_output(pending.example, + pending.example.execution_result.pending_message) end def example_failed(failure) - output.puts failure_output(failure.example, failure.example.execution_result.exception) + output.puts failure_output(failure.example, + failure.example.execution_result.exception) end private @@ -43,11 +45,15 @@ def passed_output(example) end def pending_output(example, message) - ConsoleCodes.wrap("#{current_indentation}#{example.description.strip} (PENDING: #{message})", :pending) + ConsoleCodes.wrap("#{current_indentation}#{example.description.strip} " \ + "(PENDING: #{message})", + :pending) end def failure_output(example, _exception) - ConsoleCodes.wrap("#{current_indentation}#{example.description.strip} (FAILED - #{next_failure_index})", :failure) + ConsoleCodes.wrap("#{current_indentation}#{example.description.strip} " \ + "(FAILED - #{next_failure_index})", + :failure) end def next_failure_index diff --git a/lib/rspec/core/formatters/helpers.rb b/lib/rspec/core/formatters/helpers.rb index 9eb867bda0..f6acca2a1c 100644 --- a/lib/rspec/core/formatters/helpers.rb +++ b/lib/rspec/core/formatters/helpers.rb @@ -1,7 +1,7 @@ module RSpec module Core module Formatters - # Formatters helpers + # Formatters helpers. module Helpers # @private SUB_SECOND_PRECISION = 5 @@ -39,8 +39,9 @@ def self.format_duration(duration) # @api private # - # Formats seconds to have 5 digits of precision with trailing zeros removed if the number - # is less than 1 or with 2 digits of precision if the number is greater than zero. + # Formats seconds to have 5 digits of precision with trailing zeros + # removed if the number is less than 1 or with 2 digits of precision if + # the number is greater than zero. # # @param float [Float] # @return [String] formatted float @@ -50,7 +51,8 @@ def self.format_duration(duration) # format_seconds(0.020000) #=> "0.02" # format_seconds(1.00000000001) #=> "1" # - # The precision used is set in {Helpers::SUB_SECOND_PRECISION} and {Helpers::DEFAULT_PRECISION}. + # The precision used is set in {Helpers::SUB_SECOND_PRECISION} and + # {Helpers::DEFAULT_PRECISION}. # # @see #strip_trailing_zeroes def self.format_seconds(float, precision=nil) diff --git a/lib/rspec/core/formatters/html_formatter.rb b/lib/rspec/core/formatters/html_formatter.rb index 5eeb23c128..30868b45ba 100644 --- a/lib/rspec/core/formatters/html_formatter.rb +++ b/lib/rspec/core/formatters/html_formatter.rb @@ -31,7 +31,9 @@ def example_group_started(notification) @example_group_number += 1 @printer.print_example_group_end unless example_group_number == 1 - @printer.print_example_group_start(example_group_number, notification.group.description, notification.group.parent_groups.size) + @printer.print_example_group_start(example_group_number, + notification.group.description, + notification.group.parent_groups.size) @printer.flush end @@ -111,15 +113,16 @@ def dump_summary(summary) private - # If these methods are declared with attr_reader Ruby will issue a warning because they are private + # If these methods are declared with attr_reader Ruby will issue a + # warning because they are private. # rubocop:disable Style/TrivialAccessors - # The number of the currently running example_group + # The number of the currently running example_group. def example_group_number @example_group_number end - # The number of the currently running example (a global counter) + # The number of the currently running example (a global counter). def example_number @example_number end @@ -133,12 +136,14 @@ def percent_done result end - # Override this method if you wish to output extra HTML for a failed spec. For example, you - # could output links to images or other files produced during the specs. - # + # Override this method if you wish to output extra HTML for a failed + # spec. For example, you could output links to images or other files + # produced during the specs. def extra_failure_content(failure) RSpec::Support.require_rspec_core "formatters/snippet_extractor" - backtrace = failure.exception.backtrace.map { |line| RSpec.configuration.backtrace_formatter.backtrace_line(line) } + backtrace = failure.exception.backtrace.map do |line| + RSpec.configuration.backtrace_formatter.backtrace_line(line) + end backtrace.compact! @snippet_extractor ||= SnippetExtractor.new "
#{@snippet_extractor.snippet(backtrace)}
" diff --git a/lib/rspec/core/formatters/html_printer.rb b/lib/rspec/core/formatters/html_printer.rb index 13c3802467..32a9f97d39 100644 --- a/lib/rspec/core/formatters/html_printer.rb +++ b/lib/rspec/core/formatters/html_printer.rb @@ -5,7 +5,7 @@ module Core module Formatters # @private class HtmlPrinter - include ERB::Util # for the #h method + include ERB::Util # For the #h method. def initialize(output) @output = output end @@ -28,11 +28,14 @@ def print_example_group_start(group_id, description, number_of_parents) def print_example_passed(description, run_time) formatted_run_time = "%.5f" % run_time - @output.puts "
#{h(description)}#{formatted_run_time}s
" + @output.puts "
" \ + "#{h(description)}" \ + "#{formatted_run_time}s
" end # rubocop:disable Style/ParameterLists - def print_example_failed(pending_fixed, description, run_time, failure_id, exception, extra_content, escape_backtrace=false) + def print_example_failed(pending_fixed, description, run_time, failure_id, + exception, extra_content, escape_backtrace=false) # rubocop:enable Style/ParameterLists formatted_run_time = "%.5f" % run_time @@ -54,7 +57,9 @@ def print_example_failed(pending_fixed, description, run_time, failure_id, excep end def print_example_pending(description, pending_message) - @output.puts "
#{h(description)} (PENDING: #{h(pending_message)})
" + @output.puts "
" \ + "#{h(description)} " \ + "(PENDING: #{h(pending_message)})
" end def print_summary(duration, example_count, failure_count, pending_count) @@ -64,8 +69,11 @@ def print_summary(duration, example_count, failure_count, pending_count) formatted_duration = "%.5f" % duration - @output.puts "" - @output.puts "" + @output.puts "" + @output.puts "" @output.puts "" @output.puts "" @output.puts "" @@ -90,13 +98,17 @@ def make_header_yellow end def make_example_group_header_red(group_id) - @output.puts " " - @output.puts " " + @output.puts " " + @output.puts " " end def make_example_group_header_yellow(group_id) - @output.puts " " - @output.puts " " + @output.puts " " + @output.puts " " end private @@ -105,6 +117,7 @@ def indentation_style(number_of_parents) "style=\"margin-left: #{(number_of_parents - 1) * 15}px;\"" end + # rubocop:disable LineLength REPORT_HEADER = <<-EOF
@@ -128,7 +141,9 @@ def indentation_style(number_of_parents)
EOF + # rubocop:enable LineLength + # rubocop:disable LineLength GLOBAL_SCRIPTS = <<-EOF function addClass(element_id, classname) { @@ -205,6 +220,7 @@ def indentation_style(number_of_parents) } } EOF + # rubocop:enable LineLength GLOBAL_STYLES = <<-EOF #rspec-header { diff --git a/lib/rspec/core/formatters/profile_formatter.rb b/lib/rspec/core/formatters/profile_formatter.rb index af57b44f83..4b95d9386f 100644 --- a/lib/rspec/core/formatters/profile_formatter.rb +++ b/lib/rspec/core/formatters/profile_formatter.rb @@ -4,7 +4,7 @@ module RSpec module Core module Formatters # @api private - # Formatter for providing profile output + # Formatter for providing profile output. class ProfileFormatter Formatters.register self, :dump_profile @@ -15,14 +15,13 @@ def initialize(output) # @private attr_reader :output - # @method dump_profile # @api public # # This method is invoked after the dumping the summary if profiling is # enabled. # - # @param profile [ProfileNotification] containing duration, slowest_examples - # and slowest_example_groups + # @param profile [ProfileNotification] containing duration, + # slowest_examples and slowest_example_groups def dump_profile(profile) dump_profile_slowest_examples(profile) dump_profile_slowest_example_groups(profile) @@ -31,11 +30,14 @@ def dump_profile(profile) private def dump_profile_slowest_examples(profile) - @output.puts "\nTop #{profile.slowest_examples.size} slowest examples (#{Helpers.format_seconds(profile.slow_duration)} seconds, #{profile.percentage}% of total time):\n" + @output.puts "\nTop #{profile.slowest_examples.size} slowest " \ + "examples (#{Helpers.format_seconds(profile.slow_duration)} " \ + "seconds, #{profile.percentage}% of total time):\n" profile.slowest_examples.each do |example| @output.puts " #{example.full_description}" - @output.puts " #{bold(Helpers.format_seconds(example.execution_result.run_time))} #{bold("seconds")} #{format_caller(example.location)}" + @output.puts " #{bold(Helpers.format_seconds(example.execution_result.run_time))} " \ + "#{bold("seconds")} #{format_caller(example.location)}" end end @@ -53,7 +55,8 @@ def dump_profile_slowest_example_groups(profile) end def format_caller(caller_info) - RSpec.configuration.backtrace_formatter.backtrace_line(caller_info.to_s.split(':in `block').first) + RSpec.configuration.backtrace_formatter.backtrace_line( + caller_info.to_s.split(':in `block').first) end def bold(text) diff --git a/lib/rspec/core/formatters/protocol.rb b/lib/rspec/core/formatters/protocol.rb index 69ac65dba6..f9e2ae5762 100644 --- a/lib/rspec/core/formatters/protocol.rb +++ b/lib/rspec/core/formatters/protocol.rb @@ -39,12 +39,14 @@ class Protocol # @api public # @group Group Notifications # - # This method is invoked at the beginning of the execution of each example group. + # This method is invoked at the beginning of the execution of each + # example group. # # The next method to be invoked after this is {#example_passed}, # {#example_pending}, or {#example_group_finished}. # - # @param notification [GroupNotification] containing example_group subclass of `RSpec::Core::ExampleGroup` + # @param notification [GroupNotification] containing example_group + # subclass of `RSpec::Core::ExampleGroup` # @method example_group_finished # @api public @@ -52,7 +54,8 @@ class Protocol # # Invoked at the end of the execution of each example group. # - # @param notification [GroupNotification] containing example_group subclass of `RSpec::Core::ExampleGroup` + # @param notification [GroupNotification] containing example_group + # subclass of `RSpec::Core::ExampleGroup` # @method example_started # @api public @@ -60,7 +63,8 @@ class Protocol # # Invoked at the beginning of the execution of each example. # - # @param notification [ExampleNotification] containing example subclass of `RSpec::Core::Example` + # @param notification [ExampleNotification] containing example subclass + # of `RSpec::Core::Example` # @method example_passed # @api public @@ -68,7 +72,8 @@ class Protocol # # Invoked when an example passes. # - # @param notification [ExampleNotification] containing example subclass of `RSpec::Core::Example` + # @param notification [ExampleNotification] containing example subclass + # of `RSpec::Core::Example` # @method example_pending # @api public @@ -76,7 +81,8 @@ class Protocol # # Invoked when an example is pending. # - # @param notification [ExampleNotification] containing example subclass of `RSpec::Core::Example` + # @param notification [ExampleNotification] containing example subclass + # of `RSpec::Core::Example` # @method example_failed # @api public @@ -84,7 +90,8 @@ class Protocol # # Invoked when an example fails. # - # @param notification [ExampleNotification] containing example subclass of `RSpec::Core::Example` + # @param notification [ExampleNotification] containing example subclass + # of `RSpec::Core::Example` # @method message # @api public @@ -98,7 +105,8 @@ class Protocol # @api public # @group Suite Notifications # - # Invoked after all examples have executed, before dumping post-run reports. + # Invoked after all examples have executed, before dumping post-run + # reports. # # @param notification [NullNotification] @@ -106,9 +114,10 @@ class Protocol # @api public # @group Suite Notifications # - # This method is invoked after all of the examples have executed. The next method - # to be invoked after this one is {#dump_failures} - # (BaseTextFormatter then calls {#dump_failure} once for each failed example.) + # This method is invoked after all of the examples have executed. The + # next method to be invoked after this one is {#dump_failures} + # (BaseTextFormatter then calls {#dump_failures} once for each failed + # example). # # @param notification [NullNotification] @@ -124,11 +133,11 @@ class Protocol # @api public # @group Suite Notifications # - # This method is invoked after the dumping of examples and failures. Each parameter - # is assigned to a corresponding attribute. + # This method is invoked after the dumping of examples and failures. + # Each parameter is assigned to a corresponding attribute. # - # @param summary [SummaryNotification] containing duration, example_count, - # failure_count and pending_count + # @param summary [SummaryNotification] containing duration, + # example_count, failure_count and pending_count # @method dump_profile # @api public @@ -137,14 +146,14 @@ class Protocol # This method is invoked after the dumping the summary if profiling is # enabled. # - # @param profile [ProfileNotification] containing duration, slowest_examples - # and slowest_example_groups + # @param profile [ProfileNotification] containing duration, + # slowest_examples and slowest_example_groups # @method dump_pending # @api public # @group Suite Notifications # - # Outputs a report of pending examples. This gets invoked + # Outputs a report of pending examples. This gets invoked # after the summary if option is set to do so. # # @param notification [NullNotification] diff --git a/lib/rspec/core/formatters/snippet_extractor.rb b/lib/rspec/core/formatters/snippet_extractor.rb index dea7ccaa5c..2546acae69 100644 --- a/lib/rspec/core/formatters/snippet_extractor.rb +++ b/lib/rspec/core/formatters/snippet_extractor.rb @@ -3,7 +3,8 @@ module Core module Formatters # @api private # - # Extracts code snippets by looking at the backtrace of the passed error and applies synax highlighting and line numbers using html. + # Extracts code snippets by looking at the backtrace of the passed error + # and applies synax highlighting and line numbers using html. class SnippetExtractor # @private class NullConverter @@ -33,7 +34,8 @@ def convert(code) # Extract lines of code corresponding to a backtrace. # # @param backtrace [String] the backtrace from a test failure - # @return [String] highlighted code snippet indicating where the test failure occured + # @return [String] highlighted code snippet indicating where the test + # failure occured # # @see #post_process def snippet(backtrace) @@ -46,7 +48,8 @@ def snippet(backtrace) # # Create a snippet from a line of code. # - # @param error_line [String] file name with line number (i.e. 'foo_spec.rb:12') + # @param error_line [String] file name with line number (i.e. + # 'foo_spec.rb:12') # @return [String] lines around the target line within the file # # @see #lines_around @@ -62,11 +65,13 @@ def snippet_for(error_line) # @api private # - # Extract lines of code centered around a particular line within a source file. + # Extract lines of code centered around a particular line within a + # source file. # # @param file [String] filename # @param line [Fixnum] line number - # @return [String] lines around the target line within the file (2 above and 1 below). + # @return [String] lines around the target line within the file (2 above + # and 1 below). def lines_around(file, line) if File.file?(file) lines = File.read(file).split("\n") @@ -84,9 +89,11 @@ def lines_around(file, line) # @api private # - # Adds line numbers to all lines and highlights the line where the failure occurred using html `span` tags. + # Adds line numbers to all lines and highlights the line where the + # failure occurred using html `span` tags. # - # @param highlighted [String] syntax-highlighted snippet surrounding the offending line of code + # @param highlighted [String] syntax-highlighted snippet surrounding the + # offending line of code # @param offending_line [Fixnum] line where failure occured # @return [String] completed snippet def post_process(highlighted, offending_line) diff --git a/lib/rspec/core/hooks.rb b/lib/rspec/core/hooks.rb index 226b24ff49..9c5601777c 100644 --- a/lib/rspec/core/hooks.rb +++ b/lib/rspec/core/hooks.rb @@ -11,18 +11,20 @@ module Hooks # # @overload before(&block) # @overload before(scope, &block) - # @param scope [Symbol] `:example`, `:context`, or `:suite` (defaults to `:example`) + # @param scope [Symbol] `:example`, `:context`, or `:suite` + # (defaults to `:example`) # @overload before(scope, conditions, &block) - # @param scope [Symbol] `:example`, `:context`, or `:suite` (defaults to `:example`) + # @param scope [Symbol] `:example`, `:context`, or `:suite` + # (defaults to `:example`) # @param conditions [Hash] # constrains this hook to examples matching these conditions e.g. - # `before(:example, :ui => true) { ... }` will only run with examples or - # groups declared with `:ui => true`. + # `before(:example, :ui => true) { ... }` will only run with examples + # or groups declared with `:ui => true`. # @overload before(conditions, &block) # @param conditions [Hash] # constrains this hook to examples matching these conditions e.g. - # `before(:example, :ui => true) { ... }` will only run with examples or - # groups declared with `:ui => true`. + # `before(:example, :ui => true) { ... }` will only run with examples + # or groups declared with `:ui => true`. # # @see #after # @see #around @@ -32,39 +34,39 @@ module Hooks # @see Configuration # # Declare a block of code to be run before each example (using `:example`) - # or once before any example (using `:context`). These are usually declared - # directly in the {ExampleGroup} to which they apply, but they can also - # be shared across multiple groups. + # or once before any example (using `:context`). These are usually + # declared directly in the {ExampleGroup} to which they apply, but they + # can also be shared across multiple groups. # # You can also use `before(:suite)` to run a block of code before any - # example groups are run. This should be declared in {RSpec.configure} + # example groups are run. This should be declared in {RSpec.configure}. # - # Instance variables declared in `before(:example)` or `before(:context)` are - # accessible within each example. + # Instance variables declared in `before(:example)` or `before(:context)` + # are accessible within each example. # # ### Order # # `before` hooks are stored in three scopes, which are run in order: - # `:suite`, `:context`, and `:example`. They can also be declared in several - # different places: `RSpec.configure`, a parent group, the current group. - # They are run in the following order: - # - # before(:suite) # declared in RSpec.configure - # before(:context) # declared in RSpec.configure - # before(:context) # declared in a parent group - # before(:context) # declared in the current group - # before(:example) # declared in RSpec.configure - # before(:example) # declared in a parent group - # before(:example) # declared in the current group + # `:suite`, `:context`, and `:example`. They can also be declared in + # several different places: `RSpec.configure`, a parent group, the current + # group. They are run in the following order: + # + # before(:suite) # Declared in RSpec.configure. + # before(:context) # Declared in RSpec.configure. + # before(:context) # Declared in a parent group. + # before(:context) # Declared in the current group. + # before(:example) # Declared in RSpec.configure. + # before(:example) # Declared in a parent group. + # before(:example) # Declared in the current group. # # If more than one `before` is declared within any one scope, they are run # in the order in which they are declared. # # ### Conditions # - # When you add a conditions hash to `before(:example)` or `before(:context)`, - # RSpec will only apply that hook to groups or examples that match the - # conditions. e.g. + # When you add a conditions hash to `before(:example)` or + # `before(:context)`, RSpec will only apply that hook to groups or + # examples that match the conditions. e.g. # # RSpec.configure do |config| # config.before(:example, :authorized => true) do @@ -73,19 +75,26 @@ module Hooks # end # # describe Something, :authorized => true do - # # the before hook will run in before each example in this group + # # The before hook will run in before each example in this group. # end # # describe SomethingElse do # it "does something", :authorized => true do - # # the before hook will run before this example + # # The before hook will run before this example. # end # # it "does something else" do - # # the hook will not run before this example + # # The hook will not run before this example. # end # end # + # Note that filtered config `:context` hooks can still be applied + # to individual examples that have matching metadata. Just like + # Ruby's object model is that every object has a singleton class + # which has only a single instance, RSpec's model is that every + # example has a singleton example group containing just the one + # example. + # # ### Warning: `before(:suite, :with => :conditions)` # # The conditions hash is used to match against specific examples. Since @@ -113,22 +122,23 @@ module Hooks # recommend that you avoid this as there are a number of gotchas, as well # as things that simply don't work. # - # #### context + # #### Context # - # `before(:context)` is run in an example that is generated to provide group - # context for the block. + # `before(:context)` is run in an example that is generated to provide + # group context for the block. # - # #### instance variables + # #### Instance variables # - # Instance variables declared in `before(:context)` are shared across all the - # examples in the group. This means that each example can change the + # Instance variables declared in `before(:context)` are shared across all + # the examples in the group. This means that each example can change the # state of a shared object, resulting in an ordering dependency that can # make it difficult to reason about failures. # - # #### unsupported rspec constructs + # #### Unsupported RSpec constructs # # RSpec has several constructs that reset state between each example - # automatically. These are not intended for use from within `before(:context)`: + # automatically. These are not intended for use from within + # `before(:context)`: # # * `let` declarations # * `subject` declarations @@ -138,13 +148,13 @@ module Hooks # # Mock object frameworks and database transaction managers (like # ActiveRecord) are typically designed around the idea of setting up - # before an example, running that one example, and then tearing down. - # This means that mocks and stubs can (sometimes) be declared in - # `before(:context)`, but get torn down before the first real example is ever - # run. + # before an example, running that one example, and then tearing down. This + # means that mocks and stubs can (sometimes) be declared in + # `before(:context)`, but get torn down before the first real example is + # ever run. # - # You _can_ create database-backed model objects in a `before(:context)` in - # rspec-rails, but it will not be wrapped in a transaction for you, so + # You _can_ create database-backed model objects in a `before(:context)` + # in rspec-rails, but it will not be wrapped in a transaction for you, so # you are on your own to clean up in an `after(:context)` block. # # @example before(:example) declared in an {ExampleGroup} @@ -155,7 +165,7 @@ module Hooks # end # # it "does something" do - # # here you can access @thing + # # Here you can access @thing. # end # end # @@ -181,6 +191,9 @@ module Hooks # # @note The `:example` and `:context` scopes are also available as # `:each` and `:all`, respectively. Use whichever you prefer. + # @note The `:scope` alias is only supported for hooks registered on + # `RSpec.configuration` since they exist independently of any + # example or example group. def before(*args, &block) hooks.register :append, :before, *args, &block end @@ -198,18 +211,20 @@ def prepend_before(*args, &block) # @api public # @overload after(&block) # @overload after(scope, &block) - # @param scope [Symbol] `:example`, `:context`, or `:suite` (defaults to `:example`) + # @param scope [Symbol] `:example`, `:context`, or `:suite` (defaults to + # `:example`) # @overload after(scope, conditions, &block) - # @param scope [Symbol] `:example`, `:context`, or `:suite` (defaults to `:example`) + # @param scope [Symbol] `:example`, `:context`, or `:suite` (defaults to + # `:example`) # @param conditions [Hash] # constrains this hook to examples matching these conditions e.g. - # `after(:example, :ui => true) { ... }` will only run with examples or - # groups declared with `:ui => true`. + # `after(:example, :ui => true) { ... }` will only run with examples + # or groups declared with `:ui => true`. # @overload after(conditions, &block) # @param conditions [Hash] # constrains this hook to examples matching these conditions e.g. - # `after(:example, :ui => true) { ... }` will only run with examples or - # groups declared with `:ui => true`. + # `after(:example, :ui => true) { ... }` will only run with examples + # or groups declared with `:ui => true`. # # @see #before # @see #around @@ -218,31 +233,31 @@ def prepend_before(*args, &block) # @see SharedExampleGroup # @see Configuration # - # Declare a block of code to be run after each example (using `:example`) or - # once after all examples n the context (using `:context`). See {#before} for - # more information about ordering. + # Declare a block of code to be run after each example (using `:example`) + # or once after all examples n the context (using `:context`). See + # {#before} for more information about ordering. # # ### Exceptions # # `after` hooks are guaranteed to run even when there are exceptions in - # `before` hooks or examples. When an exception is raised in an after + # `before` hooks or examples. When an exception is raised in an after # block, the exception is captured for later reporting, and subsequent # `after` blocks are run. # # ### Order # # `after` hooks are stored in three scopes, which are run in order: - # `:example`, `:context`, and `:suite`. They can also be declared in several - # different places: `RSpec.configure`, a parent group, the current group. - # They are run in the following order: - # - # after(:example) # declared in the current group - # after(:example) # declared in a parent group - # after(:example) # declared in RSpec.configure - # after(:context) # declared in the current group - # after(:context) # declared in a parent group - # after(:context) # declared in RSpec.configure - # after(:suite) # declared in RSpec.configure + # `:example`, `:context`, and `:suite`. They can also be declared in + # several different places: `RSpec.configure`, a parent group, the current + # group. They are run in the following order: + # + # after(:example) # Declared in the current group. + # after(:example) # Declared in a parent group. + # after(:example) # Declared in RSpec.configure. + # after(:context) # Declared in the current group. + # after(:context) # Declared in a parent group. + # after(:context) # Declared in RSpec.configure. + # after(:suite) # Declared in RSpec.configure. # # This is the reverse of the order in which `before` hooks are run. # Similarly, if more than one `after` is declared within any one scope, @@ -250,6 +265,9 @@ def prepend_before(*args, &block) # # @note The `:example` and `:context` scopes are also available as # `:each` and `:all`, respectively. Use whichever you prefer. + # @note The `:scope` alias is only supported for hooks registered on + # `RSpec.configuration` since they exist independently of any + # example or example group. def after(*args, &block) hooks.register :prepend, :after, *args, &block end @@ -274,15 +292,13 @@ def append_after(*args, &block) # @param scope [Symbol] `:example` (defaults to `:example`) # present for syntax parity with `before` and `after`, but # `:example`/`:each` is the only supported value. - # @param conditions [Hash] - # constrains this hook to examples matching these conditions e.g. - # `around(:example, :ui => true) { ... }` will only run with examples or - # groups declared with `:ui => true`. + # @param conditions [Hash] constrains this hook to examples matching + # these conditions e.g. `around(:example, :ui => true) { ... }` will + # only run with examples or groups declared with `:ui => true`. # @overload around(conditions, &block) - # @param conditions [Hash] - # constrains this hook to examples matching these conditions e.g. - # `around(:example, :ui => true) { ... }` will only run with examples or - # groups declared with `:ui => true`. + # @param conditions [Hash] constrains this hook to examples matching + # these conditions e.g. `around(:example, :ui => true) { ... }` will + # only run with examples or groups declared with `:ui => true`. # # @yield [Example] the example to run # @@ -300,13 +316,13 @@ def append_after(*args, &block) # after the example. It is your responsibility to run the example: # # around(:example) do |ex| - # # do some stuff before + # # Do some stuff before. # ex.run - # # do some stuff after + # # Do some stuff after. # end # # The yielded example aliases `run` with `call`, which lets you treat it - # like a `Proc`. This is especially handy when working with libaries + # like a `Proc`. This is especially handy when working with libaries # that manage their own setup and teardown using a block or proc syntax, # e.g. # @@ -320,12 +336,7 @@ def around(*args, &block) # @private # Holds the various registered hooks. def hooks - @hooks ||= HookCollections.new( - self, - :around => { :example => AroundHookCollection.new }, - :before => { :example => HookCollection.new, :context => HookCollection.new, :suite => HookCollection.new }, - :after => { :example => HookCollection.new, :context => HookCollection.new, :suite => HookCollection.new } - ) + @hooks ||= HookCollections.new(self, FilterableItemRepository::UpdateOptimized) end private @@ -338,10 +349,6 @@ def initialize(block, options) @block = block @options = options end - - def options_apply?(example_or_group) - example_or_group.all_apply?(options) - end end # @private @@ -363,7 +370,7 @@ class AfterContextHook < Hook def run(example) example.instance_exec(example, &block) rescue Exception => e - # TODO: come up with a better solution for this. + # TODO: Come up with a better solution for this. RSpec.configuration.reporter.message <<-EOS An error occurred in an `after(:context)` hook. @@ -379,7 +386,8 @@ class AroundHook < Hook def execute_with(example, procsy) example.instance_exec(procsy, &block) return if procsy.executed? - Pending.mark_skipped!(example, "#{hook_description} did not execute the example") + Pending.mark_skipped!(example, + "#{hook_description} did not execute the example") end if Proc.method_defined?(:source_location) @@ -394,113 +402,81 @@ def hook_description end # @private - class BaseHookCollection - Array.public_instance_methods(false).each do |name| - define_method(name) { |*a, &b| hooks.__send__(name, *a, &b) } - end - - attr_reader :hooks - protected :hooks - - alias append push - alias prepend unshift - - def initialize(hooks=[]) - @hooks = hooks - end - end - - # @private - class HookCollection < BaseHookCollection - def for(example_or_group) - self.class. - new(hooks.select { |hook| hook.options_apply?(example_or_group) }). - with(example_or_group) - end - - def with(example) - @example = example - self - end - - def run - hooks.each { |h| h.run(@example) } - end - end - - # @private - class AroundHookCollection < BaseHookCollection - def for(example, initial_procsy=nil) - self.class.new(hooks.select { |hook| hook.options_apply?(example) }). - with(example, initial_procsy) - end - - def with(example, initial_procsy) - @example = example - @initial_procsy = initial_procsy - self + # + # This provides the primary API used by other parts of rspec-core. By hiding all + # implementation details behind this facade, it's allowed us to heavily optimize + # this, so that, for example, hook collection objects are only instantiated when + # a hook is added. This allows us to avoid many object allocations for the common + # case of a group having no hooks. + # + # This is only possible because this interface provides a "tell, don't ask"-style + # API, so that callers _tell_ this class what to do with the hooks, rather than + # asking this class for a list of hooks, and then doing something with them. + class HookCollections + def initialize(owner, filterable_item_repo_class) + @owner = owner + @filterable_item_repo_class = filterable_item_repo_class + @before_example_hooks = nil + @after_example_hooks = nil + @before_context_hooks = nil + @after_context_hooks = nil + @around_example_hooks = nil end - def run - hooks.inject(@initial_procsy) do |procsy, around_hook| - procsy.wrap { around_hook.execute_with(@example, procsy) } - end.call - end - end + def register_globals(host, globals) + parent_groups = host.parent_groups - # @private - class GroupHookCollection < BaseHookCollection - def for(group) - @group = group - self - end + process(host, parent_groups, globals, :before, :example, &:options) + process(host, parent_groups, globals, :after, :example, &:options) + process(host, parent_groups, globals, :around, :example, &:options) - def run - hooks.shift.run(@group) until hooks.empty? + process(host, parent_groups, globals, :before, :context, &:options) + process(host, parent_groups, globals, :after, :context, &:options) end - end - # @private - class HookCollections - def initialize(owner, data) - @owner = owner - @data = data - end + def register_global_singleton_context_hooks(example, globals) + parent_groups = example.example_group.parent_groups - def [](key) - @data[key] + process(example, parent_groups, globals, :before, :context) { {} } + process(example, parent_groups, globals, :after, :context) { {} } end - def register_globals(host, globals) - process(host, globals, :before, :example) - process(host, globals, :after, :example) - process(host, globals, :around, :example) - - process(host, globals, :before, :context) - process(host, globals, :after, :context) - end + def register(prepend_or_append, position, *args, &block) + scope, options = scope_and_options_from(*args) - def around_example_hooks_for(example, initial_procsy=nil) - AroundHookCollection.new(FlatMap.flat_map(@owner.parent_groups) do |a| - a.hooks[:around][:example] - end).for(example, initial_procsy) - end + if scope == :suite + # TODO: consider making this an error in RSpec 4. For SemVer reasons, + # we are only warning in RSpec 3. + RSpec.warn_with "WARNING: `#{position}(:suite)` hooks are only supported on " \ + "the RSpec configuration object. This " \ + "`#{position}(:suite)` hook, registered on an example " \ + "group, will be ignored." + return + end - def register(prepend_or_append, hook, *args, &block) - scope, options = scope_and_options_from(*args) - self[hook][scope].__send__(prepend_or_append, HOOK_TYPES[hook][scope].new(block, options)) + hook = HOOK_TYPES[position][scope].new(block, options) + ensure_hooks_initialized_for(position, scope).__send__(prepend_or_append, hook, options) end # @private # # Runs all of the blocks stored with the hook in the context of the # example. If no example is provided, just calls the hook directly. - def run(hook, scope, example_or_group, initial_procsy=nil) + def run(position, scope, example_or_group) return if RSpec.configuration.dry_run? - find_hook(hook, scope, example_or_group, initial_procsy).run + + if scope == :context + run_owned_hooks_for(position, :context, example_or_group) + else + case position + when :before then run_example_hooks_for(example_or_group, :before, :reverse_each) + when :after then run_example_hooks_for(example_or_group, :after, :each) + when :around then run_around_example_hooks_for(example_or_group) { yield } + end + end end - SCOPES = [:example, :context, :suite] + SCOPES = [:example, :context] SCOPE_ALIASES = { :each => :example, :all => :context } @@ -512,17 +488,89 @@ def run(hook, scope, example_or_group, initial_procsy=nil) HOOK_TYPES[:after][:context] = AfterContextHook + protected + + EMPTY_HOOK_ARRAY = [].freeze + + def matching_hooks_for(position, scope, example_or_group) + repository = hooks_for(position, scope) { return EMPTY_HOOK_ARRAY } + + # It would be nice to not have to switch on type here, but + # we don't want to define `ExampleGroup#metadata` because then + # `metadata` from within an individual example would return the + # group's metadata but the user would probably expect it to be + # the example's metadata. + metadata = case example_or_group + when ExampleGroup then example_or_group.class.metadata + else example_or_group.metadata + end + + repository.items_for(metadata) + end + + def all_hooks_for(position, scope) + hooks_for(position, scope) { return EMPTY_HOOK_ARRAY }.items_and_filters.map(&:first) + end + + def run_owned_hooks_for(position, scope, example_or_group) + matching_hooks_for(position, scope, example_or_group).each do |hook| + hook.run(example_or_group) + end + end + + def processable_hooks_for(position, scope, host) + if scope == :example + all_hooks_for(position, scope) + else + matching_hooks_for(position, scope, host) + end + end + private - def process(host, globals, position, scope) - globals[position][scope].each do |hook| - next unless scope == :example || hook.options_apply?(host) - next if host.parent_groups.any? { |a| a.hooks[position][scope].include?(hook) } - self[position][scope] << hook + def hooks_for(position, scope) + if position == :before + scope == :example ? @before_example_hooks : @before_context_hooks + elsif position == :after + scope == :example ? @after_example_hooks : @after_context_hooks + else # around + @around_example_hooks + end || yield + end + + def ensure_hooks_initialized_for(position, scope) + if position == :before + if scope == :example + @before_example_hooks ||= @filterable_item_repo_class.new(:all?) + else + @before_context_hooks ||= @filterable_item_repo_class.new(:all?) + end + elsif position == :after + if scope == :example + @after_example_hooks ||= @filterable_item_repo_class.new(:all?) + else + @after_context_hooks ||= @filterable_item_repo_class.new(:all?) + end + else # around + @around_example_hooks ||= @filterable_item_repo_class.new(:all?) + end + end + + def process(host, parent_groups, globals, position, scope) + hooks_to_process = globals.processable_hooks_for(position, scope, host) + return if hooks_to_process.empty? + + hooks_to_process -= FlatMap.flat_map(parent_groups) do |group| + group.hooks.all_hooks_for(position, scope) end + return if hooks_to_process.empty? + + repository = ensure_hooks_initialized_for(position, scope) + hooks_to_process.each { |hook| repository.append hook, (yield hook) } end def scope_and_options_from(*args) + return :suite if args.first == :suite scope = extract_scope_from(args) meta = Metadata.build_hash_from(args, :warn_about_example_group_filtering) return scope, meta @@ -532,58 +580,51 @@ def extract_scope_from(args) if known_scope?(args.first) normalized_scope_for(args.shift) elsif args.any? { |a| a.is_a?(Symbol) } - error_message = "You must explicitly give a scope (#{SCOPES.join(", ")}) or scope alias (#{SCOPE_ALIASES.keys.join(", ")}) when using symbols as metadata for a hook." + error_message = "You must explicitly give a scope " \ + "(#{SCOPES.join(", ")}) or scope alias " \ + "(#{SCOPE_ALIASES.keys.join(", ")}) when using symbols as " \ + "metadata for a hook." raise ArgumentError.new error_message else :example end end - # @api private def known_scope?(scope) SCOPES.include?(scope) || SCOPE_ALIASES.keys.include?(scope) end - # @api private def normalized_scope_for(scope) SCOPE_ALIASES[scope] || scope end - def find_hook(hook, scope, example_or_group, initial_procsy) - case [hook, scope] - when [:before, :context] - before_context_hooks_for(example_or_group) - when [:after, :context] - after_context_hooks_for(example_or_group) - when [:around, :example] - around_example_hooks_for(example_or_group, initial_procsy) - when [:before, :example] - before_example_hooks_for(example_or_group) - when [:after, :example] - after_example_hooks_for(example_or_group) - when [:before, :suite], [:after, :suite] - self[hook][:suite].with(example_or_group) + def run_example_hooks_for(example, position, each_method) + owner_parent_groups.__send__(each_method) do |group| + group.hooks.run_owned_hooks_for(position, :example, example) end end - def before_context_hooks_for(group) - GroupHookCollection.new(self[:before][:context]).for(group) - end + def run_around_example_hooks_for(example) + hooks = FlatMap.flat_map(owner_parent_groups) do |group| + group.hooks.matching_hooks_for(:around, :example, example) + end - def after_context_hooks_for(group) - GroupHookCollection.new(self[:after][:context]).for(group) - end + return yield if hooks.empty? # exit early to avoid the extra allocation cost of `Example::Procsy` - def before_example_hooks_for(example) - HookCollection.new(FlatMap.flat_map(@owner.parent_groups.reverse) do |a| - a.hooks[:before][:example] - end).for(example) + initial_procsy = Example::Procsy.new(example) { yield } + hooks.inject(initial_procsy) do |procsy, around_hook| + procsy.wrap { around_hook.execute_with(example, procsy) } + end.call end - def after_example_hooks_for(example) - HookCollection.new(FlatMap.flat_map(@owner.parent_groups) do |a| - a.hooks[:after][:example] - end).for(example) + if respond_to?(:singleton_class) && singleton_class.ancestors.include?(singleton_class) + def owner_parent_groups + @owner.parent_groups + end + else # Ruby < 2.1 (see https://bugs.ruby-lang.org/issues/8035) + def owner_parent_groups + @owner_parent_groups ||= [@owner] + @owner.parent_groups + end end end end diff --git a/lib/rspec/core/memoized_helpers.rb b/lib/rspec/core/memoized_helpers.rb index 11e56bf1a3..bc95a07ded 100644 --- a/lib/rspec/core/memoized_helpers.rb +++ b/lib/rspec/core/memoized_helpers.rb @@ -20,7 +20,7 @@ module MemoizedHelpers # # @example # - # # explicit declaration of subject + # # Explicit declaration of subject. # describe Person do # subject { Person.new(:birthdate => 19.years.ago) } # it "should be eligible to vote" do @@ -29,7 +29,7 @@ module MemoizedHelpers # end # end # - # # implicit subject => { Person.new } + # # Implicit subject => { Person.new }. # describe Person do # it "should be eligible to vote" do # subject.should be_eligible_to_vote @@ -37,17 +37,17 @@ module MemoizedHelpers # end # end # - # # one-liner syntax - expectation is set on the subject + # # One-liner syntax - expectation is set on the subject. # describe Person do # it { is_expected.to be_eligible_to_vote } # # or # it { should be_eligible_to_vote } # end # - # @note Because `subject` is designed to create state that is reset between - # each example, and `before(:context)` is designed to setup state that is - # shared across _all_ examples in an example group, `subject` is _not_ - # intended to be used in a `before(:context)` hook. + # @note Because `subject` is designed to create state that is reset + # between each example, and `before(:context)` is designed to setup + # state that is shared across _all_ examples in an example group, + # `subject` is _not_ intended to be used in a `before(:context)` hook. # # @see #should # @see #should_not @@ -55,7 +55,7 @@ module MemoizedHelpers def subject __memoized.fetch(:subject) do __memoized[:subject] = begin - described = described_class || self.class.description + described = described_class || self.class.metadata.fetch(:description_args).first Class === described ? described.new : described end end @@ -211,8 +211,8 @@ module ClassMethods # though we have yet to see this in practice. You've been warned. # # @note Because `let` is designed to create state that is reset between - # each example, and `before(:context)` is designed to setup state that is - # shared across _all_ examples in an example group, `let` is _not_ + # each example, and `before(:context)` is designed to setup state that + # is shared across _all_ examples in an example group, `let` is _not_ # intended to be used in a `before(:context)` hook. # # @example @@ -221,10 +221,10 @@ module ClassMethods # let(:thing) { Thing.new } # # it "does something" do - # # first invocation, executes block, memoizes and returns result + # # First invocation, executes block, memoizes and returns result. # thing.do_something # - # # second invocation, returns the memoized value + # # Second invocation, returns the memoized value. # thing.should be_something # end # end @@ -302,8 +302,8 @@ def let!(name, &block) end # Declares a `subject` for an example group which can then be wrapped - # with `expect` using `is_expected` to make it the target of an expectation - # in a concise, one-line example. + # with `expect` using `is_expected` to make it the target of an + # expectation in a concise, one-line example. # # Given a `name`, defines a method with that name which returns the # `subject`. This lets you declare the subject once and access it @@ -348,9 +348,9 @@ def subject(name=nil, &block) end end - # Just like `subject`, except the block is invoked by an implicit `before` - # hook. This serves a dual purpose of setting up state and providing a - # memoized reference to that state. + # Just like `subject`, except the block is invoked by an implicit + # `before` hook. This serves a dual purpose of setting up state and + # providing a memoized reference to that state. # # @example # diff --git a/lib/rspec/core/metadata.rb b/lib/rspec/core/metadata.rb index 6eb72328a8..6f71c4bf09 100644 --- a/lib/rspec/core/metadata.rb +++ b/lib/rspec/core/metadata.rb @@ -25,19 +25,51 @@ module Core # @see Configuration#filter_run_including # @see Configuration#filter_run_excluding module Metadata + # Matches strings either at the beginning of the input or prefixed with a + # whitespace, containing the current path, either postfixed with the + # separator, or at the end of the string. Match groups are the character + # before and the character after the string if any. + # + # http://rubular.com/r/fT0gmX6VJX + # http://rubular.com/r/duOrD4i3wb + # http://rubular.com/r/sbAMHFrOx1 + def self.relative_path_regex + @relative_path_regex ||= /(\A|\s)#{File.expand_path('.')}(#{File::SEPARATOR}|\s|\Z)/ + end + # @api private # # @param line [String] current code line # @return [String] relative path to line def self.relative_path(line) - line = line.sub(File.expand_path("."), ".") - line = line.sub(/\A([^:]+:\d+)$/, '\\1') - return nil if line == '-e:1' + line = line.sub(relative_path_regex, "\\1.\\2".freeze) + line = line.sub(/\A([^:]+:\d+)$/, '\\1'.freeze) + return nil if line == '-e:1'.freeze line rescue SecurityError nil end + # @private + # Iteratively walks up from the given metadata through all + # example group ancestors, yielding each metadata hash along the way. + def self.ascending(metadata) + yield metadata + return unless (group_metadata = metadata.fetch(:example_group) { metadata[:parent_example_group] }) + + loop do + yield group_metadata + break unless (group_metadata = group_metadata[:parent_example_group]) + end + end + + # @private + # Returns an enumerator that iteratively walks up the given metadata through all + # example group ancestors, yielding each metadata hash along the way. + def self.ascend(metadata) + enum_for(:ascending, metadata) + end + # @private # Used internally to build a hash from an args array. # Symbols are converted into hash keys with a value of `true`. @@ -56,6 +88,17 @@ def self.build_hash_from(args, warn_about_example_group_filtering=false) hash end + # @private + def self.deep_hash_dup(object) + return object.dup if Array === object + return object unless Hash === object + + object.inject(object.dup) do |duplicate, (key, value)| + duplicate[key] = deep_hash_dup(value) + duplicate + end + end + # @private def self.backtrace_from(block) return caller unless block.respond_to?(:source_location) @@ -103,10 +146,11 @@ def populate_location_attributes file_path_and_line_number_from(caller) end - file_path = Metadata.relative_path(file_path) - metadata[:file_path] = file_path - metadata[:line_number] = line_number.to_i - metadata[:location] = "#{file_path}:#{line_number}" + relative_file_path = Metadata.relative_path(file_path) + metadata[:file_path] = relative_file_path + metadata[:line_number] = line_number.to_i + metadata[:location] = "#{relative_file_path}:#{line_number}" + metadata[:absolute_file_path] = File.expand_path(relative_file_path) end def file_path_and_line_number_from(backtrace) @@ -117,16 +161,16 @@ def file_path_and_line_number_from(backtrace) def description_separator(parent_part, child_part) if parent_part.is_a?(Module) && child_part =~ /^(#|::|\.)/ - '' + ''.freeze else - ' ' + ' '.freeze end end def build_description_from(parent_description=nil, my_description=nil) return parent_description.to_s unless my_description separator = description_separator(parent_description, my_description) - parent_description.to_s + separator + my_description.to_s + (parent_description.to_s + separator) << my_description.to_s end def ensure_valid_user_keys @@ -160,9 +204,11 @@ def self.create(group_metadata, user_metadata, description, block) group_metadata.update(example_metadata) example_metadata[:example_group] = group_metadata + example_metadata[:shared_group_inclusion_backtrace] = SharedExampleGroupInclusionStackFrame.current_backtrace example_metadata.delete(:parent_example_group) - hash = new(example_metadata, user_metadata, [description].compact, block) + description_args = description.nil? ? [] : [description] + hash = new(example_metadata, user_metadata, description_args, block) hash.populate hash.metadata end @@ -204,16 +250,18 @@ def self.backwards_compatibility_default_proc(&example_group_selector) Proc.new do |hash, key| case key when :example_group - # We commonly get here when rspec-core is applying a previously configured - # filter rule, such as when a gem configures: + # We commonly get here when rspec-core is applying a previously + # configured filter rule, such as when a gem configures: # # RSpec.configure do |c| # c.include MyGemHelpers, :example_group => { :file_path => /spec\/my_gem_specs/ } # end # - # It's confusing for a user to get a deprecation at this point in the code, so instead - # we issue a deprecation from the config APIs that take a metadata hash, and MetadataFilter - # sets this thread local to silence the warning here since it would be so confusing. + # It's confusing for a user to get a deprecation at this point in + # the code, so instead we issue a deprecation from the config APIs + # that take a metadata hash, and MetadataFilter sets this thread + # local to silence the warning here since it would be so + # confusing. unless RSpec.thread_local_metadata[:silence_metadata_example_group_deprecations] RSpec.deprecate("The `:example_group` key in an example group's metadata hash", :replacement => "the example group's hash directly for the " \ @@ -264,6 +312,7 @@ def full_description :parent_example_group, :execution_result, :file_path, + :absolute_file_path, :full_description, :line_number, :location, @@ -394,7 +443,8 @@ def attr_accessor(*names) # `metadata[:example_group][:described_class]` when you use # anonymous controller specs) such that changes are written # back to the top-level metadata hash. - # * Exposes the parent group metadata as `[:example_group][:example_group]`. + # * Exposes the parent group metadata as + # `[:example_group][:example_group]`. class LegacyExampleGroupHash include HashImitatable diff --git a/lib/rspec/core/metadata_filter.rb b/lib/rspec/core/metadata_filter.rb index 7c8cac6e3c..ffe8c86258 100644 --- a/lib/rspec/core/metadata_filter.rb +++ b/lib/rspec/core/metadata_filter.rb @@ -8,13 +8,8 @@ module Core module MetadataFilter class << self # @private - def any_apply?(filters, metadata) - filters.any? { |k, v| filter_applies?(k, v, metadata) } - end - - # @private - def all_apply?(filters, metadata) - filters.all? { |k, v| filter_applies?(k, v, metadata) } + def apply?(predicate, filters, metadata) + filters.__send__(predicate) { |k, v| filter_applies?(k, v, metadata) } end # @private @@ -48,9 +43,8 @@ def filter_applies_to_any_value?(key, value, metadata) end def location_filter_applies?(locations, metadata) - # it ignores location filters for other files - line_number = example_group_declaration_line(locations, metadata) - line_number ? line_number_filter_applies?(line_number, metadata) : true + line_numbers = example_group_declaration_lines(locations, metadata) + line_numbers.empty? || line_number_filter_applies?(line_numbers, metadata) end def line_number_filter_applies?(line_numbers, metadata) @@ -59,14 +53,13 @@ def line_number_filter_applies?(line_numbers, metadata) end def relevant_line_numbers(metadata) - return [] unless metadata - [metadata[:line_number]].compact + (relevant_line_numbers(parent_of metadata)) + Metadata.ascend(metadata).map { |meta| meta[:line_number] } end - def example_group_declaration_line(locations, metadata) - parent = parent_of(metadata) - return nil unless parent - locations[File.expand_path(parent[:file_path])] + def example_group_declaration_lines(locations, metadata) + FlatMap.flat_map(Metadata.ascend(metadata)) do |meta| + locations[meta[:absolute_file_path]] + end.uniq end def filters_apply?(key, value, metadata) @@ -75,14 +68,6 @@ def filters_apply?(key, value, metadata) value.all? { |k, v| filter_applies?(k, v, subhash) } end - def parent_of(metadata) - if metadata.key?(:example_group) - metadata[:example_group] - else - metadata[:parent_example_group] - end - end - def silence_metadata_example_group_deprecations RSpec.thread_local_metadata[:silence_metadata_example_group_deprecations] = true yield @@ -91,5 +76,147 @@ def silence_metadata_example_group_deprecations end end end + + # Tracks a collection of filterable items (e.g. modules, hooks, etc) + # and provides an optimized API to get the applicable items for the + # metadata of an example or example group. + # + # There are two implementations, optimized for different uses. + # @private + module FilterableItemRepository + # This implementation is simple, and is optimized for frequent + # updates but rare queries. `append` and `prepend` do no extra + # processing, and no internal memoization is done, since this + # is not optimized for queries. + # + # This is ideal for use by a example or example group, which may + # be updated multiple times with globally configured hooks, etc, + # but will not be queried frequently by other examples or examle + # groups. + # @private + class UpdateOptimized + attr_reader :items_and_filters + + def initialize(applies_predicate) + @applies_predicate = applies_predicate + @items_and_filters = [] + end + + def append(item, metadata) + @items_and_filters << [item, metadata] + end + + def prepend(item, metadata) + @items_and_filters.unshift [item, metadata] + end + + def items_for(request_meta) + @items_and_filters.each_with_object([]) do |(item, item_meta), to_return| + to_return << item if item_meta.empty? || + MetadataFilter.apply?(@applies_predicate, item_meta, request_meta) + end + end + + unless [].respond_to?(:each_with_object) # For 1.8.7 + undef items_for + def items_for(request_meta) + @items_and_filters.inject([]) do |to_return, (item, item_meta)| + to_return << item if item_meta.empty? || + MetadataFilter.apply?(@applies_predicate, item_meta, request_meta) + to_return + end + end + end + end + + # This implementation is much more complex, and is optimized for + # rare (or hopefully no) updates once the queries start. Updates + # incur a cost as it has to clear the memoization and keep track + # of applicable keys. Queries will be O(N) the first time an item + # is provided with a given set of applicable metadata; subsequent + # queries with items with the same set of applicable metadata will + # be O(1) due to internal memoization. + # + # This is ideal for use by config, where filterable items (e.g. hooks) + # are typically added at the start of the process (e.g. in `spec_helper`) + # and then repeatedly queried as example groups and examples are defined. + # @private + class QueryOptimized < UpdateOptimized + alias find_items_for items_for + private :find_items_for + + def initialize(applies_predicate) + super + @applicable_keys = Set.new + @proc_keys = Set.new + @memoized_lookups = Hash.new do |hash, applicable_metadata| + hash[applicable_metadata] = find_items_for(applicable_metadata) + end + end + + def append(item, metadata) + super + handle_mutation(metadata) + end + + def prepend(item, metadata) + super + handle_mutation(metadata) + end + + def items_for(metadata) + # The filtering of `metadata` to `applicable_metadata` is the key thing + # that makes the memoization actually useful in practice, since each + # example and example group have different metadata (e.g. location and + # description). By filtering to the metadata keys our items care about, + # we can ignore extra metadata keys that differ for each example/group. + # For example, given `config.include DBHelpers, :db`, example groups + # can be split into these two sets: those that are tagged with `:db` and those + # that are not. For each set, this method for the first group in the set is + # still an `O(N)` calculation, but all subsequent groups in the set will be + # constant time lookups when they call this method. + applicable_metadata = applicable_metadata_from(metadata) + + if applicable_metadata.any? { |k, _| @proc_keys.include?(k) } + # It's unsafe to memoize lookups involving procs (since they can + # be non-deterministic), so we skip the memoization in this case. + find_items_for(applicable_metadata) + else + @memoized_lookups[applicable_metadata] + end + end + + private + + def handle_mutation(metadata) + @applicable_keys.merge(metadata.keys) + @proc_keys.merge(proc_keys_from metadata) + @memoized_lookups.clear + end + + def applicable_metadata_from(metadata) + @applicable_keys.inject({}) do |hash, key| + hash[key] = metadata[key] if metadata.key?(key) + hash + end + end + + def proc_keys_from(metadata) + metadata.each_with_object([]) do |(key, value), to_return| + to_return << key if Proc === value + end + end + + unless [].respond_to?(:each_with_object) # For 1.8.7 + undef proc_keys_from + def proc_keys_from(metadata) + metadata.inject([]) do |to_return, (key, value)| + to_return << key if Proc === value + to_return + end + end + end + end + end end end diff --git a/lib/rspec/core/minitest_assertions_adapter.rb b/lib/rspec/core/minitest_assertions_adapter.rb index 3509f6d356..25db7514a2 100644 --- a/lib/rspec/core/minitest_assertions_adapter.rb +++ b/lib/rspec/core/minitest_assertions_adapter.rb @@ -1,9 +1,9 @@ begin - # Only the minitest 5.x gem includes the minitest.rb and assertions.rb files + # Only the minitest 5.x gem includes the minitest.rb and assertions.rb files. require 'minitest' require 'minitest/assertions' rescue LoadError - # We must be using Ruby Core's MiniTest or the Minitest gem 4.x + # We must be using Ruby Core's MiniTest or the Minitest gem 4.x. require 'minitest/unit' Minitest = MiniTest end @@ -13,6 +13,9 @@ module Core # @private module MinitestAssertionsAdapter include ::Minitest::Assertions + # Need to forcefully include Pending after Minitest::Assertions + # to make sure our own #skip method beats Minitest's. + include ::RSpec::Core::Pending # Minitest 5.x requires this accessor to be available. See # https://github.com/seattlerb/minitest/blob/38f0a5fcbd9c37c3f80a3eaad4ba84d3fc9947a0/lib/minitest/assertions.rb#L8 diff --git a/lib/rspec/core/mocking_adapters/flexmock.rb b/lib/rspec/core/mocking_adapters/flexmock.rb index 1202f866d7..91475ae7f8 100644 --- a/lib/rspec/core/mocking_adapters/flexmock.rb +++ b/lib/rspec/core/mocking_adapters/flexmock.rb @@ -15,7 +15,7 @@ def self.framework_name end def setup_mocks_for_rspec - # No setup required + # No setup required. end def verify_mocks_for_rspec diff --git a/lib/rspec/core/mocking_adapters/mocha.rb b/lib/rspec/core/mocking_adapters/mocha.rb index f2e56753d8..8caf7b6442 100644 --- a/lib/rspec/core/mocking_adapters/mocha.rb +++ b/lib/rspec/core/mocking_adapters/mocha.rb @@ -2,22 +2,22 @@ # hoops here. # # mocha >= '0.13.0': -# require 'mocha/api' is required -# require 'mocha/object' raises a LoadError b/c the file no longer exists +# require 'mocha/api' is required. +# require 'mocha/object' raises a LoadError b/c the file no longer exists. # mocha < '0.13.0', >= '0.9.7' -# require 'mocha/api' is required -# require 'mocha/object' is required +# require 'mocha/api' is required. +# require 'mocha/object' is required. # mocha < '0.9.7': -# require 'mocha/api' raises a LoadError b/c the file does not yet exist -# require 'mocha/standalone' is required -# require 'mocha/object' is required +# require 'mocha/api' raises a LoadError b/c the file does not yet exist. +# require 'mocha/standalone' is required. +# require 'mocha/object' is required. begin require 'mocha/api' begin require 'mocha/object' rescue LoadError - # Mocha >= 0.13.0 no longer contains this file nor needs it to be loaded + # Mocha >= 0.13.0 no longer contains this file nor needs it to be loaded. end rescue LoadError require 'mocha/standalone' diff --git a/lib/rspec/core/notifications.rb b/lib/rspec/core/notifications.rb index c984555918..50355d2077 100644 --- a/lib/rspec/core/notifications.rb +++ b/lib/rspec/core/notifications.rb @@ -32,17 +32,25 @@ def wrap(line, _code_or_symbol) # end # # @attr example [RSpec::Core::Example] the current example - ExampleNotification = Struct.new(:example) do + ExampleNotification = Struct.new(:example) + class ExampleNotification # @private def self.for(example) - if example.execution_result.pending_fixed? + execution_result = example.execution_result + + if execution_result.pending_fixed? PendingExampleFixedNotification.new(example) - elsif example.execution_result.status == :failed + elsif execution_result.example_skipped? + SkippedExampleNotification.new(example) + elsif execution_result.status == :pending + PendingExampleFailedAsExpectedNotification.new(example) + elsif execution_result.status == :failed FailedExampleNotification.new(example) else new(example) end end + private_class_method :new end @@ -59,35 +67,42 @@ def initialize(reporter) @reporter = reporter end - # @return [Array(RSpec::Core::Example)] list of examples + # @return [Array] list of examples def examples @reporter.examples end - # @return [Array(RSpec::Core::Example)] list of failed examples + # @return [Array] list of failed examples def failed_examples @reporter.failed_examples end - # @return [Array(RSpec::Core::Example)] list of pending examples + # @return [Array] list of pending examples def pending_examples @reporter.pending_examples end - # @return [Array(Rspec::Core::Notifications::ExampleNotification] + # @return [Array] # returns examples as notifications def notifications @notifications ||= format_examples(examples) end - # @return [Array(Rspec::Core::Notifications::FailedExampleNotification] + # @return [Array] # returns failed examples as notifications def failure_notifications @failed_notifications ||= format_examples(failed_examples) end - # @return [String] The list of failed examples, fully formatted in the way that - # RSpec's built-in formatters emit. + # @return [Array] + # returns pending examples as notifications + def pending_notifications + @pending_notifications ||= format_examples(pending_examples) + end + + # @return [String] The list of failed examples, fully formatted in the way + # that RSpec's built-in formatters emit. def fully_formatted_failed_examples(colorizer=::RSpec::Core::Formatters::ConsoleCodes) formatted = "\nFailures:\n" @@ -98,18 +113,13 @@ def fully_formatted_failed_examples(colorizer=::RSpec::Core::Formatters::Console formatted end - # @return [String] The list of pending examples, fully formatted in the way that - # RSpec's built-in formatters emit. + # @return [String] The list of pending examples, fully formatted in the + # way that RSpec's built-in formatters emit. def fully_formatted_pending_examples(colorizer=::RSpec::Core::Formatters::ConsoleCodes) - formatted = "\nPending:\n" - - pending_examples.each do |example| - formatted_caller = RSpec.configuration.backtrace_formatter.backtrace_line(example.location) + formatted = "\nPending: (Failures listed here are expected and do not affect your suite's status)\n" - formatted << - " #{colorizer.wrap(example.full_description, :pending)}\n" \ - " # #{colorizer.wrap(example.execution_result.pending_message, :detail)}\n" \ - " # #{colorizer.wrap(formatted_caller, :detail)}\n" + pending_notifications.each_with_index do |notification, index| + formatted << notification.fully_formatted(index.next, colorizer) end formatted @@ -151,24 +161,24 @@ def description # Returns the message generated for this failure line by line. # - # @return [Array(String)] The example failure message + # @return [Array] The example failure message def message_lines - add_shared_group_line(failure_lines, NullColorizer) + add_shared_group_lines(failure_lines, NullColorizer) end # Returns the message generated for this failure colorized line by line. # # @param colorizer [#wrap] An object to colorize the message_lines by - # @return [Array(String)] The example failure message colorized + # @return [Array] The example failure message colorized def colorized_message_lines(colorizer=::RSpec::Core::Formatters::ConsoleCodes) - add_shared_group_line(failure_lines, colorizer).map do |line| - colorizer.wrap line, RSpec.configuration.failure_color + add_shared_group_lines(failure_lines, colorizer).map do |line| + colorizer.wrap line, message_color end end # Returns the failures formatted backtrace. # - # @return [Array(String)] the examples backtrace lines + # @return [Array] the examples backtrace lines def formatted_backtrace backtrace_formatter.format_backtrace(exception.backtrace, example.metadata) end @@ -176,7 +186,7 @@ def formatted_backtrace # Returns the failures colorized formatted backtrace. # # @param colorizer [#wrap] An object to colorize the message_lines by - # @return [Array(String)] the examples colorized backtrace lines + # @return [Array] the examples colorized backtrace lines def colorized_formatted_backtrace(colorizer=::RSpec::Core::Formatters::ConsoleCodes) formatted_backtrace.map do |backtrace_info| colorizer.wrap "# #{backtrace_info}", RSpec.configuration.detail_color @@ -186,17 +196,7 @@ def colorized_formatted_backtrace(colorizer=::RSpec::Core::Formatters::ConsoleCo # @return [String] The failure information fully formatted in the way that # RSpec's built-in formatters emit. def fully_formatted(failure_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) - formatted = "\n #{failure_number}) #{description}\n" - - colorized_message_lines(colorizer).each do |line| - formatted << RSpec::Support::EncodedString.new(" #{line}\n", encoding_of(formatted)) - end - - colorized_formatted_backtrace(colorizer).each do |line| - formatted << RSpec::Support::EncodedString.new(" #{line}\n", encoding_of(formatted)) - end - - formatted + "\n #{failure_number}) #{description}\n#{formatted_message_and_backtrace(colorizer)}" end private @@ -232,29 +232,12 @@ def failure_lines end end - def add_shared_group_line(lines, colorizer) - unless shared_group_line == "" - lines << colorizer.wrap(shared_group_line, RSpec.configuration.default_color) + def add_shared_group_lines(lines, colorizer) + example.metadata[:shared_group_inclusion_backtrace].each do |frame| + lines << colorizer.wrap(frame.description, RSpec.configuration.default_color) end - lines - end - - def shared_group - @shared_group ||= group_and_parent_groups.find { |group| group.metadata[:shared_group_name] } - end - def shared_group_line - @shared_group_line ||= - if shared_group - "Shared Example Group: \"#{shared_group.metadata[:shared_group_name]}\"" \ - " called from #{backtrace_formatter.backtrace_line(shared_group.location)}" - else - "" - end - end - - def group_and_parent_groups - example.example_group.parent_groups + [example.example_group] + lines end def read_failed_line @@ -276,11 +259,29 @@ def read_failed_line end def find_failed_line - path = File.expand_path(example.file_path) + example_path = example.metadata[:absolute_file_path].downcase exception.backtrace.find do |line| - match = line.match(/(.+?):(\d+)(|:\d+)/) - match && match[1].downcase == path.downcase + next unless (line_path = line[/(.+?):(\d+)(|:\d+)/, 1]) + File.expand_path(line_path).downcase == example_path + end + end + + def formatted_message_and_backtrace(colorizer) + formatted = "" + + colorized_message_lines(colorizer).each do |line| + formatted << RSpec::Support::EncodedString.new(" #{line}\n", encoding_of(formatted)) end + + colorized_formatted_backtrace(colorizer).each do |line| + formatted << RSpec::Support::EncodedString.new(" #{line}\n", encoding_of(formatted)) + end + + formatted + end + + def message_color + RSpec.configuration.failure_color end end @@ -292,7 +293,7 @@ def find_failed_line class PendingExampleFixedNotification < FailedExampleNotification public_class_method :new - # Returns the examples description + # Returns the examples description. # # @return [String] The example description def description @@ -301,7 +302,7 @@ def description # Returns the message generated for this failure line by line. # - # @return [Array(String)] The example failure message + # @return [Array] The example failure message def message_lines ["Expected pending '#{example.execution_result.pending_message}' to fail. No Error was raised."] end @@ -309,15 +310,70 @@ def message_lines # Returns the message generated for this failure colorized line by line. # # @param colorizer [#wrap] An object to colorize the message_lines by - # @return [Array(String)] The example failure message colorized + # @return [Array] The example failure message colorized def colorized_message_lines(colorizer=::RSpec::Core::Formatters::ConsoleCodes) message_lines.map { |line| colorizer.wrap(line, RSpec.configuration.fixed_color) } end end - # The `GroupNotification` represents notifications sent by the reporter which - # contain information about the currently running (or soon to be) example group - # It is used by formatters to access information about that group. + # @private + module PendingExampleNotificationMethods + private + + def fully_formatted_header(pending_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) + colorizer.wrap("\n #{pending_number}) #{example.full_description}\n", :pending) << + colorizer.wrap(" # #{example.execution_result.pending_message}\n", :detail) + end + end + + # The `PendingExampleFailedAsExpectedNotification` extends `FailedExampleNotification` with + # things useful for pending specs that fail as expected. + # + # @attr [RSpec::Core::Example] example the current example + # @see ExampleNotification + class PendingExampleFailedAsExpectedNotification < FailedExampleNotification + include PendingExampleNotificationMethods + public_class_method :new + + # @return [Exception] The exception that occurred while the pending example was executed + def exception + example.execution_result.pending_exception + end + + # @return [String] The pending detail fully formatted in the way that + # RSpec's built-in formatters emit. + def fully_formatted(pending_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) + fully_formatted_header(pending_number, colorizer) << formatted_message_and_backtrace(colorizer) + end + + private + + def message_color + RSpec.configuration.pending_color + end + end + + # The `SkippedExampleNotification` extends `ExampleNotification` with + # things useful for specs that are skipped. + # + # @attr [RSpec::Core::Example] example the current example + # @see ExampleNotification + class SkippedExampleNotification < ExampleNotification + include PendingExampleNotificationMethods + public_class_method :new + + # @return [String] The pending detail fully formatted in the way that + # RSpec's built-in formatters emit. + def fully_formatted(pending_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes) + formatted_caller = RSpec.configuration.backtrace_formatter.backtrace_line(example.location) + fully_formatted_header(pending_number, colorizer) << colorizer.wrap(" # #{formatted_caller}\n", :detail) + end + end + + # The `GroupNotification` represents notifications sent by the reporter + # which contain information about the currently running (or soon to be) + # example group. It is used by formatters to access information about that + # group. # # @example # def example_group_started(notification) @@ -333,11 +389,12 @@ def colorized_message_lines(colorizer=::RSpec::Core::Formatters::ConsoleCodes) MessageNotification = Struct.new(:message) # The `SeedNotification` holds the seed used to randomize examples and - # wether that seed has been used or not. + # whether that seed has been used or not. # # @attr seed [Fixnum] the seed used to randomize ordering - # @attr used [Boolean] wether the seed has been used or not - SeedNotification = Struct.new(:seed, :used) do + # @attr used [Boolean] whether the seed has been used or not + SeedNotification = Struct.new(:seed, :used) + class SeedNotification # @api # @return [Boolean] has the seed been used? def seed_used? @@ -348,7 +405,7 @@ def seed_used? # @return [String] The seed information fully formatted in the way that # RSpec's built-in formatters emit. def fully_formatted - "\nRandomized with seed #{seed}\n\n" + "\nRandomized with seed #{seed}\n" end end @@ -357,13 +414,13 @@ def fully_formatted # of the test run. # # @attr duration [Float] the time taken (in seconds) to run the suite - # @attr examples [Array(RSpec::Core::Example)] the examples run - # @attr failed_examples [Array(RSpec::Core::Example)] the failed examples - # @attr pending_examples [Array(RSpec::Core::Example)] the pending examples + # @attr examples [Array] the examples run + # @attr failed_examples [Array] the failed examples + # @attr pending_examples [Array] the pending examples # @attr load_time [Float] the number of seconds taken to boot RSpec # and load the spec files - SummaryNotification = Struct.new(:duration, :examples, :failed_examples, :pending_examples, :load_time) do - + SummaryNotification = Struct.new(:duration, :examples, :failed_examples, :pending_examples, :load_time) + class SummaryNotification # @api # @return [Fixnum] the number of examples run def example_count @@ -420,18 +477,19 @@ def colorized_totals_line(colorizer=::RSpec::Core::Formatters::ConsoleCodes) def colorized_rerun_commands(colorizer=::RSpec::Core::Formatters::ConsoleCodes) "\nFailed examples:\n\n" + failed_examples.map do |example| - colorizer.wrap("rspec #{example.location}", RSpec.configuration.failure_color) + " " + - colorizer.wrap("# #{example.full_description}", RSpec.configuration.detail_color) + colorizer.wrap("rspec #{example.rerun_argument}", RSpec.configuration.failure_color) + " " + + colorizer.wrap("# #{example.full_description}", RSpec.configuration.detail_color) end.join("\n") end - # @return [String] a formatted version of the time it took to run the suite + # @return [String] a formatted version of the time it took to run the + # suite def formatted_duration Formatters::Helpers.format_duration(duration) end - # @return [String] a formatted version of the time it took to boot RSpec and - # load the spec files + # @return [String] a formatted version of the time it took to boot RSpec + # and load the spec files def formatted_load_time Formatters::Helpers.format_duration(load_time) end @@ -451,16 +509,16 @@ def fully_formatted(colorizer=::RSpec::Core::Formatters::ConsoleCodes) end end - # The `ProfileNotification` holds information about the results of running - # a test suite when profiling is enabled. It is used by formatters to provide + # The `ProfileNotification` holds information about the results of running a + # test suite when profiling is enabled. It is used by formatters to provide # information at the end of the test run for profiling information. # # @attr duration [Float] the time taken (in seconds) to run the suite - # @attr examples [Array(RSpec::Core::Example)] the examples run + # @attr examples [Array] the examples run # @attr number_of_examples [Fixnum] the number of examples to profile - ProfileNotification = Struct.new(:duration, :examples, :number_of_examples) do - - # @return [Array(RSpec::Core::Example)] the slowest examples + ProfileNotification = Struct.new(:duration, :examples, :number_of_examples) + class ProfileNotification + # @return [Array] the slowest examples def slowest_examples @slowest_examples ||= examples.sort_by do |example| @@ -485,7 +543,7 @@ def percentage end end - # @return [Array(RSpec::Core::Example)] the slowest example groups + # @return [Array] the slowest example groups def slowest_groups @slowest_groups ||= calculate_slowest_groups end @@ -517,14 +575,17 @@ def calculate_slowest_groups end # The `DeprecationNotification` is issued by the reporter when a deprecated - # part of RSpec is encountered. It represents information about the deprecated - # call site. + # part of RSpec is encountered. It represents information about the + # deprecated call site. # # @attr message [String] A custom message about the deprecation - # @attr deprecated [String] A custom message about the deprecation (alias of message) + # @attr deprecated [String] A custom message about the deprecation (alias of + # message) # @attr replacement [String] An optional replacement for the deprecation - # @attr call_site [String] An optional call site from which the deprecation was issued - DeprecationNotification = Struct.new(:deprecated, :message, :replacement, :call_site) do + # @attr call_site [String] An optional call site from which the deprecation + # was issued + DeprecationNotification = Struct.new(:deprecated, :message, :replacement, :call_site) + class DeprecationNotification private_class_method :new # @api diff --git a/lib/rspec/core/option_parser.rb b/lib/rspec/core/option_parser.rb index 7e406da01e..8ee17163c1 100644 --- a/lib/rspec/core/option_parser.rb +++ b/lib/rspec/core/option_parser.rb @@ -25,9 +25,9 @@ def parser(options) OptionParser.new do |parser| parser.banner = "Usage: rspec [options] [files or directories]\n\n" - parser.on('-I PATH', 'Specify PATH to add to $LOAD_PATH (may be used more than once).') do |dir| + parser.on('-I PATH', 'Specify PATH to add to $LOAD_PATH (may be used more than once).') do |dirs| options[:libs] ||= [] - options[:libs] << dir + options[:libs].concat(dirs.split(File::PATH_SEPARATOR)) end parser.on('-r', '--require PATH', 'Require a file.') do |path| @@ -59,7 +59,8 @@ def parser(options) options[:fail_fast] = false end - parser.on('--failure-exit-code CODE', Integer, 'Override the exit code used when there are failing specs.') do |code| + parser.on('--failure-exit-code CODE', Integer, + 'Override the exit code used when there are failing specs.') do |code| options[:failure_exit_code] = code end @@ -115,7 +116,8 @@ def parser(options) options[:color] = o end - parser.on('-p', '--[no-]profile [COUNT]', 'Enable profiling of examples and list the slowest examples (default: 10).') do |argument| + parser.on('-p', '--[no-]profile [COUNT]', + 'Enable profiling of examples and list the slowest examples (default: 10).') do |argument| options[:profile_examples] = if argument.nil? true elsif argument == false @@ -153,7 +155,8 @@ def parser(options) options[:pattern] = o end - parser.on('--exclude-pattern PATTERN', 'Load files except those matching pattern. Opposite effect of --pattern.') do |o| + parser.on('--exclude-pattern PATTERN', + 'Load files except those matching pattern. Opposite effect of --pattern.') do |o| options[:exclude_pattern] = o end @@ -198,18 +201,21 @@ def parser(options) exit end - # these options would otherwise be confusing to users, so we forcibly prevent them from executing - # --I is too similar to -I - # -d was a shorthand for --debugger, which is removed, but now would trigger --default-path + # These options would otherwise be confusing to users, so we forcibly + # prevent them from executing. + # + # * --I is too similar to -I. + # * -d was a shorthand for --debugger, which is removed, but now would + # trigger --default-path. invalid_options = %w[-d --I] parser.on_tail('-h', '--help', "You're looking at it.") do - # removing the blank invalid options from the output + # Removing the blank invalid options from the output. puts parser.to_s.gsub(/^\s+(#{invalid_options.join('|')})\s*$\n/, '') exit end - # this prevents usage of the invalid_options + # This prevents usage of the invalid_options. invalid_options.each do |option| parser.on(option) do raise OptionParser::InvalidOption.new diff --git a/lib/rspec/core/pending.rb b/lib/rspec/core/pending.rb index 41639c0da7..f04e3be3b7 100644 --- a/lib/rspec/core/pending.rb +++ b/lib/rspec/core/pending.rb @@ -1,9 +1,10 @@ module RSpec module Core - # Provides methods to mark examples as pending. These methods are available to be - # called from within any example or hook. + # Provides methods to mark examples as pending. These methods are available + # to be called from within any example or hook. module Pending - # Raised in the middle of an example to indicate that it should be marked as skipped. + # Raised in the middle of an example to indicate that it should be marked + # as skipped. class SkipDeclaredInExample < StandardError attr_reader :argument @@ -12,8 +13,9 @@ def initialize(argument) end end - # If Test::Unit is loaded, we'll use its error as baseclass, so that Test::Unit - # will report unmet RSpec expectations as failures rather than errors. + # If Test::Unit is loaded, we'll use its error as baseclass, so that + # Test::Unit will report unmet RSpec expectations as failures rather than + # errors. begin class PendingExampleFixedError < Test::Unit::AssertionFailedError; end rescue @@ -71,7 +73,7 @@ def pending(message=nil) if block_given? raise ArgumentError, <<-EOS.gsub(/^\s+\|/, '') |The semantics of `RSpec::Core::Pending#pending` have changed in - |RSpec 3. In RSpec 2.x, it caused the example to be skipped. In + |RSpec 3. In RSpec 2.x, it caused the example to be skipped. In |RSpec 3, the rest of the example is still run but is expected to |fail, and will be marked as a failure (rather than as pending) if |the example passes. @@ -123,7 +125,7 @@ def skip(message=nil) # @private # - # Mark example as skipped + # Mark example as skipped. # # @param example [RSpec::Core::Example] the example to mark as skipped # @param message_or_bool [Boolean, String] the message to use, or true @@ -134,7 +136,7 @@ def self.mark_skipped!(example, message_or_bool) # @private # - # Mark example as pending + # Mark example as pending. # # @param example [RSpec::Core::Example] the example to mark as pending # @param message_or_bool [Boolean, String] the message to use, or true @@ -152,7 +154,7 @@ def self.mark_pending!(example, message_or_bool) # @private # - # Mark example as fixed + # Mark example as fixed. # # @param example [RSpec::Core::Example] the example to mark as fixed def self.mark_fixed!(example) diff --git a/lib/rspec/core/project_initializer.rb b/lib/rspec/core/project_initializer.rb index 2a0a4168ee..ca707e0367 100644 --- a/lib/rspec/core/project_initializer.rb +++ b/lib/rspec/core/project_initializer.rb @@ -3,7 +3,7 @@ module RSpec module Core # @private - # Generates conventional files for an rspec project + # Generates conventional files for an RSpec project. class ProjectInitializer attr_reader :destination, :stream, :template_path diff --git a/lib/rspec/core/project_initializer/spec/spec_helper.rb b/lib/rspec/core/project_initializer/spec/spec_helper.rb index 607474b24c..b598abee98 100644 --- a/lib/rspec/core/project_initializer/spec/spec_helper.rb +++ b/lib/rspec/core/project_initializer/spec/spec_helper.rb @@ -1,14 +1,16 @@ # This file was generated by the `rspec --init` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. -# The generated `.rspec` file contains `--require spec_helper` which will cause this -# file to always be loaded, without a need to explicitly require it in any files. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. # # Given that it is always loaded, you are encouraged to keep this file as # light-weight as possible. Requiring heavyweight dependencies from this file # will add to the boot time of your test suite on EVERY test run, even for an # individual file that may not need all of that loaded. Instead, consider making # a separate helper file that requires the additional dependencies and performs -# the additional setup, and require it from the spec files that actually need it. +# the additional setup, and require it from the spec files that actually need +# it. # # The `.rspec` file also contains a few flags that are not defaults but that # users commonly want. @@ -22,10 +24,10 @@ # This option will default to `true` in RSpec 4. It makes the `description` # and `failure_message` of custom matchers include text for helper methods # defined using `chain`, e.g.: - # be_bigger_than(2).and_smaller_than(4).description - # # => "be bigger than 2 and smaller than 4" + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" # ...rather than: - # # => "be bigger than 2" + # # => "be bigger than 2" expectations.include_chain_clauses_in_custom_matcher_descriptions = true end @@ -48,8 +50,8 @@ config.filter_run :focus config.run_all_when_everything_filtered = true - # Limits the available syntax to the non-monkey patched syntax that is recommended. - # For more details, see: + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching diff --git a/lib/rspec/core/rake_task.rb b/lib/rspec/core/rake_task.rb index 349a809071..baf1c441d6 100644 --- a/lib/rspec/core/rake_task.rb +++ b/lib/rspec/core/rake_task.rb @@ -1,71 +1,51 @@ require 'rake' require 'rake/tasklib' -require 'shellwords' +require 'rspec/support/ruby_features' module RSpec module Core - # Rspec rake task + # RSpec rake task # # @see Rakefile class RakeTask < ::Rake::TaskLib include ::Rake::DSL if defined?(::Rake::DSL) - # Default path to the rspec executable + # Default path to the RSpec executable. DEFAULT_RSPEC_PATH = File.expand_path('../../../../exe/rspec', __FILE__) # Default pattern for spec files. DEFAULT_PATTERN = 'spec/**{,/*/**}/*_spec.rb' - # Name of task. - # - # default: - # :spec + # Name of task. Defaults to `:spec`. attr_accessor :name # Files matching this pattern will be loaded. - # - # default: - # 'spec/**{,/*/**}/*_spec.rb' + # Defaults to `'spec/**{,/*/**}/*_spec.rb'`. attr_accessor :pattern # Files matching this pattern will be excluded. - # - # default: - # 'spec/**/*_spec.rb' + # Defaults to `nil`. attr_accessor :exclude_pattern - # Whether or not to fail Rake when an error occurs (typically when examples fail). - # - # default: - # true + # Whether or not to fail Rake when an error occurs (typically when + # examples fail). Defaults to `true`. attr_accessor :fail_on_error # A message to print to stderr when there are failures. attr_accessor :failure_message # Use verbose output. If this is set to true, the task will print the - # executed spec command to stdout. - # - # default: - # true + # executed spec command to stdout. Defaults to `true`. attr_accessor :verbose - # Command line options to pass to ruby. - # - # default: - # nil + # Command line options to pass to ruby. Defaults to `nil`. attr_accessor :ruby_opts - # Path to rspec - # - # default: - # 'rspec' + # Path to RSpec. Defaults to the absolute path to the + # rspec binary from the loaded rspec-core gem. attr_accessor :rspec_path - # Command line options to pass to rspec. - # - # default: - # nil + # Command line options to pass to RSpec. Defaults to `nil`. attr_accessor :rspec_opts def initialize(*args, &task_block) @@ -93,7 +73,7 @@ def run_task(verbose) return unless fail_on_error && !success - $stderr.puts "#{command} failed" + $stderr.puts "#{command} failed" if verbose exit $?.exitstatus end @@ -114,22 +94,52 @@ def define(args, &task_block) def file_inclusion_specification if ENV['SPEC'] FileList[ ENV['SPEC']].sort - elsif File.exist?(pattern) - # The provided pattern is a directory or a file, not a file glob. Historically, this - # worked because `FileList[some_dir]` would return `[some_dir]` which would - # get passed to `rspec` and cause it to load files under that dir that match - # the default pattern. To continue working, we need to pass it on to `rspec` - # directly rather than treating it as a `--pattern` option. - # - # TODO: consider deprecating support for this and removing it in RSpec 4. - pattern.shellescape + elsif String === pattern && !File.exist?(pattern) + "--pattern #{escape pattern}" else - "--pattern #{pattern.shellescape}" + # Before RSpec 3.1, we used `FileList` to get the list of matched + # files, and then pass that along to the `rspec` command. Starting + # with 3.1, we prefer to pass along the pattern as-is to the `rspec` + # command, for 3 reasons: + # + # * It's *much* less verbose to pass one `--pattern` option than a + # long list of files. + # * It ensures `task.pattern` and `--pattern` have the same + # behavior. + # * It fixes a bug, where + # `task.pattern = pattern_that_matches_no_files` would run *all* + # files because it would cause no pattern or file args to get + # passed to `rspec`, which causes all files to get run. + # + # However, `FileList` is *far* more flexible than the `--pattern` + # option. Specifically, it supports individual files and directories, + # as well as arrays of files, directories and globs, as well as other + # `FileList` objects. + # + # For backwards compatibility, we have to fall back to using FileList + # if the user has passed a `pattern` option that will not work with + # `--pattern`. + # + # TODO: consider deprecating support for this and removing it in + # RSpec 4. + FileList[pattern].sort.map { |file| escape file } + end + end + + if RSpec::Support::OS.windows? + def escape(shell_command) + "'#{shell_command.gsub("'", "\'")}'" + end + else + require 'shellwords' + + def escape(shell_command) + shell_command.shellescape end end def file_exclusion_specification - " --exclude-pattern #{exclude_pattern.shellescape}" if exclude_pattern + " --exclude-pattern #{escape exclude_pattern}" if exclude_pattern end def spec_command @@ -137,7 +147,7 @@ def spec_command cmd_parts << RUBY cmd_parts << ruby_opts cmd_parts << rspec_load_path - cmd_parts << rspec_path + cmd_parts << escape(rspec_path) cmd_parts << file_inclusion_specification cmd_parts << file_exclusion_specification cmd_parts << rspec_opts @@ -152,9 +162,9 @@ def rspec_load_path @rspec_load_path ||= begin core_and_support = $LOAD_PATH.grep( /#{File::SEPARATOR}rspec-(core|support)[^#{File::SEPARATOR}]*#{File::SEPARATOR}lib/ - ) + ).uniq - "-I#{core_and_support.map(&:shellescape).join(File::PATH_SEPARATOR)}" + "-I#{core_and_support.map { |file| escape file }.join(File::PATH_SEPARATOR)}" end end end diff --git a/lib/rspec/core/reporter.rb b/lib/rspec/core/reporter.rb index 8cb9c9aa6d..0405889355 100644 --- a/lib/rspec/core/reporter.rb +++ b/lib/rspec/core/reporter.rb @@ -14,11 +14,20 @@ def initialize(configuration) # @private attr_reader :examples, :failed_examples, :pending_examples - # Registers a listener to a list of notifications. The reporter will send notification of - # events to all registered listeners + # @private + def reset + @examples = [] + @failed_examples = [] + @pending_examples = [] + end + + # Registers a listener to a list of notifications. The reporter will send + # notification of events to all registered listeners. # - # @param listener [Object] An obect that wishes to be notified of reporter events - # @param notifications [Array] Array of symbols represents the events a listener wishes to subscribe too + # @param listener [Object] An obect that wishes to be notified of reporter + # events + # @param notifications [Array] Array of symbols represents the events a + # listener wishes to subscribe too def register_listener(listener, *notifications) notifications.each do |notification| @listeners[notification.to_sym] << listener @@ -61,6 +70,7 @@ def start(expected_example_count, time=RSpec::Core::Time.now) @start = time @load_time = (@start - @configuration.start_time).to_f notify :start, Notifications::StartNotification.new(expected_example_count, @load_time) + notify :seed, Notifications::SeedNotification.new(@configuration.seed, seed_used?) end # @private @@ -113,10 +123,12 @@ def finish notify :dump_pending, Notifications::ExamplesNotification.new(self) notify :dump_failures, Notifications::ExamplesNotification.new(self) notify :deprecation_summary, Notifications::NullNotification - notify :dump_summary, Notifications::SummaryNotification.new(@duration, @examples, @failed_examples, @pending_examples, @load_time) unless mute_profile_output? - notify :dump_profile, Notifications::ProfileNotification.new(@duration, @examples, @configuration.profile_examples) + notify :dump_profile, Notifications::ProfileNotification.new(@duration, @examples, + @configuration.profile_examples) end + notify :dump_summary, Notifications::SummaryNotification.new(@duration, @examples, @failed_examples, + @pending_examples, @load_time) notify :seed, Notifications::SeedNotification.new(@configuration.seed, seed_used?) ensure notify :close, Notifications::NullNotification @@ -138,7 +150,8 @@ def notify(event, notification) private def mute_profile_output? - # Don't print out profiled info if there are failures and `--fail-fast` is used, it just clutters the output + # Don't print out profiled info if there are failures and `--fail-fast` is + # used, it just clutters the output. !@configuration.profile_examples? || (@configuration.fail_fast? && @failed_examples.size > 0) end @@ -146,4 +159,14 @@ def seed_used? @configuration.seed && @configuration.seed_used? end end + + # @private + # # Used in place of a {Reporter} for situations where we don't want reporting output. + class NullReporter + private + + def method_missing(*) + # ignore + end + end end diff --git a/lib/rspec/core/ruby_project.rb b/lib/rspec/core/ruby_project.rb index 2e9a23d2a9..10c89f9762 100644 --- a/lib/rspec/core/ruby_project.rb +++ b/lib/rspec/core/ruby_project.rb @@ -1,9 +1,6 @@ # This is borrowed (slightly modified) from Scott Taylor's # project_path project: # http://github.com/smtlaissezfaire/project_path - -require 'pathname' - module RSpec module Core # @private @@ -29,8 +26,19 @@ def find_first_parent_containing(dir) end def ascend_until - Pathname(File.expand_path('.')).ascend do |path| + fs = File::SEPARATOR + escaped_slash = "\\#{fs}" + special = "_RSPEC_ESCAPED_SLASH_" + project_path = File.expand_path(".") + parts = project_path.gsub(escaped_slash, special).squeeze(fs).split(fs).map do |x| + x.gsub(special, escaped_slash) + end + + until parts.empty? + path = parts.join(fs) + path = fs if path == "" return path if yield(path) + parts.pop end end diff --git a/lib/rspec/core/runner.rb b/lib/rspec/core/runner.rb index 5b495ae5a9..af5612b92c 100644 --- a/lib/rspec/core/runner.rb +++ b/lib/rspec/core/runner.rb @@ -24,14 +24,15 @@ def self.autorun next unless $!.nil? || $!.is_a?(SystemExit) # We got here because either the end of the program was reached or - # somebody called Kernel#exit. Run the specs and then override any + # somebody called Kernel#exit. Run the specs and then override any # existing exit status with RSpec's exit status if any specs failed. invoke end @installed_at_exit = true end - # Runs the suite of specs and exits the process with an appropriate exit code. + # Runs the suite of specs and exits the process with an appropriate exit + # code. def self.invoke disable_autorun! status = run(ARGV, $stderr, $stdout).to_i @@ -105,12 +106,8 @@ def setup(err, out) # failed. def run_specs(example_groups) @configuration.reporter.report(@world.example_count(example_groups)) do |reporter| - begin - hook_context = SuiteHookContext.new - @configuration.hooks.run(:before, :suite, hook_context) + @configuration.with_suite_hooks do example_groups.map { |g| g.run(reporter) }.all? ? 0 : @configuration.failure_exit_code - ensure - @configuration.hooks.run(:after, :suite, hook_context) end end end @@ -150,7 +147,7 @@ def self.trap_interrupt trap('INT') do exit!(1) if RSpec.world.wants_to_quit RSpec.world.wants_to_quit = true - STDERR.puts "\nExiting... Interrupt again to exit immediately." + STDERR.puts "\nRSpec is shutting down and will print the summary report... Interrupt again to force quit." end end end diff --git a/lib/rspec/core/sandbox.rb b/lib/rspec/core/sandbox.rb new file mode 100644 index 0000000000..e7d518c2e9 --- /dev/null +++ b/lib/rspec/core/sandbox.rb @@ -0,0 +1,37 @@ +module RSpec + module Core + # A sandbox isolates the enclosed code into an environment that looks 'new' + # meaning globally accessed objects are reset for the duration of the + # sandbox. + # + # @note This module is not normally available. You must require + # `rspec/core/sandbox` to load it. + module Sandbox + # Execute a provided block with RSpec global objects (configuration, + # world) reset. This is used to test RSpec with RSpec. + # + # When calling this the configuration is passed into the provided block. + # Use this to set custom configs for your sandboxed examples. + # + # ``` + # Sandbox.sandboxed do |config| + # config.before(:context) { RSpec.current_example = nil } + # end + # ``` + def self.sandboxed + orig_config = RSpec.configuration + orig_world = RSpec.world + orig_example = RSpec.current_example + + RSpec.configuration = RSpec::Core::Configuration.new + RSpec.world = RSpec::Core::World.new(RSpec.configuration) + + yield RSpec.configuration + ensure + RSpec.configuration = orig_config + RSpec.world = orig_world + RSpec.current_example = orig_example + end + end + end +end diff --git a/lib/rspec/core/shared_example_group.rb b/lib/rspec/core/shared_example_group.rb index f744f24209..4af32607cb 100644 --- a/lib/rspec/core/shared_example_group.rb +++ b/lib/rspec/core/shared_example_group.rb @@ -1,5 +1,32 @@ module RSpec module Core + # Represents some functionality that is shared with multiple example groups. + # The functionality is defined by the provided block, which is lazily + # eval'd when the `SharedExampleGroupModule` instance is included in an example + # group. + class SharedExampleGroupModule < Module + def initialize(description, definition) + @description = description + @definition = definition + end + + # Provides a human-readable representation of this module. + def inspect + "#<#{self.class.name} #{@description.inspect}>" + end + alias to_s inspect + + # Ruby callback for when a module is included in another module is class. + # Our definition evaluates the shared group block in the context of the + # including example group. + def included(klass) + inclusion_line = klass.metadata[:location] + SharedExampleGroupInclusionStackFrame.with_frame(@description, inclusion_line) do + klass.class_exec(&@definition) + end + end + end + # Shared example groups let you define common context and/or common # examples that you wish to use in multiple example groups. # @@ -15,16 +42,20 @@ module Core # groups defined at the top level can be included from any example group. module SharedExampleGroup # @overload shared_examples(name, &block) - # @param name [String, Symbol, Module] identifer to use when looking up this shared group + # @param name [String, Symbol, Module] identifer to use when looking up + # this shared group # @param block The block to be eval'd # @overload shared_examples(name, metadata, &block) - # @param name [String, Symbol, Module] identifer to use when looking up this shared group - # @param metadata [Array, Hash] metadata to attach to this group; any example group - # with matching metadata will automatically include this shared example group. + # @param name [String, Symbol, Module] identifer to use when looking up + # this shared group + # @param metadata [Array, Hash] metadata to attach to this + # group; any example group or example with matching metadata will + # automatically include this shared example group. # @param block The block to be eval'd # @overload shared_examples(metadata, &block) - # @param metadata [Array, Hash] metadata to attach to this group; any example group - # with matching metadata will automatically include this shared example group. + # @param metadata [Array, Hash] metadata to attach to this + # group; any example group or example with matching metadata will + # automatically include this shared example group. # @param block The block to be eval'd # # Stores the block for later use. The block will be evaluated @@ -62,7 +93,7 @@ def shared_examples(name, *args, &block) # @api private # - # Shared examples top level DSL + # Shared examples top level DSL. module TopLevelDSL # @private def self.definitions @@ -82,7 +113,7 @@ def self.exposed_globally? # @api private # - # Adds the top level DSL methods to Module and the top level binding + # Adds the top level DSL methods to Module and the top level binding. def self.expose_globally! return if exposed_globally? Core::DSL.change_global_dsl(&definitions) @@ -91,7 +122,7 @@ def self.expose_globally! # @api private # - # Removes the top level DSL methods to Module and the top level binding + # Removes the top level DSL methods to Module and the top level binding. def self.remove_globally! return unless exposed_globally? @@ -118,12 +149,7 @@ def add(context, name, *metadata_args, &block) end return if metadata_args.empty? - - mod = Module.new - (class << mod; self; end).__send__(:define_method, :included) do |host| - host.class_exec(&block) - end - RSpec.configuration.include mod, *metadata_args + RSpec.configuration.include SharedExampleGroupModule.new(name, block), *metadata_args end def find(lookup_contexts, name) diff --git a/lib/rspec/core/test_unit_assertions_adapter.rb b/lib/rspec/core/test_unit_assertions_adapter.rb index 8fb09eb914..d84ecb1441 100644 --- a/lib/rspec/core/test_unit_assertions_adapter.rb +++ b/lib/rspec/core/test_unit_assertions_adapter.rb @@ -16,14 +16,14 @@ module TestUnitAssertionsAdapter # adding a shim for the new updates. Thus instead of checking on the # RUBY_VERSION we need to check ancestors. begin - # MiniTest is 4.x - # Minitest is 5.x + # MiniTest is 4.x. + # Minitest is 5.x. if ancestors.include?(::Minitest::Assertions) require 'rspec/core/minitest_assertions_adapter' include ::RSpec::Core::MinitestAssertionsAdapter end rescue NameError - # No-op. Minitest 5.x was not loaded + # No-op. Minitest 5.x was not loaded. end end end diff --git a/lib/rspec/core/version.rb b/lib/rspec/core/version.rb index 30d470a5c9..5fa13d465d 100644 --- a/lib/rspec/core/version.rb +++ b/lib/rspec/core/version.rb @@ -3,7 +3,7 @@ module Core # Version information for RSpec Core. module Version # Current version of RSpec Core, in semantic versioning format. - STRING = '3.1.0' + STRING = '3.2.0' end end end diff --git a/lib/rspec/core/warnings.rb b/lib/rspec/core/warnings.rb index ce324e62d8..b8800591bc 100644 --- a/lib/rspec/core/warnings.rb +++ b/lib/rspec/core/warnings.rb @@ -6,7 +6,7 @@ module Core module Warnings # @private # - # Used internally to print deprecation warnings + # Used internally to print deprecation warnings. def deprecate(deprecated, data={}) RSpec.configuration.reporter.deprecation( { @@ -18,7 +18,7 @@ def deprecate(deprecated, data={}) # @private # - # Used internally to print deprecation warnings + # Used internally to print deprecation warnings. def warn_deprecation(message, opts={}) RSpec.configuration.reporter.deprecation opts.merge(:message => message) end diff --git a/lib/rspec/core/world.rb b/lib/rspec/core/world.rb index e36f56694c..307538eecc 100644 --- a/lib/rspec/core/world.rb +++ b/lib/rspec/core/world.rb @@ -2,14 +2,12 @@ module RSpec module Core # @api private # - # Internal container for global non-configuration data + # Internal container for global non-configuration data. class World - include RSpec::Core::Hooks - # @private attr_reader :example_groups, :filtered_examples - # Used internally to determine what to do when a SIGINT is received + # Used internally to determine what to do when a SIGINT is received. attr_accessor :wants_to_quit def initialize(configuration=RSpec.configuration) @@ -26,19 +24,14 @@ def initialize(configuration=RSpec.configuration) end # @private - # Used internally to clear remaining groups when fail_fast is set + # Used internally to clear remaining groups when fail_fast is set. def clear_remaining_example_groups example_groups.clear end - # @private - def windows_os? - RbConfig::CONFIG['host_os'] =~ /cygwin|mswin|mingw|bccwin|wince|emx/ - end - # @api private # - # Apply ordering strategy from configuration to example groups + # Apply ordering strategy from configuration to example groups. def ordered_example_groups ordering_strategy = @configuration.ordering_registry.fetch(:global) ordering_strategy.order(@example_groups) @@ -46,7 +39,7 @@ def ordered_example_groups # @api private # - # Reset world to 'scratch' before running suite + # Reset world to 'scratch' before running suite. def reset example_groups.clear @shared_example_group_registry = nil @@ -59,7 +52,7 @@ def filter_manager # @api private # - # Register an example group + # Register an example group. def register(example_group) example_groups << example_group example_group @@ -80,14 +73,9 @@ def exclusion_filter @configuration.exclusion_filter end - # @private - def configure_group(group) - @configuration.configure_group(group) - end - # @api private # - # Get count of examples to be run + # Get count of examples to be run. def example_count(groups=example_groups) FlatMap.flat_map(groups) { |g| g.descendants }. inject(0) { |a, e| a + e.filtered_examples.size } @@ -95,7 +83,7 @@ def example_count(groups=example_groups) # @api private # - # Find line number of previous declaration + # Find line number of previous declaration. def preceding_declaration_line(filter_line) declaration_line_numbers.sort.inject(nil) do |highest_prior_declaration_line, line| line <= filter_line ? line : highest_prior_declaration_line @@ -109,7 +97,7 @@ def reporter # @api private # - # Notify reporter of filters + # Notify reporter of filters. def announce_filters filter_announcements = [] @@ -153,7 +141,7 @@ def everything_filtered_message # @api private # - # Add inclusion filters to announcement message + # Add inclusion filters to announcement message. def announce_inclusion_filter(announcements) return if inclusion_filter.empty? @@ -162,7 +150,7 @@ def announce_inclusion_filter(announcements) # @api private # - # Add exclusion filters to announcement message + # Add exclusion filters to announcement message. def announce_exclusion_filter(announcements) return if exclusion_filter.empty? @@ -172,9 +160,7 @@ def announce_exclusion_filter(announcements) private def declaration_line_numbers - @line_numbers ||= example_groups.inject([]) do |lines, g| - lines + g.declaration_line_numbers - end + @declaration_line_numbers ||= FlatMap.flat_map(example_groups, &:declaration_line_numbers) end end end diff --git a/rspec-core.gemspec b/rspec-core.gemspec index 08b258849e..301dba5b3f 100644 --- a/rspec-core.gemspec +++ b/rspec-core.gemspec @@ -42,9 +42,9 @@ Gem::Specification.new do |s| s.add_development_dependency "rake", "~> 10.0.0" s.add_development_dependency "cucumber", "~> 1.3" s.add_development_dependency "minitest", "~> 5.3" - s.add_development_dependency "aruba", "~> 0.5" + s.add_development_dependency "aruba", "~> 0.6" - s.add_development_dependency "nokogiri", "1.5.2" + s.add_development_dependency "nokogiri", (RUBY_VERSION < '1.9.3' ? "1.5.2" : "~> 1.5") s.add_development_dependency "coderay", "~> 1.0.9" s.add_development_dependency "mocha", "~> 0.13.0" diff --git a/script/clone_all_rspec_repos b/script/clone_all_rspec_repos index f57555608b..f83d2e910f 100755 --- a/script/clone_all_rspec_repos +++ b/script/clone_all_rspec_repos @@ -1,8 +1,8 @@ #!/bin/bash -# This file was generated on 2014-08-23T21:27:12-07:00 from the rspec-dev repo. +# This file was generated on 2015-01-07T22:08:46-08: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 -x +set -e source script/functions.sh if is_mri; then diff --git a/script/functions.sh b/script/functions.sh index 5c7e3d0daa..a96a5c71b4 100644 --- a/script/functions.sh +++ b/script/functions.sh @@ -1,80 +1,15 @@ -# This file was generated on 2014-08-23T21:27:12-07:00 from the rspec-dev repo. +# This file was generated on 2015-01-07T22:08:46-08: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 )" +source $SCRIPT_DIR/travis_functions.sh +source $SCRIPT_DIR/predicate_functions.sh + # idea taken from: http://blog.headius.com/2010/03/jruby-startup-time-tips.html export JRUBY_OPTS="${JRUBY_OPTS} -X-C" # disable JIT since these processes are so short lived SPECS_HAVE_RUN_FILE=specs.out MAINTENANCE_BRANCH=`cat maintenance-branch` -# Taken from: -# https://github.com/travis-ci/travis-build/blob/e9314616e182a23e6a280199cd9070bfc7cae548/lib/travis/build/script/templates/header.sh#L34-L53 -travis_retry() { - local result=0 - local count=1 - while [ $count -le 3 ]; do - [ $result -ne 0 ] && { - echo -e "\n\033[33;1mThe command \"$@\" failed. Retrying, $count of 3.\033[0m\n" >&2 - } - "$@" - result=$? - [ $result -eq 0 ] && break - count=$(($count + 1)) - sleep 1 - done - - [ $count -eq 3 ] && { - echo "\n\033[33;1mThe command \"$@\" failed 3 times.\033[0m\n" >&2 - } - - return $result -} - -function is_mri { - if ruby -e "exit(!defined?(RUBY_ENGINE) || RUBY_ENGINE == 'ruby')"; then - # RUBY_ENGINE only returns 'ruby' on MRI. - # MRI 1.8.7 lacks the constant but all other rubies have it (including JRuby in 1.8 mode) - return 0 - else - return 1 - fi; -} - -function is_mri_192 { - if is_mri; then - if ruby -e "exit(RUBY_VERSION == '1.9.2')"; then - return 0 - else - return 1 - fi - else - return 1 - fi -} - -function rspec_support_compatible { - if [ "$MAINTENANCE_BRANCH" != "2-99-maintenance" ] && [ "$MAINTENANCE_BRANCH" != "2-14-maintenance" ]; then - return 0 - else - return 1 - fi -} - -function documentation_enforced { - if [ -x ./bin/yard ]; then - return 0 - else - return 1 - fi -} - -function style_and_lint_enforced { - if [ -x ./bin/rubocop ]; then - return 0 - else - return 1 - 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" @@ -90,6 +25,7 @@ function run_specs_and_record_done { rspec_bin=script/rspec_with_simplecov fi; + echo "${PWD}/bin/rspec" $rspec_bin spec --backtrace --format progress --profile --format progress --out $SPECS_HAVE_RUN_FILE } @@ -102,6 +38,8 @@ function run_cukes { # spec failures in our spec suite due to problems with this mode. export JAVA_OPTS='-client -XX:+TieredCompilation -XX:TieredStopAtLevel=1' + echo "${PWD}/bin/cucumber" + if is_mri_192; then # For some reason we get SystemStackError on 1.9.2 when using # the bin/cucumber approach below. That approach is faster @@ -118,6 +56,8 @@ function run_cukes { } function run_specs_one_by_one { + echo "Running each spec file, one-by-one..." + for file in `find spec -iname '*_spec.rb'`; do bin/rspec $file -b --format progress done @@ -125,10 +65,8 @@ function run_specs_one_by_one { function run_spec_suite_for { if [ ! -f ../$1/$SPECS_HAVE_RUN_FILE ]; then # don't rerun specs that have already run - pushd ../$1 - echo echo "Running specs for $1" - echo + pushd ../$1 unset BUNDLE_GEMFILE bundle_install_flags=`cat .travis.yml | grep bundler_args | tr -d '"' | grep -o " .*"` travis_retry eval "bundle install $bundle_install_flags" @@ -138,8 +76,11 @@ function run_spec_suite_for { } function check_documentation_coverage { + echo "bin/yard stats --list-undoc" + bin/yard stats --list-undoc | ruby -e " while line = gets + has_warnings ||= line.start_with?('[warn]:') coverage ||= line[/([\d\.]+)% documented/, 1] puts line end @@ -148,21 +89,41 @@ function check_documentation_coverage { puts \"\n\nMissing documentation coverage (currently at #{coverage}%)\" exit(1) end + + if has_warnings + puts \"\n\nYARD emitted documentation warnings.\" + exit(1) + end + " + + # Some warnings only show up when generating docs, so do that as well. + bin/yard doc --no-cache | ruby -e " + while line = gets + has_warnings ||= line.start_with?('[warn]:') + has_errors ||= line.start_with?('[error]:') + puts line + end + + if has_warnings || has_errors + puts \"\n\nYARD emitted documentation warnings or errors.\" + exit(1) + end " } function check_style_and_lint { + echo "bin/rubucop lib" bin/rubocop lib } function run_all_spec_suites { - run_specs_one_by_one - run_spec_suite_for "rspec-core" - run_spec_suite_for "rspec-expectations" - run_spec_suite_for "rspec-mocks" - run_spec_suite_for "rspec-rails" + fold "one-by-one specs" run_specs_one_by_one + 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_support_compatible; then - run_spec_suite_for "rspec-support" + fold "rspec-support specs" run_spec_suite_for "rspec-support" fi } diff --git a/script/predicate_functions.sh b/script/predicate_functions.sh new file mode 100644 index 0000000000..fc5d372c50 --- /dev/null +++ b/script/predicate_functions.sh @@ -0,0 +1,64 @@ +# This file was generated on 2015-01-07T22:08:46-08: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 { + if ruby -e "exit(!defined?(RUBY_ENGINE) || RUBY_ENGINE == 'ruby')"; then + # RUBY_ENGINE only returns 'ruby' on MRI. + # MRI 1.8.7 lacks the constant but all other rubies have it (including JRuby in 1.8 mode) + return 0 + else + return 1 + fi; +} + +function is_mri_192 { + if is_mri; then + if ruby -e "exit(RUBY_VERSION == '1.9.2')"; then + return 0 + else + return 1 + fi + else + return 1 + fi +} + +function is_mri_2plus { + if is_mri; then + if ruby -e "exit(RUBY_VERSION.to_f > 2.0)"; then + return 0 + else + return 1 + fi + else + return 1 + fi +} + +function rspec_support_compatible { + if [ "$MAINTENANCE_BRANCH" != "2-99-maintenance" ] && [ "$MAINTENANCE_BRANCH" != "2-14-maintenance" ]; then + return 0 + else + return 1 + fi +} + +function documentation_enforced { + if [ -x ./bin/yard ]; then + if is_mri_2plus; then + return 0 + else + return 1 + fi + else + return 1 + fi +} + +function style_and_lint_enforced { + if [ -x ./bin/rubocop ]; then + return 0 + else + return 1 + fi +} diff --git a/script/run_build b/script/run_build index 10c1c23091..e1edcef3a9 100755 --- a/script/run_build +++ b/script/run_build @@ -1,8 +1,8 @@ #!/bin/bash -# This file was generated on 2014-08-23T21:27:12-07:00 from the rspec-dev repo. +# This file was generated on 2015-01-07T22:08:46-08: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 -x +set -e source script/functions.sh # Allow repos to override the default functions and add their own @@ -10,15 +10,15 @@ if [ -f script/custom_build_functions.sh ]; then source script/custom_build_functions.sh fi -run_specs_and_record_done -run_cukes +fold "specs" run_specs_and_record_done +fold "cukes" run_cukes if documentation_enforced; then - check_documentation_coverage + fold "doc check" check_documentation_coverage fi if style_and_lint_enforced; then - check_style_and_lint + fold "rubocop" check_style_and_lint fi if is_mri; then diff --git a/script/travis_functions.sh b/script/travis_functions.sh new file mode 100644 index 0000000000..77829b3638 --- /dev/null +++ b/script/travis_functions.sh @@ -0,0 +1,69 @@ +# This file was generated on 2015-01-07T22:08:46-08: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: +# https://github.com/travis-ci/travis-build/blob/e9314616e182a23e6a280199cd9070bfc7cae548/lib/travis/build/script/templates/header.sh#L34-L53 +travis_retry() { + local result=0 + local count=1 + while [ $count -le 3 ]; do + [ $result -ne 0 ] && { + echo -e "\n\033[33;1mThe command \"$@\" failed. Retrying, $count of 3.\033[0m\n" >&2 + } + "$@" + result=$? + [ $result -eq 0 ] && break + count=$(($count + 1)) + sleep 1 + done + + [ $count -eq 3 ] && { + echo "\n\033[33;1mThe command \"$@\" failed 3 times.\033[0m\n" >&2 + } + + return $result +} + +# Taken from https://github.com/vcr/vcr/commit/fa96819c92b783ec0c794f788183e170e4f684b2 +# and https://github.com/vcr/vcr/commit/040aaac5370c68cd13c847c076749cd547a6f9b1 +nano_cmd="$(type -p gdate date | head -1)" +nano_format="+%s%N" +[ "$(uname -s)" != "Darwin" ] || nano_format="${nano_format/%N/000000000}" + +travis_time_start() { + travis_timer_id=$(printf %08x $(( RANDOM * RANDOM ))) + travis_start_time=$($nano_cmd -u "$nano_format") + printf "travis_time:start:%s\r\e[0m" $travis_timer_id +} + +travis_time_finish() { + local travis_end_time=$($nano_cmd -u "$nano_format") + local duration=$(($travis_end_time-$travis_start_time)) + printf "travis_time:end:%s:start=%s,finish=%s,duration=%s\r\e[0m" \ + $travis_timer_id $travis_start_time $travis_end_time $duration +} + +fold() { + local name="$1" + local status=0 + shift 1 + if [ -n "$TRAVIS" ]; then + printf "travis_fold:start:%s\r\e[0m" "$name" + travis_time_start + fi + + "$@" + status=$? + + [ -z "$TRAVIS" ] || travis_time_finish + + if [ "$status" -eq 0 ]; then + if [ -n "$TRAVIS" ]; then + printf "travis_fold:end:%s\r\e[0m" "$name" + fi + else + STATUS="$status" + fi + + return $status +} diff --git a/spec/integration/filtering_spec.rb b/spec/integration/filtering_spec.rb new file mode 100644 index 0000000000..46feb427a7 --- /dev/null +++ b/spec/integration/filtering_spec.rb @@ -0,0 +1,126 @@ +require 'support/aruba_support' + +RSpec.describe 'Filtering' do + include_context "aruba support" + before { clean_current_dir } + + it 'prints a rerun command for shared examples in external files that works to rerun' do + write_file "spec/support/shared_examples.rb", """ + RSpec.shared_examples 'a failing example' do + example { expect(1).to eq(2) } + end + """ + + write_file "spec/host_group_spec.rb", """ + load File.expand_path('../support/shared_examples.rb', __FILE__) + + RSpec.describe 'A group with shared examples' do + include_examples 'a failing example' + end + + RSpec.describe 'A group with a passing example' do + example { expect(1).to eq(1) } + end + """ + + run_command "" + expect(last_cmd_stdout).to include("2 examples, 1 failure") + run_rerun_command_for_failing_spec + expect(last_cmd_stdout).to include("1 example, 1 failure") + # There was originally a bug when doing it again... + run_rerun_command_for_failing_spec + expect(last_cmd_stdout).to include("1 example, 1 failure") + end + + def run_rerun_command_for_failing_spec + command = last_cmd_stdout[/Failed examples:\s+rspec (\S+) #/, 1] + run_command command + end + + context "with a shared example containing a context in a separate file" do + it "runs the example nested inside the shared" do + write_file_formatted 'spec/shared_example.rb', """ + RSpec.shared_examples_for 'a shared example' do + it 'succeeds' do + end + + context 'with a nested context' do + it 'succeeds (nested)' do + end + end + end + """ + + write_file_formatted 'spec/simple_spec.rb', """ + require File.join(File.dirname(__FILE__), 'shared_example.rb') + + RSpec.describe 'top level' do + it_behaves_like 'a shared example' + end + """ + + run_command 'spec/simple_spec.rb:3 -fd' + expect(last_cmd_stdout).to match(/2 examples, 0 failures/) + end + end + + context "passing a line-number filter" do + it "trumps exclusions, except for :if/:unless (which are absolute exclusions)" do + write_file_formatted 'spec/a_spec.rb', """ + RSpec.configure do |c| + c.filter_run_excluding :slow + end + + RSpec.describe 'A slow group', :slow do + example('ex 1') { } + example('ex 2') { } + end + + RSpec.describe 'A group with a slow example' do + example('ex 3' ) { } + example('ex 4', :slow ) { } + example('ex 5', :if => false) { } + end + """ + + run_command "spec/a_spec.rb -fd" + expect(last_cmd_stdout).to include("1 example, 0 failures", "ex 3").and exclude("ex 1", "ex 2", "ex 4", "ex 5") + + run_command "spec/a_spec.rb:5 -fd" # selecting 'A slow group' + expect(last_cmd_stdout).to include("2 examples, 0 failures", "ex 1", "ex 2").and exclude("ex 3", "ex 4", "ex 5") + + run_command "spec/a_spec.rb:12 -fd" # selecting slow example + expect(last_cmd_stdout).to include("1 example, 0 failures", "ex 4").and exclude("ex 1", "ex 2", "ex 3", "ex 5") + + run_command "spec/a_spec.rb:13 -fd" # selecting :if => false example + expect(last_cmd_stdout).to include("0 examples, 0 failures").and exclude("ex 1", "ex 2", "ex 3", "ex 4", "ex 5") + end + end + + context "passing a line-number-filtered file and a non-filtered file" do + it "applies the line number filtering only to the filtered file, running all specs in the non-filtered file except excluded ones" do + write_file_formatted "spec/file_1_spec.rb", """ + RSpec.describe 'File 1' do + it('passes') { } + it('fails') { fail } + end + """ + + write_file_formatted "spec/file_2_spec.rb", """ + RSpec.configure do |c| + c.filter_run_excluding :exclude_me + end + + RSpec.describe 'File 2' do + it('passes') { } + it('passes') { } + it('fails', :exclude_me) { fail } + end + """ + + run_command "spec/file_1_spec.rb:2 spec/file_2_spec.rb -fd" + expect(last_cmd_stdout).to match(/3 examples, 0 failures/) + expect(last_cmd_stdout).not_to match(/fails/) + end + end +end diff --git a/spec/command_line/order_spec.rb b/spec/integration/order_spec.rb similarity index 96% rename from spec/command_line/order_spec.rb rename to spec/integration/order_spec.rb index 741705086c..5d5921e136 100644 --- a/spec/command_line/order_spec.rb +++ b/spec/integration/order_spec.rb @@ -1,8 +1,7 @@ -require 'spec_helper' +require 'support/aruba_support' -RSpec.describe 'command line', :ui, :slow do - let(:stderr) { StringIO.new } - let(:stdout) { StringIO.new } +RSpec.describe 'command line', :ui do + include_context "aruba support" before :all do write_file 'spec/simple_spec.rb', """ @@ -202,12 +201,4 @@ def split_in_half(array) length, midpoint = array.length, array.length / 2 return array.slice(0, midpoint), array.slice(midpoint, length) end - - def run_command(cmd) - in_current_dir do - RSpec::Core::Runner.run(cmd.split, stderr, stdout) - end - ensure - RSpec.reset - end end diff --git a/spec/rspec/core/backtrace_formatter_spec.rb b/spec/rspec/core/backtrace_formatter_spec.rb index 257d546495..133f121719 100644 --- a/spec/rspec/core/backtrace_formatter_spec.rb +++ b/spec/rspec/core/backtrace_formatter_spec.rb @@ -1,5 +1,3 @@ -require "spec_helper" - module RSpec::Core RSpec.describe BacktraceFormatter do def make_backtrace_formatter(exclusion_patterns=nil, inclusion_patterns=nil) @@ -35,9 +33,10 @@ def make_backtrace_formatter(exclusion_patterns=nil, inclusion_patterns=nil) expect(make_backtrace_formatter.exclude?("#{Dir.getwd}/arbitrary")).to be false end - it "includes something in the current working directory even with a matching exclusion pattern" do - formatter = make_backtrace_formatter([/foo/]) - expect(formatter.exclude? "#{Dir.getwd}/foo").to be false + it 'allows users to exclude their bundler vendor directory' do + formatter = make_backtrace_formatter([%r{/vendor/bundle/}]) + vendored_gem_line = File.join(Dir.getwd, "vendor/bundle/gems/mygem-4.1.6/lib/my_gem:241") + expect(formatter.exclude? vendored_gem_line).to be true end context "when the exclusion list has been replaced" do @@ -102,7 +101,7 @@ def make_backtrace_formatter(exclusion_patterns=nil, inclusion_patterns=nil) end describe "#format_backtrace" do - it "excludes lines from rspec libs by default", :unless => RSpec.world.windows_os? do + it "excludes lines from rspec libs by default", :unless => RSpec::Support::OS.windows? do backtrace = [ "/path/to/rspec-expectations/lib/rspec/expectations/foo.rb:37", "/path/to/rspec-expectations/lib/rspec/matchers/foo.rb:37", @@ -114,7 +113,7 @@ def make_backtrace_formatter(exclusion_patterns=nil, inclusion_patterns=nil) expect(BacktraceFormatter.new.format_backtrace(backtrace)).to eq(["./my_spec.rb:5"]) end - it "excludes lines from rspec libs by default", :if => RSpec.world.windows_os? do + it "excludes lines from rspec libs by default", :failing_on_appveyor, :if => RSpec::Support::OS.windows? do backtrace = [ "\\path\\to\\rspec-expectations\\lib\\rspec\\expectations\\foo.rb:37", "\\path\\to\\rspec-expectations\\lib\\rspec\\matchers\\foo.rb:37", @@ -137,16 +136,16 @@ def make_backtrace_formatter(exclusion_patterns=nil, inclusion_patterns=nil) end it "includes full backtrace" do - expect(BacktraceFormatter.new.format_backtrace(backtrace).take(4)).to eq backtrace + expect(BacktraceFormatter.new.format_backtrace(self.backtrace).take(4)).to eq self.backtrace end it "adds a message explaining everything was filtered" do - expect(BacktraceFormatter.new.format_backtrace(backtrace).drop(4).join).to match(/Showing full backtrace/) + expect(BacktraceFormatter.new.format_backtrace(self.backtrace).drop(4).join).to match(/Showing full backtrace/) end end context "when rspec is installed in the current working directory" do - it "excludes lines from rspec libs by default", :unless => RSpec.world.windows_os? do + it "excludes lines from rspec libs by default", :unless => RSpec::Support::OS.windows? do backtrace = [ "#{Dir.getwd}/.bundle/path/to/rspec-expectations/lib/rspec/expectations/foo.rb:37", "#{Dir.getwd}/.bundle/path/to/rspec-expectations/lib/rspec/matchers/foo.rb:37", @@ -209,24 +208,48 @@ def make_backtrace_formatter(exclusion_patterns=nil, inclusion_patterns=nil) let(:formatter) { BacktraceFormatter.new } it "trims current working directory" do - expect(formatter.__send__(:backtrace_line, File.expand_path(__FILE__))).to eq("./spec/rspec/core/backtrace_formatter_spec.rb") + expect(self.formatter.__send__(:backtrace_line, File.expand_path(__FILE__))).to eq("./spec/rspec/core/backtrace_formatter_spec.rb") end it "preserves the original line" do original_line = File.expand_path(__FILE__) - formatter.__send__(:backtrace_line, original_line) + self.formatter.__send__(:backtrace_line, original_line) expect(original_line).to eq(File.expand_path(__FILE__)) end it "deals gracefully with a security error" do safely do - formatter.__send__(:backtrace_line, __FILE__) + self.formatter.__send__(:backtrace_line, __FILE__) # on some rubies, this doesn't raise a SecurityError; this test just # assures that if it *does* raise an error, the error is caught inside end end end + context "when the current directory matches one of the default exclusion patterns" do + include_context "isolated directory" + + around do |ex| + FileUtils.mkdir_p("bin") + Dir.chdir("./bin", &ex) + end + + let(:line) { File.join(Dir.getwd, "foo.rb:13") } + + it 'does not exclude lines from files in the current directory' do + expect(make_backtrace_formatter.exclude? self.line).to be false + end + + context "with inclusion_patterns cleared" do + it 'excludes lines from files in the current directory' do + formatter = make_backtrace_formatter + formatter.inclusion_patterns.clear + + expect(formatter.exclude? self.line).to be true + end + end + end + context "with no patterns" do it "keeps all lines" do lines = ["/tmp/a_file", "some_random_text", "hello\330\271!"] diff --git a/spec/rspec/core/configuration_options_spec.rb b/spec/rspec/core/configuration_options_spec.rb index 206c40f411..f0a3794f36 100644 --- a/spec/rspec/core/configuration_options_spec.rb +++ b/spec/rspec/core/configuration_options_spec.rb @@ -1,11 +1,10 @@ -require 'spec_helper' require 'ostruct' require 'rspec/core/drb' RSpec.describe RSpec::Core::ConfigurationOptions, :isolated_directory => true, :isolated_home => true do include ConfigOptionsHelper - it "warns when HOME env var is not set", :unless => (RUBY_PLATFORM == 'java') do + it "warns when HOME env var is not set", :unless => (RUBY_PLATFORM == 'java' || RSpec::Support::OS.windows?) do without_env_vars 'HOME' do expect_warning_with_call_site(__FILE__, __LINE__ + 1) RSpec::Core::ConfigurationOptions.new([]).options @@ -23,46 +22,45 @@ it "configures deprecation_stream before loading requires (since required files may issue deprecations)" do opts = config_options_object(*%w[--deprecation-out path/to/log --require foo]) - config = instance_double(RSpec::Core::Configuration).as_null_object + configuration = instance_double(RSpec::Core::Configuration).as_null_object - opts.configure(config) + opts.configure(configuration) - expect(config).to have_received(:force).with(:deprecation_stream => "path/to/log").ordered - expect(config).to have_received(:requires=).ordered + expect(configuration).to have_received(:force).with(:deprecation_stream => "path/to/log").ordered + expect(configuration).to have_received(:requires=).ordered end it "configures deprecation_stream before configuring filter_manager" do opts = config_options_object(*%w[--deprecation-out path/to/log --tag foo]) filter_manager = instance_double(RSpec::Core::FilterManager).as_null_object - config = instance_double(RSpec::Core::Configuration, :filter_manager => filter_manager).as_null_object + configuration = instance_double(RSpec::Core::Configuration, :filter_manager => filter_manager).as_null_object - opts.configure(config) + opts.configure(configuration) - expect(config).to have_received(:force).with(:deprecation_stream => "path/to/log").ordered + expect(configuration).to have_received(:force).with(:deprecation_stream => "path/to/log").ordered expect(filter_manager).to have_received(:include).with(:foo => true).ordered end it "configures deprecation_stream before configuring formatters" do opts = config_options_object(*%w[--deprecation-out path/to/log --format doc]) - config = instance_double(RSpec::Core::Configuration).as_null_object + configuration = instance_double(RSpec::Core::Configuration).as_null_object - opts.configure(config) + opts.configure(configuration) - expect(config).to have_received(:force).with(:deprecation_stream => "path/to/log").ordered - expect(config).to have_received(:add_formatter).ordered + expect(configuration).to have_received(:force).with(:deprecation_stream => "path/to/log").ordered + expect(configuration).to have_received(:add_formatter).ordered end it "sends libs before requires" do opts = config_options_object(*%w[--require a/path -I a/lib]) - config = double("config").as_null_object - expect(config).to receive(:libs=).ordered - expect(config).to receive(:requires=).ordered - opts.configure(config) + configuration = double("config").as_null_object + expect(configuration).to receive(:libs=).ordered + expect(configuration).to receive(:requires=).ordered + opts.configure(configuration) end it "loads requires before loading specs" do opts = config_options_object(*%w[-rspec_helper]) - config = RSpec::Core::Configuration.new expect(config).to receive(:requires=).ordered expect(config).to receive(:get_files_to_run).ordered opts.configure(config) @@ -71,15 +69,14 @@ it "sets up load path and requires before formatter" do opts = config_options_object(*%w[--require a/path -f a/formatter]) - config = double("config").as_null_object - expect(config).to receive(:requires=).ordered - expect(config).to receive(:add_formatter).ordered - opts.configure(config) + configuration = double("config").as_null_object + expect(configuration).to receive(:requires=).ordered + expect(configuration).to receive(:add_formatter).ordered + opts.configure(configuration) end it "sets default_path before loading specs" do opts = config_options_object(*%w[--default-path spec]) - config = RSpec::Core::Configuration.new expect(config).to receive(:force).with(:default_path => 'spec').ordered expect(config).to receive(:get_files_to_run).ordered opts.configure(config) @@ -88,7 +85,6 @@ it "sets `files_or_directories_to_run` before `requires` so users can check `files_to_run` in a spec_helper loaded by `--require`" do opts = config_options_object(*%w[--require spec_helper]) - config = RSpec::Core::Configuration.new expect(config).to receive(:files_or_directories_to_run=).ordered expect(config).to receive(:requires=).ordered opts.configure(config) @@ -96,16 +92,23 @@ it "sets default_path before `files_or_directories_to_run` since it relies on it" do opts = config_options_object(*%w[--default-path spec]) - config = RSpec::Core::Configuration.new expect(config).to receive(:force).with(:default_path => 'spec').ordered expect(config).to receive(:files_or_directories_to_run=).ordered opts.configure(config) end + it 'configures the seed (via `order`) before requires so that required files can use the configured seed' do + opts = config_options_object(*%w[ --seed 1234 --require spec_helper ]) + + expect(config).to receive(:force).with(:order => "rand:1234").ordered + expect(config).to receive(:requires=).ordered + + opts.configure(config) + end + { "pattern" => :pattern, "exclude-pattern" => :exclude_pattern }.each do |flag, attr| it "sets #{attr} before `requires` so users can check `files_to_run` in a `spec_helper` loaded by `--require`" do opts = config_options_object(*%W[--require spec_helpe --#{flag} **/*.spec]) - config = RSpec::Core::Configuration.new expect(config).to receive(:force).with(attr => '**/*.spec').ordered expect(config).to receive(:requires=).ordered opts.configure(config) @@ -126,7 +129,6 @@ it "forces color" do opts = config_options_object(*%w[--color]) - config = RSpec::Core::Configuration.new expect(config).to receive(:force).with(:color => true) opts.configure(config) end @@ -142,7 +144,6 @@ ].each do |cli_option, cli_value, config_key, config_value| it "forces #{config_key}" do opts = config_options_object(cli_option, cli_value) - config = RSpec::Core::Configuration.new expect(config).to receive(:force) do |pair| expect(pair.keys.first).to eq(config_key) expect(pair.values.first).to eq(config_value) diff --git a/spec/rspec/core/configuration_spec.rb b/spec/rspec/core/configuration_spec.rb index f73c18e45c..331d734ee3 100644 --- a/spec/rspec/core/configuration_spec.rb +++ b/spec/rspec/core/configuration_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'tmpdir' require 'rspec/support/spec/in_sub_process' @@ -393,52 +392,62 @@ def stub_expectation_adapters end end - describe "#expecting_with_rspec?" do - before do - stub_expectation_adapters + describe "#files_to_run" do + it "loads files not following pattern if named explicitly" do + assign_files_or_directories_to_run "spec/rspec/core/resources/a_bar.rb" + expect(config.files_to_run).to contain_files("spec/rspec/core/resources/a_bar.rb") end - it "returns false by default" do - expect(config).not_to be_expecting_with_rspec + it "prevents repetition of dir when start of the pattern" do + config.pattern = "spec/**/a_spec.rb" + assign_files_or_directories_to_run "spec" + expect(config.files_to_run).to contain_files("spec/rspec/core/resources/a_spec.rb") end - it "returns true when `expect_with :rspec` has been configured" do - config.expect_with :rspec - expect(config).to be_expecting_with_rspec + it "does not prevent repetition of dir when later of the pattern" do + config.pattern = "rspec/**/a_spec.rb" + assign_files_or_directories_to_run "spec" + expect(config.files_to_run).to contain_files("spec/rspec/core/resources/a_spec.rb") end - it "returns true when `expect_with :rspec, :minitest` has been configured" do - config.expect_with :rspec, :minitest - expect(config).to be_expecting_with_rspec + it "supports patterns starting with ./" do + config.pattern = "./spec/**/a_spec.rb" + assign_files_or_directories_to_run "spec" + expect(config.files_to_run).to contain_files("./spec/rspec/core/resources/a_spec.rb") end - it "returns true when `expect_with :minitest, :rspec` has been configured" do - config.expect_with :minitest, :rspec - expect(config).to be_expecting_with_rspec - end + it "supports absolute path patterns", :failing_on_appveyor, + :pending => false, + :skip => (ENV['APPVEYOR'] ? "Failing on AppVeyor but :pending isn't working for some reason" : false) do + dir = File.expand_path("../resources", __FILE__) + config.pattern = File.join(dir, "**/*_spec.rb") + assign_files_or_directories_to_run "spec" - it "returns false when `expect_with :minitest` has been configured" do - config.expect_with :minitest - expect(config).not_to be_expecting_with_rspec + expect(config.files_to_run).to contain_files( + "./spec/rspec/core/resources/acceptance/foo_spec.rb", + "./spec/rspec/core/resources/a_spec.rb" + ) end - end - describe "#files_to_run" do - it "loads files not following pattern if named explicitly" do - assign_files_or_directories_to_run "spec/rspec/core/resources/a_bar.rb" - expect(config.files_to_run).to eq([ "spec/rspec/core/resources/a_bar.rb"]) - end + it "supports relative path patterns for an alternate directory from `spec`" do + Dir.chdir("./spec/rspec/core") do + config.pattern = "resources/**/*_spec.rb" + assign_files_or_directories_to_run "spec" # default dir - it "prevents repetition of dir when start of the pattern" do - config.pattern = "spec/**/a_spec.rb" - assign_files_or_directories_to_run "spec" - expect(config.files_to_run).to eq(["spec/rspec/core/resources/a_spec.rb"]) + expect(config.files_to_run).to contain_files( + "resources/acceptance/foo_spec.rb", + "resources/a_spec.rb" + ) + end end - it "does not prevent repetition of dir when later of the pattern" do - config.pattern = "rspec/**/a_spec.rb" - assign_files_or_directories_to_run "spec" - expect(config.files_to_run).to eq(["spec/rspec/core/resources/a_spec.rb"]) + it "does not attempt to treat the pattern relative to `.` if it uses `**` in the first path segment as that would cause it load specs from vendored gems" do + Dir.chdir("./spec/rspec/core") do + config.pattern = "**/*_spec.rb" + assign_files_or_directories_to_run "spec" # default dir + + expect(config.files_to_run).to contain_files() + end end it 'reloads when `files_or_directories_to_run` is reassigned' do @@ -448,18 +457,21 @@ def stub_expectation_adapters expect { config.files_or_directories_to_run = "spec" }.to change { config.files_to_run }. - to(["spec/rspec/core/resources/a_spec.rb"]) + to(a_file_collection("spec/rspec/core/resources/a_spec.rb")) end - context "with :" do - it "overrides inclusion filters set on config" do - config.filter_run_including :foo => :bar - assign_files_or_directories_to_run "path/to/file.rb:37" - expect(inclusion_filter.size).to eq(1) - expect(inclusion_filter[:locations].keys.first).to match(/path\/to\/file\.rb$/) - expect(inclusion_filter[:locations].values.first).to eq([37]) - end + it 'attempts to load the provided file names' do + assign_files_or_directories_to_run "path/to/some/file.rb" + expect(config.files_to_run).to eq(["path/to/some/file.rb"]) + end + + it 'does not attempt to load a file at the `default_path`' do + config.default_path = "path/to/dir" + assign_files_or_directories_to_run "path/to/dir" + expect(config.files_to_run).to eq([]) + end + context "with :" do it "overrides inclusion filters set before config" do config.force(:inclusion_filter => {:foo => :bar}) assign_files_or_directories_to_run "path/to/file.rb:37" @@ -468,13 +480,6 @@ def stub_expectation_adapters expect(inclusion_filter[:locations].values.first).to eq([37]) end - it "clears exclusion filters set on config" do - config.exclusion_filter = { :foo => :bar } - assign_files_or_directories_to_run "path/to/file.rb:37" - expect(exclusion_filter).to be_empty, - "expected exclusion filter to be empty:\n#{exclusion_filter}" - end - it "clears exclusion filters set before config" do config.force(:exclusion_filter => { :foo => :bar }) assign_files_or_directories_to_run "path/to/file.rb:37" @@ -486,17 +491,17 @@ def stub_expectation_adapters context "with default pattern" do it "loads files named _spec.rb" do assign_files_or_directories_to_run "spec/rspec/core/resources" - expect(config.files_to_run).to contain_exactly("spec/rspec/core/resources/a_spec.rb", "spec/rspec/core/resources/acceptance/foo_spec.rb") + expect(config.files_to_run).to contain_files("spec/rspec/core/resources/a_spec.rb", "spec/rspec/core/resources/acceptance/foo_spec.rb") end - it "loads files in Windows", :if => RSpec.world.windows_os? do + it "loads files in Windows", :if => RSpec::Support::OS.windows? do assign_files_or_directories_to_run "C:\\path\\to\\project\\spec\\sub\\foo_spec.rb" - expect(config.files_to_run).to eq(["C:/path/to/project/spec/sub/foo_spec.rb"]) + expect(config.files_to_run).to contain_files("C:/path/to/project/spec/sub/foo_spec.rb") end - it "loads files in Windows when directory is specified", :if => RSpec.world.windows_os? do + it "loads files in Windows when directory is specified", :failing_on_appveyor, :if => RSpec::Support::OS.windows? do assign_files_or_directories_to_run "spec\\rspec\\core\\resources" - expect(config.files_to_run).to eq(["spec/rspec/core/resources/a_spec.rb"]) + expect(config.files_to_run).to contain_files("spec/rspec/core/resources/a_spec.rb") end it_behaves_like "handling symlinked directories when loading spec files" do @@ -529,9 +534,11 @@ def loaded_files end def specify_consistent_ordering_of_files_to_run + allow(File).to receive(:directory?).and_call_original allow(File).to receive(:directory?).with('a') { true } globbed_files = nil allow(Dir).to receive(:[]).with(/^\{?a/) { globbed_files } + allow(Dir).to receive(:[]).with(a_string_starting_with(Dir.getwd)) { [] } orderings = [ %w[ a/1.rb a/2.rb a/3.rb ], @@ -829,7 +836,7 @@ def you_call_this_a_blt? it_behaves_like "metadata hash builder" do def metadata_hash(*args) config.include(InstanceLevelMethods, *args) - config.include_or_extend_modules.last.last + config.instance_variable_get(:@include_modules).items_and_filters.last.last end end @@ -839,7 +846,7 @@ def metadata_hash(*args) c.include(InstanceLevelMethods) end - group = ExampleGroup.describe('does like, stuff and junk', :magic_key => :include) { } + group = RSpec.describe('does like, stuff and junk', :magic_key => :include) { } expect(group).not_to respond_to(:you_call_this_a_blt?) expect(group.new.you_call_this_a_blt?).to eq("egad man, where's the mayo?!?!?") end @@ -851,12 +858,74 @@ def metadata_hash(*args) c.include(InstanceLevelMethods, :magic_key => :include) end - group = ExampleGroup.describe('does like, stuff and junk', :magic_key => :include) { } + group = RSpec.describe('does like, stuff and junk', :magic_key => :include) { } expect(group).not_to respond_to(:you_call_this_a_blt?) expect(group.new.you_call_this_a_blt?).to eq("egad man, where's the mayo?!?!?") end - end + it "includes the given module into the singleton class of matching examples" do + RSpec.configure do |c| + c.include(InstanceLevelMethods, :magic_key => :include) + end + + value = ex1 = ex2 = nil + + RSpec.describe("Group") do + ex1 = example("ex", :magic_key => :include) do + value = you_call_this_a_blt? + end + + ex2 = example("ex") { you_call_this_a_blt? } + end.run + + expect(ex1.execution_result.exception).to be_nil + expect(value).to match(/egad/) + expect(ex2.execution_result.exception).to be_a(NameError) + end + + it "ensures that `before` hooks have access to the module methods, even when only included in the singleton class of one example" do + RSpec.configure do |c| + c.include(Module.new { def which_mod; :mod_1; end }, :mod_1) + c.include(Module.new { def which_mod; :mod_2; end }, :mod_2) + end + + ex1_value = ex2_value = ex3 = nil + + RSpec.describe("group") do + before { @value = which_mod } + example("ex", :mod_1) { ex1_value = @value } + example("ex", :mod_2) { ex2_value = @value } + ex3 = example("ex") { } + end.run + + expect(ex1_value).to eq(:mod_1) + expect(ex2_value).to eq(:mod_2) + expect(ex3.execution_result.exception).to be_a(NameError) + end + + it "does not include the module in an example's singleton class when it has already been included in the group" do + mod = Module.new do + def self.inclusions + @inclusions ||= [] + end + + def self.included(klass) + inclusions << klass + end + end + + RSpec.configure do |c| + c.include mod, :magic_key + end + + group = RSpec.describe("Group", :magic_key) do + example("ex", :magic_key) { } + end + + group.run + expect(mod.inclusions).to eq([group]) + end + end end describe "#extend" do @@ -870,7 +939,7 @@ def that_thing it_behaves_like "metadata hash builder" do def metadata_hash(*args) config.extend(ThatThingISentYou, *args) - config.include_or_extend_modules.last.last + config.instance_variable_get(:@extend_modules).items_and_filters.last.last end end @@ -879,12 +948,52 @@ def metadata_hash(*args) c.extend(ThatThingISentYou, :magic_key => :extend) end - group = ExampleGroup.describe(ThatThingISentYou, :magic_key => :extend) { } + group = RSpec.describe(ThatThingISentYou, :magic_key => :extend) { } expect(group).to respond_to(:that_thing) end end + describe "#prepend", :if => RSpec::Support::RubyFeatures.module_prepends_supported? do + include_examples "warning of deprecated `:example_group` during filtering configuration", :prepend, Enumerable + + module SomeRandomMod + def foo + "foobar" + end + end + + it_behaves_like "metadata hash builder" do + def metadata_hash(*args) + config.prepend(SomeRandomMod, *args) + config.instance_variable_get(:@prepend_modules).items_and_filters.last.last + end + end + + context "with no filter" do + it "prepends the given module into each example group" do + RSpec.configure do |c| + c.prepend(SomeRandomMod) + end + + group = RSpec.describe('yo') { } + expect(group.new.foo).to eq("foobar") + end + end + + context "with a filter" do + it "prepends the given module into each matching example group" do + RSpec.configure do |c| + c.prepend(SomeRandomMod, :magic_key => :include) + end + + group = RSpec.describe('yo', :magic_key => :include) { } + expect(group.new.foo).to eq("foobar") + end + end + + end + describe "#run_all_when_everything_filtered?" do it "defaults to false" do @@ -952,54 +1061,56 @@ def metadata_hash(*args) expect(config.color_enabled?(output)).to be_falsey end end + end - context "on windows" do - before do - @original_host = RbConfig::CONFIG['host_os'] - RbConfig::CONFIG['host_os'] = 'mingw' - allow(config).to receive(:require) - end + context "on windows" do + before do + @original_host = RbConfig::CONFIG['host_os'] + RbConfig::CONFIG['host_os'] = 'mingw' + allow(config).to receive(:require) + end - after do - RbConfig::CONFIG['host_os'] = @original_host - end + after do + RbConfig::CONFIG['host_os'] = @original_host + end - context "with ANSICON available" do - around(:each) { |e| with_env_vars('ANSICON' => 'ANSICON', &e) } + context "with ANSICON available" do + around(:each) { |e| with_env_vars('ANSICON' => 'ANSICON', &e) } - it "enables colors" do - config.output_stream = StringIO.new - allow(config.output_stream).to receive_messages :tty? => true - config.color = true - expect(config.color).to be_truthy - end + it "enables colors" do + config.output_stream = StringIO.new + allow(config.output_stream).to receive_messages :tty? => true + config.color = true + expect(config.color).to be_truthy + end - it "leaves output stream intact" do - config.output_stream = $stdout - allow(config).to receive(:require) do |what| - config.output_stream = 'foo' if what =~ /Win32/ - end - config.color = true - expect(config.output_stream).to eq($stdout) + it "leaves output stream intact" do + config.output_stream = $stdout + allow(config).to receive(:require) do |what| + config.output_stream = 'foo' if what =~ /Win32/ end + config.color = true + expect(config.output_stream).to eq($stdout) end + end - context "with ANSICON NOT available" do - before do - allow_warning - end + context "with ANSICON NOT available" do + around { |e| without_env_vars('ANSICON', &e) } - it "warns to install ANSICON" do - allow(config).to receive(:require) { raise LoadError } - expect_warning_with_call_site(__FILE__, __LINE__ + 1, /You must use ANSICON/) - config.color = true - end + before do + allow_warning + end - it "sets color to false" do - allow(config).to receive(:require) { raise LoadError } - config.color = true - expect(config.color).to be_falsey - end + it "warns to install ANSICON" do + allow(config).to receive(:require) { raise LoadError } + expect_warning_with_call_site(__FILE__, __LINE__ + 1, /You must use ANSICON/) + config.color = true + end + + it "sets color to false" do + allow(config).to receive(:require) { raise LoadError } + config.color = true + expect(config.color).to be_falsey end end end @@ -1065,10 +1176,10 @@ def metadata_hash(*args) it 'remembers changes' do legacy_formatter = Class.new - config = RSpec.configuration - config.default_formatter = legacy_formatter - config.reporter - expect(config.default_formatter).to eq(legacy_formatter) + configuration = RSpec.configuration + configuration.default_formatter = legacy_formatter + configuration.reporter + expect(configuration.default_formatter).to eq(legacy_formatter) end end @@ -1177,7 +1288,6 @@ def metadata_hash(*args) describe "#backtrace_exclusion_patterns=" do it "actually receives the new filter values" do - config = Configuration.new config.backtrace_exclusion_patterns = [/.*/] expect(config.backtrace_formatter.exclude? "this").to be_truthy end @@ -1197,7 +1307,6 @@ def metadata_hash(*args) describe "#backtrace_exclusion_patterns" do it "can be appended to" do - config = Configuration.new config.backtrace_exclusion_patterns << /.*/ expect(config.backtrace_formatter.exclude? "this").to be_truthy end @@ -1274,7 +1383,7 @@ def exclude?(line) group_bar_value = example_bar_value = nil RSpec.describe "Group", :foo do - group_bar_value = metadata[:bar] + group_bar_value = self.metadata[:bar] example_bar_value = example("ex", :foo).metadata[:bar] end @@ -1412,16 +1521,16 @@ def exclude?(line) describe "#configure_group" do it "extends with 'extend'" do mod = Module.new - group = ExampleGroup.describe("group", :foo => :bar) + group = RSpec.describe("group", :foo => :bar) config.extend(mod, :foo => :bar) config.configure_group(group) expect(group).to be_a(mod) end - it "extends with 'module'" do + it "includes with 'include'" do mod = Module.new - group = ExampleGroup.describe("group", :foo => :bar) + group = RSpec.describe("group", :foo => :bar) config.include(mod, :foo => :bar) config.configure_group(group) @@ -1430,31 +1539,14 @@ def exclude?(line) it "requires only one matching filter" do mod = Module.new - group = ExampleGroup.describe("group", :foo => :bar) + group = RSpec.describe("group", :foo => :bar) config.include(mod, :foo => :bar, :baz => :bam) config.configure_group(group) expect(group.included_modules).to include(mod) end - it "includes each one before deciding whether to include the next" do - mod1 = Module.new do - def self.included(host) - host.metadata[:foo] = :bar - end - end - mod2 = Module.new - - group = ExampleGroup.describe("group") - - config.include(mod1) - config.include(mod2, :foo => :bar) - config.configure_group(group) - expect(group.included_modules).to include(mod1) - expect(group.included_modules).to include(mod2) - end - - module IncludeOrExtendMeOnce + module IncludeExtendOrPrependMeOnce def self.included(host) raise "included again" if host.instance_methods.include?(:foobar) host.class_exec { def foobar; end } @@ -1464,12 +1556,17 @@ def self.extended(host) raise "extended again" if host.respond_to?(:foobar) def host.foobar; end end + + def self.prepended(host) + raise "prepended again" if host.instance_methods.include?(:barbaz) + host.class_exec { def barbaz; end } + end end it "doesn't include a module when already included in ancestor" do - config.include(IncludeOrExtendMeOnce, :foo => :bar) + config.include(IncludeExtendOrPrependMeOnce, :foo => :bar) - group = ExampleGroup.describe("group", :foo => :bar) + group = RSpec.describe("group", :foo => :bar) child = group.describe("child") config.configure_group(group) @@ -1477,9 +1574,20 @@ def host.foobar; end end it "doesn't extend when ancestor is already extended with same module" do - config.extend(IncludeOrExtendMeOnce, :foo => :bar) + config.extend(IncludeExtendOrPrependMeOnce, :foo => :bar) - group = ExampleGroup.describe("group", :foo => :bar) + group = RSpec.describe("group", :foo => :bar) + child = group.describe("child") + + config.configure_group(group) + config.configure_group(child) + end + + it "doesn't prepend a module when already present in ancestor chain", + :if => RSpec::Support::RubyFeatures.module_prepends_supported? do + config.prepend(IncludeExtendOrPrependMeOnce, :foo => :bar) + + group = RSpec.describe("group", :foo => :bar) child = group.describe("child") config.configure_group(group) @@ -1502,6 +1610,10 @@ class << self undef :my_group_method if method_defined? :my_group_method end end + + Module.class_exec do + undef :my_group_method if method_defined? :my_group_method + end end it_behaves_like "metadata hash builder" do @@ -1512,6 +1624,23 @@ def metadata_hash(*args) end end + it 'overrides existing definitions of the aliased method name without issueing warnings' do + config.expose_dsl_globally = true + + class << ExampleGroup + def my_group_method; :original; end + end + + Module.class_exec do + def my_group_method; :original; end + end + + config.alias_example_group_to :my_group_method + + expect(ExampleGroup.my_group_method).to be < ExampleGroup + expect(Module.new.my_group_method).to be < ExampleGroup + end + it "allows adding additional metadata" do config.alias_example_group_to :my_group_method, { :some => "thing" } group = ExampleGroup.my_group_method("a group", :another => "thing") @@ -1551,7 +1680,7 @@ class << self end def metadata_hash(*args) config.alias_example_to :my_example_method, *args - group = ExampleGroup.describe("group") + group = RSpec.describe("group") example = group.my_example_method("description") example.metadata end @@ -1774,7 +1903,7 @@ def strategy.order(list) value_1 = value_2 = nil - ExampleGroup.describe "Group" do + RSpec.describe "Group" do it "works" do value_1 = the_example value_2 = another_example_helper diff --git a/spec/rspec/core/drb_spec.rb b/spec/rspec/core/drb_spec.rb index 46350bdb6f..505294d565 100644 --- a/spec/rspec/core/drb_spec.rb +++ b/spec/rspec/core/drb_spec.rb @@ -1,4 +1,3 @@ -require "spec_helper" require 'rspec/core/drb' RSpec.describe RSpec::Core::DRbRunner, :isolated_directory => true, :isolated_home => true, :type => :drb, :unless => RUBY_PLATFORM == 'java' do diff --git a/spec/rspec/core/dsl_spec.rb b/spec/rspec/core/dsl_spec.rb index b22a8ff7a5..cb204f7a8a 100644 --- a/spec/rspec/core/dsl_spec.rb +++ b/spec/rspec/core/dsl_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'rspec/support/spec/in_sub_process' main = self @@ -80,6 +79,27 @@ def changing_expose_dsl_globally end end end + + context "when adding duplicate aliases" do + it "only a single alias is created" do + in_sub_process do + RSpec.configuration.alias_example_group_to(:detail) + RSpec.configuration.alias_example_group_to(:detail) + expect(RSpec::Core::DSL.example_group_aliases.count(:detail)).to eq(1) + end + end + + it "does not undefine the alias multiple times", :issue => 1824 do + in_sub_process do + RSpec.configuration.expose_dsl_globally = true + RSpec.configuration.alias_example_group_to(:detail) + RSpec.configuration.alias_example_group_to(:detail) + + expect { + RSpec.configuration.expose_dsl_globally = false + }.not_to raise_error + end + end + end end end - diff --git a/spec/rspec/core/example_execution_result_spec.rb b/spec/rspec/core/example_execution_result_spec.rb index 148b212810..da60e6240d 100644 --- a/spec/rspec/core/example_execution_result_spec.rb +++ b/spec/rspec/core/example_execution_result_spec.rb @@ -1,5 +1,3 @@ -require 'spec_helper' - module RSpec module Core class Example diff --git a/spec/rspec/core/example_group_constants_spec.rb b/spec/rspec/core/example_group_constants_spec.rb new file mode 100644 index 0000000000..ebeefb79a8 --- /dev/null +++ b/spec/rspec/core/example_group_constants_spec.rb @@ -0,0 +1,15 @@ +# encoding: utf-8 + +RSpec.describe "::RSpec::Core::ExampleGroup" do + context "does not cause problems when users reference a top level constant of the same name" do + file_in_outer_group = File + example { expect(File).to eq ::File } + example { expect(file_in_outer_group).to be(::File) } + + describe "File" do + file_in_inner_group = File + example { expect(File).to eq ::File } + example { expect(file_in_inner_group).to be(::File) } + end + end +end diff --git a/spec/rspec/core/example_group_spec.rb b/spec/rspec/core/example_group_spec.rb index 8b560499c1..f19175777f 100644 --- a/spec/rspec/core/example_group_spec.rb +++ b/spec/rspec/core/example_group_spec.rb @@ -1,30 +1,69 @@ # encoding: utf-8 -require 'spec_helper' module RSpec::Core RSpec.describe ExampleGroup do it_behaves_like "metadata hash builder" do def metadata_hash(*args) - group = ExampleGroup.describe('example description', *args) + group = RSpec.describe('example description', *args) group.metadata end end + %w[ expect double spy ].each do |method| + context "when calling `#{method}`, an example API, on an example group" do + it "tells the user they are in the wrong scope for that API" do + expect { + RSpec.describe { __send__(method, "foo") } + }.to raise_error(ExampleGroup::WrongScopeError) + end + end + end + + %w[ describe context let before it it_behaves_like ].each do |method| + context "when calling `#{method}`, an example group API, from within an example" do + it "tells the user they are in the wrong scope for that API" do + ex = nil + + RSpec.describe do + ex = example { __send__(method, "foo") } + end.run + + expect(ex).to fail_with(ExampleGroup::WrongScopeError) + end + end + end + + it "surfaces NameError from an example group for other missing APIs, like normal" do + expect { + RSpec.describe { foobar } + }.to raise_error(NameError, /foobar/) + end + + it "surfaces NameError from an example for other missing APIs, like normal" do + ex = nil + + RSpec.describe do + ex = example { foobar } + end.run + + expect(ex).to fail_with(NameError) + end + context "when RSpec.configuration.format_docstrings is set to a block" do it "formats the description with that block" do RSpec.configuration.format_docstrings { |s| s.upcase } - group = ExampleGroup.describe(' an example ') + group = RSpec.describe(' an example ') expect(group.description).to eq(' AN EXAMPLE ') end end it 'does not treat the first argument as a metadata key even if it is a symbol' do - group = ExampleGroup.describe(:symbol) + group = RSpec.describe(:symbol) expect(group.metadata).not_to include(:symbol) end it 'treats the first argument as part of the description when it is a symbol' do - group = ExampleGroup.describe(:symbol) + group = RSpec.describe(:symbol) expect(group.description).to eq("symbol") end @@ -39,23 +78,36 @@ def metadata_hash(*args) end end - RSpec::Matchers.define :have_class_const do |class_name| - match do |group| - group.name == "RSpec::ExampleGroups::#{class_name}" && - group == class_name.split('::').inject(RSpec::ExampleGroups) do |mod, name| - mod.const_get(name) + if RUBY_VERSION == "1.9.2" + RSpec::Matchers.define :have_class_const do |class_name| + match do |group| + class_name.gsub!('::','_::') + class_name << '_' + group.name == "RSpec::ExampleGroups::#{class_name}" && + group == class_name.split('::').inject(RSpec::ExampleGroups) do |mod, name| + mod.const_get(name) + end + end + end + else + RSpec::Matchers.define :have_class_const do |class_name, _| + match do |group| + group.name == "RSpec::ExampleGroups::#{class_name}" && + group == class_name.split('::').inject(RSpec::ExampleGroups) do |mod, name| + mod.const_get(name) + end end end end it 'gives groups friendly human readable class names' do stub_const("MyGem::Klass", Class.new) - parent = ExampleGroup.describe(MyGem::Klass) + parent = RSpec.describe(MyGem::Klass) expect(parent).to have_class_const("MyGemKlass") end it 'nests constants to match the group nesting' do - grandparent = ExampleGroup.describe("The grandparent") + grandparent = RSpec.describe("The grandparent") parent = grandparent.describe("the parent") child = parent.describe("the child") @@ -64,7 +116,7 @@ def metadata_hash(*args) end it 'removes non-ascii characters from the const name since some rubies barf on that' do - group = ExampleGroup.describe("A chinese character: 们") + group = RSpec.describe("A chinese character: 们") expect(group).to have_class_const("AChineseCharacter") end @@ -73,12 +125,12 @@ def metadata_hash(*args) ExampleGroup.const_set("1B", Object.new) }.to raise_error(NameError) - group = ExampleGroup.describe("1B") + group = RSpec.describe("1B") expect(group).to have_class_const("Nested1B") end it 'does not warn when defining a Config example group (since RbConfig triggers warnings when Config is referenced)' do - expect { ExampleGroup.describe("Config") }.not_to output.to_stderr + expect { RSpec.describe("Config") }.not_to output.to_stderr end it 'ignores top level constants that have the same name' do @@ -88,8 +140,8 @@ def metadata_hash(*args) expect(child).to have_class_const("SomeParentGroup::Hash") end - it 'disambiguates name collisions by appending a number' do - groups = 10.times.map { ExampleGroup.describe("Collision") } + it 'disambiguates name collisions by appending a number', :unless => RUBY_VERSION == '1.9.2' do + groups = 10.times.map { RSpec.describe("Collision") } expect(groups[0]).to have_class_const("Collision") expect(groups[1]).to have_class_const("Collision_2") expect(groups[8]).to have_class_const("Collision_9") @@ -105,34 +157,34 @@ def metadata_hash(*args) # so the presence of another anonymous group in our # test suite doesn't cause an unexpected number # to be appended. - group = ExampleGroup.describe("name of unnamed group") + group = RSpec.describe("name of unnamed group") subgroup = group.describe expect(subgroup).to have_class_const("NameOfUnnamedGroup::Anonymous") end it 'assigns the const before evaling the group so error messages include the name' do expect { - ExampleGroup.describe("Calling an undefined method") { foo } + RSpec.describe("Calling an undefined method") { foo } }.to raise_error(/ExampleGroups::CallingAnUndefinedMethod/) end - it 'does not have problems with example groups named "Core"' do - ExampleGroup.describe("Core") + it 'does not have problems with example groups named "Core"', :unless => RUBY_VERSION == '1.9.2' do + RSpec.describe("Core") expect(defined?(::RSpec::ExampleGroups::Core)).to be_truthy # The original bug was triggered when a group was defined AFTER one named `Core`, # due to it not using the fully qualified `::RSpec::Core::ExampleGroup` constant. - group = ExampleGroup.describe("Another group") + group = RSpec.describe("Another group") expect(group).to have_class_const("AnotherGroup") end - it 'does not have problems with example groups named "RSpec"' do - ExampleGroup.describe("RSpec") + it 'does not have problems with example groups named "RSpec"', :unless => RUBY_VERSION == '1.9.2' do + RSpec.describe("RSpec") expect(defined?(::RSpec::ExampleGroups::RSpec)).to be_truthy # The original bug was triggered when a group was defined AFTER one named `RSpec`, # due to it not using the fully qualified `::RSpec::Core::ExampleGroup` constant. - group = ExampleGroup.describe("Yet Another group") + group = RSpec.describe("Yet Another group") expect(group).to have_class_const("YetAnotherGroup") end end @@ -143,7 +195,7 @@ def metadata_hash(*args) RSpec.configuration.order = :random run_order = [] - group = ExampleGroup.describe "outer", :order => :defined do + group = RSpec.describe "outer", :order => :defined do context "subgroup 1" do example { run_order << :g1_e1 } example { run_order << :g1_e2 } @@ -166,7 +218,7 @@ def metadata_hash(*args) let(:group) do order = self.run_order - ExampleGroup.describe "group", :order => :unrecognized do + RSpec.describe "group", :order => :unrecognized do example { order << :ex_1 } example { order << :ex_2 } end @@ -174,19 +226,19 @@ def metadata_hash(*args) before do RSpec.configuration.register_ordering(:global, &:reverse) - allow(group).to receive(:warn) + allow(self.group).to receive(:warn) end it 'falls back to the global ordering' do - group.run - expect(run_order).to eq([:ex_2, :ex_1]) + self.group.run + expect(self.run_order).to eq([:ex_2, :ex_1]) end it 'prints a warning so users are notified of their mistake' do warning = nil - allow(group).to receive(:warn) { |msg| warning = msg } + allow(self.group).to receive(:warn) { |msg| warning = msg } - group.run + self.group.run expect(warning).to match(/unrecognized/) expect(warning).to match(/#{File.basename __FILE__}:#{definition_line}/) @@ -206,7 +258,7 @@ def ascending_numbers end run_order = [] - group = ExampleGroup.describe "outer", :order => :custom do + group = RSpec.describe "outer", :order => :custom do example("e2") { run_order << :e2 } example("e1") { run_order << :e1 } @@ -244,7 +296,7 @@ def ascending_numbers describe "top level group" do it "runs its children" do examples_run = [] - group = ExampleGroup.describe("parent") do + group = RSpec.describe("parent") do describe("child") do it "does something" do |ex| examples_run << ex @@ -259,7 +311,7 @@ def ascending_numbers context "with a failure in the top level group" do it "runs its children " do examples_run = [] - group = ExampleGroup.describe("parent") do + group = RSpec.describe("parent") do it "fails" do |ex| examples_run << ex raise "fail" @@ -278,7 +330,7 @@ def ascending_numbers describe "descendants" do it "returns self + all descendants" do - group = ExampleGroup.describe("parent") do + group = RSpec.describe("parent") do describe("child") do describe("grandchild 1") {} describe("grandchild 2") {} @@ -291,14 +343,14 @@ def ascending_numbers describe "child" do it "is known by parent" do - parent = ExampleGroup.describe + parent = RSpec.describe child = parent.describe expect(parent.children).to eq([child]) end it "is not registered in world" do world = RSpec::Core::World.new - parent = ExampleGroup.describe + parent = RSpec.describe world.register(parent) parent.describe expect(world.example_groups).to eq([parent]) @@ -307,25 +359,25 @@ def ascending_numbers describe "filtering" do let(:world) { World.new } - before { allow(RSpec).to receive_messages(:world => world) } + before { allow(RSpec).to receive_messages(:world => self.world) } shared_examples "matching filters" do context "inclusion" do before do filter_manager = FilterManager.new filter_manager.include filter_metadata - allow(world).to receive_messages(:filter_manager => filter_manager) + allow(self.world).to receive_messages(:filter_manager => filter_manager) end it "includes examples in groups matching filter" do - group = ExampleGroup.describe("does something", spec_metadata) + group = RSpec.describe("does something", spec_metadata) all_examples = [ group.example("first"), group.example("second") ] expect(group.filtered_examples).to eq(all_examples) end it "includes examples directly matching filter" do - group = ExampleGroup.describe("does something") + group = RSpec.describe("does something") filtered_examples = [ group.example("first", spec_metadata), group.example("second", spec_metadata) @@ -340,18 +392,18 @@ def ascending_numbers before do filter_manager = FilterManager.new filter_manager.exclude filter_metadata - allow(world).to receive_messages(:filter_manager => filter_manager) + allow(self.world).to receive_messages(:filter_manager => filter_manager) end it "excludes examples in groups matching filter" do - group = ExampleGroup.describe("does something", spec_metadata) + group = RSpec.describe("does something", spec_metadata) [ group.example("first"), group.example("second") ] expect(group.filtered_examples).to be_empty end it "excludes examples directly matching filter" do - group = ExampleGroup.describe("does something") + group = RSpec.describe("does something") [ group.example("first", spec_metadata), group.example("second", spec_metadata) @@ -431,8 +483,8 @@ def ascending_numbers context "with no filters" do it "returns all" do - group = ExampleGroup.describe - allow(group).to receive(:world) { world } + group = RSpec.describe + allow(group).to receive(:world) { self.world } example = group.example("does something") expect(group.filtered_examples).to eq([example]) end @@ -442,9 +494,9 @@ def ascending_numbers it "returns none" do filter_manager = FilterManager.new filter_manager.include :awesome => false - allow(world).to receive_messages(:filter_manager => filter_manager) - group = ExampleGroup.describe - allow(group).to receive(:world) { world } + allow(self.world).to receive_messages(:filter_manager => filter_manager) + group = RSpec.describe + allow(group).to receive(:world) { self.world } group.example("does something") expect(group.filtered_examples).to eq([]) end @@ -455,20 +507,20 @@ def ascending_numbers context "with a constant as the first parameter" do it "is that constant" do - expect(ExampleGroup.describe(Object) { }.described_class).to eq(Object) + expect(RSpec.describe(Object) { }.described_class).to eq(Object) end end context "with a string as the first parameter" do it "is nil" do - expect(ExampleGroup.describe("i'm a computer") { }.described_class).to be_nil + expect(RSpec.describe("i'm a computer") { }.described_class).to be_nil end end context "with a constant in an outer group" do context "and a string in an inner group" do it "is the top level constant" do - group = ExampleGroup.describe(String) do + group = RSpec.describe(String) do describe "inner" do example "described_class is String" do expect(described_class).to eq(String) @@ -482,7 +534,7 @@ def ascending_numbers context "and metadata redefinition after `described_class` call" do it "is the redefined level constant" do - group = ExampleGroup.describe(String) do + group = RSpec.describe(String) do described_class metadata[:described_class] = Object describe "inner" do @@ -499,7 +551,7 @@ def ascending_numbers context "in a nested group" do it "inherits the described class/module from the outer group" do - group = ExampleGroup.describe(String) do + group = RSpec.describe(String) do describe "nested" do example "describes is String" do expect(described_class).to eq(String) @@ -514,7 +566,7 @@ def ascending_numbers def described_class_value value = nil - ExampleGroup.describe(String) do + RSpec.describe(String) do yield if block_given? describe Array do example { value = described_class } @@ -538,7 +590,7 @@ def described_class_value def define_and_run_group(define_outer_example = false) outer_described_class = inner_described_class = nil - ExampleGroup.describe("some string") do + RSpec.describe("some string") do example { outer_described_class = described_class } if define_outer_example describe Array do @@ -575,32 +627,32 @@ def define_and_run_group(define_outer_example = false) describe '#description' do it "grabs the description from the metadata" do - group = ExampleGroup.describe(Object, "my desc") { } + group = RSpec.describe(Object, "my desc") { } expect(group.description).to eq(group.metadata[:description]) end end describe '#metadata' do it "adds the third parameter to the metadata" do - expect(ExampleGroup.describe(Object, nil, 'foo' => 'bar') { }.metadata).to include({ "foo" => 'bar' }) + expect(RSpec.describe(Object, nil, 'foo' => 'bar') { }.metadata).to include({ "foo" => 'bar' }) end it "adds the the file_path to metadata" do - expect(ExampleGroup.describe(Object) { }.metadata[:file_path]).to eq(relative_path(__FILE__)) + expect(RSpec.describe(Object) { }.metadata[:file_path]).to eq(relative_path(__FILE__)) end it "has a reader for file_path" do - expect(ExampleGroup.describe(Object) { }.file_path).to eq(relative_path(__FILE__)) + expect(RSpec.describe(Object) { }.file_path).to eq(relative_path(__FILE__)) end it "adds the line_number to metadata" do - expect(ExampleGroup.describe(Object) { }.metadata[:line_number]).to eq(__LINE__) + expect(RSpec.describe(Object) { }.metadata[:line_number]).to eq(__LINE__) end end [:focus, :fexample, :fit, :fspecify].each do |example_alias| describe ".#{example_alias}" do - let(:focused_example) { ExampleGroup.describe.send example_alias, "a focused example" } + let(:focused_example) { RSpec.describe.send example_alias, "a focused example" } it 'defines an example that can be filtered with :focus => true' do expect(focused_example.metadata[:focus]).to be_truthy @@ -611,7 +663,7 @@ def define_and_run_group(define_outer_example = false) describe "#before, after, and around hooks" do describe "scope aliasing" do it "aliases the `:context` hook scope to `:all` for before-hooks" do - group = ExampleGroup.describe + group = RSpec.describe order = [] group.before(:context) { order << :before_context } group.example("example") { order << :example } @@ -622,7 +674,7 @@ def define_and_run_group(define_outer_example = false) end it "aliases the `:example` hook scope to `:each` for before-hooks" do - group = ExampleGroup.describe + group = RSpec.describe order = [] group.before(:example) { order << :before_example } group.example("example") { order << :example } @@ -633,7 +685,7 @@ def define_and_run_group(define_outer_example = false) end it "aliases the `:context` hook scope to `:all` for after-hooks" do - group = ExampleGroup.describe + group = RSpec.describe order = [] group.example("example") { order << :example } group.example("example") { order << :example } @@ -644,7 +696,7 @@ def define_and_run_group(define_outer_example = false) end it "aliases the `:example` hook scope to `:each` for after-hooks" do - group = ExampleGroup.describe + group = RSpec.describe order = [] group.example("example") { order << :example } group.example("example") { order << :example } @@ -656,7 +708,7 @@ def define_and_run_group(define_outer_example = false) end it "runs the before alls in order" do - group = ExampleGroup.describe + group = RSpec.describe order = [] group.before(:all) { order << 1 } group.before(:all) { order << 2 } @@ -669,7 +721,7 @@ def define_and_run_group(define_outer_example = false) end it "does not set RSpec.world.wants_to_quit in case of an error in before all (without fail_fast?)" do - group = ExampleGroup.describe + group = RSpec.describe group.before(:all) { raise "error in before all" } group.example("example") {} @@ -678,7 +730,7 @@ def define_and_run_group(define_outer_example = false) end it "runs the before eachs in order" do - group = ExampleGroup.describe + group = RSpec.describe order = [] group.before(:each) { order << 1 } group.before(:each) { order << 2 } @@ -691,7 +743,7 @@ def define_and_run_group(define_outer_example = false) end it "runs the after eachs in reverse order" do - group = ExampleGroup.describe + group = RSpec.describe order = [] group.after(:each) { order << 1 } group.after(:each) { order << 2 } @@ -704,7 +756,7 @@ def define_and_run_group(define_outer_example = false) end it "runs the after alls in reverse order" do - group = ExampleGroup.describe + group = RSpec.describe order = [] group.after(:all) { order << 1 } group.after(:all) { order << 2 } @@ -723,7 +775,7 @@ def define_and_run_group(define_outer_example = false) c.filter_run :focus => true end - unfiltered_group = ExampleGroup.describe "unfiltered" do + unfiltered_group = RSpec.describe "unfiltered" do before(:all) { hooks_run << :unfiltered_before_all } after(:all) { hooks_run << :unfiltered_after_all } @@ -732,7 +784,7 @@ def define_and_run_group(define_outer_example = false) end end - filtered_group = ExampleGroup.describe "filtered", :focus => true do + filtered_group = RSpec.describe "filtered", :focus => true do before(:all) { hooks_run << :filtered_before_all } after(:all) { hooks_run << :filtered_after_all } @@ -755,7 +807,7 @@ def define_and_run_group(define_outer_example = false) c.after(:all) { order << :after_all_defined_in_config } end - group = ExampleGroup.describe + group = RSpec.describe group.before(:all) { order << :top_level_before_all } group.before(:each) { order << :before_each } group.after(:each) { order << :after_each } @@ -792,7 +844,7 @@ def define_and_run_group(define_outer_example = false) end context "after(:all)" do - let(:outer) { ExampleGroup.describe } + let(:outer) { RSpec.describe } let(:inner) { outer.describe } it "has access to state defined before(:all)" do @@ -825,7 +877,7 @@ def define_and_run_group(define_outer_example = false) end it "treats an error in before(:each) as a failure" do - group = ExampleGroup.describe + group = RSpec.describe group.before(:each) { raise "error in before each" } example = group.example("equality") { expect(1).to eq(2) } expect(group.run).to be(false) @@ -834,7 +886,7 @@ def define_and_run_group(define_outer_example = false) end it "treats an error in before(:all) as a failure" do - group = ExampleGroup.describe + group = RSpec.describe group.before(:all) { raise "error in before all" } example = group.example("equality") { expect(1).to eq(2) } expect(group.run).to be_falsey @@ -847,7 +899,7 @@ def define_and_run_group(define_outer_example = false) it "exposes instance variables set in before(:all) from after(:all) even if a before(:all) error occurs" do ivar_value_in_after_hook = nil - group = ExampleGroup.describe do + group = RSpec.describe do before(:all) do @an_ivar = :set_in_before_all raise "fail" @@ -864,7 +916,7 @@ def define_and_run_group(define_outer_example = false) it "treats an error in before(:all) as a failure for a spec in a nested group" do example = nil - group = ExampleGroup.describe do + group = RSpec.describe do before(:all) { raise "error in before all" } describe "nested" do @@ -887,7 +939,7 @@ def define_and_run_group(define_outer_example = false) end let(:group) do - ExampleGroup.describe do + RSpec.describe do after(:all) { hooks_run << :one; raise "An error in an after(:all) hook" } after(:all) { hooks_run << :two; raise "A different hook raising an error" } it("equality") { expect(1).to eq(1) } @@ -895,41 +947,41 @@ def define_and_run_group(define_outer_example = false) end it "allows the example to pass" do - group.run - example = group.examples.first + self.group.run + example = self.group.examples.first expect(example.execution_result.status).to eq(:passed) end it "rescues any error(s) and prints them out" do expect(RSpec.configuration.reporter).to receive(:message).with(/An error in an after\(:all\) hook/) expect(RSpec.configuration.reporter).to receive(:message).with(/A different hook raising an error/) - group.run + self.group.run end it "still runs both after blocks" do - group.run + self.group.run expect(hooks_run).to eq [:two,:one] end end end describe ".pending" do - let(:group) { ExampleGroup.describe { pending { fail } } } + let(:group) { RSpec.describe { pending { fail } } } it "generates a pending example" do - group.run - expect(group.examples.first).to be_pending + self.group.run + expect(self.group.examples.first).to be_pending end it "sets the pending message" do - group.run - expect(group.examples.first.execution_result.pending_message).to eq(RSpec::Core::Pending::NO_REASON_GIVEN) + self.group.run + expect(self.group.examples.first.execution_result.pending_message).to eq(RSpec::Core::Pending::NO_REASON_GIVEN) end it 'sets the backtrace to the example definition so it can be located by the user' do file = RSpec::Core::Metadata.relative_path(__FILE__) expected = [file, __LINE__ + 2].map(&:to_s) - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do pending { } end group.run @@ -947,97 +999,97 @@ def define_and_run_group(define_outer_example = false) end describe "pending with metadata" do - let(:group) { ExampleGroup.describe { + let(:group) { RSpec.describe { example("unimplemented", :pending => true) { fail } } } it "generates a pending example" do - group.run - expect(group.examples.first).to be_pending + self.group.run + expect(self.group.examples.first).to be_pending end it "sets the pending message" do - group.run - expect(group.examples.first.execution_result.pending_message).to eq(RSpec::Core::Pending::NO_REASON_GIVEN) + self.group.run + expect(self.group.examples.first.execution_result.pending_message).to eq(RSpec::Core::Pending::NO_REASON_GIVEN) end end describe "pending with message in metadata" do - let(:group) { ExampleGroup.describe { + let(:group) { RSpec.describe { example("unimplemented", :pending => 'not done') { fail } } } it "generates a pending example" do - group.run - expect(group.examples.first).to be_pending + self.group.run + expect(self.group.examples.first).to be_pending end it "sets the pending message" do - group.run - expect(group.examples.first.execution_result.pending_message).to eq("not done") + self.group.run + expect(self.group.examples.first.execution_result.pending_message).to eq("not done") end end describe ".skip" do - let(:group) { ExampleGroup.describe { skip("skip this") { } } } + let(:group) { RSpec.describe { skip("skip this") { } } } it "generates a skipped example" do - group.run - expect(group.examples.first).to be_skipped + self.group.run + expect(self.group.examples.first).to be_skipped end it "sets the pending message" do - group.run - expect(group.examples.first.execution_result.pending_message).to eq(RSpec::Core::Pending::NO_REASON_GIVEN) + self.group.run + expect(self.group.examples.first.execution_result.pending_message).to eq(RSpec::Core::Pending::NO_REASON_GIVEN) end end describe "skip with metadata" do - let(:group) { ExampleGroup.describe { + let(:group) { RSpec.describe { example("skip this", :skip => true) { } } } it "generates a skipped example" do - group.run - expect(group.examples.first).to be_skipped + self.group.run + expect(self.group.examples.first).to be_skipped end it "sets the pending message" do - group.run - expect(group.examples.first.execution_result.pending_message).to eq(RSpec::Core::Pending::NO_REASON_GIVEN) + self.group.run + expect(self.group.examples.first.execution_result.pending_message).to eq(RSpec::Core::Pending::NO_REASON_GIVEN) end end describe "skip with message in metadata" do - let(:group) { ExampleGroup.describe { + let(:group) { RSpec.describe { example("skip this", :skip => 'not done') { } } } it "generates a skipped example" do - group.run - expect(group.examples.first).to be_skipped + self.group.run + expect(self.group.examples.first).to be_skipped end it "sets the pending message" do - group.run - expect(group.examples.first.execution_result.pending_message).to eq('not done') + self.group.run + expect(self.group.examples.first.execution_result.pending_message).to eq('not done') end end %w[xit xspecify xexample].each do |method_name| describe ".#{method_name}" do - let(:group) { ExampleGroup.describe.tap {|x| + let(:group) { RSpec.describe.tap {|x| x.send(method_name, "is pending") { } }} it "generates a skipped example" do - group.run - expect(group.examples.first).to be_skipped + self.group.run + expect(self.group.examples.first).to be_skipped end it "sets the pending message" do - group.run - expect(group.examples.first.execution_result.pending_message).to eq("Temporarily skipped with #{method_name}") + self.group.run + expect(self.group.examples.first.execution_result.pending_message).to eq("Temporarily skipped with #{method_name}") end end end @@ -1077,7 +1129,7 @@ def executed_examples_of(group) it "generates an example group that can be filtered with :focus" do RSpec.configuration.filter_run :focus - parent_group = ExampleGroup.describe do + parent_group = RSpec.describe do describe "not focused" do example("not focused example") { } end @@ -1103,7 +1155,7 @@ def extract_execution_results(group) end it 'marks every example as pending' do - group = ExampleGroup.describe("group", :pending => true) do + group = RSpec.describe("group", :pending => true) do it("passes") { } it("fails", :pending => 'unimplemented') { fail } end @@ -1125,13 +1177,13 @@ def extract_execution_results(group) describe "adding examples" do it "allows adding an example using 'it'" do - group = ExampleGroup.describe + group = RSpec.describe group.it("should do something") { } expect(group.examples.size).to eq(1) end it "exposes all examples at examples" do - group = ExampleGroup.describe + group = RSpec.describe group.it("should do something 1") { } group.it("should do something 2") { } group.it("should do something 3") { } @@ -1139,7 +1191,7 @@ def extract_execution_results(group) end it "maintains the example order" do - group = ExampleGroup.describe + group = RSpec.describe group.it("should 1") { } group.it("should 2") { } group.it("should 3") { } @@ -1177,7 +1229,7 @@ def extract_execution_results(group) let(:reporter) { double("reporter").as_null_object } it "returns true if all examples pass" do - group = ExampleGroup.describe('group') do + group = RSpec.describe('group') do example('ex 1') { expect(1).to eq(1) } example('ex 2') { expect(1).to eq(1) } end @@ -1186,7 +1238,7 @@ def extract_execution_results(group) end it "returns false if any of the examples fail" do - group = ExampleGroup.describe('group') do + group = RSpec.describe('group') do example('ex 1') { expect(1).to eq(1) } example('ex 2') { expect(1).to eq(2) } end @@ -1195,7 +1247,7 @@ def extract_execution_results(group) end it "runs all examples, regardless of any of them failing" do - group = ExampleGroup.describe('group') do + group = RSpec.describe('group') do example('ex 1') { expect(1).to eq(2) } example('ex 2') { expect(1).to eq(1) } end @@ -1270,7 +1322,7 @@ def extract_execution_results(group) describe "#top_level_description" do it "returns the description from the outermost example group" do group = nil - ExampleGroup.describe("top") do + RSpec.describe("top") do context "middle" do group = describe "bottom" do end @@ -1286,32 +1338,32 @@ def extract_execution_results(group) context "with fail_fast? => true" do let(:group) do - group = RSpec::Core::ExampleGroup.describe + group = RSpec.describe allow(group).to receive(:fail_fast?) { true } group end it "does not run examples after the failed example" do examples_run = [] - group.example('example 1') { examples_run << self } - group.example('example 2') { examples_run << self; fail; } - group.example('example 3') { examples_run << self } + self.group.example('example 1') { examples_run << self } + self.group.example('example 2') { examples_run << self; fail; } + self.group.example('example 3') { examples_run << self } - group.run + self.group.run expect(examples_run.length).to eq(2) end it "sets RSpec.world.wants_to_quit flag if encountering an exception in before(:all)" do - group.before(:all) { raise "error in before all" } - group.example("equality") { expect(1).to eq(2) } - expect(group.run).to be_falsey + self.group.before(:all) { raise "error in before all" } + self.group.example("equality") { expect(1).to eq(2) } + expect(self.group.run).to be_falsey expect(RSpec.world.wants_to_quit).to be_truthy end end context "with RSpec.world.wants_to_quit=true" do - let(:group) { RSpec::Core::ExampleGroup.describe } + let(:group) { RSpec.describe } before do allow(RSpec.world).to receive(:wants_to_quit) { true } @@ -1320,19 +1372,19 @@ def extract_execution_results(group) it "returns without starting the group" do expect(reporter).not_to receive(:example_group_started) - group.run(reporter) + self.group.run(reporter) end context "at top level" do it "purges remaining groups" do expect(RSpec.world).to receive(:clear_remaining_example_groups) - group.run(reporter) + self.group.run(reporter) end end context "in a nested group" do it "does not purge remaining groups" do - nested_group = group.describe + nested_group = self.group.describe expect(RSpec.world).not_to receive(:clear_remaining_example_groups) nested_group.run(reporter) end @@ -1341,7 +1393,7 @@ def extract_execution_results(group) context "with all examples passing" do it "returns true" do - group = RSpec::Core::ExampleGroup.describe("something") do + group = RSpec.describe("something") do it "does something" do # pass end @@ -1358,7 +1410,7 @@ def extract_execution_results(group) context "with top level example failing" do it "returns false" do - group = RSpec::Core::ExampleGroup.describe("something") do + group = RSpec.describe("something") do it "does something (wrong - fail)" do raise "fail" end @@ -1375,7 +1427,7 @@ def extract_execution_results(group) context "with nested example failing" do it "returns true" do - group = RSpec::Core::ExampleGroup.describe("something") do + group = RSpec.describe("something") do it "does something" do # pass end @@ -1393,27 +1445,41 @@ def extract_execution_results(group) %w[include_examples include_context].each do |name| describe "##{name}" do - let(:group) { ExampleGroup.describe } + let(:group) { RSpec.describe } before do - group.shared_examples "named this" do + self.group.shared_examples "named this" do example("does something") {} end end it "includes the named examples" do - group.send(name, "named this") - expect(group.examples.first.description).to eq("does something") + self.group.send(name, "named this") + expect(self.group.examples.first.description).to eq("does something") end it "raises a helpful error message when shared content is not found" do expect do - group.send(name, "shared stuff") + self.group.send(name, "shared stuff") end.to raise_error(ArgumentError, /Could not find .* "shared stuff"/) end + it "leaves RSpec's thread metadata unchanged" do + expect { + self.group.send(name, "named this") + }.to avoid_changing(RSpec, :thread_local_metadata) + end + + it "leaves RSpec's thread metadata unchanged, even when an error occurs during evaluation" do + expect { + self.group.send(name, "named this") do + raise "boom" + end + }.to raise_error("boom").and avoid_changing(RSpec, :thread_local_metadata) + end + it "passes parameters to the shared content" do passed_params = {} - group = ExampleGroup.describe + group = RSpec.describe group.shared_examples "named this with params" do |param1, param2| it("has access to the given parameters") do @@ -1429,7 +1495,7 @@ def extract_execution_results(group) end it "adds shared instance methods to the group" do - group = ExampleGroup.describe('fake group') + group = RSpec.describe('fake group') group.shared_examples "named this with params" do |param1| def foo; end end @@ -1439,7 +1505,7 @@ def foo; end it "evals the shared example group only once" do eval_count = 0 - group = ExampleGroup.describe('fake group') + group = RSpec.describe('fake group') group.shared_examples("named this with params") { |p| eval_count += 1 } group.send(name, "named this with params", :a) expect(eval_count).to eq(1) @@ -1447,7 +1513,7 @@ def foo; end it "evals the block when given" do key = "#{__FILE__}:#{__LINE__}" - group = ExampleGroup.describe do + group = RSpec.describe do shared_examples(key) do it("does something") do expect(foo).to eq("bar") @@ -1465,7 +1531,7 @@ def foo; "bar"; end describe "#it_should_behave_like" do it "creates a nested group" do - group = ExampleGroup.describe('fake group') + group = RSpec.describe('fake group') group.shared_examples_for("thing") {} group.it_should_behave_like("thing") expect(group.children.count).to eq(1) @@ -1473,14 +1539,14 @@ def foo; "bar"; end it "creates a nested group for a class" do klass = Class.new - group = ExampleGroup.describe('fake group') + group = RSpec.describe('fake group') group.shared_examples_for(klass) {} group.it_should_behave_like(klass) expect(group.children.count).to eq(1) end it "adds shared examples to nested group" do - group = ExampleGroup.describe('fake group') + group = RSpec.describe('fake group') group.shared_examples_for("thing") do it("does something") end @@ -1489,7 +1555,7 @@ def foo; "bar"; end end it "adds shared instance methods to nested group" do - group = ExampleGroup.describe('fake group') + group = RSpec.describe('fake group') group.shared_examples_for("thing") do def foo; end end @@ -1498,7 +1564,7 @@ def foo; end end it "adds shared class methods to nested group" do - group = ExampleGroup.describe('fake group') + group = RSpec.describe('fake group') group.shared_examples_for("thing") do def self.foo; end end @@ -1509,7 +1575,7 @@ def self.foo; end it "passes parameters to the shared example group" do passed_params = {} - group = ExampleGroup.describe("group") do + group = RSpec.describe("group") do shared_examples_for("thing") do |param1, param2| it("has access to the given parameters") do passed_params[:param1] = param1 @@ -1526,7 +1592,7 @@ def self.foo; end end it "adds shared instance methods to nested group" do - group = ExampleGroup.describe('fake group') + group = RSpec.describe('fake group') group.shared_examples_for("thing") do |param1| def foo; end end @@ -1536,7 +1602,7 @@ def foo; end it "evals the shared example group only once" do eval_count = 0 - group = ExampleGroup.describe('fake group') + group = RSpec.describe('fake group') group.shared_examples_for("thing") { |p| eval_count += 1 } group.it_should_behave_like("thing", :a) expect(eval_count).to eq(1) @@ -1545,7 +1611,7 @@ def foo; end context "given a block" do it "evaluates the block in nested group" do scopes = [] - group = ExampleGroup.describe("group") do + group = RSpec.describe("group") do shared_examples_for("thing") do it("gets run in the nested group") do scopes << self.class @@ -1565,18 +1631,39 @@ def foo; end it "raises a helpful error message when shared context is not found" do expect do - ExampleGroup.describe do + RSpec.describe do it_should_behave_like "shared stuff" end end.to raise_error(ArgumentError,%q|Could not find shared examples "shared stuff"|) end + + it "leaves RSpec's thread metadata unchanged" do + expect { + RSpec.describe do + shared_examples_for("stuff") { } + it_should_behave_like "stuff" + end + }.to avoid_changing(RSpec, :thread_local_metadata) + end + + it "leaves RSpec's thread metadata unchanged, even when an error occurs during evaluation" do + expect { + RSpec.describe do + shared_examples_for("stuff") { } + it_should_behave_like "stuff" do + raise "boom" + end + end + }.to raise_error("boom").and avoid_changing(RSpec, :thread_local_metadata) + end end it 'minimizes the number of methods that users could inadvertantly overwrite' do rspec_core_methods = ExampleGroup.instance_methods - RSpec::Matchers.instance_methods - RSpec::Mocks::ExampleMethods.instance_methods - - Object.instance_methods + Object.instance_methods - + ["singleton_class"] # Feel free to expand this list if you intend to add another public API # for users. RSpec internals should not add methods here, though. @@ -1599,10 +1686,102 @@ def foo; end it 'prevents defining nested isolated shared contexts' do expect { - ExampleGroup.describe do + RSpec.describe do ExampleGroup.shared_examples("common functionality") {} end }.to raise_error(/not allowed/) end + + describe 'inspect output', :unless => RUBY_VERSION == '1.9.2' do + context 'when there is no inspect output provided' do + it "uses '(no description provided)' instead" do + expect(ExampleGroup.new.inspect).to eq('#') + end + end + + context 'when an example has a description' do + it 'includes description and location' do + an_example = nil + + line = __LINE__ + 2 + group = RSpec.describe 'SomeClass1' do + example 'an example' do + an_example = self + end + end + + group.run + + path = RSpec::Core::Metadata.relative_path(__FILE__) + expect(an_example.inspect).to eq("#") + end + end + + context 'when an example does not have a description' do + it 'includes fallback description' do + an_example = nil + + line = __LINE__ + 2 + group = RSpec.describe 'SomeClass2' do + example do + an_example = self + end + end + + group.run + + path = RSpec::Core::Metadata.relative_path(__FILE__) + expect(an_example.inspect).to eq("#") + end + end + + it 'handles before context hooks' do + a_before_hook = nil + + group = RSpec.describe 'SomeClass3' do + before(:context) do + a_before_hook = self + end + + example {} + end + + group.run + expect(a_before_hook.inspect).to eq("#") + end + + it 'handles after context hooks' do + an_after_hook = nil + + group = RSpec.describe 'SomeClass4' do + after(:context) do + an_after_hook = self + end + + example {} + end + + group.run + expect(an_after_hook.inspect).to eq("#") + end + + it "does not pollute an example's `inspect` output with the inspect ivar from `before(:context)`" do + inspect_output = nil + + line = __LINE__ + 2 + group = RSpec.describe do + example do + inspect_output = inspect + end + + before(:context) {} + end + + group.run + + path = RSpec::Core::Metadata.relative_path(__FILE__) + expect(inspect_output).to end_with("\"example at #{path}:#{line}\">") + end + end end end diff --git a/spec/rspec/core/example_spec.rb b/spec/rspec/core/example_spec.rb index b6e6e00fb7..7fa62cba8a 100644 --- a/spec/rspec/core/example_spec.rb +++ b/spec/rspec/core/example_spec.rb @@ -1,11 +1,10 @@ -require 'spec_helper' require 'pp' require 'stringio' RSpec.describe RSpec::Core::Example, :parent_metadata => 'sample' do let(:example_group) do - RSpec::Core::ExampleGroup.describe('group description') + RSpec.describe('group description') end let(:example_instance) do @@ -53,8 +52,6 @@ def metadata_hash(*args) describe "when there is no explicit description" do def expect_with(*frameworks) - RSpec.configuration.expecting_with_rspec = frameworks.include?(:rspec) - if frameworks.include?(:stdlib) example_group.class_exec do def assert(val) @@ -115,6 +112,55 @@ def assert(val) expect(example.description).to match(/example at #{relative_path(__FILE__)}:#{__LINE__ - 2}/) end end + + context "when an `after(:example)` hook raises an error" do + it 'still assigns the description' do + ex = nil + + RSpec.describe do + ex = example { expect(2).to eq(2) } + after { raise "boom" } + end.run + + expect(ex.description).to eq("should eq 2") + end + end + + context "when the matcher's `description` method raises an error" do + description_line = __LINE__ + 3 + RSpec::Matchers.define :matcher_with_failing_description do + match { true } + description { raise ArgumentError, "boom" } + end + + it 'allows the example to pass and surfaces the failing description in the example description' do + ex = nil + + RSpec.describe do + ex = example { expect(2).to matcher_with_failing_description } + end.run + + expect(ex).to pass.and have_attributes(:description => a_string_including( + "example at #{ex.location}", + "ArgumentError", + "boom", + "#{__FILE__}:#{description_line}" + )) + end + end + + context "when an `after(:example)` hook has an expectation" do + it "assigns the description based on the example's last expectation, ignoring the `after` expectation since it can apply to many examples" do + ex = nil + + RSpec.describe do + ex = example { expect(nil).to be_nil } + after { expect(true).to eq(true) } + end.run + + expect(ex).to pass.and have_attributes(:description => "should be nil") + end + end end context "when `expect_with :rspec, :stdlib` is configured" do @@ -154,10 +200,6 @@ def assert(val) example_group.run expect(example.description).to match(/example at #{relative_path(__FILE__)}:#{__LINE__ - 2}/) end - - # Needed since `expecting_with_rspec?` in this context returns false - # so it won't automatically clear it for us. - after { RSpec::Matchers.clear_generated_description } end end @@ -186,7 +228,7 @@ def assert(val) describe "#run" do it "sets its reference to the example group instance to nil" do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do example('example') { expect(1).to eq(1) } end group.run @@ -194,7 +236,7 @@ def assert(val) end it "generates a description before tearing down mocks in case a mock object is used in the description" do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do example { test = double('Test'); expect(test).to eq test } end @@ -206,7 +248,7 @@ def assert(val) it "runs after(:each) when the example passes" do after_run = false - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do after(:each) { after_run = true } example('example') { expect(1).to eq(1) } end @@ -216,7 +258,7 @@ def assert(val) it "runs after(:each) when the example fails" do after_run = false - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do after(:each) { after_run = true } example('example') { expect(1).to eq(2) } end @@ -226,7 +268,7 @@ def assert(val) it "runs after(:each) when the example raises an Exception" do after_run = false - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do after(:each) { after_run = true } example('example') { raise "this error" } end @@ -237,7 +279,7 @@ def assert(val) context "with an after(:each) that raises" do it "runs subsequent after(:each)'s" do after_run = false - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do after(:each) { after_run = true } after(:each) { raise "FOO" } example('example') { expect(1).to eq(1) } @@ -247,7 +289,7 @@ def assert(val) end it "stores the exception" do - group = RSpec::Core::ExampleGroup.describe + group = RSpec.describe group.after(:each) { raise "FOO" } example = group.example('example') { expect(1).to eq(1) } @@ -259,7 +301,7 @@ def assert(val) it "wraps before/after(:each) inside around" do results = [] - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do around(:each) do |e| results << "around (before)" e.run @@ -282,7 +324,7 @@ def assert(val) context "clearing ivars" do it "sets ivars to nil to prep them for GC" do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do before(:all) { @before_all = :before_all } before(:each) { @before_each = :before_each } after(:each) { @after_each = :after_each } @@ -301,7 +343,7 @@ def assert(val) end it "does not impact the before_all_ivars which are copied to each example" do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do before(:all) { @before_all = "abc" } example("first") { expect(@before_all).not_to be_nil } example("second") { expect(@before_all).not_to be_nil } @@ -322,7 +364,7 @@ def run_and_capture_reported_message(group) end it "prints any around hook errors rather than silencing them" do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do around(:each) { |e| e.run; raise "around" } example("e") { raise "example" } end @@ -332,7 +374,7 @@ def run_and_capture_reported_message(group) end it "prints any after hook errors rather than silencing them" do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do after(:each) { raise "after" } example("e") { raise "example" } end @@ -342,7 +384,7 @@ def run_and_capture_reported_message(group) end it "does not print mock expectation errors" do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do example do foo = double expect(foo).to receive(:bar) @@ -361,7 +403,7 @@ def run_and_capture_reported_message(group) exception = StandardError.new exception.set_backtrace([]) - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do example { raise exception.freeze } end group.run @@ -385,7 +427,7 @@ def run_and_capture_reported_message(group) c.around(:each) { |ex| executed << :around_each_config; ex.run } end - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do before(:all) { executed << :before_all } before(:each) { executed << :before_each } after(:all) { executed << :after_all } @@ -420,7 +462,7 @@ def expect_pending_result(example) context "in the example" do it "sets the example to pending" do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do example { pending; fail } end group.run @@ -429,7 +471,7 @@ def expect_pending_result(example) it "allows post-example processing in around hooks (see https://github.com/rspec/rspec-core/issues/322)" do blah = nil - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do around do |example| example.run blah = :success @@ -443,7 +485,7 @@ def expect_pending_result(example) it 'sets the backtrace to the example definition so it can be located by the user' do file = RSpec::Core::Metadata.relative_path(__FILE__) expected = [file, __LINE__ + 2].map(&:to_s) - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do example { pending } @@ -457,7 +499,7 @@ def expect_pending_result(example) context "in before(:each)" do it "sets each example to pending" do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do before(:each) { pending } example { fail } example { fail } @@ -468,7 +510,7 @@ def expect_pending_result(example) end it 'sets example to pending when failure occurs in before(:each)' do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do before(:each) { pending; fail } example {} end @@ -479,7 +521,7 @@ def expect_pending_result(example) context "in before(:all)" do it "is forbidden" do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do before(:all) { pending } example { fail } example { fail } @@ -491,7 +533,7 @@ def expect_pending_result(example) end it "fails with an ArgumentError if a block is provided" do - group = RSpec::Core::ExampleGroup.describe('group') do + group = RSpec.describe('group') do before(:all) do pending { :no_op } end @@ -508,7 +550,7 @@ def expect_pending_result(example) context "in around(:each)" do it "sets the example to pending" do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do around(:each) { pending } example { fail } end @@ -517,7 +559,7 @@ def expect_pending_result(example) end it 'sets example to pending when failure occurs in around(:each)' do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do around(:each) { pending; fail } example {} end @@ -528,7 +570,7 @@ def expect_pending_result(example) context "in after(:each)" do it "sets each example to pending" do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do after(:each) { pending; fail } example { } example { } @@ -544,7 +586,7 @@ def expect_pending_result(example) describe "#skip" do context "in the example" do it "sets the example to skipped" do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do example { skip } end group.run @@ -553,7 +595,7 @@ def expect_pending_result(example) it "allows post-example processing in around hooks (see https://github.com/rspec/rspec-core/issues/322)" do blah = nil - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do around do |example| example.run blah = :success @@ -566,7 +608,7 @@ def expect_pending_result(example) context "with a message" do it "sets the example to skipped with the provided message" do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do example { skip "lorem ipsum" } end group.run @@ -577,7 +619,7 @@ def expect_pending_result(example) context "in before(:each)" do it "sets each example to skipped" do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do before(:each) { skip } example {} example {} @@ -589,8 +631,8 @@ def expect_pending_result(example) end context "in before(:all)" do - it "sets each example to pending" do - group = RSpec::Core::ExampleGroup.describe do + it "sets each example to skipped" do + group = RSpec.describe do before(:all) { skip("not done"); fail } example {} example {} @@ -603,7 +645,7 @@ def expect_pending_result(example) context "in around(:each)" do it "sets the example to skipped" do - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do around(:each) { skip } example {} end @@ -616,7 +658,7 @@ def expect_pending_result(example) describe "timing" do it "uses RSpec::Core::Time as to not be affected by changes to time in examples" do reporter = double(:reporter).as_null_object - group = RSpec::Core::ExampleGroup.describe + group = RSpec.describe example = group.example example.__send__ :start, reporter allow(Time).to receive_messages(:now => Time.utc(2012, 10, 1)) @@ -630,7 +672,7 @@ def expect_pending_result(example) RSpec.configuration.order = :random - RSpec::Core::ExampleGroup.describe do + RSpec.describe do # The bug was only triggered when the examples # were in nested contexts; see https://github.com/rspec/rspec-core/pull/837 context { example { values << rand } } @@ -649,7 +691,7 @@ def expect_pending_result(example) describe "setting the current example" do it "sets RSpec.current_example to the example that is currently running" do - group = RSpec::Core::ExampleGroup.describe("an example group") + group = RSpec.describe("an example group") current_examples = [] example1 = group.example("example 1") { current_examples << RSpec.current_example } @@ -659,4 +701,35 @@ def expect_pending_result(example) expect(current_examples).to eq([example1, example2]) end end + + describe "mock framework integration" do + it 'verifies mock expectations after each example' do + ex = nil + + RSpec.describe do + let(:dbl) { double } + ex = example do + expect(dbl).to receive(:foo) + end + end.run + + expect(ex).to fail_with(RSpec::Mocks::MockExpectationError) + end + + it 'allows `after(:example)` hooks to satisfy mock expectations, since examples are not complete until their `after` hooks run' do + ex = nil + + RSpec.describe do + let(:dbl) { double } + + ex = example do + expect(dbl).to receive(:foo) + end + + after { dbl.foo } + end.run + + expect(ex).to pass + end + end end diff --git a/spec/rspec/core/failed_example_notification_spec.rb b/spec/rspec/core/failed_example_notification_spec.rb index cf99646b3a..846b658f9d 100644 --- a/spec/rspec/core/failed_example_notification_spec.rb +++ b/spec/rspec/core/failed_example_notification_spec.rb @@ -1,14 +1,12 @@ -require "spec_helper" - module RSpec::Core::Notifications - describe FailedExampleNotification do + RSpec.describe FailedExampleNotification do before do allow(RSpec.configuration).to receive(:color_enabled?).and_return(true) end it "uses the default color for the shared example backtrace line" do example = nil - group = RSpec::Core::ExampleGroup.describe "testing" do + group = RSpec.describe "testing" do shared_examples_for "a" do example = it "fails" do expect(1).to eq(2) diff --git a/spec/rspec/core/filter_manager_spec.rb b/spec/rspec/core/filter_manager_spec.rb index a421bf8b27..676a6ba3f4 100644 --- a/spec/rspec/core/filter_manager_spec.rb +++ b/spec/rspec/core/filter_manager_spec.rb @@ -1,5 +1,3 @@ -require 'spec_helper' - module RSpec::Core RSpec.describe FilterManager do def opposite(name) @@ -36,25 +34,23 @@ def opposite(name) end if name == "include" - [:locations, :full_description].each do |filter| - context "with :#{filter}" do - it "clears previous inclusions" do - filter_manager.include :foo => :bar - filter_manager.include filter => "value" - expect(rules).to eq({filter => "value"}) - end - - it "clears previous exclusion" do - filter_manager.include :foo => :bar - filter_manager.include filter => "value" - expect(opposite_rules).to be_empty - end - - it "does nothing when :#{filter} previously set" do - filter_manager.include filter => "a_value" - filter_manager.include :foo => :bar - expect(rules).to eq(filter => "a_value") - end + context "with :full_description" do + it "clears previous inclusions" do + filter_manager.include :foo => :bar + filter_manager.include :full_description => "value" + expect(rules).to eq(:full_description => "value") + end + + it "clears previous exclusion" do + filter_manager.include :foo => :bar + filter_manager.include :full_description => "value" + expect(opposite_rules).to be_empty + end + + it "does nothing when :full_description previously set" do + filter_manager.include :full_description => "a_value" + filter_manager.include :foo => :bar + expect(rules).to eq(:full_description => "a_value") end end end @@ -129,6 +125,25 @@ def example_with(*args) expect(filter_manager.prune([included, excluded])).to eq([included]) end + context "with examples from multiple spec source files" do + it "applies exclusions only to examples defined in files with no location filters" do + group = RSpec.describe("group") + line = __LINE__ + 1 + this_file_example = group.example("ex 1", :slow) { } + + # Using eval in order to make ruby think this got defined in another file. + other_file_example = instance_eval "ex = nil; RSpec.describe('group') { ex = it('ex 2', :slow) { } }; ex", "some/external/file.rb", 1 + + filter_manager.exclude_with_low_priority :slow => true + + expect { + filter_manager.add_location(__FILE__, [line]) + }.to change { + filter_manager.prune([this_file_example, other_file_example]).map(&:description) + }.from([]).to([this_file_example.description]) + end + end + it "prefers description to exclusion filter" do group = RSpec.describe("group") included = group.example("include", :slow => true) {} @@ -183,6 +198,19 @@ def example_with(*args) filter_manager.include_with_low_priority :foo => :bar expect(filter_manager.prune([included, excluded])).to eq([included]) end + + context "with multiple inclusion filters" do + it 'includes objects that match any of them' do + examples = [ + included_1 = example_with(:foo => true), + included_2 = example_with(:bar => true), + example_with(:bazz => true) + ] + + filter_manager.include :foo => true, :bar => true + expect(filter_manager.prune(examples)).to contain_exactly(included_1, included_2) + end + end end describe "#inclusions#description" do @@ -249,49 +277,53 @@ def example_with_metadata(metadata) value end + def exclude?(example) + filter_manager.prune([example]).empty? + end + describe "the default :if filter" do it "does not exclude a spec with { :if => true } metadata" do example = example_with_metadata(:if => true) - expect(filter_manager.exclude?(example)).to be_falsey + expect(exclude?(example)).to be_falsey end it "excludes a spec with { :if => false } metadata" do example = example_with_metadata(:if => false) - expect(filter_manager.exclude?(example)).to be_truthy + expect(exclude?(example)).to be_truthy end it "excludes a spec with { :if => nil } metadata" do example = example_with_metadata(:if => nil) - expect(filter_manager.exclude?(example)).to be_truthy + expect(exclude?(example)).to be_truthy end it "continues to be an exclusion even if exclusions are cleared" do example = example_with_metadata(:if => false) filter_manager.exclusions.clear - expect(filter_manager.exclude?(example)).to be_truthy + expect(exclude?(example)).to be_truthy end end describe "the default :unless filter" do it "excludes a spec with { :unless => true } metadata" do example = example_with_metadata(:unless => true) - expect(filter_manager.exclude?(example)).to be_truthy + expect(exclude?(example)).to be_truthy end it "does not exclude a spec with { :unless => false } metadata" do example = example_with_metadata(:unless => false) - expect(filter_manager.exclude?(example)).to be_falsey + expect(exclude?(example)).to be_falsey end it "does not exclude a spec with { :unless => nil } metadata" do example = example_with_metadata(:unless => nil) - expect(filter_manager.exclude?(example)).to be_falsey + expect(exclude?(example)).to be_falsey end it "continues to be an exclusion even if exclusions are cleared" do example = example_with_metadata(:unless => true) filter_manager.exclusions.clear - expect(filter_manager.exclude?(example)).to be_truthy + expect(exclude?(example)).to be_truthy end end end diff --git a/spec/rspec/core/filterable_item_repository_spec.rb b/spec/rspec/core/filterable_item_repository_spec.rb new file mode 100644 index 0000000000..8171a43d1a --- /dev/null +++ b/spec/rspec/core/filterable_item_repository_spec.rb @@ -0,0 +1,202 @@ +module RSpec + module Core + RSpec.describe FilterableItemRepository, "#items_for" do + FilterableItem = Struct.new(:name) + + def self.it_behaves_like_a_filterable_item_repo(&when_the_repo_has_items_with_metadata) + let(:repo) { described_class.new(:any?) } + let(:item_1) { FilterableItem.new("Item 1") } + let(:item_2) { FilterableItem.new("Item 2") } + let(:item_3) { FilterableItem.new("Item 3") } + let(:item_4) { FilterableItem.new("Item 4") } + + context "when the repository is empty" do + it 'returns an empty list' do + expect(repo.items_for(:foo => "bar")).to eq([]) + end + end + + shared_examples_for "adding items to the repository" do |add_method| + describe "adding items using `#{add_method}`" do + define_method :add_item do |*args| + repo.__send__ add_method, *args + end + + context "when the repository has items that have no metadata" do + before do + add_item item_1, {} + add_item item_2, {} + end + + it "returns those items, regardless of the provided argument" do + expect(repo.items_for({})).to contain_exactly(item_1, item_2) + expect(repo.items_for(:foo => "bar")).to contain_exactly(item_1, item_2) + end + end + + context "when the repository has items that have metadata" do + before do + add_item item_1, :foo => "bar" + add_item item_2, :slow => true + add_item item_3, :foo => "bar" + end + + it 'return an empty list when given empty metadata' do + expect(repo.items_for({})).to eq([]) + end + + it 'return an empty list when given metadata that matches no items' do + expect(repo.items_for(:slow => false, :foo => "bazz")).to eq([]) + end + + it 'returns matching items for the provided metadata' do + expect(repo.items_for(:slow => true)).to contain_exactly(item_2) + expect(repo.items_for(:foo => "bar")).to contain_exactly(item_1, item_3) + expect(repo.items_for(:slow => true, :foo => "bar")).to contain_exactly(item_1, item_2, item_3) + end + + it 'returns the matching items in the correct order' do + expect(repo.items_for(:slow => true, :foo => "bar")).to eq items_in_expected_order + end + + it 'ignores other metadata keys that are not related to the appended items' do + expect(repo.items_for(:slow => true, :other => "foo")).to contain_exactly(item_2) + end + + it 'differentiates between an applicable key being missing and having an explicit `nil` value' do + add_item item_4, :bar => nil + + expect(repo.items_for({})).to eq([]) + expect(repo.items_for(:bar => nil)).to contain_exactly(item_4) + end + + it 'returns the correct items when they are appended after a memoized lookup' do + expect { + add_item item_4, :slow => true + }.to change { repo.items_for(:slow => true) }. + from(a_collection_containing_exactly(item_2)). + to(a_collection_containing_exactly(item_2, item_4)) + end + + let(:flip_proc) do + return_val = true + Proc.new { return_val.tap { |v| return_val = !v } } + end + + context "with proc values" do + before do + add_item item_4, { :include_it => flip_proc } + end + + it 'evaluates the proc each time since the logic can return a different value each time' do + expect(repo.items_for(:include_it => nil)).to contain_exactly(item_4) + expect(repo.items_for(:include_it => nil)).to eq([]) + expect(repo.items_for(:include_it => nil)).to contain_exactly(item_4) + expect(repo.items_for(:include_it => nil)).to eq([]) + end + end + + context "when initialized with the `:any?` predicate" do + let(:repo) { FilterableItemRepository::QueryOptimized.new(:any?) } + + it 'matches against multi-entry items when any of the metadata entries match' do + add_item item_4, :key_1 => "val_1", :key_2 => "val_2" + + expect(repo.items_for(:key_1 => "val_1")).to contain_exactly(item_4) + expect(repo.items_for(:key_2 => "val_2")).to contain_exactly(item_4) + expect(repo.items_for(:key_1 => "val_1", :key_2 => "val_2")).to contain_exactly(item_4) + end + end + + context "when initialized with the `:all?` predicate" do + let(:repo) { FilterableItemRepository::QueryOptimized.new(:all?) } + + it 'matches against multi-entry items when all of the metadata entries match' do + add_item item_4, :key_1 => "val_1", :key_2 => "val_2" + + expect(repo.items_for(:key_1 => "val_1")).to eq([]) + expect(repo.items_for(:key_2 => "val_2")).to eq([]) + expect(repo.items_for(:key_1 => "val_1", :key_2 => "val_2")).to contain_exactly(item_4) + end + end + + module_eval(&when_the_repo_has_items_with_metadata) if when_the_repo_has_items_with_metadata + end + end + end + + it_behaves_like "adding items to the repository", :append do + let(:items_in_expected_order) { [item_1, item_2, item_3] } + end + + it_behaves_like "adding items to the repository", :prepend do + let(:items_in_expected_order) { [item_3, item_2, item_1] } + end + end + + describe FilterableItemRepository::UpdateOptimized do + it_behaves_like_a_filterable_item_repo + end + + describe FilterableItemRepository::QueryOptimized do + it_behaves_like_a_filterable_item_repo do + describe "performance optimization" do + # NOTE: the specs in this context are potentially brittle because they are + # coupled to the implementation's usage of `MetadataFilter.apply?`. However, + # they demonstrate the perf optimization that was the reason we created + # this class, and thus have value in demonstrating the memoization is working + # properly and in documenting the reason the class exists in the first place. + # Still, if these prove to be brittle in the future, feel free to delete them since + # they are not concerned with externally visible behaviors. + + it 'is optimized to check metadata filter application for a given pair of metadata hashes only once' do + # TODO: use mock expectations for this once https://github.com/rspec/rspec-mocks/pull/841 is fixed. + call_counts = track_metadata_filter_apply_calls + + 3.times do + expect(repo.items_for(:slow => true, :other => "foo")).to contain_exactly(item_2) + end + + expect(call_counts[:slow => true]).to eq(1) + end + + it 'ignores extraneous metadata keys when doing memoized lookups' do + # TODO: use mock expectations for this once https://github.com/rspec/rspec-mocks/pull/841 is fixed. + call_counts = track_metadata_filter_apply_calls + + expect(repo.items_for(:slow => true, :other => "foo")).to contain_exactly(item_2) + expect(repo.items_for(:slow => true, :other => "bar")).to contain_exactly(item_2) + expect(repo.items_for(:slow => true, :goo => "bazz")).to contain_exactly(item_2) + + expect(call_counts[:slow => true]).to eq(1) + end + + context "when there are some proc keys" do + before do + add_item item_4, { :include_it => flip_proc } + end + + it 'still performs memoization for metadata hashes that lack those keys' do + call_counts = track_metadata_filter_apply_calls + + expect(repo.items_for(:slow => true, :other => "foo")).to contain_exactly(item_2) + expect(repo.items_for(:slow => true, :other => "foo")).to contain_exactly(item_2) + + expect(call_counts[:slow => true]).to eq(1) + end + end + + def track_metadata_filter_apply_calls + Hash.new(0).tap do |call_counts| + allow(MetadataFilter).to receive(:apply?).and_wrap_original do |original, predicate, item_meta, request_meta| + call_counts[item_meta] += 1 + original.call(predicate, item_meta, request_meta) + end + end + end + end + end + end + end + end +end diff --git a/spec/rspec/core/formatters/base_text_formatter_spec.rb b/spec/rspec/core/formatters/base_text_formatter_spec.rb index 16eb163f10..bd5d476137 100644 --- a/spec/rspec/core/formatters/base_text_formatter_spec.rb +++ b/spec/rspec/core/formatters/base_text_formatter_spec.rb @@ -1,5 +1,4 @@ # encoding: utf-8 -require 'spec_helper' require 'rspec/core/formatters/base_text_formatter' RSpec.describe RSpec::Core::Formatters::BaseTextFormatter do @@ -7,9 +6,9 @@ context "when closing the formatter", :isolated_directory => true do it 'does not close an already closed output stream' do - output = File.new("./output_to_close", "w") - formatter = described_class.new(output) - output.close + output_to_close = File.new("./output_to_close", "w") + formatter = described_class.new(output_to_close) + output_to_close.close expect { formatter.close(RSpec::Core::Notifications::NullNotification) }.not_to raise_error end @@ -32,19 +31,37 @@ end it "includes command to re-run each failed example" do - group = RSpec::Core::ExampleGroup.describe("example group") do + example_group = RSpec.describe("example group") do it("fails") { fail } end line = __LINE__ - 2 - group.run(reporter) - examples = group.examples + + expect(output_from_running example_group).to include("rspec #{RSpec::Core::Metadata::relative_path("#{__FILE__}:#{line}")} # example group fails") + end + + context "for an example defined in an file required by the user rather than loaded by rspec" do + it "looks through ancestor metadata to find a workable re-run command" do + line = __LINE__ + 1 + example_group = RSpec.describe("example group") do + # Using eval in order to make it think this got defined in an external file. + instance_eval "it('fails') { fail }", "some/external/file.rb", 1 + end + + expect(output_from_running example_group).to include("rspec #{RSpec::Core::Metadata::relative_path("#{__FILE__}:#{line}")} # example group fails") + end + end + + def output_from_running(example_group) + allow(RSpec.configuration).to receive(:loaded_spec_files) { [File.expand_path(__FILE__)].to_set } + example_group.run(reporter) + examples = example_group.examples send_notification :dump_summary, summary_notification(1, examples, examples, [], 0) - expect(output.string).to include("rspec #{RSpec::Core::Metadata::relative_path("#{__FILE__}:#{line}")} # example group fails") + output.string end end describe "#dump_failures" do - let(:group) { RSpec::Core::ExampleGroup.describe("group name") } + let(:group) { RSpec.describe("group name") } before { allow(RSpec.configuration).to receive(:color_enabled?) { false } } @@ -129,40 +146,71 @@ def run_all_and_dump_failures end end - context 'for #shared_examples' do - it 'outputs the name and location' do - group.shared_examples 'foo bar' do - it("example name") { expect("this").to eq("that") } - end - - line = __LINE__.next - group.it_should_behave_like('foo bar') - - run_all_and_dump_failures - - expect(output.string).to include( - 'Shared Example Group: "foo bar" called from ' + - "#{RSpec::Core::Metadata.relative_path(__FILE__)}:#{line}" - ) - end - - context 'that contains nested example groups' do + %w[ include_examples it_should_behave_like ].each do |inclusion_method| + context "for #shared_examples included using #{inclusion_method}" do it 'outputs the name and location' do group.shared_examples 'foo bar' do - describe 'nested group' do - it("example name") { expect("this").to eq("that") } - end + it("example name") { expect("this").to eq("that") } end line = __LINE__.next - group.it_should_behave_like('foo bar') + group.__send__(inclusion_method, 'foo bar') run_all_and_dump_failures - expect(output.string).to include( + expect(output.string.lines).to include(a_string_ending_with( 'Shared Example Group: "foo bar" called from ' + - "./spec/rspec/core/formatters/base_text_formatter_spec.rb:#{line}" - ) + "#{RSpec::Core::Metadata.relative_path(__FILE__)}:#{line}\n" + )) + end + + context 'that contains nested example groups' do + it 'outputs the name and location' do + group.shared_examples 'foo bar' do + describe 'nested group' do + it("example name") { expect("this").to eq("that") } + end + end + + line = __LINE__.next + group.__send__(inclusion_method, 'foo bar') + + run_all_and_dump_failures + + expect(output.string.lines).to include(a_string_ending_with( + 'Shared Example Group: "foo bar" called from ' + + "./spec/rspec/core/formatters/base_text_formatter_spec.rb:#{line}\n" + )) + end + end + + context "that contains shared group nesting" do + it 'includes each inclusion location in the output' do + group.shared_examples "inner" do + example { expect(1).to eq(2) } + end + + inner_line = __LINE__ + 2 + group.shared_examples "outer" do + __send__(inclusion_method, "inner") + end + + outer_line = __LINE__ + 1 + group.__send__(inclusion_method, 'outer') + + run_all_and_dump_failures + + expect(output.string.lines.grep(/Shared Example Group/)).to match [ + a_string_ending_with( + 'Shared Example Group: "inner" called from ' + + "./spec/rspec/core/formatters/base_text_formatter_spec.rb:#{inner_line}\n" + ), + a_string_ending_with( + 'Shared Example Group: "outer" called from ' + + "./spec/rspec/core/formatters/base_text_formatter_spec.rb:#{outer_line}\n" + ), + ] + end end end end diff --git a/spec/rspec/core/formatters/console_codes_spec.rb b/spec/rspec/core/formatters/console_codes_spec.rb index af5edf77eb..04a9c7d830 100644 --- a/spec/rspec/core/formatters/console_codes_spec.rb +++ b/spec/rspec/core/formatters/console_codes_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'rspec/core/formatters/console_codes' RSpec.describe "RSpec::Core::Formatters::ConsoleCodes" do diff --git a/spec/rspec/core/formatters/deprecation_formatter_spec.rb b/spec/rspec/core/formatters/deprecation_formatter_spec.rb index e1401b90c4..122eb510a0 100644 --- a/spec/rspec/core/formatters/deprecation_formatter_spec.rb +++ b/spec/rspec/core/formatters/deprecation_formatter_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'rspec/core/reporter' require 'rspec/core/formatters/deprecation_formatter' require 'tempfile' @@ -111,6 +110,12 @@ def notification(hash) expect(summary_stream.string).to match(/1 deprecation/) expect(File.read(deprecation_stream.path)).to eq("foo is deprecated.\n#{DeprecationFormatter::RAISE_ERROR_CONFIG_NOTICE}") end + + it "can handle when the stream is reopened to a system stream", :unless => RSpec::Support::OS.windows? do + send_notification :deprecation, notification(:deprecated => 'foo') + deprecation_stream.reopen(IO.for_fd(IO.sysopen('/dev/null', "w+"))) + send_notification :deprecation_summary, null_notification + end end context "with an Error deprecation_stream" do diff --git a/spec/rspec/core/formatters/documentation_formatter_spec.rb b/spec/rspec/core/formatters/documentation_formatter_spec.rb index ff23c886e2..43eac1beaa 100644 --- a/spec/rspec/core/formatters/documentation_formatter_spec.rb +++ b/spec/rspec/core/formatters/documentation_formatter_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'rspec/core/formatters/documentation_formatter' module RSpec::Core::Formatters @@ -19,11 +18,11 @@ def execution_result(values) it "numbers the failures" do send_notification :example_failed, example_notification( double("example 1", :description => "first example", - :execution_result => execution_result(:status => 'failed', :exception => Exception.new) + :execution_result => execution_result(:status => :failed, :exception => Exception.new) )) send_notification :example_failed, example_notification( double("example 2", :description => "second example", - :execution_result => execution_result(:status => 'failed', :exception => Exception.new) + :execution_result => execution_result(:status => :failed, :exception => Exception.new) )) expect(output.string).to match(/first example \(FAILED - 1\)/m) @@ -31,7 +30,7 @@ def execution_result(values) end it "represents nested group using hierarchy tree" do - group = RSpec::Core::ExampleGroup.describe("root") + group = RSpec.describe("root") context1 = group.describe("context 1") context1.example("nested example 1.1"){} context1.example("nested example 1.2"){} @@ -61,7 +60,7 @@ def execution_result(values) end it "strips whitespace for each row" do - group = RSpec::Core::ExampleGroup.describe(" root ") + group = RSpec.describe(" root ") context1 = group.describe(" nested ") context1.example(" example 1 ") {} context1.example(" example 2 ", :pending => true){ fail } diff --git a/spec/rspec/core/formatters/helpers_spec.rb b/spec/rspec/core/formatters/helpers_spec.rb index a49ee2fff6..6fe50854b0 100644 --- a/spec/rspec/core/formatters/helpers_spec.rb +++ b/spec/rspec/core/formatters/helpers_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'rspec/core/formatters/helpers' RSpec.describe RSpec::Core::Formatters::Helpers do diff --git a/spec/rspec/core/formatters/html_formatter_spec.rb b/spec/rspec/core/formatters/html_formatter_spec.rb index 2b177a9c2b..255c29eb88 100644 --- a/spec/rspec/core/formatters/html_formatter_spec.rb +++ b/spec/rspec/core/formatters/html_formatter_spec.rb @@ -1,12 +1,14 @@ # encoding: utf-8 -require 'spec_helper' require 'rspec/core/formatters/html_formatter' -require 'nokogiri' + +# For some reason we get load errors when loading nokogiri on AppVeyor +# on Ruby 2.1. On 1.9.3 it works just fine. No idea why. +require 'nokogiri' unless ENV['APPVEYOR'] && RUBY_VERSION.to_f >= 2.1 module RSpec module Core module Formatters - RSpec.describe HtmlFormatter do + RSpec.describe HtmlFormatter, :failing_on_appveyor => (RUBY_VERSION.to_f >= 2.1) do include FormatterSupport let(:root) { File.expand_path("#{File.dirname(__FILE__)}/../../../..") } diff --git a/spec/rspec/core/formatters/json_formatter_spec.rb b/spec/rspec/core/formatters/json_formatter_spec.rb index 3aeaa7d237..4e42215be1 100644 --- a/spec/rspec/core/formatters/json_formatter_spec.rb +++ b/spec/rspec/core/formatters/json_formatter_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'rspec/core/formatters/json_formatter' require 'json' require 'rspec/core/reporter' @@ -15,7 +14,7 @@ include FormatterSupport it "outputs json (brittle high level functional test)" do - group = RSpec::Core::ExampleGroup.describe("one apiece") do + group = RSpec.describe("one apiece") do it("succeeds") { expect(1).to eq 1 } it("fails") { fail "eek" } it("pends") { pending "world peace"; fail "eek" } @@ -128,7 +127,7 @@ def profile *groups context "with one example group" do before do - profile( RSpec::Core::ExampleGroup.describe("group") do + profile( RSpec.describe("group") do example("example") { } end) end @@ -154,13 +153,13 @@ def profile *groups before do example_clock = class_double(RSpec::Core::Time, :now => RSpec::Core::Time.now + 0.5) - group1 = RSpec::Core::ExampleGroup.describe("slow group") do + group1 = RSpec.describe("slow group") do example("example") do |example| # make it look slow without actually taking up precious time example.clock = example_clock end end - group2 = RSpec::Core::ExampleGroup.describe("fast group") do + group2 = RSpec.describe("fast group") do example("example 1") { } example("example 2") { } end diff --git a/spec/rspec/core/formatters/profile_formatter_spec.rb b/spec/rspec/core/formatters/profile_formatter_spec.rb index dd31448e12..ee53ce3b42 100644 --- a/spec/rspec/core/formatters/profile_formatter_spec.rb +++ b/spec/rspec/core/formatters/profile_formatter_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'rspec/core/formatters/profile_formatter' RSpec.describe RSpec::Core::Formatters::ProfileFormatter do @@ -41,7 +40,7 @@ def profile *groups before do example_clock = class_double(RSpec::Core::Time, :now => RSpec::Core::Time.now + 0.5) - profile(RSpec::Core::ExampleGroup.describe("group") do + profile(RSpec.describe("group") do example("example") do |example| # make it look slow without actually taking up precious time example.clock = example_clock @@ -57,14 +56,14 @@ def profile *groups before do example_clock = class_double(RSpec::Core::Time, :now => RSpec::Core::Time.now + 0.5) - group1 = RSpec::Core::ExampleGroup.describe("slow group") do + group1 = RSpec.describe("slow group") do example("example") do |example| # make it look slow without actually taking up precious time example.clock = example_clock end example_line_number = __LINE__ - 4 end - group2 = RSpec::Core::ExampleGroup.describe("fast group") do + group2 = RSpec.describe("fast group") do example("example 1") { } example("example 2") { } end @@ -86,7 +85,7 @@ def profile *groups it "depends on parent_groups to get the top level example group" do ex = nil - group = RSpec::Core::ExampleGroup.describe + group = RSpec.describe group.describe("group 2") do describe "group 3" do ex = example("nested example 1") diff --git a/spec/rspec/core/formatters/progress_formatter_spec.rb b/spec/rspec/core/formatters/progress_formatter_spec.rb index bf143aaad9..a92a9fdb0f 100644 --- a/spec/rspec/core/formatters/progress_formatter_spec.rb +++ b/spec/rspec/core/formatters/progress_formatter_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'rspec/core/formatters/progress_formatter' RSpec.describe RSpec::Core::Formatters::ProgressFormatter do diff --git a/spec/rspec/core/formatters/snippet_extractor_spec.rb b/spec/rspec/core/formatters/snippet_extractor_spec.rb index 341007bb2f..5227e2532f 100644 --- a/spec/rspec/core/formatters/snippet_extractor_spec.rb +++ b/spec/rspec/core/formatters/snippet_extractor_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'rspec/core/formatters/snippet_extractor' module RSpec diff --git a/spec/rspec/core/formatters_spec.rb b/spec/rspec/core/formatters_spec.rb index 57423e37b7..1cf61feaba 100644 --- a/spec/rspec/core/formatters_spec.rb +++ b/spec/rspec/core/formatters_spec.rb @@ -1,8 +1,7 @@ -require 'spec_helper' require 'pathname' module RSpec::Core::Formatters - describe Loader do + RSpec.describe Loader do let(:output) { StringIO.new } let(:reporter) { instance_double "Reporter", :register_listener => nil } diff --git a/spec/rspec/core/hooks_filtering_spec.rb b/spec/rspec/core/hooks_filtering_spec.rb index 9d2804cd87..2cf81960eb 100644 --- a/spec/rspec/core/hooks_filtering_spec.rb +++ b/spec/rspec/core/hooks_filtering_spec.rb @@ -1,5 +1,3 @@ -require "spec_helper" - module RSpec::Core RSpec.describe "config block hook filtering" do describe "unfiltered hooks" do @@ -12,7 +10,7 @@ module RSpec::Core c.after(:each) { filters << "after each in config"} c.after(:all) { filters << "after all in config"} end - group = ExampleGroup.describe + group = RSpec.describe group.example("example") {} group.run expect(filters).to eq([ @@ -35,7 +33,7 @@ module RSpec::Core c.before(:match => true) { filters << "before each in config"} c.after(:match => true) { filters << "after each in config"} end - group = ExampleGroup.describe("group", :match => true) + group = RSpec.describe("group", :match => true) group.example("example") {} group.run expect(filters).to eq([ @@ -55,7 +53,7 @@ module RSpec::Core c.after(:each, :match => true) { filters << "after each in config"} c.after(:all, :match => true) { filters << "after all in config"} end - group = ExampleGroup.describe("group", :match => true) + group = RSpec.describe("group", :match => true) group.example("example") {} group.run expect(filters).to eq([ @@ -76,7 +74,7 @@ module RSpec::Core example_1_filters = example_2_filters = nil - group = ExampleGroup.describe "group" do + group = RSpec.describe "group" do it("example 1") { example_1_filters = filters.dup } describe "subgroup", :match => true do it("example 2") { example_2_filters = filters.dup } @@ -98,7 +96,7 @@ module RSpec::Core example_1_filters = example_2_filters = example_3_filters = nil - group = ExampleGroup.describe "group", :match => true do + group = RSpec.describe "group", :match => true do it("example 1") { example_1_filters = filters.dup } describe "subgroup", :match => true do it("example 2") { example_2_filters = filters.dup } @@ -125,7 +123,7 @@ module RSpec::Core c.after(:each, :match => false) { filters << "after each in config"} c.after(:all, :match => false) { filters << "after all in config"} end - group = ExampleGroup.describe(:match => true) + group = RSpec.describe(:match => true) group.example("example") {} group.run expect(filters).to eq([]) @@ -135,9 +133,9 @@ module RSpec::Core let(:each_filters) { [] } let(:all_filters) { [] } - let(:group) do + let(:example_group) do md = example_metadata - ExampleGroup.describe do + RSpec.describe do it("example", md) { } end end @@ -157,7 +155,7 @@ def filters c.after(:all, :foo => :bar) { af << "after all in config"} end - group.run + example_group.run end describe 'an example with matching metadata' do @@ -170,17 +168,13 @@ def filters 'after each in config' ]) end - - it "does not run the `:all` hooks" do - expect(all_filters).to be_empty - end end describe 'an example without matching metadata' do let(:example_metadata) { { :foo => :bazz } } it "does not run any of the hooks" do - expect(filters).to be_empty + expect(self.filters).to be_empty end end end @@ -196,7 +190,7 @@ def filters c.after(:each, :one => 1, :two => 2, :three => 3) { filters << "after each in config"} c.after(:all, :one => 1, :three => 3) { filters << "after all in config"} end - group = ExampleGroup.describe("group", :one => 1, :two => 2, :three => 3) + group = RSpec.describe("group", :one => 1, :two => 2, :three => 3) group.example("example") {} group.run expect(filters).to eq([ @@ -209,18 +203,81 @@ def filters end it "does not run if some hook filters don't match the group's filters" do + sequence = [] + + RSpec.configure do |c| + c.before(:all, :one => 1, :four => 4) { sequence << "before all in config"} + c.around(:each, :two => 2, :four => 4) {|example| sequence << "around each in config"; example.run} + c.before(:each, :one => 1, :two => 2, :four => 4) { sequence << "before each in config"} + c.after(:each, :one => 1, :two => 2, :three => 3, :four => 4) { sequence << "after each in config"} + c.after(:all, :one => 1, :three => 3, :four => 4) { sequence << "after all in config"} + end + + RSpec.describe "group", :one => 1, :two => 2, :three => 3 do + example("ex1") { sequence << "ex1" } + example("ex2", :four => 4) { sequence << "ex2" } + end.run + + expect(sequence).to eq([ + "ex1", + "before all in config", + "around each in config", + "before each in config", + "ex2", + "after each in config", + "after all in config" + ]) + end + + it "does not run for examples that do not match, even if their group matches" do filters = [] + RSpec.configure do |c| - c.before(:all, :one => 1, :four => 4) { filters << "before all in config"} - c.around(:each, :two => 2, :four => 4) {|example| filters << "around each in config"; example.run} - c.before(:each, :one => 1, :two => 2, :four => 4) { filters << "before each in config"} - c.after(:each, :one => 1, :two => 2, :three => 3, :four => 4) { filters << "after each in config"} - c.after(:all, :one => 1, :three => 3, :four => 4) { filters << "after all in config"} + c.before(:each, :apply_it) { filters << :before_each } end - group = ExampleGroup.describe(:one => 1, :two => 2, :three => 3) - group.example("example") {} - group.run - expect(filters).to eq([]) + + RSpec.describe "Group", :apply_it do + example("ex1") { filters << :matching_example } + example("ex2", :apply_it => false) { filters << :nonmatching_example } + end.run + + expect(filters).to eq([:before_each, :matching_example, :nonmatching_example]) + end + end + + describe ":context hooks defined in configuration with metadata" do + it 'applies to individual matching examples' do + sequence = [] + + RSpec.configure do |config| + config.before(:context, :apply_it) { sequence << :before_context } + config.after(:context, :apply_it) { sequence << :after_context } + end + + RSpec.describe do + example("ex", :apply_it) { sequence << :example } + end.run + + expect(sequence).to eq([:before_context, :example, :after_context]) + end + + it 'does not apply to individual matching examples for which it also applies to a parent example group' do + sequence = [] + + RSpec.configure do |config| + config.before(:context, :apply_it) { sequence << :before_context } + config.after(:context, :apply_it) { sequence << :after_context } + end + + RSpec.describe "Group", :apply_it do + example("ex") { sequence << :outer_example } + + context "nested", :apply_it => false do + example("ex", :apply_it) { sequence << :inner_example } + end + end.run + + expect(sequence).to eq([:before_context, :outer_example, :inner_example, :after_context]) end end end diff --git a/spec/rspec/core/hooks_spec.rb b/spec/rspec/core/hooks_spec.rb index 50ebee6be2..d77b6f4b95 100644 --- a/spec/rspec/core/hooks_spec.rb +++ b/spec/rspec/core/hooks_spec.rb @@ -1,5 +1,3 @@ -require "spec_helper" - module RSpec::Core RSpec.describe Hooks do class HooksHost @@ -8,6 +6,16 @@ class HooksHost def parent_groups [] end + + def register_hook(position, scope, *args, &block) + block ||= Proc.new { } + __send__(position, scope, *args, &block) + hook_collection_for(position, scope).first + end + + def hook_collection_for(position, scope) + hooks.send(:all_hooks_for, position, scope) + end end [:before, :after, :around].each do |type| @@ -17,10 +25,7 @@ def parent_groups describe "##{type}(#{scope})" do it_behaves_like "metadata hash builder" do define_method :metadata_hash do |*args| - instance = HooksHost.new - args.unshift scope if scope - hooks = instance.send(type, *args) {} - hooks.first.options + HooksHost.new.register_hook(type, scope, *args).options end end end @@ -30,38 +35,35 @@ def parent_groups let(:instance) { HooksHost.new } it "defaults to :example scope if no arguments are given" do - hooks = instance.send(type) {} - hook = hooks.first - expect(instance.hooks[type][:example]).to include(hook) + expect { + instance.__send__(type) {} + }.to change { instance.hook_collection_for(type, :example).count }.by(1) end it "defaults to :example scope if the only argument is a metadata hash" do - hooks = instance.send(type, :foo => :bar) {} - hook = hooks.first - expect(instance.hooks[type][:example]).to include(hook) + expect { + instance.__send__(type, :foo => :bar) {} + }.to change { instance.hook_collection_for(type, :example).count }.by(1) end it "raises an error if only metadata symbols are given as arguments" do - expect { instance.send(type, :foo, :bar) {} }.to raise_error(ArgumentError) + expect { instance.__send__(type, :foo, :bar) {} }.to raise_error(ArgumentError) end end end [:before, :after].each do |type| - [:example, :context, :suite].each do |scope| + [:example, :context].each do |scope| describe "##{type}(#{scope.inspect})" do let(:instance) { HooksHost.new } - let!(:hook) do - hooks = instance.send(type, scope) {} - hooks.first - end + let!(:hook) { instance.register_hook(type, scope) } it "does not make #{scope.inspect} a metadata key" do expect(hook.options).to be_empty end it "is scoped to #{scope.inspect}" do - expect(instance.hooks[type][scope]).to include(hook) + expect(instance.hook_collection_for(type, scope)).to include(hook) end it 'does not run when in dry run mode' do @@ -76,26 +78,6 @@ def parent_groups end end - context "when an error happens in `after(:suite)`" do - it 'allows the error to propagate to the user' do - RSpec.configuration.after(:suite) { 1 / 0 } - - expect { - RSpec.configuration.hooks.run(:after, :suite, SuiteHookContext.new) - }.to raise_error(ZeroDivisionError) - end - end - - context "when an error happens in `before(:suite)`" do - it 'allows the error to propagate to the user' do - RSpec.configuration.before(:suite) { 1 / 0 } - - expect { - RSpec.configuration.hooks.run(:before, :suite, SuiteHookContext.new) - }.to raise_error(ZeroDivisionError) - end - end - describe "#around" do context "when it does not run the example" do context "for a hook declared in the group" do @@ -182,7 +164,7 @@ def transactionally; end context "when not running the example within the around block" do it "does not run the example" do examples = [] - group = ExampleGroup.describe do + group = RSpec.describe do around do end it "foo" do @@ -197,7 +179,7 @@ def transactionally; end context "when running the example within the around block" do it "runs the example" do examples = [] - group = ExampleGroup.describe do + group = RSpec.describe do around do |example| example.run end @@ -211,7 +193,7 @@ def transactionally; end it "exposes example metadata to each around hook" do foos = {} - group = ExampleGroup.describe do + group = RSpec.describe do around do |ex| foos[:first] = ex.metadata[:foo] ex.run @@ -233,7 +215,7 @@ def transactionally; end data_2 = {} ex = nil - group = ExampleGroup.describe do + group = RSpec.describe do def self.data_from(ex) { :description => ex.description, @@ -266,7 +248,7 @@ def self.data_from(ex) it "exposes a sensible inspect value" do inspect_value = nil - group = ExampleGroup.describe do + group = RSpec.describe do around do |ex| inspect_value = ex.inspect end @@ -283,7 +265,7 @@ def self.data_from(ex) context "when running the example within a block passed to a method" do it "runs the example" do examples = [] - group = ExampleGroup.describe do + group = RSpec.describe do def yielder yield end @@ -312,7 +294,7 @@ def yielder RSpec.configure { |config| config.before(scope) { messages << "config 4" } } RSpec.configure { |config| config.prepend_before(scope) { messages << "config 1" } } - group = ExampleGroup.describe { example {} } + group = RSpec.describe { example {} } group.before(scope) { messages << "group 3" } group.prepend_before(scope) { messages << "group 2" } group.before(scope) { messages << "group 4" } @@ -341,7 +323,7 @@ def yielder RSpec.configure { |config| config.append_before(scope) { messages << "config 2" } } RSpec.configure { |config| config.before(scope) { messages << "config 3" } } - group = ExampleGroup.describe { example {} } + group = RSpec.describe { example {} } group.before(scope) { messages << "group 1" } group.append_before(scope) { messages << "group 2" } group.before(scope) { messages << "group 3" } @@ -367,7 +349,7 @@ def yielder RSpec.configure { |config| config.prepend_after(scope) { messages << "config 2" } } RSpec.configure { |config| config.after(scope) { messages << "config 1" } } - group = ExampleGroup.describe { example {} } + group = RSpec.describe { example {} } group.after(scope) { messages << "group 3" } group.prepend_after(scope) { messages << "group 2" } group.after(scope) { messages << "group 1" } @@ -394,7 +376,7 @@ def yielder RSpec.configure { |config| config.after(scope) { messages << "config 1" } } RSpec.configure { |config| config.append_after(scope) { messages << "config 4" } } - group = ExampleGroup.describe { example {} } + group = RSpec.describe { example {} } group.after(scope) { messages << "group 2" } group.append_after(scope) { messages << "group 3" } group.after(scope) { messages << "group 1" } @@ -427,7 +409,7 @@ def yielder c.around(:each, &hook) end - group = ExampleGroup.describe { example { messages << "example" } } + group = RSpec.describe { example { messages << "example" } } group.run expect(messages).to eq ["hook 1", "hook 2", "example"] end diff --git a/spec/rspec/core/memoized_helpers_spec.rb b/spec/rspec/core/memoized_helpers_spec.rb index 401b4f0231..7c9ad11105 100644 --- a/spec/rspec/core/memoized_helpers_spec.rb +++ b/spec/rspec/core/memoized_helpers_spec.rb @@ -1,14 +1,12 @@ -require 'spec_helper' - module RSpec::Core RSpec.describe MemoizedHelpers do before(:each) { RSpec.configuration.configure_expectation_framework } def subject_value_for(describe_arg, &block) - group = ExampleGroup.describe(describe_arg, &block) + example_group = RSpec.describe(describe_arg, &block) subject_value = nil - group.example { subject_value = subject } - group.run + example_group.example { subject_value = subject } + example_group.run subject_value end @@ -49,10 +47,28 @@ def subject_value_for(describe_arg, &block) end end + describe "with true" do + it "returns `true`" do + expect(subject_value_for(true)).to eq(true) + end + end + + describe "with false" do + it "returns `false`" do + expect(subject_value_for(false)).to eq(false) + end + end + + describe "with nil" do + it "returns `nil`" do + expect(subject_value_for(nil)).to eq(nil) + end + end + it "can be overriden and super'd to from a nested group" do outer_subject_value = inner_subject_value = nil - ExampleGroup.describe(Array) do + RSpec.describe(Array) do subject { super() << :parent_group } example { outer_subject_value = subject } @@ -72,10 +88,10 @@ def subject_value_for(describe_arg, &block) example_yielded_to_subject = nil example_yielded_to_example = nil - group = ExampleGroup.describe - group.subject { |e| example_yielded_to_subject = e } - group.example { |e| subject; example_yielded_to_example = e } - group.run + example_group = RSpec.describe + example_group.subject { |e| example_yielded_to_subject = e } + example_group.example { |e| subject; example_yielded_to_example = e } + example_group.run expect(example_yielded_to_subject).to eq example_yielded_to_example end @@ -95,18 +111,18 @@ def working_with?(double) [false, nil].each do |falsy_value| context "with a value of #{falsy_value.inspect}" do it "is evaluated once per example" do - group = ExampleGroup.describe(Array) - group.before do + example_group = RSpec.describe(Array) + example_group.before do expect(Object).to receive(:this_question?).once.and_return(falsy_value) end - group.subject do + example_group.subject do Object.this_question? end - group.example do + example_group.example do subject subject end - expect(group.run).to be_truthy, "expected subject block to be evaluated only once" + expect(example_group.run).to be_truthy, "expected subject block to be evaluated only once" end end end @@ -122,7 +138,7 @@ def working_with?(double) describe "defined in a top level group" do let(:group) do - ExampleGroup.describe do + RSpec.describe do subject{ [4, 5, 6] } end end @@ -162,7 +178,7 @@ def working_with?(double) result = nil line = nil - ExampleGroup.describe do + RSpec.describe do subject { nil } send(hook, :all) { result = (subject rescue $!) }; line = __LINE__ example { } @@ -179,7 +195,7 @@ def working_with?(double) example_yielded_to_subject = nil example_yielded_to_example = nil - group = ExampleGroup.describe + group = RSpec.describe group.subject(:foo) { |e| example_yielded_to_subject = e } group.example { |e| foo; example_yielded_to_example = e } group.run @@ -190,7 +206,7 @@ def working_with?(double) it "defines a method that returns the memoized subject" do list_value_1 = list_value_2 = subject_value_1 = subject_value_2 = nil - ExampleGroup.describe do + RSpec.describe do subject(:list) { [1, 2, 3] } example do list_value_1 = list @@ -210,7 +226,7 @@ def working_with?(double) it "is referred from inside subject by the name" do inner_subject_value = nil - ExampleGroup.describe do + RSpec.describe do subject(:list) { [1, 2, 3] } describe 'first' do subject(:first_element) { list.first } @@ -224,7 +240,7 @@ def working_with?(double) it 'can continue to be referenced by the name even when an inner group redefines the subject' do named_value = nil - ExampleGroup.describe do + RSpec.describe do subject(:named) { :outer } describe "inner" do @@ -242,7 +258,7 @@ def working_with?(double) it 'can continue to reference an inner subject after the outer subject name is referenced' do subject_value = nil - ExampleGroup.describe do + RSpec.describe do subject(:named) { :outer } describe "inner" do @@ -260,7 +276,7 @@ def working_with?(double) it 'is not overriden when an inner group defines a new method with the same name' do subject_value = nil - ExampleGroup.describe do + RSpec.describe do subject(:named) { :outer_subject } describe "inner" do @@ -276,7 +292,7 @@ def working_with?(double) def should_raise_not_supported_error(&block) ex = nil - ExampleGroup.describe do + RSpec.describe do let(:list) { ["a", "b", "c"] } subject { [1, 2, 3] } @@ -312,7 +328,7 @@ def should_raise_not_supported_error(&block) context "using 'self' as an explicit subject" do it "delegates matcher to the ExampleGroup" do - group = ExampleGroup.describe("group") do + group = RSpec.describe("group") do subject { self } def ok?; true; end def not_ok?; false; end @@ -326,7 +342,7 @@ def not_ok?; false; end end it 'supports a new expect-based syntax' do - group = ExampleGroup.describe([1, 2, 3]) do + group = RSpec.describe([1, 2, 3]) do it { is_expected.to be_an Array } it { is_expected.not_to include 4 } end @@ -401,7 +417,7 @@ def count it 'raises a useful error when called without a block' do expect do - ExampleGroup.describe { let(:list) } + RSpec.describe { let(:list) } end.to raise_error(/#let or #subject called without a block/) end @@ -437,7 +453,7 @@ def count result = nil line = nil - ExampleGroup.describe do + RSpec.describe do let(:foo) { nil } send(hook, :all) { result = (foo rescue $!) }; line = __LINE__ example { } @@ -450,7 +466,7 @@ def count context "when included modules have hooks that define memoized helpers" do it "allows memoized helpers to override methods in previously included modules" do - group = ExampleGroup.describe do + group = RSpec.describe do include Module.new { def self.included(m); m.let(:unrelated) { :unrelated }; end } diff --git a/spec/rspec/core/metadata_filter_spec.rb b/spec/rspec/core/metadata_filter_spec.rb index 35b16183c2..98b77b32fb 100644 --- a/spec/rspec/core/metadata_filter_spec.rb +++ b/spec/rspec/core/metadata_filter_spec.rb @@ -1,5 +1,3 @@ -require 'spec_helper' - module RSpec module Core RSpec.describe MetadataFilter do @@ -9,9 +7,9 @@ module Core def create_metadatas container = self - RSpec.describe "parent group", :caller => ["foo_spec.rb:#{__LINE__}"] do; container.parent_group_metadata = metadata - describe "group", :caller => ["foo_spec.rb:#{__LINE__}"] do; container.group_metadata = metadata - container.example_metadata = it("example", :caller => ["foo_spec.rb:#{__LINE__}"], :if => true).metadata + RSpec.describe "parent group", :caller => ["/foo_spec.rb:#{__LINE__}"] do; container.parent_group_metadata = metadata + describe "group", :caller => ["/foo_spec.rb:#{__LINE__}"] do; container.group_metadata = metadata + container.example_metadata = it("example", :caller => ["/foo_spec.rb:#{__LINE__}"], :if => true).metadata end end end @@ -86,10 +84,6 @@ def filter_applies?(key, value, metadata) end end - it "ignores location filters for other files" do - expect(filter_applies?(:locations, {"/path/to/other_spec.rb" => [3,5,7]}, example_metadata)).to be_truthy - end - it "matches a proc with no arguments that evaluates to true" do expect(filter_applies?(:if, lambda { true }, example_metadata)).to be_truthy end diff --git a/spec/rspec/core/metadata_spec.rb b/spec/rspec/core/metadata_spec.rb index 1a29691480..b646a7dfc7 100644 --- a/spec/rspec/core/metadata_spec.rb +++ b/spec/rspec/core/metadata_spec.rb @@ -1,5 +1,3 @@ -require 'spec_helper' - module RSpec module Core RSpec.describe Metadata do @@ -24,6 +22,13 @@ module Core end end + it 'should not transform directories beginning with the same prefix' do + #E.g. /foo/bar_baz is not relative to /foo/bar !! + + similar_directory = "#{File.expand_path(".")}_similar" + expect(Metadata.relative_path(similar_directory)).to eq similar_directory + end + end context "when created" do @@ -65,9 +70,9 @@ def metadata_for(*args) RSpec::Matchers.define :have_value do |value| chain(:for) { |key| @key = key } - match do |metadata| - expect(metadata.fetch(@key)).to eq(value) - expect(metadata[@key]).to eq(value) + match do |meta| + expect(meta.fetch(@key)).to eq(value) + expect(meta[@key]).to eq(value) end end @@ -123,15 +128,15 @@ def metadata_for(*args) end it 'does not include example-group specific keys' do - metadata = nil + meta = nil RSpec.describe "group" do context "nested" do - metadata = example("foo").metadata + meta = example("foo").metadata end end - expect(metadata.keys).not_to include(:parent_example_group) + expect(meta.keys).not_to include(:parent_example_group) end end @@ -154,6 +159,99 @@ def metadata_for(*args) end end + describe ":shared_group_inclusion_backtrace" do + context "for an example group" do + it "is not set since we do not yet need it internally (but we can add it in the future if needed)" do + group = RSpec.describe("group") + expect(group.metadata).not_to include(:shared_group_inclusion_backtrace) + end + end + + context "for an example" do + context "not generated by a shared group" do + it "is a blank array" do + meta = nil + RSpec.describe { meta = example { }.metadata } + expect(meta).to include(:shared_group_inclusion_backtrace => []) + end + end + + context "generated by an unnested shared group included via metadata" do + it "is an array containing an object with shared group name and inclusion location" do + meta = nil + + RSpec.shared_examples_for("some shared behavior", :include_it => true) do + meta = example { }.metadata + end + + line = __LINE__ + 1 + RSpec.describe("Group", :include_it => true) { } + + expect(meta[:shared_group_inclusion_backtrace]).to match [ an_object_having_attributes( + :shared_group_name => "some shared behavior", + :inclusion_location => a_string_including("#{Metadata.relative_path __FILE__}:#{line}") + ) ] + end + end + + { + :it_behaves_like => "generates a nested group", + :include_examples => "adds the examples directly to the host group" + }.each do |inclusion_method, description| + context "generated by an unnested shared group using an inclusion method that #{description}" do + it "is an array containing an object with shared group name and inclusion location" do + meta = nil + + RSpec.shared_examples_for("some shared behavior") do + meta = example { }.metadata + end + + line = __LINE__ + 2 + RSpec.describe do + __send__ inclusion_method, "some shared behavior" + end + + expect(meta[:shared_group_inclusion_backtrace]).to match [ an_object_having_attributes( + :shared_group_name => "some shared behavior", + :inclusion_location => a_string_including("#{Metadata.relative_path __FILE__}:#{line}") + ) ] + end + end + + context "generated by a nested shared group using an inclusion method that #{description}" do + it "contains a stack frame for each inclusion, in the same order as ruby backtraces" do + meta = nil + + RSpec.shared_examples_for "inner" do + meta = example { }.metadata + end + + inner_line = __LINE__ + 2 + RSpec.shared_examples_for "outer" do + __send__ inclusion_method, "inner" + end + + outer_line = __LINE__ + 2 + RSpec.describe do + __send__ inclusion_method, "outer" + end + + expect(meta[:shared_group_inclusion_backtrace]).to match [ + an_object_having_attributes( + :shared_group_name => "inner", + :inclusion_location => a_string_including("#{Metadata.relative_path __FILE__}:#{inner_line}") + ), + an_object_having_attributes( + :shared_group_name => "outer", + :inclusion_location => a_string_including("#{Metadata.relative_path __FILE__}:#{outer_line}") + ), + ] + end + end + end + end + end + describe ":described_class" do value_from = lambda do |group| group.metadata[:described_class] @@ -180,6 +278,12 @@ def metadata_for(*args) it "returns the class" do expect(value_for String).to be(String) end + + context "when the class is Regexp" do + it "returns the class" do + expect(value_for Regexp).to be(Regexp) + end + end end end @@ -517,10 +621,10 @@ def value_for(*args) end it 'allows integration libraries like VCR to infer a fixture name from the example description by walking up nesting structure' do - fixture_name_for = lambda do |metadata| - description = metadata[:description] + fixture_name_for = lambda do |meta| + description = meta[:description] - if example_group = metadata[:example_group] + if example_group = meta[:example_group] [fixture_name_for[example_group], description].join('/') else description @@ -562,7 +666,8 @@ def value_for(*args) line = __LINE__ + 1 RSpec.describe("group") { meta = metadata } - applies = MetadataFilter.any_apply?( + applies = MetadataFilter.apply?( + :any?, { :example_group => { :line_number => line } }, meta ) diff --git a/spec/rspec/core/notifications_spec.rb b/spec/rspec/core/notifications_spec.rb index 2d3eded4fd..83428f5026 100644 --- a/spec/rspec/core/notifications_spec.rb +++ b/spec/rspec/core/notifications_spec.rb @@ -1,5 +1,5 @@ -require 'spec_helper' require 'rspec/core/notifications' +require 'pathname' RSpec.describe "FailedExampleNotification" do include FormatterSupport @@ -7,7 +7,7 @@ let(:notification) { ::RSpec::Core::Notifications::FailedExampleNotification.new(example) } before do - allow(example).to receive(:file_path) { __FILE__ } + example.metadata[:absolute_file_path] = __FILE__ end # ported from `base_formatter_spec` should be refactored by final @@ -45,6 +45,16 @@ end end + context "when the stacktrace includes relative paths (which can happen when using `rspec/autorun` and running files through `ruby`)" do + let(:relative_file) { Pathname(__FILE__).relative_path_from(Pathname(Dir.pwd)) } + line = __LINE__ + let(:exception) { instance_double(Exception, :backtrace => ["#{relative_file}:#{line}"]) } + + it 'still finds the backtrace line' do + expect(notification.send(:read_failed_line)).to include("line = __LINE__") + end + end + context "when String alias to_int to_i" do before do String.class_exec do @@ -83,7 +93,9 @@ end it 'returns failures_lines without color when they are part of a shared example group' do - allow(example_group).to receive(:metadata) { {:shared_group_name => 'double shared group'} } + example.metadata[:shared_group_inclusion_backtrace] << + RSpec::Core::SharedExampleGroupInclusionStackFrame.new("foo", "bar") + lines = notification.message_lines expect(lines[0]).to match %r{\AFailure\/Error} expect(lines[1]).to match %r{\A\s*Test exception\z} diff --git a/spec/rspec/core/option_parser_spec.rb b/spec/rspec/core/option_parser_spec.rb index feee8c1010..f387dbb3f6 100644 --- a/spec/rspec/core/option_parser_spec.rb +++ b/spec/rspec/core/option_parser_spec.rb @@ -1,5 +1,3 @@ -require "spec_helper" - module RSpec::Core RSpec.describe OptionParser do before do @@ -61,6 +59,20 @@ def generate_help_text expect { generate_help_text }.to_not output(useless_lines).to_stdout end + describe "-I" do + it "sets the path" do + options = Parser.parse(%w[-I path/to/foo]) + expect(options[:libs]).to eq %w[path/to/foo] + end + + context "with a string containing `#{File::PATH_SEPARATOR}`" do + it "splits into multiple paths, just like Ruby's `-I` option" do + options = Parser.parse(%W[-I path/to/foo -I path/to/bar#{File::PATH_SEPARATOR}path/to/baz]) + expect(options[:libs]).to eq %w[path/to/foo path/to/bar path/to/baz] + end + end + end + describe "--default-path" do it "sets the default path where RSpec looks for examples" do options = Parser.parse(%w[--default-path foo]) @@ -79,9 +91,8 @@ def generate_help_text %w[--out -o].each do |option| describe option do - let(:options) { Parser.parse([option, 'out.txt']) } - it "sets the output stream for the formatter" do + options = Parser.parse([option, 'out.txt']) expect(options[:formatters].last).to eq(['progress', 'out.txt']) end diff --git a/spec/rspec/core/ordering_spec.rb b/spec/rspec/core/ordering_spec.rb index 2a96328cc0..2cda655899 100644 --- a/spec/rspec/core/ordering_spec.rb +++ b/spec/rspec/core/ordering_spec.rb @@ -1,5 +1,3 @@ -require "spec_helper" - module RSpec module Core module Ordering diff --git a/spec/rspec/core/pending_example_spec.rb b/spec/rspec/core/pending_example_spec.rb index 35da981bcd..5e2f25ed4a 100644 --- a/spec/rspec/core/pending_example_spec.rb +++ b/spec/rspec/core/pending_example_spec.rb @@ -1,9 +1,7 @@ -require 'spec_helper' - RSpec.describe "an example" do context "declared pending with metadata" do it "uses the value assigned to :pending as the message" do - group = RSpec::Core::ExampleGroup.describe('group') do + group = RSpec.describe('group') do example "example", :pending => 'just because' do fail end @@ -14,7 +12,7 @@ end it "sets the message to 'No reason given' if :pending => true" do - group = RSpec::Core::ExampleGroup.describe('group') do + group = RSpec.describe('group') do example "example", :pending => true do fail end @@ -25,7 +23,7 @@ end it "passes if a mock expectation is not satisifed" do - group = RSpec::Core::ExampleGroup.describe('group') do + group = RSpec.describe('group') do example "example", :pending => "because" do expect(RSpec).to receive(:a_message_in_a_bottle) end @@ -38,7 +36,7 @@ end it "does not mutate the :pending attribute of the user metadata when handling mock expectation errors" do - group = RSpec::Core::ExampleGroup.describe('group') do + group = RSpec.describe('group') do example "example", :pending => "because" do expect(RSpec).to receive(:a_message_in_a_bottle) end @@ -50,9 +48,43 @@ end end + context "made pending with `define_derived_metadata`" do + before do + RSpec.configure do |config| + config.define_derived_metadata(:not_ready) do |meta| + meta[:pending] ||= "Not ready" + end + end + end + + it 'has a pending result if there is an error' do + group = RSpec.describe "group" do + example "something", :not_ready do + boom + end + end + + group.run + example = group.examples.first + expect(example).to be_pending_with("Not ready") + end + + it 'fails if there is no error' do + group = RSpec.describe "group" do + example "something", :not_ready do + end + end + + group.run + example = group.examples.first + expect(example.execution_result.status).to be(:failed) + expect(example.execution_result.exception.message).to include("Expected example to fail") + end + end + context "with no block" do it "is listed as pending with 'Not yet implemented'" do - group = RSpec::Core::ExampleGroup.describe('group') do + group = RSpec.describe('group') do it "has no block" end example = group.examples.first @@ -63,7 +95,7 @@ context "with no args" do it "is listed as pending with the default message" do - group = RSpec::Core::ExampleGroup.describe('group') do + group = RSpec.describe('group') do it "does something" do pending fail @@ -76,7 +108,7 @@ it "fails when the rest of the example passes" do called = false - group = RSpec::Core::ExampleGroup.describe('group') do + group = RSpec.describe('group') do it "does something" do pending called = true @@ -92,7 +124,7 @@ end it "does not mutate the :pending attribute of the user metadata when the rest of the example passes" do - group = RSpec::Core::ExampleGroup.describe('group') do + group = RSpec.describe('group') do it "does something" do pending end @@ -106,41 +138,43 @@ context "with no docstring" do context "declared with the pending method" do - it "has an auto-generated description" do - group = RSpec::Core::ExampleGroup.describe('group') do + it "has an auto-generated description if it has an expectation" do + ex = nil + + RSpec.describe('group') do it "checks something" do expect((3+4)).to eq(7) end - pending do + ex = pending do expect("string".reverse).to eq("gnirts") end - end - example = group.examples.last - example.run(group.new, double.as_null_object) - expect(example.description).to eq('should eq "gnirts"') + end.run + + expect(ex.description).to eq('should eq "gnirts"') end end context "after another example with some assertion" do it "does not show any message" do - group = RSpec::Core::ExampleGroup.describe('group') do + ex = nil + + RSpec.describe('group') do it "checks something" do expect((3+4)).to eq(7) end - specify do + ex = specify do pending end - end - example = group.examples.last - example.run(group.new, double.as_null_object) - expect(example.description).to match(/example at/) + end.run + + expect(ex.description).to match(/example at/) end end end context "with a message" do it "is listed as pending with the supplied message" do - group = RSpec::Core::ExampleGroup.describe('group') do + group = RSpec.describe('group') do it "does something" do pending("just because") fail @@ -154,7 +188,7 @@ context "with a block" do it "fails with an ArgumentError stating the syntax is deprecated" do - group = RSpec::Core::ExampleGroup.describe('group') do + group = RSpec.describe('group') do it "calls pending with a block" do pending("with invalid syntax") do :no_op @@ -172,7 +206,7 @@ it "does not yield to the block" do example_to_have_yielded = :did_not_yield - group = RSpec::Core::ExampleGroup.describe('group') do + group = RSpec.describe('group') do it "calls pending with a block" do pending("just because") do example_to_have_yielded = :pending_block diff --git a/spec/rspec/core/pending_spec.rb b/spec/rspec/core/pending_spec.rb index 8556503e69..8fe1631e9c 100644 --- a/spec/rspec/core/pending_spec.rb +++ b/spec/rspec/core/pending_spec.rb @@ -1,5 +1,3 @@ -require 'spec_helper' - RSpec.describe RSpec::Core::Pending do it 'only defines methods that are part of the DSL' do expect(RSpec::Core::Pending.instance_methods(false).map(&:to_sym)).to \ diff --git a/spec/rspec/core/project_initializer_spec.rb b/spec/rspec/core/project_initializer_spec.rb index 24d7780e17..2ae9467bbd 100644 --- a/spec/rspec/core/project_initializer_spec.rb +++ b/spec/rspec/core/project_initializer_spec.rb @@ -1,4 +1,3 @@ -require "spec_helper" require 'rspec/core/project_initializer' module RSpec::Core diff --git a/spec/rspec/core/rake_task_spec.rb b/spec/rspec/core/rake_task_spec.rb index 96673da51f..772dea4162 100644 --- a/spec/rspec/core/rake_task_spec.rb +++ b/spec/rspec/core/rake_task_spec.rb @@ -1,4 +1,3 @@ -require "spec_helper" require "rspec/core/rake_task" require 'tempfile' @@ -24,11 +23,11 @@ def spec_command context "with args passed to the rake task" do it "correctly passes along task arguments" do - task = RakeTask.new(:rake_task_args, :files) do |t, args| + the_task = RakeTask.new(:rake_task_args, :files) do |t, args| expect(args[:files]).to eq "first_spec.rb" end - expect(task).to receive(:run_task) { true } + expect(the_task).to receive(:run_task) { true } expect(Rake.application.invoke_task("rake_task_args[first_spec.rb]")).to be_truthy end end @@ -37,14 +36,21 @@ def spec_command context "default" do it "renders rspec" do - expect(spec_command).to match(/^#{ruby} #{default_load_path_opts} #{task.rspec_path}/) + expect(spec_command).to match(/^#{ruby} #{default_load_path_opts} '?#{task.rspec_path}'?/) + end + end + + context "with space", :unless => RSpec::Support::OS.windows? do + it "renders rspec with space escaped" do + task.rspec_path = '/path with space/exe/rspec' + expect(spec_command).to match(/^#{ruby} #{default_load_path_opts} \/path\\ with\\ space\/exe\/rspec/) end end context "with ruby options" do it "renders them before the rspec path" do task.ruby_opts = "-w" - expect(spec_command).to match(/^#{ruby} -w #{default_load_path_opts} #{task.rspec_path}/) + expect(spec_command).to match(/^#{ruby} -w #{default_load_path_opts} '?#{task.rspec_path}'?/) end end @@ -58,26 +64,59 @@ def spec_command context "with pattern" do it "adds the pattern" do task.pattern = "complex_pattern" - expect(spec_command).to include(" --pattern complex_pattern") + expect(spec_command).to match(/ --pattern '?complex_pattern'?/) end - it "shellescapes the pattern as necessary" do + it "shellescapes the pattern as necessary", :unless => RSpec::Support::OS.windows? do task.pattern = "foo'bar" expect(spec_command).to include(" --pattern foo\\'bar") end end context 'with custom exit status' do + def silence_output(&block) + expect(&block).to output(anything).to_stdout.and output(anything).to_stderr + end + it 'returns the correct status on exit', :slow do - with_isolated_stderr do - expect($stderr).to receive(:puts) { |cmd| expect(cmd).to match(/-e "exit\(2\);".* failed/) } - expect(task).to receive(:exit).with(2) + expect(task).to receive(:exit).with(2) + + silence_output do task.ruby_opts = '-e "exit(2);" ;#' - task.run_task false + task.run_task true end end end + context 'with verbose enabled' do + it 'prints the command only to stdout for passing specs', :slow do + expect { + task.ruby_opts = '-e ""' + task.run_task true + }.to output(/-e ""/).to_stdout.and avoid_outputting.to_stderr + end + + it 'prints an additional message to stderr for failures', :slow do + allow(task).to receive(:exit) + + expect { + task.ruby_opts = '-e "exit(1);" ;#' + task.run_task true + }.to output(/-e "exit\(1\);" ;#/).to_stdout.and output(/-e "exit\(1\);".* failed/).to_stderr + end + end + + context 'with verbose disabled' do + it 'does not print to stdout or stderr', :slow do + allow(task).to receive(:exit) + + expect { + task.ruby_opts = '-e "exit(1);" ;#' + task.run_task false + }.to avoid_outputting.to_stdout.and avoid_outputting.to_stderr + end + end + def loaded_files args = Shellwords.split(spec_command) args -= [task.class::RUBY, "-S", task.rspec_path] @@ -118,6 +157,12 @@ def specify_consistent_ordering_of_files_to_run(pattern, file_searcher) describe "load path manipulation" do def self.it_configures_rspec_load_path(description, path_template) context "when rspec is installed as #{description}" do + # Matchers are lazily loaded via `autoload`, so we need to get the matcher before + # the load path is manipulated, so we're using `let!` here to do that. + let!(:include_expected_load_path_option) do + match(/ -I'?#{path_template % "rspec-core"}'?#{File::PATH_SEPARATOR}'?#{path_template % "rspec-support"}'? /) + end + it "adds the current rspec-core and rspec-support dirs to the load path to ensure the current version is used" do $LOAD_PATH.replace([ path_template % "rspec-core", @@ -127,7 +172,18 @@ def self.it_configures_rspec_load_path(description, path_template) path_template % "rake" ]) - expect(spec_command).to include(" -I#{path_template % "rspec-core"}:#{path_template % "rspec-support"} ") + expect(spec_command).to include_expected_load_path_option + end + + it "avoids adding the same load path entries twice" do + $LOAD_PATH.replace([ + path_template % "rspec-core", + path_template % "rspec-support", + path_template % "rspec-core", + path_template % "rspec-support" + ]) + + expect(spec_command).to include_expected_load_path_option end end end @@ -142,22 +198,25 @@ def self.it_configures_rspec_load_path(description, path_template) "/Users/myron/.gem/ruby/1.9.3/gems/%s-3.1.0.beta1/lib" it "does not include extra load path entries for other gems that have `rspec-core` in its path" do + # matchers are lazily loaded with autoload, so we need to get the matcher before manipulating the load path. + include_extra_load_path_entries = include("simplecov", "minitest", "rspec-core/spec") + # these are items on my load path due to `bundle install --standalone`, # and my initial logic caused all these to be included in the `-I` option. $LOAD_PATH.replace([ - "/Users/myron/code/rspec-dev/repos/rspec-core/spec", - "/Users/myron/code/rspec-dev/repos/rspec-core/bundle/ruby/1.9.1/gems/simplecov-0.8.2/lib", - "/Users/myron/code/rspec-dev/repos/rspec-core/bundle/ruby/1.9.1/gems/simplecov-html-0.8.0/lib", - "/Users/myron/code/rspec-dev/repos/rspec-core/bundle/ruby/1.9.1/gems/minitest-5.3.3/lib", - "/Users/myron/code/rspec-dev/repos/rspec/lib", - "/Users/myron/code/rspec-dev/repos/rspec-mocks/lib", - "/Users/myron/code/rspec-dev/repos/rspec-core/lib", - "/Users/myron/code/rspec-dev/repos/rspec-expectations/lib", - "/Users/myron/code/rspec-dev/repos/rspec-support/lib", - "/Users/myron/code/rspec-dev/repos/rspec-core/bundle", + "/Users/user/code/rspec-dev/repos/rspec-core/spec", + "/Users/user/code/rspec-dev/repos/rspec-core/bundle/ruby/1.9.1/gems/simplecov-0.8.2/lib", + "/Users/user/code/rspec-dev/repos/rspec-core/bundle/ruby/1.9.1/gems/simplecov-html-0.8.0/lib", + "/Users/user/code/rspec-dev/repos/rspec-core/bundle/ruby/1.9.1/gems/minitest-5.3.3/lib", + "/Users/user/code/rspec-dev/repos/rspec/lib", + "/Users/user/code/rspec-dev/repos/rspec-mocks/lib", + "/Users/user/code/rspec-dev/repos/rspec-core/lib", + "/Users/user/code/rspec-dev/repos/rspec-expectations/lib", + "/Users/user/code/rspec-dev/repos/rspec-support/lib", + "/Users/user/code/rspec-dev/repos/rspec-core/bundle", ]) - expect(spec_command).not_to include("simplecov", "minitest", "rspec-core/spec") + expect(spec_command).not_to include_extra_load_path_entries end end @@ -166,24 +225,108 @@ def self.it_configures_rspec_load_path(description, path_template) specify_consistent_ordering_of_files_to_run('a/*.rb', Dir) end - context "with a pattern that matches no files" do - it "runs nothing" do - task.pattern = 'a/*.no_match' - expect(loaded_files).to eq([]) + context "with a pattern value" do + context "that matches no files" do + it "runs nothing" do + task.pattern = 'a/*.no_match' + expect(loaded_files).to eq([]) + end end - end - context "with a pattern value that is an existing directory, not a file glob" do - it "loads the spec files in that directory" do - task.pattern = "./spec/rspec/core/resources/acceptance" - expect(loaded_files).to eq(["./spec/rspec/core/resources/acceptance/foo_spec.rb"]) + context "that is an existing directory, not a file glob" do + it "loads the spec files in that directory" do + task.pattern = "./spec/rspec/core/resources/acceptance" + expect(loaded_files).to contain_files("./spec/rspec/core/resources/acceptance/foo_spec.rb") + end end - end - context "with a pattern value that is an existing file, not a file glob" do - it "loads the spec file" do - task.pattern = "./spec/rspec/core/resources/acceptance/foo_spec.rb" - expect(loaded_files).to eq(["./spec/rspec/core/resources/acceptance/foo_spec.rb"]) + context "that is an existing file, not a file glob" do + it "loads the spec file" do + task.pattern = "./spec/rspec/core/resources/acceptance/foo_spec.rb" + expect(loaded_files).to contain_files("./spec/rspec/core/resources/acceptance/foo_spec.rb") + end + end + + context "that is an absolute path file glob" do + it "loads the matching spec files", :failing_on_appveyor, + :pending => false, + :skip => (ENV['APPVEYOR'] ? "Failing on AppVeyor but :pending isn't working for some reason" : false) do + dir = File.expand_path("../resources", __FILE__) + task.pattern = File.join(dir, "**/*_spec.rb") + + expect(loaded_files).to contain_files( + "./spec/rspec/core/resources/acceptance/foo_spec.rb", + "./spec/rspec/core/resources/a_spec.rb" + ) + end + end + + context "that is a relative file glob, for a path not under the default spec dir (`spec`)" do + it "loads the matching spec files" do + Dir.chdir("./spec/rspec/core") do + task.pattern = "resources/**/*_spec.rb" + + expect(loaded_files).to contain_files( + "resources/acceptance/foo_spec.rb", + "resources/a_spec.rb" + ) + end + end + end + + context "that is an array of existing files or directories, not a file glob" do + it "loads the specified spec files, and spec files from the specified directories" do + task.pattern = ["./spec/rspec/core/resources/acceptance", + "./spec/rspec/core/resources/a_bar.rb"] + + expect(loaded_files).to contain_files( + "./spec/rspec/core/resources/acceptance/foo_spec.rb", + "./spec/rspec/core/resources/a_bar.rb" + ) + end + end + + # https://github.com/rspec/rspec-core/issues/1695 + context "that is a single glob that starts with ./" do + it "loads the spec files that match the glob" do + task.pattern = "./spec/rspec/core/resources/acceptance/**/*_spec.rb" + expect(loaded_files).to contain_files("./spec/rspec/core/resources/acceptance/foo_spec.rb") + end + end + + context "that is an array of globs relative to the current working dir" do + it "loads spec files that match any of the globs" do + task.pattern = ["./spec/rspec/core/resources/acceptance/*_spec.rb", + "./spec/rspec/core/resources/*_bar.rb"] + + expect(loaded_files).to contain_files( + "./spec/rspec/core/resources/acceptance/foo_spec.rb", + "./spec/rspec/core/resources/a_bar.rb" + ) + end + end + + context "that is a mixture of file globs and individual files or dirs" do + it "loads all specified or matching files" do + task.pattern = ["./spec/rspec/core/resources/acceptance/*_spec.rb", + "./spec/rspec/core/resources/a_bar.rb"] + + expect(loaded_files).to contain_files( + "./spec/rspec/core/resources/acceptance/foo_spec.rb", + "./spec/rspec/core/resources/a_bar.rb" + ) + end + end + + context "that is a FileList" do + it "loads the files from the FileList" do + task.pattern = FileList["spec/rspec/core/resources/**/*_spec.rb"] + + expect(loaded_files).to contain_exactly( + "spec/rspec/core/resources/a_spec.rb", + "spec/rspec/core/resources/acceptance/foo_spec.rb" + ) + end end end @@ -216,7 +359,7 @@ def make_files_in_dir(dir) make_files_in_dir "acceptance" end - it "shellescapes the pattern as necessary" do + it "shellescapes the pattern as necessary", :unless => RSpec::Support::OS.windows? do task.exclude_pattern = "foo'bar" expect(spec_command).to include(" --exclude-pattern foo\\'bar") end @@ -225,14 +368,22 @@ def make_files_in_dir(dir) task.pattern = "spec/**/*_spec.rb" unit_files = make_files_in_dir "unit" - expect(loaded_files).to match_array(unit_files) + expect(loaded_files).to contain_files(*unit_files) + end + + it "excludes files when pattern and exclusion_pattern don't consistently start with ./" do + task.exclude_pattern = "./spec/acceptance/*_spec.rb" + task.pattern = "spec/**/*_spec.rb" + unit_files = make_files_in_dir "unit" + + expect(loaded_files).to contain_files(*unit_files) end end context "with paths with quotes or spaces" do include_context "isolated directory" - it "matches files with quotes and spaces" do + it "matches files with quotes and spaces", :failing_on_appveyor do spec_dir = File.join(Dir.getwd, "spec") task.pattern = "spec/*spec.rb" FileUtils.mkdir_p(spec_dir) @@ -241,7 +392,7 @@ def make_files_in_dir(dir) File.join("spec", file_name).tap { |f| FileUtils.touch(f) } end - expect(loaded_files).to match_array(files) + expect(loaded_files).to contain_files(*files) end end diff --git a/spec/rspec/core/random_spec.rb b/spec/rspec/core/random_spec.rb index 0875d4cec9..f1e8ea9c9a 100644 --- a/spec/rspec/core/random_spec.rb +++ b/spec/rspec/core/random_spec.rb @@ -1,5 +1,3 @@ -require 'spec_helper' - module RSpec module Core RSpec.describe RandomNumberGenerator do diff --git a/spec/rspec/core/reporter_spec.rb b/spec/rspec/core/reporter_spec.rb index 409db087a6..0c195a7db4 100644 --- a/spec/rspec/core/reporter_spec.rb +++ b/spec/rspec/core/reporter_spec.rb @@ -1,5 +1,3 @@ -require "spec_helper" - module RSpec::Core RSpec.describe Reporter do include FormatterSupport @@ -24,11 +22,13 @@ module RSpec::Core end end - it "dumps the failure summary after the deprecation summary so failures don't scroll off the screen and get missed" do + it "dumps the failure summary after the profile and deprecation summary so failures don't scroll off the screen and get missed" do + config.profile_examples = 10 formatter = instance_double("RSpec::Core::Formatter::ProgressFormatter") - reporter.register_listener(formatter, :dump_summary, :deprecation_summary) + reporter.register_listener(formatter, :dump_summary, :dump_profile, :deprecation_summary) expect(formatter).to receive(:deprecation_summary).ordered + expect(formatter).to receive(:dump_profile).ordered expect(formatter).to receive(:dump_summary).ordered reporter.finish @@ -49,6 +49,16 @@ module RSpec::Core reporter.start 3, (start_time + 5) end + + it 'notifies formatters of the seed used' do + formatter = double("formatter") + reporter.register_listener formatter, :seed + + expect(formatter).to receive(:seed).with( + an_object_having_attributes(:seed => config.seed, :seed_used? => config.seed_used?) + ) + reporter.start 1 + end end context "given one formatter" do @@ -72,7 +82,7 @@ module RSpec::Core reporter.register_listener formatter, :example_group_started, :example_group_finished - group = ExampleGroup.describe("root") + group = RSpec.describe("root") group.describe("context 1") do example("ignore") {} end @@ -101,7 +111,7 @@ module RSpec::Core reporter.register_listener formatter, :example_group_started, :example_group_finished - group = ExampleGroup.describe("root") + group = RSpec.describe("root") group.run(reporter) end diff --git a/spec/rspec/core/resources/utf8_encoded.rb b/spec/rspec/core/resources/utf8_encoded.rb index 7cbdd6908f..b173bce3b4 100644 --- a/spec/rspec/core/resources/utf8_encoded.rb +++ b/spec/rspec/core/resources/utf8_encoded.rb @@ -3,6 +3,7 @@ module Custom class ExampleUTF8ClassNameVarietà def self.è così = :però + così end end end diff --git a/spec/rspec/core/rspec_matchers_spec.rb b/spec/rspec/core/rspec_matchers_spec.rb index 70e51b3c5d..564021637f 100644 --- a/spec/rspec/core/rspec_matchers_spec.rb +++ b/spec/rspec/core/rspec_matchers_spec.rb @@ -1,5 +1,3 @@ -require 'spec_helper' - module RSpec::Matchers def __method_with_super super diff --git a/spec/rspec/core/ruby_project_spec.rb b/spec/rspec/core/ruby_project_spec.rb index 7091c188b9..985a721231 100644 --- a/spec/rspec/core/ruby_project_spec.rb +++ b/spec/rspec/core/ruby_project_spec.rb @@ -1,5 +1,3 @@ -require 'spec_helper' - module RSpec module Core RSpec.describe RubyProject do @@ -21,6 +19,33 @@ module Core end end + + describe "#ascend_until" do + subject { RubyProject } + + def expect_ascend(source_path, *yielded_paths) + expect { |probe| + allow(File).to receive(:expand_path).with('.') { source_path } + subject.ascend_until(&probe) + }.to yield_successive_args(*yielded_paths) + end + + it "works with a normal path" do + expect_ascend("/var//ponies/", "/var/ponies", "/var", "/") + end + + it "works with a path with a trailing slash" do + expect_ascend("/var//ponies/", "/var/ponies", "/var", "/") + end + + it "works with a path with double slashes" do + expect_ascend("/var//ponies/", "/var/ponies", "/var", "/") + end + + it "works with a path with escaped slashes" do + expect_ascend("/var\\/ponies/", "/var\\/ponies", "/") + end + end end end end diff --git a/spec/rspec/core/runner_spec.rb b/spec/rspec/core/runner_spec.rb index 80e961739f..82c917c9a4 100644 --- a/spec/rspec/core/runner_spec.rb +++ b/spec/rspec/core/runner_spec.rb @@ -1,5 +1,5 @@ -require 'spec_helper' require 'rspec/core/drb' +require 'support/runner_support' module RSpec::Core RSpec.describe Runner do @@ -142,10 +142,7 @@ def run_specs end context "when run" do - let(:out) { StringIO.new } - let(:err) { StringIO.new } - let(:config) { RSpec.configuration } - let(:world) { RSpec.world } + include_context "Runner support" before do allow(config.hooks).to receive(:run) @@ -257,31 +254,6 @@ def run_specs expect(runner.run(err, out)).to eq 2 end end - - context "running hooks" do - before { allow(config).to receive :load_spec_files } - - it "runs before suite hooks" do - expect(config.hooks).to receive(:run).with(:before, :suite, instance_of(SuiteHookContext)) - runner = build_runner - runner.run err, out - end - - it "runs after suite hooks" do - expect(config.hooks).to receive(:run).with(:after, :suite, instance_of(SuiteHookContext)) - runner = build_runner - runner.run err, out - end - - it "runs after suite hooks even after an error" do - expect(config.hooks).to receive(:run).with(:before, :suite, instance_of(SuiteHookContext)).and_raise "this error" - expect(config.hooks).to receive(:run).with(:after , :suite, instance_of(SuiteHookContext)) - expect do - runner = build_runner - runner.run err, out - end.to raise_error("this error") - end - end end describe "#run with custom output" do @@ -296,14 +268,6 @@ def run_specs expect(runner.instance_exec { @configuration.output_stream }).to eq output_file end end - - def build_runner *args - Runner.new build_config_options(*args) - end - - def build_config_options *args - ConfigurationOptions.new args - end end end end diff --git a/spec/rspec/core/shared_context_spec.rb b/spec/rspec/core/shared_context_spec.rb index 6bf0c78225..2fb1cc9228 100644 --- a/spec/rspec/core/shared_context_spec.rb +++ b/spec/rspec/core/shared_context_spec.rb @@ -1,5 +1,3 @@ -require "spec_helper" - RSpec.describe RSpec::SharedContext do it "is accessible as RSpec::Core::SharedContext" do RSpec::Core::SharedContext @@ -21,7 +19,7 @@ after(:each) { after_each_hook = true } after(:all) { after_all_hook = true } end - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do include shared example { } end @@ -74,7 +72,7 @@ subject { 17 } end - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do include shared end @@ -89,7 +87,7 @@ example {} end end - group = RSpec::Core::ExampleGroup.describe do + group = RSpec.describe do include shared end diff --git a/spec/rspec/core/shared_example_group_spec.rb b/spec/rspec/core/shared_example_group_spec.rb index 7dcfc4a9fe..65b82b8576 100644 --- a/spec/rspec/core/shared_example_group_spec.rb +++ b/spec/rspec/core/shared_example_group_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'rspec/support/spec/in_sub_process' module RandomTopLevelModule @@ -27,19 +26,19 @@ module Core end RSpec::Matchers.define :have_example_descriptions do |*descriptions| - match do |group| - group.examples.map(&:description) == descriptions + match do |example_group| + example_group.examples.map(&:description) == descriptions end - failure_message do |group| - actual = group.examples.map(&:description) - "expected #{group.name} to have descriptions: #{descriptions.inspect} but had #{actual.inspect}" + failure_message do |example_group| + actual = example_group.examples.map(&:description) + "expected #{example_group.name} to have descriptions: #{descriptions.inspect} but had #{actual.inspect}" end end %w[shared_examples shared_examples_for shared_context].each do |shared_method_name| describe shared_method_name do - let(:group) { ExampleGroup.describe('example group') } + let(:group) { RSpec.describe('example group') } define_method :define_shared_group do |*args, &block| group.send(shared_method_name, *args, &block) @@ -53,6 +52,7 @@ module Core end it "is not exposed to the global namespace when monkey patching is disabled" do + RSpec.configuration.expose_dsl_globally = false expect(Kernel).to_not respond_to(shared_method_name) end @@ -71,7 +71,24 @@ module Core it 'works with top level defined examples in modules' do expect(RSpec::configuration.reporter).to_not receive(:deprecation) - ExampleGroup.describe('example group') { include_context 'top level in module' } + RSpec.describe('example group') { include_context 'top level in module' } + end + + it 'generates a named (rather than anonymous) module' do + define_shared_group("shared behaviors", :include_it) { } + example_group = RSpec.describe("Group", :include_it) { } + + anonymous_module_regex = /#/ + expect(Module.new.inspect).to match(anonymous_module_regex) + + include_a_named_rather_than_anonymous_module = ( + include(a_string_including( + "# :bar, &implementation) - a = RSpec.configuration.include_or_extend_modules.first - expect(a[0]).to eq(:include) - expect(Class.new.send(:include, a[1]).new.bar).to eq('bar') - expect(a[2]).to eq(:foo => :bar) + + matching_group = RSpec.describe "Group", :foo => :bar + non_matching_group = RSpec.describe "Group" + + expect(matching_group.bar).to eq("bar") + expect(non_matching_group).not_to respond_to(:bar) end end @@ -104,12 +123,78 @@ module Core end it "delegates include on configuration" do - implementation = Proc.new { def bar; 'bar'; end } + implementation = Proc.new { def self.bar; 'bar'; end } define_shared_group("name", :foo => :bar, &implementation) - a = RSpec.configuration.include_or_extend_modules.first - expect(a[0]).to eq(:include) - expect(Class.new.send(:include, a[1]).new.bar).to eq('bar') - expect(a[2]).to eq(:foo => :bar) + + matching_group = RSpec.describe "Group", :foo => :bar + non_matching_group = RSpec.describe "Group" + + expect(matching_group.bar).to eq("bar") + expect(non_matching_group).not_to respond_to(:bar) + end + + describe "hooks for individual examples that have matching metadata" do + before do + skip "These specs pass in 2.0 mode on JRuby 1.7.8 but fail on " \ + "1.7.15 when the entire spec suite runs. They pass on " \ + "1.7.15 when this one spec file is run or if we filter to " \ + "just them. Given that 2.0 support on JRuby 1.7 is " \ + "experimental, we're just skipping these specs." + end if RUBY_VERSION == "2.0.0" && RSpec::Support::Ruby.jruby? + + it 'runs them' do + sequence = [] + + define_shared_group("name", :include_it) do + before(:context) { sequence << :before_context } + after(:context) { sequence << :after_context } + + before(:example) { sequence << :before_example } + after(:example) { sequence << :after_example } + + around(:example) do |ex| + sequence << :around_example_before + ex.run + sequence << :around_example_after + end + end + + RSpec.describe "group" do + example("ex1") { sequence << :unmatched_example_1 } + example("ex2", :include_it) { sequence << :matched_example } + example("ex3") { sequence << :unmatched_example_2 } + end.run + + expect(sequence).to eq([ + :unmatched_example_1, + :before_context, + :around_example_before, + :before_example, + :matched_example, + :after_example, + :around_example_after, + :after_context, + :unmatched_example_2 + ]) + end + + it 'runs the `after(:context)` hooks even if the `before(:context)` hook raises an error' do + sequence = [] + + define_shared_group("name", :include_it) do + before(:context) do + sequence << :before_context + raise "boom" + end + after(:context) { sequence << :after_context } + end + + RSpec.describe "group" do + example("ex", :include_it) { sequence << :example } + end.run + + expect(sequence).to eq([ :before_context, :after_context ]) + end end end diff --git a/spec/rspec/core/suite_hooks_spec.rb b/spec/rspec/core/suite_hooks_spec.rb new file mode 100644 index 0000000000..2af89f8e06 --- /dev/null +++ b/spec/rspec/core/suite_hooks_spec.rb @@ -0,0 +1,112 @@ +require "support/runner_support" + +module RSpec::Core + RSpec.describe "Configuration :suite hooks" do + [:before, :after, :prepend_before, :append_before, :prepend_after, :append_after].each do |registration_method| + type = registration_method.to_s.split('_').last + + describe "a `:suite` hook registered with `#{registration_method}" do + it 'is skipped when in dry run mode' do + RSpec.configuration.dry_run = true + + expect { |b| + RSpec.configuration.__send__(registration_method, :suite, &b) + RSpec.configuration.with_suite_hooks { } + }.not_to yield_control + end + + it 'allows errors in the hook to propagate to the user' do + RSpec.configuration.__send__(registration_method, :suite) { 1 / 0 } + + expect { + RSpec.configuration.with_suite_hooks { } + }.to raise_error(ZeroDivisionError) + end + + context "registered on an example group" do + it "is ignored with a clear warning" do + sequence = [] + + expect { + RSpec.describe "Group" do + __send__(registration_method, :suite) { sequence << :suite_hook } + example { sequence << :example } + end.run + }.to change { sequence }.to([:example]). + and output(a_string_including("#{type}(:suite)")).to_stderr + end + end + + context "registered with metadata" do + it "explicitly warns that the metadata is ignored" do + expect { + RSpec.configure do |c| + c.__send__(registration_method, :suite, :some => :metadata) + end + }.to output(a_string_including(":suite", "metadata")).to_stderr + end + end + end + end + + it 'always runs `after(:suite)` hooks even in the face of errors' do + expect { |b| + RSpec.configuration.after(:suite, &b) + RSpec.configuration.with_suite_hooks { raise "boom" } + }.to raise_error("boom").and yield_control + end + + describe "the runner" do + include_context "Runner support" + + def define_and_run_example_group(&block) + example_group = class_double(ExampleGroup, :descendants => []) + + allow(example_group).to receive(:run, &block) + allow(world).to receive_messages(:ordered_example_groups => [example_group]) + allow(config).to receive :load_spec_files + + runner = build_runner + runner.run err, out + end + + it "still runs :suite hooks with metadata even though the metadata is ignored" do + sequence = [] + allow(RSpec).to receive(:warn_with) + + config.before(:suite, :foo) { sequence << :before_suite } + config.after(:suite, :foo) { sequence << :after_suite } + define_and_run_example_group { sequence << :example_groups } + + expect(sequence).to eq([ :before_suite, :example_groups, :after_suite ]) + end + + it "runs :suite hooks before and after example groups in the correct order" do + sequence = [] + + config.before(:suite) { sequence << :before_suite_2 } + config.before(:suite) { sequence << :before_suite_3 } + config.append_before(:suite) { sequence << :before_suite_4 } + config.prepend_before(:suite) { sequence << :before_suite_1 } + config.after(:suite) { sequence << :after_suite_3 } + config.after(:suite) { sequence << :after_suite_2 } + config.prepend_after(:suite) { sequence << :after_suite_1 } + config.append_after(:suite) { sequence << :after_suite_4 } + + define_and_run_example_group { sequence << :example_groups } + + expect(sequence).to eq([ + :before_suite_1, + :before_suite_2, + :before_suite_3, + :before_suite_4, + :example_groups, + :after_suite_1, + :after_suite_2, + :after_suite_3, + :after_suite_4 + ]) + end + end + end +end diff --git a/spec/rspec/core/warnings_spec.rb b/spec/rspec/core/warnings_spec.rb index 09ad6329d9..5b45053a8f 100644 --- a/spec/rspec/core/warnings_spec.rb +++ b/spec/rspec/core/warnings_spec.rb @@ -1,5 +1,3 @@ -require "spec_helper" - RSpec.describe "rspec warnings and deprecations" do describe "#deprecate" do diff --git a/spec/rspec/core/world_spec.rb b/spec/rspec/core/world_spec.rb index 330418d624..a837a13c05 100644 --- a/spec/rspec/core/world_spec.rb +++ b/spec/rspec/core/world_spec.rb @@ -1,5 +1,3 @@ -require 'spec_helper' - class Bar; end class Foo; end @@ -19,15 +17,15 @@ module RSpec::Core describe "#example_groups" do it "contains all registered example groups" do - group = RSpec::Core::ExampleGroup.describe("group"){} - world.register(group) - expect(world.example_groups).to include(group) + example_group = RSpec.describe("group") {} + world.register(example_group) + expect(world.example_groups).to include(example_group) end end describe "#preceding_declaration_line (again)" do let(:group) do - RSpec::Core::ExampleGroup.describe("group") do + RSpec.describe("group") do example("example") {} @@ -35,7 +33,7 @@ module RSpec::Core end let(:second_group) do - RSpec::Core::ExampleGroup.describe("second_group") do + RSpec.describe("second_group") do example("second_example") {} diff --git a/spec/rspec/core_spec.rb b/spec/rspec/core_spec.rb index 424ec92b77..a367e92bc8 100644 --- a/spec/rspec/core_spec.rb +++ b/spec/rspec/core_spec.rb @@ -1,4 +1,3 @@ -require 'spec_helper' require 'rspec/support/spec/prevent_load_time_warnings' RSpec.describe RSpec do @@ -7,18 +6,32 @@ # Loading minitest issues warnings, so we put our fake minitest on the load # path to prevent the real minitest from being loaded. "$LOAD_PATH.unshift '#{fake_minitest}'", 'require "rspec/core"', 'RSpec::Core::Runner.disable_autorun!' do - if RUBY_VERSION == '1.9.2' || (RUBY_PLATFORM == 'java' && RUBY_VERSION == '2.0.0') - before { pending "Not working on #{RUBY_DESCRIPTION}" } + + pending_when = { + '1.9.2' => { :description => "issues no warnings when loaded" }, + '1.8.7' => { :description => "issues no warnings when the spec files are loaded" }, + '2.0.0' => { } + } + + if RUBY_VERSION == '1.9.2' || RUBY_VERSION == '1.8.7' + before(:example, pending_when.fetch(RUBY_VERSION)) do + pending "Not working on #{RUBY_DESCRIPTION}" + end + end + if (RUBY_PLATFORM == 'java' && RUBY_VERSION == '2.0.0') + before(:example, pending_when.fetch(RUBY_VERSION)) do + skip "Not reliably working on #{RUBY_DESCRIPTION}" + end end end - describe "::configuration" do + describe ".configuration" do it "returns the same object every time" do expect(RSpec.configuration).to equal(RSpec.configuration) end end - describe "::configuration=" do + describe ".configuration=" do it "sets the configuration object" do configuration = RSpec::Core::Configuration.new @@ -28,7 +41,7 @@ end end - describe "::configure" do + describe ".configure" do it "yields the current configuration" do RSpec.configure do |config| expect(config).to equal(RSpec::configuration) @@ -36,13 +49,13 @@ end end - describe "::world" do + describe ".world" do it "returns the same object every time" do expect(RSpec.world).to equal(RSpec.world) end end - describe "::world=" do + describe ".world=" do it "sets the world object" do world = RSpec::Core::World.new @@ -54,7 +67,7 @@ describe ".current_example" do it "sets the example being executed" do - group = RSpec::Core::ExampleGroup.describe("an example group") + group = RSpec.describe("an example group") example = group.example("an example") RSpec.current_example = example @@ -62,7 +75,7 @@ end end - describe "::reset" do + describe ".reset" do it "resets the configuration and world objects" do config_before_reset = RSpec.configuration world_before_reset = RSpec.world @@ -74,8 +87,100 @@ end end + describe ".clear_examples" do + let(:listener) { double("listener") } + let(:reporter) { RSpec.configuration.reporter } + + before do + RSpec.configuration.output_stream = StringIO.new + RSpec.configuration.error_stream = StringIO.new + end + + it "clears example groups" do + RSpec.world.example_groups << :example_group + + RSpec.clear_examples + + expect(RSpec.world.example_groups).to be_empty + end + + it "resets start_time" do + start_time_before_clear = RSpec.configuration.start_time + + RSpec.clear_examples + + expect(RSpec.configuration.start_time).not_to eq(start_time_before_clear) + end + + it "clears examples, failed_examples and pending_examples" do + reporter.start(3) + pending_ex = failing_ex = nil + + RSpec.describe do + pending_ex = pending { fail } + failing_ex = example { fail } + end.run + + reporter.example_started(failing_ex) + reporter.example_failed(failing_ex) + + reporter.example_started(pending_ex) + reporter.example_pending(pending_ex) + reporter.finish + + reporter.register_listener(listener, :dump_summary) + + expect(listener).to receive(:dump_summary) do |notification| + expect(notification.examples).to be_empty + expect(notification.failed_examples).to be_empty + expect(notification.pending_examples).to be_empty + end + + RSpec.clear_examples + reporter.start(0) + reporter.finish + end + + it "restores inclusion rules set by configuration" do + file_path = File.expand_path("foo_spec.rb") + RSpec.configure do |config| + config.filter_run_including(:locations => { file_path => [12] }) + end + allow(RSpec.configuration).to receive(:load).with(file_path) + allow(reporter).to receive(:report) + RSpec::Core::Runner.run(["foo_spec.rb:14"]) + + expect( + RSpec.configuration.filter_manager.inclusions[:locations] + ).to eq(file_path => [12, 14]) + + RSpec.clear_examples + + expect( + RSpec.configuration.filter_manager.inclusions[:locations] + ).to eq(file_path => [12]) + end + + it "restores exclusion rules set by configuration" do + RSpec.configure { |config| config.filter_run_excluding(:slow => true) } + allow(RSpec.configuration).to receive(:load) + allow(reporter).to receive(:report) + RSpec::Core::Runner.run(["--tag", "~fast"]) + + expect( + RSpec.configuration.filter_manager.exclusions.rules + ).to eq(:slow => true, :fast => true) + + RSpec.clear_examples + + expect( + RSpec.configuration.filter_manager.exclusions.rules + ).to eq(:slow => true) + end + end + describe "::Core.path_to_executable" do - it 'returns the absolute location of the exe/rspec file' do + it 'returns the absolute location of the exe/rspec file', :failing_on_appveyor do expect(File.exist? RSpec::Core.path_to_executable).to be_truthy expect(File.executable? RSpec::Core.path_to_executable).to be_truthy end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e13bad3efa..81bc9b3a65 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,160 +1,74 @@ require 'rubygems' if RUBY_VERSION.to_f < 1.9 -begin - require 'spork' -rescue LoadError - module Spork - def self.prefork - yield - end +require 'rspec/support/spec' - def self.each_run - yield - end - end +if RUBY_PLATFORM == 'java' + # Works around https://jira.codehaus.org/browse/JRUBY-5678 + require 'fileutils' + ENV['TMPDIR'] = File.expand_path('../../tmp', __FILE__) + FileUtils.mkdir_p(ENV['TMPDIR']) end -Spork.prefork do - require 'rspec/support/spec' - - module ArubaLoader - extend RSpec::Support::WithIsolatedStdErr - with_isolated_stderr do - require 'aruba/api' - end - end - - class << RSpec - attr_writer :configuration, :world - end - - if RUBY_PLATFORM == 'java' - # Works around https://jira.codehaus.org/browse/JRUBY-5678 - require 'fileutils' - ENV['TMPDIR'] = File.expand_path('../../tmp', __FILE__) - FileUtils.mkdir_p(ENV['TMPDIR']) - end +$rspec_core_without_stderr_monkey_patch = RSpec::Core::Configuration.new - $rspec_core_without_stderr_monkey_patch = RSpec::Core::Configuration.new - - class RSpec::Core::Configuration - def self.new(*args, &block) - super.tap do |config| - # We detect ruby warnings via $stderr, - # so direct our deprecations to $stdout instead. - config.deprecation_stream = $stdout - end - end - end - - Dir['./spec/support/**/*.rb'].map {|f| require f} - - class NullObject - private - def method_missing(method, *args, &block) - # ignore +class RSpec::Core::Configuration + def self.new(*args, &block) + super.tap do |config| + # We detect ruby warnings via $stderr, + # so direct our deprecations to $stdout instead. + config.deprecation_stream = $stdout end end +end - module Sandboxing - def self.sandboxed(&block) - @orig_config = RSpec.configuration - @orig_world = RSpec.world - @orig_example = RSpec.current_example - new_config = RSpec::Core::Configuration.new - new_config.expose_dsl_globally = false - new_config.expecting_with_rspec = true - new_world = RSpec::Core::World.new(new_config) - RSpec.configuration = new_config - RSpec.world = new_world - object = Object.new - object.extend(RSpec::Core::SharedExampleGroup) +Dir['./spec/support/**/*.rb'].map do |file| + # Ensure requires are relative to `spec`, which is on the + # load path. This helps prevent double requires on 1.8.7. + require file.gsub("./spec/support", "support") +end - (class << RSpec::Core::ExampleGroup; self; end).class_exec do - alias_method :orig_run, :run - def run(reporter=nil) - RSpec.current_example = nil - orig_run(reporter || NullObject.new) - end - end +module EnvHelpers + def with_env_vars(vars) + original = ENV.to_hash + vars.each { |k, v| ENV[k] = v } - RSpec::Mocks.with_temporary_scope do - object.instance_exec(&block) - end + begin + yield ensure - (class << RSpec::Core::ExampleGroup; self; end).class_exec do - remove_method :run - alias_method :run, :orig_run - remove_method :orig_run - end - - RSpec.configuration = @orig_config - RSpec.world = @orig_world - RSpec.current_example = @orig_example - end - end - - module EnvHelpers - def with_env_vars(vars) - original = ENV.to_hash - vars.each { |k, v| ENV[k] = v } - - begin - yield - ensure - ENV.replace(original) - end - end - - def without_env_vars(*vars) - original = ENV.to_hash - vars.each { |k| ENV.delete(k) } - - begin - yield - ensure - ENV.replace(original) - end + ENV.replace(original) end end - RSpec.configure do |c| - # structural - c.alias_it_behaves_like_to 'it_has_behavior' - c.around {|example| Sandboxing.sandboxed { example.run }} - c.around do |ex| - orig_load_path = $LOAD_PATH.dup - ex.run - $LOAD_PATH.replace(orig_load_path) - end - - c.include(RSpecHelpers) - c.include Aruba::Api, :file_path => /spec\/command_line/ - - c.expect_with :rspec do |expectations| - expectations.syntax = :expect - end + def without_env_vars(*vars) + original = ENV.to_hash + vars.each { |k| ENV.delete(k) } - c.mock_with :rspec do |mocks| - mocks.syntax = :expect + begin + yield + ensure + ENV.replace(original) end - - # runtime options - c.raise_errors_for_deprecations! - c.color = true - c.include EnvHelpers - c.filter_run_excluding :ruby => lambda {|version| - case version.to_s - when "!jruby" - RUBY_ENGINE == "jruby" - when /^> (.*)/ - !(RUBY_VERSION.to_s > $1) - else - !(RUBY_VERSION.to_s =~ /^#{version.to_s}/) - end - } end end -Spork.each_run do +RSpec.configure do |c| + # structural + c.alias_it_behaves_like_to 'it_has_behavior' + c.include(RSpecHelpers) + c.disable_monkey_patching! + + # runtime options + c.raise_errors_for_deprecations! + c.color = true + c.include EnvHelpers + c.filter_run_excluding :ruby => lambda {|version| + case version.to_s + when "!jruby" + RUBY_ENGINE == "jruby" + when /^> (.*)/ + !(RUBY_VERSION.to_s > $1) + else + !(RUBY_VERSION.to_s =~ /^#{version.to_s}/) + end + } end diff --git a/spec/support/aruba_support.rb b/spec/support/aruba_support.rb new file mode 100644 index 0000000000..662ddbb97d --- /dev/null +++ b/spec/support/aruba_support.rb @@ -0,0 +1,57 @@ +module ArubaLoader + extend RSpec::Support::WithIsolatedStdErr + with_isolated_stderr do + require 'aruba/api' + end +end + +RSpec.shared_context "aruba support" do + include Aruba::Api + let(:stderr) { StringIO.new } + let(:stdout) { StringIO.new } + + attr_reader :last_cmd_stdout, :last_cmd_stderr + + def run_command(cmd) + temp_stdout = StringIO.new + temp_stderr = StringIO.new + RSpec::Core::Metadata.instance_variable_set(:@relative_path_regex, nil) + + in_current_dir do + RSpec::Core::Runner.run(cmd.split, temp_stderr, temp_stdout) + end + ensure + RSpec.reset + RSpec::Core::Metadata.instance_variable_set(:@relative_path_regex, nil) + + # Ensure it gets cached with a proper value -- if we leave it set to nil, + # and the next spec operates in a different dir, it could get set to an + # invalid value. + RSpec::Core::Metadata.relative_path_regex + + @last_cmd_stdout = temp_stdout.string + @last_cmd_stderr = temp_stderr.string + stdout.write(@last_cmd_stdout) + stderr.write(@last_cmd_stderr) + end + + def write_file_formatted(file_name, contents) + # remove blank line at the start of the string and + # strip extra indentation. + formatted_contents = unindent(contents.sub(/\A\n/, "")) + write_file file_name, formatted_contents + end + + # Intended for use with indented heredocs. + # taken from Ruby Tapas: + # https://rubytapas.dpdcart.com/subscriber/post?id=616#files + def unindent(s) + s.gsub(/^#{s.scan(/^[ \t]+(?=\S)/).min}/, "") + end +end + +RSpec.configure do |c| + c.define_derived_metadata(:file_path => %r{spec/integration}) do |meta| + meta[:slow] = true + end +end diff --git a/spec/support/formatter_support.rb b/spec/support/formatter_support.rb index b49012d938..0d146ce0b9 100644 --- a/spec/support/formatter_support.rb +++ b/spec/support/formatter_support.rb @@ -32,13 +32,25 @@ def run_example_specs_with_formatter(formatter_option) if RUBY_VERSION.to_f < 1.9 def expected_summary_output_for_example_specs <<-EOS.gsub(/^\s+\|/, '').chomp - |Pending: - | pending spec with no implementation is pending - | # Not yet implemented - | # ./spec/rspec/core/resources/formatter_specs.rb:4 - | pending command with block format with content that would fail is pending - | # No reason given - | # ./spec/rspec/core/resources/formatter_specs.rb:9 + |Pending: (Failures listed here are expected and do not affect your suite's status) + | + | 1) pending spec with no implementation is pending + | # Not yet implemented + | # ./spec/rspec/core/resources/formatter_specs.rb:4 + | + | 2) pending command with block format with content that would fail is pending + | # No reason given + | Failure/Error: expect(1).to eq(2) + | + | expected: 2 + | got: 1 + | + | (compared using ==) + | # ./spec/rspec/core/resources/formatter_specs.rb:11 + | # ./spec/support/formatter_support.rb:13:in `run_example_specs_with_formatter' + | # ./spec/support/sandboxing.rb:16 + | # ./spec/support/sandboxing.rb:14 + | # ./spec/support/sandboxing.rb:8 | |Failures: | @@ -54,14 +66,10 @@ def expected_summary_output_for_example_specs | | (compared using ==) | # ./spec/rspec/core/resources/formatter_specs.rb:31 - | # ./spec/spec_helper.rb:77:in `run' | # ./spec/support/formatter_support.rb:13:in `run_example_specs_with_formatter' - | # ./spec/spec_helper.rb:127 - | # ./spec/spec_helper.rb:124 - | # ./spec/spec_helper.rb:82:in `instance_exec' - | # ./spec/spec_helper.rb:82:in `sandboxed' - | # ./spec/spec_helper.rb:81:in `sandboxed' - | # ./spec/spec_helper.rb:124 + | # ./spec/support/sandboxing.rb:16 + | # ./spec/support/sandboxing.rb:14 + | # ./spec/support/sandboxing.rb:8 | | 3) a failing spec with odd backtraces fails with a backtrace that has no file | Failure/Error: Unable to find matching line from backtrace @@ -89,13 +97,25 @@ def expected_summary_output_for_example_specs else def expected_summary_output_for_example_specs <<-EOS.gsub(/^\s+\|/, '').chomp - |Pending: - | pending spec with no implementation is pending - | # Not yet implemented - | # ./spec/rspec/core/resources/formatter_specs.rb:4 - | pending command with block format with content that would fail is pending - | # No reason given - | # ./spec/rspec/core/resources/formatter_specs.rb:9 + |Pending: (Failures listed here are expected and do not affect your suite's status) + | + | 1) pending spec with no implementation is pending + | # Not yet implemented + | # ./spec/rspec/core/resources/formatter_specs.rb:4 + | + | 2) pending command with block format with content that would fail is pending + | # No reason given + | Failure/Error: expect(1).to eq(2) + | + | expected: 2 + | got: 1 + | + | (compared using ==) + | # ./spec/rspec/core/resources/formatter_specs.rb:11:in `block (3 levels) in ' + | # ./spec/support/formatter_support.rb:13:in `run_example_specs_with_formatter' + | # ./spec/support/sandboxing.rb:16:in `block (4 levels) in ' + | # ./spec/support/sandboxing.rb:14:in `block (3 levels) in ' + | # ./spec/support/sandboxing.rb:8:in `block (2 levels) in ' | |Failures: | @@ -111,14 +131,10 @@ def expected_summary_output_for_example_specs | | (compared using ==) | # ./spec/rspec/core/resources/formatter_specs.rb:31:in `block (2 levels) in ' - | # ./spec/spec_helper.rb:77:in `run' | # ./spec/support/formatter_support.rb:13:in `run_example_specs_with_formatter' - | # ./spec/spec_helper.rb:127:in `block (3 levels) in ' - | # ./spec/spec_helper.rb:124:in `block (4 levels) in ' - | # ./spec/spec_helper.rb:82:in `instance_exec' - | # ./spec/spec_helper.rb:82:in `block in sandboxed' - | # ./spec/spec_helper.rb:81:in `sandboxed' - | # ./spec/spec_helper.rb:124:in `block (3 levels) in ' + | # ./spec/support/sandboxing.rb:16:in `block (4 levels) in ' + | # ./spec/support/sandboxing.rb:14:in `block (3 levels) in ' + | # ./spec/support/sandboxing.rb:8:in `block (2 levels) in ' | | 3) a failing spec with odd backtraces fails with a backtrace that has no file | Failure/Error: ERB.new("<%= raise 'foo' %>").result @@ -126,14 +142,10 @@ def expected_summary_output_for_example_specs | foo | # (erb):1:in `
' | # ./spec/rspec/core/resources/formatter_specs.rb:39:in `block (2 levels) in ' - | # ./spec/spec_helper.rb:77:in `run' | # ./spec/support/formatter_support.rb:13:in `run_example_specs_with_formatter' - | # ./spec/spec_helper.rb:127:in `block (3 levels) in ' - | # ./spec/spec_helper.rb:124:in `block (4 levels) in ' - | # ./spec/spec_helper.rb:82:in `instance_exec' - | # ./spec/spec_helper.rb:82:in `block in sandboxed' - | # ./spec/spec_helper.rb:81:in `sandboxed' - | # ./spec/spec_helper.rb:124:in `block (3 levels) in ' + | # ./spec/support/sandboxing.rb:16:in `block (4 levels) in ' + | # ./spec/support/sandboxing.rb:14:in `block (3 levels) in ' + | # ./spec/support/sandboxing.rb:8:in `block (2 levels) in ' | | 4) a failing spec with odd backtraces fails with a backtrace containing an erb file | Failure/Error: Unable to find matching line from backtrace @@ -197,8 +209,9 @@ def example @example ||= begin result = instance_double(RSpec::Core::Example::ExecutionResult, - :pending_fixed? => false, - :status => :passed + :pending_fixed? => false, + :example_skipped? => false, + :status => :passed ) allow(result).to receive(:exception) { exception } instance_double(RSpec::Core::Example, @@ -206,7 +219,10 @@ def example :full_description => "Example", :execution_result => result, :location => "", - :metadata => {} + :rerun_argument => "", + :metadata => { + :shared_group_inclusion_backtrace => [] + } ) end end diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb index 0596bca6ff..b83b9789ca 100644 --- a/spec/support/matchers.rb +++ b/spec/support/matchers.rb @@ -9,7 +9,7 @@ @file = file end - failure_message_for_should do + failure_message do "expected #{@autotest.class} to map #{@specs.inspect} to #{@file.inspect}\ngot #{@actual.inspect}" end @@ -22,10 +22,11 @@ def prepare(autotest) RSpec::Matchers.define :fail_with do |exception_klass| match do |example| + !example.execution_result.example_skipped? && failure_reason(example, exception_klass).nil? end - failure_message_for_should do |example| + failure_message do |example| "expected example to fail with a #{exception_klass} exception, but #{failure_reason(example, exception_klass)}" end @@ -42,10 +43,11 @@ def failure_reason(example, exception_klass) RSpec::Matchers.define :pass do match do |example| + !example.execution_result.example_skipped? && failure_reason(example).nil? end - failure_message_for_should do |example| + failure_message do |example| "expected example to pass, but #{failure_reason(example)}" end @@ -67,12 +69,16 @@ def failure_reason(example) RSpec::Matchers.define :be_pending_with do |message| match do |example| example.pending? && + !example.execution_result.example_skipped? && + example.execution_result.pending_exception && example.execution_result.status == :pending && example.execution_result.pending_message == message end - failure_message_for_should do |example| - "expected: example pending with #{message.inspect}\n got: #{example.execution_result.pending_message.inspect}" + failure_message do |example| + "expected: example pending with #{message.inspect}\n got: #{example.execution_result.pending_message.inspect}".tap do |msg| + msg << " (but had no pending exception)" unless example.execution_result.pending_exception + end end end @@ -80,10 +86,29 @@ def failure_reason(example) match do |example| example.skipped? && example.pending? && + example.execution_result.example_skipped? && example.execution_result.pending_message == message end - failure_message_for_should do |example| + failure_message do |example| "expected: example skipped with #{message.inspect}\n got: #{example.execution_result.pending_message.inspect}" end end + +RSpec::Matchers.define :contain_files do |*expected_files| + contain_exactly_matcher = RSpec::Matchers::BuiltIn::ContainExactly.new(expected_files.map { |f| File.expand_path(f) }) + + match do |actual_files| + files = actual_files.map { |f| File.expand_path(f) } + contain_exactly_matcher.matches?(files) + end + + failure_message { contain_exactly_matcher.failure_message } + failure_message_when_negated { contain_exactly_matcher.failure_message_when_negated } +end + +RSpec::Matchers.alias_matcher :a_file_collection, :contain_files + +RSpec::Matchers.define_negated_matcher :avoid_outputting, :output +RSpec::Matchers.define_negated_matcher :exclude, :include +RSpec::Matchers.define_negated_matcher :avoid_changing, :change diff --git a/spec/support/mathn_integration_support.rb b/spec/support/mathn_integration_support.rb index 2c8833d391..f6ca400418 100644 --- a/spec/support/mathn_integration_support.rb +++ b/spec/support/mathn_integration_support.rb @@ -3,10 +3,23 @@ module MathnIntegrationSupport include RSpec::Support::InSubProcess - def with_mathn_loaded - in_sub_process do - require 'mathn' - yield + if RUBY_VERSION.to_f >= 2.2 + def with_mathn_loaded + skip "lib/mathn.rb is deprecated in Ruby 2.2" + end + elsif RUBY_VERSION.to_f < 1.9 + def with_mathn_loaded + in_sub_process do + expect { require 'mathn' }.to output.to_stderr + yield + end + end + else + def with_mathn_loaded + in_sub_process do + require 'mathn' + yield + end end end end diff --git a/spec/support/runner_support.rb b/spec/support/runner_support.rb new file mode 100644 index 0000000000..d4b1d62849 --- /dev/null +++ b/spec/support/runner_support.rb @@ -0,0 +1,16 @@ +module RSpec::Core + RSpec.shared_context "Runner support" do + let(:out) { StringIO.new } + let(:err) { StringIO.new } + let(:config) { RSpec.configuration } + let(:world) { RSpec.world } + + def build_runner(*args) + Runner.new(build_config_options(*args)) + end + + def build_config_options(*args) + ConfigurationOptions.new(args) + end + end +end diff --git a/spec/support/sandboxing.rb b/spec/support/sandboxing.rb new file mode 100644 index 0000000000..dc04bb0c97 --- /dev/null +++ b/spec/support/sandboxing.rb @@ -0,0 +1,21 @@ +require 'rspec/core/sandbox' +require 'rspec/mocks' + +# Because testing RSpec with RSpec tries to modify the same global +# objects, we sandbox every test. +RSpec.configure do |c| + c.around do |ex| + RSpec::Core::Sandbox.sandboxed do |config| + # If there is an example-within-an-example, we want to make sure the inner example + # does not get a reference to the outer example (the real spec) if it calls + # something like `pending` + config.before(:context) { RSpec.current_example = nil } + + RSpec::Mocks.with_temporary_scope do + orig_load_path = $LOAD_PATH.dup + ex.run + $LOAD_PATH.replace(orig_load_path) + end + end + end +end diff --git a/spec/support/shared_example_groups.rb b/spec/support/shared_example_groups.rb index 1a58242f58..4a732c1baa 100644 --- a/spec/support/shared_example_groups.rb +++ b/spec/support/shared_example_groups.rb @@ -15,6 +15,10 @@ include_context "isolated directory" let(:project_dir) { Dir.getwd } + before(:example) do + pending "Windows does not support symlinking" + end if RSpec::Support::OS.windows? + it "finds the files" do foos_dir = File.join(project_dir, "spec/foos") FileUtils.mkdir_p foos_dir @@ -26,7 +30,7 @@ FileUtils.ln_s bars_dir, File.join(project_dir, "spec/bars") - expect(loaded_files).to contain_exactly( + expect(loaded_files).to contain_files( "spec/bars/bar_spec.rb", "spec/foos/foo_spec.rb" ) @@ -38,6 +42,6 @@ FileUtils.touch("subtrees/DD/spec/dd_foo_spec.rb") FileUtils.ln_s(File.join(project_dir, "subtrees/DD/spec"), "spec/lib/DD") - expect(loaded_files).to contain_exactly("spec/lib/DD/dd_foo_spec.rb") + expect(loaded_files).to contain_files("spec/lib/DD/dd_foo_spec.rb") end end