├── .gem_release.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── docs-lint.yml │ ├── jruby-test.yml │ ├── rubocop.yml │ ├── test-edge.yml │ ├── test.yml │ └── truffle-test.yml ├── .gitignore ├── .mdlrc ├── .rubocop-md.yml ├── .rubocop.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── RELEASING.md ├── bin └── mspec ├── default.mspec ├── forspell.dict ├── gemfiles └── rubocop.gemfile ├── lib ├── require-hooks.rb └── require-hooks │ ├── api.rb │ ├── mode │ ├── bootsnap.rb │ ├── kernel_patch.rb │ └── load_iseq.rb │ ├── setup.rb │ └── version.rb ├── require-hooks.gemspec └── spec ├── core └── kernel │ ├── load_spec.rb │ ├── require_relative_spec.rb │ ├── require_spec.rb │ └── shared │ ├── load.rb │ ├── require.rb │ └── then.rb ├── fixtures ├── code │ ├── a │ │ ├── load_fixture.bundle │ │ ├── load_fixture.dll │ │ ├── load_fixture.dylib │ │ └── load_fixture.so │ ├── b │ │ └── load_fixture.rb │ ├── concurrent.rb │ ├── concurrent2.rb │ ├── concurrent3.rb │ ├── concurrent_require_fixture.rb │ ├── file_fixture.rb │ ├── gem │ │ └── load_fixture.rb │ ├── line_fixture.rb │ ├── load_ext_fixture.rb │ ├── load_fixture │ ├── load_fixture.bundle │ ├── load_fixture.dll │ ├── load_fixture.dylib │ ├── load_fixture.ext │ ├── load_fixture.ext.bundle │ ├── load_fixture.ext.dll │ ├── load_fixture.ext.dylib │ ├── load_fixture.ext.rb │ ├── load_fixture.ext.so │ ├── load_fixture.rb │ ├── load_fixture.so │ ├── load_fixture_and__FILE__.rb │ ├── load_wrap_fixture.rb │ ├── load_wrap_method_fixture.rb │ ├── methods_fixture.rb │ ├── raise_fixture.rb │ ├── recursive_load_fixture.rb │ ├── recursive_require_fixture.rb │ └── symlink │ │ ├── symlink1.rb │ │ └── symlink2 │ │ └── symlink2.rb └── code_loading.rb ├── require-hooks ├── around_load_spec.rb ├── bootsnap_spec.rb ├── fixtures │ ├── bootsnap-syntax-error.rb │ ├── bootsnap.rb │ ├── freeze.rb │ ├── hello.rb │ ├── hi_jack.rb │ └── syntax_error.rb ├── hijack_load_spec.rb └── source_transform_spec.rb ├── spec_helper.rb └── support └── command_testing.rb /.gem_release.yml: -------------------------------------------------------------------------------- 1 | bump: 2 | file: lib/require-hooks/version.rb 3 | skip_ci: true 4 | 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: palkan 7 | 8 | --- 9 | 10 | ## What did you do? 11 | 12 | ## What did you expect to happen? 13 | 14 | ## What actually happened? 15 | 16 | ## Additional context 17 | 18 | ## Environment 19 | 20 | **Ruby Version:** 21 | 22 | **Framework Version (Rails, whatever):** 23 | 24 | **Require Hooks Version:** 25 | 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: palkan 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? Please describe. 11 | 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | ## Describe the solution you'd like 15 | 16 | A clear and concise description of what you want to happen. 17 | 18 | ## Describe alternatives you've considered 19 | 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | ## Additional context 23 | 24 | Add any other context or screenshots about the feature request here. 25 | 26 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## What is the purpose of this pull request? 8 | 9 | 14 | 15 | ## What changes did you make? (overview) 16 | 17 | ## Is there anything you'd like reviewers to focus on? 18 | 19 | ## Checklist 20 | 21 | - [ ] I've added tests for this change 22 | - [ ] I've added a Changelog entry 23 | - [ ] I've updated a documentation 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/docs-lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - "*.md" 9 | - "**/*.md" 10 | - ".github/workflows/docs-lint.yml" 11 | pull_request: 12 | paths: 13 | - "*.md" 14 | - "**/*.md" 15 | - ".github/workflows/docs-lint.yml" 16 | 17 | jobs: 18 | docs-lint: 19 | uses: anycable/github-actions/.github/workflows/docs-lint.yml@master 20 | with: 21 | mdl-path: README.md CHANGELOG.md 22 | forspell-args: "*.md" 23 | -------------------------------------------------------------------------------- /.github/workflows/jruby-test.yml: -------------------------------------------------------------------------------- 1 | name: JRuby Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | jruby-test: 12 | timeout-minutes: 20 13 | runs-on: ubuntu-latest 14 | env: 15 | BUNDLE_JOBS: 4 16 | BUNDLE_RETRY: 3 17 | CI: true 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: jruby 23 | bundler-cache: true 24 | - name: Run MSpec 25 | run: bundle exec bin/mspec 26 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yml: -------------------------------------------------------------------------------- 1 | name: Lint Ruby 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | rubocop: 11 | runs-on: ubuntu-latest 12 | env: 13 | BUNDLE_GEMFILE: gemfiles/rubocop.gemfile 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: 3.1 19 | bundler-cache: true 20 | - name: Lint Ruby code with RuboCop 21 | run: | 22 | bundle exec rubocop 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/test-edge.yml: -------------------------------------------------------------------------------- 1 | name: Build Edge 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | schedule: 10 | - cron: "10 4 * * */2" 11 | 12 | jobs: 13 | test: 14 | timeout-minutes: 5 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | ruby: [head] 20 | mode: ['auto', 'patch'] 21 | env: 22 | BUNDLE_JOBS: 4 23 | BUNDLE_RETRY: 3 24 | CI: true 25 | REQUIRE_HOOKS_MODE: ${{ matrix.mode }} 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{ matrix.ruby }} 31 | bundler-cache: true 32 | - name: Run MSpec 33 | run: bundle exec bin/mspec 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | timeout-minutes: 5 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | ruby: [2.5, 2.6, 2.7, 3.0, 3.1, 3.2, 3.3] 17 | mode: ['auto', 'patch'] 18 | env: 19 | BUNDLE_JOBS: 4 20 | BUNDLE_RETRY: 3 21 | CI: true 22 | REQUIRE_HOOKS_MODE: ${{ matrix.mode }} 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: ${{ matrix.ruby }} 28 | bundler-cache: true 29 | - name: Run MSpec 30 | run: bundle exec bin/mspec 31 | -------------------------------------------------------------------------------- /.github/workflows/truffle-test.yml: -------------------------------------------------------------------------------- 1 | name: TruffleRuby Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | truffle-test-require-hooks: 12 | timeout-minutes: 20 13 | runs-on: ubuntu-latest 14 | env: 15 | BUNDLE_JOBS: 4 16 | BUNDLE_RETRY: 3 17 | BUNDLE_PATH: /home/runner/bundle 18 | TRUFFLERUBYOPT: "--engine.Mode=latency" 19 | CI: true 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: truffleruby-head 25 | bundler-cache: true 26 | - name: Run MSpec 27 | run: bundle exec bin/mspec 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.orig 5 | *.log 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.vi 10 | *~ 11 | *.sass-cache 12 | *.iml 13 | .idea/ 14 | 15 | # Sublime 16 | *.sublime-project 17 | *.sublime-workspace 18 | 19 | # OS or Editor folders 20 | .DS_Store 21 | .cache 22 | .project 23 | .settings 24 | .tmproj 25 | Thumbs.db 26 | 27 | .bundle/ 28 | log/*.log 29 | pkg/ 30 | spec/dummy/db/*.sqlite3 31 | spec/dummy/db/*.sqlite3-journal 32 | spec/dummy/tmp/ 33 | 34 | Gemfile.lock 35 | Gemfile.local 36 | .rspec-local 37 | .ruby-version 38 | *.gem 39 | 40 | tmp/ 41 | .rbnext/ 42 | 43 | gemfiles/*.lock 44 | 45 | mspec/ 46 | rubyspec_temp/ 47 | -------------------------------------------------------------------------------- /.mdlrc: -------------------------------------------------------------------------------- 1 | rules "~MD013", "~MD033", "~MD024", "~MD007" 2 | -------------------------------------------------------------------------------- /.rubocop-md.yml: -------------------------------------------------------------------------------- 1 | inherit_from: ".rubocop.yml" 2 | 3 | require: 4 | - rubocop-md 5 | 6 | AllCops: 7 | Include: 8 | - '**/*.md' 9 | 10 | # FIXME 11 | Layout/InitialIndentation: 12 | Exclude: 13 | - 'CHANGELOG.md' -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - standard 3 | 4 | inherit_gem: 5 | standard: config/base.yml 6 | 7 | AllCops: 8 | Exclude: 9 | - 'bin/*' 10 | - 'tmp/**/*' 11 | - 'Gemfile' 12 | - 'vendor/**/*' 13 | - 'gemfiles/**/*' 14 | - 'lib/.rbnext/**/*' 15 | - 'lib/generators/**/templates/*.rb' 16 | - '.github/**/*' 17 | - '**/*/syntax_error.rb' 18 | - 'spec/fixtures/**/*.rb' 19 | - 'spec/core/**/*.rb' 20 | - 'mspec/**/*' 21 | DisplayCopNames: true 22 | SuggestExtensions: false 23 | NewCops: disable 24 | TargetRubyVersion: 2.5 25 | 26 | Style/FrozenStringLiteralComment: 27 | Enabled: true 28 | Exclude: 29 | - 'spec/require-hooks/fixtures/*.rb' 30 | 31 | Style/GlobalVars: 32 | Exclude: 33 | - 'spec/**/*' 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## master 4 | 5 | ## 0.2.2 (2023-12-19) 6 | 7 | - Fix handing uncompilable source code with Bootsnap. ([@palkan][]) 8 | 9 | ## 0.2.1 (2023-12-19) 10 | 11 | - Fix constant resolution in Bootsnap error handling (`Bootsnap::CompileCache` -> `::Bootsnap::CompileCache`). ([@palkan][]) 12 | 13 | ## 0.2.0 (2023-08-23) 14 | 15 | - Add `patterns` and `exclude_patterns` options to hooks. ([@palkan][]) 16 | 17 | ## 0.1.0 (2023-07-14) 18 | 19 | - Extracted from Ruby Next. ([@palkan][]) 20 | 21 | [@palkan]: https://github.com/palkan 22 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "debug", platform: :mri 6 | gem "bootsnap", platform: [:mri, :truffleruby] 7 | 8 | gemspec 9 | 10 | eval_gemfile "gemfiles/rubocop.gemfile" 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Vladimir Dementyev 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/require-hooks.svg)](https://rubygems.org/gems/require-hooks) 2 | [![Build](https://github.com/ruby-next/require-hooks/workflows/Build/badge.svg)](https://github.com/palkan/require-hooks/actions) 3 | [![JRuby Build](https://github.com/ruby-next/require-hooks/workflows/JRuby%20Build/badge.svg)](https://github.com/ruby-next/require-hooks/actions) 4 | [![TruffleRuby Build](https://github.com/ruby-next/require-hooks/workflows/TruffleRuby%20Build/badge.svg)](https://github.com/ruby-next/require-hooks/actions) 5 | 6 | # Require Hooks 7 | 8 | Require Hooks is a library providing universal interface for injecting custom code into the Ruby's loading mechanism. It works on MRI, JRuby, and TruffleRuby. 9 | 10 | Require hooks allows you to interfere with `Kernel#require` (incl. `Kernel#require_relative`) and `Kernel#load`. 11 | 12 | 13 | Sponsored by Evil Martians 14 | 15 | ## Examples 16 | 17 | - [Ruby Next][ruby-next] 18 | - [Freezolite](https://github.com/ruby-next/freezolite) 19 | 20 | ## Installation 21 | 22 | Add to your Gemfile: 23 | 24 | ```ruby 25 | gem "require-hooks" 26 | ``` 27 | 28 | or gemspec: 29 | 30 | ```ruby 31 | spec.add_dependency "require-hooks" 32 | ``` 33 | 34 | ## Usage 35 | 36 | To enable hooks, you need to load `require-hooks/setup` before any code that you want to pre-process via hooks: 37 | 38 | ```ruby 39 | require "require-hooks/setup" 40 | ``` 41 | 42 | For example, in an application (e.g., Rails), you may want to only process the source files you own, so you must activate Require Hooks after loading the dependencies (e.g., in the `config/application.rb` file right after `Bundler.require(*)`). 43 | 44 | If you want to pre-process all files, you can activate Require Hooks earlier. 45 | 46 | Then, you can add hooks: 47 | 48 | - **around_load:** a hook that wraps code loading operation. Useful for logging and debugging purposes. 49 | 50 | ```ruby 51 | # Simple logging 52 | RequireHooks.around_load(patterns: ["/gem/dir/*.rb"]) do |path, &block| 53 | puts "Loading #{path}" 54 | block.call.tap { puts "Loaded #{path}" } 55 | end 56 | 57 | # Error enrichment. 58 | # No patterns — all files are affected. 59 | RequireHooks.around_load do |path, &block| 60 | block.call 61 | rescue SyntaxError => e 62 | raise "Oops, your Ruby is not Ruby: #{e.message}" 63 | end 64 | ``` 65 | 66 | The return value MUST be a result of calling the passed block. 67 | 68 | - **source_transform:** perform source-to-source transformations. 69 | 70 | ```ruby 71 | RequireHooks.source_transform(patterns: ["/my_project/*.rb"], exclude_patterns: ["/my_project/vendor/*"]) do |path, source| 72 | source ||= File.read(path) 73 | "# frozen_string_literal: true\n#{source}" 74 | end 75 | ``` 76 | 77 | The return value MUST be either String (new source code) or `nil` (indicating that no transformations were performed). The second argument (`source`) MAY be `nil``, indicating that no transformer tried to transform the source code. 78 | 79 | - **hijack_load:** a hook that is used to manually compile byte code for VM to load it. 80 | 81 | ```ruby 82 | # Pattern can be a Proc. If it returns `true`, the hijacker is used. 83 | RequireHooks.hijack_load(patterns: ["/my_project/*.rb"]) do |path, source| 84 | source ||= File.read(path) 85 | if defined?(RubyVM::InstructionSequence) 86 | RubyVM::InstructionSequence.compile(source) 87 | elsif defined?(JRUBY_VERSION) 88 | JRuby.compile(source) 89 | end 90 | end 91 | ``` 92 | 93 | The return value is platform-specific. If there are multiple _hijackers_, the first one that returns a non-`nil` value is used, others are ignored. 94 | 95 | **NOTE:** The `patterns` and `exclude_patterns` arguments accept globs as recognized by [File.fnmatch](https://rubyapi.org/3.2/o/file#method-c-fnmatch). 96 | 97 | ## Modes 98 | 99 | Depending on the runtime conditions, Require Hooks picks an optimal strategy for injecting the code. You can enforce a particular _mode_ by setting the `REQUIRE_HOOKS_MODE` env variable (`patch`, `load_iseq` or `bootsnap`). In practice, only setting to `patch` may makes sense. 100 | 101 | ### Via `#load_iseq` 102 | 103 | If `RubyVM::InstructionSequence` is available, we use more robust way of hijacking code loading—`RubyVM::InstructionSequence#load_iseq`. 104 | 105 | Keep in mind that if there is already a `#load_iseq` callback defined, it will only have an effect if Require Hooks hijackers return `nil`. 106 | 107 | ### Kernel patching 108 | 109 | In this mode, Require Hooks monkey-patches `Kernel#require` and friends. This mode is used in JRuby by default. 110 | 111 | ### Bootsnap integration 112 | 113 | [Bootsnap][] is a great tool to speed-up your application load and it's included into the default Rails Gemfile. And it uses `#load_iseq`. Require Hooks activates a custom Bootsnap-compatible mode, so you can benefit from both tools. 114 | 115 | You can use require-hooks with Bootsnap to customize code loading. Just make sure you load `require-hooks/setup` after setting up Bootsnap, for example: 116 | 117 | ```ruby 118 | require "bootsnap/setup" 119 | require "require-hooks/setup" 120 | ``` 121 | 122 | The _around load_ hooks are executed for all files independently of whether they are cached or not. Source transformation and hijacking is only done for non-cached files. 123 | 124 | Thus, if you introduce new source transformers or hijackers, you must invalidate the cache. (We plan to implement automatic invalidation in future versions.) 125 | 126 | ## Limitations 127 | 128 | - `Kernel#load` with a wrap argument (e.g., `load "some_path", true` or `load "some_path", MyModule)`) is not supported (fallbacked to the original implementation). The biggest challenge here is to support constants nesting. 129 | - Some very edgy symlinking scenarios are not supported (unlikely to affect real-world projects). 130 | 131 | ## Performance 132 | 133 | We conducted a benchmark to measure the performance overhead of Require Hooks using a large Rails project with the following characteristics: 134 | 135 | ```sh 136 | $ find config/ lib/ app/ -name "*.rb" | wc -l 137 | 138 | 2689 139 | ``` 140 | 141 | ```sh 142 | $ bundle list | wc -l 143 | 144 | 427 145 | ``` 146 | 147 | Total number of `#require` calls: **12741**. 148 | 149 | We activated Require Hooks in the very start of the program (`config/boot.rb`). 150 | 151 | There is a single around load hook to count all the calls: 152 | 153 | ```ruby 154 | counter = 0 155 | RequireHooks.around_load do |_, &block| 156 | counter += 1 157 | block.call 158 | end 159 | 160 | at_exit { puts "Total hooked calls: #{counter}" } 161 | ``` 162 | 163 | ## Results 164 | 165 | All tests made with `eager_load=true`. 166 | 167 | Test script: `time bundle exec rails runner 'puts "done"'`. 168 | 169 | | | | 170 | |-------------------------------------|--------------| 171 | | baseline | 29s | 172 | | baseline w/bootsnap  | 12s | 173 | | rhooks (iseq)  | 30s | 174 | | rhooks (patch)  | **8m** | 175 | | rhooks (bootsnap)  | 12s | 176 | 177 | You can see that requiring tons of files with Require Hooks in patch mode is very slow for now. Why? Mostly because we MUST check `$LOADED_FEATURES` for the presence of the file we want to load and currently we do this via `$LOADED_FEATURES.include?(path)` call, which becomes very slow when `$LOADED_FEATURES` is huge. Thus, we recommend activating Require Hooks after loading all the dependencies and limiting the scope of affected files (via the `patterns` option) on non-MRI platforms to avoid this overhead. 178 | 179 | **NOTE:** Why Ruby's internal implementations is fast despite from doing the same checks? It uses an internal hash table to keep track of the loaded features (`vm->loaded_features_realpaths`), not an array. Unfortunately, it's not accessible from Ruby. 180 | 181 | Here are the numbers for the same project with scoped hooks (only some folders) activated after `Bundler.require(*)`: 182 | 183 | - 732 files affected: 2m36s (vs. 30s w/o hooks) 184 | - 153 files affected: 55s (vs. 30s w/o hooks) 185 | 186 | ## Contributing 187 | 188 | Bug reports and pull requests are welcome on GitHub at [https://github.com/ruby-next/require-hooks](https://github.com/ruby-next/require-hooks). 189 | 190 | ## License 191 | 192 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 193 | 194 | [Bootsnap]: https://github.com/Shopify/bootsnap 195 | [ruby-next]: https://github.com/ruby-next/ruby-next 196 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # How to release a gem 2 | 3 | This document describes a process of releasing a new version of a gem. 4 | 5 | 1. Bump version. 6 | 7 | ```sh 8 | git commit -m "Bump 1.." 9 | ``` 10 | 11 | We're (kinda) using semantic versioning: 12 | 13 | - Bugfixes should be released as fast as possible as patch versions. 14 | - New features could be combined and released as minor or patch version upgrades (depending on the _size of the feature_—it's up to maintainers to decide). 15 | - Breaking API changes should be avoided in minor and patch releases. 16 | - Breaking dependencies changes (e.g., dropping older Ruby support) could be released in minor versions. 17 | 18 | How to bump a version: 19 | 20 | - Change the version number in `lib/require-hooks/version.rb` file. 21 | - Update the changelog (add new heading with the version name and date). 22 | - Update the installation documentation if necessary (e.g., during minor and major updates). 23 | 24 | 2. Push code to GitHub and make sure CI passes. 25 | 26 | ```sh 27 | git push 28 | ``` 29 | 30 | 3. Release a gem. 31 | 32 | ```sh 33 | gem release -t 34 | git push --tags 35 | ``` 36 | 37 | We use [gem-release](https://github.com/svenfuchs/gem-release) for publishing gems with a single command: 38 | 39 | ```sh 40 | gem release -t 41 | ``` 42 | 43 | Don't forget to push tags and write release notes on GitHub (if necessary). 44 | -------------------------------------------------------------------------------- /bin/mspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | unless File.directory?('mspec/lib') 4 | $stdout.puts "Cloning mspec..." 5 | system("git clone https://github.com/ruby/mspec.git mspec") 6 | end 7 | 8 | $:.unshift File.expand_path(File.join(__dir__, '..', 'mspec', 'lib')) 9 | 10 | require 'mspec/commands/mspec' 11 | MSpecMain.main(false) 12 | -------------------------------------------------------------------------------- /default.mspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("../lib", __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | 6 | begin 7 | require "debugger" 8 | rescue LoadError, NameError 9 | end 10 | 11 | require "require-hooks/setup" 12 | -------------------------------------------------------------------------------- /forspell.dict: -------------------------------------------------------------------------------- 1 | # Format: one word per line. Empty lines and #-comments are supported too. 2 | # If you want to add word with its forms, you can write 'word: example' (without quotes) on the line, 3 | # where 'example' is existing word with the same possible forms (endings) as your word. 4 | # Example: deduplicate: duplicate 5 | Bootsnap 6 | Bootsnap's 7 | entrypoint 8 | mruby 9 | polyfills 10 | pragma 11 | Startless 12 | Transpile 13 | transpile 14 | transpiler 15 | transpiled 16 | Transpiling 17 | transpiling 18 | iseq 19 | palkan 20 | Freezolite 21 | rhooks 22 | uncompilable 23 | -------------------------------------------------------------------------------- /gemfiles/rubocop.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" do 2 | gem "standard", "~> 1.0" 3 | gem "rubocop-md", "~> 1.0" 4 | end 5 | 6 | -------------------------------------------------------------------------------- /lib/require-hooks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "require-hooks/version" 4 | -------------------------------------------------------------------------------- /lib/require-hooks/api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RequireHooks 4 | @@around_load = [] 5 | @@source_transform = [] 6 | @@hijack_load = [] 7 | 8 | class Context 9 | def initialize(around_load, source_transform, hijack_load) 10 | @around_load = around_load 11 | @source_transform = source_transform 12 | @hijack_load = hijack_load 13 | end 14 | 15 | def empty? 16 | @around_load.empty? && @source_transform.empty? && @hijack_load.empty? 17 | end 18 | 19 | def source_transform? 20 | @source_transform.any? 21 | end 22 | 23 | def hijack? 24 | @hijack_load.any? 25 | end 26 | 27 | def run_around_load_callbacks(path) 28 | return yield if @around_load.empty? 29 | 30 | chain = @around_load.reverse.inject do |acc_proc, next_proc| 31 | proc { |path, &block| acc_proc.call(path) { next_proc.call(path, &block) } } 32 | end 33 | 34 | chain.call(path) { yield } 35 | end 36 | 37 | def perform_source_transform(path) 38 | return unless @source_transform.any? 39 | 40 | source = nil 41 | 42 | @source_transform.each do |transform| 43 | source = transform.call(path, source) || source 44 | end 45 | 46 | source 47 | end 48 | 49 | def try_hijack_load(path, source) 50 | return unless @hijack_load.any? 51 | 52 | @hijack_load.each do |hijack| 53 | result = hijack.call(path, source) 54 | return result if result 55 | end 56 | nil 57 | end 58 | end 59 | 60 | class << self 61 | attr_accessor :print_warnings 62 | 63 | # Define a block to wrap the code loading. 64 | # The return value MUST be a result of calling the passed block. 65 | # For example, you can use such hooks for instrumentation, debugging purposes. 66 | # 67 | # RequireHooks.around_load do |path, &block| 68 | # puts "Loading #{path}" 69 | # block.call.tap { puts "Loaded #{path}" } 70 | # end 71 | def around_load(patterns: nil, exclude_patterns: nil, &block) 72 | @@around_load << [patterns, exclude_patterns, block] 73 | end 74 | 75 | # Define hooks to perform source-to-source transformations. 76 | # The return value MUST be either String (new source code) or nil (indicating that no transformations were performed). 77 | # 78 | # NOTE: The second argument (`source`) MAY be nil, indicating that no transformer tried to transform the source code. 79 | # 80 | # For example, you can prepend each file with `# frozen_string_literal: true` pragma: 81 | # 82 | # RequireHooks.source_transform do |path, source| 83 | # "# frozen_string_literal: true\n#{source}" 84 | # end 85 | def source_transform(patterns: nil, exclude_patterns: nil, &block) 86 | @@source_transform << [patterns, exclude_patterns, block] 87 | end 88 | 89 | # This hook should be used to manually compile byte code to be loaded by the VM. 90 | # The arguments are (path, source = nil), where source is only defined if transformations took place. 91 | # Otherwise, you MUST read the source code from the file yourself. 92 | # 93 | # The return value MUST be either nil (continue to the next hook or default behavior) or a platform-specific bytecode object (e.g., RubyVM::InstructionSequence). 94 | # 95 | # RequireHooks.hijack_load do |path, source| 96 | # source ||= File.read(path) 97 | # if defined?(RubyVM::InstructionSequence) 98 | # RubyVM::InstructionSequence.compile(source) 99 | # elsif defined?(JRUBY_VERSION) 100 | # JRuby.compile(source) 101 | # end 102 | # end 103 | def hijack_load(patterns: nil, exclude_patterns: nil, &block) 104 | @@hijack_load << [patterns, exclude_patterns, block] 105 | end 106 | 107 | def context_for(path) 108 | around_load = @@around_load.select do |patterns, exclude_patterns, _block| 109 | next unless !patterns || patterns.any? { |pattern| File.fnmatch?(pattern, path) } 110 | next if exclude_patterns&.any? { |pattern| File.fnmatch?(pattern, path) } 111 | 112 | true 113 | end.map { |_patterns, _exclude_patterns, block| block } 114 | 115 | source_transform = @@source_transform.select do |patterns, exclude_patterns, _block| 116 | next unless !patterns || patterns.any? { |pattern| File.fnmatch?(pattern, path) } 117 | next if exclude_patterns&.any? { |pattern| File.fnmatch?(pattern, path) } 118 | 119 | true 120 | end.map { |_patterns, _exclude_patterns, block| block } 121 | 122 | hijack_load = @@hijack_load.select do |patterns, exclude_patterns, _block| 123 | next unless !patterns || patterns.any? { |pattern| File.fnmatch?(pattern, path) } 124 | next if exclude_patterns&.any? { |pattern| File.fnmatch?(pattern, path) } 125 | 126 | true 127 | end.map { |_patterns, _exclude_patterns, block| block } 128 | 129 | Context.new(around_load, source_transform, hijack_load) 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/require-hooks/mode/bootsnap.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RequireHooks 4 | module Bootsnap 5 | module CompileCacheExt 6 | def input_to_storage(source, path, *) 7 | ctx = RequireHooks.context_for(path) 8 | 9 | new_contents = ctx.perform_source_transform(path) 10 | hijacked = ctx.try_hijack_load(path, new_contents) 11 | 12 | if hijacked 13 | raise TypeError, "Unsupported bytecode format for #{path}: #{hijack.class}" unless hijacked.is_a?(::RubyVM::InstructionSequence) 14 | return hijacked.to_binary 15 | elsif new_contents 16 | return RubyVM::InstructionSequence.compile(new_contents, path, path, 1).to_binary 17 | end 18 | 19 | super 20 | rescue SyntaxError, TypeError 21 | ::Bootsnap::CompileCache::UNCOMPILABLE 22 | end 23 | end 24 | 25 | module LoadIseqExt 26 | # Around hooks must be performed every time we trigger a file load, even if 27 | # the file is already cached. 28 | def load_iseq(path) 29 | RequireHooks.context_for(path).run_around_load_callbacks(path) { super } 30 | end 31 | end 32 | end 33 | end 34 | 35 | Bootsnap::CompileCache::ISeq.singleton_class.prepend(RequireHooks::Bootsnap::CompileCacheExt) 36 | RubyVM::InstructionSequence.singleton_class.prepend(RequireHooks::Bootsnap::LoadIseqExt) 37 | -------------------------------------------------------------------------------- /lib/require-hooks/mode/kernel_patch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "pathname" 4 | 5 | module RequireHooks 6 | module KernelPatch 7 | class << self 8 | def load(path) 9 | ctx = RequireHooks.context_for(path) 10 | 11 | ctx.run_around_load_callbacks(path) do 12 | next load_without_require_hooks(path) unless ctx.source_transform? || ctx.hijack? 13 | 14 | new_contents = ctx.perform_source_transform(path) 15 | hijacked = ctx.try_hijack_load(path, new_contents) 16 | 17 | return try_evaluate(path, hijacked) if hijacked 18 | 19 | if new_contents 20 | evaluate(new_contents, path) 21 | true 22 | else 23 | load_without_require_hooks(path) 24 | end 25 | end 26 | end 27 | 28 | private 29 | 30 | def try_evaluate(path, bytecode) 31 | if defined?(::RubyVM::InstructionSequence) && bytecode.is_a?(::RubyVM::InstructionSequence) 32 | bytecode.eval 33 | else 34 | raise TypeError, "Unknown bytecode format for #{path}: #{bytecode.inspect}" 35 | end 36 | 37 | true 38 | end 39 | 40 | if defined?(JRUBY_VERSION) || defined?(TruffleRuby) 41 | def evaluate(code, filepath) 42 | new_toplevel.eval(code, filepath) 43 | end 44 | 45 | def new_toplevel 46 | # Create new "toplevel" binding to avoid lexical scope re-use 47 | # (aka "leaking refinements") 48 | eval "proc{binding}.call", TOPLEVEL_BINDING, __FILE__, __LINE__ 49 | end 50 | else 51 | def evaluate(code, filepath) 52 | # This is workaround to solve the "leaking refinements" problem in MRI 53 | RubyVM::InstructionSequence.compile(code, filepath).tap do |iseq| 54 | iseq.eval 55 | end 56 | end 57 | end 58 | end 59 | 60 | module Features 61 | class Locker 62 | class PathLock 63 | def initialize 64 | @mu = Mutex.new 65 | @resolved = false 66 | end 67 | 68 | def owned? 69 | @mu.owned? 70 | end 71 | 72 | def locked? 73 | @mu.locked? 74 | end 75 | 76 | def lock! 77 | @mu.lock 78 | end 79 | 80 | def unlock! 81 | @mu.unlock 82 | end 83 | 84 | def resolve! 85 | @resolved = true 86 | end 87 | 88 | def resolved? 89 | @resolved 90 | end 91 | end 92 | 93 | attr_reader :features, :mu 94 | 95 | def initialize 96 | @mu = Mutex.new 97 | @features = {} 98 | end 99 | 100 | def lock_feature(fname) 101 | lock = mu.synchronize do 102 | features[fname] ||= PathLock.new 103 | end 104 | 105 | # Can this even happen? 106 | return yield(true) if lock.resolved? 107 | 108 | # Recursive require 109 | if lock.owned? && lock.locked? 110 | warn "loading in progress, circular require considered harmful: #{fname}" if RequireHooks.print_warnings 111 | return yield(true) 112 | end 113 | 114 | lock.lock! 115 | begin 116 | yield(lock.resolved?).tap do 117 | lock.resolve! 118 | end 119 | ensure 120 | lock.unlock! 121 | 122 | mu.synchronize do 123 | features.delete(fname) 124 | end 125 | end 126 | end 127 | 128 | def locked_feature?(fname) 129 | mu.synchronize { features.key?(fname) } 130 | end 131 | end 132 | 133 | LOCK = Locker.new 134 | 135 | class << self 136 | def feature_path(path, implitic_ext: true) 137 | path = resolve_feature_path(path, implitic_ext: implitic_ext) 138 | return if path.nil? 139 | return if File.extname(path) != ".rb" && implitic_ext 140 | path 141 | end 142 | 143 | # Based on https://github.com/ruby/ruby/blob/b588fd552390c55809719100d803c36bc7430f2f/load.c#L403-L415 144 | def feature_loaded?(feature) 145 | return true if $LOADED_FEATURES.include?(feature) && !LOCK.locked_feature?(feature) 146 | 147 | feature = Pathname.new(feature).cleanpath.to_s 148 | efeature = File.expand_path(feature) 149 | 150 | # Check absoulute and relative paths 151 | return true if $LOADED_FEATURES.include?(efeature) && !LOCK.locked_feature?(efeature) 152 | 153 | candidates = [] 154 | 155 | $LOADED_FEATURES.each do |lf| 156 | candidates << lf if lf.end_with?("/#{feature}") 157 | end 158 | 159 | return false if candidates.empty? 160 | 161 | $LOAD_PATH.each do |lp| 162 | lp_feature = File.join(lp, feature) 163 | return true if candidates.include?(lp_feature) && !LOCK.locked_feature?(lp_feature) 164 | end 165 | 166 | false 167 | end 168 | 169 | private 170 | 171 | def lookup_feature_path(path, implitic_ext: true) 172 | path = "#{path}.rb" if File.extname(path).empty? && implitic_ext 173 | 174 | # Resolve relative paths only against current directory 175 | if path.match?(/^\.\.?\//) 176 | path = File.expand_path(path) 177 | return path if File.file?(path) 178 | return nil 179 | end 180 | 181 | if Pathname.new(path).absolute? 182 | path = File.expand_path(path) 183 | return File.file?(path) ? path : nil 184 | end 185 | 186 | # not a relative, not an absolute path — bare path; try looking relative to current dir, 187 | # if it's in the $LOAD_PATH 188 | if $LOAD_PATH.include?(Dir.pwd) && File.file?(path) 189 | return File.expand_path(path) 190 | end 191 | 192 | $LOAD_PATH.find do |lp| 193 | lpath = File.join(lp, path) 194 | return File.expand_path(lpath) if File.file?(lpath) 195 | end 196 | end 197 | 198 | if $LOAD_PATH.respond_to?(:resolve_feature_path) 199 | def resolve_feature_path(feature, implitic_ext: true) 200 | if implitic_ext 201 | path = $LOAD_PATH.resolve_feature_path(feature) 202 | path.last if path # rubocop:disable Style/SafeNavigation 203 | else 204 | lookup_feature_path(feature, implitic_ext: implitic_ext) 205 | end 206 | rescue LoadError 207 | end 208 | else 209 | def resolve_feature_path(feature, implitic_ext: true) 210 | lookup_feature_path(feature, implitic_ext: implitic_ext) 211 | end 212 | end 213 | end 214 | end 215 | end 216 | end 217 | 218 | # Patch Kernel to hijack require/require_relative/load 219 | module Kernel 220 | module_function 221 | 222 | alias_method :require_without_require_hooks, :require 223 | # See https://github.com/ruby/ruby/blob/d814722fb8299c4baace3e76447a55a3d5478e3a/load.c#L1181 224 | def require(path) 225 | path = path.to_path if path.respond_to?(:to_path) 226 | raise TypeError unless path.respond_to?(:to_str) 227 | 228 | path = path.to_str 229 | 230 | raise TypeError unless path.is_a?(::String) 231 | 232 | realpath = nil 233 | feature = path 234 | 235 | # if extname == ".rb" => lookup feature -> resolve feature -> load 236 | # if extname != ".rb" => append ".rb" - lookup feature -> resolve feature -> lookup orig (no ext) -> resolve orig (no ext) -> load 237 | if File.extname(path) != ".rb" 238 | realpath = RequireHooks::KernelPatch::Features.feature_path(path + ".rb") 239 | 240 | if realpath 241 | feature = path + ".rb" 242 | end 243 | end 244 | 245 | realpath ||= RequireHooks::KernelPatch::Features.feature_path(path) 246 | 247 | return require_without_require_hooks(path) unless realpath 248 | 249 | ctx = RequireHooks.context_for(realpath) 250 | 251 | return require_without_require_hooks(path) if ctx.empty? 252 | 253 | return false if RequireHooks::KernelPatch::Features.feature_loaded?(feature) 254 | 255 | RequireHooks::KernelPatch::Features::LOCK.lock_feature(feature) do |loaded| 256 | return false if loaded 257 | 258 | $LOADED_FEATURES << realpath 259 | RequireHooks::KernelPatch.load(realpath) 260 | true 261 | end 262 | rescue LoadError => e 263 | $LOADED_FEATURES.delete(realpath) if realpath 264 | warn "RequireHooks failed to require '#{path}': #{e.message}" if RequireHooks.print_warnings 265 | require_without_require_hooks(path) 266 | rescue Errno::ENOENT, Errno::EACCES 267 | raise LoadError, "cannot load such file -- #{path}" 268 | rescue 269 | $LOADED_FEATURES.delete(realpath) if realpath 270 | raise 271 | end 272 | 273 | alias_method :require_relative_without_require_hooks, :require_relative 274 | def require_relative(path) 275 | path = path.to_path if path.respond_to?(:to_path) 276 | raise TypeError unless path.respond_to?(:to_str) 277 | path = path.to_str 278 | 279 | raise TypeError unless path.is_a?(::String) 280 | 281 | return require(path) if Pathname.new(path).absolute? 282 | 283 | loc = caller_locations(1..1).first 284 | from = loc.absolute_path || loc.path || File.join(Dir.pwd, "main") 285 | realpath = File.absolute_path( 286 | File.join( 287 | File.dirname(File.absolute_path(from)), 288 | path 289 | ) 290 | ) 291 | 292 | require(realpath) 293 | end 294 | 295 | alias_method :load_without_require_hooks, :load 296 | def load(path, wrap = false) 297 | if wrap 298 | warn "RequireHooks does not support `load(smth, wrap: ...)`. Falling back to original `Kernel#load`" if RequireHooks.print_warnings 299 | return load_without_require_hooks(path, wrap) 300 | end 301 | 302 | path = path.to_path if path.respond_to?(:to_path) 303 | raise TypeError unless path.respond_to?(:to_str) 304 | 305 | path = path.to_str 306 | 307 | raise TypeError unless path.is_a?(::String) 308 | 309 | realpath = 310 | if path =~ /^\.\.?\// 311 | path 312 | else 313 | RequireHooks::KernelPatch::Features.feature_path(path, implitic_ext: false) 314 | end 315 | 316 | return load_without_require_hooks(path, wrap) unless realpath 317 | 318 | RequireHooks::KernelPatch.load(realpath) 319 | rescue Errno::ENOENT, Errno::EACCES 320 | raise LoadError, "cannot load such file -- #{path}" 321 | rescue LoadError => e 322 | warn "RuquireHooks failed to load '#{path}': #{e.message}" if RequireHooks.print_warnings 323 | load_without_require_hooks(path) 324 | end 325 | end 326 | -------------------------------------------------------------------------------- /lib/require-hooks/mode/load_iseq.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RequireHooks 4 | module LoadIseq 5 | def load_iseq(path) 6 | ctx = RequireHooks.context_for(path) 7 | 8 | ctx.run_around_load_callbacks(path) do 9 | if ctx.source_transform? || ctx.hijack? 10 | new_contents = ctx.perform_source_transform(path) 11 | hijacked = ctx.try_hijack_load(path, new_contents) 12 | 13 | if hijacked 14 | raise TypeError, "Unsupported bytecode format for #{path}: #{hijack.class}" unless hijacked.is_a?(::RubyVM::InstructionSequence) 15 | return hijacked 16 | elsif new_contents 17 | return RubyVM::InstructionSequence.compile(new_contents, path, path, 1) 18 | end 19 | end 20 | 21 | defined?(super) ? super : RubyVM::InstructionSequence.compile_file(path) 22 | end 23 | end 24 | end 25 | end 26 | 27 | if RubyVM::InstructionSequence.respond_to?(:load_iseq) 28 | warn "require-hooks: RubyVM::InstructionSequence.load_iseq is already defined. It won't be used by files processed by require-hooks." 29 | end 30 | 31 | RubyVM::InstructionSequence.singleton_class.prepend(RequireHooks::LoadIseq) 32 | -------------------------------------------------------------------------------- /lib/require-hooks/setup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "require-hooks/api" 4 | 5 | mode = ENV["REQUIRE_HOOKS_MODE"] 6 | 7 | case mode 8 | when "patch" 9 | require "require-hooks/mode/kernel_patch" 10 | when "load_iseq" 11 | require "require-hooks/mode/load_iseq" 12 | when "bootsnap" 13 | require "require-hooks/mode/bootsnap" 14 | else 15 | if defined?(::RubyVM::InstructionSequence) 16 | # Check if Bootsnap has been loaded. 17 | # Based on https://github.com/kddeisz/preval/blob/master/lib/preval.rb 18 | if RubyVM::InstructionSequence.respond_to?(:load_iseq) && 19 | (load_iseq = RubyVM::InstructionSequence.method(:load_iseq)) && 20 | load_iseq.source_location[0].include?("/bootsnap/") 21 | require "require-hooks/mode/bootsnap" 22 | else 23 | require "require-hooks/mode/load_iseq" 24 | end 25 | else 26 | require "require-hooks/mode/kernel_patch" 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/require-hooks/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RequireHooks 4 | VERSION = "0.2.2" 5 | end 6 | -------------------------------------------------------------------------------- /require-hooks.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/require-hooks/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "require-hooks" 7 | s.version = RequireHooks::VERSION 8 | s.authors = ["Vladimir Dementyev"] 9 | s.email = ["dementiev.vm@gmail.com"] 10 | s.homepage = "https://github.com/ruby-next/ruby-next" 11 | s.summary = "Require Hooks provide infrastructure for intercepting require/load calls in Ruby." 12 | s.description = "Require Hooks provide infrastructure for intercepting require/load calls in Ruby" 13 | 14 | s.metadata = { 15 | "bug_tracker_uri" => "https://github.com/ruby-next/ruby-next/issues", 16 | "changelog_uri" => "https://github.com/ruby-next/ruby-next/blob/master/CHANGELOG.md", 17 | "documentation_uri" => "https://github.com/ruby-next/ruby-next/blob/master/README.md", 18 | "homepage_uri" => "https://github.com/ruby-next/ruby-next", 19 | "source_code_uri" => "https://github.com/ruby-next/ruby-next", 20 | "funding_uri" => "https://github.com/sponsors/palkan" 21 | } 22 | 23 | s.license = "MIT" 24 | 25 | s.files = Dir.glob("lib/**/*") + %w[README.md LICENSE.txt CHANGELOG.md] 26 | s.require_paths = ["lib"] 27 | s.required_ruby_version = ">= 2.2" 28 | end 29 | -------------------------------------------------------------------------------- /spec/core/kernel/load_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | require_relative '../../fixtures/code_loading' 3 | require_relative 'shared/load' 4 | require_relative 'shared/require' 5 | 6 | describe "Kernel#load" do 7 | before :each do 8 | CodeLoadingSpecs.spec_setup 9 | end 10 | 11 | after :each do 12 | CodeLoadingSpecs.spec_cleanup 13 | end 14 | 15 | it "is a private method" do 16 | Kernel.should have_private_instance_method(:load) 17 | end 18 | 19 | it_behaves_like :kernel_require_basic, :load, CodeLoadingSpecs::Method.new 20 | end 21 | 22 | describe "Kernel#load" do 23 | it_behaves_like :kernel_load, :load, CodeLoadingSpecs::Method.new 24 | end 25 | 26 | describe "Kernel.load" do 27 | before :each do 28 | CodeLoadingSpecs.spec_setup 29 | end 30 | 31 | after :each do 32 | CodeLoadingSpecs.spec_cleanup 33 | end 34 | 35 | it_behaves_like :kernel_require_basic, :load, Kernel 36 | end 37 | 38 | describe "Kernel.load" do 39 | it_behaves_like :kernel_load, :load, Kernel 40 | end 41 | -------------------------------------------------------------------------------- /spec/core/kernel/require_relative_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | require_relative '../../fixtures/code_loading' 3 | 4 | describe "Kernel#require_relative with a relative path" do 5 | before :each do 6 | CodeLoadingSpecs.spec_setup 7 | @dir = "../../fixtures/code" 8 | @abs_dir = File.realpath(@dir, File.dirname(__FILE__)) 9 | @path = "#{@dir}/load_fixture.rb" 10 | @abs_path = File.realpath(@path, File.dirname(__FILE__)) 11 | end 12 | 13 | after :each do 14 | CodeLoadingSpecs.spec_cleanup 15 | end 16 | 17 | platform_is_not :windows do 18 | describe "when file is a symlink" do 19 | before :each do 20 | @link = tmp("symlink.rb", false) 21 | @real_path = "#{@abs_dir}/symlink/symlink1.rb" 22 | File.symlink(@real_path, @link) 23 | end 24 | 25 | after :each do 26 | rm_r @link 27 | end 28 | 29 | it "loads a path relative to current file" do 30 | require_relative(@link).should be_true 31 | ScratchPad.recorded.should == [:loaded] 32 | end 33 | end 34 | end 35 | 36 | it "loads a path relative to the current file" do 37 | require_relative(@path).should be_true 38 | ScratchPad.recorded.should == [:loaded] 39 | end 40 | 41 | describe "in an #instance_eval with a" do 42 | 43 | it "synthetic file base name loads a file base name relative to the working directory" do 44 | Dir.chdir @abs_dir do 45 | Object.new.instance_eval("require_relative(#{File.basename(@path).inspect})", "foo.rb").should be_true 46 | end 47 | ScratchPad.recorded.should == [:loaded] 48 | end 49 | 50 | it "synthetic file path loads a relative path relative to the working directory plus the directory of the synthetic path" do 51 | Dir.chdir @abs_dir do 52 | Object.new.instance_eval("require_relative(File.join('..', #{File.basename(@path).inspect}))", "bar/foo.rb").should be_true 53 | end 54 | ScratchPad.recorded.should == [:loaded] 55 | end 56 | 57 | platform_is_not :windows do 58 | it "synthetic relative file path with a Windows path separator specified loads a relative path relative to the working directory" do 59 | Dir.chdir @abs_dir do 60 | Object.new.instance_eval("require_relative(#{File.basename(@path).inspect})", "bar\\foo.rb").should be_true 61 | end 62 | ScratchPad.recorded.should == [:loaded] 63 | end 64 | end 65 | 66 | it "absolute file path loads a path relative to the absolute path" do 67 | Object.new.instance_eval("require_relative(#{@path.inspect})", __FILE__).should be_true 68 | ScratchPad.recorded.should == [:loaded] 69 | end 70 | 71 | it "absolute file path loads a path relative to the root directory" do 72 | root = @abs_path 73 | until File.dirname(root) == root 74 | root = File.dirname(root) 75 | end 76 | root_relative = @abs_path[root.size..-1] 77 | Object.new.instance_eval("require_relative(#{root_relative.inspect})", "/").should be_true 78 | ScratchPad.recorded.should == [:loaded] 79 | end 80 | 81 | end 82 | 83 | it "loads a file defining many methods" do 84 | require_relative("#{@dir}/methods_fixture.rb").should be_true 85 | ScratchPad.recorded.should == [:loaded] 86 | end 87 | 88 | it "raises a LoadError if the file does not exist" do 89 | -> { require_relative("#{@dir}/nonexistent.rb") }.should raise_error(LoadError) 90 | ScratchPad.recorded.should == [] 91 | end 92 | 93 | it "raises a LoadError that includes the missing path" do 94 | next skip if defined?(JRUBY_VERSION) 95 | missing_path = "#{@dir}/nonexistent.rb" 96 | expanded_missing_path = File.expand_path(missing_path, File.dirname(__FILE__)) 97 | -> { require_relative(missing_path) }.should raise_error(LoadError) { |e| 98 | e.message.should include(expanded_missing_path) 99 | e.path.should == expanded_missing_path 100 | } 101 | ScratchPad.recorded.should == [] 102 | end 103 | 104 | it "raises a LoadError if basepath does not exist" do 105 | -> { eval("require_relative('#{@dir}/nonexistent.rb')") }.should raise_error(LoadError) 106 | end 107 | 108 | it "stores the missing path in a LoadError object" do 109 | next skip if defined?(JRUBY_VERSION) 110 | 111 | path = "#{@dir}/nonexistent.rb" 112 | 113 | -> { 114 | require_relative(path) 115 | }.should(raise_error(LoadError) { |e| 116 | e.path.should == File.expand_path(path, @abs_dir) 117 | }) 118 | end 119 | 120 | it "calls #to_str on non-String objects" do 121 | name = mock("load_fixture.rb mock") 122 | name.should_receive(:to_str).and_return(@path).at_least(1) 123 | require_relative(name).should be_true 124 | ScratchPad.recorded.should == [:loaded] 125 | end 126 | 127 | it "raises a TypeError if argument does not respond to #to_str" do 128 | -> { require_relative(nil) }.should raise_error(TypeError) 129 | -> { require_relative(42) }.should raise_error(TypeError) 130 | -> { 131 | require_relative([@path,@path]) 132 | }.should raise_error(TypeError) 133 | end 134 | 135 | it "raises a TypeError if passed an object that has #to_s but not #to_str" do 136 | name = mock("load_fixture.rb mock") 137 | name.stub!(:to_s).and_return(@path) 138 | -> { require_relative(name) }.should raise_error(TypeError) 139 | end 140 | 141 | it "raises a TypeError if #to_str does not return a String" do 142 | name = mock("#to_str returns nil") 143 | name.should_receive(:to_str).at_least(1).times.and_return(nil) 144 | -> { require_relative(name) }.should raise_error(TypeError) 145 | end 146 | 147 | it "calls #to_path on non-String objects" do 148 | name = mock("load_fixture.rb mock") 149 | name.should_receive(:to_path).and_return(@path).at_least(1) 150 | require_relative(name).should be_true 151 | ScratchPad.recorded.should == [:loaded] 152 | end 153 | 154 | it "calls #to_str on non-String objects returned by #to_path" do 155 | name = mock("load_fixture.rb mock") 156 | to_path = mock("load_fixture_rb #to_path mock") 157 | name.should_receive(:to_path).and_return(to_path).at_least(1) 158 | to_path.should_receive(:to_str).and_return(@path).at_least(1) 159 | require_relative(name).should be_true 160 | ScratchPad.recorded.should == [:loaded] 161 | end 162 | 163 | describe "(file extensions)" do 164 | it "loads a .rb extensioned file when passed a non-extensioned path" do 165 | require_relative("#{@dir}/load_fixture").should be_true 166 | ScratchPad.recorded.should == [:loaded] 167 | end 168 | 169 | it "loads a .rb extensioned file when a C-extension file of the same name is loaded" do 170 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.bundle" 171 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.dylib" 172 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.so" 173 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.dll" 174 | require_relative(@path).should be_true 175 | ScratchPad.recorded.should == [:loaded] 176 | end 177 | 178 | it "does not load a C-extension file if a .rb extensioned file is already loaded" do 179 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.rb" 180 | require_relative("#{@dir}/load_fixture").should be_false 181 | ScratchPad.recorded.should == [] 182 | end 183 | 184 | it "loads a .rb extensioned file when passed a non-.rb extensioned path" do 185 | require_relative("#{@dir}/load_fixture.ext").should be_true 186 | ScratchPad.recorded.should == [:loaded] 187 | $LOADED_FEATURES.should include "#{@abs_dir}/load_fixture.ext.rb" 188 | end 189 | 190 | it "loads a .rb extensioned file when a complex-extensioned C-extension file of the same name is loaded" do 191 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.bundle" 192 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.dylib" 193 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.so" 194 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.dll" 195 | require_relative("#{@dir}/load_fixture.ext").should be_true 196 | ScratchPad.recorded.should == [:loaded] 197 | $LOADED_FEATURES.should include "#{@abs_dir}/load_fixture.ext.rb" 198 | end 199 | 200 | it "does not load a C-extension file if a complex-extensioned .rb file is already loaded" do 201 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.rb" 202 | require_relative("#{@dir}/load_fixture.ext").should be_false 203 | ScratchPad.recorded.should == [] 204 | end 205 | end 206 | 207 | describe "($LOADED_FEATURES)" do 208 | it "stores an absolute path" do 209 | require_relative(@path).should be_true 210 | $LOADED_FEATURES.should include(@abs_path) 211 | end 212 | 213 | platform_is_not :windows, :wasi do 214 | describe "with symlinks" do 215 | before :each do 216 | @symlink_to_code_dir = tmp("codesymlink") 217 | File.symlink(CODE_LOADING_DIR, @symlink_to_code_dir) 218 | @symlink_basename = File.basename(@symlink_to_code_dir) 219 | @requiring_file = tmp("requiring") 220 | end 221 | 222 | after :each do 223 | rm_r @symlink_to_code_dir, @requiring_file 224 | end 225 | 226 | it "does not canonicalize the path and stores a path with symlinks" do 227 | next skip if defined?(JRUBY_VERSION) 228 | 229 | symlink_path = "#{@symlink_basename}/load_fixture.rb" 230 | absolute_path = "#{tmp("")}#{symlink_path}" 231 | canonical_path = "#{CODE_LOADING_DIR}/load_fixture.rb" 232 | touch(@requiring_file) { |f| 233 | f.puts "require_relative #{symlink_path.inspect}" 234 | } 235 | load(@requiring_file) 236 | ScratchPad.recorded.should == [:loaded] 237 | 238 | features = $LOADED_FEATURES.select { |path| path.end_with?('load_fixture.rb') } 239 | features.should include(absolute_path) 240 | features.should_not include(canonical_path) 241 | end 242 | 243 | it "stores the same path that __FILE__ returns in the required file" do 244 | symlink_path = "#{@symlink_basename}/load_fixture_and__FILE__.rb" 245 | touch(@requiring_file) { |f| 246 | f.puts "require_relative #{symlink_path.inspect}" 247 | } 248 | load(@requiring_file) 249 | loaded_feature = $LOADED_FEATURES.last 250 | ScratchPad.recorded.should == [loaded_feature] 251 | end 252 | end 253 | end 254 | 255 | it "does not store the path if the load fails" do 256 | saved_loaded_features = $LOADED_FEATURES.dup 257 | -> { require_relative("#{@dir}/raise_fixture.rb") }.should raise_error(RuntimeError) 258 | $LOADED_FEATURES.should == saved_loaded_features 259 | end 260 | 261 | it "does not load an absolute path that is already stored" do 262 | $LOADED_FEATURES << @abs_path 263 | require_relative(@path).should be_false 264 | ScratchPad.recorded.should == [] 265 | end 266 | 267 | it "adds the suffix of the resolved filename" do 268 | require_relative("#{@dir}/load_fixture").should be_true 269 | $LOADED_FEATURES.should include("#{@abs_dir}/load_fixture.rb") 270 | end 271 | 272 | it "loads a path for a file already loaded with a relative path" do 273 | $LOAD_PATH << File.expand_path(@dir) 274 | $LOADED_FEATURES << "load_fixture.rb" << "load_fixture" 275 | require_relative(@path).should be_true 276 | $LOADED_FEATURES.should include(@abs_path) 277 | ScratchPad.recorded.should == [:loaded] 278 | end 279 | end 280 | end 281 | 282 | describe "Kernel#require_relative with an absolute path" do 283 | before :each do 284 | CodeLoadingSpecs.spec_setup 285 | @dir = File.expand_path "../../fixtures/code", File.dirname(__FILE__) 286 | @abs_dir = @dir 287 | @path = File.join @dir, "load_fixture.rb" 288 | @abs_path = @path 289 | end 290 | 291 | after :each do 292 | CodeLoadingSpecs.spec_cleanup 293 | end 294 | 295 | it "loads a path relative to the current file" do 296 | require_relative(@path).should be_true 297 | ScratchPad.recorded.should == [:loaded] 298 | end 299 | 300 | it "loads a file defining many methods" do 301 | require_relative("#{@dir}/methods_fixture.rb").should be_true 302 | ScratchPad.recorded.should == [:loaded] 303 | end 304 | 305 | it "raises a LoadError if the file does not exist" do 306 | -> { require_relative("#{@dir}/nonexistent.rb") }.should raise_error(LoadError) 307 | ScratchPad.recorded.should == [] 308 | end 309 | 310 | it "raises a LoadError if basepath does not exist" do 311 | -> { eval("require_relative('#{@dir}/nonexistent.rb')") }.should raise_error(LoadError) 312 | end 313 | 314 | it "stores the missing path in a LoadError object" do 315 | next skip if defined?(JRUBY_VERSION) 316 | 317 | path = "#{@dir}/nonexistent.rb" 318 | 319 | -> { 320 | require_relative(path) 321 | }.should(raise_error(LoadError) { |e| 322 | e.path.should == File.expand_path(path, @abs_dir) 323 | }) 324 | end 325 | 326 | it "calls #to_str on non-String objects" do 327 | name = mock("load_fixture.rb mock") 328 | name.should_receive(:to_str).and_return(@path).at_least(1) 329 | require_relative(name).should be_true 330 | ScratchPad.recorded.should == [:loaded] 331 | end 332 | 333 | it "raises a TypeError if argument does not respond to #to_str" do 334 | -> { require_relative(nil) }.should raise_error(TypeError) 335 | -> { require_relative(42) }.should raise_error(TypeError) 336 | -> { 337 | require_relative([@path,@path]) 338 | }.should raise_error(TypeError) 339 | end 340 | 341 | it "raises a TypeError if passed an object that has #to_s but not #to_str" do 342 | name = mock("load_fixture.rb mock") 343 | name.stub!(:to_s).and_return(@path) 344 | -> { require_relative(name) }.should raise_error(TypeError) 345 | end 346 | 347 | it "raises a TypeError if #to_str does not return a String" do 348 | name = mock("#to_str returns nil") 349 | name.should_receive(:to_str).at_least(1).times.and_return(nil) 350 | -> { require_relative(name) }.should raise_error(TypeError) 351 | end 352 | 353 | it "calls #to_path on non-String objects" do 354 | name = mock("load_fixture.rb mock") 355 | name.should_receive(:to_path).and_return(@path).at_least(1) 356 | require_relative(name).should be_true 357 | ScratchPad.recorded.should == [:loaded] 358 | end 359 | 360 | it "calls #to_str on non-String objects returned by #to_path" do 361 | name = mock("load_fixture.rb mock") 362 | to_path = mock("load_fixture_rb #to_path mock") 363 | name.should_receive(:to_path).and_return(to_path).at_least(1) 364 | to_path.should_receive(:to_str).and_return(@path).at_least(1) 365 | require_relative(name).should be_true 366 | ScratchPad.recorded.should == [:loaded] 367 | end 368 | 369 | describe "(file extensions)" do 370 | it "loads a .rb extensioned file when passed a non-extensioned path" do 371 | require_relative("#{@dir}/load_fixture").should be_true 372 | ScratchPad.recorded.should == [:loaded] 373 | end 374 | 375 | it "loads a .rb extensioned file when a C-extension file of the same name is loaded" do 376 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.bundle" 377 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.dylib" 378 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.so" 379 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.dll" 380 | require_relative(@path).should be_true 381 | ScratchPad.recorded.should == [:loaded] 382 | end 383 | 384 | it "does not load a C-extension file if a .rb extensioned file is already loaded" do 385 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.rb" 386 | require_relative("#{@dir}/load_fixture").should be_false 387 | ScratchPad.recorded.should == [] 388 | end 389 | 390 | it "loads a .rb extensioned file when passed a non-.rb extensioned path" do 391 | require_relative("#{@dir}/load_fixture.ext").should be_true 392 | ScratchPad.recorded.should == [:loaded] 393 | $LOADED_FEATURES.should include "#{@abs_dir}/load_fixture.ext.rb" 394 | end 395 | 396 | it "loads a .rb extensioned file when a complex-extensioned C-extension file of the same name is loaded" do 397 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.bundle" 398 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.dylib" 399 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.so" 400 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.dll" 401 | require_relative("#{@dir}/load_fixture.ext").should be_true 402 | ScratchPad.recorded.should == [:loaded] 403 | $LOADED_FEATURES.should include "#{@abs_dir}/load_fixture.ext.rb" 404 | end 405 | 406 | it "does not load a C-extension file if a complex-extensioned .rb file is already loaded" do 407 | $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.rb" 408 | require_relative("#{@dir}/load_fixture.ext").should be_false 409 | ScratchPad.recorded.should == [] 410 | end 411 | end 412 | 413 | describe "($LOAD_FEATURES)" do 414 | it "stores an absolute path" do 415 | require_relative(@path).should be_true 416 | $LOADED_FEATURES.should include(@abs_path) 417 | end 418 | 419 | it "does not store the path if the load fails" do 420 | saved_loaded_features = $LOADED_FEATURES.dup 421 | -> { require_relative("#{@dir}/raise_fixture.rb") }.should raise_error(RuntimeError) 422 | $LOADED_FEATURES.should == saved_loaded_features 423 | end 424 | 425 | it "does not load an absolute path that is already stored" do 426 | $LOADED_FEATURES << @abs_path 427 | require_relative(@path).should be_false 428 | ScratchPad.recorded.should == [] 429 | end 430 | 431 | it "adds the suffix of the resolved filename" do 432 | require_relative("#{@dir}/load_fixture").should be_true 433 | $LOADED_FEATURES.should include("#{@abs_dir}/load_fixture.rb") 434 | end 435 | 436 | it "loads a path for a file already loaded with a relative path" do 437 | $LOAD_PATH << File.expand_path(@dir) 438 | $LOADED_FEATURES << "load_fixture.rb" << "load_fixture" 439 | require_relative(@path).should be_true 440 | $LOADED_FEATURES.should include(@abs_path) 441 | ScratchPad.recorded.should == [:loaded] 442 | end 443 | end 444 | end 445 | -------------------------------------------------------------------------------- /spec/core/kernel/require_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | require_relative '../../fixtures/code_loading' 3 | require_relative 'shared/require' 4 | 5 | describe "Kernel#require" do 6 | before :each do 7 | CodeLoadingSpecs.spec_setup 8 | end 9 | 10 | after :each do 11 | CodeLoadingSpecs.spec_cleanup 12 | end 13 | 14 | # if this fails, update your rubygems 15 | it "is a private method" do 16 | Kernel.should have_private_instance_method(:require) 17 | end 18 | 19 | provided = %w[complex enumerator rational thread ruby2_keywords] 20 | ruby_version_is "3.1" do 21 | provided << "fiber" 22 | end 23 | 24 | it "#{provided.join(', ')} are already required" do 25 | # Irrelevant to our environment 26 | next skip 27 | out = ruby_exe("puts $LOADED_FEATURES", options: '--disable-gems --disable-did-you-mean') 28 | features = out.lines.map { |line| File.basename(line.chomp, '.*') } 29 | 30 | # Ignore CRuby internals 31 | features -= %w[encdb transdb windows_1252] 32 | features.reject! { |feature| feature.end_with?('-fake') } 33 | 34 | features.sort.should == provided.sort 35 | 36 | code = provided.map { |f| "puts require #{f.inspect}\n" }.join 37 | required = ruby_exe(code, options: '--disable-gems') 38 | required.should == "false\n" * provided.size 39 | end 40 | 41 | it_behaves_like :kernel_require_basic, :require, CodeLoadingSpecs::Method.new 42 | it_behaves_like :kernel_require, :require, CodeLoadingSpecs::Method.new 43 | end 44 | 45 | describe "Kernel.require" do 46 | before :each do 47 | CodeLoadingSpecs.spec_setup 48 | end 49 | 50 | after :each do 51 | CodeLoadingSpecs.spec_cleanup 52 | end 53 | 54 | it_behaves_like :kernel_require_basic, :require, Kernel 55 | it_behaves_like :kernel_require, :require, Kernel 56 | end 57 | -------------------------------------------------------------------------------- /spec/core/kernel/shared/load.rb: -------------------------------------------------------------------------------- 1 | main = self 2 | 3 | describe :kernel_load, shared: true do 4 | before :each do 5 | CodeLoadingSpecs.spec_setup 6 | @path = File.expand_path "load_fixture.rb", CODE_LOADING_DIR 7 | end 8 | 9 | after :each do 10 | CodeLoadingSpecs.spec_cleanup 11 | end 12 | 13 | it "loads a non-extensioned file as a Ruby source file" do 14 | path = File.expand_path "load_fixture", CODE_LOADING_DIR 15 | @object.load(path).should be_true 16 | ScratchPad.recorded.should == [:no_ext] 17 | end 18 | 19 | it "loads a non .rb extensioned file as a Ruby source file" do 20 | path = File.expand_path "load_fixture.ext", CODE_LOADING_DIR 21 | @object.load(path).should be_true 22 | ScratchPad.recorded.should == [:no_rb_ext] 23 | end 24 | 25 | it "loads from the current working directory" do 26 | Dir.chdir CODE_LOADING_DIR do 27 | @object.load("load_fixture.rb").should be_true 28 | ScratchPad.recorded.should == [:loaded] 29 | end 30 | end 31 | 32 | it "loads a file that recursively requires itself" do 33 | path = File.expand_path "recursive_require_fixture.rb", CODE_LOADING_DIR 34 | -> { 35 | @object.load(path).should be_true 36 | }.should complain(/circular require considered harmful/, verbose: true) 37 | ScratchPad.recorded.should == [:loaded, :loaded] 38 | end 39 | 40 | it "loads a file that recursively loads itself" do 41 | path = File.expand_path "recursive_load_fixture.rb", CODE_LOADING_DIR 42 | @object.load(path).should be_true 43 | ScratchPad.recorded.should == [:loaded, :loaded] 44 | end 45 | 46 | it "loads a file each time the method is called" do 47 | @object.load(@path).should be_true 48 | @object.load(@path).should be_true 49 | ScratchPad.recorded.should == [:loaded, :loaded] 50 | end 51 | 52 | it "loads a file even when the name appears in $LOADED_FEATURES" do 53 | $LOADED_FEATURES << @path 54 | @object.load(@path).should be_true 55 | ScratchPad.recorded.should == [:loaded] 56 | end 57 | 58 | it "loads a file that has been loaded by #require" do 59 | @object.require(@path).should be_true 60 | @object.load(@path).should be_true 61 | ScratchPad.recorded.should == [:loaded, :loaded] 62 | end 63 | 64 | it "loads file even after $LOAD_PATH change" do 65 | $LOAD_PATH << CODE_LOADING_DIR 66 | @object.load("load_fixture.rb").should be_true 67 | $LOAD_PATH.unshift CODE_LOADING_DIR + "/gem" 68 | @object.load("load_fixture.rb").should be_true 69 | ScratchPad.recorded.should == [:loaded, :loaded_gem] 70 | end 71 | 72 | it "does not cause #require with the same path to fail" do 73 | @object.load(@path).should be_true 74 | @object.require(@path).should be_true 75 | ScratchPad.recorded.should == [:loaded, :loaded] 76 | end 77 | 78 | it "does not add the loaded path to $LOADED_FEATURES" do 79 | saved_loaded_features = $LOADED_FEATURES.dup 80 | @object.load(@path).should be_true 81 | $LOADED_FEATURES.should == saved_loaded_features 82 | end 83 | 84 | it "raises a LoadError if passed a non-extensioned path that does not exist but a .rb extensioned path does exist" do 85 | path = File.expand_path "load_ext_fixture", CODE_LOADING_DIR 86 | -> { @object.load(path) }.should raise_error(LoadError) 87 | end 88 | 89 | describe "when passed true for 'wrap'" do 90 | next skip 91 | 92 | it "loads from an existing path" do 93 | path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR 94 | @object.load(path, true).should be_true 95 | end 96 | 97 | it "sets the enclosing scope to an anonymous module" do 98 | path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR 99 | @object.load(path, true) 100 | 101 | Object.const_defined?(:LoadSpecWrap).should be_false 102 | 103 | wrap_module = ScratchPad.recorded[1] 104 | wrap_module.should be_an_instance_of(Module) 105 | end 106 | 107 | it "allows referencing outside namespaces" do 108 | path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR 109 | @object.load(path, true) 110 | 111 | ScratchPad.recorded[0].should equal(String) 112 | end 113 | 114 | it "sets self as a copy of the top-level main" do 115 | path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR 116 | @object.load(path, true) 117 | 118 | top_level = ScratchPad.recorded[2] 119 | top_level.to_s.should == "main" 120 | top_level.method(:to_s).owner.should == top_level.singleton_class 121 | top_level.should_not equal(main) 122 | top_level.should be_an_instance_of(Object) 123 | end 124 | 125 | it "includes modules included in main's singleton class in self's class" do 126 | mod = Module.new 127 | main.extend(mod) 128 | 129 | main_ancestors = main.singleton_class.ancestors[1..-1] 130 | main_ancestors.first.should == mod 131 | 132 | path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR 133 | @object.load(path, true) 134 | 135 | top_level = ScratchPad.recorded[2] 136 | top_level_ancestors = top_level.singleton_class.ancestors[-main_ancestors.size..-1] 137 | top_level_ancestors.should == main_ancestors 138 | 139 | wrap_module = ScratchPad.recorded[1] 140 | top_level.singleton_class.ancestors.should == [top_level.singleton_class, wrap_module, *main_ancestors] 141 | end 142 | 143 | describe "with top-level methods" do 144 | before :each do 145 | path = File.expand_path "load_wrap_method_fixture.rb", CODE_LOADING_DIR 146 | @object.load(path, true) 147 | end 148 | 149 | it "allows calling top-level methods" do 150 | ScratchPad.recorded.last.should == :load_wrap_loaded 151 | end 152 | 153 | it "does not pollute the receiver" do 154 | -> { @object.send(:top_level_method) }.should raise_error(NameError) 155 | end 156 | end 157 | end 158 | 159 | describe "when passed a module for 'wrap'" do 160 | next skip 161 | 162 | ruby_version_is "3.1" do 163 | it "sets the enclosing scope to the supplied module" do 164 | path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR 165 | mod = Module.new 166 | @object.load(path, mod) 167 | 168 | Object.const_defined?(:LoadSpecWrap).should be_false 169 | mod.const_defined?(:LoadSpecWrap).should be_true 170 | 171 | wrap_module = ScratchPad.recorded[1] 172 | wrap_module.should == mod 173 | end 174 | 175 | it "makes constants and instance methods in the source file reachable with the supplied module" do 176 | path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR 177 | mod = Module.new 178 | @object.load(path, mod) 179 | 180 | mod::LOAD_WRAP_SPECS_TOP_LEVEL_CONSTANT.should == 1 181 | obj = Object.new 182 | obj.extend(mod) 183 | obj.send(:load_wrap_specs_top_level_method).should == :load_wrap_specs_top_level_method 184 | end 185 | 186 | it "makes instance methods in the source file private" do 187 | path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR 188 | mod = Module.new 189 | @object.load(path, mod) 190 | 191 | mod.private_instance_methods.include?(:load_wrap_specs_top_level_method).should == true 192 | end 193 | end 194 | end 195 | 196 | describe "(shell expansion)" do 197 | before :each do 198 | @env_home = ENV["HOME"] 199 | ENV["HOME"] = CODE_LOADING_DIR 200 | end 201 | 202 | after :each do 203 | ENV["HOME"] = @env_home 204 | end 205 | 206 | it "expands a tilde to the HOME environment variable as the path to load" do 207 | @object.require("~/load_fixture.rb").should be_true 208 | ScratchPad.recorded.should == [:loaded] 209 | end 210 | end 211 | end 212 | -------------------------------------------------------------------------------- /spec/core/kernel/shared/require.rb: -------------------------------------------------------------------------------- 1 | describe :kernel_require_basic, shared: true do 2 | describe "(path resolution)" do 3 | it "loads an absolute path" do 4 | path = File.expand_path "load_fixture.rb", CODE_LOADING_DIR 5 | @object.send(@method, path).should be_true 6 | ScratchPad.recorded.should == [:loaded] 7 | end 8 | 9 | it "loads a non-canonical absolute path" do 10 | path = File.join CODE_LOADING_DIR, "..", "code", "load_fixture.rb" 11 | @object.send(@method, path).should be_true 12 | ScratchPad.recorded.should == [:loaded] 13 | end 14 | 15 | it "loads a file defining many methods" do 16 | path = File.expand_path "methods_fixture.rb", CODE_LOADING_DIR 17 | @object.send(@method, path).should be_true 18 | ScratchPad.recorded.should == [:loaded] 19 | end 20 | 21 | it "raises a LoadError if the file does not exist" do 22 | path = File.expand_path "nonexistent.rb", CODE_LOADING_DIR 23 | File.should_not.exist?(path) 24 | -> { @object.send(@method, path) }.should raise_error(LoadError) 25 | ScratchPad.recorded.should == [] 26 | end 27 | 28 | # Can't make a file unreadable on these platforms 29 | platform_is_not :windows, :cygwin do 30 | as_user do 31 | describe "with an unreadable file" do 32 | before :each do 33 | @path = tmp("unreadable_file.rb") 34 | touch @path 35 | File.chmod 0000, @path 36 | end 37 | 38 | after :each do 39 | File.chmod 0666, @path 40 | rm_r @path 41 | end 42 | 43 | it "raises a LoadError" do 44 | File.should.exist?(@path) 45 | -> { @object.send(@method, @path) }.should raise_error(LoadError) 46 | end 47 | end 48 | end 49 | end 50 | 51 | it "calls #to_str on non-String objects" do 52 | path = File.expand_path "load_fixture.rb", CODE_LOADING_DIR 53 | name = mock("load_fixture.rb mock") 54 | name.should_receive(:to_str).and_return(path).at_least(1) 55 | @object.send(@method, name).should be_true 56 | ScratchPad.recorded.should == [:loaded] 57 | end 58 | 59 | it "raises a TypeError if passed nil" do 60 | -> { @object.send(@method, nil) }.should raise_error(TypeError) 61 | end 62 | 63 | it "raises a TypeError if passed an Integer" do 64 | -> { @object.send(@method, 42) }.should raise_error(TypeError) 65 | end 66 | 67 | it "raises a TypeError if passed an Array" do 68 | -> { @object.send(@method, []) }.should raise_error(TypeError) 69 | end 70 | 71 | it "raises a TypeError if passed an object that does not provide #to_str" do 72 | -> { @object.send(@method, mock("not a filename")) }.should raise_error(TypeError) 73 | end 74 | 75 | it "raises a TypeError if passed an object that has #to_s but not #to_str" do 76 | name = mock("load_fixture.rb mock") 77 | name.stub!(:to_s).and_return("load_fixture.rb") 78 | $LOAD_PATH << "." 79 | Dir.chdir CODE_LOADING_DIR do 80 | -> { @object.send(@method, name) }.should raise_error(TypeError) 81 | end 82 | end 83 | 84 | it "raises a TypeError if #to_str does not return a String" do 85 | name = mock("#to_str returns nil") 86 | name.should_receive(:to_str).at_least(1).times.and_return(nil) 87 | -> { @object.send(@method, name) }.should raise_error(TypeError) 88 | end 89 | 90 | it "calls #to_path on non-String objects" do 91 | name = mock("load_fixture.rb mock") 92 | name.stub!(:to_path).and_return("load_fixture.rb") 93 | $LOAD_PATH << "." 94 | Dir.chdir CODE_LOADING_DIR do 95 | @object.send(@method, name).should be_true 96 | end 97 | ScratchPad.recorded.should == [:loaded] 98 | end 99 | 100 | it "calls #to_path on a String" do 101 | path = File.expand_path "load_fixture.rb", CODE_LOADING_DIR 102 | str = mock("load_fixture.rb mock") 103 | str.should_receive(:to_path).and_return(path).at_least(1) 104 | @object.send(@method, str).should be_true 105 | ScratchPad.recorded.should == [:loaded] 106 | end 107 | 108 | it "calls #to_str on non-String objects returned by #to_path" do 109 | path = File.expand_path "load_fixture.rb", CODE_LOADING_DIR 110 | name = mock("load_fixture.rb mock") 111 | to_path = mock("load_fixture_rb #to_path mock") 112 | name.should_receive(:to_path).and_return(to_path).at_least(1) 113 | to_path.should_receive(:to_str).and_return(path).at_least(1) 114 | @object.send(@method, name).should be_true 115 | ScratchPad.recorded.should == [:loaded] 116 | end 117 | 118 | # "http://redmine.ruby-lang.org/issues/show/2578" 119 | it "loads a ./ relative path from the current working directory with empty $LOAD_PATH" do 120 | Dir.chdir CODE_LOADING_DIR do 121 | @object.send(@method, "./load_fixture.rb").should be_true 122 | end 123 | ScratchPad.recorded.should == [:loaded] 124 | end 125 | 126 | it "loads a ../ relative path from the current working directory with empty $LOAD_PATH" do 127 | Dir.chdir CODE_LOADING_DIR do 128 | @object.send(@method, "../code/load_fixture.rb").should be_true 129 | end 130 | ScratchPad.recorded.should == [:loaded] 131 | end 132 | 133 | it "loads a ./ relative path from the current working directory with non-empty $LOAD_PATH" do 134 | $LOAD_PATH << "an_irrelevant_dir" 135 | Dir.chdir CODE_LOADING_DIR do 136 | @object.send(@method, "./load_fixture.rb").should be_true 137 | end 138 | ScratchPad.recorded.should == [:loaded] 139 | end 140 | 141 | it "loads a ../ relative path from the current working directory with non-empty $LOAD_PATH" do 142 | $LOAD_PATH << "an_irrelevant_dir" 143 | Dir.chdir CODE_LOADING_DIR do 144 | @object.send(@method, "../code/load_fixture.rb").should be_true 145 | end 146 | ScratchPad.recorded.should == [:loaded] 147 | end 148 | 149 | it "loads a non-canonical path from the current working directory with non-empty $LOAD_PATH" do 150 | $LOAD_PATH << "an_irrelevant_dir" 151 | Dir.chdir CODE_LOADING_DIR do 152 | @object.send(@method, "../code/../code/load_fixture.rb").should be_true 153 | end 154 | ScratchPad.recorded.should == [:loaded] 155 | end 156 | 157 | it "resolves a filename against $LOAD_PATH entries" do 158 | $LOAD_PATH << CODE_LOADING_DIR 159 | @object.send(@method, "load_fixture.rb").should be_true 160 | ScratchPad.recorded.should == [:loaded] 161 | end 162 | 163 | it "accepts an Object with #to_path in $LOAD_PATH" do 164 | obj = mock("to_path") 165 | obj.should_receive(:to_path).at_least(:once).and_return(CODE_LOADING_DIR) 166 | $LOAD_PATH << obj 167 | @object.send(@method, "load_fixture.rb").should be_true 168 | ScratchPad.recorded.should == [:loaded] 169 | end 170 | 171 | it "does not require file twice after $LOAD_PATH change" do 172 | $LOAD_PATH << CODE_LOADING_DIR 173 | @object.require("load_fixture.rb").should be_true 174 | $LOAD_PATH.push CODE_LOADING_DIR + "/gem" 175 | @object.require("load_fixture.rb").should be_false 176 | ScratchPad.recorded.should == [:loaded] 177 | end 178 | 179 | it "does not resolve a ./ relative path against $LOAD_PATH entries" do 180 | $LOAD_PATH << CODE_LOADING_DIR 181 | -> do 182 | @object.send(@method, "./load_fixture.rb") 183 | end.should raise_error(LoadError) 184 | ScratchPad.recorded.should == [] 185 | end 186 | 187 | it "does not resolve a ../ relative path against $LOAD_PATH entries" do 188 | $LOAD_PATH << CODE_LOADING_DIR 189 | -> do 190 | @object.send(@method, "../code/load_fixture.rb") 191 | end.should raise_error(LoadError) 192 | ScratchPad.recorded.should == [] 193 | end 194 | 195 | it "resolves a non-canonical path against $LOAD_PATH entries" do 196 | $LOAD_PATH << File.dirname(CODE_LOADING_DIR) 197 | @object.send(@method, "code/../code/load_fixture.rb").should be_true 198 | ScratchPad.recorded.should == [:loaded] 199 | end 200 | 201 | it "loads a path with duplicate path separators" do 202 | $LOAD_PATH << "." 203 | sep = File::Separator + File::Separator 204 | path = ["..", "code", "load_fixture.rb"].join(sep) 205 | Dir.chdir CODE_LOADING_DIR do 206 | @object.send(@method, path).should be_true 207 | end 208 | ScratchPad.recorded.should == [:loaded] 209 | end 210 | end 211 | end 212 | 213 | describe :kernel_require, shared: true do 214 | describe "(path resolution)" do 215 | # For reference see [ruby-core:24155] in which matz confirms this feature is 216 | # intentional for security reasons. 217 | it "does not load a bare filename unless the current working directory is in $LOAD_PATH" do 218 | Dir.chdir CODE_LOADING_DIR do 219 | -> { @object.require("load_fixture.rb") }.should raise_error(LoadError) 220 | ScratchPad.recorded.should == [] 221 | end 222 | end 223 | 224 | it "does not load a relative path unless the current working directory is in $LOAD_PATH" do 225 | Dir.chdir File.dirname(CODE_LOADING_DIR) do 226 | -> do 227 | @object.require("code/load_fixture.rb") 228 | end.should raise_error(LoadError) 229 | ScratchPad.recorded.should == [] 230 | end 231 | end 232 | 233 | it "loads a file that recursively requires itself" do 234 | path = File.expand_path "recursive_require_fixture.rb", CODE_LOADING_DIR 235 | -> { 236 | @object.require(path).should be_true 237 | }.should complain(/circular require considered harmful/, verbose: true) 238 | ScratchPad.recorded.should == [:loaded] 239 | end 240 | 241 | ruby_bug "#17340", ''...'3.3' do 242 | it "loads a file concurrently" do 243 | path = File.expand_path "concurrent_require_fixture.rb", CODE_LOADING_DIR 244 | ScratchPad.record(@object) 245 | -> { 246 | @object.require(path) 247 | }.should_not complain(/circular require considered harmful/, verbose: true) 248 | ScratchPad.recorded.join 249 | end 250 | end 251 | end 252 | 253 | describe "(non-extensioned path)" do 254 | before :each do 255 | a = File.expand_path "a", CODE_LOADING_DIR 256 | b = File.expand_path "b", CODE_LOADING_DIR 257 | $LOAD_PATH.replace [a, b] 258 | end 259 | 260 | it "loads a .rb extensioned file when a C-extension file exists on an earlier load path" do 261 | @object.require("load_fixture").should be_true 262 | ScratchPad.recorded.should == [:loaded] 263 | end 264 | 265 | it "does not load a feature twice when $LOAD_PATH has been modified" do 266 | $LOAD_PATH.replace [CODE_LOADING_DIR] 267 | @object.require("load_fixture").should be_true 268 | $LOAD_PATH.replace [File.expand_path("b", CODE_LOADING_DIR), CODE_LOADING_DIR] 269 | 270 | @object.require("load_fixture").should be_false 271 | end 272 | end 273 | 274 | describe "(file extensions)" do 275 | it "loads a .rb extensioned file when passed a non-extensioned path" do 276 | path = File.expand_path "load_fixture", CODE_LOADING_DIR 277 | File.should.exist?(path) 278 | @object.require(path).should be_true 279 | ScratchPad.recorded.should == [:loaded] 280 | end 281 | 282 | it "loads a .rb extensioned file when a C-extension file of the same name is loaded" do 283 | $LOADED_FEATURES << File.expand_path("load_fixture.bundle", CODE_LOADING_DIR) 284 | $LOADED_FEATURES << File.expand_path("load_fixture.dylib", CODE_LOADING_DIR) 285 | $LOADED_FEATURES << File.expand_path("load_fixture.so", CODE_LOADING_DIR) 286 | $LOADED_FEATURES << File.expand_path("load_fixture.dll", CODE_LOADING_DIR) 287 | path = File.expand_path "load_fixture", CODE_LOADING_DIR 288 | @object.require(path).should be_true 289 | ScratchPad.recorded.should == [:loaded] 290 | end 291 | 292 | it "does not load a C-extension file if a .rb extensioned file is already loaded" do 293 | $LOADED_FEATURES << File.expand_path("load_fixture.rb", CODE_LOADING_DIR) 294 | path = File.expand_path "load_fixture", CODE_LOADING_DIR 295 | @object.require(path).should be_false 296 | ScratchPad.recorded.should == [] 297 | end 298 | 299 | it "loads a .rb extensioned file when passed a non-.rb extensioned path" do 300 | path = File.expand_path "load_fixture.ext", CODE_LOADING_DIR 301 | File.should.exist?(path) 302 | @object.require(path).should be_true 303 | ScratchPad.recorded.should == [:loaded] 304 | end 305 | 306 | it "loads a .rb extensioned file when a complex-extensioned C-extension file of the same name is loaded" do 307 | $LOADED_FEATURES << File.expand_path("load_fixture.ext.bundle", CODE_LOADING_DIR) 308 | $LOADED_FEATURES << File.expand_path("load_fixture.ext.dylib", CODE_LOADING_DIR) 309 | $LOADED_FEATURES << File.expand_path("load_fixture.ext.so", CODE_LOADING_DIR) 310 | $LOADED_FEATURES << File.expand_path("load_fixture.ext.dll", CODE_LOADING_DIR) 311 | path = File.expand_path "load_fixture.ext", CODE_LOADING_DIR 312 | @object.require(path).should be_true 313 | ScratchPad.recorded.should == [:loaded] 314 | end 315 | 316 | it "does not load a C-extension file if a complex-extensioned .rb file is already loaded" do 317 | $LOADED_FEATURES << File.expand_path("load_fixture.ext.rb", CODE_LOADING_DIR) 318 | path = File.expand_path "load_fixture.ext", CODE_LOADING_DIR 319 | @object.require(path).should be_false 320 | ScratchPad.recorded.should == [] 321 | end 322 | end 323 | 324 | describe "($LOADED_FEATURES)" do 325 | before :each do 326 | @path = File.expand_path("load_fixture.rb", CODE_LOADING_DIR) 327 | end 328 | 329 | it "stores an absolute path" do 330 | @object.require(@path).should be_true 331 | $LOADED_FEATURES.should include(@path) 332 | end 333 | 334 | platform_is_not :windows do 335 | describe "with symlinks" do 336 | before :each do 337 | @symlink_to_code_dir = tmp("codesymlink") 338 | File.symlink(CODE_LOADING_DIR, @symlink_to_code_dir) 339 | 340 | $LOAD_PATH.delete(CODE_LOADING_DIR) 341 | $LOAD_PATH.unshift(@symlink_to_code_dir) 342 | end 343 | 344 | after :each do 345 | rm_r @symlink_to_code_dir 346 | end 347 | 348 | it "does not canonicalize the path and stores a path with symlinks" do 349 | next skip if defined?(JRUBY_VERSION) 350 | 351 | symlink_path = "#{@symlink_to_code_dir}/load_fixture.rb" 352 | canonical_path = "#{CODE_LOADING_DIR}/load_fixture.rb" 353 | @object.require(symlink_path).should be_true 354 | ScratchPad.recorded.should == [:loaded] 355 | 356 | features = $LOADED_FEATURES.select { |path| path.end_with?('load_fixture.rb') } 357 | features.should include(symlink_path) 358 | features.should_not include(canonical_path) 359 | end 360 | 361 | it "stores the same path that __FILE__ returns in the required file" do 362 | symlink_path = "#{@symlink_to_code_dir}/load_fixture_and__FILE__.rb" 363 | @object.require(symlink_path).should be_true 364 | loaded_feature = $LOADED_FEATURES.last 365 | ScratchPad.recorded.should == [loaded_feature] 366 | end 367 | 368 | it "requires only once when a new matching file added to path" do 369 | # WONTFIX: too much effort; maybe, later; calling #realpath for every candidate is too expensive 370 | next skip 371 | @object.require('load_fixture').should be_true 372 | ScratchPad.recorded.should == [:loaded] 373 | 374 | symlink_to_code_dir_two = tmp("codesymlinktwo") 375 | File.symlink("#{CODE_LOADING_DIR}/b", symlink_to_code_dir_two) 376 | begin 377 | $LOAD_PATH.unshift(symlink_to_code_dir_two) 378 | 379 | @object.require('load_fixture').should be_false 380 | ensure 381 | rm_r symlink_to_code_dir_two 382 | end 383 | end 384 | end 385 | 386 | describe "with symlinks in the required feature and $LOAD_PATH" do 387 | next skip if defined?(JRUBY_VERSION) 388 | 389 | before :each do 390 | @dir = tmp("realdir") 391 | mkdir_p @dir 392 | @file = "#{@dir}/realfile.rb" 393 | touch(@file) { |f| f.puts 'ScratchPad << __FILE__' } 394 | 395 | @symlink_to_dir = tmp("symdir").freeze 396 | File.symlink(@dir, @symlink_to_dir) 397 | @symlink_to_file = "#{@dir}/symfile.rb" 398 | File.symlink("realfile.rb", @symlink_to_file) 399 | end 400 | 401 | after :each do 402 | rm_r @dir, @symlink_to_dir 403 | end 404 | 405 | it "canonicalizes the entry in $LOAD_PATH but not the filename passed to #require" do 406 | next skip unless RUBY_VERSION >= "2.7.0" 407 | 408 | $LOAD_PATH.unshift(@symlink_to_dir) 409 | @object.require("symfile").should be_true 410 | loaded_feature = "#{@dir}/symfile.rb" 411 | ScratchPad.recorded.should == [loaded_feature] 412 | $".last.should == loaded_feature 413 | $LOAD_PATH[0].should == @symlink_to_dir 414 | end 415 | end 416 | end 417 | 418 | it "does not store the path if the load fails" do 419 | $LOAD_PATH << CODE_LOADING_DIR 420 | saved_loaded_features = $LOADED_FEATURES.dup 421 | -> { @object.require("raise_fixture.rb") }.should raise_error(RuntimeError) 422 | $LOADED_FEATURES.should == saved_loaded_features 423 | end 424 | 425 | it "does not load an absolute path that is already stored" do 426 | $LOADED_FEATURES << @path 427 | @object.require(@path).should be_false 428 | ScratchPad.recorded.should == [] 429 | end 430 | 431 | it "does not load a ./ relative path that is already stored" do 432 | $LOADED_FEATURES << "./load_fixture.rb" 433 | Dir.chdir CODE_LOADING_DIR do 434 | @object.require("./load_fixture.rb").should be_false 435 | end 436 | ScratchPad.recorded.should == [] 437 | end 438 | 439 | it "does not load a ../ relative path that is already stored" do 440 | $LOADED_FEATURES << "../load_fixture.rb" 441 | Dir.chdir CODE_LOADING_DIR do 442 | @object.require("../load_fixture.rb").should be_false 443 | end 444 | ScratchPad.recorded.should == [] 445 | end 446 | 447 | it "does not load a non-canonical path that is already stored" do 448 | $LOADED_FEATURES << "code/../code/load_fixture.rb" 449 | $LOAD_PATH << File.dirname(CODE_LOADING_DIR) 450 | @object.require("code/../code/load_fixture.rb").should be_false 451 | ScratchPad.recorded.should == [] 452 | end 453 | 454 | it "respects being replaced with a new array" do 455 | prev = $LOADED_FEATURES.dup 456 | 457 | @object.require(@path).should be_true 458 | $LOADED_FEATURES.should include(@path) 459 | 460 | $LOADED_FEATURES.replace(prev) 461 | 462 | $LOADED_FEATURES.should_not include(@path) 463 | @object.require(@path).should be_true 464 | $LOADED_FEATURES.should include(@path) 465 | end 466 | 467 | it "does not load twice the same file with and without extension" do 468 | $LOAD_PATH << CODE_LOADING_DIR 469 | @object.require("load_fixture.rb").should be_true 470 | @object.require("load_fixture").should be_false 471 | end 472 | 473 | describe "when a non-extensioned file is in $LOADED_FEATURES" do 474 | before :each do 475 | $LOADED_FEATURES << "load_fixture" 476 | end 477 | 478 | it "loads a .rb extensioned file when a non extensioned file is in $LOADED_FEATURES" do 479 | $LOAD_PATH << CODE_LOADING_DIR 480 | @object.require("load_fixture").should be_true 481 | ScratchPad.recorded.should == [:loaded] 482 | end 483 | 484 | it "loads a .rb extensioned file from a subdirectory" do 485 | $LOAD_PATH << File.dirname(CODE_LOADING_DIR) 486 | @object.require("code/load_fixture").should be_true 487 | ScratchPad.recorded.should == [:loaded] 488 | end 489 | 490 | it "returns false if the file is not found" do 491 | Dir.chdir File.dirname(CODE_LOADING_DIR) do 492 | @object.require("load_fixture").should be_false 493 | ScratchPad.recorded.should == [] 494 | end 495 | end 496 | 497 | it "returns false when passed a path and the file is not found" do 498 | $LOADED_FEATURES << "code/load_fixture" 499 | Dir.chdir CODE_LOADING_DIR do 500 | @object.require("code/load_fixture").should be_false 501 | ScratchPad.recorded.should == [] 502 | end 503 | end 504 | end 505 | 506 | it "stores ../ relative paths as absolute paths" do 507 | Dir.chdir CODE_LOADING_DIR do 508 | @object.require("../code/load_fixture.rb").should be_true 509 | end 510 | $LOADED_FEATURES.should include(@path) 511 | end 512 | 513 | it "stores ./ relative paths as absolute paths" do 514 | Dir.chdir CODE_LOADING_DIR do 515 | @object.require("./load_fixture.rb").should be_true 516 | end 517 | $LOADED_FEATURES.should include(@path) 518 | end 519 | 520 | it "collapses duplicate path separators" do 521 | $LOAD_PATH << "." 522 | sep = File::Separator + File::Separator 523 | path = ["..", "code", "load_fixture.rb"].join(sep) 524 | Dir.chdir CODE_LOADING_DIR do 525 | @object.require(path).should be_true 526 | end 527 | $LOADED_FEATURES.should include(@path) 528 | end 529 | 530 | it "expands absolute paths containing .." do 531 | path = File.join CODE_LOADING_DIR, "..", "code", "load_fixture.rb" 532 | @object.require(path).should be_true 533 | $LOADED_FEATURES.should include(@path) 534 | end 535 | 536 | it "adds the suffix of the resolved filename" do 537 | $LOAD_PATH << CODE_LOADING_DIR 538 | @object.require("load_fixture").should be_true 539 | $LOADED_FEATURES.should include(@path) 540 | end 541 | 542 | it "does not load a non-canonical path for a file already loaded" do 543 | $LOADED_FEATURES << @path 544 | $LOAD_PATH << File.dirname(CODE_LOADING_DIR) 545 | @object.require("code/../code/load_fixture.rb").should be_false 546 | ScratchPad.recorded.should == [] 547 | end 548 | 549 | it "does not load a ./ relative path for a file already loaded" do 550 | $LOADED_FEATURES << @path 551 | $LOAD_PATH << "an_irrelevant_dir" 552 | Dir.chdir CODE_LOADING_DIR do 553 | @object.require("./load_fixture.rb").should be_false 554 | end 555 | ScratchPad.recorded.should == [] 556 | end 557 | 558 | it "does not load a ../ relative path for a file already loaded" do 559 | $LOADED_FEATURES << @path 560 | $LOAD_PATH << "an_irrelevant_dir" 561 | Dir.chdir CODE_LOADING_DIR do 562 | @object.require("../code/load_fixture.rb").should be_false 563 | end 564 | ScratchPad.recorded.should == [] 565 | end 566 | 567 | it "unicode_normalize is part of core and not $LOADED_FEATURES" do 568 | next skip if defined?(JRUBY_VERSION) || !Process.respond_to?(:last_status) 569 | features = ruby_exe("puts $LOADED_FEATURES", options: '--disable-gems') 570 | features.lines.each { |feature| 571 | feature.should_not include("unicode_normalize") 572 | } 573 | 574 | -> { @object.require("unicode_normalize") }.should raise_error(LoadError) 575 | end 576 | 577 | it "does not load a file earlier on the $LOAD_PATH when other similar features were already loaded" do 578 | Dir.chdir CODE_LOADING_DIR do 579 | @object.send(@method, "../code/load_fixture").should be_true 580 | end 581 | ScratchPad.recorded.should == [:loaded] 582 | 583 | $LOAD_PATH.unshift "#{CODE_LOADING_DIR}/b" 584 | # This loads because the above load was not on the $LOAD_PATH 585 | @object.send(@method, "load_fixture").should be_true 586 | ScratchPad.recorded.should == [:loaded, :loaded] 587 | 588 | $LOAD_PATH.unshift "#{CODE_LOADING_DIR}/c" 589 | # This does not load because the above load was on the $LOAD_PATH 590 | @object.send(@method, "load_fixture").should be_false 591 | ScratchPad.recorded.should == [:loaded, :loaded] 592 | end 593 | end 594 | 595 | describe "(shell expansion)" do 596 | before :each do 597 | @path = File.expand_path("load_fixture.rb", CODE_LOADING_DIR) 598 | @env_home = ENV["HOME"] 599 | ENV["HOME"] = CODE_LOADING_DIR 600 | end 601 | 602 | after :each do 603 | ENV["HOME"] = @env_home 604 | end 605 | 606 | # "#3171" 607 | it "performs tilde expansion on a .rb file before storing paths in $LOADED_FEATURES" do 608 | @object.require("~/load_fixture.rb").should be_true 609 | $LOADED_FEATURES.should include(@path) 610 | end 611 | 612 | it "performs tilde expansion on a non-extensioned file before storing paths in $LOADED_FEATURES" do 613 | @object.require("~/load_fixture").should be_true 614 | $LOADED_FEATURES.should include(@path) 615 | end 616 | end 617 | 618 | describe "(concurrently)" do 619 | before :each do 620 | ScratchPad.record [] 621 | @path = File.expand_path "concurrent.rb", CODE_LOADING_DIR 622 | @path2 = File.expand_path "concurrent2.rb", CODE_LOADING_DIR 623 | @path3 = File.expand_path "concurrent3.rb", CODE_LOADING_DIR 624 | end 625 | 626 | after :each do 627 | ScratchPad.clear 628 | $LOADED_FEATURES.delete @path 629 | $LOADED_FEATURES.delete @path2 630 | $LOADED_FEATURES.delete @path3 631 | end 632 | 633 | # Quick note about these specs: 634 | # 635 | # The behavior we're spec'ing requires that t2 enter #require, see t1 is 636 | # loading @path, grab a lock, and wait on it. 637 | # 638 | # We do make sure that t2 starts the require once t1 is in the middle 639 | # of concurrent.rb, but we then need to get t2 to get far enough into #require 640 | # to see t1's lock and try to lock it. 641 | it "blocks a second thread from returning while the 1st is still requiring" do 642 | fin = false 643 | 644 | t1_res = nil 645 | t2_res = nil 646 | 647 | t2 = nil 648 | t1 = Thread.new do 649 | Thread.pass until t2 650 | Thread.current[:wait_for] = t2 651 | t1_res = @object.require(@path) 652 | Thread.pass until fin 653 | ScratchPad.recorded << :t1_post 654 | end 655 | 656 | t2 = Thread.new do 657 | Thread.pass until t1[:in_concurrent_rb] 658 | $VERBOSE, @verbose = nil, $VERBOSE 659 | begin 660 | t2_res = @object.require(@path) 661 | ScratchPad.recorded << :t2_post 662 | ensure 663 | $VERBOSE = @verbose 664 | fin = true 665 | end 666 | end 667 | 668 | t1.join 669 | t2.join 670 | 671 | t1_res.should be_true 672 | t2_res.should be_false 673 | 674 | ScratchPad.recorded.should == [:con_pre, :con_post, :t2_post, :t1_post] 675 | end 676 | 677 | it "blocks based on the path" do 678 | t1_res = nil 679 | t2_res = nil 680 | 681 | t2 = nil 682 | t1 = Thread.new do 683 | Thread.pass until t2 684 | Thread.current[:concurrent_require_thread] = t2 685 | t1_res = @object.require(@path2) 686 | end 687 | 688 | t2 = Thread.new do 689 | Thread.pass until t1[:in_concurrent_rb2] 690 | t2_res = @object.require(@path3) 691 | end 692 | 693 | t1.join 694 | t2.join 695 | 696 | t1_res.should be_true 697 | t2_res.should be_true 698 | 699 | ScratchPad.recorded.should == [:con2_pre, :con3, :con2_post] 700 | end 701 | 702 | it "allows a 2nd require if the 1st raised an exception" do 703 | fin = false 704 | 705 | t2_res = nil 706 | 707 | t2 = nil 708 | t1 = Thread.new do 709 | Thread.pass until t2 710 | Thread.current[:wait_for] = t2 711 | Thread.current[:con_raise] = true 712 | 713 | -> { 714 | @object.require(@path) 715 | }.should raise_error(RuntimeError) 716 | 717 | Thread.pass until fin 718 | ScratchPad.recorded << :t1_post 719 | end 720 | 721 | t2 = Thread.new do 722 | Thread.pass until t1[:in_concurrent_rb] 723 | $VERBOSE, @verbose = nil, $VERBOSE 724 | begin 725 | t2_res = @object.require(@path) 726 | ScratchPad.recorded << :t2_post 727 | ensure 728 | $VERBOSE = @verbose 729 | fin = true 730 | end 731 | end 732 | 733 | t1.join 734 | t2.join 735 | 736 | t2_res.should be_true 737 | 738 | ScratchPad.recorded.should == [:con_pre, :con_pre, :con_post, :t2_post, :t1_post] 739 | end 740 | 741 | # "redmine #5754" 742 | it "blocks a 3rd require if the 1st raises an exception and the 2nd is still running" do 743 | fin = false 744 | 745 | t1_res = nil 746 | t2_res = nil 747 | 748 | raised = false 749 | 750 | t2 = nil 751 | t1 = Thread.new do 752 | Thread.current[:con_raise] = true 753 | 754 | -> { 755 | @object.require(@path) 756 | }.should raise_error(RuntimeError) 757 | 758 | raised = true 759 | 760 | # This hits the bug. Because MRI removes its internal lock from a table 761 | # when the exception is raised, this #require doesn't see that t2 is in 762 | # the middle of requiring the file, so this #require runs when it should not. 763 | Thread.pass until t2 && t2[:in_concurrent_rb] 764 | t1_res = @object.require(@path) 765 | 766 | Thread.pass until fin 767 | ScratchPad.recorded << :t1_post 768 | end 769 | 770 | t2 = Thread.new do 771 | Thread.pass until raised 772 | Thread.current[:wait_for] = t1 773 | begin 774 | t2_res = @object.require(@path) 775 | ScratchPad.recorded << :t2_post 776 | ensure 777 | fin = true 778 | end 779 | end 780 | 781 | t1.join 782 | t2.join 783 | 784 | t1_res.should be_false 785 | t2_res.should be_true 786 | 787 | ScratchPad.recorded.should == [:con_pre, :con_pre, :con_post, :t2_post, :t1_post] 788 | end 789 | end 790 | 791 | it "stores the missing path in a LoadError object" do 792 | path = "abcd1234" 793 | 794 | -> { 795 | @object.send(@method, path) 796 | }.should raise_error(LoadError) { |e| 797 | e.path.should == path 798 | } 799 | end 800 | end 801 | -------------------------------------------------------------------------------- /spec/core/kernel/shared/then.rb: -------------------------------------------------------------------------------- 1 | # source: https://github.com/ruby/spec/blob/master/core/kernel/shared/then.rb 2 | 3 | # NOTE: `send(@method)` was changed to the direct `then` call to make it work in JRuby. 4 | # See https://github.com/jruby/jruby/issues/5945 5 | describe :kernel_then, shared: true do 6 | it "yields self" do 7 | object = Object.new 8 | object.then { |o| o.should equal object } 9 | end 10 | 11 | it "returns the block return value" do 12 | object = Object.new 13 | object.then { 42 }.should equal 42 14 | end 15 | 16 | it "returns a sized Enumerator when no block given" do 17 | object = Object.new 18 | enum = object.then 19 | enum.should be_an_instance_of Enumerator 20 | enum.size.should equal 1 21 | enum.peek.should equal object 22 | enum.first.should equal object 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/fixtures/code/a/load_fixture.bundle: -------------------------------------------------------------------------------- 1 | ScratchPad << :ext_bundle 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/a/load_fixture.dll: -------------------------------------------------------------------------------- 1 | ScratchPad << :ext_dll 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/a/load_fixture.dylib: -------------------------------------------------------------------------------- 1 | ScratchPad << :ext_dylib 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/a/load_fixture.so: -------------------------------------------------------------------------------- 1 | ScratchPad << :ext_so 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/b/load_fixture.rb: -------------------------------------------------------------------------------- 1 | ScratchPad << :loaded 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/concurrent.rb: -------------------------------------------------------------------------------- 1 | ScratchPad.recorded << :con_pre 2 | Thread.current[:in_concurrent_rb] = true 3 | 4 | if t = Thread.current[:wait_for] 5 | Thread.pass until t.backtrace && t.backtrace.any? { |call| call.include? 'require' } && t.stop? 6 | end 7 | 8 | if Thread.current[:con_raise] 9 | raise "con1" 10 | end 11 | 12 | ScratchPad.recorded << :con_post 13 | -------------------------------------------------------------------------------- /spec/fixtures/code/concurrent2.rb: -------------------------------------------------------------------------------- 1 | ScratchPad.recorded << :con2_pre 2 | 3 | Thread.current[:in_concurrent_rb2] = true 4 | 5 | t = Thread.current[:concurrent_require_thread] 6 | Thread.pass until t[:in_concurrent_rb3] 7 | 8 | ScratchPad.recorded << :con2_post 9 | -------------------------------------------------------------------------------- /spec/fixtures/code/concurrent3.rb: -------------------------------------------------------------------------------- 1 | ScratchPad.recorded << :con3 2 | Thread.current[:in_concurrent_rb3] = true 3 | -------------------------------------------------------------------------------- /spec/fixtures/code/concurrent_require_fixture.rb: -------------------------------------------------------------------------------- 1 | object = ScratchPad.recorded 2 | thread = Thread.new { object.require(__FILE__) } 3 | Thread.pass until thread.stop? 4 | ScratchPad.record(thread) 5 | -------------------------------------------------------------------------------- /spec/fixtures/code/file_fixture.rb: -------------------------------------------------------------------------------- 1 | ScratchPad << __FILE__ 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/gem/load_fixture.rb: -------------------------------------------------------------------------------- 1 | ScratchPad << :loaded_gem 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/line_fixture.rb: -------------------------------------------------------------------------------- 1 | ScratchPad << __LINE__ 2 | 3 | # line 3 4 | 5 | ScratchPad << __LINE__ 6 | -------------------------------------------------------------------------------- /spec/fixtures/code/load_ext_fixture.rb: -------------------------------------------------------------------------------- 1 | ScratchPad << :loaded 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/load_fixture: -------------------------------------------------------------------------------- 1 | ScratchPad << :no_ext 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/load_fixture.bundle: -------------------------------------------------------------------------------- 1 | ScratchPad << :ext_bundle 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/load_fixture.dll: -------------------------------------------------------------------------------- 1 | ScratchPad << :ext_dll 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/load_fixture.dylib: -------------------------------------------------------------------------------- 1 | ScratchPad << :ext_dylib 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/load_fixture.ext: -------------------------------------------------------------------------------- 1 | ScratchPad << :no_rb_ext 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/load_fixture.ext.bundle: -------------------------------------------------------------------------------- 1 | ScratchPad << :ext_bundle 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/load_fixture.ext.dll: -------------------------------------------------------------------------------- 1 | ScratchPad << :ext_dll 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/load_fixture.ext.dylib: -------------------------------------------------------------------------------- 1 | ScratchPad << :ext_dylib 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/load_fixture.ext.rb: -------------------------------------------------------------------------------- 1 | ScratchPad << :loaded 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/load_fixture.ext.so: -------------------------------------------------------------------------------- 1 | ScratchPad << :ext_so 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/load_fixture.rb: -------------------------------------------------------------------------------- 1 | ScratchPad << :loaded 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/load_fixture.so: -------------------------------------------------------------------------------- 1 | ScratchPad << :ext_so 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/load_fixture_and__FILE__.rb: -------------------------------------------------------------------------------- 1 | ScratchPad << __FILE__ 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/load_wrap_fixture.rb: -------------------------------------------------------------------------------- 1 | class LoadSpecWrap 2 | ScratchPad << String 3 | end 4 | 5 | LOAD_WRAP_SPECS_TOP_LEVEL_CONSTANT = 1 6 | 7 | def load_wrap_specs_top_level_method 8 | :load_wrap_specs_top_level_method 9 | end 10 | ScratchPad << method(:load_wrap_specs_top_level_method).owner 11 | 12 | ScratchPad << self 13 | -------------------------------------------------------------------------------- /spec/fixtures/code/load_wrap_method_fixture.rb: -------------------------------------------------------------------------------- 1 | def top_level_method 2 | ::ScratchPad << :load_wrap_loaded 3 | end 4 | 5 | begin 6 | top_level_method 7 | rescue NameError 8 | ::ScratchPad << :load_wrap_error 9 | end 10 | -------------------------------------------------------------------------------- /spec/fixtures/code/methods_fixture.rb: -------------------------------------------------------------------------------- 1 | def foo1 2 | end 3 | 4 | def foo2 5 | end 6 | 7 | def foo3 8 | end 9 | 10 | def foo4 11 | end 12 | 13 | def foo5 14 | end 15 | 16 | def foo6 17 | end 18 | 19 | def foo7 20 | end 21 | 22 | def foo8 23 | end 24 | 25 | def foo9 26 | end 27 | 28 | def foo10 29 | end 30 | 31 | def foo11 32 | end 33 | 34 | def foo12 35 | end 36 | 37 | def foo13 38 | end 39 | 40 | def foo14 41 | end 42 | 43 | def foo15 44 | end 45 | 46 | def foo16 47 | end 48 | 49 | def foo17 50 | end 51 | 52 | def foo18 53 | end 54 | 55 | def foo19 56 | end 57 | 58 | def foo20 59 | end 60 | 61 | def foo21 62 | end 63 | 64 | def foo22 65 | end 66 | 67 | def foo23 68 | end 69 | 70 | def foo24 71 | end 72 | 73 | def foo25 74 | end 75 | 76 | def foo26 77 | end 78 | 79 | def foo27 80 | end 81 | 82 | def foo28 83 | end 84 | 85 | def foo29 86 | end 87 | 88 | def foo30 89 | end 90 | 91 | def foo31 92 | end 93 | 94 | def foo32 95 | end 96 | 97 | def foo33 98 | end 99 | 100 | def foo34 101 | end 102 | 103 | def foo35 104 | end 105 | 106 | def foo36 107 | end 108 | 109 | def foo37 110 | end 111 | 112 | def foo38 113 | end 114 | 115 | def foo39 116 | end 117 | 118 | def foo40 119 | end 120 | 121 | def foo41 122 | end 123 | 124 | def foo42 125 | end 126 | 127 | def foo43 128 | end 129 | 130 | def foo44 131 | end 132 | 133 | def foo45 134 | end 135 | 136 | def foo46 137 | end 138 | 139 | def foo47 140 | end 141 | 142 | def foo48 143 | end 144 | 145 | def foo49 146 | end 147 | 148 | def foo50 149 | end 150 | 151 | def foo51 152 | end 153 | 154 | def foo52 155 | end 156 | 157 | def foo53 158 | end 159 | 160 | def foo54 161 | end 162 | 163 | def foo55 164 | end 165 | 166 | def foo56 167 | end 168 | 169 | def foo57 170 | end 171 | 172 | def foo58 173 | end 174 | 175 | def foo59 176 | end 177 | 178 | def foo60 179 | end 180 | 181 | def foo61 182 | end 183 | 184 | def foo62 185 | end 186 | 187 | def foo63 188 | end 189 | 190 | def foo64 191 | end 192 | 193 | def foo65 194 | end 195 | 196 | def foo66 197 | end 198 | 199 | def foo67 200 | end 201 | 202 | def foo68 203 | end 204 | 205 | def foo69 206 | end 207 | 208 | def foo70 209 | end 210 | 211 | def foo71 212 | end 213 | 214 | def foo72 215 | end 216 | 217 | def foo73 218 | end 219 | 220 | def foo74 221 | end 222 | 223 | def foo75 224 | end 225 | 226 | def foo76 227 | end 228 | 229 | def foo77 230 | end 231 | 232 | def foo78 233 | end 234 | 235 | def foo79 236 | end 237 | 238 | def foo80 239 | end 240 | 241 | def foo81 242 | end 243 | 244 | def foo82 245 | end 246 | 247 | def foo83 248 | end 249 | 250 | def foo84 251 | end 252 | 253 | def foo85 254 | end 255 | 256 | def foo86 257 | end 258 | 259 | def foo87 260 | end 261 | 262 | def foo88 263 | end 264 | 265 | def foo89 266 | end 267 | 268 | def foo90 269 | end 270 | 271 | def foo91 272 | end 273 | 274 | def foo92 275 | end 276 | 277 | def foo93 278 | end 279 | 280 | def foo94 281 | end 282 | 283 | def foo95 284 | end 285 | 286 | def foo96 287 | end 288 | 289 | def foo97 290 | end 291 | 292 | def foo98 293 | end 294 | 295 | def foo99 296 | end 297 | 298 | def foo100 299 | end 300 | 301 | def foo101 302 | end 303 | 304 | def foo102 305 | end 306 | 307 | def foo103 308 | end 309 | 310 | def foo104 311 | end 312 | 313 | def foo105 314 | end 315 | 316 | def foo106 317 | end 318 | 319 | def foo107 320 | end 321 | 322 | def foo108 323 | end 324 | 325 | def foo109 326 | end 327 | 328 | def foo110 329 | end 330 | 331 | def foo111 332 | end 333 | 334 | def foo112 335 | end 336 | 337 | def foo113 338 | end 339 | 340 | def foo114 341 | end 342 | 343 | def foo115 344 | end 345 | 346 | def foo116 347 | end 348 | 349 | def foo117 350 | end 351 | 352 | def foo118 353 | end 354 | 355 | def foo119 356 | end 357 | 358 | def foo120 359 | end 360 | 361 | def foo121 362 | end 363 | 364 | ScratchPad << :loaded 365 | -------------------------------------------------------------------------------- /spec/fixtures/code/raise_fixture.rb: -------------------------------------------------------------------------------- 1 | raise "Exception loading a file" 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/recursive_load_fixture.rb: -------------------------------------------------------------------------------- 1 | ScratchPad << :loaded 2 | 3 | if ScratchPad.recorded == [:loaded] 4 | load File.expand_path("../recursive_load_fixture.rb", __FILE__) 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/code/recursive_require_fixture.rb: -------------------------------------------------------------------------------- 1 | require_relative 'recursive_require_fixture' 2 | 3 | ScratchPad << :loaded 4 | -------------------------------------------------------------------------------- /spec/fixtures/code/symlink/symlink1.rb: -------------------------------------------------------------------------------- 1 | require_relative 'symlink2/symlink2' 2 | -------------------------------------------------------------------------------- /spec/fixtures/code/symlink/symlink2/symlink2.rb: -------------------------------------------------------------------------------- 1 | ScratchPad << :loaded 2 | -------------------------------------------------------------------------------- /spec/fixtures/code_loading.rb: -------------------------------------------------------------------------------- 1 | module CodeLoadingSpecs 2 | # The #require instance method is private, so this class enables 3 | # calling #require like obj.require(file). This is used to share 4 | # specs between Kernel#require and Kernel.require. 5 | class Method 6 | def require(name) 7 | super name 8 | end 9 | 10 | def load(name, wrap=false) 11 | super 12 | end 13 | end 14 | 15 | def self.preload_rubygems 16 | # Require RubyGems eagerly, to ensure #require is already the RubyGems 17 | # version and RubyGems is only loaded once, before starting #require/#autoload specs 18 | # which snapshot $LOADED_FEATURES and could cause RubyGems to load twice. 19 | # #require specs also snapshot #require, and could end up redefining #require as the original core Kernel#require. 20 | @rubygems ||= begin 21 | require "rubygems" 22 | true 23 | rescue LoadError 24 | true 25 | end 26 | end 27 | 28 | def self.spec_setup 29 | preload_rubygems 30 | 31 | @saved_loaded_features = $LOADED_FEATURES.clone 32 | @saved_load_path = $LOAD_PATH.clone 33 | ScratchPad.record [] 34 | end 35 | 36 | def self.spec_cleanup 37 | $LOADED_FEATURES.replace @saved_loaded_features 38 | $LOAD_PATH.replace @saved_load_path 39 | ScratchPad.clear 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/require-hooks/around_load_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../spec_helper" 4 | 5 | $around_hooks_enabled = false 6 | $events = [] 7 | 8 | # SyntaxSuggest example 9 | RequireHooks.around_load(patterns: [File.join(__dir__, "fixtures/*.rb")], exclude_patterns: ["*/hi_jack.rb"]) do |path, &block| 10 | next block.call unless $around_hooks_enabled 11 | 12 | $events << [:before, File.basename(path)] 13 | 14 | block.call 15 | end 16 | 17 | RequireHooks.around_load(patterns: [File.join(__dir__, "fixtures/*.rb")], exclude_patterns: ["*/hi_jack.rb"]) do |path, &block| 18 | next block.call unless $around_hooks_enabled 19 | 20 | begin 21 | block.call 22 | rescue SyntaxError => e 23 | raise "My custom syntax error: #{e.message}" 24 | end 25 | end 26 | 27 | # rubocop:disable Lint/Void 28 | describe "require-hooks around_load" do 29 | before do 30 | $around_hooks_enabled = true 31 | end 32 | 33 | after do 34 | $around_hooks_enabled = false 35 | $events.clear 36 | end 37 | 38 | it "invoked before and after load" do 39 | load File.join(__dir__, "fixtures/freeze.rb") 40 | 41 | Freezy.weather.should == "cold" 42 | 43 | $events.should == [[:before, "freeze.rb"]] 44 | end 45 | 46 | it "is not invoked when no matching files required" do 47 | $source_transform_enabled = false 48 | load File.join(__dir__, "fixtures/hi_jack.rb") 49 | 50 | $events.should == [] 51 | end 52 | 53 | it "can catch errors" do 54 | -> { load File.join(__dir__, "fixtures/syntax_error.rb") } 55 | .should raise_error(RuntimeError, /My custom syntax error/) 56 | end 57 | end 58 | # rubocop:enable Lint/Void 59 | -------------------------------------------------------------------------------- /spec/require-hooks/bootsnap_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "fileutils" 4 | require_relative "../support/command_testing" 5 | 6 | describe "require-hooks: bootsnap mode" do 7 | next skip if defined?(JRUBY_VERSION) || defined?(TruffleRuby) 8 | # Bootsnap requires Ruby 2.3+ 9 | next skip unless RUBY_VERSION >= "2.3.0" 10 | 11 | before do 12 | cache_path = File.join(__dir__, "fixtures", "tmp") 13 | if File.directory?(cache_path) 14 | FileUtils.rm_rf(cache_path) 15 | end 16 | end 17 | 18 | it "performs transformations within Bootsnap (thus caching the results)" do 19 | cache_path = File.join(__dir__, "fixtures", "bootsnap", "tmp") 20 | if File.directory?(cache_path) 21 | FileUtils.rm_rf(cache_path) 22 | end 23 | 24 | run_ruby( 25 | File.join(__dir__, "fixtures", "bootsnap.rb").to_s 26 | ) do |_status, output, _err| 27 | output.should include("Good-bye (false)\n") 28 | output.should include("Good-bye (true)\n") 29 | 30 | unless ENV["REQUIRE_HOOKS_MODE"] == "patch" 31 | output.should include("miss: hello.rb\n") 32 | misses = output.scan(/miss: (.*)$/).flatten 33 | misses.size.should == 1 34 | end 35 | end 36 | end 37 | 38 | it "re-raises syntax errors" do 39 | cache_path = File.join(__dir__, "fixtures", "bootsnap", "tmp") 40 | if File.directory?(cache_path) 41 | FileUtils.rm_rf(cache_path) 42 | end 43 | 44 | run_ruby( 45 | File.join(__dir__, "fixtures", "bootsnap-syntax-error.rb").to_s, 46 | should_fail: true 47 | ) do |_status, _output, err| 48 | err.should include("SyntaxError") 49 | err.should include("bootsnap-syntax-error.rb:1") 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/require-hooks/fixtures/bootsnap-syntax-error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bootsnap" 4 | Bootsnap.setup( 5 | cache_dir: File.join(__dir__, "tmp/cache"), 6 | development_mode: true, 7 | load_path_cache: true, 8 | compile_cache_iseq: true, 9 | compile_cache_yaml: true 10 | ) 11 | 12 | require "require-hooks/setup" 13 | 14 | RequireHooks.source_transform do |path, source| 15 | raise SyntaxError, "bla-bla" 16 | end 17 | 18 | load File.join(__dir__, "syntax_error.rb") 19 | -------------------------------------------------------------------------------- /spec/require-hooks/fixtures/bootsnap.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bootsnap" 4 | Bootsnap.setup( 5 | cache_dir: File.join(__dir__, "tmp/cache"), 6 | development_mode: true, 7 | load_path_cache: true, 8 | compile_cache_iseq: true, 9 | compile_cache_yaml: true 10 | ) 11 | 12 | require "require-hooks/setup" 13 | 14 | Bootsnap.instrumentation = ->(event, path) { 15 | puts "#{event}: #{File.basename(path)}" 16 | } 17 | 18 | RequireHooks.source_transform do |path, source| 19 | next unless path =~ /fixtures\/hello\.rb$/ 20 | 21 | source ||= File.read(path) 22 | source.gsub!("Hello", "Good-bye") 23 | source 24 | end 25 | 26 | load File.join(__dir__, "hello.rb") 27 | 28 | RequireHooks.around_load do |path, &block| 29 | next unless path =~ /fixtures\/hello\.rb$/ 30 | 31 | was_frozen_string_literal = RubyVM::InstructionSequence.compile_option[:frozen_string_literal] 32 | begin 33 | RubyVM::InstructionSequence.compile_option = {frozen_string_literal: true} 34 | block.call 35 | ensure 36 | RubyVM::InstructionSequence.compile_option = {frozen_string_literal: was_frozen_string_literal} 37 | end 38 | end 39 | 40 | load File.join(__dir__, "hello.rb") 41 | -------------------------------------------------------------------------------- /spec/require-hooks/fixtures/freeze.rb: -------------------------------------------------------------------------------- 1 | class Freezy 2 | class << self 3 | def weather 4 | "cold" 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/require-hooks/fixtures/hello.rb: -------------------------------------------------------------------------------- 1 | # Ruby 3.4 introduced chilled strings that 2 | # pretend to be frozen (i.e., responds with true to #frozen?). 3 | # That will probably change in the final release though: 4 | # https://bugs.ruby-lang.org/issues/20205#note-45 5 | def frozen?(str) 6 | str.sub!(/$/, "x") 7 | str.sub!(/x$/, "") 8 | false 9 | rescue FrozenError 10 | true 11 | end 12 | 13 | str = "Hello" 14 | puts str + " (#{frozen?(str)})" 15 | -------------------------------------------------------------------------------- /spec/require-hooks/fixtures/hi_jack.rb: -------------------------------------------------------------------------------- 1 | module HiJack 2 | def self.say 3 | "yo" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/require-hooks/fixtures/syntax_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | def x in y = 0 4 | -------------------------------------------------------------------------------- /spec/require-hooks/hijack_load_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../spec_helper" 4 | 5 | $hijack_load_enabled = false 6 | $source_transform_enabled = false 7 | 8 | RequireHooks.source_transform(patterns: [File.join(__dir__, "fixtures/*.rb")]) do |path, source| 9 | next unless $hijack_load_enabled 10 | next unless $source_transform_enabled 11 | 12 | source ||= File.read(path) 13 | 14 | source.gsub("cold", "hot") 15 | end 16 | 17 | RequireHooks.hijack_load(patterns: [File.join(__dir__, "fixtures/freeze.rb")]) do |path, source| 18 | next unless $hijack_load_enabled 19 | 20 | iseq = 21 | if source 22 | RubyVM::InstructionSequence.compile(source, path, path, 1, {frozen_string_literal: true}) 23 | else 24 | RubyVM::InstructionSequence.compile_file(path, {frozen_string_literal: true}) 25 | end 26 | 27 | iseq 28 | end 29 | 30 | RequireHooks.hijack_load do |path, source| 31 | next unless $hijack_load_enabled 32 | 33 | RubyVM::InstructionSequence.compile_file(path) 34 | end 35 | 36 | # rubocop:disable Lint/Void 37 | describe "require-hooks hijack_load" do 38 | # TODO: add support for other Rubies 39 | next skip unless defined?(RubyVM::InstructionSequence) 40 | 41 | before do 42 | $source_transform_enabled = true 43 | $hijack_load_enabled = true 44 | end 45 | 46 | after do 47 | $source_transform_enabled = false 48 | $hijack_load_enabled = false 49 | end 50 | 51 | it "loads bytecode with the first hijack" do 52 | load File.join(__dir__, "fixtures/freeze.rb") 53 | 54 | Freezy.weather.should == "hot" 55 | end 56 | 57 | it "fallbacks to the next hijack if the first one skipped" do 58 | load File.join(__dir__, "fixtures/hi_jack.rb") 59 | 60 | HiJack.say.should == "yo" 61 | HiJack.say.sub!("yo", "hi").should == "hi" 62 | end 63 | 64 | it "loads original source code if no hijacks were invoked" do 65 | $source_transform_enabled = false 66 | $hijack_load_enabled = false 67 | 68 | load File.join(__dir__, "fixtures/freeze.rb") 69 | 70 | Freezy.weather.should == "cold" 71 | 72 | load File.join(__dir__, "fixtures/hi_jack.rb") 73 | 74 | HiJack.say.should == "yo" 75 | end 76 | 77 | it "reads source code if no transformations" do 78 | $source_transform_enabled = false 79 | load File.join(__dir__, "fixtures/freeze.rb") 80 | 81 | Freezy.weather.should == "cold" 82 | 83 | -> { Freezy.weather.sub!("c", "h") }.should raise_error(FrozenError) 84 | end 85 | end 86 | # rubocop:enable Lint/Void 87 | -------------------------------------------------------------------------------- /spec/require-hooks/source_transform_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../spec_helper" 4 | 5 | $source_transform_enabled = false 6 | 7 | RequireHooks.source_transform do |path, source| 8 | next unless $source_transform_enabled 9 | 10 | source ||= File.read(path) 11 | source.gsub("cold", "hot") 12 | end 13 | 14 | RequireHooks.source_transform do |path, source| 15 | next unless $source_transform_enabled 16 | next unless path =~ /fixtures\/freeze\.rb$/ 17 | 18 | source ||= File.read(path) 19 | 20 | "# frozen_string_literal: true\n#{source}" 21 | end 22 | 23 | describe "require-hooks source_transform" do 24 | before do 25 | $source_transform_enabled = true 26 | end 27 | 28 | after do 29 | $source_transform_enabled = false 30 | end 31 | 32 | it "loads transformed source code" do 33 | load File.join(__dir__, "fixtures/freeze.rb") 34 | 35 | Freezy.weather.should == "hot" 36 | end 37 | 38 | it "loads original source code if transformers return nil" do 39 | $source_transform_enabled = false 40 | load File.join(__dir__, "fixtures/freeze.rb") 41 | 42 | Freezy.weather.should == "cold" 43 | end 44 | 45 | it "invoke all transformers" do 46 | load File.join(__dir__, "fixtures/freeze.rb") 47 | 48 | -> { Freezy.weather.sub!("c", "h") }.should raise_error(FrozenError) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # suppress "warning: is experimental, and the behavior may change in future versions of Ruby!" 4 | $VERBOSE = nil 5 | 6 | # override ruby_version_is method to always run tests 7 | def ruby_version_is(*) 8 | yield 9 | end 10 | 11 | # Backports for older mspec 12 | unless defined?(MSpecEnv) 13 | def suppress_warning 14 | yield 15 | end 16 | 17 | unless defined?(SkippedSpecError) 18 | def skip(_ = nil) 19 | 1.should == 1 20 | end 21 | end 22 | end 23 | 24 | root = File.dirname(__FILE__) 25 | dir = "fixtures/code" 26 | CODE_LOADING_DIR = File.realpath(dir, root) 27 | -------------------------------------------------------------------------------- /spec/support/command_testing.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "open3" 4 | 5 | module Kernel 6 | RUBY_RUNNER = if defined?(JRUBY_VERSION) 7 | # See https://github.com/jruby/jruby/wiki/Improving-startup-time#bundle-exec 8 | "jruby -G" 9 | else 10 | "bundle exec ruby" 11 | end 12 | 13 | def run_command(command, chdir: nil, should_fail: false, env: {}, input: nil) 14 | output, err, status = 15 | Open3.capture3( 16 | env, 17 | command, 18 | chdir: chdir || File.expand_path("../..", __dir__), 19 | stdin_data: input&.join("\n") 20 | ) 21 | 22 | if ENV["COMMAND_DEBUG"] || (!status.success? && !should_fail) 23 | puts "\n\nCOMMAND:\n#{command}\n\nOUTPUT:\n#{output}\nERROR:\n#{err}\n" 24 | end 25 | 26 | status.success?.should == true unless should_fail 27 | 28 | yield status, output, err if block_given? 29 | end 30 | 31 | def run_ruby(command, **options, &block) 32 | run_command("#{RUBY_RUNNER} -rbundler/setup -I#{File.expand_path(File.join(__dir__, "../../lib"))} #{command}", **options, &block) 33 | end 34 | end 35 | --------------------------------------------------------------------------------