Repository: tycooon/memery Branch: master Commit: 63b2fec75a3c Files: 15 Total size: 27.6 KB Directory structure: gitextract_hubzy71a/ ├── .editorconfig ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── benchmark.rb ├── lib/ │ ├── memery/ │ │ └── version.rb │ └── memery.rb ├── memery.gemspec └── spec/ ├── memery_spec.rb └── spec_helper.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest # We want to run on external PRs, but not on our own internal PRs as they'll be run on push event if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'tycooon/memery' strategy: fail-fast: false matrix: ruby: ["3.2", "3.3", "3.4"] name: ${{ matrix.ruby }} steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - run: bundle exec rake - uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ /.bundle/ /.yardoc /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ .ruby-version # rspec failure tracking .rspec_status ================================================ FILE: .rspec ================================================ --format documentation --color --require spec_helper --warnings ================================================ FILE: .rubocop.yml ================================================ inherit_gem: rubocop-config-umbrellio: lib/rubocop.yml AllCops: DisplayCopNames: true TargetRubyVersion: 3.2 Naming/MethodParameterName: AllowedNames: ["x", "y", "z"] RSpec/EmptyLineAfterHook: Enabled: false ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source "https://rubygems.org" gemspec gem "activesupport" gem "benchmark-ips" gem "benchmark-memory" gem "bundler" gem "pry" gem "rake" gem "rspec" gem "rubocop-config-umbrellio" gem "simplecov" gem "simplecov-lcov" ================================================ FILE: LICENSE.txt ================================================ The MIT License (MIT) Copyright (c) 2017 Yuri Smirnov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Memery   [![Gem Version](https://badge.fury.io/rb/memery.svg)](https://badge.fury.io/rb/memery) ![Build Status](https://github.com/tycooon/memery/actions/workflows/ci.yml/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/tycooon/memery/badge.svg?branch=master)](https://coveralls.io/github/tycooon/memery?branch=master) Memery is a Ruby gem that simplifies memoization of method return values. In Ruby, memoization typically looks like this: ```ruby def user @user ||= User.find(some_id) end ``` However, this approach fails if the calculated result can be `nil` or `false`, or if the method uses arguments. Additionally, multi-line methods require extra `begin`/`end` blocks: ```ruby def user @user ||= begin some_id = calculate_id klass = calculate_klass klass.find(some_id) end end ``` To handle these situations, memoization gems like Memery exist. The example above can be rewritten using Memery as follows: ```ruby memoize def user some_id = calculate_id klass = calculate_klass klass.find(some_id) end ``` ## Installation Add `gem "memery"` to your Gemfile. ## Usage ```ruby class A include Memery memoize def call puts "calculating" 42 end # Alternatively: # def call # ... # end # memoize :call end a = A.new a.call # => 42 a.call # => 42 a.call # => 42 # "calculating" will only be printed once. a.call { 1 } # => 42 # "calculating" will be printed again because passing a block disables memoization. ``` Memoization works with methods that take arguments. The memoization is based on these arguments using an internal hash, so the following will work as expected: ```ruby class A include Memery memoize def call(arg1, arg2) puts "calculating" arg1 + arg2 end end a = A.new a.call(1, 5) # => 6 a.call(2, 15) # => 17 a.call(1, 5) # => 6 # "calculating" will be printed twice, once for each unique argument list. ``` For class methods: ```ruby class B class << self include Memery memoize def call puts "calculating" 42 end end end B.call # => 42 B.call # => 42 B.call # => 42 # "calculating" will only be printed once. ``` ### Conditional Memoization ```ruby class A include Memery attr_accessor :environment def call puts "calculating" 42 end memoize :call, condition: -> { environment == 'production' } end a = A.new a.environment = 'development' a.call # => 42 # calculating a.call # => 42 # calculating a.call # => 42 # calculating # Text will be printed every time because result of condition block is `false`. a.environment = 'production' a.call # => 42 # calculating a.call # => 42 a.call # => 42 # Text will be printed only once because there is memoization # with `true` result of condition block. ``` ### Memoization with Time-to-Live (TTL) ```ruby class A include Memery def call puts "calculating" 42 end memoize :call, ttl: 3 # seconds end a = A.new a.call # => 42 # calculating a.call # => 42 a.call # => 42 # Text will be printed again only after 3 seconds of time-to-live. # 3 seconds later... a.call # => 42 # calculating a.call # => 42 a.call # => 42 # another 3 seconds later... a.call # => 42 # calculating a.call # => 42 a.call # => 42 ``` ### Checking if a Method is Memoized ```ruby class A include Memery memoize def call puts "calculating" 42 end def execute puts "non-memoized" end end a = A.new a.memoized?(:call) # => true a.memoized?(:execute) # => false ``` ### Marshal-compatible Memoization In order for objects to be marshaled and loaded in a different Ruby process, hashed arguments must be disabled in order for memoized values to be retained. Note that this can have a performance impact if the memoized method contains arguments. ```ruby Memery.use_hashed_arguments = false class A include Memery memoize def call puts "calculating" 42 end end a = A.new a.call Marshal.dump(a) # => "\x04\bo:\x06A\x06:\x1D@_memery_memoized_values{\x06:\tcallS:3Memery::ClassMethods::MemoizationModule::Cache\a:\vresulti/:\ttimef\x14663237.14822323" # ...in another Ruby process: a = Marshal.load("\x04\bo:\x06A\x06:\x1D@_memery_memoized_values{\x06:\tcallS:3Memery::ClassMethods::MemoizationModule::Cache\a:\vresulti/:\ttimef\x14663237.14822323") a.call # => 42 ``` ## Differences from Other Gems Memery is similar to [Memoist](https://github.com/matthewrudy/memoist), but it doesn't override methods. Instead, it uses Ruby 2's `Module.prepend` feature. This approach is cleaner, allowing you to inspect the original method body with `method(:x).super_method.source`, and it ensures that subclasses' methods function properly. If you redefine a memoized method in a subclass, it won't be memoized by default. You can memoize it normally without needing an awkward `identifier: ` argument, and it will just work: ```ruby class A include Memery memoize def x(param) param end end class B < A memoize def x(param) super(2) * param end end b = B.new b.x(1) # => 2 b.x(2) # => 4 b.x(3) # => 6 b.instance_variable_get(:@_memery_memoized_values) # => {:x_70318201388120=>{[1]=>2, [2]=>4, [3]=>6}, :x_70318184636620=>{[2]=>2}} ``` Note how both methods' return values are cached separately without interfering with each other. Another key difference is that Memery doesn't change the method's signature (no extra `reload` parameter). If you need an unmemoized result, simply create an unmemoized version of the method: ```ruby memoize def users get_users end def get_users # ... end ``` Alternatively, you can clear the entire instance's cache: ```ruby a.clear_memery_cache! ``` You can also provide a block, though this approach is somewhat hacky: ```ruby a.users {} ``` ## Object Shape Optimization In Ruby 3.2, a new optimization called "object shape" was introduced, which can have negative interactions with dynamically added instance variables. Memery minimizes this impact by introducing only one new instance variable after initialization (`@_memery_memoized_values`). If you need to ensure a specific object shape, you can call `clear_memery_cache!` in your initializer to set the instance variable ahead of time. ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/tycooon/memery. ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). ## Author Created by Yuri Smirnov. ================================================ FILE: Rakefile ================================================ # frozen_string_literal: true require "bundler/gem_tasks" require "rspec/core/rake_task" require "rubocop/rake_task" RSpec::Core::RakeTask.new(:spec) RuboCop::RakeTask.new(:lint) task default: %i[lint spec] desc "run benchmark" task :benchmark do require_relative "benchmark" end ================================================ FILE: benchmark.rb ================================================ # frozen_string_literal: true require "bundler/setup" Bundler.setup require "benchmark" require "benchmark/ips" require "benchmark/memory" puts "```ruby" puts File.read(__FILE__) puts "```" puts puts "### Output" puts puts "```" require_relative "lib/memery" class Foo class << self include Memery def base_find(char) ("a".."k").find { |letter| letter == char } end memoize def find_z base_find("z") end memoize def find_new(char) base_find(char) end memoize def find_optional(*) base_find("z") end end end def test_no_args Foo.find_z end def test_with_args Foo.find_new("d") end def test_empty_args Foo.find_optional end Benchmark.ips do |x| x.report("test_no_args") { test_no_args } end Benchmark.memory do |x| x.report("test_no_args") { 100.times { test_no_args } } end Benchmark.ips do |x| x.report("test_empty_args") { test_empty_args } end Benchmark.memory do |x| x.report("test_empty_args") { 100.times { test_empty_args } } end Benchmark.ips do |x| x.report("test_with_args") { test_with_args } end Benchmark.memory do |x| x.report("test_with_args") { 100.times { test_with_args } } end Memery.use_hashed_arguments = false Benchmark.ips do |x| x.report("test_with_args_no_hash") { test_with_args } end Benchmark.memory do |x| x.report("test_with_args_no_hash") { 100.times { test_with_args } } end Memery.use_hashed_arguments = true puts "```" ================================================ FILE: lib/memery/version.rb ================================================ # frozen_string_literal: true module Memery VERSION = "1.8.0" end ================================================ FILE: lib/memery.rb ================================================ # frozen_string_literal: true require "memery/version" module Memery class << self attr_accessor :use_hashed_arguments def monotonic_clock Process.clock_gettime(Process::CLOCK_MONOTONIC) end end @use_hashed_arguments = true OUR_BLOCK = lambda do extend(ClassMethods) include(InstanceMethods) extend ModuleMethods if instance_of?(Module) end private_constant :OUR_BLOCK module ModuleMethods def included(base = nil, &block) if base.nil? && block super do instance_exec(&block) instance_exec(&OUR_BLOCK) end else base.instance_exec(&OUR_BLOCK) end end end extend ModuleMethods module ClassMethods def memoize(*method_names, condition: nil, ttl: nil) if method_names.empty? @_memery_memoize_next_method = { condition: condition, ttl: ttl } return end prepend_memery_module! method_names.each do |method_name| define_memoized_method!(method_name, condition: condition, ttl: ttl) end method_names.length > 1 ? method_names : method_names.first end def memoized?(method_name) return false unless defined?(@_memery_module) @_memery_module.method_defined?(method_name) || @_memery_module.private_method_defined?(method_name) end def method_added(name) super return unless @_memery_memoize_next_method memoize(name, **@_memery_memoize_next_method) @_memery_memoize_next_method = nil end private def prepend_memery_module! return if defined?(@_memery_module) @_memery_module = Module.new { extend MemoizationModule } prepend(@_memery_module) end def define_memoized_method!(method_name, **) @_memery_module.define_memoized_method!(self, method_name, **) end module MemoizationModule Cache = Struct.new(:result, :time) do def fresh?(ttl) return true if ttl.nil? Memery.monotonic_clock <= time + ttl end end # rubocop:disable Metrics/MethodLength def define_memoized_method!(klass, method_name, condition: nil, ttl: nil) # Include a suffix in the method key to differentiate between methods of the same name # being memoized throughout a class inheritance hierarchy method_key = "#{method_name}_#{klass.name || object_id}" original_visibility = method_visibility(klass, method_name) define_method(method_name) do |*args, &block| if block || (condition && !instance_exec(&condition)) return super(*args, &block) end cache_store = (@_memery_memoized_values ||= {}) cache_key = if args.empty? method_key else key_parts = [method_key, *args] Memery.use_hashed_arguments ? key_parts.hash : key_parts end cache = cache_store[cache_key] return cache.result if cache&.fresh?(ttl) result = super(*args) new_cache = Cache.new(result, Memery.monotonic_clock) cache_store[cache_key] = new_cache result end ruby2_keywords(method_name) send(original_visibility, method_name) end # rubocop:enable Metrics/MethodLength private def method_visibility(klass, method_name) if klass.private_method_defined?(method_name) :private elsif klass.protected_method_defined?(method_name) :protected elsif klass.public_method_defined?(method_name) :public else raise ArgumentError, "Method #{method_name} is not defined on #{klass}" end end end private_constant :MemoizationModule end module InstanceMethods def clear_memery_cache! @_memery_memoized_values = {} end end end ================================================ FILE: memery.gemspec ================================================ # frozen_string_literal: true lib = File.expand_path("lib", __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "memery/version" Gem::Specification.new do |spec| spec.required_ruby_version = ">= 3.2.0" spec.name = "memery" spec.version = Memery::VERSION spec.authors = ["Yuri Smirnov"] spec.email = ["tycoooon@gmail.com"] spec.summary = "A gem for memoization." spec.description = "Memery is a gem for memoization." spec.homepage = "https://github.com/tycooon/memery" spec.license = "MIT" spec.files = `git ls-files -z`.split("\x0").reject do |f| f.match(%r{^(test|spec|features)/}) end spec.require_paths = ["lib"] end ================================================ FILE: spec/memery_spec.rb ================================================ # frozen_string_literal: true # rubocop:disable Style/MutableConstant CALLS = [] B_CALLS = [] # rubocop:enable Style/MutableConstant class A include Memery attr_accessor :environment memoize def m m_private end memoize def m_different_line CALLS << :m_different_line :m_different_line end def not_memoized; end memoize def m_nil m_protected end memoize def m_args(x, y) CALLS << [x, y] [x, y] end memoize def m_kwargs(x, y: 42) CALLS << [x, y] [x, y] end memoize def m_double_splat(x, **kwargs) CALLS << [x, kwargs] [x, kwargs] end def m_condition CALLS << __method__ __method__ end memoize :m_condition, condition: -> { environment == "production" } def m_ttl(x, y) CALLS << [x, y] [x, y] end memoize :m_ttl, ttl: 3 protected memoize def m_protected CALLS << nil nil end private memoize def m_private CALLS << :m :m end end class B < A memoize def m_args(x, y) B_CALLS << [x, y] super(1, 2) 100 end end module M include Memery memoize def m CALLS << :m :m end memoize def m_different_line CALLS << :m_different_line :m_different_line end def not_memoized; end private memoize def m_private; end end class C include M memoize def m_class CALLS << __method__ __method__ end end class D class << self include Memery memoize def m_args(x, y) CALLS << [x, y] [x, y] end end end class E extend Forwardable def_delegator :a, :m include Memery memoize def a A.new end end class F include Memery def m; end end class G include Memery def self.macro(name) define_method(:macro_received) { name } end macro memoize def g; end end class H include Memery [:a, :b, :m, :n, :x, :y].each do |name| define_method(name) do CALLS << name name end end memoize :m, :n memoize :x, :y, ttl: 3 end RSpec.describe Memery do subject(:a) { A.new } before { CALLS.clear } before { B_CALLS.clear } before { Memery.use_hashed_arguments = true } let(:unmemoized_class) do Class.new do include Memery attr_reader :a, :b, :m, :n, :x, :y end end context "methods without args" do specify do values = [ a.m, a.m_nil, a.m, a.m_nil ] expect(values).to eq([:m, nil, :m, nil]) expect(CALLS).to eq([:m, nil]) end end context "methods without args memoize on new line" do specify do values = [ a.m_different_line, a.m_nil, a.m_different_line, a.m_nil ] expect(values).to eq([:m_different_line, nil, :m_different_line, nil]) expect(CALLS).to eq([:m_different_line, nil]) end end context "flushing cache" do specify do values = [ a.m, a.m ] a.clear_memery_cache! values << a.m expect(values).to eq([:m, :m, :m]) expect(CALLS).to eq([:m, :m]) end end context "method with args" do specify do values = [ a.m_args(1, 1), a.m_args(1, 1), a.m_args(1, 2) ] expect(values).to eq([[1, 1], [1, 1], [1, 2]]) expect(CALLS).to eq([[1, 1], [1, 2]]) end context "receiving Hash-like object" do let(:object_class) do Struct.new(:first_name, :last_name) do # For example, Sequel models have such implicit coercion, # which conflicts with `**kwargs`. alias_method :to_hash, :to_h end end let(:object) { object_class.new("John", "Wick") } specify do values = [ a.m_args(1, object), a.m_args(1, object), a.m_args(1, 2) ] expect(values).to eq([[1, object], [1, object], [1, 2]]) expect(CALLS).to eq([[1, object], [1, 2]]) end end end context "method with keyword args" do specify do values = [ a.m_kwargs(1, y: 2), a.m_kwargs(1, y: 2), a.m_kwargs(1, y: 3) ] expect(values).to eq([[1, 2], [1, 2], [1, 3]]) expect(CALLS).to eq([[1, 2], [1, 3]]) end end context "method with double splat argument" do specify do values = [ a.m_double_splat(1, y: 2), a.m_double_splat(1, y: 2), a.m_double_splat(1, y: 3) ] expect(values).to eq([[1, { y: 2 }], [1, { y: 2 }], [1, { y: 3 }]]) expect(CALLS).to eq([[1, { y: 2 }], [1, { y: 3 }]]) end end context "calling method with block" do specify do values = [] values << a.m_args(1, 1) { nil } values << a.m_args(1, 1) { nil } expect(values).to eq([[1, 1], [1, 1]]) expect(CALLS).to eq([[1, 1], [1, 1]]) end end context "calling private method" do specify do expect { a.m_private }.to raise_error(NoMethodError, /private method/) end end context "calling protected method" do specify do expect { a.m_protected }.to raise_error(NoMethodError, /protected method/) end end context "Chaining macros" do subject(:g) { G.new } specify do expect(g.macro_received).to eq :g end end context "inherited class" do subject(:b) { B.new } specify do values = [ b.m_args(1, 1), b.m_args(1, 2), b.m_args(1, 1) ] expect(values).to eq([100, 100, 100]) expect(CALLS).to eq([[1, 2]]) expect(B_CALLS).to eq([[1, 1], [1, 2]]) end end context "anonymous inherited class" do let(:anonymous_class) do Class.new(A) do memoize def m_args(x, y) B_CALLS << [x, y] super(1, 2) 100 end end end subject(:b) { anonymous_class.new } specify do values = [ b.m_args(1, 1), b.m_args(1, 2), b.m_args(1, 1) ] expect(values).to eq([100, 100, 100]) expect(CALLS).to eq([[1, 2]]) expect(B_CALLS).to eq([[1, 1], [1, 2]]) end end context "module" do subject(:c) { C.new } specify do values = [c.m, c.m, c.m] expect(values).to eq([:m, :m, :m]) expect(CALLS).to eq([:m]) end specify do values = [c.m_different_line, c.m_different_line, c.m_different_line] expect(values).to eq([:m_different_line, :m_different_line, :m_different_line]) expect(CALLS).to eq([:m_different_line]) end context "memoization in class" do specify do values = [c.m_class, c.m_class, c.m_class] expect(values).to eq([:m_class, :m_class, :m_class]) expect(CALLS).to eq([:m_class]) end end end context "module with self.included method defined" do subject(:c) { C.new } before { C.include(some_mixin) } let(:some_mixin) do Module.new do extend ActiveSupport::Concern include Memery included do attr_accessor :a end end end it "doesn't override existing method" do c.a = 15 expect(c.a).to eq(15) end end context "class method with args" do subject(:d) { D } specify do values = [ d.m_args(1, 1), d.m_args(1, 1), d.m_args(1, 2) ] expect(values).to eq([[1, 1], [1, 1], [1, 2]]) expect(CALLS).to eq([[1, 1], [1, 2]]) end end context "memoizing inexistent method" do subject(:klass) do Class.new do include Memery memoize :foo end end specify do expect { klass }.to raise_error(ArgumentError, /Method foo is not defined/) end end context "Forwardable" do subject(:e) { E.new } specify do values = [e.m, e.m, e.m] expect(values).to eq([:m, :m, :m]) expect(CALLS).to eq([:m]) end end context "without hashed arguments" do before { Memery.use_hashed_arguments = false } context "methods without args" do specify do values = [ a.m, a.m_nil, a.m, a.m_nil ] expect(values).to eq([:m, nil, :m, nil]) expect(CALLS).to eq([:m, nil]) end end context "method with args" do specify do values = [ a.m_args(1, 1), a.m_args(1, 1), a.m_args(1, 2) ] expect(values).to eq([[1, 1], [1, 1], [1, 2]]) expect(CALLS).to eq([[1, 1], [1, 2]]) end end end describe ":condition option" do before do a.environment = environment end context "returns true" do let(:environment) { "production" } specify do values = [ a.m_condition, a.m_nil, a.m_condition, a.m_nil ] expect(values).to eq([:m_condition, nil, :m_condition, nil]) expect(CALLS).to eq([:m_condition, nil]) end end context "returns false" do let(:environment) { "development" } specify do values = [ a.m_condition, a.m_nil, a.m_condition, a.m_nil ] expect(values).to eq([:m_condition, nil, :m_condition, nil]) expect(CALLS).to eq([:m_condition, nil, :m_condition]) end end end describe ":ttl option" do specify do values = [ a.m_ttl(1, 1), a.m_ttl(1, 1), a.m_ttl(1, 2) ] expect(values).to eq([[1, 1], [1, 1], [1, 2]]) expect(CALLS).to eq([[1, 1], [1, 2]]) allow(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC) .and_wrap_original { |m, *args| m.call(*args) + 5 } values = [ a.m_ttl(1, 1), a.m_ttl(1, 1), a.m_ttl(1, 2) ] expect(values).to eq([[1, 1], [1, 1], [1, 2]]) expect(CALLS).to eq([[1, 1], [1, 2], [1, 1], [1, 2]]) end context "returns false" do let(:environment) { "development" } specify do values = [ a.m_condition, a.m_nil, a.m_condition, a.m_nil ] expect(values).to eq([:m_condition, nil, :m_condition, nil]) expect(CALLS).to eq([:m_condition, nil, :m_condition]) end end end describe "with multiple methods" do let(:h) { H.new } specify do values = [h.m, h.n, h.m, h.n] expect(values).to eq([:m, :n, :m, :n]) expect(CALLS).to eq([:m, :n]) end specify do values = [h.x, h.y, h.x, h.y] expect(values).to eq([:x, :y, :x, :y]) expect(CALLS).to eq([:x, :y]) end specify do expect(unmemoized_class.memoize(:x, :y, ttl: 3)).to eq([:x, :y]) end end describe ".memoize return value" do specify do expect(unmemoized_class.memoize(:x)).to eq(:x) expect(unmemoized_class.memoize(:m, ttl: 3)).to eq(:m) expect(unmemoized_class.memoize(:a, condition: -> { 1 == 2 })).to eq(:a) end specify do expect(unmemoized_class.memoize(:x, :y)).to eq([:x, :y]) expect(unmemoized_class.memoize(:m, :n, ttl: 3)).to eq([:m, :n]) expect(unmemoized_class.memoize(:a, :b, condition: -> { 1 == 2 })).to eq([:a, :b]) end end describe ".memoized?" do subject { object.memoized?(method_name) } context "class without memoized methods" do let(:object) { F } let(:method_name) { :m } it { is_expected.to be false } end shared_examples "works correctly" do context "public memoized method" do let(:method_name) { :m } it { is_expected.to be true } end context "memoize is on a different line" do let(:method_name) { :m_different_line } it { is_expected.to be true } end context "private memoized method" do let(:method_name) { :m_private } it { is_expected.to be true } end context "non-memoized method" do let(:method_name) { :not_memoized } it { is_expected.to be false } end context "standard class method" do let(:method_name) { :constants } it { is_expected.to be false } end context "standard instance method" do let(:method_name) { :to_s } it { is_expected.to be false } end end context "class" do let(:object) { A } it_behaves_like "works correctly" end context "module" do let(:object) { M } it_behaves_like "works correctly" end end end ================================================ FILE: spec/spec_helper.rb ================================================ # frozen_string_literal: true require "simplecov" require "simplecov-lcov" SimpleCov::Formatter::LcovFormatter.config do |config| config.report_with_single_file = true config.single_report_path = "coverage/lcov.info" end SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::HTMLFormatter, SimpleCov::Formatter::LcovFormatter, ]) SimpleCov.start do enable_coverage(:branch) minimum_coverage(line: 100, branch: 100) end require "memery" require "active_support/concern" RSpec.configure do |config| # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = ".rspec_status" # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching! config.expect_with :rspec do |c| c.syntax = :expect end end