├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_rspec_base.yml ├── .rubocop_todo.yml ├── BUILD_DETAIL.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Changelog.md ├── DEVELOPMENT.md ├── Gemfile ├── ISSUE_TEMPLATE.md ├── LICENSE.md ├── README.md ├── REPORT_TEMPLATE.md ├── Rakefile ├── SECURITY.md ├── benchmarks ├── caller.rb ├── caller_vs_caller_locations.rb ├── caller_vs_caller_locations_vs_raise.rb ├── class_exec_vs_klass_exec.rb ├── map_hash.rb ├── ripper.rb └── skip_frames_for_caller_filter.rb ├── lib └── rspec │ ├── support.rb │ └── support │ ├── caller_filter.rb │ ├── comparable_version.rb │ ├── differ.rb │ ├── directory_maker.rb │ ├── encoded_string.rb │ ├── fuzzy_matcher.rb │ ├── hunk_generator.rb │ ├── matcher_definition.rb │ ├── method_signature_verifier.rb │ ├── mutex.rb │ ├── object_formatter.rb │ ├── recursive_const_methods.rb │ ├── reentrant_mutex.rb │ ├── ruby_features.rb │ ├── source.rb │ ├── source │ ├── location.rb │ ├── node.rb │ └── token.rb │ ├── spec.rb │ ├── spec │ ├── deprecation_helpers.rb │ ├── diff_helpers.rb │ ├── formatting_support.rb │ ├── in_sub_process.rb │ ├── library_wide_checks.rb │ ├── shell_out.rb │ ├── stderr_splitter.rb │ ├── string_matcher.rb │ ├── with_isolated_directory.rb │ └── with_isolated_stderr.rb │ ├── version.rb │ ├── warnings.rb │ └── with_keywords_when_needed.rb ├── maintenance-branch ├── rspec-support.gemspec ├── script ├── ci_functions.sh ├── clone_all_rspec_repos ├── cucumber.sh ├── functions.sh ├── legacy_setup.sh ├── predicate_functions.sh ├── run_build ├── run_rubocop └── update_rubygems_and_install_bundler └── spec ├── rspec ├── support │ ├── caller_filter_spec.rb │ ├── comparable_version_spec.rb │ ├── deprecation_helpers_spec.rb │ ├── differ_spec.rb │ ├── directory_maker_spec.rb │ ├── encoded_string_spec.rb │ ├── fuzzy_matcher_spec.rb │ ├── matcher_definition_spec.rb │ ├── method_signature_verifier_spec.rb │ ├── mutex_spec.rb │ ├── object_formatter_spec.rb │ ├── recursive_const_methods_spec.rb │ ├── reentrant_mutex_spec.rb │ ├── ruby_features_spec.rb │ ├── source │ │ ├── node_spec.rb │ │ └── token_spec.rb │ ├── source_spec.rb │ ├── spec │ │ ├── in_sub_process_spec.rb │ │ ├── shell_out_spec.rb │ │ ├── stderr_splitter_spec.rb │ │ └── with_isolated_std_err_spec.rb │ ├── warnings_spec.rb │ └── with_keywords_when_needed_spec.rb └── support_spec.rb └── spec_helper.rb /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # This file was generated on 2023-04-16T20:53:24+01:00 from the rspec-dev repo. 2 | # DO NOT modify it by hand as your changes will get lost the next time it is generated. 3 | 4 | github: [JonRowe, benoittgt] 5 | open_collective: rspec 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file was generated on 2024-09-03T15:05:50+01:00 from the rspec-dev repo. 2 | # DO NOT modify it by hand as your changes will get lost the next time it is generated. 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was generated on 2024-09-03T15:05:50+01:00 from the rspec-dev repo. 2 | # DO NOT modify it by hand as your changes will get lost the next time it is generated. 3 | 4 | name: RSpec CI 5 | on: 6 | push: 7 | branches: 8 | - 'main' 9 | - '*-maintenance' 10 | - '*-dev' 11 | pull_request: 12 | branches: 13 | - '*' 14 | permissions: 15 | contents: read 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | env: 20 | RSPEC_CI: true 21 | # This tells rspec-rails what branch to run in ci 22 | RSPEC_VERSION: '= 3.14.0.pre' 23 | jobs: 24 | rubocop: 25 | name: Rubocop 26 | runs-on: 'ubuntu-20.04' 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: '3.0' 32 | - run: script/update_rubygems_and_install_bundler 33 | - run: script/clone_all_rspec_repos 34 | - run: bundle install --standalone 35 | - run: bundle binstubs --all 36 | - run: script/run_rubocop 37 | 38 | test: 39 | name: Ruby ${{ matrix.ruby }} ${{ matrix.name_extra || '' }} 40 | runs-on: ${{ matrix.os || 'ubuntu-20.04' }} 41 | strategy: 42 | matrix: 43 | ruby: 44 | - '3.3' 45 | - '3.2' 46 | - '3.1' 47 | - '3.0' 48 | - 2.7 49 | - 2.6 50 | - 2.5 51 | - 2.4 52 | - 2.3 53 | - 2.2 54 | env: 55 | - 56 | DIFF_LCS_VERSION: "> 1.4.3" 57 | include: 58 | - ruby: ruby-head 59 | env: 60 | RUBY_HEAD: true 61 | - ruby: jruby-9.2.13.0 62 | env: 63 | JRUBY_OPTS: "--dev" 64 | - ruby: 2.7 65 | name_extra: "with diff-lcs 1.3" 66 | env: 67 | DIFF_LCS_VERSION: "~> 1.3.0" 68 | - ruby: 2.7 69 | name_extra: "with diff-lcs 1.4.3" 70 | env: 71 | DIFF_LCS_VERSION: "1.4.3" 72 | fail-fast: false 73 | continue-on-error: ${{ matrix.allow_failure || endsWith(matrix.ruby, 'head') }} 74 | env: ${{ matrix.env }} 75 | steps: 76 | - uses: actions/checkout@v4 77 | - uses: ruby/setup-ruby@v1 78 | with: 79 | bundler: ${{ matrix.bundler || '2.2.22' }} 80 | ruby-version: ${{ matrix.ruby }} 81 | - run: script/update_rubygems_and_install_bundler 82 | - run: script/clone_all_rspec_repos 83 | - run: bundle install --standalone 84 | - run: bundle binstubs --all 85 | - run: script/run_build 86 | - name: Store coverage output for reference 87 | uses: actions/upload-artifact@v4 88 | if: failure() 89 | with: 90 | name: coverage-report-${{ matrix.ruby }} 91 | path: coverage/index.html 92 | retention-days: 14 93 | 94 | legacy: 95 | name: Legacy Ruby Builds (${{ matrix.container.version }}) 96 | runs-on: ubuntu-20.04 97 | container: 98 | image: ${{ matrix.container.tag }} 99 | options: ${{ matrix.container.options || '--add-host github-complains-if-this-is-empty.com:127.0.0.1' }} 100 | strategy: 101 | fail-fast: false 102 | matrix: 103 | container: 104 | - version: "2.1.9" 105 | tag: ghcr.io/rspec/docker-ci:2.1.9 106 | post: git config --global --add safe.directory `pwd` 107 | - version: "2.0" 108 | tag: ghcr.io/rspec/docker-ci:2.0.0 109 | - version: "1.9.3" 110 | tag: ghcr.io/rspec/docker-ci:1.9.3 111 | - version: "1.9.2" 112 | tag: ghcr.io/rspec/docker-ci:1.9.2 113 | options: "--add-host rubygems.org:151.101.129.227 --add-host api.rubygems.org:151.101.129.227" 114 | - version: "1.8.7" 115 | tag: ghcr.io/rspec/docker-ci:1.8.7 116 | options: "--add-host rubygems.org:151.101.129.227 --add-host api.rubygems.org:151.101.129.227" 117 | - version: "REE" 118 | tag: ghcr.io/rspec/docker-ci:ree 119 | options: "--add-host rubygems.org:151.101.129.227 --add-host api.rubygems.org:151.101.129.227" 120 | - version: "JRuby 1.7" 121 | tag: ghcr.io/rspec/docker-ci:jruby-1.7 122 | - version: "JRuby 1.7 1.8 mode" 123 | tag: ghcr.io/rspec/docker-ci:jruby-1.7 124 | jruby_opts: '--dev --1.8' 125 | pre: gem uninstall jruby-openssl 126 | options: "--add-host rubygems.org:151.101.129.227 --add-host api.rubygems.org:151.101.129.227" 127 | - version: "JRuby 9.1.17.0" 128 | tag: ghcr.io/rspec/docker-ci:jruby-9.1.17.0 129 | options: "--add-host rubygems.org:151.101.129.227 --add-host api.rubygems.org:151.101.129.227" 130 | env: 131 | ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true 132 | LEGACY_CI: true 133 | JRUBY_OPTS: ${{ matrix.container.jruby_opts || '--dev' }} 134 | NO_COVERAGE: true 135 | steps: 136 | - uses: actions/checkout@v3 137 | - run: ${{ matrix.container.pre }} 138 | - run: script/legacy_setup.sh 139 | - run: ${{ matrix.container.post }} 140 | - run: bundle exec bin/rspec 141 | - run: bundle exec script/cucumber.sh 142 | 143 | windows: 144 | name: Ruby ${{ matrix.ruby }} (Windows) 145 | runs-on: windows-latest 146 | strategy: 147 | matrix: 148 | ruby: 149 | - 2.7 150 | - 2.6 151 | - 2.5 152 | - 2.4 153 | - 2.3 154 | - 2.2 155 | fail-fast: false 156 | steps: 157 | - uses: actions/checkout@v4 158 | - uses: ruby/setup-ruby@v1 159 | with: 160 | bundler: '2.2.22' 161 | ruby-version: ${{ matrix.ruby }} 162 | bundler-cache: true 163 | - run: choco install ansicon 164 | - run: bundle exec rspec --backtrace 165 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | bin 19 | bundle 20 | 21 | Gemfile-custom 22 | spec/examples.txt 23 | specs.out 24 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --order random 2 | --color 3 | --warnings 4 | -r spec_helper 5 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - .rubocop_rspec_base.yml 3 | - .rubocop_todo.yml 4 | 5 | # Over time we'd like to get this down, but this is what we're at now. 6 | Metrics/AbcSize: 7 | Max: 28 8 | 9 | # Over time we'd like to get this down, but this is what we're at now. 10 | Metrics/BlockLength: 11 | Max: 86 12 | Exclude: 13 | - spec/**/* 14 | 15 | # Over time we'd like to get this down, but this is what we're at now. 16 | Metrics/PerceivedComplexity: 17 | Max: 10 18 | 19 | Security/MarshalLoad: 20 | Exclude: 21 | - 'lib/rspec/support/spec/in_sub_process.rb' 22 | 23 | Style/EvalWithLocation: 24 | Exclude: 25 | # eval is only used here to check syntax 26 | - 'lib/rspec/support/ruby_features.rb' 27 | - 'benchmarks/skip_frames_for_caller_filter.rb' 28 | - 'spec/rspec/support/method_signature_verifier_spec.rb' 29 | 30 | Lint/AssignmentInCondition: 31 | Exclude: 32 | # The pattern makes sense here 33 | - 'lib/rspec/support/mutex.rb' 34 | 35 | Style/FrozenStringLiteralComment: 36 | Include: 37 | - lib/**/*.rb 38 | Layout/EmptyLineAfterMagicComment: 39 | Enabled: true 40 | -------------------------------------------------------------------------------- /.rubocop_rspec_base.yml: -------------------------------------------------------------------------------- 1 | # This file was generated on 2024-09-03T15:05:50+01:00 from the rspec-dev repo. 2 | # DO NOT modify it by hand as your changes will get lost the next time it is generated. 3 | 4 | # This file contains defaults for RSpec projects. Individual projects 5 | # can customize by inheriting this file and overriding particular settings. 6 | 7 | Layout/AccessModifierIndentation: 8 | Enabled: false 9 | 10 | # "Use alias_method instead of alias" 11 | # We're fine with `alias`. 12 | Style/Alias: 13 | Enabled: false 14 | 15 | # "Avoid the use of the case equality operator ===" 16 | # We prefer using `Class#===` over `Object#is_a?` because `Class#===` 17 | # is less likely to be monkey patched than `is_a?` on a user object. 18 | Style/CaseEquality: 19 | Enabled: false 20 | 21 | # Warns when the class is excessively long. 22 | Metrics/ClassLength: 23 | Max: 100 24 | 25 | Style/CollectionMethods: 26 | PreferredMethods: 27 | reduce: 'inject' 28 | 29 | # Over time we'd like to get this down, but this is what we're at now. 30 | Metrics/CyclomaticComplexity: 31 | Max: 10 32 | 33 | # We use YARD to enforce documentation. It works better than rubocop's 34 | # enforcement...rubocop complains about the places we re-open 35 | # `RSpec::Expectations` and `RSpec::Matchers` w/o having doc commments. 36 | Style/Documentation: 37 | Enabled: false 38 | 39 | # We still support 1.8.7 which requires trailing dots 40 | Layout/DotPosition: 41 | EnforcedStyle: trailing 42 | 43 | Style/DoubleNegation: 44 | Enabled: false 45 | 46 | # each_with_object is unavailable on 1.8.7 so we have to disable this one. 47 | Style/EachWithObject: 48 | Enabled: false 49 | 50 | Style/FormatString: 51 | EnforcedStyle: percent 52 | 53 | # As long as we support ruby 1.8.7 we have to use hash rockets. 54 | Style/HashSyntax: 55 | EnforcedStyle: hash_rockets 56 | 57 | # We can't use the new lambda syntax, since we still support 1.8.7. 58 | Style/Lambda: 59 | Enabled: false 60 | 61 | # Over time we'd like to get this down, but this is what we're at now. 62 | Layout/LineLength: 63 | Max: 100 64 | 65 | # Over time we'd like to get this down, but this is what we're at now. 66 | Metrics/MethodLength: 67 | Max: 15 68 | 69 | # Who cares what we call the argument for binary operator methods? 70 | Naming/BinaryOperatorParameterName: 71 | Enabled: false 72 | 73 | Style/PercentLiteralDelimiters: 74 | PreferredDelimiters: 75 | '%': () # double-quoted string 76 | '%i': '[]' # array of symbols 77 | '%q': () # single-quoted string 78 | '%Q': () # double-quoted string 79 | '%r': '{}' # regular expression pattern 80 | '%s': () # a symbol 81 | '%w': '[]' # array of single-quoted strings 82 | '%W': '[]' # array of double-quoted strings 83 | '%x': () # a shell command as a string 84 | 85 | # We have too many special cases where we allow generator methods or prefer a 86 | # prefixed predicate due to it's improved readability. 87 | Naming/PredicateName: 88 | Enabled: false 89 | 90 | # On 1.8 `proc` is `lambda`, so we use `Proc.new` to ensure we get real procs on all supported versions. 91 | # http://batsov.com/articles/2014/02/04/the-elements-of-style-in-ruby-number-12-proc-vs-proc-dot-new/ 92 | Style/Proc: 93 | Enabled: false 94 | 95 | # Exceptions should be rescued with `Support::AllExceptionsExceptOnesWeMustNotRescue` 96 | Lint/RescueException: 97 | Enabled: true 98 | 99 | # We haven't adopted the `fail` to signal exceptions vs `raise` for re-raises convention. 100 | Style/SignalException: 101 | Enabled: false 102 | 103 | # We've tended to use no space, so it's less of a change to stick with that. 104 | Layout/SpaceAroundEqualsInParameterDefault: 105 | EnforcedStyle: no_space 106 | 107 | # We don't care about single vs double qoutes. 108 | Style/StringLiterals: 109 | Enabled: false 110 | 111 | # This rule favors constant names from the English standard library which we don't load. 112 | Style/SpecialGlobalVars: 113 | Enabled: false 114 | 115 | Style/TrailingCommaInArrayLiteral: 116 | Enabled: false 117 | 118 | Style/TrailingCommaInHashLiteral: 119 | Enabled: false 120 | 121 | Style/TrailingCommaInArguments: 122 | Enabled: false 123 | 124 | Style/TrivialAccessors: 125 | AllowDSLWriters: true 126 | AllowPredicates: true 127 | ExactNameMatch: true 128 | 129 | Style/ParallelAssignment: 130 | Enabled: false 131 | 132 | Layout/EmptyLineBetweenDefs: 133 | Enabled: false 134 | 135 | Layout/FirstParameterIndentation: 136 | Enabled: false 137 | 138 | Layout/ParameterAlignment: 139 | EnforcedStyle: with_first_parameter 140 | 141 | Layout/SpaceInsideBlockBraces: 142 | Enabled: false 143 | 144 | Layout/SpaceInsideParens: 145 | Enabled: false 146 | 147 | Naming/ConstantName: 148 | Enabled: false 149 | 150 | Style/ClassCheck: 151 | Enabled: false 152 | 153 | Style/ConditionalAssignment: 154 | Enabled: false 155 | 156 | Style/EmptyMethod: 157 | Enabled: false 158 | 159 | Style/FormatStringToken: 160 | Enabled: false 161 | 162 | Style/GuardClause: 163 | Enabled: false 164 | 165 | Style/IdenticalConditionalBranches: 166 | Enabled: false 167 | 168 | Style/IfUnlessModifier: 169 | Enabled: false 170 | 171 | Style/IfUnlessModifierOfIfUnless: 172 | Enabled: false 173 | 174 | Lint/MissingSuper: 175 | Enabled: false 176 | 177 | Style/MissingRespondToMissing: 178 | Enabled: false 179 | 180 | Style/MixinUsage: 181 | Enabled: false 182 | 183 | Style/MultipleComparison: 184 | Enabled: false 185 | 186 | Style/MutableConstant: 187 | Enabled: false 188 | 189 | Style/NestedModifier: 190 | Enabled: false 191 | 192 | Style/NestedParenthesizedCalls: 193 | Enabled: false 194 | 195 | Style/NumericPredicate: 196 | Enabled: false 197 | 198 | Style/RedundantParentheses: 199 | Enabled: false 200 | 201 | Style/StringLiteralsInInterpolation: 202 | Enabled: false 203 | 204 | Style/SymbolArray: 205 | Enabled: false 206 | 207 | Style/SymbolProc: 208 | Enabled: false 209 | 210 | Style/YodaCondition: 211 | Enabled: false 212 | 213 | Style/ZeroLengthPredicate: 214 | Enabled: false 215 | 216 | Layout/ClosingParenthesisIndentation: 217 | Enabled: false 218 | 219 | Layout/ExtraSpacing: 220 | Enabled: false 221 | 222 | Layout/MultilineMethodCallBraceLayout: 223 | Enabled: false 224 | 225 | Layout/MultilineMethodCallIndentation: 226 | Enabled: false 227 | 228 | Layout/MultilineOperationIndentation: 229 | Enabled: false 230 | 231 | Layout/SpaceAroundBlockParameters: 232 | Enabled: false 233 | 234 | Layout/SpaceAroundOperators: 235 | Enabled: false 236 | 237 | Layout/SpaceBeforeComma: 238 | Enabled: false 239 | 240 | Style/BlockDelimiters: 241 | Enabled: false 242 | 243 | Style/EmptyCaseCondition: 244 | Enabled: false 245 | 246 | Style/MultilineIfModifier: 247 | Enabled: false 248 | 249 | Style/RescueStandardError: 250 | Enabled: false 251 | 252 | Style/StderrPuts: 253 | Enabled: false 254 | 255 | Style/TernaryParentheses: 256 | Enabled: false 257 | 258 | Naming/HeredocDelimiterNaming: 259 | Enabled: false 260 | 261 | Layout/AssignmentIndentation: 262 | Enabled: false 263 | 264 | Layout/EmptyLineAfterMagicComment: 265 | Enabled: false 266 | 267 | Layout/FirstArrayElementIndentation: 268 | Enabled: false 269 | 270 | Layout/HeredocIndentation: 271 | Enabled: false 272 | 273 | Layout/SpaceInsidePercentLiteralDelimiters: 274 | Enabled: false 275 | 276 | Style/EmptyElse: 277 | Enabled: false 278 | 279 | Style/IfInsideElse: 280 | Enabled: false 281 | 282 | Style/RedundantReturn: 283 | Enabled: false 284 | 285 | Style/StructInheritance: 286 | Enabled: false 287 | 288 | Naming/VariableNumber: 289 | Enabled: false 290 | 291 | Layout/SpaceInsideStringInterpolation: 292 | Enabled: false 293 | 294 | Style/DateTime: 295 | Enabled: false 296 | 297 | Style/ParenthesesAroundCondition: 298 | Enabled: false 299 | 300 | Layout/EmptyLinesAroundBlockBody: 301 | Enabled: false 302 | 303 | Lint/ImplicitStringConcatenation: 304 | Enabled: false 305 | 306 | Lint/NestedMethodDefinition: 307 | Enabled: false 308 | 309 | Style/RegexpLiteral: 310 | Enabled: false 311 | 312 | Style/TrailingUnderscoreVariable: 313 | Enabled: false 314 | 315 | Layout/EmptyLinesAroundAccessModifier: 316 | Enabled: false 317 | -------------------------------------------------------------------------------- /BUILD_DETAIL.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # The CI build, in detail 7 | 8 | The [Travis CI build](https://travis-ci.org/rspec/rspec-support) 9 | runs many verification steps to prevent regressions and 10 | ensure high-quality code. To run the Travis build locally, run: 11 | 12 | ``` 13 | $ script/run_build 14 | ``` 15 | 16 | It can be useful to run the build steps individually 17 | to repro a failing part of a Travis build. Let's break 18 | the build down into the individual steps. 19 | 20 | ## Specs 21 | 22 | RSpec dogfoods itself. Its primary defense against regressions is its spec suite. Run with: 23 | 24 | ``` 25 | $ bundle exec rspec 26 | 27 | # or, if you installed your bundle with `--standalone --binstubs`: 28 | 29 | $ bin/rspec 30 | ``` 31 | 32 | The spec suite performs a couple extra checks that are worth noting: 33 | 34 | * *That all the code is warning-free.* Any individual example that produces output 35 | to `stderr` will fail. We also have a spec that loads all the `lib` and `spec` 36 | files in a newly spawned process to detect load-time warnings and fail if there 37 | are any. RSpec must be warning-free so that users who enable Ruby warnings will 38 | not get warnings from our code. 39 | * *That only a minimal set of stdlibs are loaded.* Since Ruby makes loaded libraries 40 | available for use in any context, we want to minimize how many bits of the standard 41 | library we load and use. Otherwise, RSpec's use of part of the standard library could 42 | mask a problem where a gem author forgets to load a part of the standard library they 43 | rely on. The spec suite contains a spec that defines a list of allowed loaded 44 | stdlibs. 45 | 46 | In addition, we use [SimpleCov](https://github.com/colszowka/simplecov) 47 | to measure and enforce test coverage. If the coverage falls below a 48 | project-specific threshold, the build will fail. 49 | 50 | ## Cukes 51 | 52 | RSpec uses [cucumber](https://cucumber.io/) for both acceptance testing 53 | and [documentation](https://rspec.info/documentation). Since we publish our cukes 54 | as documentation, please limit new cucumber scenarios to user-facing examples 55 | that help demonstrate usage. Any tests that exist purely to prevent regressions 56 | should be written as specs, even if they are written in an acceptance style. 57 | Duplication between our YARD API docs and the cucumber documentation is fine. 58 | 59 | Run with: 60 | 61 | ``` 62 | $ bundle exec cucumber 63 | 64 | # or, if you installed your bundle with `--standalone --binstubs`: 65 | 66 | $ bin/cucumber 67 | ``` 68 | 69 | ## YARD documentation 70 | 71 | RSpec uses [YARD](https://yardoc.org/) for API documentation on the [rspec.info site](https://rspec.info/). 72 | Our commitment to [SemVer](https://semver.org) requires that we explicitly 73 | declare our public API, and our build uses YARD to ensure that every 74 | class, module and method has either been labeled `@private` or has at 75 | least some level of documentation. For new APIs, this forces us to make 76 | an intentional decision about whether or not it should be part of 77 | RSpec's public API or not. 78 | 79 | To run the YARD documentation coverage check, run: 80 | 81 | ``` 82 | $ bundle exec yard stats --list-undoc 83 | 84 | # or, if you installed your bundle with `--standalone --binstubs`: 85 | 86 | $ bin/yard stats --list-undoc 87 | ``` 88 | 89 | We also want to prevent YARD errors or warnings when actually generating 90 | the docs. To check for those, run: 91 | 92 | ``` 93 | $ bundle exec yard doc --no-cache 94 | 95 | # or, if you installed your bundle with `--standalone --binstubs`: 96 | 97 | $ bin/yard doc --no-cache 98 | ``` 99 | 100 | ## RuboCop 101 | 102 | We use [RuboCop](https://github.com/rubocop-hq/rubocop) to enforce style 103 | conventions on the project so that the code has stylistic consistency 104 | throughout. Run with: 105 | 106 | ``` 107 | $ bundle exec rubocop lib 108 | 109 | # or, if you installed your bundle with `--standalone --binstubs`: 110 | 111 | $ bin/rubocop lib 112 | ``` 113 | 114 | Our RuboCop configuration is a work-in-progress, so if you get a failure 115 | due to a RuboCop default, feel free to ask about changing the 116 | configuration. Otherwise, you'll need to address the RuboCop failure, 117 | or, as a measure of last resort, by wrapping the offending code in 118 | comments like `# rubocop:disable SomeCheck` and `# rubocop:enable SomeCheck`. 119 | 120 | ## Run spec files one-by-one 121 | 122 | A fast TDD cycle depends upon being able to run a single spec file, 123 | without the rest of the test suite. While rare, it's fairly easy to 124 | create a situation where a spec passes when the entire suite runs 125 | but fails when its individual file is run. To guard against this, 126 | our CI build runs each spec file individually, using a bit of bash like: 127 | 128 | ``` 129 | for file in `find spec -iname '*_spec.rb'`; do 130 | echo "Running $file" 131 | bin/rspec $file -b --format progress 132 | done 133 | ``` 134 | 135 | Since this step boots RSpec so many times, it runs much, much 136 | faster when we can avoid the overhead of bundler. This is a main reason our 137 | CI build installs the bundle with `--standalone --binstubs` and 138 | runs RSpec via `bin/rspec` rather than `bundle exec rspec`. 139 | 140 | ## Running the spec suite for each of the other repos 141 | 142 | While each of the RSpec repos is an independent gem (generally designed 143 | to be usable on its own), there are interdependencies between the gems, 144 | and the specs for each tend to use features from the other gems. We 145 | don't want to merge a pull request for one repo that might break the 146 | build for another repo, so our CI build includes a spec that runs the 147 | spec suite of each of the _other_ project repos. Note that we only run 148 | the spec suite, not the full build, of the other projects, as the spec 149 | suite runs very quickly compared to the full build. 150 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Contributor Code of Conduct 7 | 8 | For the purpose of building a welcoming, harassment-free community that 9 | values contributions from anyone, the RSpec project has adopted the 10 | following code of conduct. All contributors and participants (including 11 | maintainers!) are expected to abide by its terms. 12 | 13 | As contributors and maintainers of this project, and in the interest of 14 | fostering an open and welcoming community, we pledge to respect all people who 15 | contribute through reporting issues, posting feature requests, updating 16 | documentation, submitting pull requests or patches, and other activities. 17 | 18 | We are committed to making participation in this project a harassment-free 19 | experience for everyone, regardless of level of experience, gender, gender 20 | identity and expression, sexual orientation, disability, personal appearance, 21 | body size, race, ethnicity, age, religion, or nationality. 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery 26 | * Personal attacks 27 | * Trolling or insulting/derogatory comments 28 | * Public or private harassment 29 | * Publishing other's private information, such as physical or electronic 30 | addresses, without explicit permission 31 | * Other unethical or unprofessional conduct 32 | 33 | Project maintainers have the right and responsibility to remove, edit, or 34 | reject comments, commits, code, wiki edits, issues, and other contributions 35 | that are not aligned to this Code of Conduct, or to ban temporarily or 36 | permanently any contributor for other behaviors that they deem inappropriate, 37 | threatening, offensive, or harmful. 38 | 39 | By adopting this Code of Conduct, project maintainers commit themselves to 40 | fairly and consistently applying these principles to every aspect of managing 41 | this project. Project maintainers who do not follow or enforce the Code of 42 | Conduct may be permanently removed from the project team. 43 | 44 | This Code of Conduct applies both within project spaces and in public spaces 45 | when an individual is representing the project or its community. 46 | 47 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 48 | reported by contacting one of the project maintainers listed at 49 | https://rspec.info/about/. All complaints will be reviewed and investigated 50 | and will result in a response that is deemed necessary and appropriate to the 51 | circumstances. Maintainers are obligated to maintain confidentiality with 52 | regard to the reporter of an incident. 53 | 54 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 55 | version 1.3.0, available at 56 | [https://contributor-covenant.org/version/1/3/0/][version] 57 | 58 | [homepage]: https://contributor-covenant.org 59 | [version]: https://contributor-covenant.org/version/1/3/0/ 60 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Contributing 7 | 8 | RSpec is a community-driven project that has benefited from improvements from over *500* contributors. 9 | We welcome contributions from *everyone*. While contributing, please follow the project [code of conduct](CODE_OF_CONDUCT.md), so that everyone can be included. 10 | 11 | If you'd like to help make RSpec better, here are some ways you can contribute: 12 | 13 | - by running RSpec HEAD to help us catch bugs before new releases 14 | - by [reporting bugs you encounter](https://github.com/rspec/rspec-support/issues/new) with [report template](#report-template) 15 | - by [suggesting new features](https://github.com/rspec/rspec-support/issues/new) 16 | - by improving RSpec's Feature or API [documentation](https://rspec.info/documentation/) 17 | - by improving [RSpec's website](https://rspec.info/) ([source](https://github.com/rspec/rspec.github.io)) 18 | - by taking part in [feature and issue discussions](https://github.com/rspec/rspec-support/issues) 19 | - by adding a failing test for reproducible [reported bugs](https://github.com/rspec/rspec-support/issues) 20 | - by reviewing [pull requests](https://github.com/rspec/rspec-support/pulls) and suggesting improvements 21 | - by [writing code](DEVELOPMENT.md) (no patch is too small! fix typos or bad whitespace) 22 | 23 | If you need help getting started, check out the [DEVELOPMENT](DEVELOPMENT.md) file for steps that will get you up and running. 24 | 25 | Thanks for helping us make RSpec better! 26 | 27 | ## `Small` issues 28 | 29 | These issue are ones that we be believe are best suited for new contributors to 30 | get started with. They represent a meaningful contribution to the project that 31 | should not be too hard to pull off. 32 | 33 | ## Report template 34 | 35 | Having a way to reproduce your issue will be very helpful for others to help confirm, 36 | investigate and ultimately fix your issue. You can do this by providing an executable 37 | test case. To make this process easier, we have prepared one basic 38 | [bug report templates](REPORT_TEMPLATE.md) for you to use as a starting point. 39 | 40 | ## Maintenance branches 41 | 42 | Maintenance branches are how we manage the different supported point releases 43 | of RSpec. As such, while they might look like good candidates to merge into 44 | main, please do not open pull requests to merge them. 45 | 46 | ## Working on multiple RSpec gems at the same time 47 | 48 | RSpec is composed of multiple gems (`rspec-core`, `rspec-mocks`, etc). Sometimes you have 49 | to work on a combination of them at the same time. When submitting your code for review, 50 | we ask that you get a passing build (green CI). If you are working across the repositories, 51 | please add a commit that temporarily pins your PR to the right branch of the other repository 52 | you depend on. For example, if we wanted a change in `rspec-expectations` that relied on a 53 | change for on `rspec-mocks`. We add a commit with the title: 54 | 55 | >[WIP] Use rspec-mocks with "custom-failure-message" branch 56 | 57 | And content: 58 | 59 | ```diff 60 | diff --git a/Gemfile b/Gemfile 61 | 62 | -%w[rspec rspec-core rspec-mocks rspec-support].each do |lib| 63 | +%w[rspec rspec-core rspec-support].each do |lib| 64 | library_path = File.expand_path("../../#{lib}", __FILE__) 65 | if File.exist?(library_path) && !ENV['USE_GIT_REPOS'] 66 | gem lib, :path => library_path 67 | @@ -11,6 +11,7 @@ branch = File.read(File.expand_path("../maintenance-branch", __FILE__)).chomp 68 | gem lib, :git => "https://github.com/rspec/#{lib}.git", :branch => branch 69 | end 70 | end 71 | +gem 'rspec-mocks', :git => "https://github.com/rspec/rspec-mocks.git", :branch => "custom-failure-message" 72 | ``` 73 | 74 | In general the process is: 75 | 1. Create PRs explaining what you are trying to achieve. 76 | 2. Pin the repositories to each other. 77 | 3. Check they pass (go green). 78 | 4. Await review if appropriate. 79 | 5. Remove the commit from step 2. We will merge ignoring the failure. 80 | 6. Remove the commit from the other, check it passes with the other commit now on `main`. 81 | 7. Merge the other. 82 | 8. We will trigger builds for the `main` branch of affected repositories to check if everything is in order. 83 | 84 | Steps 5-8 should happen continuously (e.g. one after another but within a short timespan) 85 | so that we don't leave a broken main around. It is important to triage that build process 86 | and revert if necessary. 87 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Development Setup 7 | 8 | Generally speaking, you only need to clone the project and install 9 | the dependencies with [Bundler](https://bundler.io/). You can either 10 | get a full RSpec development environment using 11 | [rspec-dev](https://github.com/rspec/rspec-dev#README) or you can 12 | set this project up individually. 13 | 14 | ## Setting up rspec-support individually 15 | 16 | For most contributors, setting up the project individually will be simpler. 17 | Unless you have a specific reason to use rspec-dev, we recommend using this approach. 18 | 19 | Clone the repo: 20 | 21 | ``` 22 | $ git clone git@github.com:rspec/rspec-support.git 23 | ``` 24 | 25 | Install the dependencies using [Bundler](https://bundler.io/): 26 | 27 | ``` 28 | $ cd rspec-support 29 | $ bundle install 30 | ``` 31 | 32 | To minimize boot time and to ensure we don't depend upon any extra dependencies 33 | loaded by Bundler, our CI builds avoid loading Bundler at runtime 34 | by using Bundler's [`--standalone option`](https://myronmars.to/n/dev-blog/2012/03/faster-test-boot-times-with-bundler-standalone). 35 | While not strictly necessary (many/most of our contributors do not do this!), 36 | if you want to exactly reproduce our CI builds you'll want to do the same: 37 | 38 | ``` 39 | $ bundle install --standalone --binstubs 40 | ``` 41 | 42 | The `--binstubs` option creates the `bin/rspec` file that, like `bundle exec rspec`, will load 43 | all the versions specified in `Gemfile.lock` without loading bundler at runtime! 44 | 45 | ## Using rspec-dev 46 | 47 | See the [rspec-dev README](https://github.com/rspec/rspec-dev#README) 48 | for setup instructions. 49 | 50 | The rspec-dev project contains many rake tasks for helping manage 51 | an RSpec development environment, making it easy to do things like: 52 | 53 | * Change branches across all repos 54 | * Update all repos with the latest code from `main` 55 | * Cut a new release across all repos 56 | * Push out updated build scripts to all repos 57 | 58 | These sorts of tasks are essential for the RSpec maintainers but will 59 | probably be unnecessary complexity if you're just contributing to one 60 | repository. If you are getting setup to make your first contribution, 61 | we recommend you take the simpler route of setting up rspec-support 62 | individually. 63 | 64 | ## Gotcha: Version mismatch from sibling repos 65 | 66 | The [Gemfile](Gemfile) is designed to be flexible and support using 67 | the other RSpec repositories either from a local sibling directory 68 | (e.g. `../rspec-`) or, if there is no such directory, 69 | directly from git. This generally does the "right thing", but can 70 | be a gotcha in some situations. For example, if you are setting up 71 | `rspec-core`, and you happen to have an old clone of `rspec-expectations` 72 | in a sibling directory, it'll be used even though it might be months or 73 | years out of date, which can cause confusing failures. 74 | 75 | To avoid this problem, you can either `export USE_GIT_REPOS=1` to force 76 | the use of `:git` dependencies instead of local dependencies, or update 77 | the code in the sibling directory. rspec-dev contains rake tasks to 78 | help you keep all repos in sync. 79 | 80 | ## Extra Gems 81 | 82 | If you need additional gems for any tasks---such as `benchmark-ips` for benchmarking 83 | or `byebug` for debugging---you can create a `Gemfile-custom` file containing those 84 | gem declarations. The `Gemfile` evaluates that file if it exists, and it is git-ignored. 85 | 86 | # Running the build 87 | 88 | The [Travis CI build](https://travis-ci.org/rspec/rspec-support) 89 | runs many verification steps to prevent regressions and 90 | ensure high-quality code. To run the Travis build locally, run: 91 | 92 | ``` 93 | $ script/run_build 94 | ``` 95 | 96 | See [build detail](BUILD_DETAIL.md) for more detail. 97 | 98 | # What to Expect 99 | 100 | To ensure high, uniform code quality, all code changes (including 101 | changes from the maintainers!) are subject to a pull request code 102 | review. We'll often ask for clarification or suggest alternate ways 103 | to do things. Our code reviews are intended to be a two-way 104 | conversation. 105 | 106 | Here's a short, non-exhaustive checklist of things we typically ask contributors to do before PRs are ready to merge. It can help get your PR merged faster if you do these in advance! 107 | 108 | - [ ] New behavior is covered by tests and all tests are passing. 109 | - [ ] No Ruby warnings are issued by your changes. 110 | - [ ] Documentation reflects changes and renders as intended. 111 | - [ ] RuboCop passes (e.g. `bundle exec rubocop lib`). 112 | - [ ] Commits are squashed into a reasonable number of logical changesets that tell an easy-to-follow story. 113 | - [ ] No changelog entry is necessary (we'll add it as part of the merge process!) 114 | 115 | # Adding Docs 116 | 117 | RSpec uses [YARD](https://yardoc.org/) for its API documentation. To 118 | ensure the docs render well, we recommend running a YARD server and 119 | viewing your edits in a browser. 120 | 121 | To run a YARD server: 122 | 123 | ``` 124 | $ bundle exec yard server --reload 125 | 126 | # or, if you installed your bundle with `--standalone --binstubs`: 127 | 128 | $ bin/yard server --reload 129 | ``` 130 | 131 | Then navigate to `localhost:8808` to view the rendered docs. 132 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in rspec-support.gemspec 4 | gemspec 5 | 6 | branch = File.read(File.expand_path("../maintenance-branch", __FILE__)).chomp 7 | %w[rspec rspec-core rspec-expectations rspec-mocks].each do |lib| 8 | library_path = File.expand_path("../../#{lib}", __FILE__) 9 | if File.exist?(library_path) && !ENV['USE_GIT_REPOS'] 10 | gem lib, :path => library_path 11 | else 12 | if lib == 'rspec' 13 | gem 'rspec', :git => "https://github.com/rspec/rspec-metagem.git", :branch => branch 14 | else 15 | gem lib, :git => "https://github.com/rspec/#{lib}.git", :branch => branch 16 | end 17 | end 18 | end 19 | 20 | if RUBY_VERSION < '1.9.3' 21 | gem 'rake', '< 11.0.0' # rake 11 requires Ruby 1.9.3 or later 22 | elsif RUBY_VERSION < '2.0.0' 23 | gem 'rake', '< 12.0.0' # rake 12 requires Ruby 2.0.0 or later 24 | else 25 | gem 'rake', '>= 12.3.3' 26 | end 27 | 28 | if ENV['DIFF_LCS_VERSION'] 29 | gem 'diff-lcs', ENV['DIFF_LCS_VERSION'] 30 | else 31 | gem 'diff-lcs', '~> 1.4', '>= 1.4.3' 32 | end 33 | 34 | if RUBY_VERSION >= '3.3.0' 35 | # This is being extracted in Ruby 3.4 and issues a warning on 3.3 36 | gem 'bigdecimal', :require => false 37 | end 38 | 39 | if RUBY_VERSION < '2.3.0' && !!(RbConfig::CONFIG['host_os'] =~ /cygwin|mswin|mingw|bccwin|wince|emx/) 40 | gem "childprocess", "< 1.0.0" 41 | elsif RUBY_VERSION < '2.0.0' 42 | gem "childprocess", "< 1.0.0" 43 | elsif RUBY_VERSION < '2.3.0' 44 | gem "childprocess", "< 3.0.0" 45 | else 46 | gem "childprocess", ">= 3.0.0" 47 | end 48 | 49 | group :coverage do 50 | ### dep for ci/coverage 51 | gem 'simplecov', '~> 0.8' 52 | end 53 | 54 | if RUBY_VERSION < '2.0.0' || RUBY_ENGINE == 'java' 55 | gem 'json', '< 2.0.0' # is a dependency of simplecov 56 | else 57 | gem 'json', '> 2.3.0' 58 | end 59 | 60 | if RUBY_VERSION < '2.2.0' && !!(RbConfig::CONFIG['host_os'] =~ /cygwin|mswin|mingw|bccwin|wince|emx/) 61 | gem 'ffi', '< 1.10' 62 | elsif RUBY_VERSION < '2.4.0' && !!(RbConfig::CONFIG['host_os'] =~ /cygwin|mswin|mingw|bccwin|wince|emx/) 63 | gem 'ffi', '< 1.15' 64 | elsif RUBY_VERSION < '2.0' 65 | # ffi dropped Ruby 1.8 support in 1.9.19 and Ruby 1.9 support in 1.11.0 66 | gem 'ffi', '< 1.9.19' 67 | elsif RUBY_VERSION < '2.3.0' 68 | gem 'ffi', '~> 1.12.0' 69 | else 70 | gem 'ffi', '~> 1.13.0' 71 | end 72 | 73 | # No need to run rubocop on earlier versions 74 | if RUBY_VERSION >= '2.4' && RUBY_ENGINE == 'ruby' 75 | gem 'rubocop', "~> 1.0", "< 1.12" 76 | end 77 | 78 | eval File.read('Gemfile-custom') if File.exist?('Gemfile-custom') 79 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | ### Subject of the issue 7 | 10 | 11 | ### Your environment 12 | * Ruby version: 13 | * rspec-support version: 14 | 15 | ### Steps to reproduce 16 | 20 | 21 | ### Expected behavior 22 | 25 | 26 | ### Actual behavior 27 | 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ==================== 3 | 4 | * Copyright © 2013 David Chelimsky, Myron Marston, Jon Rowe, Sam Phippen, Xavier Shay, Bradley Schaefer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RSpec::Support [![Build Status](https://github.com/rspec/rspec-support/workflows/RSpec%20CI/badge.svg)](https://github.com/rspec/rspec-support/actions) 2 | 3 | **This is the old rspec support repository, please see the monorepo rspec/rspec for new issues and releases.** 4 | 5 | `RSpec::Support` provides common functionality to `RSpec::Core`, 6 | `RSpec::Expectations` and `RSpec::Mocks`. It is considered 7 | suitable for internal use only at this time. 8 | 9 | ## Installation / Usage 10 | 11 | Install one or more of the `RSpec` gems. 12 | 13 | Want to run against the `main` branch? You'll need to include the dependent 14 | RSpec repos as well. Add the following to your `Gemfile`: 15 | 16 | ```ruby 17 | %w[rspec-core rspec-expectations rspec-mocks rspec-support].each do |lib| 18 | gem lib, :git => "https://github.com/rspec/#{lib}.git", :branch => 'main' 19 | end 20 | ``` 21 | 22 | ## Contributing 23 | 24 | Once you've set up the environment, you'll need to cd into the working 25 | directory of whichever repo you want to work in. From there you can run the 26 | specs and cucumber features, and make patches. 27 | 28 | NOTE: You do not need to use rspec-dev to work on a specific RSpec repo. You 29 | can treat each RSpec repo as an independent project. 30 | 31 | - [Build details](BUILD_DETAIL.md) 32 | - [Code of Conduct](CODE_OF_CONDUCT.md) 33 | - [Detailed contributing guide](CONTRIBUTING.md) 34 | - [Development setup guide](DEVELOPMENT.md) 35 | 36 | ## Patches 37 | 38 | Please submit a pull request or a github issue. If you submit an issue, please 39 | include a link to either of: 40 | 41 | * a gist (or equivalent) of the patch 42 | * a branch or commit in your github fork of the repo 43 | -------------------------------------------------------------------------------- /REPORT_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Report template 7 | 8 | ```ruby 9 | # frozen_string_literal: true 10 | 11 | begin 12 | require "bundler/inline" 13 | rescue LoadError => e 14 | $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" 15 | raise e 16 | end 17 | 18 | gemfile(true) do 19 | source "https://rubygems.org" 20 | 21 | gem "rspec", "3.7.0" # Activate the gem and version you are reporting the issue against. 22 | end 23 | 24 | puts "Ruby version is: #{RUBY_VERSION}" 25 | require 'rspec/autorun' 26 | 27 | RSpec.describe 'additions' do 28 | it 'returns 2' do 29 | expect(1 + 1).to eq(2) 30 | end 31 | 32 | it 'returns 1' do 33 | expect(3 - 1).to eq(-1) 34 | end 35 | end 36 | ``` 37 | 38 | Simply copy the content of the appropriate template into a `.rb` file on your computer 39 | and make the necessary changes to demonstrate the issue. You can execute it by running 40 | `ruby rspec_report.rb` in your terminal. 41 | 42 | You can then share your executable test case as a [gist](https://gist.github.com), or 43 | simply paste the content into the issue description. 44 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler" 2 | Bundler.setup 3 | Bundler::GemHelper.install_tasks 4 | 5 | require "rake" 6 | 7 | require "rspec/core/rake_task" 8 | require "rspec/core/version" 9 | 10 | desc "Run all examples" 11 | RSpec::Core::RakeTask.new(:spec) do |t| 12 | t.ruby_opts = %w[-w] 13 | end 14 | 15 | task :default => [:spec] 16 | 17 | task :verify_private_key_present do 18 | private_key = File.expand_path('~/.gem/rspec-gem-private_key.pem') 19 | unless File.exist?(private_key) 20 | raise "Your private key is not present. This gem should not be built without it." 21 | end 22 | end 23 | 24 | task :build => :verify_private_key_present 25 | 26 | begin 27 | require 'rubocop/rake_task' 28 | desc 'Run RuboCop on the lib directory' 29 | RuboCop::RakeTask.new(:rubocop) do |task| 30 | task.patterns = ['lib/**/*.rb'] 31 | end 32 | rescue LoadError 33 | # No rubocop means no rubocop rake task 34 | end 35 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security contact information 2 | 3 | To report a security vulnerability, please use the 4 | [Tidelift security contact](https://tidelift.com/security). 5 | Tidelift will coordinate the fix and disclosure. 6 | -------------------------------------------------------------------------------- /benchmarks/caller.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.expand_path("../../lib", __FILE__)) 2 | 3 | require 'benchmark' 4 | require 'rspec/support/caller_filter' 5 | 6 | n = 10000 7 | 8 | puts "#{n} times - ruby #{RUBY_VERSION}" 9 | puts 10 | 11 | puts "* Using a chunked fetch is quicker than the old method of array-access." 12 | Benchmark.bm(20) do |bm| 13 | bm.report("CallerFilter") do 14 | n.times do 15 | RSpec::CallerFilter.first_non_rspec_line 16 | end 17 | end 18 | 19 | bm.report("Direct caller access") do 20 | n.times do 21 | caller(1)[4] 22 | end 23 | end 24 | end 25 | 26 | puts 27 | puts "* Chunking fetches of caller adds a ~17% overhead." 28 | Benchmark.bm(20) do |bm| 29 | bm.report("Chunked") do 30 | n.times do 31 | caller(1, 2) 32 | caller(3, 2) 33 | caller(5, 2) 34 | end 35 | end 36 | 37 | bm.report("All at once") do 38 | n.times do 39 | caller(1, 6) 40 | end 41 | end 42 | end 43 | 44 | puts 45 | puts "* `caller` scales linearly with length parameter." 46 | Benchmark.bm(20) do |bm| 47 | (1..10).each do |x| 48 | bm.report(x) do 49 | n.times do 50 | caller(1, x) 51 | end 52 | end 53 | end 54 | end 55 | 56 | 57 | # > ruby benchmarks/caller.rb 58 | # 10000 times - ruby 2.0.0 59 | # 60 | # * Using a chunked fetch is quicker than the old method of array-access. 61 | # user system total real 62 | # CallerFilter 0.140000 0.010000 0.150000 ( 0.145381) 63 | # Direct caller access 0.170000 0.000000 0.170000 ( 0.180610) 64 | # 65 | # * Chunking fetches of caller adds a ~17% overhead. 66 | # user system total real 67 | # Chunked 0.150000 0.000000 0.150000 ( 0.181162) 68 | # All at once 0.130000 0.010000 0.140000 ( 0.138732) 69 | # 70 | # * `caller` scales linearly with length parameter. 71 | # user system total real 72 | # 1 0.030000 0.000000 0.030000 ( 0.035000) 73 | # 2 0.050000 0.000000 0.050000 ( 0.059879) 74 | # 3 0.080000 0.000000 0.080000 ( 0.098468) 75 | # 4 0.090000 0.010000 0.100000 ( 0.097619) 76 | # 5 0.110000 0.000000 0.110000 ( 0.126220) 77 | # 6 0.130000 0.000000 0.130000 ( 0.136739) 78 | # 7 0.150000 0.000000 0.150000 ( 0.159055) 79 | # 8 0.160000 0.010000 0.170000 ( 0.172416) 80 | # 9 0.180000 0.000000 0.180000 ( 0.203038) 81 | # 10 0.200000 0.000000 0.200000 ( 0.210551) 82 | -------------------------------------------------------------------------------- /benchmarks/caller_vs_caller_locations.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark/ips' 2 | 3 | Benchmark.ips do |x| 4 | x.report("caller() ") { caller } 5 | x.report("caller_locations() ") { caller_locations } 6 | x.report("caller(1, 2) ") { caller(1, 2) } 7 | x.report("caller_locations(1, 2)") { caller_locations(1, 2) } 8 | end 9 | 10 | __END__ 11 | 12 | caller() 13 | 118.586k (±17.7%) i/s - 573.893k 14 | caller_locations() 15 | 355.988k (±17.8%) i/s - 1.709M 16 | caller(1, 2) 17 | 336.841k (±18.6%) i/s - 1.615M 18 | caller_locations(1, 2) 19 | 781.330k (±23.5%) i/s - 3.665M 20 | -------------------------------------------------------------------------------- /benchmarks/caller_vs_caller_locations_vs_raise.rb: -------------------------------------------------------------------------------- 1 | # This benchmark arose from rspec/rspec-support#199 where we experimented with 2 | # faster ways of generating / capturing a backtrace and whether it made sense 3 | # to lazily generate it using `raise` to capture the backtrace via an exception. 4 | # See also rspec/rspec-mocks#937 5 | 6 | require 'benchmark/ips' 7 | 8 | def use_raise_to_capture_caller 9 | use_raise_lazily.backtrace 10 | end 11 | 12 | def use_raise_lazily 13 | raise "nope" 14 | rescue StandardError => exception 15 | return exception 16 | end 17 | 18 | def create_stack_trace(n, &block) 19 | return create_stack_trace(n - 1, &block) if n > 0 20 | yield 21 | end 22 | 23 | [10, 50, 100].each do |frames| 24 | puts "-" * 80 25 | puts "With #{frames} extra stack frames" 26 | puts "-" * 80 27 | create_stack_trace(frames) do 28 | Benchmark.ips do |x| 29 | x.report("caller() ") { caller } 30 | x.report("caller_locations() ") { caller_locations } 31 | x.report("raise with backtrace ") { use_raise_to_capture_caller } 32 | x.report("raise and store (lazy)") { use_raise_lazily } 33 | x.report("caller(1, 2) ") { caller(1, 2) } 34 | x.report("caller_locations(1, 2)") { caller_locations(1, 2) } 35 | x.compare! 36 | end 37 | end 38 | end 39 | 40 | __END__ 41 | -------------------------------------------------------------------------------- 42 | With 10 extra stack frames 43 | -------------------------------------------------------------------------------- 44 | Calculating ------------------------------------- 45 | caller() 46 | 5.583k i/100ms 47 | caller_locations() 48 | 14.540k i/100ms 49 | raise with backtrace 50 | 4.544k i/100ms 51 | raise and store (lazy) 52 | 27.028k i/100ms 53 | caller(1, 2) 54 | 25.739k i/100ms 55 | caller_locations(1, 2) 56 | 48.848k i/100ms 57 | ------------------------------------------------- 58 | caller() 59 | 61.386k (±11.6%) i/s - 307.065k 60 | caller_locations() 61 | 176.033k (±12.8%) i/s - 872.400k 62 | raise with backtrace 63 | 48.348k (±10.5%) i/s - 240.832k 64 | raise and store (lazy) 65 | 425.768k (±10.7%) i/s - 2.108M 66 | caller(1, 2) 67 | 368.142k (±18.9%) i/s - 1.776M 68 | caller_locations(1, 2) 69 | 834.431k (±17.8%) i/s - 4.054M 70 | 71 | Comparison: 72 | caller_locations(1, 2): 834431.2 i/s 73 | raise and store (lazy): 425767.6 i/s - 1.96x slower 74 | caller(1, 2) : 368142.0 i/s - 2.27x slower 75 | caller_locations() : 176032.6 i/s - 4.74x slower 76 | caller() : 61386.0 i/s - 13.59x slower 77 | raise with backtrace : 48348.2 i/s - 17.26x slower 78 | 79 | -------------------------------------------------------------------------------- 80 | With 50 extra stack frames 81 | -------------------------------------------------------------------------------- 82 | Calculating ------------------------------------- 83 | caller() 84 | 2.282k i/100ms 85 | caller_locations() 86 | 6.446k i/100ms 87 | raise with backtrace 88 | 2.138k i/100ms 89 | raise and store (lazy) 90 | 23.649k i/100ms 91 | caller(1, 2) 92 | 22.113k i/100ms 93 | caller_locations(1, 2) 94 | 36.586k i/100ms 95 | ------------------------------------------------- 96 | caller() 97 | 24.105k (± 9.9%) i/s - 120.946k 98 | caller_locations() 99 | 68.610k (± 7.9%) i/s - 341.638k 100 | raise with backtrace 101 | 21.458k (± 9.6%) i/s - 106.900k 102 | raise and store (lazy) 103 | 341.152k (± 8.1%) i/s - 1.703M 104 | caller(1, 2) 105 | 297.805k (±12.5%) i/s - 1.482M 106 | caller_locations(1, 2) 107 | 557.278k (±16.6%) i/s - 2.744M 108 | 109 | Comparison: 110 | caller_locations(1, 2): 557278.2 i/s 111 | raise and store (lazy): 341151.6 i/s - 1.63x slower 112 | caller(1, 2) : 297804.8 i/s - 1.87x slower 113 | caller_locations() : 68610.3 i/s - 8.12x slower 114 | caller() : 24105.5 i/s - 23.12x slower 115 | raise with backtrace : 21458.2 i/s - 25.97x slower 116 | 117 | -------------------------------------------------------------------------------- 118 | With 100 extra stack frames 119 | -------------------------------------------------------------------------------- 120 | Calculating ------------------------------------- 121 | caller() 122 | 1.327k i/100ms 123 | caller_locations() 124 | 3.773k i/100ms 125 | raise with backtrace 126 | 1.235k i/100ms 127 | raise and store (lazy) 128 | 19.990k i/100ms 129 | caller(1, 2) 130 | 18.269k i/100ms 131 | caller_locations(1, 2) 132 | 29.668k i/100ms 133 | ------------------------------------------------- 134 | caller() 135 | 13.879k (± 9.9%) i/s - 69.004k 136 | caller_locations() 137 | 39.070k (± 7.6%) i/s - 196.196k 138 | raise with backtrace 139 | 12.703k (±12.7%) i/s - 62.985k 140 | raise and store (lazy) 141 | 243.959k (± 8.3%) i/s - 1.219M 142 | caller(1, 2) 143 | 230.289k (± 8.2%) i/s - 1.151M 144 | caller_locations(1, 2) 145 | 406.804k (± 8.8%) i/s - 2.047M 146 | 147 | Comparison: 148 | caller_locations(1, 2): 406804.3 i/s 149 | raise and store (lazy): 243958.7 i/s - 1.67x slower 150 | caller(1, 2) : 230288.9 i/s - 1.77x slower 151 | caller_locations() : 39069.8 i/s - 10.41x slower 152 | caller() : 13879.4 i/s - 29.31x slower 153 | raise with backtrace : 12702.9 i/s - 32.02x slower 154 | -------------------------------------------------------------------------------- /benchmarks/class_exec_vs_klass_exec.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark/ips' 2 | require 'rspec/support' 3 | require 'rspec/support/with_keywords_when_needed' 4 | 5 | Klass = Class.new do 6 | def test(*args, **kwargs) 7 | end 8 | end 9 | 10 | def class_exec_args 11 | Klass.class_exec(:a, :b) { } 12 | end 13 | 14 | def klass_exec_args 15 | RSpec::Support::WithKeywordsWhenNeeded.class_exec(Klass, :a, :b) { } 16 | end 17 | 18 | def class_exec_kw_args 19 | Klass.class_exec(a: :b) { |a:| } 20 | end 21 | 22 | def klass_exec_kw_args 23 | RSpec::Support::WithKeywordsWhenNeeded.class_exec(Klass, a: :b) { |a:| } 24 | end 25 | 26 | Benchmark.ips do |x| 27 | x.report("class_exec(*args) ") { class_exec_args } 28 | x.report("klass_exec(*args) ") { klass_exec_args } 29 | x.report("class_exec(*args, **kwargs)") { class_exec_kw_args } 30 | x.report("klass_exec(*args, **kwargs)") { klass_exec_kw_args } 31 | end 32 | 33 | __END__ 34 | 35 | Calculating ------------------------------------- 36 | class_exec(*args) 37 | 5.555M (± 1.6%) i/s - 27.864M in 5.017682s 38 | klass_exec(*args) 39 | 657.945k (± 4.6%) i/s - 3.315M in 5.051511s 40 | class_exec(*args, **kwargs) 41 | 2.882M (± 3.3%) i/s - 14.555M in 5.056905s 42 | klass_exec(*args, **kwargs) 43 | 52.710k (± 4.1%) i/s - 265.188k in 5.041218s 44 | -------------------------------------------------------------------------------- /benchmarks/map_hash.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark/ips' 2 | 3 | def use_map_and_hash_bracket(input) 4 | Hash[ input.map { |k, v| [k.to_s, v.to_s] } ] 5 | end 6 | 7 | def use_inject(input) 8 | input.inject({}) do |hash, (k, v)| 9 | hash[k.to_s] = v.to_s 10 | hash 11 | end 12 | end 13 | 14 | [10, 100, 1000].each do |size| 15 | hash = Hash[1.upto(size).map { |i| [i, i] }] 16 | unless use_map_and_hash_bracket(hash) == use_inject(hash) 17 | raise "Not the same!" 18 | end 19 | 20 | puts 21 | puts "A hash of #{size} pairs" 22 | 23 | Benchmark.ips do |x| 24 | x.report("Use map and Hash[]") { use_map_and_hash_bracket(hash) } 25 | x.report("Use inject") { use_inject(hash) } 26 | x.compare! 27 | end 28 | end 29 | 30 | __END__ 31 | 32 | `inject` appears to be slightly faster. 33 | 34 | A hash of 10 pairs 35 | Calculating ------------------------------------- 36 | Use map and Hash[] 8.742k i/100ms 37 | Use inject 9.565k i/100ms 38 | ------------------------------------------------- 39 | Use map and Hash[] 98.220k (± 6.8%) i/s - 489.552k 40 | Use inject 110.130k (± 6.1%) i/s - 554.770k 41 | 42 | Comparison: 43 | Use inject: 110129.9 i/s 44 | Use map and Hash[]: 98219.8 i/s - 1.12x slower 45 | 46 | 47 | A hash of 100 pairs 48 | Calculating ------------------------------------- 49 | Use map and Hash[] 1.080k i/100ms 50 | Use inject 1.124k i/100ms 51 | ------------------------------------------------- 52 | Use map and Hash[] 10.931k (± 4.5%) i/s - 55.080k 53 | Use inject 11.494k (± 5.0%) i/s - 57.324k 54 | 55 | Comparison: 56 | Use inject: 11494.4 i/s 57 | Use map and Hash[]: 10930.7 i/s - 1.05x slower 58 | 59 | 60 | A hash of 1000 pairs 61 | Calculating ------------------------------------- 62 | Use map and Hash[] 106.000 i/100ms 63 | Use inject 111.000 i/100ms 64 | ------------------------------------------------- 65 | Use map and Hash[] 1.081k (± 5.1%) i/s - 5.406k 66 | Use inject 1.111k (± 4.8%) i/s - 5.550k 67 | 68 | Comparison: 69 | Use inject: 1111.2 i/s 70 | Use map and Hash[]: 1080.8 i/s - 1.03x slower 71 | -------------------------------------------------------------------------------- /benchmarks/ripper.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark/ips' 2 | require 'ripper' 3 | 4 | ruby_version = defined?(JRUBY_VERSION) ? JRUBY_VERSION : RUBY_VERSION 5 | puts "#{RUBY_ENGINE} #{ruby_version}" 6 | 7 | source = File.read(__FILE__) 8 | 9 | Benchmark.ips do |x| 10 | x.report("Ripper") do 11 | Ripper.sexp(source) 12 | Ripper.lex(source) 13 | end 14 | end 15 | 16 | __END__ 17 | 18 | ruby 1.9.3 19 | Calculating ------------------------------------- 20 | Ripper 284.000 i/100ms 21 | 22 | ruby 2.2.3 23 | Calculating ------------------------------------- 24 | Ripper 320.000 i/100ms 25 | 26 | jruby 1.7.5 27 | Calculating ------------------------------------- 28 | Ripper 24.000 i/100ms 29 | 30 | jruby 1.7.13 31 | Calculating ------------------------------------- 32 | Ripper 25.000 i/100ms 33 | 34 | jruby 1.7.14 35 | Calculating ------------------------------------- 36 | Ripper 239.000 i/100ms 37 | 38 | jruby 1.7.22 39 | Calculating ------------------------------------- 40 | Ripper 231.000 i/100ms 41 | 42 | jruby 9.0.1.0 43 | Calculating ------------------------------------- 44 | Ripper 218.000 i/100ms 45 | -------------------------------------------------------------------------------- /benchmarks/skip_frames_for_caller_filter.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/support/caller_filter' 2 | 3 | eval <<-EOS, binding, "/lib/rspec/core/some_file.rb", 1 # make it think this code is in rspec-core 4 | class Recurser 5 | def self.recurse(times, *args) 6 | return RSpec::CallerFilter.first_non_rspec_line(*args) if times.zero? 7 | recurse(times - 1, *args) 8 | end 9 | end 10 | EOS 11 | 12 | # Ensure the correct first_non_rspec_line is found in these cases. 13 | puts Recurser.recurse(20, 19) 14 | puts Recurser.recurse(20) 15 | 16 | require 'benchmark/ips' 17 | 18 | Benchmark.ips do |x| 19 | x.report("(no args)") { Recurser.recurse(20) } 20 | x.report("(19)") { Recurser.recurse(20, 19) } 21 | x.report("(19, 2)") { Recurser.recurse(20, 19, 2) } 22 | end 23 | 24 | __END__ 25 | (no args) 13.789k (± 7.8%) i/s - 69.377k 26 | (19) 55.410k (± 8.4%) i/s - 275.123k 27 | (19, 2) 61.076k (± 8.6%) i/s - 303.637k 28 | -------------------------------------------------------------------------------- /lib/rspec/support.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RSpec 4 | module Support 5 | # @api private 6 | # 7 | # Defines a helper method that is optimized to require files from the 8 | # named lib. The passed block MUST be `{ |f| require_relative f }` 9 | # because for `require_relative` to work properly from within the named 10 | # lib the line of code must be IN that lib. 11 | # 12 | # `require_relative` is preferred when available because it is always O(1), 13 | # regardless of the number of dirs in $LOAD_PATH. `require`, on the other 14 | # hand, does a linear O(N) search over the dirs in the $LOAD_PATH until 15 | # it can resolve the file relative to one of the dirs. 16 | def self.define_optimized_require_for_rspec(lib, &require_relative) 17 | name = "require_rspec_#{lib}" 18 | 19 | if RUBY_PLATFORM == 'java' && !Kernel.respond_to?(:require) 20 | # JRuby 9.1.17.0 has developed a regression for require 21 | (class << self; self; end).__send__(:define_method, name) do |f| 22 | Kernel.send(:require, "rspec/#{lib}/#{f}") 23 | end 24 | elsif Kernel.respond_to?(:require_relative) 25 | (class << self; self; end).__send__(:define_method, name) do |f| 26 | require_relative.call("#{lib}/#{f}") 27 | end 28 | else 29 | (class << self; self; end).__send__(:define_method, name) do |f| 30 | require "rspec/#{lib}/#{f}" 31 | end 32 | end 33 | end 34 | 35 | define_optimized_require_for_rspec(:support) { |f| require_relative(f) } 36 | require_rspec_support "version" 37 | require_rspec_support "ruby_features" 38 | 39 | # @api private 40 | KERNEL_METHOD_METHOD = ::Kernel.instance_method(:method) 41 | 42 | # @api private 43 | # 44 | # Used internally to get a method handle for a particular object 45 | # and method name. 46 | # 47 | # Includes handling for a few special cases: 48 | # 49 | # - Objects that redefine #method (e.g. an HTTPRequest struct) 50 | # - BasicObject subclasses that mixin a Kernel dup (e.g. SimpleDelegator) 51 | # - Objects that undefine method and delegate everything to another 52 | # object (e.g. Mongoid association objects) 53 | if RubyFeatures.supports_rebinding_module_methods? 54 | def self.method_handle_for(object, method_name) 55 | KERNEL_METHOD_METHOD.bind(object).call(method_name) 56 | rescue NameError => original 57 | begin 58 | handle = object.method(method_name) 59 | raise original unless handle.is_a? Method 60 | handle 61 | rescue Support::AllExceptionsExceptOnesWeMustNotRescue 62 | raise original 63 | end 64 | end 65 | else 66 | def self.method_handle_for(object, method_name) 67 | if ::Kernel === object 68 | KERNEL_METHOD_METHOD.bind(object).call(method_name) 69 | else 70 | object.method(method_name) 71 | end 72 | rescue NameError => original 73 | begin 74 | handle = object.method(method_name) 75 | raise original unless handle.is_a? Method 76 | handle 77 | rescue Support::AllExceptionsExceptOnesWeMustNotRescue 78 | raise original 79 | end 80 | end 81 | end 82 | 83 | # @api private 84 | # 85 | # Used internally to get a class of a given object, even if it does not respond to #class. 86 | def self.class_of(object) 87 | object.class 88 | rescue NoMethodError 89 | singleton_class = class << object; self; end 90 | singleton_class.ancestors.find { |ancestor| !ancestor.equal?(singleton_class) } 91 | end 92 | 93 | # A single thread local variable so we don't excessively pollute that namespace. 94 | if RUBY_VERSION.to_f >= 2 95 | def self.thread_local_data 96 | Thread.current.thread_variable_get(:__rspec) || Thread.current.thread_variable_set(:__rspec, {}) 97 | end 98 | else 99 | def self.thread_local_data 100 | Thread.current[:__rspec] ||= {} 101 | end 102 | end 103 | 104 | # @api private 105 | def self.failure_notifier=(callable) 106 | thread_local_data[:failure_notifier] = callable 107 | end 108 | 109 | # @private 110 | DEFAULT_FAILURE_NOTIFIER = lambda { |failure, _opts| raise failure } 111 | 112 | # @api private 113 | def self.failure_notifier 114 | thread_local_data[:failure_notifier] || DEFAULT_FAILURE_NOTIFIER 115 | end 116 | 117 | # @api private 118 | def self.notify_failure(failure, options={}) 119 | failure_notifier.call(failure, options) 120 | end 121 | 122 | # @api private 123 | def self.with_failure_notifier(callable) 124 | orig_notifier = failure_notifier 125 | self.failure_notifier = callable 126 | yield 127 | ensure 128 | self.failure_notifier = orig_notifier 129 | end 130 | 131 | class << self 132 | # @api private 133 | attr_writer :warning_notifier 134 | end 135 | 136 | # @private 137 | DEFAULT_WARNING_NOTIFIER = lambda { |warning| ::Kernel.warn warning } 138 | 139 | # @api private 140 | def self.warning_notifier 141 | @warning_notifier ||= DEFAULT_WARNING_NOTIFIER 142 | end 143 | 144 | # @private 145 | module AllExceptionsExceptOnesWeMustNotRescue 146 | # These exceptions are dangerous to rescue as rescuing them 147 | # would interfere with things we should not interfere with. 148 | AVOID_RESCUING = [NoMemoryError, SignalException, Interrupt, SystemExit] 149 | 150 | def self.===(exception) 151 | AVOID_RESCUING.none? { |ar| ar === exception } 152 | end 153 | end 154 | 155 | # The Differ is only needed when a spec fails with a diffable failure. 156 | # In the more common case of all specs passing or the only failures being 157 | # non-diffable, we can avoid the extra cost of loading the differ, diff-lcs, 158 | # pp, etc by avoiding an unnecessary require. Instead, autoload will take 159 | # care of loading the differ on first use. 160 | autoload :Differ, "rspec/support/differ" 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /lib/rspec/support/caller_filter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec::Support.require_rspec_support "ruby_features" 4 | 5 | module RSpec 6 | # Consistent implementation for "cleaning" the caller method to strip out 7 | # non-rspec lines. This enables errors to be reported at the call site in 8 | # the code using the library, which is far more useful than the particular 9 | # internal method that raised an error. 10 | class CallerFilter 11 | RSPEC_LIBS = %w[ 12 | core 13 | mocks 14 | expectations 15 | support 16 | matchers 17 | rails 18 | ] 19 | 20 | ADDITIONAL_TOP_LEVEL_FILES = %w[ autorun ] 21 | 22 | LIB_REGEX = %r{/lib/rspec/(#{(RSPEC_LIBS + ADDITIONAL_TOP_LEVEL_FILES).join('|')})(\.rb|/)} 23 | 24 | # rubygems/core_ext/kernel_require.rb isn't actually part of rspec (obviously) but we want 25 | # it ignored when we are looking for the first meaningful line of the backtrace outside 26 | # of RSpec. It can show up in the backtrace as the immediate first caller 27 | # when `CallerFilter.first_non_rspec_line` is called from the top level of a required 28 | # file, but it depends on if rubygems is loaded or not. We don't want to have to deal 29 | # with this complexity in our `RSpec.deprecate` calls, so we ignore it here. 30 | IGNORE_REGEX = Regexp.union(LIB_REGEX, "rubygems/core_ext/kernel_require.rb", "(other) 16 | other = self.class.new(other) unless other.is_a?(self.class) 17 | 18 | return 0 if string == other.string 19 | 20 | longer_segment_count = [self, other].map { |version| version.segments.count }.max 21 | 22 | longer_segment_count.times do |index| 23 | self_segment = segments[index] || 0 24 | other_segment = other.segments[index] || 0 25 | 26 | if self_segment.class == other_segment.class 27 | result = self_segment <=> other_segment 28 | return result unless result == 0 29 | else 30 | return self_segment.is_a?(String) ? -1 : 1 31 | end 32 | end 33 | 34 | 0 35 | end 36 | 37 | def segments 38 | @segments ||= string.scan(/[a-z]+|\d+/i).map do |segment| 39 | if segment =~ /\A\d+\z/ 40 | segment.to_i 41 | else 42 | segment 43 | end 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/rspec/support/differ.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec::Support.require_rspec_support 'encoded_string' 4 | RSpec::Support.require_rspec_support 'hunk_generator' 5 | RSpec::Support.require_rspec_support "object_formatter" 6 | 7 | require 'pp' 8 | 9 | module RSpec 10 | module Support 11 | # rubocop:disable Metrics/ClassLength 12 | class Differ 13 | def diff(actual, expected) 14 | diff = "" 15 | 16 | unless actual.nil? || expected.nil? 17 | if all_strings?(actual, expected) 18 | if any_multiline_strings?(actual, expected) 19 | diff = diff_as_string(coerce_to_string(actual), coerce_to_string(expected)) 20 | end 21 | elsif all_hashes?(actual, expected) 22 | diff = diff_hashes_as_object(actual, expected) 23 | elsif no_procs?(actual, expected) && no_numbers?(actual, expected) 24 | diff = diff_as_object(actual, expected) 25 | end 26 | end 27 | 28 | diff.to_s 29 | end 30 | 31 | # rubocop:disable Metrics/MethodLength 32 | def diff_as_string(actual, expected) 33 | encoding = EncodedString.pick_encoding(actual, expected) 34 | 35 | actual = EncodedString.new(actual, encoding) 36 | expected = EncodedString.new(expected, encoding) 37 | 38 | output = EncodedString.new("\n", encoding) 39 | hunks = build_hunks(actual, expected) 40 | 41 | hunks.each_cons(2) do |prev_hunk, current_hunk| 42 | begin 43 | if current_hunk.overlaps?(prev_hunk) 44 | add_old_hunk_to_hunk(current_hunk, prev_hunk) 45 | else 46 | add_to_output(output, prev_hunk.diff(format_type).to_s) 47 | end 48 | ensure 49 | add_to_output(output, "\n") 50 | end 51 | end 52 | 53 | finalize_output(output, hunks.last.diff(format_type).to_s) if hunks.last 54 | 55 | color_diff output 56 | rescue Encoding::CompatibilityError 57 | handle_encoding_errors(actual, expected) 58 | end 59 | # rubocop:enable Metrics/MethodLength 60 | 61 | if defined?(RSpec::Mocks::ArgumentMatchers::AnyArgMatcher) 62 | def diff_hashes_as_object(actual, expected) 63 | actual_to_diff = 64 | actual.keys.reduce({}) do |hash, key| 65 | if RSpec::Mocks::ArgumentMatchers::AnyArgMatcher === expected[key] 66 | hash[key] = expected[key] 67 | else 68 | hash[key] = actual[key] 69 | end 70 | hash 71 | end 72 | diff_as_object(actual_to_diff, expected) 73 | end 74 | else 75 | def diff_hashes_as_object(actual, expected) 76 | diff_as_object(actual, expected) 77 | end 78 | end 79 | 80 | def diff_as_object(actual, expected) 81 | actual_as_string = object_to_string(actual) 82 | expected_as_string = object_to_string(expected) 83 | diff_as_string(actual_as_string, expected_as_string) 84 | end 85 | 86 | def color? 87 | @color 88 | end 89 | 90 | def initialize(opts={}) 91 | @color = opts.fetch(:color, false) 92 | @object_preparer = opts.fetch(:object_preparer, lambda { |string| string }) 93 | end 94 | 95 | private 96 | 97 | def no_procs?(*args) 98 | safely_flatten(args).none? { |a| Proc === a } 99 | end 100 | 101 | def all_hashes?(actual, expected) 102 | (Hash === actual) && (Hash === expected) 103 | end 104 | 105 | def all_strings?(*args) 106 | safely_flatten(args).all? { |a| String === a } 107 | end 108 | 109 | def any_multiline_strings?(*args) 110 | all_strings?(*args) && safely_flatten(args).any? { |a| multiline?(a) } 111 | end 112 | 113 | def no_numbers?(*args) 114 | safely_flatten(args).none? { |a| Numeric === a } 115 | end 116 | 117 | def coerce_to_string(string_or_array) 118 | return string_or_array unless Array === string_or_array 119 | diffably_stringify(string_or_array).join("\n") 120 | end 121 | 122 | def diffably_stringify(array) 123 | array.map do |entry| 124 | if Array === entry 125 | entry.inspect 126 | else 127 | entry.to_s.gsub("\n", "\\n").gsub("\r", "\\r") 128 | end 129 | end 130 | end 131 | 132 | if String.method_defined?(:encoding) 133 | def multiline?(string) 134 | string.include?("\n".encode(string.encoding)) 135 | end 136 | else 137 | def multiline?(string) 138 | string.include?("\n") 139 | end 140 | end 141 | 142 | def build_hunks(actual, expected) 143 | HunkGenerator.new(actual, expected).hunks 144 | end 145 | 146 | def finalize_output(output, final_line) 147 | add_to_output(output, final_line) 148 | add_to_output(output, "\n") 149 | end 150 | 151 | def add_to_output(output, string) 152 | output << string 153 | end 154 | 155 | def add_old_hunk_to_hunk(hunk, oldhunk) 156 | hunk.merge(oldhunk) 157 | end 158 | 159 | def safely_flatten(array) 160 | array = array.flatten(1) until (array == array.flatten(1)) 161 | array 162 | end 163 | 164 | def format_type 165 | :unified 166 | end 167 | 168 | def color(text, color_code) 169 | "\e[#{color_code}m#{text}\e[0m" 170 | end 171 | 172 | def red(text) 173 | color(text, 31) 174 | end 175 | 176 | def green(text) 177 | color(text, 32) 178 | end 179 | 180 | def blue(text) 181 | color(text, 34) 182 | end 183 | 184 | def normal(text) 185 | color(text, 0) 186 | end 187 | 188 | def color_diff(diff) 189 | return diff unless color? 190 | 191 | diff.lines.map do |line| 192 | case line[0].chr 193 | when "+" 194 | green line 195 | when "-" 196 | red line 197 | when "@" 198 | line[1].chr == "@" ? blue(line) : normal(line) 199 | else 200 | normal(line) 201 | end 202 | end.join 203 | end 204 | 205 | def object_to_string(object) 206 | object = @object_preparer.call(object) 207 | case object 208 | when Hash 209 | hash_to_string(object) 210 | when Array 211 | PP.pp(ObjectFormatter.prepare_for_inspection(object), "".dup) 212 | when String 213 | object =~ /\n/ ? object : object.inspect 214 | else 215 | PP.pp(object, "".dup) 216 | end 217 | end 218 | 219 | def hash_to_string(hash) 220 | formatted_hash = ObjectFormatter.prepare_for_inspection(hash) 221 | formatted_hash.keys.sort_by { |k| k.to_s }.map do |key| 222 | pp_key = PP.singleline_pp(key, "".dup) 223 | pp_value = PP.singleline_pp(formatted_hash[key], "".dup) 224 | 225 | "#{pp_key} => #{pp_value}," 226 | end.join("\n") 227 | end 228 | 229 | def handle_encoding_errors(actual, expected) 230 | if actual.source_encoding != expected.source_encoding 231 | "Could not produce a diff because the encoding of the actual string " \ 232 | "(#{actual.source_encoding}) differs from the encoding of the expected " \ 233 | "string (#{expected.source_encoding})" 234 | else 235 | "Could not produce a diff because of the encoding of the string " \ 236 | "(#{expected.source_encoding})" 237 | end 238 | end 239 | end 240 | # rubocop:enable Metrics/ClassLength 241 | end 242 | end 243 | -------------------------------------------------------------------------------- /lib/rspec/support/directory_maker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec::Support.require_rspec_support 'ruby_features' 4 | 5 | module RSpec 6 | module Support 7 | # @api private 8 | # 9 | # Replacement for fileutils#mkdir_p because we don't want to require parts 10 | # of stdlib in RSpec. 11 | class DirectoryMaker 12 | # @api private 13 | # 14 | # Implements nested directory construction 15 | def self.mkdir_p(path) 16 | stack = generate_stack(path) 17 | path.split(File::SEPARATOR).each do |part| 18 | stack = generate_path(stack, part) 19 | begin 20 | Dir.mkdir(stack) unless directory_exists?(stack) 21 | rescue Errno::EEXIST => e 22 | raise e unless directory_exists?(stack) 23 | rescue Errno::ENOTDIR => e 24 | raise Errno::EEXIST, e.message 25 | end 26 | end 27 | end 28 | 29 | if OS.windows_file_path? 30 | def self.generate_stack(path) 31 | if path.start_with?(File::SEPARATOR) 32 | File::SEPARATOR 33 | elsif path[1] == ':' 34 | '' 35 | else 36 | '.' 37 | end 38 | end 39 | def self.generate_path(stack, part) 40 | if stack == '' 41 | part 42 | elsif stack == File::SEPARATOR 43 | File.join('', part) 44 | else 45 | File.join(stack, part) 46 | end 47 | end 48 | else 49 | def self.generate_stack(path) 50 | path.start_with?(File::SEPARATOR) ? File::SEPARATOR : "." 51 | end 52 | def self.generate_path(stack, part) 53 | File.join(stack, part) 54 | end 55 | end 56 | 57 | def self.directory_exists?(dirname) 58 | File.exist?(dirname) && File.directory?(dirname) 59 | end 60 | private_class_method :directory_exists? 61 | private_class_method :generate_stack 62 | private_class_method :generate_path 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/rspec/support/encoded_string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RSpec 4 | module Support 5 | # @private 6 | class EncodedString 7 | # Reduce allocations by storing constants. 8 | UTF_8 = "UTF-8" 9 | US_ASCII = "US-ASCII" 10 | 11 | # Ruby's default replacement string is: 12 | # U+FFFD ("\xEF\xBF\xBD"), for Unicode encoding forms, else 13 | # ? ("\x3F") 14 | REPLACE = "?" 15 | 16 | def initialize(string, encoding=nil) 17 | @encoding = encoding 18 | @source_encoding = detect_source_encoding(string) 19 | @string = matching_encoding(string) 20 | end 21 | attr_reader :source_encoding 22 | 23 | delegated_methods = String.instance_methods.map(&:to_s) & %w[eql? lines == encoding empty?] 24 | delegated_methods.each do |name| 25 | define_method(name) { |*args, &block| @string.__send__(name, *args, &block) } 26 | end 27 | 28 | def <<(string) 29 | @string << matching_encoding(string) 30 | end 31 | 32 | if Ruby.jruby? 33 | def split(regex_or_string) 34 | @string.split(matching_encoding(regex_or_string)) 35 | rescue ArgumentError 36 | # JRuby raises an ArgumentError when splitting a source string that 37 | # contains invalid bytes. 38 | remove_invalid_bytes(@string).split regex_or_string 39 | end 40 | else 41 | def split(regex_or_string) 42 | @string.split(matching_encoding(regex_or_string)) 43 | end 44 | end 45 | 46 | def to_s 47 | @string 48 | end 49 | alias :to_str :to_s 50 | 51 | if String.method_defined?(:encoding) 52 | 53 | private 54 | 55 | # Encoding Exceptions: 56 | # 57 | # Raised by Encoding and String methods: 58 | # Encoding::UndefinedConversionError: 59 | # when a transcoding operation fails 60 | # if the String contains characters invalid for the target encoding 61 | # e.g. "\x80".encode('UTF-8','ASCII-8BIT') 62 | # vs "\x80".encode('UTF-8','ASCII-8BIT', undef: :replace, replace: '') 63 | # # => '' 64 | # Encoding::CompatibilityError 65 | # when Encoding.compatible?(str1, str2) is nil 66 | # e.g. utf_16le_emoji_string.split("\n") 67 | # e.g. valid_unicode_string.encode(utf8_encoding) << ascii_string 68 | # Encoding::InvalidByteSequenceError: 69 | # when the string being transcoded contains a byte invalid for 70 | # either the source or target encoding 71 | # e.g. "\x80".encode('UTF-8','US-ASCII') 72 | # vs "\x80".encode('UTF-8','US-ASCII', invalid: :replace, replace: '') 73 | # # => '' 74 | # ArgumentError 75 | # when operating on a string with invalid bytes 76 | # e.g."\x80".split("\n") 77 | # TypeError 78 | # when a symbol is passed as an encoding 79 | # Encoding.find(:"UTF-8") 80 | # when calling force_encoding on an object 81 | # that doesn't respond to #to_str 82 | # 83 | # Raised by transcoding methods: 84 | # Encoding::ConverterNotFoundError: 85 | # when a named encoding does not correspond with a known converter 86 | # e.g. 'abc'.force_encoding('UTF-8').encode('foo') 87 | # or a converter path cannot be found 88 | # e.g. "\x80".force_encoding('ASCII-8BIT').encode('Emacs-Mule') 89 | # 90 | # Raised by byte <-> char conversions 91 | # RangeError: out of char range 92 | # e.g. the UTF-16LE emoji: 128169.chr 93 | def matching_encoding(string) 94 | string = remove_invalid_bytes(string) 95 | string.encode(@encoding) 96 | rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError 97 | # Originally defined as a constant to avoid unneeded allocations, this hash must 98 | # be defined inline (without {}) to avoid warnings on Ruby 2.7 99 | # 100 | # In MRI 2.1 'invalid: :replace' changed to also replace an invalid byte sequence 101 | # see https://github.com/ruby/ruby/blob/v2_1_0/NEWS#L176 102 | # https://www.ruby-forum.com/topic/6861247 103 | # https://twitter.com/nalsh/status/553413844685438976 104 | # 105 | # For example, given: 106 | # "\x80".force_encoding("Emacs-Mule").encode(:invalid => :replace).bytes.to_a 107 | # 108 | # On MRI 2.1 or above: 63 # '?' 109 | # else : 128 # "\x80" 110 | # 111 | string.encode(@encoding, :invalid => :replace, :undef => :replace, :replace => REPLACE) 112 | rescue Encoding::ConverterNotFoundError 113 | # Originally defined as a constant to avoid unneeded allocations, this hash must 114 | # be defined inline (without {}) to avoid warnings on Ruby 2.7 115 | string.dup.force_encoding(@encoding).encode(:invalid => :replace, :replace => REPLACE) 116 | end 117 | 118 | # Prevents raising ArgumentError 119 | if String.method_defined?(:scrub) 120 | # https://github.com/ruby/ruby/blob/eeb05e8c11/doc/NEWS-2.1.0#L120-L123 121 | # https://github.com/ruby/ruby/blob/v2_1_0/string.c#L8242 122 | # https://github.com/hsbt/string-scrub 123 | # https://github.com/rubinius/rubinius/blob/v2.5.2/kernel/common/string.rb#L1913-L1972 124 | def remove_invalid_bytes(string) 125 | string.scrub(REPLACE) 126 | end 127 | else 128 | # http://stackoverflow.com/a/8711118/879854 129 | # Loop over chars in a string replacing chars 130 | # with invalid encoding, which is a pretty good proxy 131 | # for the invalid byte sequence that causes an ArgumentError 132 | def remove_invalid_bytes(string) 133 | string.chars.map do |char| 134 | char.valid_encoding? ? char : REPLACE 135 | end.join 136 | end 137 | end 138 | 139 | def detect_source_encoding(string) 140 | string.encoding 141 | end 142 | 143 | def self.pick_encoding(source_a, source_b) 144 | Encoding.compatible?(source_a, source_b) || Encoding.default_external 145 | end 146 | else 147 | 148 | def self.pick_encoding(_source_a, _source_b) 149 | end 150 | 151 | private 152 | 153 | def matching_encoding(string) 154 | string 155 | end 156 | 157 | def detect_source_encoding(_string) 158 | US_ASCII 159 | end 160 | end 161 | end 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /lib/rspec/support/fuzzy_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RSpec 4 | module Support 5 | # Provides a means to fuzzy-match between two arbitrary objects. 6 | # Understands array/hash nesting. Uses `===` or `==` to 7 | # perform the matching. 8 | module FuzzyMatcher 9 | # @api private 10 | def self.values_match?(expected, actual) 11 | if Hash === actual 12 | return hashes_match?(expected, actual) if Hash === expected 13 | elsif Array === expected && Enumerable === actual && !(Struct === actual) 14 | return arrays_match?(expected, actual.to_a) 15 | end 16 | 17 | return true if expected == actual 18 | 19 | begin 20 | expected === actual 21 | rescue ArgumentError 22 | # Some objects, like 0-arg lambdas on 1.9+, raise 23 | # ArgumentError for `expected === actual`. 24 | false 25 | end 26 | end 27 | 28 | # @private 29 | def self.arrays_match?(expected_list, actual_list) 30 | return false if expected_list.size != actual_list.size 31 | 32 | expected_list.zip(actual_list).all? do |expected, actual| 33 | values_match?(expected, actual) 34 | end 35 | end 36 | 37 | # @private 38 | def self.hashes_match?(expected_hash, actual_hash) 39 | return false if expected_hash.size != actual_hash.size 40 | 41 | expected_hash.all? do |expected_key, expected_value| 42 | actual_value = actual_hash.fetch(expected_key) { return false } 43 | values_match?(expected_value, actual_value) 44 | end 45 | end 46 | 47 | private_class_method :arrays_match?, :hashes_match? 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/rspec/support/hunk_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'diff/lcs' 4 | require 'diff/lcs/hunk' 5 | 6 | module RSpec 7 | module Support 8 | # @private 9 | class HunkGenerator 10 | def initialize(actual, expected) 11 | @actual = actual 12 | @expected = expected 13 | end 14 | 15 | def hunks 16 | @file_length_difference = 0 17 | @hunks ||= diffs.map do |piece| 18 | build_hunk(piece) 19 | end 20 | end 21 | 22 | private 23 | 24 | def diffs 25 | Diff::LCS.diff(expected_lines, actual_lines) 26 | end 27 | 28 | def expected_lines 29 | @expected.split("\n").map! { |e| e.chomp } 30 | end 31 | 32 | def actual_lines 33 | @actual.split("\n").map! { |e| e.chomp } 34 | end 35 | 36 | def build_hunk(piece) 37 | Diff::LCS::Hunk.new( 38 | expected_lines, actual_lines, piece, context_lines, @file_length_difference 39 | ).tap do |h| 40 | @file_length_difference = h.file_length_difference 41 | end 42 | end 43 | 44 | def context_lines 45 | 3 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/rspec/support/matcher_definition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RSpec 4 | module Support 5 | # @private 6 | def self.matcher_definitions 7 | @matcher_definitions ||= [] 8 | end 9 | 10 | # Used internally to break cyclic dependency between mocks, expectations, 11 | # and support. We don't currently have a consistent implementation of our 12 | # matchers, though we are considering changing that: 13 | # https://github.com/rspec/rspec-mocks/issues/513 14 | # 15 | # @private 16 | def self.register_matcher_definition(&block) 17 | matcher_definitions << block 18 | end 19 | 20 | # Remove a previously registered matcher. Useful for cleaning up after 21 | # yourself in specs. 22 | # 23 | # @private 24 | def self.deregister_matcher_definition(&block) 25 | matcher_definitions.delete(block) 26 | end 27 | 28 | # @private 29 | def self.is_a_matcher?(object) 30 | matcher_definitions.any? { |md| md.call(object) } 31 | end 32 | 33 | # @api private 34 | # 35 | # gives a string representation of an object for use in RSpec descriptions 36 | def self.rspec_description_for_object(object) 37 | if RSpec::Support.is_a_matcher?(object) && object.respond_to?(:description) 38 | object.description 39 | else 40 | object 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/rspec/support/mutex.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RSpec 4 | module Support 5 | # On 1.8.7, it's in the stdlib. 6 | # We don't want to load the stdlib, b/c this is a test tool, and can affect 7 | # the test environment, causing tests to pass where they should fail. 8 | # 9 | # So we're transcribing/modifying it from 10 | # https://github.com/ruby/ruby/blob/v1_8_7_374/lib/thread.rb#L56 11 | # Some methods we don't need are deleted. Anything I don't 12 | # understand (there's quite a bit, actually) is left in. 13 | # 14 | # Some formatting changes are made to appease the robot overlord: 15 | # https://travis-ci.org/rspec/rspec-core/jobs/54410874 16 | # @private 17 | class Mutex 18 | def initialize 19 | @waiting = [] 20 | @locked = false 21 | @waiting.taint 22 | taint 23 | end 24 | 25 | # @private 26 | def lock 27 | while Thread.critical = true && @locked 28 | @waiting.push Thread.current 29 | Thread.stop 30 | end 31 | @locked = true 32 | Thread.critical = false 33 | self 34 | end 35 | 36 | # @private 37 | def unlock 38 | return unless @locked 39 | Thread.critical = true 40 | @locked = false 41 | wakeup_and_run_waiting_thread 42 | self 43 | end 44 | 45 | # @private 46 | def synchronize 47 | lock 48 | begin 49 | yield 50 | ensure 51 | unlock 52 | end 53 | end 54 | 55 | private 56 | 57 | def wakeup_and_run_waiting_thread 58 | begin 59 | t = @waiting.shift 60 | t.wakeup if t 61 | rescue ThreadError 62 | retry 63 | end 64 | Thread.critical = false 65 | begin 66 | t.run if t 67 | rescue ThreadError 68 | :noop 69 | end 70 | end 71 | 72 | # Avoid warnings for library wide checks spec 73 | end unless defined?(::RSpec::Support::Mutex) || defined?(::Mutex) 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/rspec/support/object_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec::Support.require_rspec_support 'matcher_definition' 4 | 5 | module RSpec 6 | module Support 7 | # Provide additional output details beyond what `inspect` provides when 8 | # printing Time, DateTime, or BigDecimal 9 | # @api private 10 | class ObjectFormatter # rubocop:disable Metrics/ClassLength 11 | ELLIPSIS = "..." 12 | 13 | attr_accessor :max_formatted_output_length 14 | 15 | # Methods are deferred to a default instance of the class to maintain the interface 16 | # For example, calling ObjectFormatter.format is still possible 17 | def self.default_instance 18 | @default_instance ||= new 19 | end 20 | 21 | def self.format(object) 22 | default_instance.format(object) 23 | end 24 | 25 | def self.prepare_for_inspection(object) 26 | default_instance.prepare_for_inspection(object) 27 | end 28 | 29 | def initialize(max_formatted_output_length=200) 30 | @max_formatted_output_length = max_formatted_output_length 31 | @current_structure_stack = [] 32 | end 33 | 34 | def format(object) 35 | if max_formatted_output_length.nil? 36 | prepare_for_inspection(object).inspect 37 | else 38 | formatted_object = prepare_for_inspection(object).inspect 39 | if formatted_object.length < max_formatted_output_length 40 | formatted_object 41 | else 42 | beginning = truncate_string formatted_object, 0, max_formatted_output_length / 2 43 | ending = truncate_string formatted_object, -max_formatted_output_length / 2, -1 44 | beginning + ELLIPSIS + ending 45 | end 46 | end 47 | end 48 | 49 | # Prepares the provided object to be formatted by wrapping it as needed 50 | # in something that, when `inspect` is called on it, will produce the 51 | # desired output. 52 | # 53 | # This allows us to apply the desired formatting to hash/array data structures 54 | # at any level of nesting, simply by walking that structure and replacing items 55 | # with custom items that have `inspect` defined to return the desired output 56 | # for that item. Then we can just use `Array#inspect` or `Hash#inspect` to 57 | # format the entire thing. 58 | def prepare_for_inspection(object) 59 | case object 60 | when Array 61 | prepare_array(object) 62 | when Hash 63 | prepare_hash(object) 64 | else 65 | inspector_class = INSPECTOR_CLASSES.find { |inspector| inspector.can_inspect?(object) } 66 | inspector_class.new(object, self) 67 | end 68 | end 69 | 70 | def prepare_array(array) 71 | with_entering_structure(array) do 72 | array.map { |element| prepare_element(element) } 73 | end 74 | end 75 | 76 | def prepare_hash(input_hash) 77 | with_entering_structure(input_hash) do 78 | sort_hash_keys(input_hash).inject({}) do |output_hash, key_and_value| 79 | key, value = key_and_value.map { |element| prepare_element(element) } 80 | output_hash[key] = value 81 | output_hash 82 | end 83 | end 84 | end 85 | 86 | def sort_hash_keys(input_hash) 87 | if input_hash.keys.all? { |k| k.is_a?(String) || k.is_a?(Symbol) } 88 | Hash[input_hash.sort_by { |k, _v| k.to_s }] 89 | else 90 | input_hash 91 | end 92 | end 93 | 94 | def prepare_element(element) 95 | if recursive_structure?(element) 96 | case element 97 | when Array then InspectableItem.new('[...]') 98 | when Hash then InspectableItem.new('{...}') 99 | else raise # This won't happen 100 | end 101 | else 102 | prepare_for_inspection(element) 103 | end 104 | end 105 | 106 | def with_entering_structure(structure) 107 | @current_structure_stack.push(structure) 108 | return_value = yield 109 | @current_structure_stack.pop 110 | return_value 111 | end 112 | 113 | def recursive_structure?(object) 114 | @current_structure_stack.any? { |seen_structure| seen_structure.equal?(object) } 115 | end 116 | 117 | InspectableItem = Struct.new(:text) do 118 | def inspect 119 | text 120 | end 121 | 122 | def pretty_print(pp) 123 | pp.text(text) 124 | end 125 | end 126 | 127 | BaseInspector = Struct.new(:object, :formatter) do 128 | def self.can_inspect?(_object) 129 | raise NotImplementedError 130 | end 131 | 132 | def inspect 133 | raise NotImplementedError 134 | end 135 | 136 | def pretty_print(pp) 137 | pp.text(inspect) 138 | end 139 | end 140 | 141 | class TimeInspector < BaseInspector 142 | FORMAT = "%Y-%m-%d %H:%M:%S" 143 | 144 | def self.can_inspect?(object) 145 | Time === object 146 | end 147 | 148 | if Time.method_defined?(:nsec) 149 | def inspect 150 | object.strftime("#{FORMAT}.#{"%09d" % object.nsec} %z") 151 | end 152 | else # for 1.8.7 153 | def inspect 154 | object.strftime("#{FORMAT}.#{"%06d" % object.usec} %z") 155 | end 156 | end 157 | end 158 | 159 | class DateTimeInspector < BaseInspector 160 | FORMAT = "%a, %d %b %Y %H:%M:%S.%N %z" 161 | 162 | def self.can_inspect?(object) 163 | defined?(DateTime) && DateTime === object 164 | end 165 | 166 | # ActiveSupport sometimes overrides inspect. If `ActiveSupport` is 167 | # defined use a custom format string that includes more time precision. 168 | def inspect 169 | if defined?(ActiveSupport) 170 | object.strftime(FORMAT) 171 | else 172 | object.inspect 173 | end 174 | end 175 | end 176 | 177 | class BigDecimalInspector < BaseInspector 178 | def self.can_inspect?(object) 179 | defined?(BigDecimal) && BigDecimal === object 180 | end 181 | 182 | def inspect 183 | "#{object.to_s('F')} (#{object.inspect})" 184 | end 185 | end 186 | 187 | class DescribableMatcherInspector < BaseInspector 188 | def self.can_inspect?(object) 189 | Support.is_a_matcher?(object) && object.respond_to?(:description) 190 | end 191 | 192 | def inspect 193 | object.description 194 | end 195 | end 196 | 197 | class UninspectableObjectInspector < BaseInspector 198 | OBJECT_ID_FORMAT = '%#016x' 199 | 200 | def self.can_inspect?(object) 201 | object.inspect 202 | false 203 | rescue NoMethodError 204 | true 205 | end 206 | 207 | def inspect 208 | "#<#{klass}:#{native_object_id}>" 209 | end 210 | 211 | def klass 212 | Support.class_of(object) 213 | end 214 | 215 | # http://stackoverflow.com/a/2818916 216 | def native_object_id 217 | OBJECT_ID_FORMAT % (object.__id__ << 1) 218 | rescue NoMethodError 219 | # In Ruby 1.9.2, BasicObject responds to none of #__id__, #object_id, #id... 220 | '-' 221 | end 222 | end 223 | 224 | class DelegatorInspector < BaseInspector 225 | def self.can_inspect?(object) 226 | defined?(Delegator) && Delegator === object 227 | end 228 | 229 | def inspect 230 | "#<#{object.class}(#{formatter.format(object.send(:__getobj__))})>" 231 | end 232 | end 233 | 234 | class InspectableObjectInspector < BaseInspector 235 | def self.can_inspect?(object) 236 | object.inspect 237 | true 238 | rescue NoMethodError 239 | false 240 | end 241 | 242 | def inspect 243 | object.inspect 244 | end 245 | end 246 | 247 | INSPECTOR_CLASSES = [ 248 | TimeInspector, 249 | DateTimeInspector, 250 | BigDecimalInspector, 251 | UninspectableObjectInspector, 252 | DescribableMatcherInspector, 253 | DelegatorInspector, 254 | InspectableObjectInspector 255 | ].tap do |classes| 256 | # 2.4 has improved BigDecimal formatting so we do not need 257 | # to provide our own. 258 | # https://github.com/ruby/bigdecimal/pull/42 259 | classes.delete(BigDecimalInspector) if RUBY_VERSION >= '2.4' 260 | end 261 | 262 | private 263 | 264 | # Returns the substring defined by the start_index and end_index 265 | # If the string ends with a partial ANSI code code then that 266 | # will be removed as printing partial ANSI 267 | # codes to the terminal can lead to corruption 268 | def truncate_string(str, start_index, end_index) 269 | cut_str = str[start_index..end_index] 270 | 271 | # ANSI color codes are like: \e[33m so anything with \e[ and a 272 | # number without a 'm' is an incomplete color code 273 | cut_str.sub(/\e\[\d+$/, '') 274 | end 275 | end 276 | end 277 | end 278 | -------------------------------------------------------------------------------- /lib/rspec/support/recursive_const_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RSpec 4 | module Support 5 | # Provides recursive constant lookup methods useful for 6 | # constant stubbing. 7 | module RecursiveConstMethods 8 | # We only want to consider constants that are defined directly on a 9 | # particular module, and not include top-level/inherited constants. 10 | # Unfortunately, the constant API changed between 1.8 and 1.9, so 11 | # we need to conditionally define methods to ignore the top-level/inherited 12 | # constants. 13 | # 14 | # Given: 15 | # class A; B = 1; end 16 | # class C < A; end 17 | # 18 | # On 1.8: 19 | # - C.const_get("Hash") # => ::Hash 20 | # - C.const_defined?("Hash") # => false 21 | # - C.constants # => ["B"] 22 | # - None of these methods accept the extra `inherit` argument 23 | # On 1.9: 24 | # - C.const_get("Hash") # => ::Hash 25 | # - C.const_defined?("Hash") # => true 26 | # - C.const_get("Hash", false) # => raises NameError 27 | # - C.const_defined?("Hash", false) # => false 28 | # - C.constants # => [:B] 29 | # - C.constants(false) #=> [] 30 | if Module.method(:const_defined?).arity == 1 31 | def const_defined_on?(mod, const_name) 32 | mod.const_defined?(const_name) 33 | end 34 | 35 | def get_const_defined_on(mod, const_name) 36 | return mod.const_get(const_name) if const_defined_on?(mod, const_name) 37 | 38 | raise NameError, "uninitialized constant #{mod.name}::#{const_name}" 39 | end 40 | 41 | def constants_defined_on(mod) 42 | mod.constants.select { |c| const_defined_on?(mod, c) } 43 | end 44 | else 45 | def const_defined_on?(mod, const_name) 46 | mod.const_defined?(const_name, false) 47 | end 48 | 49 | def get_const_defined_on(mod, const_name) 50 | mod.const_get(const_name, false) 51 | end 52 | 53 | def constants_defined_on(mod) 54 | mod.constants(false) 55 | end 56 | end 57 | 58 | def recursive_const_get(const_name) 59 | normalize_const_name(const_name).split('::').inject(Object) do |mod, name| 60 | get_const_defined_on(mod, name) 61 | end 62 | end 63 | 64 | def recursive_const_defined?(const_name) 65 | parts = normalize_const_name(const_name).split('::') 66 | parts.inject([Object, '']) do |(mod, full_name), name| 67 | yield(full_name, name) if block_given? && !(Module === mod) 68 | return false unless const_defined_on?(mod, name) 69 | [get_const_defined_on(mod, name), [mod.name, name].join('::')] 70 | end 71 | end 72 | 73 | def normalize_const_name(const_name) 74 | const_name.sub(/\A::/, '') 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/rspec/support/reentrant_mutex.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RSpec 4 | module Support 5 | # Allows a thread to lock out other threads from a critical section of code, 6 | # while allowing the thread with the lock to reenter that section. 7 | # 8 | # Based on Monitor as of 2.2 - 9 | # https://github.com/ruby/ruby/blob/eb7ddaa3a47bf48045d26c72eb0f263a53524ebc/lib/monitor.rb#L9 10 | # 11 | # Depends on Mutex, but Mutex is only available as part of core since 1.9.1: 12 | # exists - http://ruby-doc.org/core-1.9.1/Mutex.html 13 | # dne - http://ruby-doc.org/core-1.9.0/Mutex.html 14 | # 15 | # @private 16 | class ReentrantMutex 17 | def initialize 18 | @owner = nil 19 | @count = 0 20 | @mutex = Mutex.new 21 | end 22 | 23 | def synchronize 24 | enter 25 | yield 26 | ensure 27 | exit 28 | end 29 | 30 | private 31 | 32 | # This is fixing a bug #501 that is specific to Ruby 3.0. The new implementation 33 | # depends on `owned?` that was introduced in Ruby 2.0, so both should work for Ruby 2.x. 34 | if RUBY_VERSION.to_f >= 3.0 35 | def enter 36 | @mutex.lock unless @mutex.owned? 37 | @count += 1 38 | end 39 | 40 | def exit 41 | unless @mutex.owned? 42 | raise ThreadError, "Attempt to unlock a mutex which is locked by another thread/fiber" 43 | end 44 | @count -= 1 45 | @mutex.unlock if @count == 0 46 | end 47 | else 48 | def enter 49 | @mutex.lock if @owner != Thread.current 50 | @owner = Thread.current 51 | @count += 1 52 | end 53 | 54 | def exit 55 | @count -= 1 56 | return unless @count == 0 57 | @owner = nil 58 | @mutex.unlock 59 | end 60 | end 61 | end 62 | 63 | if defined? ::Mutex 64 | # On 1.9 and up, this is in core, so we just use the real one 65 | class Mutex < ::Mutex 66 | # If you mock Mutex.new you break our usage of Mutex, so 67 | # instead we capture the original method to return Mutexes. 68 | NEW_MUTEX_METHOD = Mutex.method(:new) 69 | 70 | def self.new 71 | NEW_MUTEX_METHOD.call 72 | end 73 | end 74 | else # For 1.8.7 75 | # :nocov: 76 | RSpec::Support.require_rspec_support "mutex" 77 | # :nocov: 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/rspec/support/ruby_features.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rbconfig' 4 | RSpec::Support.require_rspec_support "comparable_version" 5 | 6 | module RSpec 7 | module Support 8 | # @api private 9 | # 10 | # Provides query methods for different OS or OS features. 11 | module OS 12 | module_function 13 | 14 | def windows? 15 | !!(RbConfig::CONFIG['host_os'] =~ /cygwin|mswin|mingw|bccwin|wince|emx/) 16 | end 17 | 18 | def windows_file_path? 19 | ::File::ALT_SEPARATOR == '\\' 20 | end 21 | end 22 | 23 | # @api private 24 | # 25 | # Provides query methods for different rubies 26 | module Ruby 27 | module_function 28 | 29 | def jruby? 30 | RUBY_PLATFORM == 'java' 31 | end 32 | 33 | def jruby_version 34 | @jruby_version ||= ComparableVersion.new(JRUBY_VERSION) 35 | end 36 | 37 | def jruby_9000? 38 | jruby? && JRUBY_VERSION >= '9.0.0.0' 39 | end 40 | 41 | def rbx? 42 | defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx' 43 | end 44 | 45 | def non_mri? 46 | !mri? 47 | end 48 | 49 | def mri? 50 | !defined?(RUBY_ENGINE) || RUBY_ENGINE == 'ruby' 51 | end 52 | 53 | def truffleruby? 54 | defined?(RUBY_ENGINE) && RUBY_ENGINE == 'truffleruby' 55 | end 56 | end 57 | 58 | # @api private 59 | # 60 | # Provides query methods for ruby features that differ among 61 | # implementations. 62 | module RubyFeatures 63 | module_function 64 | 65 | if Ruby.jruby? && RUBY_VERSION.to_f < 1.9 66 | # On JRuby 1.7 `--1.8` mode, `Process.respond_to?(:fork)` returns true, 67 | # but when you try to fork, it raises an error: 68 | # NotImplementedError: fork is not available on this platform 69 | # 70 | # When we drop support for JRuby 1.7 and/or Ruby 1.8, we can drop 71 | # this special case. 72 | def fork_supported? 73 | false 74 | end 75 | else 76 | def fork_supported? 77 | Process.respond_to?(:fork) 78 | end 79 | end 80 | 81 | def optional_and_splat_args_supported? 82 | Method.method_defined?(:parameters) 83 | end 84 | 85 | def caller_locations_supported? 86 | respond_to?(:caller_locations, true) 87 | end 88 | 89 | if Exception.method_defined?(:cause) 90 | def supports_exception_cause? 91 | true 92 | end 93 | else 94 | def supports_exception_cause? 95 | false 96 | end 97 | end 98 | 99 | if RUBY_VERSION.to_f >= 3.2 100 | def supports_syntax_suggest? 101 | true 102 | end 103 | else 104 | def supports_syntax_suggest? 105 | false 106 | end 107 | end 108 | 109 | if RUBY_VERSION.to_f >= 3.0 110 | # https://rubyreferences.github.io/rubychanges/3.0.html#keyword-arguments-are-now-fully-separated-from-positional-arguments 111 | def kw_arg_separation? 112 | true 113 | end 114 | else 115 | def kw_arg_separation? 116 | false 117 | end 118 | end 119 | 120 | if RUBY_VERSION.to_f >= 2.7 121 | def supports_taint? 122 | false 123 | end 124 | else 125 | def supports_taint? 126 | true 127 | end 128 | end 129 | ripper_requirements = [ComparableVersion.new(RUBY_VERSION) >= '1.9.2'] 130 | 131 | ripper_requirements.push(false) if Ruby.rbx? 132 | 133 | if Ruby.jruby? 134 | ripper_requirements.push(Ruby.jruby_version >= '1.7.5') 135 | # Ripper on JRuby 9.0.0.0.rc1 - 9.1.8.0 reports wrong line number 136 | # or cannot parse source including `:if`. 137 | # Ripper on JRuby 9.x.x.x < 9.1.17.0 can't handle keyword arguments 138 | # Neither can JRuby 9.2, e.g. < 9.2.1.0 139 | ripper_requirements.push(!Ruby.jruby_version.between?('9.0.0.0.rc1', '9.2.0.0')) 140 | end 141 | 142 | # TruffleRuby disables ripper due to low performance 143 | ripper_requirements.push(false) if Ruby.truffleruby? 144 | 145 | if ripper_requirements.all? 146 | def ripper_supported? 147 | true 148 | end 149 | else 150 | def ripper_supported? 151 | false 152 | end 153 | end 154 | 155 | def distincts_kw_args_from_positional_hash? 156 | RUBY_VERSION >= '3.0.0' 157 | end 158 | 159 | if Ruby.mri? 160 | def kw_args_supported? 161 | RUBY_VERSION >= '2.0.0' 162 | end 163 | 164 | def required_kw_args_supported? 165 | RUBY_VERSION >= '2.1.0' 166 | end 167 | 168 | def supports_rebinding_module_methods? 169 | RUBY_VERSION.to_i >= 2 170 | end 171 | else 172 | # RBX / JRuby et al support is unknown for keyword arguments 173 | begin 174 | eval("o = Object.new; def o.m(a: 1); end;"\ 175 | " raise SyntaxError unless o.method(:m).parameters.include?([:key, :a])") 176 | 177 | def kw_args_supported? 178 | true 179 | end 180 | rescue SyntaxError 181 | def kw_args_supported? 182 | false 183 | end 184 | end 185 | 186 | begin 187 | eval("o = Object.new; def o.m(a: ); end;"\ 188 | "raise SyntaxError unless o.method(:m).parameters.include?([:keyreq, :a])") 189 | 190 | def required_kw_args_supported? 191 | true 192 | end 193 | rescue SyntaxError 194 | def required_kw_args_supported? 195 | false 196 | end 197 | end 198 | 199 | begin 200 | Module.new { def foo; end }.instance_method(:foo).bind(Object.new) 201 | 202 | def supports_rebinding_module_methods? 203 | true 204 | end 205 | rescue TypeError 206 | def supports_rebinding_module_methods? 207 | false 208 | end 209 | end 210 | end 211 | 212 | def module_refinement_supported? 213 | Module.method_defined?(:refine) || Module.private_method_defined?(:refine) 214 | end 215 | 216 | def module_prepends_supported? 217 | Module.method_defined?(:prepend) || Module.private_method_defined?(:prepend) 218 | end 219 | end 220 | end 221 | end 222 | -------------------------------------------------------------------------------- /lib/rspec/support/source.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec::Support.require_rspec_support 'encoded_string' 4 | RSpec::Support.require_rspec_support 'ruby_features' 5 | 6 | module RSpec 7 | module Support 8 | # @private 9 | # Represents a Ruby source file and provides access to AST and tokens. 10 | class Source 11 | attr_reader :source, :path 12 | 13 | # This class protects us against having File read and expand_path 14 | # stubbed out within tests. 15 | class File 16 | class << self 17 | [:read, :expand_path].each do |method_name| 18 | define_method(method_name, &::File.method(method_name)) 19 | end 20 | end 21 | end 22 | 23 | def self.from_file(path) 24 | source = File.read(path) 25 | new(source, path) 26 | end 27 | 28 | if String.method_defined?(:encoding) 29 | def initialize(source_string, path=nil) 30 | @source = RSpec::Support::EncodedString.new(source_string, Encoding.default_external) 31 | @path = path ? File.expand_path(path) : '(string)' 32 | end 33 | else # for 1.8.7 34 | # :nocov: 35 | def initialize(source_string, path=nil) 36 | @source = RSpec::Support::EncodedString.new(source_string) 37 | @path = path ? File.expand_path(path) : '(string)' 38 | end 39 | # :nocov: 40 | end 41 | 42 | def lines 43 | @lines ||= source.split("\n") 44 | end 45 | 46 | def inspect 47 | "#<#{self.class} #{path}>" 48 | end 49 | 50 | if RSpec::Support::RubyFeatures.ripper_supported? 51 | RSpec::Support.require_rspec_support 'source/node' 52 | RSpec::Support.require_rspec_support 'source/token' 53 | 54 | def ast 55 | @ast ||= begin 56 | require 'ripper' 57 | sexp = Ripper.sexp(source) 58 | raise SyntaxError unless sexp 59 | Node.new(sexp) 60 | end 61 | end 62 | 63 | def tokens 64 | @tokens ||= begin 65 | require 'ripper' 66 | tokens = Ripper.lex(source) 67 | Token.tokens_from_ripper_tokens(tokens) 68 | end 69 | end 70 | 71 | def nodes_by_line_number 72 | @nodes_by_line_number ||= begin 73 | nodes_by_line_number = ast.select(&:location).group_by { |node| node.location.line } 74 | Hash.new { |hash, key| hash[key] = [] }.merge(nodes_by_line_number) 75 | end 76 | end 77 | 78 | def tokens_by_line_number 79 | @tokens_by_line_number ||= begin 80 | nodes_by_line_number = tokens.group_by { |token| token.location.line } 81 | Hash.new { |hash, key| hash[key] = [] }.merge(nodes_by_line_number) 82 | end 83 | end 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/rspec/support/source/location.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RSpec 4 | module Support 5 | class Source 6 | # @private 7 | # Represents a source location of node or token. 8 | Location = Struct.new(:line, :column) do 9 | include Comparable 10 | 11 | def self.location?(array) 12 | array.is_a?(Array) && array.size == 2 && array.all? { |e| e.is_a?(Integer) } 13 | end 14 | 15 | def <=>(other) 16 | line_comparison = (line <=> other.line) 17 | return line_comparison unless line_comparison == 0 18 | column <=> other.column 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/rspec/support/source/node.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec::Support.require_rspec_support 'source/location' 4 | 5 | module RSpec 6 | module Support 7 | class Source 8 | # @private 9 | # A wrapper for Ripper AST node which is generated with `Ripper.sexp`. 10 | class Node 11 | include Enumerable 12 | 13 | attr_reader :sexp, :parent 14 | 15 | def self.sexp?(array) 16 | array.is_a?(Array) && array.first.is_a?(Symbol) 17 | end 18 | 19 | def initialize(ripper_sexp, parent=nil) 20 | @sexp = ripper_sexp.freeze 21 | @parent = parent 22 | end 23 | 24 | def type 25 | sexp[0] 26 | end 27 | 28 | def args 29 | @args ||= raw_args.map do |raw_arg| 30 | if Node.sexp?(raw_arg) 31 | Node.new(raw_arg, self) 32 | elsif Location.location?(raw_arg) 33 | Location.new(*raw_arg) 34 | elsif raw_arg.is_a?(Array) 35 | ExpressionSequenceNode.new(raw_arg, self) 36 | else 37 | raw_arg 38 | end 39 | end.freeze 40 | end 41 | 42 | def children 43 | @children ||= args.select { |arg| arg.is_a?(Node) }.freeze 44 | end 45 | 46 | def location 47 | @location ||= args.find { |arg| arg.is_a?(Location) } 48 | end 49 | 50 | # We use a loop here (instead of recursion) to prevent SystemStackError 51 | def each 52 | return to_enum(__method__) unless block_given? 53 | 54 | node_queue = [] 55 | node_queue << self 56 | 57 | while (current_node = node_queue.shift) 58 | yield current_node 59 | node_queue.concat(current_node.children) 60 | end 61 | end 62 | 63 | def each_ancestor 64 | return to_enum(__method__) unless block_given? 65 | 66 | current_node = self 67 | 68 | while (current_node = current_node.parent) 69 | yield current_node 70 | end 71 | end 72 | 73 | def inspect 74 | "#<#{self.class} #{type}>" 75 | end 76 | 77 | private 78 | 79 | def raw_args 80 | sexp[1..-1] || [] 81 | end 82 | end 83 | 84 | # @private 85 | # Basically `Ripper.sexp` generates arrays whose first element is a symbol (type of sexp), 86 | # but it exceptionally generates typeless arrays for expression sequence: 87 | # 88 | # Ripper.sexp('foo; bar') 89 | # => [ 90 | # :program, 91 | # [ # Typeless array 92 | # [:vcall, [:@ident, "foo", [1, 0]]], 93 | # [:vcall, [:@ident, "bar", [1, 5]]] 94 | # ] 95 | # ] 96 | # 97 | # We wrap typeless arrays in this pseudo type node 98 | # so that it can be handled in the same way as other type node. 99 | class ExpressionSequenceNode < Node 100 | def type 101 | :_expression_sequence 102 | end 103 | 104 | private 105 | 106 | def raw_args 107 | sexp 108 | end 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/rspec/support/source/token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec::Support.require_rspec_support 'source/location' 4 | 5 | module RSpec 6 | module Support 7 | class Source 8 | # @private 9 | # A wrapper for Ripper token which is generated with `Ripper.lex`. 10 | class Token 11 | CLOSING_TYPES_BY_OPENING_TYPE = { 12 | :on_lbracket => :on_rbracket, 13 | :on_lparen => :on_rparen, 14 | :on_lbrace => :on_rbrace, 15 | :on_heredoc_beg => :on_heredoc_end 16 | }.freeze 17 | 18 | CLOSING_KEYWORDS_BY_OPENING_KEYWORD = { 19 | 'def' => 'end', 20 | 'do' => 'end', 21 | }.freeze 22 | 23 | attr_reader :token 24 | 25 | def self.tokens_from_ripper_tokens(ripper_tokens) 26 | ripper_tokens.map { |ripper_token| new(ripper_token) }.freeze 27 | end 28 | 29 | def initialize(ripper_token) 30 | @token = ripper_token.freeze 31 | end 32 | 33 | def location 34 | @location ||= Location.new(*token[0]) 35 | end 36 | 37 | def type 38 | token[1] 39 | end 40 | 41 | def string 42 | token[2] 43 | end 44 | 45 | def ==(other) 46 | token == other.token 47 | end 48 | 49 | alias_method :eql?, :== 50 | 51 | def inspect 52 | "#<#{self.class} #{type} #{string.inspect}>" 53 | end 54 | 55 | def keyword? 56 | type == :on_kw 57 | end 58 | 59 | def equals_operator? 60 | type == :on_op && string == '=' 61 | end 62 | 63 | def opening? 64 | opening_delimiter? || opening_keyword? 65 | end 66 | 67 | def closed_by?(other) 68 | delimiter_closed_by?(other) || keyword_closed_by?(other) 69 | end 70 | 71 | private 72 | 73 | def opening_delimiter? 74 | CLOSING_TYPES_BY_OPENING_TYPE.key?(type) 75 | end 76 | 77 | def opening_keyword? 78 | return false unless keyword? 79 | CLOSING_KEYWORDS_BY_OPENING_KEYWORD.key?(string) 80 | end 81 | 82 | def delimiter_closed_by?(other) 83 | other.type == CLOSING_TYPES_BY_OPENING_TYPE[type] 84 | end 85 | 86 | def keyword_closed_by?(other) 87 | return false unless keyword? 88 | return true if other.string == CLOSING_KEYWORDS_BY_OPENING_KEYWORD[string] 89 | 90 | # Ruby 3's `end`-less method definition: `def method_name = body` 91 | string == 'def' && other.equals_operator? && location.line == other.location.line 92 | end 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/rspec/support/spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec/support' 4 | require 'rspec/support/spec/in_sub_process' 5 | 6 | RSpec::Support.require_rspec_support "spec/deprecation_helpers" 7 | RSpec::Support.require_rspec_support "spec/diff_helpers" 8 | RSpec::Support.require_rspec_support "spec/with_isolated_stderr" 9 | RSpec::Support.require_rspec_support "spec/stderr_splitter" 10 | RSpec::Support.require_rspec_support "spec/formatting_support" 11 | RSpec::Support.require_rspec_support "spec/with_isolated_directory" 12 | RSpec::Support.require_rspec_support "ruby_features" 13 | 14 | warning_preventer = $stderr = RSpec::Support::StdErrSplitter.new($stderr) 15 | 16 | RSpec.configure do |c| 17 | c.include RSpecHelpers 18 | c.include RSpec::Support::WithIsolatedStdErr 19 | c.include RSpec::Support::FormattingSupport 20 | c.include RSpec::Support::InSubProcess 21 | 22 | unless defined?(Debugger) # debugger causes warnings when used 23 | c.before do 24 | warning_preventer.reset! 25 | end 26 | 27 | c.after do 28 | warning_preventer.verify_no_warnings! 29 | end 30 | end 31 | 32 | if c.files_to_run.one? 33 | c.full_backtrace = true 34 | c.default_formatter = 'doc' 35 | end 36 | 37 | c.filter_run_when_matching :focus 38 | 39 | c.example_status_persistence_file_path = "./spec/examples.txt" 40 | 41 | c.define_derived_metadata :failing_on_windows_ci do |meta| 42 | meta[:pending] ||= "This spec fails on Windows CI and needs someone to fix it." 43 | end if RSpec::Support::OS.windows? && ENV['CI'] 44 | end 45 | 46 | module RSpec 47 | module Support 48 | module Spec 49 | def self.setup_simplecov(&block) 50 | # Simplecov emits some ruby warnings when loaded, so silence them. 51 | old_verbose, $VERBOSE = $VERBOSE, false 52 | 53 | return if ENV['NO_COVERAGE'] || RUBY_VERSION < '1.9.3' 54 | return if RUBY_ENGINE != 'ruby' || RSpec::Support::OS.windows? 55 | 56 | # Don't load it when we're running a single isolated 57 | # test file rather than the whole suite. 58 | return if RSpec.configuration.files_to_run.one? 59 | 60 | require 'simplecov' 61 | start_simplecov(&block) 62 | rescue LoadError 63 | warn "Simplecov could not be loaded" 64 | ensure 65 | $VERBOSE = old_verbose 66 | end 67 | 68 | def self.start_simplecov(&block) 69 | SimpleCov.start do 70 | add_filter "bundle/" 71 | add_filter "tmp/" 72 | add_filter do |source_file| 73 | # Filter out `spec` directory except when it is under `lib` 74 | # (as is the case in rspec-support) 75 | source_file.filename.include?('/spec/') && !source_file.filename.include?('/lib/') 76 | end 77 | 78 | instance_eval(&block) if block 79 | end 80 | end 81 | private_class_method :start_simplecov 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/rspec/support/spec/deprecation_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RSpecHelpers 4 | def expect_deprecation_with_call_site(file, line, snippet=//) 5 | expect(RSpec.configuration.reporter).to receive(:deprecation). 6 | with(include(:deprecated => match(snippet), :call_site => include([file, line].join(':')))) 7 | end 8 | 9 | def expect_deprecation_without_call_site(snippet=//) 10 | expect(RSpec.configuration.reporter).to receive(:deprecation). 11 | with(include(:deprecated => match(snippet), :call_site => eq(nil))) 12 | end 13 | 14 | def expect_warn_deprecation_with_call_site(file, line, snippet=//) 15 | expect(RSpec.configuration.reporter).to receive(:deprecation). 16 | with(include(:message => match(snippet), :call_site => include([file, line].join(':')))) 17 | end 18 | 19 | def expect_warn_deprecation(snippet=//) 20 | expect(RSpec.configuration.reporter).to receive(:deprecation). 21 | with(include(:message => match(snippet))) 22 | end 23 | 24 | def allow_deprecation 25 | allow(RSpec.configuration.reporter).to receive(:deprecation) 26 | end 27 | 28 | def expect_no_deprecations 29 | expect(RSpec.configuration.reporter).not_to receive(:deprecation) 30 | end 31 | alias expect_no_deprecation expect_no_deprecations 32 | 33 | def expect_warning_without_call_site(expected=//) 34 | expect(::Kernel).to receive(:warn). 35 | with(match(expected).and(satisfy { |message| !(/Called from/ =~ message) })) 36 | end 37 | 38 | def expect_warning_with_call_site(file, line, expected=//) 39 | expect(::Kernel).to receive(:warn). 40 | with(match(expected).and(match(/Called from #{file}:#{line}/))) 41 | end 42 | 43 | def expect_no_warnings 44 | expect(::Kernel).not_to receive(:warn) 45 | end 46 | 47 | def allow_warning 48 | allow(::Kernel).to receive(:warn) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/rspec/support/spec/diff_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'diff/lcs' 4 | 5 | module RSpec 6 | module Support 7 | module Spec 8 | module DiffHelpers 9 | # In the updated version of diff-lcs several diff headers change format slightly 10 | # compensate for this and change minimum version in RSpec 4 11 | if ::Diff::LCS::VERSION.to_f < 1.4 12 | def one_line_header(line_number=2) 13 | "-1,#{line_number} +1,#{line_number}" 14 | end 15 | else 16 | def one_line_header(_=2) 17 | "-1 +1" 18 | end 19 | end 20 | 21 | if Diff::LCS::VERSION.to_f < 1.4 || Diff::LCS::VERSION >= "1.4.4" 22 | def removing_two_line_header 23 | "-1,3 +1" 24 | end 25 | else 26 | def removing_two_line_header 27 | "-1,3 +1,5" 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/rspec/support/spec/formatting_support.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RSpec 4 | module Support 5 | module FormattingSupport 6 | def dedent(string) 7 | string.gsub(/^\s+\|/, '').chomp 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/rspec/support/spec/in_sub_process.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RSpec 4 | module Support 5 | module InSubProcess 6 | if Process.respond_to?(:fork) && !(Ruby.jruby? && RUBY_VERSION == '1.8.7') 7 | 8 | UnmarshableObject = Struct.new(:error) 9 | 10 | # Useful as a way to isolate a global change to a subprocess. 11 | 12 | def in_sub_process(prevent_warnings=true) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize 13 | exception_reader, exception_writer = IO.pipe 14 | result_reader, result_writer = IO.pipe 15 | 16 | # Set binary mode to avoid errors surrounding ascii-8bit to utf-8 conversion 17 | # this happens with warnings on rspec-rails for example 18 | [exception_reader, exception_writer, result_reader, result_writer].each { |io| io.binmode } 19 | 20 | pid = Process.fork do 21 | warning_preventer = $stderr = RSpec::Support::StdErrSplitter.new($stderr) 22 | 23 | begin 24 | result = yield 25 | warning_preventer.verify_no_warnings! if prevent_warnings 26 | # rubocop:disable Lint/HandleExceptions 27 | rescue Support::AllExceptionsExceptOnesWeMustNotRescue => exception 28 | # rubocop:enable Lint/HandleExceptions 29 | end 30 | 31 | exception_writer.write marshal_dump_with_unmarshable_object_handling(exception) 32 | exception_reader.close 33 | exception_writer.close 34 | 35 | result_writer.write marshal_dump_with_unmarshable_object_handling(result) 36 | result_reader.close 37 | result_writer.close 38 | 39 | exit! # prevent at_exit hooks from running (e.g. minitest) 40 | end 41 | 42 | exception_writer.close 43 | result_writer.close 44 | Process.waitpid(pid) 45 | 46 | exception = Marshal.load(exception_reader.read) 47 | exception_reader.close 48 | raise exception if exception 49 | 50 | result = Marshal.load(result_reader.read) 51 | result_reader.close 52 | result 53 | end 54 | alias :in_sub_process_if_possible :in_sub_process 55 | 56 | def marshal_dump_with_unmarshable_object_handling(object) 57 | Marshal.dump(object) 58 | rescue TypeError => error 59 | Marshal.dump(UnmarshableObject.new(error)) 60 | end 61 | else 62 | def in_sub_process(*) 63 | skip "This spec requires forking to work properly, " \ 64 | "and your platform does not support forking" 65 | end 66 | 67 | def in_sub_process_if_possible(*) 68 | yield 69 | end 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/rspec/support/spec/library_wide_checks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec/support/spec/shell_out' 4 | 5 | module RSpec 6 | module Support 7 | module WhitespaceChecks 8 | # This malformed whitespace detection logic has been borrowed from bundler: 9 | # https://github.com/bundler/bundler/blob/v1.8.0/spec/quality_spec.rb 10 | def check_for_tab_characters(filename) 11 | failing_lines = [] 12 | File.readlines(filename).each_with_index do |line, number| 13 | failing_lines << number + 1 if line =~ /\t/ 14 | end 15 | 16 | return if failing_lines.empty? 17 | "#{filename} has tab characters on lines #{failing_lines.join(', ')}" 18 | end 19 | 20 | def check_for_extra_spaces(filename) 21 | failing_lines = [] 22 | File.readlines(filename).each_with_index do |line, number| 23 | next if line =~ /^\s+#.*\s+\n$/ 24 | failing_lines << number + 1 if line =~ /\s+\n$/ 25 | end 26 | 27 | return if failing_lines.empty? 28 | "#{filename} has spaces on the EOL on lines #{failing_lines.join(', ')}" 29 | end 30 | end 31 | end 32 | end 33 | 34 | RSpec.shared_examples_for "library wide checks" do |lib, options| 35 | consider_a_test_env_file = options.fetch(:consider_a_test_env_file, /MATCHES NOTHING/) 36 | allowed_loaded_feature_regexps = options.fetch(:allowed_loaded_feature_regexps, []) 37 | preamble_for_lib = options[:preamble_for_lib] 38 | preamble_for_spec = "require 'rspec/core'; require 'spec_helper'" 39 | skip_spec_files = options.fetch(:skip_spec_files, /MATCHES NOTHING/) 40 | 41 | include RSpec::Support::ShellOut 42 | include RSpec::Support::WhitespaceChecks 43 | 44 | define_method :files_to_require_for do |sub_dir| 45 | slash = File::SEPARATOR 46 | lib_path_re = /#{slash + lib}[^#{slash}]*#{slash}lib/ 47 | load_path = $LOAD_PATH.grep(lib_path_re).first 48 | directory = load_path.sub(/lib$/, sub_dir) 49 | files = Dir["#{directory}/**/*.rb"] 50 | extract_regex = /#{Regexp.escape(directory) + File::SEPARATOR}(.+)\.rb$/ 51 | 52 | # We sort to ensure the files are loaded in a consistent order, regardless 53 | # of OS. Otherwise, it could load in a different order on Travis than 54 | # locally, and potentially trigger a "circular require considered harmful" 55 | # warning or similar. 56 | files.sort.map { |file| file[extract_regex, 1] } 57 | end 58 | 59 | def command_from(code_lines) 60 | code_lines.join("\n") 61 | end 62 | 63 | def load_all_files(files, preamble, postamble=nil) 64 | requires = files.map { |f| "require '#{f}'" } 65 | command = command_from(Array(preamble) + requires + Array(postamble)) 66 | 67 | stdout, stderr, status = with_env 'NO_COVERAGE' => '1' do 68 | options = %w[ -w ] 69 | options << "--disable=gem" if RUBY_VERSION.to_f >= 1.9 && RSpec::Support::Ruby.mri? 70 | run_ruby_with_current_load_path(command, *options) 71 | end 72 | 73 | [stdout, strip_known_warnings(stderr), status.exitstatus] 74 | end 75 | 76 | define_method :load_all_lib_files do 77 | files = all_lib_files - lib_test_env_files 78 | preamble = ['orig_loaded_features = $".dup', preamble_for_lib] 79 | postamble = ['puts(($" - orig_loaded_features).join("\n"))'] 80 | 81 | @loaded_feature_lines, stderr, exitstatus = load_all_files(files, preamble, postamble) 82 | ["", stderr, exitstatus] 83 | end 84 | 85 | define_method :load_all_spec_files do 86 | files = files_to_require_for("spec") + lib_test_env_files 87 | files = files.reject { |f| f =~ skip_spec_files } 88 | load_all_files(files, preamble_for_spec) 89 | end 90 | 91 | attr_reader :all_lib_files, :lib_test_env_files, 92 | :lib_file_results, :spec_file_results 93 | 94 | before(:context) do 95 | @all_lib_files = files_to_require_for("lib") 96 | @lib_test_env_files = all_lib_files.grep(consider_a_test_env_file) 97 | 98 | @lib_file_results, @spec_file_results = [ 99 | # Load them in parallel so it's faster... 100 | Thread.new { load_all_lib_files }, 101 | Thread.new { load_all_spec_files } 102 | ].map(&:join).map(&:value) 103 | end 104 | 105 | def have_successful_no_warnings_output 106 | eq ["", "", 0] 107 | end 108 | 109 | it "issues no warnings when loaded", :slow do 110 | expect(lib_file_results).to have_successful_no_warnings_output 111 | end 112 | 113 | it "issues no warnings when the spec files are loaded", :slow do 114 | expect(spec_file_results).to have_successful_no_warnings_output 115 | end 116 | 117 | it 'only loads a known set of stdlibs so gem authors are forced ' \ 118 | 'to load libs they use to have passing specs', :slow do 119 | loaded_features = @loaded_feature_lines.split("\n") 120 | if RUBY_VERSION == '1.8.7' 121 | # On 1.8.7, $" returns the relative require path if that was used 122 | # to require the file. LIB_REGEX will not match the relative version 123 | # since it has a `/lib` prefix. Here we deal with this by expanding 124 | # relative files relative to the $LOAD_PATH dir (lib). 125 | Dir.chdir("lib") { loaded_features.map! { |f| File.expand_path(f) } } 126 | end 127 | 128 | loaded_features.reject! { |feature| RSpec::CallerFilter::LIB_REGEX =~ feature } 129 | loaded_features.reject! { |feature| allowed_loaded_feature_regexps.any? { |r| r =~ feature } } 130 | 131 | expect(loaded_features).to eq([]) 132 | end 133 | 134 | RSpec::Matchers.define :be_well_formed do 135 | match do |actual| 136 | actual.empty? 137 | end 138 | 139 | failure_message do |actual| 140 | actual.join("\n") 141 | end 142 | end 143 | 144 | it "has no malformed whitespace", :slow do 145 | error_messages = [] 146 | `git ls-files -z`.split("\x0").each do |filename| 147 | error_messages << check_for_tab_characters(filename) 148 | error_messages << check_for_extra_spaces(filename) 149 | end 150 | expect(error_messages.compact).to be_well_formed 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /lib/rspec/support/spec/shell_out.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'open3' 4 | require 'rake/file_utils' 5 | require 'shellwords' 6 | 7 | module RSpec 8 | module Support 9 | module ShellOut 10 | def with_env(vars) 11 | original = ENV.to_hash 12 | vars.each { |k, v| ENV[k] = v } 13 | 14 | begin 15 | yield 16 | ensure 17 | ENV.replace(original) 18 | end 19 | end 20 | 21 | if Open3.respond_to?(:capture3) # 1.9+ 22 | def shell_out(*command) 23 | stdout, stderr, status = Open3.capture3(*command) 24 | return stdout, filter(stderr), status 25 | end 26 | else # 1.8.7 27 | # popen3 doesn't provide the exit status so we fake it out. 28 | FakeProcessStatus = Struct.new(:exitstatus) 29 | 30 | def shell_out(*command) 31 | stdout = stderr = nil 32 | 33 | Open3.popen3(*command) do |_in, out, err| 34 | stdout = out.read 35 | stderr = err.read 36 | end 37 | 38 | status = FakeProcessStatus.new(0) 39 | return stdout, filter(stderr), status 40 | end 41 | end 42 | 43 | def run_ruby_with_current_load_path(ruby_command, *flags) 44 | command = [ 45 | FileUtils::RUBY, 46 | "-I#{$LOAD_PATH.map(&:shellescape).join(File::PATH_SEPARATOR)}", 47 | "-e", ruby_command, *flags 48 | ] 49 | 50 | # Unset these env vars because `ruby -w` will issue warnings whenever 51 | # they are set to non-default values. 52 | with_env 'RUBY_GC_HEAP_FREE_SLOTS' => nil, 'RUBY_GC_MALLOC_LIMIT' => nil, 53 | 'RUBY_FREE_MIN' => nil do 54 | shell_out(*command) 55 | end 56 | end 57 | 58 | LINES_TO_IGNORE = 59 | [ 60 | # Ignore bundler warning. 61 | %r{bundler/source/rubygems}, 62 | # Ignore bundler + rubygems warning. 63 | %r{site_ruby/\d\.\d\.\d/rubygems}, 64 | %r{jruby-\d\.\d\.\d+\.\d/lib/ruby/stdlib/rubygems}, 65 | # This is required for windows for some reason 66 | %r{lib/bundler/rubygems}, 67 | # This is a JRuby file that generates warnings on 9.0.3.0 68 | %r{lib/ruby/stdlib/jar}, 69 | # This is a JRuby file that generates warnings on 9.1.7.0 70 | %r{org/jruby/RubyKernel\.java}, 71 | # This is a JRuby gem that generates warnings on 9.1.7.0 72 | %r{ffi-1\.13\.\d+-java}, 73 | %r{uninitialized constant FFI}, 74 | # These are related to the above, there is a warning about io from FFI 75 | %r{jruby-\d\.\d\.\d+\.\d/lib/ruby/stdlib/io}, 76 | %r{io/console on JRuby shells out to stty for most operations}, 77 | # This is a JRuby 9.1.17.0 error on Github Actions 78 | %r{io/console not supported; tty will not be manipulated}, 79 | # This is a JRuby 9.2.1.x error 80 | %r{jruby/kernel/gem_prelude}, 81 | %r{lib/jruby\.jar!/jruby/preludes}, 82 | # Ignore some JRuby errors for gems 83 | %r{jruby/\d\.\d(\.\d)?/gems/aruba}, 84 | %r{jruby/\d\.\d(\.\d)?/gems/ffi}, 85 | ] 86 | 87 | def strip_known_warnings(input) 88 | input.split("\n").reject do |l| 89 | LINES_TO_IGNORE.any? { |to_ignore| l =~ to_ignore } || 90 | # Remove blank lines 91 | l == "" || l.nil? 92 | end.join("\n") 93 | end 94 | 95 | private 96 | 97 | if Ruby.jruby? 98 | def filter(output) 99 | output.each_line.reject do |line| 100 | line.include?("lib/ruby/shared/rubygems") 101 | end.join($/) 102 | end 103 | else 104 | def filter(output) 105 | output 106 | end 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/rspec/support/spec/stderr_splitter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'stringio' 4 | 5 | module RSpec 6 | module Support 7 | class StdErrSplitter 8 | def initialize(original) 9 | @orig_stderr = original 10 | @output_tracker = ::StringIO.new 11 | @last_line = nil 12 | end 13 | 14 | respond_to_name = (::RUBY_VERSION.to_f < 1.9) ? :respond_to? : :respond_to_missing? 15 | define_method respond_to_name do |*args| 16 | @orig_stderr.respond_to?(*args) || super(*args) 17 | end 18 | 19 | def method_missing(name, *args, &block) 20 | @output_tracker.__send__(name, *args, &block) if @output_tracker.respond_to?(name) 21 | @orig_stderr.__send__(name, *args, &block) 22 | end 23 | 24 | def clone 25 | StdErrSplitter.new(@orig_stderr.clone) 26 | end 27 | 28 | def ==(other) 29 | @orig_stderr == other 30 | end 31 | 32 | def reopen(*args) 33 | reset! 34 | @orig_stderr.reopen(*args) 35 | end 36 | 37 | # To work around JRuby error: 38 | # can't convert RSpec::Support::StdErrSplitter into String 39 | def to_io 40 | @orig_stderr.to_io 41 | end 42 | 43 | # To work around JRuby error: 44 | # TypeError: $stderr must have write method, RSpec::StdErrSplitter given 45 | def write(line) 46 | return if line =~ %r{^\S+/gems/\S+:\d+: warning:} # http://rubular.com/r/kqeUIZOfPG 47 | 48 | # Ruby 2.7.0 warnings from keyword arguments span multiple lines, extend check above 49 | # to look for the next line. 50 | return if @last_line =~ %r{^\S+/gems/\S+:\d+: warning:} && 51 | line =~ %r{warning: The called method .* is defined here} 52 | 53 | # Ruby 2.7.0 complains about hashes used in place of keyword arguments 54 | # Aruba 0.14.2 uses this internally triggering that here 55 | return if line =~ %r{lib/ruby/2\.7\.0/fileutils\.rb:622: warning:} 56 | 57 | @orig_stderr.write(line) 58 | @output_tracker.write(line) 59 | ensure 60 | @last_line = line 61 | end 62 | 63 | def has_output? 64 | !output.empty? 65 | end 66 | 67 | def reset! 68 | @output_tracker = ::StringIO.new 69 | end 70 | 71 | def verify_no_warnings! 72 | raise "Warnings were generated: #{output}" if has_output? 73 | reset! 74 | end 75 | 76 | def output 77 | @output_tracker.string 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/rspec/support/spec/string_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec/matchers' 4 | # Special matcher for comparing encoded strings so that 5 | # we don't run any expectation failures through the Differ, 6 | # which also relies on EncodedString. Instead, confirm the 7 | # strings have the same bytes. 8 | RSpec::Matchers.define :be_identical_string do |expected| 9 | if String.method_defined?(:encoding) 10 | match do 11 | expected_encoding? && 12 | actual.bytes.to_a == expected.bytes.to_a 13 | end 14 | 15 | failure_message do 16 | "expected\n#{actual.inspect} (#{actual.encoding.name}) to be identical to\n"\ 17 | "#{expected.inspect} (#{expected.encoding.name})\n"\ 18 | "The exact bytes are printed below for more detail:\n"\ 19 | "#{actual.bytes.to_a}\n"\ 20 | "#{expected.bytes.to_a}\n"\ 21 | end 22 | 23 | # Depends on chaining :with_same_encoding for it to 24 | # check for string encoding. 25 | def expected_encoding? 26 | if defined?(@expect_same_encoding) && @expect_same_encoding 27 | actual.encoding == expected.encoding 28 | else 29 | true 30 | end 31 | end 32 | else 33 | match do 34 | actual.split(//) == expected.split(//) 35 | end 36 | 37 | failure_message do 38 | "expected\n#{actual.inspect} to be identical to\n#{expected.inspect}\n" 39 | end 40 | end 41 | 42 | chain :with_same_encoding do 43 | @expect_same_encoding ||= true 44 | end 45 | end 46 | RSpec::Matchers.alias_matcher :a_string_identical_to, :be_identical_string 47 | RSpec::Matchers.alias_matcher :be_diffed_as, :be_identical_string 48 | -------------------------------------------------------------------------------- /lib/rspec/support/spec/with_isolated_directory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tmpdir' 4 | 5 | RSpec.shared_context "isolated directory" do 6 | around do |ex| 7 | Dir.mktmpdir do |tmp_dir| 8 | Dir.chdir(tmp_dir, &ex) 9 | end 10 | end 11 | end 12 | 13 | RSpec.configure do |c| 14 | c.include_context "isolated directory", :isolated_directory => true 15 | end 16 | -------------------------------------------------------------------------------- /lib/rspec/support/spec/with_isolated_stderr.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RSpec 4 | module Support 5 | module WithIsolatedStdErr 6 | def with_isolated_stderr 7 | original = $stderr 8 | $stderr = StringIO.new 9 | yield 10 | ensure 11 | $stderr = original 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/rspec/support/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RSpec 4 | module Support 5 | module Version 6 | STRING = '3.14.0.pre' 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/rspec/support/warnings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec/support' 4 | RSpec::Support.require_rspec_support "caller_filter" 5 | 6 | module RSpec 7 | module Support 8 | module Warnings 9 | def deprecate(deprecated, options={}) 10 | warn_with "DEPRECATION: #{deprecated} is deprecated.", options 11 | end 12 | 13 | # @private 14 | # 15 | # Used internally to print deprecation warnings 16 | # when rspec-core isn't loaded 17 | def warn_deprecation(message, options={}) 18 | warn_with "DEPRECATION: \n #{message}", options 19 | end 20 | 21 | # @private 22 | # 23 | # Used internally to print warnings 24 | def warning(text, options={}) 25 | warn_with "WARNING: #{text}.", options 26 | end 27 | 28 | # @private 29 | # 30 | # Used internally to print longer warnings 31 | def warn_with(message, options={}) 32 | call_site = options.fetch(:call_site) { CallerFilter.first_non_rspec_line } 33 | message += " Use #{options[:replacement]} instead." if options[:replacement] 34 | message += " Called from #{call_site}." if call_site 35 | Support.warning_notifier.call message 36 | end 37 | end 38 | end 39 | 40 | extend RSpec::Support::Warnings 41 | end 42 | -------------------------------------------------------------------------------- /lib/rspec/support/with_keywords_when_needed.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec::Support.require_rspec_support("method_signature_verifier") 4 | 5 | module RSpec 6 | module Support 7 | module WithKeywordsWhenNeeded 8 | # This module adds keyword sensitive support for core ruby methods 9 | # where we cannot use `ruby2_keywords` directly. 10 | 11 | module_function 12 | 13 | if RSpec::Support::RubyFeatures.kw_args_supported? 14 | # Remove this in RSpec 4 in favour of explicitly passed in kwargs where 15 | # this is used. Works around a warning in Ruby 2.7 16 | 17 | def class_exec(klass, *args, &block) 18 | if MethodSignature.new(block).has_kw_args_in?(args) 19 | binding.eval(<<-CODE, __FILE__, __LINE__) 20 | kwargs = args.pop 21 | klass.class_exec(*args, **kwargs, &block) 22 | CODE 23 | else 24 | klass.class_exec(*args, &block) 25 | end 26 | end 27 | ruby2_keywords :class_exec if respond_to?(:ruby2_keywords, true) 28 | else 29 | def class_exec(klass, *args, &block) 30 | klass.class_exec(*args, &block) 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /maintenance-branch: -------------------------------------------------------------------------------- 1 | main 2 | -------------------------------------------------------------------------------- /rspec-support.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'rspec/support/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "rspec-support" 8 | spec.version = RSpec::Support::Version::STRING 9 | spec.authors = ["David Chelimsky","Myron Marson","Jon Rowe","Sam Phippen","Xaviery Shay","Bradley Schaefer"] 10 | spec.email = "rspec-users@rubyforge.org" 11 | spec.homepage = "https://github.com/rspec/rspec-support" 12 | spec.summary = "rspec-support-#{RSpec::Support::Version::STRING}" 13 | spec.description = "Support utilities for RSpec gems" 14 | spec.license = "MIT" 15 | 16 | spec.metadata = { 17 | 'bug_tracker_uri' => 'https://github.com/rspec/rspec-support/issues', 18 | 'changelog_uri' => "https://github.com/rspec/rspec-support/blob/v#{spec.version}/Changelog.md", 19 | 'documentation_uri' => 'https://rspec.info/documentation/', 20 | 'mailing_list_uri' => 'https://groups.google.com/forum/#!forum/rspec', 21 | 'source_code_uri' => 'https://github.com/rspec/rspec-support', 22 | } 23 | 24 | spec.files = `git ls-files -- lib/*`.split("\n") 25 | spec.files += %w[README.md LICENSE.md Changelog.md] 26 | spec.test_files = [] 27 | spec.rdoc_options = ["--charset=UTF-8"] 28 | spec.require_paths = ["lib"] 29 | 30 | private_key = File.expand_path('~/.gem/rspec-gem-private_key.pem') 31 | if File.exist?(private_key) 32 | spec.signing_key = private_key 33 | spec.cert_chain = [File.expand_path('~/.gem/rspec-gem-public_cert.pem')] 34 | end 35 | 36 | spec.required_ruby_version = '>= 1.8.7' 37 | 38 | spec.add_development_dependency "rake", "> 10.0.0" 39 | spec.add_development_dependency "thread_order", "~> 1.1.0" 40 | end 41 | -------------------------------------------------------------------------------- /script/ci_functions.sh: -------------------------------------------------------------------------------- 1 | # This file was generated on 2024-09-03T15:05:50+01:00 from the rspec-dev repo. 2 | # DO NOT modify it by hand as your changes will get lost the next time it is generated. 3 | 4 | # Taken from: 5 | # https://github.com/travis-ci/travis-build/blob/e9314616e182a23e6a280199cd9070bfc7cae548/lib/travis/build/script/templates/header.sh#L34-L53 6 | ci_retry() { 7 | local result=0 8 | local count=1 9 | while [ $count -le 3 ]; do 10 | [ $result -ne 0 ] && { 11 | echo -e "\n\033[33;1mThe command \"$@\" failed. Retrying, $count of 3.\033[0m\n" >&2 12 | } 13 | "$@" 14 | result=$? 15 | [ $result -eq 0 ] && break 16 | count=$(($count + 1)) 17 | sleep 1 18 | done 19 | 20 | [ $count -eq 3 ] && { 21 | echo "\n\033[33;1mThe command \"$@\" failed 3 times.\033[0m\n" >&2 22 | } 23 | 24 | return $result 25 | } 26 | 27 | # Taken from https://github.com/vcr/vcr/commit/fa96819c92b783ec0c794f788183e170e4f684b2 28 | # and https://github.com/vcr/vcr/commit/040aaac5370c68cd13c847c076749cd547a6f9b1 29 | nano_cmd="$(type -p gdate date | head -1)" 30 | nano_format="+%s%N" 31 | [ "$(uname -s)" != "Darwin" ] || nano_format="${nano_format/%N/000000000}" 32 | 33 | fold() { 34 | local name="$1" 35 | local status=0 36 | shift 1 37 | echo "============= Starting $name ===============" 38 | 39 | "$@" 40 | status=$? 41 | 42 | if [ "$status" -eq 0 ]; then 43 | echo "============= Ending $name ===============" 44 | else 45 | STATUS="$status" 46 | fi 47 | 48 | return $status 49 | } 50 | -------------------------------------------------------------------------------- /script/clone_all_rspec_repos: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file was generated on 2024-09-03T15:05:50+01:00 from the rspec-dev repo. 3 | # DO NOT modify it by hand as your changes will get lost the next time it is generated. 4 | 5 | set -e 6 | source script/functions.sh 7 | 8 | if is_mri; then 9 | pushd .. 10 | 11 | clone_repo "rspec-metagem" "rspec" 12 | clone_repo "rspec-core" 13 | clone_repo "rspec-expectations" 14 | clone_repo "rspec-mocks" 15 | clone_repo "rspec-rails" 16 | 17 | if rspec_support_compatible; then 18 | clone_repo "rspec-support" 19 | fi 20 | 21 | popd 22 | else 23 | echo "Not cloning all repos since we are not on MRI and they are only needed for the MRI build" 24 | fi 25 | -------------------------------------------------------------------------------- /script/cucumber.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file was generated on 2024-09-03T15:05:50+01:00 from the rspec-dev repo. 3 | # DO NOT modify it by hand as your changes will get lost the next time it is generated. 4 | 5 | set -e 6 | source script/functions.sh 7 | 8 | run_cukes 9 | -------------------------------------------------------------------------------- /script/functions.sh: -------------------------------------------------------------------------------- 1 | # This file was generated on 2024-09-03T15:05:50+01:00 from the rspec-dev repo. 2 | # DO NOT modify it by hand as your changes will get lost the next time it is generated. 3 | 4 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | source $SCRIPT_DIR/ci_functions.sh 6 | source $SCRIPT_DIR/predicate_functions.sh 7 | 8 | # If JRUBY_OPTS isn't set, use these. 9 | export JRUBY_OPTS=${JRUBY_OPTS:-"--server -Xcompile.invokedynamic=false"} 10 | SPECS_HAVE_RUN_FILE=specs.out 11 | MAINTENANCE_BRANCH=`cat maintenance-branch` 12 | 13 | # Don't allow rubygems to pollute what's loaded. Also, things boot faster 14 | # without the extra load time of rubygems. Only works on MRI Ruby 1.9+ 15 | if is_mri_192_plus; then 16 | export RUBYOPT="--disable=gem" 17 | fi 18 | 19 | function clone_repo { 20 | if [ ! -d $1 ]; then # don't clone if the dir is already there 21 | if [ -z "$2" ]; then 22 | DIR_TARGET="$1" 23 | else 24 | DIR_TARGET="$2" 25 | fi 26 | 27 | if [ -z "$3" ]; then 28 | BRANCH_TO_CLONE="${MAINTENANCE_BRANCH?}"; 29 | else 30 | BRANCH_TO_CLONE="$3"; 31 | fi; 32 | 33 | ci_retry eval "git clone https://github.com/rspec/$1 --depth 1 --branch ${BRANCH_TO_CLONE?} ${DIR_TARGET?}" 34 | fi; 35 | } 36 | 37 | function run_specs_and_record_done { 38 | local rspec_bin=bin/rspec 39 | 40 | # rspec-core needs to run with a special script that loads simplecov first, 41 | # so that it can instrument rspec-core's code before rspec-core has been loaded. 42 | if [ -f script/rspec_with_simplecov ] && is_mri; then 43 | rspec_bin=script/rspec_with_simplecov 44 | fi; 45 | 46 | echo "${PWD}/bin/rspec" 47 | $rspec_bin spec --backtrace --format progress --profile --format progress --out $SPECS_HAVE_RUN_FILE 48 | } 49 | 50 | function run_cukes { 51 | if [ -d features ]; then 52 | # force jRuby to use client mode JVM or a compilation mode thats as close as possible, 53 | # idea taken from https://github.com/jruby/jruby/wiki/Improving-startup-time 54 | # 55 | # Note that we delay setting this until we run the cukes because we've seen 56 | # spec failures in our spec suite due to problems with this mode. 57 | export JAVA_OPTS='-client -XX:+TieredCompilation -XX:TieredStopAtLevel=1' 58 | 59 | echo "${PWD}/bin/cucumber" 60 | 61 | if is_mri_192; then 62 | # For some reason we get SystemStackError on 1.9.2 when using 63 | # the bin/cucumber approach below. That approach is faster 64 | # (as it avoids the bundler tax), so we use it on rubies where we can. 65 | bundle exec cucumber --strict 66 | elif is_jruby; then 67 | # For some reason JRuby doesn't like our improved bundler setup 68 | RUBYOPT="-I${PWD}/../bundle -rbundler/setup" \ 69 | PATH="${PWD}/bin:$PATH" \ 70 | bin/cucumber --strict 71 | elif is_ruby_head; then 72 | # This is a monkey patch to fix an issue with cucumber using outdated hash syntax, remove when cucumber is updated or ruby 3.4 released 73 | sed -i '$i\class Hash; alias :__initialize :initialize; def initialize(*args, **_kw, &block) = __initialize(*args, &block); end' bin/cucumber 74 | 75 | RUBYOPT="${RUBYOPT} -I${PWD}/../bundle -rbundler/setup" \ 76 | PATH="${PWD}/bin:$PATH" \ 77 | bin/cucumber --strict 78 | else 79 | # Prepare RUBYOPT for scenarios that are shelling out to ruby, 80 | # and PATH for those that are using `rspec` or `rake`. 81 | RUBYOPT="${RUBYOPT} -I${PWD}/../bundle -rbundler/setup" \ 82 | PATH="${PWD}/bin:$PATH" \ 83 | bin/cucumber --strict 84 | fi 85 | fi 86 | } 87 | 88 | function run_specs_one_by_one { 89 | echo "Running each spec file, one-by-one..." 90 | 91 | for file in `find spec -iname '*_spec.rb'`; do 92 | echo "Running $file" 93 | bin/rspec $file -b --format progress 94 | done 95 | } 96 | 97 | function run_spec_suite_for { 98 | if [ ! -f ../$1/$SPECS_HAVE_RUN_FILE ]; then # don't rerun specs that have already run 99 | if [ -d ../$1 ]; then 100 | echo "Running specs for $1" 101 | pushd ../$1 102 | unset BUNDLE_GEMFILE 103 | bundle_install_flags=`cat .github/workflows/ci.yml | grep "bundle install" | sed 's/.* bundle install//'` 104 | ci_retry eval "(unset RUBYOPT; exec bundle install $bundle_install_flags)" 105 | ci_retry eval "(unset RUBYOPT; exec bundle binstubs --all)" 106 | run_specs_and_record_done 107 | popd 108 | else 109 | echo "" 110 | echo "WARNING: The ../$1 directory does not exist. Usually the" 111 | echo "build cds into that directory and run the specs to ensure" 112 | echo "the specs still pass with your latest changes, but we are" 113 | echo "going to skip that step." 114 | echo "" 115 | fi; 116 | fi; 117 | } 118 | 119 | function check_binstubs { 120 | echo "Checking required binstubs" 121 | 122 | local success=0 123 | local binstubs="" 124 | local gems="" 125 | 126 | if [ ! -x ./bin/rspec ]; then 127 | binstubs="$binstubs bin/rspec" 128 | gems="$gems rspec-core" 129 | success=1 130 | fi 131 | 132 | if [ ! -x ./bin/rake ]; then 133 | binstubs="$binstubs bin/rake" 134 | gems="$gems rake" 135 | success=1 136 | fi 137 | 138 | if [ -d features ]; then 139 | if [ ! -x ./bin/cucumber ]; then 140 | binstubs="$binstubs bin/cucumber" 141 | gems="$gems cucumber" 142 | success=1 143 | fi 144 | fi 145 | 146 | if [ $success -eq 1 ]; then 147 | echo 148 | echo "Missing binstubs:$binstubs" 149 | echo "Install missing binstubs using one of the following:" 150 | echo 151 | echo " # Create the missing binstubs" 152 | echo " $ bundle binstubs$gems" 153 | echo 154 | echo " # To binstub all gems" 155 | echo " $ bundle binstubs --all" 156 | fi 157 | 158 | return $success 159 | } 160 | 161 | function check_documentation_coverage { 162 | echo "bin/yard stats --list-undoc" 163 | 164 | bin/yard stats --list-undoc | ruby -e " 165 | while line = gets 166 | has_warnings ||= line.start_with?('[warn]:') 167 | coverage ||= line[/([\d\.]+)% documented/, 1] 168 | puts line 169 | end 170 | 171 | unless Float(coverage) == 100 172 | puts \"\n\nMissing documentation coverage (currently at #{coverage}%)\" 173 | exit(1) 174 | end 175 | 176 | if has_warnings 177 | puts \"\n\nYARD emitted documentation warnings.\" 178 | exit(1) 179 | end 180 | " 181 | 182 | # Some warnings only show up when generating docs, so do that as well. 183 | bin/yard doc --no-cache | ruby -e " 184 | while line = gets 185 | has_warnings ||= line.start_with?('[warn]:') 186 | has_errors ||= line.start_with?('[error]:') 187 | puts line 188 | end 189 | 190 | if has_warnings || has_errors 191 | puts \"\n\nYARD emitted documentation warnings or errors.\" 192 | exit(1) 193 | end 194 | " 195 | } 196 | 197 | function check_style_and_lint { 198 | echo "bin/rubocop lib" 199 | eval "(unset RUBYOPT; exec bin/rubocop lib)" 200 | } 201 | 202 | function run_all_spec_suites { 203 | fold "rspec-core specs" run_spec_suite_for "rspec-core" 204 | fold "rspec-expectations specs" run_spec_suite_for "rspec-expectations" 205 | fold "rspec-mocks specs" run_spec_suite_for "rspec-mocks" 206 | if rspec_rails_compatible; then 207 | fold "rspec-rails specs" run_spec_suite_for "rspec-rails" 208 | fi 209 | 210 | if rspec_support_compatible; then 211 | fold "rspec-support specs" run_spec_suite_for "rspec-support" 212 | fi 213 | } 214 | -------------------------------------------------------------------------------- /script/legacy_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file was generated on 2024-09-03T15:05:50+01:00 from the rspec-dev repo. 3 | # DO NOT modify it by hand as your changes will get lost the next time it is generated. 4 | 5 | set -e 6 | source script/functions.sh 7 | 8 | bundle install --standalone --binstubs --without coverage documentation 9 | 10 | if [ -x ./bin/rspec ]; then 11 | echo "RSpec bin detected" 12 | else 13 | if [ -x ./exe/rspec ]; then 14 | cp ./exe/rspec ./bin/rspec 15 | echo "RSpec restored from exe" 16 | else 17 | echo "No RSpec bin available" 18 | exit 1 19 | fi 20 | fi 21 | -------------------------------------------------------------------------------- /script/predicate_functions.sh: -------------------------------------------------------------------------------- 1 | # This file was generated on 2024-09-03T15:05:50+01:00 from the rspec-dev repo. 2 | # DO NOT modify it by hand as your changes will get lost the next time it is generated. 3 | 4 | function is_mri { 5 | if ruby -e "exit(!defined?(RUBY_ENGINE) || RUBY_ENGINE == 'ruby')"; then 6 | # RUBY_ENGINE only returns 'ruby' on MRI. 7 | # MRI 1.8.7 lacks the constant but all other rubies have it (including JRuby in 1.8 mode) 8 | return 0 9 | else 10 | return 1 11 | fi; 12 | } 13 | 14 | function is_ruby_head { 15 | # This checks for the presence of our CI's ruby-head env variable 16 | if [ -z ${RUBY_HEAD+x} ]; then 17 | return 1 18 | else 19 | return 0 20 | fi; 21 | } 22 | 23 | function supports_cross_build_checks { 24 | if is_mri; then 25 | # We don't run cross build checks on ruby-head 26 | if is_ruby_head; then 27 | return 1 28 | else 29 | return 0 30 | fi 31 | else 32 | return 1 33 | fi 34 | } 35 | 36 | function is_jruby { 37 | if ruby -e "exit(defined?(RUBY_PLATFORM) && RUBY_PLATFORM == 'java')"; then 38 | # RUBY_ENGINE only returns 'ruby' on MRI. 39 | # MRI 1.8.7 lacks the constant but all other rubies have it (including JRuby in 1.8 mode) 40 | return 0 41 | else 42 | return 1 43 | fi; 44 | } 45 | 46 | function is_mri_192 { 47 | if is_mri; then 48 | if ruby -e "exit(RUBY_VERSION == '1.9.2')"; then 49 | return 0 50 | else 51 | return 1 52 | fi 53 | else 54 | return 1 55 | fi 56 | } 57 | 58 | function is_mri_192_plus { 59 | if is_mri; then 60 | if ruby -e "exit(RUBY_VERSION.to_f > 1.8)"; then 61 | return 0 62 | else 63 | return 1 64 | fi 65 | else 66 | return 1 67 | fi 68 | } 69 | 70 | function is_mri_2plus { 71 | if is_mri; then 72 | if ruby -e "exit(RUBY_VERSION.to_f > 2.0)"; then 73 | return 0 74 | else 75 | return 1 76 | fi 77 | else 78 | return 1 79 | fi 80 | } 81 | 82 | function is_ruby_23_plus { 83 | if ruby -e "exit(RUBY_VERSION.to_f >= 2.3)"; then 84 | return 0 85 | else 86 | return 1 87 | fi 88 | } 89 | 90 | function is_ruby_25_plus { 91 | if ruby -e "exit(RUBY_VERSION.to_f >= 2.5)"; then 92 | return 0 93 | else 94 | return 1 95 | fi 96 | } 97 | 98 | function is_ruby_27_plus { 99 | if ruby -e "exit(RUBY_VERSION.to_f >= 2.7)"; then 100 | return 0 101 | else 102 | return 1 103 | fi 104 | } 105 | 106 | function is_ruby_31_plus { 107 | if ruby -e "exit(RUBY_VERSION.to_f >= 3.1)"; then 108 | return 0 109 | else 110 | return 1 111 | fi 112 | } 113 | 114 | function rspec_rails_compatible { 115 | if is_ruby_27_plus; then 116 | return 0 117 | else 118 | return 1 119 | fi 120 | } 121 | 122 | function rspec_support_compatible { 123 | if [ "$MAINTENANCE_BRANCH" != "2-99-maintenance" ] && [ "$MAINTENANCE_BRANCH" != "2-14-maintenance" ]; then 124 | return 0 125 | else 126 | return 1 127 | fi 128 | } 129 | 130 | function additional_specs_available { 131 | type run_additional_specs > /dev/null 2>&1 132 | return $? 133 | } 134 | 135 | function documentation_enforced { 136 | if [ -x ./bin/yard ]; then 137 | if is_mri_2plus; then 138 | return 0 139 | else 140 | return 1 141 | fi 142 | else 143 | return 1 144 | fi 145 | } 146 | 147 | function style_and_lint_enforced { 148 | if is_ruby_head; then 149 | return 1 150 | else 151 | if [ -x ./bin/rubocop ]; then 152 | return 0 153 | else 154 | return 1 155 | fi 156 | fi 157 | } 158 | -------------------------------------------------------------------------------- /script/run_build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file was generated on 2024-09-03T15:05:50+01:00 from the rspec-dev repo. 3 | # DO NOT modify it by hand as your changes will get lost the next time it is generated. 4 | 5 | set -e 6 | source script/functions.sh 7 | 8 | # Allow repos to override the default functions and add their own 9 | if [ -f script/custom_build_functions.sh ]; then 10 | source script/custom_build_functions.sh 11 | fi 12 | 13 | fold "binstub check" check_binstubs 14 | 15 | fold "specs" run_specs_and_record_done 16 | 17 | if additional_specs_available; then 18 | fold "additional specs" run_additional_specs 19 | fi 20 | 21 | fold "cukes" run_cukes 22 | 23 | if documentation_enforced; then 24 | fold "doc check" check_documentation_coverage 25 | fi 26 | 27 | if supports_cross_build_checks; then 28 | fold "one-by-one specs" run_specs_one_by_one 29 | export NO_COVERAGE=true 30 | run_all_spec_suites 31 | else 32 | echo "Skipping the rest of the build on non-MRI rubies" 33 | fi 34 | -------------------------------------------------------------------------------- /script/run_rubocop: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file was generated on 2024-09-03T15:05:50+01:00 from the rspec-dev repo. 3 | # DO NOT modify it by hand as your changes will get lost the next time it is generated. 4 | 5 | set -e 6 | source script/functions.sh 7 | 8 | # Allow repos to override the default functions and add their own 9 | if [ -f script/custom_build_functions.sh ]; then 10 | source script/custom_build_functions.sh 11 | fi 12 | 13 | 14 | fold "rubocop" check_style_and_lint 15 | -------------------------------------------------------------------------------- /script/update_rubygems_and_install_bundler: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file was generated on 2024-09-03T15:05:50+01:00 from the rspec-dev repo. 3 | # DO NOT modify it by hand as your changes will get lost the next time it is generated. 4 | 5 | set -e 6 | source script/functions.sh 7 | 8 | if is_ruby_31_plus; then 9 | echo "Installing most recent rubygems / bundler" 10 | yes | gem update --no-document --system 11 | yes | gem install --no-document bundler 12 | elif is_ruby_23_plus; then 13 | echo "Installing rubygems 3.2.22 / bundler 2.2.22" 14 | yes | gem update --system '3.2.22' 15 | yes | gem install bundler -v '2.2.22' 16 | else 17 | echo "Warning installing older versions of Rubygems / Bundler" 18 | gem update --system '2.7.8' 19 | gem install bundler -v '1.17.3' 20 | fi 21 | -------------------------------------------------------------------------------- /spec/rspec/support/caller_filter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fileutils' 3 | require 'rspec/support/caller_filter' 4 | 5 | module RSpec 6 | describe CallerFilter do 7 | it 'can receive skip_frames and increment arguments' do 8 | expect(RSpec::CallerFilter.first_non_rspec_line(1, 5)).to include("#{__FILE__}:#{__LINE__}") 9 | end 10 | 11 | it 'returns the immediate caller when called from a spec' do 12 | expect(RSpec::CallerFilter.first_non_rspec_line).to include("#{__FILE__}:#{__LINE__}") 13 | end 14 | 15 | describe "the filtering regex" do 16 | def ruby_files_in_lib(lib) 17 | # http://rubular.com/r/HYpUMftlG2 18 | path = $LOAD_PATH.find { |p| p.match(/\/rspec-#{lib}(-[a-f0-9]+)?\/lib/) } 19 | 20 | Dir["#{path}/**/*.rb"].sort.tap do |files| 21 | # Just a sanity check... 22 | expect(files.count).to be > 5 23 | end 24 | end 25 | 26 | def unmatched_from(files) 27 | files.reject { |file| file.match(CallerFilter::IGNORE_REGEX) } 28 | end 29 | 30 | %w[ core mocks expectations support ].each do |lib| 31 | it "matches all ruby files in rspec-#{lib}" do 32 | files = ruby_files_in_lib(lib) 33 | expect(unmatched_from files).to eq([]) 34 | end 35 | end 36 | 37 | it "does not match other ruby files" do 38 | files = %w[ 39 | /path/to/lib/rspec/some-extension/foo.rb 40 | /path/to/spec/rspec/core/some_spec.rb 41 | ] 42 | 43 | expect(unmatched_from files).to eq(files) 44 | end 45 | 46 | def in_rspec_support_lib(name) 47 | root = File.expand_path("../../../../lib/rspec/support", __FILE__) 48 | dir = "#{root}/#{name}" 49 | FileUtils.mkdir(dir) 50 | yield dir 51 | ensure 52 | FileUtils.rm_rf(dir) 53 | end 54 | 55 | it 'does not match rubygems lines from `require` statements' do 56 | with_isolated_stderr do 57 | require 'rubygems' # ensure rubygems is loaded 58 | end 59 | 60 | in_rspec_support_lib("test_dir") do |dir| 61 | File.open("#{dir}/file.rb", "w") do |file| 62 | file.write("$_caller_filter = RSpec::CallerFilter.first_non_rspec_line") 63 | end 64 | 65 | $_caller_filter = nil 66 | 67 | expect { 68 | require "rspec/support/test_dir/file" 69 | }.to change { $_caller_filter }.to(include __FILE__) 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/rspec/support/comparable_version_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/support/comparable_version' 2 | 3 | module RSpec::Support 4 | RSpec.describe ComparableVersion do 5 | describe '#<=>' do 6 | [ 7 | ['1.2.3', '1.2.3', 0], 8 | ['1.2.4', '1.2.3', 1], 9 | ['1.3.0', '1.2.3', 1], 10 | ['1.2.3', '1.2.4', -1], 11 | ['1.2.3', '1.3.0', -1], 12 | ['1.2.10', '1.2.3', 1], 13 | ['1.2.3', '1.2.10', -1], 14 | ['1.2.3.0', '1.2.3', 0], 15 | ['1.2.3', '1.2.3.0', 0], 16 | ['1.2.3.1', '1.2.3', 1], 17 | ['1.2.3.1', '1.2.3.0', 1], 18 | ['1.2.3', '1.2.3.1', -1], 19 | ['1.2.3.0', '1.2.3.1', -1], 20 | ['1.2.3.rc1', '1.2.3', -1], 21 | ['1.2.3.rc1', '1.2.3.rc2', -1], 22 | ['1.2.3.rc2', '1.2.3.rc10', -1], 23 | ['1.2.3.alpha2', '1.2.3.beta1', -1], 24 | ['1.2.3', '1.2.3.rc1', 1], 25 | ['1.2.3.rc2', '1.2.3.rc1', 1], 26 | ['1.2.3.rc10', '1.2.3.rc2', 1], 27 | ['1.2.3.beta1', '1.2.3.alpha2', 1] 28 | ].each do |subject_string, other_string, expected| 29 | context "with #{subject_string.inspect} and #{other_string.inspect}" do 30 | subject do 31 | ComparableVersion.new(subject_string) <=> ComparableVersion.new(other_string) 32 | end 33 | 34 | it { is_expected.to eq(expected) } 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/rspec/support/deprecation_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/matchers/fail_matchers' 2 | 3 | RSpec.describe RSpecHelpers do 4 | def deprecate!(message) 5 | RSpec.configuration.reporter.deprecation(:message => message) 6 | end 7 | 8 | def fail_with(snippet) 9 | raise_error(RSpec::Mocks::MockExpectationError, snippet) 10 | end 11 | 12 | def raise_unrelated_expectation! 13 | raise(RSpec::Expectations::ExpectationNotMetError, 'abracadabra') 14 | end 15 | 16 | describe '#expect_no_deprecations' do 17 | shared_examples_for 'expects no deprecations' do 18 | it 'passes when there were no deprecations' do 19 | expectation 20 | end 21 | 22 | it 'fails when there was a deprecation warning' do 23 | in_sub_process do 24 | expect { 25 | expectation 26 | deprecate!('foo') 27 | }.to fail_with(/received: 1 time/) 28 | end 29 | end 30 | 31 | it 'fails with a MockExpectationError when there was also an ExpectationNotMetError' do 32 | in_sub_process do 33 | expect { 34 | expectation 35 | deprecate!('bar') 36 | raise_unrelated_expectation! 37 | }.to fail_with(/received: 1 time/) 38 | end 39 | end 40 | end 41 | 42 | it_behaves_like 'expects no deprecations' do 43 | def expectation 44 | expect_no_deprecations 45 | end 46 | end 47 | 48 | # Alias 49 | it_behaves_like 'expects no deprecations' do 50 | def expectation 51 | expect_no_deprecation 52 | end 53 | end 54 | end 55 | 56 | describe '#expect_warn_deprecation' do 57 | it 'passes when there was a deprecation warning' do 58 | in_sub_process do 59 | expect_warn_deprecation(/bar/) 60 | deprecate!('bar') 61 | end 62 | end 63 | 64 | pending 'fails when there were no deprecations' do 65 | in_sub_process do 66 | expect { 67 | expect_warn_deprecation(/bar/) 68 | }.to raise_error(/received: 0 times/) 69 | end 70 | end 71 | 72 | it 'fails with a MockExpectationError when there was also an ExpectationNotMetError' do 73 | in_sub_process do 74 | expect { 75 | expect_warn_deprecation(/bar/) 76 | deprecate!('bar') 77 | raise_unrelated_expectation! 78 | }.to raise_error(RSpec::Expectations::ExpectationNotMetError) 79 | end 80 | end 81 | 82 | it 'fails when deprecation message is different' do 83 | in_sub_process do 84 | expect { 85 | expect_warn_deprecation(/bar/) 86 | deprecate!('foo') 87 | }.to raise_error(%r{match /bar/}) 88 | end 89 | end 90 | 91 | it 'fails when deprecation message is different and an ExpectationNotMetError was raised' do 92 | in_sub_process do 93 | expect { 94 | expect_warn_deprecation(/bar/) 95 | deprecate!('foo') 96 | raise_unrelated_expectation! 97 | }.to raise_error(%r{match /bar/}) 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/rspec/support/directory_maker_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "fileutils" 3 | 4 | RSpec::Support.require_rspec_support("directory_maker") 5 | 6 | module RSpec::Support 7 | RSpec.describe DirectoryMaker do 8 | shared_examples_for "an mkdir_p implementation" do 9 | include_context "isolated directory" 10 | 11 | let(:dirname) { File.join(%w[tmp a recursive structure]) } 12 | 13 | def directory_exists?(dirname) 14 | File.exist?(dirname) && File.directory?(dirname) 15 | end 16 | 17 | it "makes directories recursively" do 18 | mkdir_p.call(dirname) 19 | expect(directory_exists?(dirname)).to be true 20 | end 21 | 22 | it "does not raise if the directory already exists" do 23 | Dir.mkdir("tmp") 24 | mkdir_p.call(dirname) 25 | expect(directory_exists?(dirname)).to be true 26 | end 27 | 28 | context "when a file already exists" do 29 | before { File.open("tmp", "w") } 30 | 31 | it "raises, as it can't make the directory", :failing_on_windows_ci do 32 | expect { 33 | mkdir_p.call(dirname) 34 | }.to raise_error(Errno::EEXIST) 35 | end 36 | end 37 | 38 | context "when the path specified is absolute" do 39 | let(:dirname) { "bees/ponies" } 40 | 41 | it "makes directories recursively" do 42 | mkdir_p.call(File.expand_path(dirname)) 43 | expect(directory_exists?(dirname)).to be true 44 | end 45 | end 46 | end 47 | 48 | describe ".mkdir_p" do 49 | subject(:mkdir_p) { DirectoryMaker.method(:mkdir_p) } 50 | it_behaves_like "an mkdir_p implementation" 51 | end 52 | 53 | describe "FileUtils.mkdir_p" do 54 | subject(:mkdir_p) { FileUtils.method(:mkdir_p) } 55 | it_behaves_like "an mkdir_p implementation" 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/rspec/support/fuzzy_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec/support/fuzzy_matcher' 3 | 4 | module RSpec 5 | module Support 6 | RSpec.describe FuzzyMatcher, ".values_match?" do 7 | matcher :match_against do |actual| 8 | match { |expected| FuzzyMatcher.values_match?(expected, actual) } 9 | end 10 | 11 | it 'returns true when given equal values' do 12 | expect(1).to match_against(1.0) 13 | end 14 | 15 | it 'returns false when given unequal values that do not provide match logic' do 16 | expect(1).not_to match_against(1.1) 17 | end 18 | 19 | it 'can match a regex against a string' do 20 | expect(/foo/).to match_against("foobar") 21 | expect(/foo/).not_to match_against("fobar") 22 | end 23 | 24 | it 'can match a regex against itself' do 25 | expect(/foo/).to match_against(/foo/) 26 | expect(/foo/).not_to match_against(/bar/) 27 | end 28 | 29 | it 'can match a class against an instance' do 30 | expect(String).to match_against("foo") 31 | expect(String).not_to match_against(123) 32 | end 33 | 34 | it 'can match a class against itself' do 35 | expect(String).to match_against(String) 36 | expect(String).not_to match_against(Regexp) 37 | end 38 | 39 | it 'can match against a matcher' do 40 | expect(be_within(0.1).of(2)).to match_against(2.05) 41 | expect(be_within(0.1).of(2)).not_to match_against(2.15) 42 | end 43 | 44 | it 'does not ask the second argument if it fuzzy matches (===)' do 45 | expect("foo").not_to match_against(String) 46 | end 47 | 48 | context "when given two 0-arg lambdas" do 49 | it 'returns true when given the same lambda' do 50 | k = lambda { 3 } 51 | expect(k).to match_against(k) 52 | end 53 | 54 | it 'returns false when given different lambdas' do 55 | expect(lambda { 3 }).not_to match_against(lambda { 4 }) 56 | end 57 | end 58 | 59 | context "when given an object whose implementation of `==` wrongly assumes it will only be called with objects of the same type" do 60 | Color = Struct.new(:r, :g, :b) do 61 | def ==(other) 62 | other.r == r && other.g == g && other.b == b 63 | end 64 | end 65 | 66 | before(:context) do 67 | expect { Color.new(0, 0, 0) == Object.new }.to raise_error(NoMethodError, /undefined method [`']r'/) 68 | end 69 | 70 | it 'can match against an expected value that matches anything' do 71 | anything = Object.new.tap do |o| 72 | def o.===(*); true; end 73 | end 74 | 75 | expect(anything).to match_against(Color.new(0, 0, 0)) 76 | end 77 | 78 | it 'surfaces the `NoMethodError` when used as the expected value' do 79 | expect { 80 | FuzzyMatcher.values_match?(Color.new(0, 0, 0), Object.new) 81 | }.to raise_error(NoMethodError, /undefined method [`']r'/) 82 | end 83 | 84 | it 'can match against objects of the same type' do 85 | expect(Color.new(0, 0, 0)).to match_against(Color.new(0, 0, 0)) 86 | expect(Color.new(0, 0, 0)).not_to match_against(Color.new(0, 1, 0)) 87 | end 88 | end 89 | 90 | context "when given an object whose implementation of `==` raises an ArgumentError" do 91 | it 'surfaces the error' do 92 | klass = Class.new do 93 | attr_accessor :foo 94 | def ==(other) 95 | other.foo == foo 96 | end 97 | end 98 | instance = klass.new 99 | 100 | other = Object.new 101 | def other.foo(arg); end 102 | 103 | expect { instance == other }.to raise_error(ArgumentError) 104 | expect { FuzzyMatcher.values_match?(instance, other) }.to raise_error(ArgumentError) 105 | end 106 | end 107 | 108 | it "does not match a struct against an array" do 109 | struct = Struct.new(:foo, :bar).new("first", 2) 110 | expect(["first", 2]).not_to match_against(struct) 111 | end 112 | 113 | context "when given two arrays" do 114 | it 'returns true if they have equal values' do 115 | expect([1, 2.0]).to match_against([1.0, 2]) 116 | end 117 | 118 | it 'returns false when given unequal values that do not provide match logic' do 119 | expect([1, 2.0]).not_to match_against([1.1, 2]) 120 | end 121 | 122 | it 'does the fuzzy matching on the individual elements' do 123 | expect([String, Integer]).to match_against(["a", 2]) 124 | expect([String, Integer]).not_to match_against([2, "a"]) 125 | end 126 | 127 | it 'returns false if they have a different number of elements' do 128 | expect([String, Integer]).not_to match_against(['a', 2, nil]) 129 | end 130 | 131 | it 'supports arbitrary nested arrays' do 132 | a1 = [ 133 | [String, Integer, [be_within(0.1).of(2)]], 134 | 3, [[[ /foo/ ]]] 135 | ] 136 | 137 | a2 = [ 138 | ["a", 1, [2.05]], 139 | 3, [[[ "foobar" ]]] 140 | ] 141 | 142 | expect(a1).to match_against(a2) 143 | a2[0][2][0] += 1 144 | expect(a1).not_to match_against(a2) 145 | end 146 | end 147 | 148 | it 'can match an array an arbitrary enumerable' do 149 | my_enum = Class.new do 150 | include Enumerable 151 | 152 | def each 153 | yield 1; yield "foo" 154 | end 155 | end.new 156 | 157 | expect([Integer, String]).to match_against(my_enum) 158 | expect([String, Integer]).not_to match_against(my_enum) 159 | end 160 | 161 | it 'does not match an empty hash against an empty array or vice-versa' do 162 | expect({}).not_to match_against([]) 163 | expect([]).not_to match_against({}) 164 | end 165 | 166 | context 'when given two hashes' do 167 | it 'returns true when their keys and values are equal' do 168 | expect(:a => 5, :b => 2.0).to match_against(:a => 5.0, :b => 2) 169 | end 170 | 171 | it 'returns false when given unequal values that do not provide match logic' do 172 | expect(:a => 5).not_to match_against(:a => 5.1) 173 | end 174 | 175 | it 'does the fuzzy matching on the individual values' do 176 | expect(:a => String, :b => /bar/).to match_against(:a => "foo", :b => "barn") 177 | expect(:a => String, :b => /bar/).not_to match_against(:a => "foo", :b => "brn") 178 | end 179 | 180 | it 'returns false if the expected hash has nil values that are not in the actual hash' do 181 | expect(:a => 'b', :b => nil).not_to match_against(:a => "b") 182 | end 183 | 184 | it 'returns false if actual hash has extra entries' do 185 | expect(:a => 'b').not_to match_against(:a => "b", :b => nil) 186 | end 187 | 188 | it 'does not fuzzy match on keys' do 189 | expect(/foo/ => 1).not_to match_against("foo" => 1) 190 | end 191 | 192 | it 'supports arbitrary nested hashes' do 193 | h1 = { 194 | :a => { 195 | :b => [String, Integer], 196 | :c => { :d => be_within(0.1).of(2) } 197 | } 198 | } 199 | 200 | h2 = { 201 | :a => { 202 | :b => ["foo", 5], 203 | :c => { :d => 2.05 } 204 | } 205 | } 206 | 207 | expect(h1).to match_against(h2) 208 | h2[:a][:c][:d] += 1 209 | expect(h1).not_to match_against(h2) 210 | end 211 | end 212 | end 213 | end 214 | end 215 | 216 | -------------------------------------------------------------------------------- /spec/rspec/support/matcher_definition_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module RSpec 4 | module Support 5 | RSpec.describe "matcher definitions" do 6 | RSpec::Matchers.define :fake_matcher do |expected| 7 | match { |actual| expected == actual } 8 | description { :fake_matcher } 9 | end 10 | 11 | RSpec::Matchers.define :matcher_with_no_description do 12 | match { true } 13 | undef description 14 | end 15 | 16 | describe ".rspec_description_for_object" do 17 | it "returns the object for a non matcher object" do 18 | o = Object.new 19 | expect(RSpec::Support.rspec_description_for_object(o)).to be o 20 | end 21 | 22 | it "returns the object's description for a matcher object that has a description" do 23 | expect(RSpec::Support.rspec_description_for_object(fake_matcher(nil))).to eq :fake_matcher 24 | end 25 | 26 | it "returns the object for a matcher that does not have a description" do 27 | matcher = matcher_with_no_description 28 | 29 | expect(matcher_with_no_description).not_to respond_to(:description) 30 | expect(RSpec::Support.is_a_matcher?(matcher_with_no_description)).to eq true 31 | 32 | expect(RSpec::Support.rspec_description_for_object(matcher)).to be matcher 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/rspec/support/mutex_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/support/mutex' 2 | 3 | RSpec.describe RSpec::Support::Mutex do 4 | it "allows ::Mutex to be mocked", :if => defined?(::Mutex) do 5 | expect(Mutex).to receive(:new) 6 | ::Mutex.new 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/rspec/support/recursive_const_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/support/recursive_const_methods' 2 | 3 | module RSpec 4 | module Support 5 | RSpec.describe RecursiveConstMethods do 6 | include described_class 7 | 8 | module Foo 9 | class Parent 10 | UNDETECTED = 'Not seen when looking up constants in Bar' 11 | end 12 | 13 | class Bar < Parent 14 | VAL = 10 15 | end 16 | end 17 | 18 | describe '#recursive_const_defined?' do 19 | it 'finds constants' do 20 | const, _ = recursive_const_defined?('::RSpec::Support::Foo::Bar::VAL') 21 | 22 | expect(const).to eq(10) 23 | end 24 | 25 | it 'returns the fully qualified name of the constant' do 26 | _, name = recursive_const_defined?('::RSpec::Support::Foo::Bar::VAL') 27 | 28 | expect(name).to eq('RSpec::Support::Foo::Bar::VAL') 29 | end 30 | 31 | it 'does not find constants in ancestors' do 32 | expect(recursive_const_defined?('::RSpec::Support::Foo::Bar::UNDETECTED')).to be_falsy 33 | end 34 | 35 | it 'does not blow up on buggy classes that raise weird errors on `to_str`' do 36 | allow(Foo::Bar).to receive(:to_str).and_raise("boom!") 37 | const, _ = recursive_const_defined?('::RSpec::Support::Foo::Bar::VAL') 38 | 39 | expect(const).to eq(10) 40 | end 41 | end 42 | 43 | describe '#recursive_const_get' do 44 | it 'gets constants' do 45 | expect(recursive_const_get('::RSpec::Support::Foo::Bar::VAL')).to eq(10) 46 | end 47 | 48 | it 'does not get constants in ancestors' do 49 | expect do 50 | recursive_const_get('::RSpec::Support::Foo::Bar::UNDETECTED') 51 | end.to raise_error(NameError) 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/rspec/support/reentrant_mutex_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/support/reentrant_mutex' 2 | require 'thread_order' 3 | 4 | # There are no assertions specifically 5 | # They are pass if they don't deadlock 6 | RSpec.describe RSpec::Support::ReentrantMutex do 7 | let!(:mutex) { described_class.new } 8 | let!(:order) { ThreadOrder.new } 9 | after { order.apocalypse! } 10 | 11 | it 'can repeatedly synchronize within the same thread' do 12 | mutex.synchronize { mutex.synchronize { } } 13 | end 14 | 15 | it 'locks other threads out while in the synchronize block' do 16 | order.declare(:before) { mutex.synchronize { } } 17 | order.declare(:within) { mutex.synchronize { } } 18 | order.declare(:after) { mutex.synchronize { } } 19 | 20 | order.pass_to :before, :resume_on => :exit 21 | mutex.synchronize { order.pass_to :within, :resume_on => :sleep } 22 | order.pass_to :after, :resume_on => :exit 23 | end 24 | 25 | it 'resumes the next thread once all its synchronize blocks have completed' do 26 | order.declare(:thread) { mutex.synchronize { } } 27 | mutex.synchronize { order.pass_to :thread, :resume_on => :sleep } 28 | order.join_all 29 | end 30 | 31 | # On Ruby 3.1.3+, 3.2.0 and RUBY_HEAD the raise in this spec can 32 | # bypass the `raise_error` capture and break this spec but 33 | # it is not sufficient to pend it as the raise can escape to the other 34 | # threads somehow therefore poisoning them so its skipped entirely. 35 | # This is a temporary work around to allow green cross project builds but 36 | # needs a fix. 37 | if RUBY_VERSION >= '3.0' && RUBY_VERSION < '3.1.3' && !ENV['RUBY_HEAD'] 38 | it 'waits when trying to lock from another Fiber' do 39 | mutex.synchronize do 40 | ready = false 41 | f = Fiber.new do 42 | expect { 43 | ready = true 44 | mutex.send(:enter) 45 | raise 'should reach here: mutex is already locked on different Fiber' 46 | }.to raise_error(Exception, 'waited correctly') 47 | end 48 | 49 | main_thread = Thread.current 50 | 51 | t = Thread.new do 52 | Thread.pass until ready && main_thread.stop? 53 | main_thread.raise Exception, 'waited correctly' 54 | end 55 | f.resume 56 | t.join 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/rspec/support/ruby_features_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/support/ruby_features' 2 | 3 | module RSpec 4 | module Support 5 | RSpec.describe OS do 6 | 7 | describe ".windows?" do 8 | %w[cygwin mswin mingw bccwin wince emx].each do |fragment| 9 | it "returns true when host os is #{fragment}" do 10 | stub_const("RbConfig::CONFIG", 'host_os' => fragment) 11 | expect(OS.windows?).to be true 12 | end 13 | end 14 | 15 | %w[darwin linux].each do |fragment| 16 | it "returns false when host os is #{fragment}" do 17 | stub_const("RbConfig::CONFIG", 'host_os' => fragment) 18 | expect(OS.windows?).to be false 19 | end 20 | end 21 | end 22 | 23 | describe ".windows_file_path?" do 24 | it "returns true when the file alt separator is a colon" do 25 | stub_const("File::ALT_SEPARATOR", "\\") unless OS.windows? 26 | expect(OS).to be_windows_file_path 27 | end 28 | 29 | it "returns false when file alt separator is not present" do 30 | stub_const("File::ALT_SEPARATOR", nil) if OS.windows? 31 | expect(OS).to_not be_windows_file_path 32 | end 33 | end 34 | end 35 | 36 | RSpec.describe Ruby do 37 | specify "jruby? reflects the state of RUBY_PLATFORM" do 38 | stub_const("RUBY_PLATFORM", "java") 39 | expect(Ruby).to be_jruby 40 | stub_const("RUBY_PLATFORM", "") 41 | expect(Ruby).to_not be_jruby 42 | end 43 | 44 | specify "rbx? reflects the state of RUBY_ENGINE" do 45 | stub_const("RUBY_ENGINE", "rbx") 46 | expect(Ruby).to be_rbx 47 | hide_const("RUBY_ENGINE") 48 | expect(Ruby).to_not be_rbx 49 | end 50 | 51 | specify "jruby_9000? reflects the state of RUBY_PLATFORM and JRUBY_VERSION" do 52 | stub_const("RUBY_PLATFORM", "java") 53 | stub_const("JRUBY_VERSION", "") 54 | expect(Ruby).to_not be_jruby_9000 55 | stub_const("JRUBY_VERSION", "9.0.3.0") 56 | expect(Ruby).to be_jruby_9000 57 | stub_const("RUBY_PLATFORM", "") 58 | expect(Ruby).to_not be_jruby_9000 59 | end 60 | 61 | specify "rbx? reflects the state of RUBY_ENGINE" do 62 | hide_const("RUBY_ENGINE") 63 | expect(Ruby).to be_mri 64 | stub_const("RUBY_ENGINE", "ruby") 65 | expect(Ruby).to be_mri 66 | stub_const("RUBY_ENGINE", "rbx") 67 | expect(Ruby).to_not be_mri 68 | end 69 | end 70 | 71 | RSpec.describe RubyFeatures do 72 | specify "#module_refinement_supported? reflects refinement support" do 73 | if Ruby.mri? && RUBY_VERSION >= '2.1.0' 74 | expect(RubyFeatures.module_refinement_supported?).to eq true 75 | end 76 | end 77 | 78 | specify "#fork_supported? exists" do 79 | RubyFeatures.fork_supported? 80 | end 81 | 82 | specify "#supports_exception_cause? exists" do 83 | RubyFeatures.supports_exception_cause? 84 | end 85 | 86 | specify "#kw_args_supported? exists" do 87 | RubyFeatures.kw_args_supported? 88 | end 89 | 90 | specify "#required_kw_args_supported? exists" do 91 | RubyFeatures.required_kw_args_supported? 92 | end 93 | 94 | specify "distincts_kw_args_from_positional_hash?" do 95 | RubyFeatures.distincts_kw_args_from_positional_hash? 96 | end 97 | 98 | specify "#supports_rebinding_module_methods? exists" do 99 | RubyFeatures.supports_rebinding_module_methods? 100 | end 101 | 102 | specify "#supports_syntax_suggest?" do 103 | expect(RubyFeatures.supports_syntax_suggest?).to eq(RUBY_VERSION.to_f >= 3.2) 104 | end 105 | 106 | specify "#supports_taint?" do 107 | RubyFeatures.supports_taint? 108 | end 109 | 110 | specify "#caller_locations_supported? exists" do 111 | RubyFeatures.caller_locations_supported? 112 | if Ruby.mri? 113 | expect(RubyFeatures.caller_locations_supported?).to eq(RUBY_VERSION >= '2.0.0') 114 | end 115 | end 116 | 117 | describe "#ripper_supported?" do 118 | def ripper_is_implemented? 119 | in_sub_process_if_possible do 120 | begin 121 | require 'ripper' 122 | !!defined?(::Ripper) && Ripper.respond_to?(:lex) 123 | rescue LoadError 124 | false 125 | end 126 | end 127 | end 128 | 129 | def ripper_works_correctly? 130 | ripper_reports_correct_line_number? && 131 | ripper_can_parse_source_including_keywordish_symbol? && 132 | ripper_can_parse_source_referencing_keyword_arguments? 133 | end 134 | 135 | # https://github.com/jruby/jruby/issues/3386 136 | def ripper_reports_correct_line_number? 137 | in_sub_process_if_possible do 138 | require 'ripper' 139 | tokens = ::Ripper.lex('foo') 140 | token = tokens.first 141 | location = token.first 142 | line_number = location.first 143 | line_number == 1 144 | end 145 | end 146 | 147 | # https://github.com/jruby/jruby/issues/4562 148 | def ripper_can_parse_source_including_keywordish_symbol? 149 | in_sub_process_if_possible do 150 | require 'ripper' 151 | sexp = ::Ripper.sexp(':if') 152 | !sexp.nil? 153 | end 154 | end 155 | 156 | # https://github.com/jruby/jruby/issues/5209 157 | def ripper_can_parse_source_referencing_keyword_arguments? 158 | in_sub_process_if_possible do 159 | require 'ripper' 160 | # It doesn't matter if keyword arguments don't exist. 161 | if Ruby.mri? || Ruby.jruby? || Ruby.truffleruby? 162 | if RUBY_VERSION < '2.0' 163 | true 164 | else 165 | begin 166 | !::Ripper.sexp('def a(**kw_args); end').nil? 167 | rescue NoMethodError 168 | false 169 | end 170 | end 171 | end 172 | end 173 | end 174 | 175 | it 'returns whether Ripper is correctly implemented in the current environment' do 176 | if RSpec::Support::Ruby.jruby? && RSpec::Support::Ruby.jruby_version.between?('9.0.0.0', '9.2.1.0') 177 | pending "Ripper is not supported on JRuby 9.1.17.0 despite this tests claims" 178 | end 179 | expect(RubyFeatures.ripper_supported?).to eq(ripper_is_implemented? && ripper_works_correctly?) 180 | end 181 | 182 | it 'does not load Ripper' do 183 | expect { RubyFeatures.ripper_supported? }.not_to change { defined?(::Ripper) } 184 | end 185 | end 186 | end 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /spec/rspec/support/source/node_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/support/source/node' 2 | 3 | class RSpec::Support::Source 4 | RSpec.describe Node, :if => RSpec::Support::RubyFeatures.ripper_supported? do 5 | let(:root_node) do 6 | Node.new(sexp) 7 | end 8 | 9 | let(:sexp) do 10 | require 'ripper' 11 | Ripper.sexp(source) 12 | end 13 | 14 | let(:source) { <<-END } 15 | variable = do_something(1, 2) 16 | variable.do_anything do |arg| 17 | puts arg 18 | end 19 | END 20 | 21 | # [:program, 22 | # [[:assign, 23 | # [:var_field, [:@ident, "variable", [1, 6]]], 24 | # [:method_add_arg, 25 | # [:fcall, [:@ident, "do_something", [1, 17]]], 26 | # [:arg_paren, 27 | # [:args_add_block, 28 | # [[:@int, "1", [1, 30]], [:@int, "2", [1, 33]]], 29 | # false]]]], 30 | # [:method_add_block, 31 | # [:call, 32 | # [:var_ref, [:@ident, "variable", [2, 6]]], 33 | # :".", 34 | # [:@ident, "do_anything", [2, 15]]], 35 | # [:do_block, 36 | # [:block_var, 37 | # [:params, [[:@ident, "arg", [2, 31]]], nil, nil, nil, nil, nil, nil], 38 | # false], 39 | # [[:command, 40 | # [:@ident, "puts", [3, 8]], 41 | # [:args_add_block, [[:var_ref, [:@ident, "arg", [3, 13]]]], false]]]]]]] 42 | 43 | describe '#args' do 44 | context 'when the sexp args consist of direct child sexps' do 45 | let(:target_node) do 46 | root_node.find { |node| node.type == :method_add_arg } 47 | end 48 | 49 | it 'returns the child nodes' do 50 | expect(target_node.args).to match([ 51 | an_object_having_attributes(:type => :fcall), 52 | an_object_having_attributes(:type => :arg_paren) 53 | ]) 54 | end 55 | end 56 | 57 | context 'when the sexp args include an array of sexps' do 58 | let(:target_node) do 59 | root_node.find { |node| node.type == :args_add_block } 60 | end 61 | 62 | it 'returns pseudo expression sequence node for the array' do 63 | expect(target_node.args).to match([ 64 | an_object_having_attributes(:type => :_expression_sequence), 65 | false 66 | ]) 67 | end 68 | end 69 | end 70 | 71 | describe '#each_ancestor' do 72 | let(:target_node) do 73 | root_node.find { |node| node.type == :arg_paren } 74 | end 75 | 76 | it 'yields ancestor nodes from parent to root' do 77 | expect { |b| target_node.each_ancestor(&b) }.to yield_successive_args( 78 | an_object_having_attributes(:type => :method_add_arg), 79 | an_object_having_attributes(:type => :assign), 80 | an_object_having_attributes(:type => :_expression_sequence), 81 | an_object_having_attributes(:type => :program) 82 | ) 83 | end 84 | end 85 | 86 | describe '#location' do 87 | context 'with identifier type node' do 88 | let(:target_node) do 89 | root_node.find { |node| node.type == :@ident } 90 | end 91 | 92 | it 'returns a Location object with line and column numbers' do 93 | expect(target_node.location).to have_attributes(:line => 1, :column => 6) 94 | end 95 | end 96 | 97 | context 'with non-identifier type node' do 98 | let(:target_node) do 99 | root_node.find { |node| node.type == :assign } 100 | end 101 | 102 | it 'returns nil' do 103 | expect(target_node.location).to be_nil 104 | end 105 | end 106 | end 107 | 108 | describe '#inspect' do 109 | it 'returns a string including class name and node type' do 110 | expect(root_node.inspect).to eq('#') 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /spec/rspec/support/source/token_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/support/source/token' 2 | 3 | class RSpec::Support::Source 4 | RSpec.describe Token, :if => RSpec::Support::RubyFeatures.ripper_supported? do 5 | let(:target_token) do 6 | tokens.first 7 | end 8 | 9 | let(:tokens) do 10 | Token.tokens_from_ripper_tokens(ripper_tokens) 11 | end 12 | 13 | let(:ripper_tokens) do 14 | require 'ripper' 15 | Ripper.lex(source) 16 | end 17 | 18 | let(:source) do 19 | 'puts :foo' 20 | end 21 | 22 | # [ 23 | # [[1, 0], :on_ident, "puts"], 24 | # [[1, 4], :on_sp, " "], 25 | # [[1, 5], :on_symbeg, ":"], 26 | # [[1, 6], :on_ident, "foo"] 27 | # ] 28 | 29 | describe "#closed_by" do 30 | context "with a normal ruby multi line method" do 31 | let(:source) { "def foo\n :bar\nend" } 32 | 33 | specify 'the first token is closed by the last' do 34 | expect(tokens.first).to be_closed_by(tokens.last) 35 | end 36 | end 37 | 38 | context "with a ruby one line method definition" do 39 | let(:source) { 'def self.foo = "bar"' } 40 | 41 | specify 'the first token is closed by the =' do 42 | expect(tokens.first).to be_closed_by(tokens[6]) 43 | end 44 | end 45 | end 46 | 47 | describe '#location' do 48 | it 'returns a Location object with line and column numbers' do 49 | expect(target_token.location).to have_attributes(:line => 1, :column => 0) 50 | end 51 | end 52 | 53 | describe '#type' do 54 | it 'returns a type of the token' do 55 | expect(target_token.type).to eq(:on_ident) 56 | end 57 | end 58 | 59 | describe '#string' do 60 | it 'returns a source string corresponding to the token' do 61 | expect(target_token.string).to eq('puts') 62 | end 63 | end 64 | 65 | describe '#==' do 66 | context 'when both tokens have same Ripper token' do 67 | it 'returns true' do 68 | expect(Token.new(ripper_tokens[0]) == Token.new(ripper_tokens[0])).to be true 69 | end 70 | end 71 | 72 | context 'when both tokens have different Ripper token' do 73 | it 'returns false' do 74 | expect(Token.new(ripper_tokens[0]) == Token.new(ripper_tokens[1])).to be false 75 | end 76 | end 77 | end 78 | 79 | describe '#inspect' do 80 | it 'returns a string including class name, token type and source string' do 81 | expect(target_token.inspect).to eq('#') 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/rspec/support/source_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/support/source' 2 | 3 | module RSpec::Support 4 | RSpec.describe Source, :if => RSpec::Support::RubyFeatures.ripper_supported? do 5 | subject(:source) do 6 | Source.new(source_string) 7 | end 8 | 9 | let(:source_string) { <<-END.gsub(/^ +\|/, '') } 10 | |2.times do 11 | | puts :foo 12 | |end 13 | END 14 | 15 | # [:program, 16 | # [[:method_add_block, 17 | # [:call, [:@int, "2", [1, 0]], :".", [:@ident, "times", [1, 2]]], 18 | # [:do_block, 19 | # nil, 20 | # [[:command, 21 | # [:@ident, "puts", [2, 2]], 22 | # [:args_add_block, 23 | # [[:symbol_literal, [:symbol, [:@ident, "foo", [2, 8]]]]], 24 | # false]]]]]]] 25 | 26 | describe '.from_file', :isolated_directory do 27 | subject(:source) do 28 | Source.from_file(path) 29 | end 30 | 31 | let(:path) do 32 | 'source.rb' 33 | end 34 | 35 | before do 36 | File.open(path, 'w') { |file| file.write(source_string) } 37 | end 38 | 39 | it 'returns a Source with the absolute path' do 40 | expect(source.lines.first).to eq('2.times do') 41 | expect(source.path).not_to eq(path) 42 | expect(source.path).to end_with(path) 43 | end 44 | 45 | it 'continues to work if File.read is stubbed' do 46 | allow(::File).to receive(:read).and_raise 47 | expect(source.lines.first).to eq('2.times do') 48 | end 49 | end 50 | 51 | describe '#lines' do 52 | it 'returns an array of lines without linefeed' do 53 | expect(source.lines).to eq([ 54 | '2.times do', 55 | ' puts :foo', 56 | 'end' 57 | ]) 58 | end 59 | 60 | it 'returns an array of lines no matter the encoding' do 61 | source_string << "\xAE" 62 | encoded_string = source_string.force_encoding('US-ASCII') 63 | expect(Source.new(encoded_string).lines).to eq([ 64 | '2.times do', 65 | ' puts :foo', 66 | 'end', 67 | '?', 68 | ]) 69 | end 70 | end 71 | 72 | describe '#ast' do 73 | it 'returns a root node' do 74 | expect(source.ast).to have_attributes(:type => :program) 75 | end 76 | end 77 | 78 | describe '#tokens' do 79 | it 'returns an array of tokens' do 80 | expect(source.tokens).to all be_a(Source::Token) 81 | end 82 | end 83 | 84 | describe '#nodes_by_line_number' do 85 | it 'returns a hash containing nodes for each line number' do 86 | expect(source.nodes_by_line_number).to match( 87 | 1 => 88 | if RUBY_VERSION >= '2.6.0' 89 | [ 90 | an_object_having_attributes(:type => :@int), 91 | an_object_having_attributes(:type => :@period), 92 | an_object_having_attributes(:type => :@ident) 93 | ] 94 | else 95 | [ 96 | an_object_having_attributes(:type => :@int), 97 | an_object_having_attributes(:type => :@ident) 98 | ] 99 | end, 100 | 2 => [ 101 | an_object_having_attributes(:type => :@ident), 102 | an_object_having_attributes(:type => :@ident) 103 | ] 104 | ) 105 | 106 | expect(source.nodes_by_line_number[0]).to be_empty 107 | end 108 | end 109 | 110 | describe '#tokens_by_line_number' do 111 | it 'returns a hash containing tokens for each line number' do 112 | expect(source.tokens_by_line_number).to match( 113 | 1 => [ 114 | an_object_having_attributes(:type => :on_int), 115 | an_object_having_attributes(:type => :on_period), 116 | an_object_having_attributes(:type => :on_ident), 117 | an_object_having_attributes(:type => :on_sp), 118 | an_object_having_attributes(:type => :on_kw), 119 | an_object_having_attributes(:type => :on_ignored_nl) 120 | ], 121 | 2 => [ 122 | an_object_having_attributes(:type => :on_sp), 123 | an_object_having_attributes(:type => :on_ident), 124 | an_object_having_attributes(:type => :on_sp), 125 | an_object_having_attributes(:type => :on_symbeg), 126 | an_object_having_attributes(:type => :on_ident), 127 | an_object_having_attributes(:type => :on_nl) 128 | ], 129 | 3 => [ 130 | an_object_having_attributes(:type => :on_kw), 131 | an_object_having_attributes(:type => :on_nl) 132 | ] 133 | ) 134 | 135 | expect(source.tokens_by_line_number[0]).to be_empty 136 | end 137 | end 138 | 139 | describe '#inspect' do 140 | it 'returns a string including class name and file path' do 141 | expect(source.inspect).to start_with('#') 142 | end 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /spec/rspec/support/spec/in_sub_process_spec.rb: -------------------------------------------------------------------------------- 1 | require 'tempfile' 2 | 3 | RSpec.describe 'isolating code to a sub process' do 4 | it 'isolates the block from the main process' do 5 | in_sub_process do 6 | module NotIsolated 7 | end 8 | expect(defined? NotIsolated).to eq "constant" 9 | end 10 | expect(defined? NotIsolated).to be_nil 11 | end 12 | 13 | if Process.respond_to?(:fork) && !(RUBY_PLATFORM == 'java' && RUBY_VERSION == '1.8.7') 14 | 15 | it 'returns the result of sub process' do 16 | expect(in_sub_process { :foo }).to eq(:foo) 17 | end 18 | 19 | it 'returns a UnmarshableObject if the result of sub process cannot be marshaled' do 20 | expect(in_sub_process { proc {} }).to be_a(RSpec::Support::InSubProcess::UnmarshableObject) 21 | end 22 | 23 | it 'captures and reraises errors to the main process' do 24 | expect { 25 | in_sub_process { raise "An Internal Error" } 26 | }.to raise_error "An Internal Error" 27 | end 28 | 29 | it 'captures and reraises test failures' do 30 | expect { 31 | in_sub_process { expect(true).to be false } 32 | }.to raise_error(/expected false/) 33 | end 34 | 35 | it 'fails if the sub process generates warnings' do 36 | expect { 37 | in_sub_process do 38 | # Redirect stderr so we don't get "boom" in our test suite output 39 | $stderr.reopen(Tempfile.new("stderr")) 40 | 41 | warn "boom" 42 | end 43 | }.to raise_error(RuntimeError, a_string_including("Warnings", "boom")) 44 | end 45 | 46 | else 47 | 48 | it 'pends the block' do 49 | expect { in_sub_process { true } }.to raise_error(/This spec requires forking to work properly/) 50 | end 51 | 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/rspec/support/spec/shell_out_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/support/spec/shell_out' 2 | 3 | RSpec.describe RSpec::Support::ShellOut, :slow do 4 | include described_class 5 | 6 | it 'shells out and returns stdout and stderr' do 7 | stdout, stderr, _ = shell_out("ruby", "-e", "$stdout.print 'yes'; $stderr.print 'no'") 8 | expect(stdout).to eq("yes") 9 | expect(stderr).to eq("no") 10 | end 11 | 12 | it 'returns the exit status as the third argument' do 13 | _, _, good_status = shell_out("ruby", "-e", '3 + 3') 14 | expect(good_status.exitstatus).to eq(0) 15 | 16 | unless RUBY_VERSION.to_f < 1.9 # except 1.8... 17 | _, _, bad_status = shell_out("ruby", "-e", 'boom') 18 | expect(bad_status.exitstatus).to eq(1) 19 | end 20 | end 21 | 22 | it 'can shell out to ruby with the current load path' do 23 | skip "Need to investigate why this is failing -- see " \ 24 | "https://travis-ci.org/rspec/rspec-core/jobs/60327106 and " \ 25 | "https://travis-ci.org/rspec/rspec-support/jobs/60296920 for examples" 26 | 27 | out, err, status = run_ruby_with_current_load_path('puts $LOAD_PATH.sort.join("\n")') 28 | expect(err).to eq("") 29 | expect(out).to include(*$LOAD_PATH.first(10)) 30 | expect(status.exitstatus).to eq(0) 31 | end 32 | 33 | it 'passes along the provided ruby flags' do 34 | out, err, status = run_ruby_with_current_load_path('puts "version"', '-v') 35 | expect(out).to include('version', RUBY_DESCRIPTION) 36 | expect(strip_known_warnings err).to eq('') 37 | expect(status.exitstatus).to eq(0) 38 | end 39 | 40 | it 'filters out the annoying output issued by `ruby -w` when the GC ENV vars are set' do 41 | with_env 'RUBY_GC_HEAP_FREE_SLOTS' => '10001', 'RUBY_GC_MALLOC_LIMIT' => '16777217', 'RUBY_FREE_MIN' => '10001' do 42 | out, err, status = run_ruby_with_current_load_path('', '-w') 43 | expect(out).to eq('') 44 | expect(strip_known_warnings err).to eq('') 45 | expect(status.exitstatus).to eq(0) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/rspec/support/spec/stderr_splitter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/support/spec/stderr_splitter' 2 | require 'tempfile' 3 | require 'rspec/support/spec/in_sub_process' 4 | 5 | RSpec.describe 'RSpec::Support::StdErrSplitter' do 6 | include RSpec::Support::InSubProcess 7 | 8 | let(:splitter) { RSpec::Support::StdErrSplitter.new stderr } 9 | let(:stderr) { STDERR } 10 | 11 | before do 12 | allow(stderr).to receive(:write) 13 | end 14 | 15 | around do |example| 16 | original = $stderr 17 | $stderr = splitter 18 | 19 | example.run 20 | 21 | $stderr = original 22 | end 23 | 24 | it 'conforms to the stderr interface' do 25 | # There some methods that appear in the list of the #methods but actually not implemented: 26 | # 27 | # $stderr.pressed? 28 | # NotImplementedError: pressed?() function is unimplemented on this machine 29 | stderr_methods = stderr.methods.select { |method| stderr.respond_to?(method) } 30 | 31 | # On 2.2, there's a weird issue where stderr sometimes responds to `birthtime` and sometimes doesn't... 32 | stderr_methods -= [:birthtime] if RUBY_VERSION =~ /^2\.2/ 33 | 34 | # No idea why, but on our AppVeyor windows builds it doesn't respond to these... 35 | stderr_methods -= [:close_on_exec?, :close_on_exec=] if RSpec::Support::OS.windows? && ENV['CI'] 36 | 37 | expect(splitter).to respond_to(*stderr_methods) 38 | end 39 | 40 | it 'acknowledges its own interface' do 41 | expect(splitter).to respond_to :==, :write, :has_output?, :reset!, :verify_no_warnings!, :output 42 | end 43 | 44 | it 'supports methods that stderr supports but StringIO does not' do 45 | expect(StringIO.new).not_to respond_to(:stat) 46 | expect(splitter.stat).to be_a(File::Stat) 47 | end 48 | 49 | it 'supports #to_io' do 50 | expect(splitter.to_io).to be(stderr.to_io) 51 | end 52 | 53 | it 'behaves like stderr' do 54 | splitter.write 'a warning' 55 | expect(stderr).to have_received(:write) 56 | end 57 | 58 | it 'pretends to be stderr' do 59 | expect(splitter).to eq stderr 60 | end 61 | 62 | it 'resets when reopened' do 63 | in_sub_process(false) do 64 | warn 'a warning' 65 | allow(stderr).to receive(:write).and_call_original 66 | 67 | Tempfile.open('stderr') do |file| 68 | splitter.reopen(file) 69 | expect { splitter.verify_no_warnings! }.not_to raise_error 70 | end 71 | end 72 | end 73 | 74 | it 'tracks when output to' do 75 | splitter.write 'a warning' 76 | expect(splitter).to have_output 77 | end 78 | 79 | it 'will ignore examples without a warning' do 80 | splitter.verify_no_warnings! 81 | end 82 | 83 | it 'will ignore examples after a reset a warning' do 84 | warn 'a warning' 85 | splitter.reset! 86 | splitter.verify_no_warnings! 87 | end 88 | 89 | unless RSpec::Support::Ruby.rbx? || RSpec::Support::Ruby.truffleruby? 90 | # TruffleRuby doesn't support warnings for now 91 | # https://github.com/oracle/truffleruby/issues/2595 92 | it 'will fail an example which generates a warning' do 93 | true unless $undefined 94 | expect { splitter.verify_no_warnings! }.to raise_error(/Warnings were generated:/) 95 | end 96 | end 97 | 98 | it 'does not reuse the stream when cloned' do 99 | expect(splitter.to_io).not_to eq(splitter.clone.to_io) 100 | end 101 | 102 | # This spec replicates what matchers do when capturing stderr, e.g `to_stderr_from_any_process` 103 | it 'is able to restore the stream from a cloned StdErrSplitter' do 104 | if RSpec::Support::Ruby.jruby? 105 | skip """ 106 | This spec is currently unsupported on JRuby on CI due to tempfiles not being 107 | a file, this situtation was discussed here https://github.com/rspec/rspec-support/pull/598#issuecomment-2200779633 108 | """ 109 | end 110 | 111 | cloned = splitter.clone 112 | expect(splitter.to_io).not_to be_a(File) 113 | 114 | tempfile = Tempfile.new("foo") 115 | begin 116 | splitter.reopen(tempfile) 117 | expect(splitter.to_io).to be_a(File) 118 | ensure 119 | splitter.reopen(cloned) 120 | tempfile.close 121 | tempfile.unlink 122 | end 123 | # This is the important part of the test that would fail without proper cloning hygeine 124 | expect(splitter.to_io).not_to be_a(File) 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /spec/rspec/support/spec/with_isolated_std_err_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/support/spec' 2 | 3 | RSpec.describe 'isolating a spec from the stderr splitter' do 4 | include RSpec::Support::WithIsolatedStdErr 5 | 6 | it 'allows a spec to output a warning' do 7 | with_isolated_stderr do 8 | $stderr.puts "Imma gonna warn you" 9 | end 10 | end 11 | 12 | it 'resets $stderr to its original value even if an error is raised' do 13 | orig_stderr = $stderr 14 | 15 | expect { 16 | with_isolated_stderr { raise "boom" } 17 | }.to raise_error("boom") 18 | 19 | expect($stderr).to be(orig_stderr) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/rspec/support/warnings_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "rspec/support/warnings" 3 | require 'rspec/support/spec/shell_out' 4 | 5 | RSpec.describe "rspec warnings and deprecations" do 6 | include RSpec::Support::ShellOut 7 | let(:warning_object) do 8 | Object.new.tap { |o| o.extend(RSpec::Support::Warnings) } 9 | end 10 | 11 | it 'works when required in isolation' do 12 | out, err, status = run_ruby_with_current_load_path("RSpec.deprecate('foo')", "-rrspec/support/warnings") 13 | expect(out).to eq("") 14 | expect(err).to start_with("DEPRECATION: foo is deprecated") 15 | expect(status.exitstatus).to eq(0) 16 | end 17 | 18 | context "when rspec-core is not available" do 19 | shared_examples "falling back to Kernel.warn" do |args| 20 | let(:method_name) { args.fetch(:method_name) } 21 | 22 | it 'falls back to warning with a plain message' do 23 | expect(::Kernel).to receive(:warn).with(/message/) 24 | warning_object.send(method_name, 'message') 25 | end 26 | 27 | it "handles being passed options" do 28 | expect(::Kernel).to receive(:warn).with(/message/) 29 | warning_object.send(method_name, "this is the message", :type => :test) 30 | end 31 | end 32 | 33 | it_behaves_like 'falling back to Kernel.warn', :method_name => :deprecate 34 | it_behaves_like 'falling back to Kernel.warn', :method_name => :warn_deprecation 35 | end 36 | 37 | shared_examples "warning helper" do |helper| 38 | it 'warns with the message text' do 39 | expect(::Kernel).to receive(:warn).with(/Message/) 40 | warning_object.send(helper, 'Message') 41 | end 42 | 43 | it 'sets the calling line' do 44 | expect(::Kernel).to receive(:warn).with(/#{__FILE__}:#{__LINE__+1}/) 45 | warning_object.send(helper, 'Message') 46 | end 47 | 48 | it 'optionally sets the replacement' do 49 | expect(::Kernel).to receive(:warn).with(/Use Replacement instead./) 50 | warning_object.send(helper, 'Message', :replacement => 'Replacement') 51 | end 52 | end 53 | 54 | describe "#warning" do 55 | it 'prepends WARNING:' do 56 | expect(::Kernel).to receive(:warn).with(/WARNING: Message\./) 57 | warning_object.warning 'Message' 58 | end 59 | 60 | it_should_behave_like 'warning helper', :warning 61 | end 62 | 63 | describe "#warn_with message, options" do 64 | it_should_behave_like 'warning helper', :warn_with 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/rspec/support/with_keywords_when_needed_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/support/with_keywords_when_needed' 2 | 3 | module RSpec::Support 4 | RSpec.describe "WithKeywordsWhenNeeded" do 5 | 6 | describe ".class_exec" do 7 | extend RubyFeatures 8 | 9 | let(:klass) do 10 | Class.new do 11 | def self.check_argument(argument) 12 | raise ArgumentError unless argument == 42 13 | end 14 | end 15 | end 16 | 17 | def run(klass, *args, &block) 18 | WithKeywordsWhenNeeded.class_exec(klass, *args, &block) 19 | end 20 | 21 | it "will run a block without keyword arguments" do 22 | run(klass, 42) { |arg| check_argument(arg) } 23 | end 24 | 25 | it "will run a block with a hash without keyword arguments" do 26 | run(klass, "value" => 42) { |arg| check_argument(arg["value"]) } 27 | end 28 | 29 | it "will run a block with optional keyword arguments when none are provided", :if => kw_args_supported? do 30 | binding.eval(<<-CODE, __FILE__, __LINE__) 31 | run(klass, 42) { |arg, val: nil| check_argument(arg) } 32 | CODE 33 | end 34 | 35 | it "will run a block with optional keyword arguments when they are provided", :if => required_kw_args_supported? do 36 | binding.eval(<<-CODE, __FILE__, __LINE__) 37 | run(klass, val: 42) { |val: nil| check_argument(val) } 38 | CODE 39 | end 40 | 41 | it "will run a block with required keyword arguments", :if => required_kw_args_supported? do 42 | binding.eval(<<-CODE, __FILE__, __LINE__) 43 | run(klass, val: 42) { |val:| check_argument(val) } 44 | CODE 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/support/spec' 2 | RSpec::Support::Spec.setup_simplecov 3 | 4 | RSpec::Matchers.define_negated_matcher :avoid_raising_errors, :raise_error 5 | RSpec::Matchers.define_negated_matcher :avoid_changing, :change 6 | --------------------------------------------------------------------------------