├── .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 | [](https://rubygems.org/gems/require-hooks)
2 | [](https://github.com/palkan/require-hooks/actions)
3 | [](https://github.com/ruby-next/require-hooks/actions)
4 | [](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 |
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 |
--------------------------------------------------------------------------------