├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── Gemfile ├── HISTORY.md ├── MIT_LICENSE ├── README.md ├── Rakefile ├── ext └── java │ └── org │ └── jruby │ └── ext │ └── ref │ ├── ReferencesService.java │ ├── RubySoftReference.java │ └── RubyWeakReference.java ├── lib ├── ref.rb ├── ref │ ├── abstract_reference_key_map.rb │ ├── abstract_reference_value_map.rb │ ├── reference.rb │ ├── reference_queue.rb │ ├── soft_key_map.rb │ ├── soft_reference.rb │ ├── soft_value_map.rb │ ├── strong_reference.rb │ ├── version.rb │ ├── weak_key_map.rb │ ├── weak_reference.rb │ ├── weak_reference │ │ ├── pure_ruby.rb │ │ └── weak_ref.rb │ └── weak_value_map.rb └── ref_ext.jar ├── ref.gemspec └── spec ├── ref ├── mock_spec.rb ├── reference_queue_spec.rb ├── soft_key_map_spec.rb ├── soft_reference_spec.rb ├── soft_value_map_spec.rb ├── strong_reference_spec.rb ├── weak_key_map_spec.rb ├── weak_reference_spec.rb └── weak_value_map_spec.rb ├── shared ├── mock.rb ├── reference_key_map_shared.rb └── reference_value_map_shared.rb └── spec_helper.rb /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | tmp 3 | rdoc 4 | *.rbc 5 | coverage 6 | .ruby-version 7 | .ruby-gemset 8 | Gemfile.lock 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | dist: trusty 3 | rvm: 4 | - 2.4.1 5 | - 2.3.4 6 | - 2.2.7 7 | - 2.1.10 8 | - 2.0.0 9 | - 1.9.3 10 | - jruby-9.1.12.0 11 | - jruby-1.7.27 12 | - ruby-head 13 | - jruby-head 14 | - rbx-3.84 15 | before_install: 16 | - gem update --system 17 | - gem install bundler 18 | jdk: 19 | - oraclejdk8 20 | 21 | sudo: false 22 | 23 | branches: 24 | only: 25 | - master 26 | 27 | matrix: 28 | fast_finish: true 29 | allow_failures: 30 | - rvm: rbx-3.84 31 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem 'rake', '~> 10.3.2' 7 | gem 'rake-compiler', '0.9.5' 8 | end 9 | 10 | group :testing do 11 | gem 'test-unit', '~> 3.0.9' 12 | gem 'rspec', '~> 3.6' 13 | gem 'coveralls', '~> 0.8.21', :require => false 14 | end 15 | 16 | group :documentation do 17 | gem 'yard', '~> 0.8.7.4', :require => false 18 | gem 'inch', '~> 0.4.6', :platforms => :mri, :require => false 19 | gem 'redcarpet', '~> 3.1.2', platforms: :mri # understands github markdown 20 | end 21 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## 2.0.0 2 | 3 | *NOTE: The changes in 2.0 are mostly non-functional changes. The original 4 | author graciously passed maintenance of this gem to the 5 | github.com/ruby-concurrency* team. 2.0 is the first release by the new 6 | maintainers.* 7 | 8 | - Added AbstractReferenceKeyMap#to_h method 9 | - Added AbstractReferenceKeyMap#merge method 10 | - Migrated all tests to RSpec 11 | - Removed support for MRI 1.8.x 12 | - Removed IronRuby optimizations 13 | - Added `#put` and `#get` method aliases to `AbstractReferenceKeyMap` 14 | - Added `#size` (alias: `#length`) and `#empty?` methods to `AbstractReferenceKeyMap` 15 | - Added continuous integration with TravisCI 16 | - Moved repo to github.com/ruby-concurrency/ref 17 | 18 | ## 1.0.5 19 | 20 | - Fix breaking test in ruby 2.0 21 | 22 | ## 1.0.4 23 | 24 | - Support for BasicObject in pure ruby weak reference implementation. 25 | 26 | ## 1.0.3 27 | 28 | - Support Ruby 2.0 WeakRef implementation 29 | - Replace autoload with require to make library thread safe 30 | 31 | ## 1.0.2 32 | 33 | - Fix mock object used for testing (Burgestrand) 34 | 35 | ## 1.0.1 36 | 37 | - No code changes. Just including the license file in the release and removing deprecated tasks. 38 | 39 | ## 1.0.0 40 | 41 | - Initial release. 42 | -------------------------------------------------------------------------------- /MIT_LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Brian Durand 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ref 2 | 3 | [![Gem Version](https://badge.fury.io/rb/ref.svg)](http://badge.fury.io/rb/ref) [![Build Status](https://travis-ci.org/ruby-concurrency/ref.svg?branch=master)](https://travis-ci.org/ruby-concurrency/ref) [![Coverage Status](https://img.shields.io/coveralls/ruby-concurrency/ref/master.svg)](https://coveralls.io/r/ruby-concurrency/ref) [![Code Climate](https://codeclimate.com/github/ruby-concurrency/ref.svg)](https://codeclimate.com/github/ruby-concurrency/ref) [![Dependency Status](https://gemnasium.com/ruby-concurrency/ref.svg)](https://gemnasium.com/ruby-concurrency/ref) [![License](https://img.shields.io/badge/license-MIT-green.svg)](http://opensource.org/licenses/MIT) [![Gitter chat](http://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/ruby-concurrency/concurrent-ruby) 4 | 5 | This library provides object references for Ruby as well as some common utilities for working with references. Object references are used to point to other objects and come in three distinct flavors that interact differently with the garbage collector. 6 | 7 | * `Ref::StrongReference` - This is a plain old pointer to another object. 8 | * `Ref::WeakReference` - This is a pointer to another object, but it is not seen by the garbage collector and the memory used by the object can be reclaimed at any time. 9 | * `Ref::SoftReference` - This is similar to a weak reference, but the garbage collector is not as eager to reclaim the referenced object. 10 | 11 | All of these classes extend from a common `Ref::Reference` class and have a common interface. 12 | 13 | Weak and soft references are useful when you have instantiated objects that you may want to use again but can recreate if necessary. Since the garbage collector determines when to reclaim the memory used by the objects, you don't need to worry about bloating the Ruby heap. 14 | 15 | ## Example Usage 16 | 17 | ```ruby 18 | ref = Ref::WeakReference.new("hello") 19 | ref.object # should be "hello" 20 | ObjectSpace.garbage_collect 21 | ref.object # should be nil (assuming the garbage collector reclaimed the reference) 22 | ``` 23 | 24 | ## Goodies 25 | 26 | This library also includes tools for some common uses of weak and soft references. 27 | 28 | * `Ref::WeakKeyMap` - A map of keys to values where the keys are weak references 29 | * `Ref::WeakValueMap` - A map of keys to values where the values are weak references 30 | * `Ref::SoftKeyMap` - A map of keys to values where the keys are soft references 31 | * `Ref::SoftValueMap` - A map of keys to values where the values are soft references 32 | * `Ref::ReferenceQueue` - A thread safe implementation of a queue that will add references to itself as their objects are garbage collected. 33 | 34 | ## Problems with WeakRef 35 | 36 | Ruby does come with the `WeakRef` class in the standard library. However, there are [issues with this class](https://bugs.ruby-lang.org/issues/4168) across several different Ruby runtimes. This gem provides a common interface to weak references that works across MRI, Ruby Enterprise Edition, YARV, JRuby and Rubinius. 37 | 38 | 1. Rubinius - Rubinius implements `WeakRef` with a lighter weight version of delegation and works very well. 39 | 2. YARV 1.9 - `WeakRef` is unsafe to use because the garbage collector can run in a different system thread than a thread allocating memory. This exposes a bug where a `WeakRef` may end up pointing to a completely different object than it originally referenced. 40 | 3. MRI Ruby 2.0+ has a good implementation of `WeakRef`. 41 | 42 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | 3 | $:.push File.join(File.dirname(__FILE__), 'lib') 4 | 5 | require 'ref' 6 | 7 | begin 8 | require 'rspec' 9 | require 'rspec/core/rake_task' 10 | 11 | RSpec::Core::RakeTask.new(:spec) do |t| 12 | t.rspec_opts = '--color --backtrace --format documentation' 13 | end 14 | 15 | task :default => :spec 16 | rescue LoadError 17 | puts 'Error loading Rspec rake tasks, probably building the gem...' 18 | end 19 | 20 | spec = eval(File.read(File.expand_path('../ref.gemspec', __FILE__))) 21 | 22 | GEM_NAME = 'ref' 23 | EXTENSION_NAME = 'extension' 24 | JAVA_EXT_NAME = 'ref_ext' 25 | CORE_GEMSPEC = Gem::Specification.load('ref.gemspec') 26 | 27 | if Ref.jruby? 28 | CORE_GEM = "#{GEM_NAME}-#{Ref::VERSION}-java.gem" 29 | 30 | require 'rake/javaextensiontask' 31 | Rake::JavaExtensionTask.new(JAVA_EXT_NAME, CORE_GEMSPEC) do |ext| 32 | ext.ext_dir = 'ext' 33 | end 34 | else 35 | CORE_GEM = "#{GEM_NAME}-#{Ref::VERSION}.gem" 36 | end 37 | 38 | task :clean do 39 | rm_f Dir.glob('./**/*.so') 40 | rm_f Dir.glob('./**/*.bundle') 41 | rm_f Dir.glob('./lib/*.jar') 42 | mkdir_p 'pkg' 43 | end 44 | 45 | namespace :build do 46 | build_deps = [:clean] 47 | build_deps << :compile if Ref.jruby? 48 | desc "Build #{CORE_GEM} into the pkg directory" 49 | task :core => build_deps do 50 | sh "gem build #{CORE_GEMSPEC.name}.gemspec" 51 | sh 'mv *.gem pkg/' 52 | end 53 | end 54 | 55 | task :build => ['build:core'] 56 | 57 | namespace :test do 58 | namespace :performance do 59 | desc "Run a speed test on how long it takes to create 100000 weak references" 60 | task :weak_reference do 61 | puts "Testing performance of weak references..." 62 | unless Ref.jruby? 63 | t = Time.now 64 | Process.fork do 65 | 100000.times do 66 | Ref::WeakReference.new(Object.new) 67 | end 68 | end 69 | Process.wait 70 | puts "Creating 100,000 weak references took #{Time.now - t} seconds" 71 | else 72 | puts 'Cannot run weak_reference performance test on JRuby - Fork is not available on this platform.' 73 | end 74 | end 75 | 76 | desc "Run a speed test on how long it takes to create 100000 soft references" 77 | task :soft_reference do 78 | puts "Testing performance of soft references..." 79 | t = Time.now 80 | 100000.times do 81 | Ref::SoftReference.new(Object.new) 82 | end 83 | GC.start 84 | GC.start 85 | puts "Creating 100,000 soft references took #{Time.now - t} seconds" 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /ext/java/org/jruby/ext/ref/ReferencesService.java: -------------------------------------------------------------------------------- 1 | package org.jruby.ext.ref; 2 | 3 | import java.io.IOException; 4 | import org.jruby.Ruby; 5 | import org.jruby.RubyClass; 6 | import org.jruby.RubyModule; 7 | import org.jruby.runtime.builtin.IRubyObject; 8 | import org.jruby.runtime.load.BasicLibraryService; 9 | 10 | /** 11 | * This library adds native Java support for weak and soft references. 12 | * 13 | * @author Brian Durand 14 | */ 15 | public class ReferencesService implements BasicLibraryService { 16 | public boolean basicLoad(Ruby runtime) throws IOException { 17 | RubyModule refModule = runtime.getModule("Ref"); 18 | RubyClass referenceClass = refModule.getClass("Reference"); 19 | 20 | RubyClass rubyWeakReferenceClass = runtime.defineClassUnder("WeakReference", referenceClass, RubyWeakReference.ALLOCATOR, refModule); 21 | rubyWeakReferenceClass.defineAnnotatedMethods(RubyWeakReference.class); 22 | 23 | RubyClass rubySoftReferenceClass = runtime.defineClassUnder("SoftReference", referenceClass, RubySoftReference.ALLOCATOR, refModule); 24 | rubySoftReferenceClass.defineAnnotatedMethods(RubySoftReference.class); 25 | 26 | return true; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ext/java/org/jruby/ext/ref/RubySoftReference.java: -------------------------------------------------------------------------------- 1 | package org.jruby.ext.ref; 2 | 3 | import java.lang.ref.SoftReference; 4 | import org.jruby.Ruby; 5 | import org.jruby.RubyClass; 6 | import org.jruby.RubyObject; 7 | import org.jruby.anno.JRubyMethod; 8 | import org.jruby.runtime.builtin.IRubyObject; 9 | import org.jruby.runtime.ObjectAllocator; 10 | import org.jruby.runtime.ThreadContext; 11 | import org.jruby.runtime.Visibility; 12 | 13 | public class RubySoftReference extends RubyObject { 14 | private SoftReference _ref; 15 | private static final String REFERENCED_OBJECT_ID_VARIABLE = "@referenced_object_id".intern(); 16 | 17 | public RubySoftReference(Ruby runtime, RubyClass klass) { 18 | super(runtime, klass); 19 | } 20 | 21 | public static final ObjectAllocator ALLOCATOR = new ObjectAllocator() { 22 | public IRubyObject allocate(Ruby runtime, RubyClass klass) { 23 | return new RubySoftReference(runtime, klass); 24 | } 25 | }; 26 | 27 | @JRubyMethod(name = "initialize", frame = true, visibility = Visibility.PRIVATE) 28 | public IRubyObject initialize(ThreadContext context, IRubyObject obj) { 29 | _ref = new SoftReference(obj); 30 | fastSetInstanceVariable(REFERENCED_OBJECT_ID_VARIABLE, obj.id()); 31 | return context.getRuntime().getNil(); 32 | } 33 | 34 | @JRubyMethod(name = "object") 35 | public IRubyObject object() { 36 | IRubyObject obj = (IRubyObject)_ref.get(); 37 | if (obj != null) { 38 | return obj; 39 | } else { 40 | return getRuntime().getNil(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ext/java/org/jruby/ext/ref/RubyWeakReference.java: -------------------------------------------------------------------------------- 1 | package org.jruby.ext.ref; 2 | 3 | import java.lang.ref.WeakReference; 4 | import org.jruby.Ruby; 5 | import org.jruby.RubyClass; 6 | import org.jruby.RubyObject; 7 | import org.jruby.anno.JRubyMethod; 8 | import org.jruby.runtime.builtin.IRubyObject; 9 | import org.jruby.runtime.ObjectAllocator; 10 | import org.jruby.runtime.ThreadContext; 11 | import org.jruby.runtime.Visibility; 12 | 13 | public class RubyWeakReference extends RubyObject { 14 | private WeakReference _ref; 15 | private static final String REFERENCED_OBJECT_ID_VARIABLE = "@referenced_object_id".intern(); 16 | 17 | public RubyWeakReference(Ruby runtime, RubyClass klass) { 18 | super(runtime, klass); 19 | } 20 | 21 | public static final ObjectAllocator ALLOCATOR = new ObjectAllocator() { 22 | public IRubyObject allocate(Ruby runtime, RubyClass klass) { 23 | return new RubyWeakReference(runtime, klass); 24 | } 25 | }; 26 | 27 | @JRubyMethod(name = "initialize", frame = true, visibility = Visibility.PRIVATE) 28 | public IRubyObject initialize(ThreadContext context, IRubyObject obj) { 29 | _ref = new WeakReference(obj); 30 | fastSetInstanceVariable(REFERENCED_OBJECT_ID_VARIABLE, obj.id()); 31 | return context.getRuntime().getNil(); 32 | } 33 | 34 | @JRubyMethod(name = "object") 35 | public IRubyObject object() { 36 | IRubyObject obj = (IRubyObject)_ref.get(); 37 | if (obj != null) { 38 | return obj; 39 | } else { 40 | return getRuntime().getNil(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/ref.rb: -------------------------------------------------------------------------------- 1 | module Ref 2 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 3 | 4 | require 'ref/abstract_reference_value_map' 5 | require 'ref/abstract_reference_key_map' 6 | require 'ref/reference' 7 | require 'ref/reference_queue' 8 | 9 | if defined?(Java) 10 | begin 11 | require 'ref_ext' 12 | require 'org/jruby/ext/ref/references' 13 | rescue LoadError 14 | require 'ref/soft_reference' 15 | require 'ref/weak_reference' 16 | warn 'Error loading Rspec rake tasks, probably building the gem...' 17 | end 18 | else 19 | require 'ref/soft_reference' 20 | if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx' 21 | # If using Rubinius set the implementation to use WeakRef since it is very efficient and using finalizers is not. 22 | require 'ref/weak_reference/weak_ref' 23 | elsif defined?(::ObjectSpace::WeakMap) 24 | # Ruby 2.0 has a working implementation of weakref.rb backed by the new ObjectSpace::WeakMap 25 | require 'ref/weak_reference/weak_ref' 26 | elsif defined?(::ObjectSpace._id2ref) 27 | # If ObjectSpace can lookup objects from their object_id, then use the pure ruby implementation. 28 | require 'ref/weak_reference/pure_ruby' 29 | else 30 | # Otherwise, wrap the standard library WeakRef class 31 | require 'ref/weak_reference/weak_ref' 32 | end 33 | end 34 | 35 | require 'ref/soft_key_map' 36 | require 'ref/soft_value_map' 37 | require 'ref/strong_reference' 38 | require 'ref/weak_key_map' 39 | require 'ref/weak_value_map' 40 | 41 | def self.jruby? 42 | defined?(Java) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/ref/abstract_reference_key_map.rb: -------------------------------------------------------------------------------- 1 | module Ref 2 | # Abstract base class for WeakKeyMap and SoftKeyMap. 3 | # 4 | # The classes behave similar to Hashes, but the keys in the map are not strong references 5 | # and can be reclaimed by the garbage collector at any time. When a key is reclaimed, the 6 | # map entry will be removed. 7 | class AbstractReferenceKeyMap 8 | class << self 9 | def reference_class=(klass) #:nodoc: 10 | @reference_class = klass 11 | end 12 | 13 | def reference_class #:nodoc: 14 | raise NotImplementedError.new("#{name} is an abstract class and cannot be instantiated") unless @reference_class 15 | @reference_class 16 | end 17 | end 18 | 19 | # Create a new map. Values added to the hash will be cleaned up by the garbage 20 | # collector if there are no other reference except in the map. 21 | def initialize 22 | @values = {} 23 | @references_to_keys_map = {} 24 | @lock = Monitor.new 25 | @reference_cleanup = lambda{|object_id| remove_reference_to(object_id)} 26 | end 27 | 28 | # Get a value from the map by key. If the value has been reclaimed by the garbage 29 | # collector, this will return nil. 30 | def [](key) 31 | @lock.synchronize do 32 | rkey = ref_key(key) 33 | @values[rkey] if rkey 34 | end 35 | end 36 | 37 | alias_method :get, :[] 38 | 39 | # Add a key/value to the map. 40 | def []=(key, value) 41 | ObjectSpace.define_finalizer(key, @reference_cleanup) 42 | @lock.synchronize do 43 | @references_to_keys_map[key.__id__] = self.class.reference_class.new(key) 44 | @values[key.__id__] = value 45 | end 46 | end 47 | 48 | alias_method :put, :[]= 49 | 50 | # Remove the value associated with the key from the map. 51 | def delete(key) 52 | @lock.synchronize do 53 | rkey = ref_key(key) 54 | if rkey 55 | @references_to_keys_map.delete(rkey) 56 | @values.delete(rkey) 57 | else 58 | nil 59 | end 60 | end 61 | end 62 | 63 | # Get an array of keys that have not yet been garbage collected. 64 | def keys 65 | @values.keys.collect{|rkey| @references_to_keys_map[rkey].object}.compact 66 | end 67 | 68 | # Turn the map into an arry of [key, value] entries. 69 | def to_a 70 | array = [] 71 | each{|k,v| array << [k, v]} 72 | array 73 | end 74 | 75 | # Returns a hash containing the names and values for the struct’s members. 76 | def to_h 77 | hash = {} 78 | each{|k,v| hash[k] = v} 79 | hash 80 | end 81 | 82 | # Iterate through all the key/value pairs in the map that have not been reclaimed 83 | # by the garbage collector. 84 | def each 85 | @references_to_keys_map.each do |rkey, ref| 86 | key = ref.object 87 | yield(key, @values[rkey]) if key 88 | end 89 | end 90 | 91 | # Clear the map of all key/value pairs. 92 | def clear 93 | @lock.synchronize do 94 | @values.clear 95 | @references_to_keys_map.clear 96 | end 97 | end 98 | 99 | # Returns a new struct containing the contents of `other` and the contents 100 | # of `self`. If no block is specified, the value for entries with duplicate 101 | # keys will be that of `other`. Otherwise the value for each duplicate key 102 | # is determined by calling the block with the key, its value in `self` and 103 | # its value in `other`. 104 | def merge(other_hash, &block) 105 | to_h.merge(other_hash, &block).reduce(self.class.new) do |map, pair| 106 | map[pair.first] = pair.last 107 | map 108 | end 109 | end 110 | 111 | # Merge the values from another hash into this map. 112 | def merge!(other_hash) 113 | @lock.synchronize do 114 | other_hash.each { |key, value| self[key] = value } 115 | end 116 | end 117 | 118 | # The number of entries in the map 119 | def size 120 | @references_to_keys_map.count do |_, ref| 121 | ref.object 122 | end 123 | end 124 | 125 | alias_method :length, :size 126 | 127 | # True if there are entries that exist in the map 128 | def empty? 129 | @references_to_keys_map.each do |_, ref| 130 | return false if ref.object 131 | end 132 | true 133 | end 134 | 135 | def inspect 136 | live_entries = {} 137 | each do |key, value| 138 | live_entries[key] = value 139 | end 140 | live_entries.inspect 141 | end 142 | 143 | private 144 | 145 | def ref_key(key) 146 | ref = @references_to_keys_map[key.__id__] 147 | if ref && ref.object 148 | ref.referenced_object_id 149 | else 150 | nil 151 | end 152 | end 153 | 154 | def remove_reference_to(object_id) 155 | @lock.synchronize do 156 | @references_to_keys_map.delete(object_id) 157 | @values.delete(object_id) 158 | end 159 | end 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /lib/ref/abstract_reference_value_map.rb: -------------------------------------------------------------------------------- 1 | module Ref 2 | # Abstract base class for WeakValueMap and SoftValueMap. 3 | # 4 | # The classes behave similar to Hashes, but the values in the map are not strong references 5 | # and can be reclaimed by the garbage collector at any time. When a value is reclaimed, the 6 | # map entry will be removed. 7 | class AbstractReferenceValueMap 8 | class << self 9 | def reference_class=(klass) #:nodoc: 10 | @reference_class = klass 11 | end 12 | 13 | def reference_class #:nodoc: 14 | raise NotImplementedError.new("#{name} is an abstract class and cannot be instantiated") unless @reference_class 15 | @reference_class 16 | end 17 | end 18 | 19 | # Create a new map. Values added to the map will be cleaned up by the garbage 20 | # collector if there are no other reference except in the map. 21 | def initialize 22 | @references = {} 23 | @references_to_keys_map = {} 24 | @lock = Monitor.new 25 | @reference_cleanup = lambda{|object_id| remove_reference_to(object_id)} 26 | end 27 | 28 | # Get a value from the map by key. If the value has been reclaimed by the garbage 29 | # collector, this will return nil. 30 | def [](key) 31 | @lock.synchronize do 32 | ref = @references[key] 33 | value = ref.object if ref 34 | value 35 | end 36 | end 37 | 38 | alias_method :get, :[] 39 | 40 | # Add a key/value to the map. 41 | def []=(key, value) 42 | ObjectSpace.define_finalizer(value, @reference_cleanup) 43 | key = key.dup if key.is_a?(String) 44 | @lock.synchronize do 45 | @references[key] = self.class.reference_class.new(value) 46 | keys_for_id = @references_to_keys_map[value.__id__] 47 | unless keys_for_id 48 | keys_for_id = [] 49 | @references_to_keys_map[value.__id__] = keys_for_id 50 | end 51 | keys_for_id << key 52 | end 53 | value 54 | end 55 | 56 | alias_method :put, :[]= 57 | 58 | # Remove the entry associated with the key from the map. 59 | def delete(key) 60 | ref = @references.delete(key) 61 | if ref 62 | keys_to_id = @references_to_keys_map[ref.referenced_object_id] 63 | if keys_to_id 64 | keys_to_id.delete(key) 65 | @references_to_keys_map.delete(ref.referenced_object_id) if keys_to_id.empty? 66 | end 67 | ref.object 68 | else 69 | nil 70 | end 71 | end 72 | 73 | # Get the list of all values that have not yet been garbage collected. 74 | def values 75 | vals = [] 76 | each{|k,v| vals << v} 77 | vals 78 | end 79 | 80 | # Turn the map into an arry of [key, value] entries 81 | def to_a 82 | array = [] 83 | each{|k,v| array << [k, v]} 84 | array 85 | end 86 | 87 | # Returns a hash containing the names and values for the struct’s members. 88 | def to_h 89 | hash = {} 90 | each{|k,v| hash[k] = v} 91 | hash 92 | end 93 | 94 | # Iterate through all the key/value pairs in the map that have not been reclaimed 95 | # by the garbage collector. 96 | def each 97 | @references.each do |key, ref| 98 | value = ref.object 99 | yield(key, value) if value 100 | end 101 | end 102 | 103 | # Clear the map of all key/value pairs. 104 | def clear 105 | @lock.synchronize do 106 | @references.clear 107 | @references_to_keys_map.clear 108 | end 109 | end 110 | 111 | # Returns a new struct containing the contents of `other` and the contents 112 | # of `self`. If no block is specified, the value for entries with duplicate 113 | # keys will be that of `other`. Otherwise the value for each duplicate key 114 | # is determined by calling the block with the key, its value in `self` and 115 | # its value in `other`. 116 | def merge(other_hash, &block) 117 | to_h.merge(other_hash, &block).reduce(self.class.new) do |map, pair| 118 | map[pair.first] = pair.last 119 | map 120 | end 121 | end 122 | 123 | # Merge the values from another hash into this map. 124 | def merge!(other_hash) 125 | @lock.synchronize do 126 | other_hash.each { |key, value| self[key] = value } 127 | end 128 | end 129 | 130 | # The number of entries in the map 131 | def size 132 | @references.count do |_, ref| 133 | ref.object 134 | end 135 | end 136 | 137 | alias_method :length, :size 138 | 139 | # True if there are entries that exist in the map 140 | def empty? 141 | @references.each do |_, ref| 142 | return false if ref.object 143 | end 144 | 145 | true 146 | end 147 | 148 | def inspect 149 | live_entries = {} 150 | each do |key, value| 151 | live_entries[key] = value 152 | end 153 | live_entries.inspect 154 | end 155 | 156 | private 157 | 158 | def remove_reference_to(object_id) 159 | @lock.synchronize do 160 | keys = @references_to_keys_map[object_id] 161 | if keys 162 | keys.each do |key| 163 | @references.delete(key) 164 | end 165 | @references_to_keys_map.delete(object_id) 166 | end 167 | end 168 | end 169 | end 170 | end 171 | -------------------------------------------------------------------------------- /lib/ref/reference.rb: -------------------------------------------------------------------------------- 1 | module Ref 2 | # This class serves as a generic reference mechanism to other objects. The 3 | # actual reference can be either a WeakReference, SoftReference, or StrongReference. 4 | class Reference 5 | # The object id of the object being referenced. 6 | attr_reader :referenced_object_id 7 | 8 | # Create a new reference to an object. 9 | def initialize(obj) 10 | raise NotImplementedError.new("cannot instantiate a generic reference") 11 | end 12 | 13 | # Get the referenced object. This could be nil if the reference 14 | # is a WeakReference or a SoftReference and the object has been reclaimed by the garbage collector. 15 | def object 16 | raise NotImplementedError 17 | end 18 | 19 | def inspect 20 | obj = object 21 | "<##{self.class.name}: #{obj ? obj.inspect : "##{referenced_object_id} (not accessible)"}>" 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/ref/reference_queue.rb: -------------------------------------------------------------------------------- 1 | module Ref 2 | # This class provides a simple thread safe container to hold a reference queue. Instances 3 | # of WeakReference can be added to the queue and as the objects pointed to by those references 4 | # are cleaned up by the garbage collector, the references will be added to the queue. 5 | # 6 | # The reason for using a reference queue is that it tends to be more efficient than adding 7 | # individual finalizers to objects and the cleanup code can be handled by a thread outside 8 | # of garbage collection. 9 | # 10 | # In general, you should create your own subclass of WeakReference that contains the logic 11 | # needed to complete the cleanup. The object pointed to will have already been cleaned up 12 | # and the reference cannot maintain a reference to the object. 13 | # 14 | # === Example usage: 15 | # 16 | # class MyRef < Ref::WeakReference 17 | # def cleanup 18 | # # Do something... 19 | # end 20 | # end 21 | # 22 | # queue = Ref::ReferenceQueue.new 23 | # ref = MyRef.new(Object.new) 24 | # queue.monitor(ref) 25 | # queue.shift # = nil 26 | # ObjectSpace.garbage_collect 27 | # r = queue.shift # = ref 28 | # r.cleanup 29 | class ReferenceQueue 30 | def initialize 31 | @queue = [] 32 | @references = {} 33 | @lock = Monitor.new 34 | @finalizer = lambda do |object_id| 35 | @lock.synchronize do 36 | ref = @references.delete(object_id) 37 | @queue.push(ref) if ref 38 | end 39 | end 40 | end 41 | 42 | # Monitor a reference. When the object the reference points to is garbage collected, 43 | # the reference will be added to the queue. 44 | def monitor(reference) 45 | obj = reference.object 46 | if obj 47 | @lock.synchronize do 48 | @references[reference.referenced_object_id] = reference 49 | end 50 | ObjectSpace.define_finalizer(obj, @finalizer) 51 | else 52 | push(reference) 53 | end 54 | end 55 | 56 | # Add a reference to the queue. 57 | def push(reference) 58 | if reference 59 | @lock.synchronize do 60 | @queue.push(reference) 61 | end 62 | end 63 | end 64 | 65 | # Pull the last reference off the queue. Returns +nil+ if their are no references. 66 | def pop 67 | @lock.synchronize do 68 | @queue.pop 69 | end 70 | end 71 | 72 | # Pull the next reference off the queue. Returns +nil+ if there are no references. 73 | def shift 74 | @lock.synchronize do 75 | @queue.shift 76 | end 77 | end 78 | 79 | # Return +true+ if the queue is empty. 80 | def empty? 81 | @queue.empty? 82 | end 83 | 84 | # Get the current size of the queue. 85 | def size 86 | @queue.size 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/ref/soft_key_map.rb: -------------------------------------------------------------------------------- 1 | module Ref 2 | # Implementation of a map in which only softly referenced keys are kept to the map values. 3 | # This allows the garbage collector to reclaim these objects if the only reference to them 4 | # is the soft reference in the map. 5 | # 6 | # This is often useful for cache implementations since the map can be allowed to grow 7 | # without bound and the garbage collector can be relied on to clean it up as necessary. 8 | # One must be careful, though, when accessing entries since they can be collected at 9 | # any time until there is a strong reference to the key. 10 | # 11 | # === Example usage: 12 | # 13 | # cache = Ref::SoftKeyMap.new 14 | # obj = MyObject.find_by_whatever 15 | # obj_info = Service.lookup_object_info(obj) 16 | # cache[obj] = Service.lookup_object_info(obj) 17 | # cache[obj] # The values looked up from the service 18 | # obj = nil 19 | # ObjectSpace.garbage_collect 20 | # cache.keys # empty array since the keys and values have been reclaimed 21 | # 22 | # See AbstractReferenceKeyMap for details. 23 | class SoftKeyMap < AbstractReferenceKeyMap 24 | self.reference_class = SoftReference 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/ref/soft_reference.rb: -------------------------------------------------------------------------------- 1 | module Ref 2 | # A SoftReference represents a reference to an object that is not seen by 3 | # the tracing phase of the garbage collector. This allows the referenced 4 | # object to be garbage collected as if nothing is referring to it. 5 | # 6 | # A SoftReference differs from a WeakReference in that the garbage collector 7 | # is not so eager to reclaim soft references so they should persist longer. 8 | # 9 | # === Example usage: 10 | # 11 | # foo = Object.new 12 | # ref = Ref::SoftReference.new(foo) 13 | # ref.object # should be foo 14 | # ObjectSpace.garbage_collect 15 | # ref.object # should be foo 16 | # ObjectSpace.garbage_collect 17 | # ObjectSpace.garbage_collect 18 | # ref.object # should be nil 19 | class SoftReference < Reference 20 | @@strong_references = [{}] 21 | @@gc_flag_set = false 22 | 23 | # Number of garbage collection cycles after an object is used before a reference to it can be reclaimed. 24 | MIN_GC_CYCLES = 10 25 | 26 | @@lock = Monitor.new 27 | 28 | @@finalizer = lambda do |object_id| 29 | @@lock.synchronize do 30 | while @@strong_references.size >= MIN_GC_CYCLES do 31 | @@strong_references.shift 32 | end 33 | @@strong_references.push({}) if @@strong_references.size < MIN_GC_CYCLES 34 | @@gc_flag_set = false 35 | end 36 | end 37 | 38 | # Create a new soft reference to an object. 39 | def initialize(obj) 40 | @referenced_object_id = obj.__id__ 41 | @weak_reference = WeakReference.new(obj) 42 | add_strong_reference(obj) 43 | end 44 | 45 | # Get the referenced object. If the object has been reclaimed by the 46 | # garbage collector, then this will return nil. 47 | def object 48 | obj = @weak_reference.object 49 | # add a temporary strong reference each time the object is referenced. 50 | add_strong_reference(obj) if obj 51 | obj 52 | end 53 | 54 | private 55 | # Create a strong reference to the object. This reference will live 56 | # for three passes of the garbage collector. 57 | def add_strong_reference(obj) #:nodoc: 58 | @@lock.synchronize do 59 | @@strong_references.last[obj] = true 60 | unless @@gc_flag_set 61 | @@gc_flag_set = true 62 | ObjectSpace.define_finalizer(Object.new, @@finalizer) 63 | end 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/ref/soft_value_map.rb: -------------------------------------------------------------------------------- 1 | module Ref 2 | # Implementation of a map in which soft references are kept to the map values. 3 | # This allows the garbage collector to reclaim these objects if the 4 | # only reference to them is the soft reference in the map. 5 | # 6 | # This is often useful for cache implementations since the map can be allowed to grow 7 | # without bound and the garbage collector can be relied on to clean it up as necessary. 8 | # One must be careful, though, when accessing entries since the values can be collected 9 | # at any time until there is a strong reference to them. 10 | # 11 | # === Example usage: 12 | # 13 | # cache = Ref::SoftValueMap.new 14 | # foo = "foo" 15 | # cache["strong"] = foo # add a value with a strong reference 16 | # cache["soft"] = "bar" # add a value without a strong reference 17 | # cache["strong"] # "foo" 18 | # cache["soft"] # "bar" 19 | # ObjectSpace.garbage_collect 20 | # ObjectSpace.garbage_collect 21 | # cache["strong"] # "foo" 22 | # cache["soft"] # nil 23 | # 24 | # See AbstractReferenceValueMap for details. 25 | class SoftValueMap < AbstractReferenceValueMap 26 | self.reference_class = SoftReference 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/ref/strong_reference.rb: -------------------------------------------------------------------------------- 1 | module Ref 2 | # This implementation of Reference holds a strong reference to an object. The 3 | # referenced object will not be garbage collected as long as the strong reference 4 | # exists. 5 | class StrongReference < Reference 6 | # Create a new strong reference to an object. 7 | def initialize(obj) 8 | @obj = obj 9 | @referenced_object_id = obj.__id__ 10 | end 11 | 12 | # Get the referenced object. 13 | def object 14 | @obj 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ref/version.rb: -------------------------------------------------------------------------------- 1 | module Ref 2 | VERSION = '2.0.0' 3 | end 4 | -------------------------------------------------------------------------------- /lib/ref/weak_key_map.rb: -------------------------------------------------------------------------------- 1 | module Ref 2 | # Implementation of a map in which only weakly referenced keys are kept to the map values. 3 | # This allows the garbage collector to reclaim these objects if the only reference to them 4 | # is the weak reference in the map. 5 | # 6 | # This is often useful for cache implementations since the map can be allowed to grow 7 | # without bound and the garbage collector can be relied on to clean it up as necessary. 8 | # One must be careful, though, when accessing entries since they can be collected at 9 | # any time until there is a strong reference to the key. 10 | # 11 | # === Example usage: 12 | # 13 | # cache = Ref::WeakKeyMap.new 14 | # obj = MyObject.find_by_whatever 15 | # obj_info = Service.lookup_object_info(obj) 16 | # cache[obj] = Service.lookup_object_info(obj) 17 | # cache[obj] # The values looked up from the service 18 | # obj = nil 19 | # ObjectSpace.garbage_collect 20 | # cache.keys # empty array since the keys and values have been reclaimed 21 | # 22 | # See AbstractReferenceKeyMap for details. 23 | class WeakKeyMap < AbstractReferenceKeyMap 24 | self.reference_class = WeakReference 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/ref/weak_reference.rb: -------------------------------------------------------------------------------- 1 | module Ref 2 | # A WeakReference represents a reference to an object that is not seen by 3 | # the tracing phase of the garbage collector. This allows the referenced 4 | # object to be garbage collected as if nothing is referring to it. 5 | # 6 | # === Example usage: 7 | # 8 | # foo = Object.new 9 | # ref = Ref::WeakReference.new(foo) 10 | # ref.object # should be foo 11 | # ObjectSpace.garbage_collect 12 | # ref.object # should be nil 13 | class WeakReference < Reference 14 | 15 | # Create a weak reference to an object. 16 | def initialize(obj) 17 | raise NotImplementedError.new("This is an abstract class; you must require an implementation") 18 | end 19 | 20 | # Get the referenced object. If the object has been reclaimed by the 21 | # garbage collector, then this will return nil. 22 | def object 23 | raise NotImplementedError.new("This is an abstract class; you must require an implementation") 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/ref/weak_reference/pure_ruby.rb: -------------------------------------------------------------------------------- 1 | module Ref 2 | # This is a pure ruby implementation of a weak reference. It is much more 3 | # efficient than the WeakRef implementation bundled in MRI 1.8 and 1.9 4 | # subclass Delegator which is very heavy to instantiate and utilizes a 5 | # because it does not fair amount of memory under Ruby 1.8. 6 | class WeakReference < Reference 7 | 8 | class ReferencePointer 9 | def initialize(object) 10 | @referenced_object_id = object.__id__ 11 | add_backreference(object) 12 | end 13 | 14 | def cleanup 15 | obj = ObjectSpace._id2ref(@referenced_object_id) rescue nil 16 | remove_backreference(obj) if obj 17 | end 18 | 19 | def object 20 | obj = ObjectSpace._id2ref(@referenced_object_id) 21 | obj if verify_backreferences(obj) 22 | rescue RangeError 23 | nil 24 | end 25 | 26 | private 27 | # Verify that the object is the same one originally set for the weak reference. 28 | def verify_backreferences(obj) #:nodoc: 29 | return nil unless supports_backreference?(obj) 30 | backreferences = obj.instance_variable_get(:@__weak_backreferences__) if obj.instance_variable_defined?(:@__weak_backreferences__) 31 | backreferences && backreferences.include?(object_id) 32 | end 33 | 34 | # Add a backreference to the object. 35 | def add_backreference(obj) #:nodoc: 36 | return unless supports_backreference?(obj) 37 | backreferences = obj.instance_variable_get(:@__weak_backreferences__) if obj.instance_variable_defined?(:@__weak_backreferences__) 38 | unless backreferences 39 | backreferences = [] 40 | obj.instance_variable_set(:@__weak_backreferences__, backreferences) 41 | end 42 | backreferences << object_id 43 | end 44 | 45 | # Remove backreferences from the object. 46 | def remove_backreference(obj) #:nodoc: 47 | return unless supports_backreference?(obj) 48 | backreferences = obj.instance_variable_get(:@__weak_backreferences__) if obj.instance_variable_defined?(:@__weak_backreferences__) 49 | if backreferences 50 | backreferences.dup.delete(object_id) 51 | obj.send(:remove_instance_variable, :@__weak_backreferences__) if backreferences.empty? 52 | end 53 | end 54 | 55 | def supports_backreference?(obj) 56 | obj.respond_to?(:instance_variable_get) && obj.respond_to?(:instance_variable_defined?) 57 | rescue NoMethodError 58 | false 59 | end 60 | end 61 | 62 | @@weak_references = {} 63 | @@lock = Monitor.new 64 | 65 | # Finalizer that cleans up weak references when references are destroyed. 66 | @@reference_finalizer = lambda do |object_id| 67 | @@lock.synchronize do 68 | reference_pointer = @@weak_references.delete(object_id) 69 | reference_pointer.cleanup if reference_pointer 70 | end 71 | end 72 | 73 | # Create a new weak reference to an object. The existence of the weak reference 74 | # will not prevent the garbage collector from reclaiming the referenced object. 75 | def initialize(obj) #:nodoc: 76 | @referenced_object_id = obj.__id__ 77 | @@lock.synchronize do 78 | @reference_pointer = ReferencePointer.new(obj) 79 | @@weak_references[self.object_id] = @reference_pointer 80 | end 81 | ObjectSpace.define_finalizer(self, @@reference_finalizer) 82 | end 83 | 84 | # Get the reference object. If the object has already been garbage collected, 85 | # then this method will return nil. 86 | def object #:nodoc: 87 | if @reference_pointer 88 | obj = @reference_pointer.object 89 | unless obj 90 | @@lock.synchronize do 91 | @@weak_references.delete(object_id) 92 | @reference_pointer.cleanup 93 | @reference_pointer = nil 94 | end 95 | end 96 | obj 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/ref/weak_reference/weak_ref.rb: -------------------------------------------------------------------------------- 1 | require 'weakref' 2 | 3 | module Ref 4 | class WeakReference < Reference 5 | # This implementation of a weak reference simply wraps the standard WeakRef implementation 6 | # that comes with the Ruby standard library. 7 | def initialize(obj) #:nodoc: 8 | @referenced_object_id = obj.__id__ 9 | @ref = ::WeakRef.new(obj) 10 | end 11 | 12 | def object #:nodoc: 13 | @ref.__getobj__ 14 | rescue => e 15 | # Jruby implementation uses RefError while MRI uses WeakRef::RefError 16 | if (defined?(RefError) && e.is_a?(RefError)) || (defined?(::WeakRef::RefError) && e.is_a?(::WeakRef::RefError)) 17 | nil 18 | else 19 | raise e 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/ref/weak_value_map.rb: -------------------------------------------------------------------------------- 1 | module Ref 2 | # Implementation of a map in which weak references are kept to the map values. 3 | # This allows the garbage collector to reclaim these objects if the 4 | # only reference to them is the weak reference in the map. 5 | # 6 | # This is often useful for cache implementations since the map can be allowed to grow 7 | # without bound and the garbage collector can be relied on to clean it up as necessary. 8 | # One must be careful, though, when accessing entries since the values can be collected 9 | # at any time until there is a strong reference to them. 10 | # 11 | # === Example usage: 12 | # 13 | # cache = Ref::WeakValueMap.new 14 | # foo = "foo" 15 | # cache["strong"] = foo # add a value with a strong reference 16 | # cache["weak"] = "bar" # add a value without a strong reference 17 | # cache["strong"] # "foo" 18 | # cache["weak"] # "bar" 19 | # ObjectSpace.garbage_collect 20 | # cache["strong"] # "foo" 21 | # cache["weak"] # nil 22 | # 23 | # See AbstractReferenceValueMap for details. 24 | class WeakValueMap < AbstractReferenceValueMap 25 | self.reference_class = WeakReference 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/ref_ext.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-concurrency/ref/7a3991fea598edfe41d4e3a9e37b344bc7b7570d/lib/ref_ext.jar -------------------------------------------------------------------------------- /ref.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.join(File.dirname(__FILE__), 'lib') 2 | 3 | require 'ref/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'ref' 7 | s.version = Ref::VERSION 8 | s.authors = ['Brian Durand', 'The Ruby Concurrency Team'] 9 | s.email = ['bbdurand@gmail.com', 'concurrent-ruby@googlegroups.com'] 10 | s.homepage = "http://github.com/ruby-concurrency/ref" 11 | s.summary = "Library that implements weak, soft, and strong references in Ruby." 12 | s.description = "Library that implements weak, soft, and strong references in Ruby that work across multiple runtimes (MRI,Jruby and Rubinius). Also includes implementation of maps/hashes that use references and a reference queue." 13 | s.license = "MIT" 14 | s.date = Time.now.strftime('%Y-%m-%d') 15 | 16 | s.files = ['README.md', 'MIT_LICENSE'] 17 | s.files += Dir['lib/**/*.*'] 18 | s.files += Dir['ext/**/*.*'] 19 | s.files += Dir['test/**/*.*'] 20 | 21 | s.require_paths = ['lib'] 22 | 23 | s.has_rdoc = true 24 | s.rdoc_options = ["--charset=UTF-8", "--main", "README.md"] 25 | s.extra_rdoc_files = ["README.md"] 26 | 27 | s.required_ruby_version = '>= 1.9.3' 28 | end 29 | -------------------------------------------------------------------------------- /spec/ref/mock_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ref::Mock do 4 | 5 | context 'gc with argument' do 6 | specify do 7 | Ref::Mock.use do 8 | obj_1 = Object.new 9 | obj_2 = Object.new 10 | 11 | ref_1 = Ref::WeakReference.new(obj_1) 12 | ref_2 = Ref::WeakReference.new(obj_2) 13 | 14 | Ref::Mock.gc(obj_1) 15 | 16 | expect(ref_1.object).to be_nil 17 | expect(ref_2.object).to equal(obj_2) 18 | end 19 | end 20 | end 21 | 22 | context 'gc with no argument' do 23 | specify do 24 | Ref::Mock.use do 25 | obj_1 = Object.new 26 | obj_2 = Object.new 27 | 28 | ref_1 = Ref::WeakReference.new(obj_1) 29 | ref_2 = Ref::WeakReference.new(obj_2) 30 | 31 | Ref::Mock.gc 32 | 33 | expect(ref_1.object).to be_nil 34 | expect(ref_2.object).to be_nil 35 | end 36 | end 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /spec/ref/reference_queue_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ref::ReferenceQueue do 4 | 5 | let(:queue) { Ref::ReferenceQueue.new } 6 | let(:obj_1) { Object.new } 7 | let(:obj_2) { Object.new } 8 | let(:ref_1) { Ref::WeakReference.new(obj_1) } 9 | let(:ref_2) { Ref::WeakReference.new(obj_2) } 10 | 11 | describe '#push' do 12 | context 'when the queue is empty' do 13 | it { expect(queue.size).to be(0) } 14 | it { expect(queue).to be_empty } 15 | end 16 | 17 | context 'when the queue is not empty' do 18 | before do 19 | queue.push(ref_1) 20 | queue.push(ref_2) 21 | end 22 | 23 | it { expect(queue.size).to be(2) } 24 | it { expect(queue).to_not be_empty } 25 | end 26 | end 27 | 28 | describe '#shift' do 29 | context 'when the queue is not empty' do 30 | before do 31 | queue.push(ref_1) 32 | queue.push(ref_2) 33 | end 34 | it { expect(queue.shift).to be(ref_1) } 35 | end 36 | 37 | context 'when the queue is empty' do 38 | it { expect(queue.shift).to be_nil } 39 | end 40 | end 41 | 42 | describe '#pop' do 43 | context 'when the queue is not empty' do 44 | before do 45 | queue.push(ref_1) 46 | queue.push(ref_2) 47 | end 48 | 49 | it { expect(queue.pop).to be(ref_2) } 50 | end 51 | context 'when the queue is empty' do 52 | it { expect(queue.pop).to be_nil } 53 | end 54 | end 55 | 56 | context 'references are added immediately if the_object has been collected' do 57 | specify do 58 | Ref::Mock.use do 59 | ref = Ref::WeakReference.new(obj_1) 60 | Ref::Mock.gc(obj_1) 61 | queue.monitor(ref) 62 | 63 | expect(ref).to equal(queue.shift) 64 | end 65 | end 66 | end 67 | 68 | context 'references are added when the object has been collected' do 69 | specify do 70 | Ref::Mock.use do 71 | ref = Ref::WeakReference.new(obj_1) 72 | queue.monitor(ref) 73 | result = queue.shift 74 | expect(result).to eq nil 75 | 76 | Ref::Mock.gc(obj_1) 77 | 78 | object = queue.shift 79 | expect(ref.referenced_object_id).to eq object.referenced_object_id 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/ref/soft_key_map_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ref::SoftKeyMap do 4 | it_behaves_like 'a reference key map' do 5 | let(:map_class) { Ref::SoftKeyMap } 6 | let(:reference_class) { Ref::SoftReference } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/ref/soft_reference_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ref::SoftReference do 4 | describe '#object' do 5 | context 'when has not garbage collected objects' do 6 | it 'gets the object' do 7 | obj = Object.new 8 | ref = Ref::SoftReference.new(obj) 9 | 10 | expect(obj).to eq ref.object 11 | expect(obj.object_id).to eq ref.referenced_object_id 12 | end 13 | end 14 | 15 | context 'when has a lot of objects' do 16 | # Since we can't reliably control the garbage collector, this is a brute force test. 17 | # It might not always fail if the garbage collector and memory allocator don't 18 | # cooperate, but it should fail often enough on continuous integration to 19 | # hilite any problems. Set the environment variable QUICK_TEST to "true" if you 20 | # want to make the tests run quickly. 21 | it 'get the correct object' do 22 | id_to_ref = {} 23 | (ENV["QUICK_TEST"] == "true" ? 1000 : 100000).times do |i| 24 | obj = Object.new 25 | if id_to_ref.key?(obj.object_id) 26 | ref = id_to_ref[obj.object_id] 27 | if ref.object 28 | fail "soft reference found with a live reference to an object that was not the one it was created with" 29 | break 30 | end 31 | end 32 | 33 | %w(Here are a bunch of objects that are allocated and can then be cleaned up by the garbage collector) 34 | id_to_ref[obj.object_id] = Ref::SoftReference.new(obj) 35 | if i % 1000 == 0 36 | GC.start 37 | sleep(0.01) 38 | end 39 | end 40 | end 41 | end 42 | 43 | context 'when references are not collected immediately' do 44 | it "the object can't be nil" do 45 | ref = Ref::SoftReference.new(Object.new) 46 | 9.times{ arr = %w(allocate some memory on the heap); arr *= 100; GC.start } 47 | expect(ref.object).to_not be_nil 48 | end 49 | end 50 | end 51 | 52 | describe '#inspect' do 53 | context 'when GC is called' do 54 | it 'inspects not be nil' do 55 | ref = Ref::SoftReference.new(Object.new) 56 | expect(ref.inspect).to_not be_nil 57 | GC.start 58 | GC.start 59 | expect(ref.inspect).to_not be_nil 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/ref/soft_value_map_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ref::SoftValueMap do 4 | it_behaves_like 'a reference value map' do 5 | let(:map_class) { Ref::SoftValueMap } 6 | let(:reference_class) { Ref::SoftReference } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/ref/strong_reference_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ref::StrongReference do 4 | let(:obj) { Object.new } 5 | let(:ref) { Ref::StrongReference.new(obj) } 6 | 7 | context '#object' do 8 | it { expect(obj).to equal(ref.object) } 9 | it { expect(obj.object_id).to equal(ref.referenced_object_id) } 10 | end 11 | 12 | context '#inspect' do 13 | it { expect(ref.inspect).to_not be_empty } 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/ref/weak_key_map_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ref::WeakKeyMap do 4 | it_behaves_like 'a reference key map' do 5 | let(:map_class) { Ref::WeakKeyMap } 6 | let(:reference_class) { Ref::WeakReference } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/ref/weak_reference_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ref::WeakReference do 4 | describe '#object' do 5 | context 'when has not garbage collected objects' do 6 | it 'gets the object' do 7 | obj = Object.new 8 | ref_1 = Ref::WeakReference.new(obj) 9 | ref_2 = Ref::WeakReference.new(obj) 10 | 11 | expect(obj).to eq ref_1.object 12 | expect(obj.object_id).to eq ref_1.referenced_object_id 13 | expect(obj).to eq ref_2.object 14 | expect(obj.object_id).to eq ref_2.referenced_object_id 15 | end 16 | end 17 | 18 | context 'when has a lot of objects' do 19 | # Since we can't reliably control the garbage collector, this is a brute force test. 20 | # It might not always fail if the garbage collector and memory allocator don't 21 | # cooperate, but it should fail often enough on continuous integration to 22 | # hilite any problems. Set the environment variable QUICK_TEST to "true" if you 23 | # want to make the tests run quickly. 24 | it 'get the correct object' do 25 | id_to_ref = {} 26 | (ENV["QUICK_TEST"] == "true" ? 1000 : 100000).times do |i| 27 | obj = Object.new 28 | if id_to_ref.key?(obj.object_id) 29 | ref = id_to_ref[obj.object_id] 30 | if ref.object 31 | fail "weak reference found with a live reference to an object that was not the one it was created with" 32 | break 33 | end 34 | end 35 | %w(Here are a bunch of objects that are allocated and can then be cleaned up by the garbage collector) 36 | id_to_ref[obj.object_id] = Ref::WeakReference.new(obj) 37 | if i % 1000 == 0 38 | GC.start 39 | sleep(0.01) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | 46 | describe '#inspect' do 47 | context 'when GC is called' do 48 | it 'inspects not be nil' do 49 | ref = Ref::SoftReference.new(Object.new) 50 | expect(ref.inspect).to_not be_nil 51 | GC.start 52 | GC.start 53 | expect(ref.inspect).to_not be_nil 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/ref/weak_value_map_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ref::WeakValueMap do 4 | it_behaves_like 'a reference value map' do 5 | let(:map_class) { Ref::WeakValueMap } 6 | let(:reference_class) { Ref::WeakReference } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/shared/mock.rb: -------------------------------------------------------------------------------- 1 | module Ref 2 | # This module provides mock weak and strong references that are designed to be 3 | # used in tests. You can define a block where all weak and soft references created 4 | # will be mock references. You can then mimic running the garbage collector on 5 | # the objects pointed to by the references. 6 | # 7 | # Example usage: 8 | # 9 | # Ref::Mock.use do 10 | # obj = Object.new 11 | # ref = Ref::WeakReference.new(obj) 12 | # ref.object # obj 13 | # Ref::Mock.gc(obj) # mimics the garbage collector reclaiming the referenced object 14 | # ref.object # nil 15 | # end 16 | module Mock 17 | class << self 18 | # Use the mock implementation inside a block and then restore the original implementation. 19 | def use 20 | if object_space 21 | yield 22 | else 23 | setup 24 | begin 25 | yield 26 | ensure 27 | cleanup 28 | end 29 | end 30 | end 31 | 32 | # Start using mock references. 33 | def setup 34 | raise "Ref::Mock already setup" if object_space 35 | 36 | @object_space = {} 37 | 38 | class << ObjectSpace 39 | unless method_defined?(:define_finalizer_with_mock_reference) 40 | def define_finalizer_with_mock_reference(obj, finalizer) 41 | if ::Ref::Mock.object_space.include?(obj.__id__) 42 | ::Ref::Mock.object_space[obj.__id__] << finalizer 43 | else 44 | define_finalizer_without_mock_reference(obj, finalizer) 45 | end 46 | end 47 | end 48 | 49 | alias_method :define_finalizer_without_mock_reference, :define_finalizer 50 | alias_method :define_finalizer, :define_finalizer_with_mock_reference 51 | end 52 | 53 | class << WeakReference 54 | unless method_defined?(:new_with_mock_reference) 55 | def new_with_mock_reference(obj) 56 | if self == Mock::MockWeakReference 57 | new_without_mock_reference(obj) 58 | else 59 | Mock::MockWeakReference.new(obj) 60 | end 61 | end 62 | end 63 | 64 | alias_method :new_without_mock_reference, :new 65 | alias_method :new, :new_with_mock_reference 66 | end 67 | 68 | class << SoftReference 69 | unless method_defined?(:new_with_mock_reference) 70 | def new_with_mock_reference(obj) 71 | if self == Mock::MockSoftReference 72 | new_without_mock_reference(obj) 73 | else 74 | Mock::MockSoftReference.new(obj) 75 | end 76 | end 77 | end 78 | 79 | alias_method :new_without_mock_reference, :new 80 | alias_method :new, :new_with_mock_reference 81 | end 82 | end 83 | 84 | # Stop using mock references. 85 | def cleanup 86 | @object_space = nil 87 | class << ObjectSpace 88 | alias_method :define_finalizer_with_mock_reference, :define_finalizer 89 | alias_method :define_finalizer, :define_finalizer_without_mock_reference 90 | end 91 | 92 | class << WeakReference 93 | alias_method :new_with_mock_reference, :new 94 | alias_method :new, :new_without_mock_reference 95 | end 96 | 97 | class << SoftReference 98 | alias_method :new_with_mock_reference, :new 99 | alias_method :new, :new_without_mock_reference 100 | end 101 | end 102 | 103 | def object_space # :nodoc: 104 | @object_space if instance_variable_defined?(:@object_space) 105 | end 106 | 107 | # Simulate garbage collection of the objects passed in as arguments. If no objects 108 | # are specified, all objects will be reclaimed. 109 | def gc(*objects) 110 | objects = if objects.empty? 111 | object_space.keys 112 | else 113 | objects.map { |obj| obj.__id__ } 114 | end 115 | 116 | objects.each do |id| 117 | finalizers = object_space.delete(id) 118 | if finalizers 119 | finalizers.each{|finalizer| finalizer.call(id)} 120 | end 121 | end 122 | end 123 | end 124 | 125 | module MockReference #:nodoc: 126 | def initialize(obj) 127 | @object = obj 128 | @referenced_object_id = obj.__id__ 129 | raise "Reference::Mock not setup yet" unless Mock.object_space 130 | Mock.object_space[obj.__id__] ||= [] 131 | end 132 | 133 | def object 134 | if @object && Mock.object_space.include?(@object.__id__) 135 | @object 136 | else 137 | @object = nil 138 | end 139 | end 140 | end 141 | 142 | class MockWeakReference < WeakReference #:nodoc: 143 | include MockReference 144 | end 145 | 146 | class MockSoftReference < SoftReference #:nodoc: 147 | include MockReference 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /spec/shared/reference_key_map_shared.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'a reference key map' do 4 | let(:value_1) { 'value 1' } 5 | let(:value_2) { 'value 2' } 6 | let(:value_3) { 'value 3' } 7 | let(:key_1) { Object.new } 8 | let(:key_2) { Object.new } 9 | let(:key_3) { Object.new } 10 | let(:hash) { map_class.new } 11 | 12 | context 'keeps entries with strong references' do 13 | specify do 14 | Ref::Mock.use do 15 | hash[key_1] = "value 1" 16 | hash[key_2] = "value 2" 17 | expect(hash[key_1]).to eq "value 1" 18 | expect(hash[key_2]).to eq "value 2" 19 | end 20 | end 21 | end 22 | 23 | context 'removes entries that have been garbage collected' do 24 | specify do 25 | Ref::Mock.use do 26 | hash[key_1] = value_1 27 | hash[key_2] = value_2 28 | expect(hash[key_1]).to eq(value_1) 29 | expect(hash[key_2]).to eq(value_2) 30 | Ref::Mock.gc(key_2) 31 | expect(hash[key_1]).to eq(value_1) 32 | expect(hash[key_2]).to be_nil 33 | end 34 | end 35 | end 36 | 37 | context 'can clear the map' do 38 | specify do 39 | Ref::Mock.use do 40 | hash[key_1] = value_1 41 | hash[key_2] = value_2 42 | hash.clear 43 | expect(hash[key_1]).to be_nil 44 | expect(hash[key_2]).to be_nil 45 | end 46 | end 47 | end 48 | 49 | context 'can delete entries' do 50 | specify do 51 | Ref::Mock.use do 52 | hash[key_1] = value_1 53 | hash[key_2] = value_2 54 | Ref::Mock.gc(key_2) 55 | expect(hash.delete(key_2)).to be_nil 56 | expect(hash.delete(key_1)).to eq(value_1) 57 | expect(hash[key_1]).to be_nil 58 | end 59 | end 60 | end 61 | 62 | context 'can merge in another hash' do 63 | specify do 64 | Ref::Mock.use do 65 | hash[key_1] = value_1 66 | hash[key_2] = value_2 67 | hash.merge!(key_3 => value_3) 68 | 69 | expect(hash[key_2]).to eq 'value 2' 70 | expect(hash[key_1]).to eq value_1 71 | 72 | Ref::Mock.gc(key_2) 73 | 74 | expect(hash[key_2]).to be_nil 75 | expect(hash[key_1]).to eq value_1 76 | expect(hash[key_3]).to eq value_3 77 | end 78 | end 79 | end 80 | 81 | context 'can get all keys' do 82 | specify do 83 | Ref::Mock.use do 84 | hash[key_1] = value_1 85 | hash[key_2] = value_2 86 | hash[key_3] = value_3 87 | expect([key_1, key_2, key_3] - hash.keys).to eq [] 88 | Ref::Mock.gc(key_2) 89 | expect([key_1, key_2, key_3] - hash.keys).to eq [key_2] 90 | end 91 | end 92 | end 93 | 94 | context 'can turn into an array' do 95 | specify do 96 | Ref::Mock.use do 97 | hash[key_1] = value_1 98 | hash[key_2] = value_2 99 | hash[key_3] = value_3 100 | order = lambda{|a,b| a.last <=> b.last} 101 | expect([[key_1, "value 1"], [key_2, "value 2"], [key_3, "value 3"]].sort(&order)).to eq hash.to_a.sort(&order) 102 | Ref::Mock.gc(key_2) 103 | expect([[key_1, "value 1"], [key_3, "value 3"]].sort(&order)).to eq hash.to_a.sort(&order) 104 | end 105 | end 106 | end 107 | 108 | context 'can turn into a hash' do 109 | specify do 110 | Ref::Mock.use do 111 | hash[key_1] = value_1 112 | hash[key_2] = value_2 113 | hash[key_3] = value_3 114 | order = lambda{|a,b| a.last <=> b.last} 115 | expect({key_1 => "value 1", key_2 => "value 2", key_3 => "value 3"}.sort(&order)).to eq hash.to_h.sort(&order) 116 | Ref::Mock.gc(key_2) 117 | expect({key_1 => "value 1", key_3 => "value 3"}.sort(&order)).to eq hash.to_h.sort(&order) 118 | end 119 | end 120 | end 121 | 122 | context 'can interate over all entries' do 123 | specify do 124 | Ref::Mock.use do 125 | hash[key_1] = value_1 126 | hash[key_2] = value_2 127 | hash[key_3] = value_3 128 | keys = [] 129 | values = [] 130 | hash.each{|k,v| keys << k; values << v} 131 | expect([key_1, key_2, key_3] - keys).to eq [] 132 | expect(["value 1", "value 2", "value 3"]).to eq values.sort 133 | Ref::Mock.gc(key_2) 134 | keys = [] 135 | values = [] 136 | hash.each{|k,v| keys << k; values << v} 137 | expect([key_1, key_2, key_3] - keys).to eq [key_2] 138 | expect(["value 1", "value 3"]).to eq values.sort 139 | end 140 | end 141 | end 142 | 143 | context 'size' do 144 | specify do 145 | Ref::Mock.use do 146 | hash = map_class.new 147 | expect(hash.empty?).to eq true 148 | expect(hash.size).to eq 0 149 | key_1 = Object.new 150 | key_2 = Object.new 151 | hash[key_1] = "value 1" 152 | hash[key_2] = "value 2" 153 | expect(hash.size).to eq 2 154 | Ref::Mock.gc(key_2) 155 | expect(hash.empty?).to eq false 156 | expect(hash.size).to eq 1 157 | end 158 | end 159 | end 160 | 161 | context 'inspect' do 162 | specify do 163 | Ref::Mock.use do 164 | hash[Object.new] = "value 1" 165 | expect(hash.inspect).to_not be_nil 166 | end 167 | end 168 | end 169 | 170 | context '#merge' do 171 | 172 | let(:new_value){ Object.new } 173 | let(:values){ {key_1 => value_1, key_2 => value_2, key_3 => value_3} } 174 | let(:this) do 175 | values.each{|k, v| hash[k] = v } 176 | hash 177 | end 178 | let(:other){ {key_3 => new_value} } 179 | 180 | it 'updates all members with the new values from a given hash' do 181 | expect(this.merge(other)[key_3]).to eq new_value 182 | end 183 | 184 | it 'calls the given block for each key in `other`' do 185 | actual = 0 186 | this.merge(key_2 => 'yes', key_3 => 'no'){|member, thisval, otherval| actual += 1; Object.new } 187 | expect(actual).to eq 2 188 | end 189 | 190 | it 'retains the value for all members without values in the given hash' do 191 | expect(this.merge(other)[key_1]).to eq value_1 192 | end 193 | 194 | it 'adds members not in the original hash' do 195 | new_key = Object.new 196 | new_value = 'life, the universe, and everything' 197 | expect(this.merge(new_key => new_value)[new_key]).to eq new_value 198 | end 199 | 200 | it 'returns a deep copy when merging an empty hash' do 201 | other = this.merge({}) 202 | values.each do |key, value| 203 | expect(other[key]).to eq value 204 | end 205 | end 206 | 207 | it 'returns a new object' do 208 | expect(this.merge(other).object_id).to_not eq this.object_id 209 | expect(this.merge(other).object_id).to_not eq other.object_id 210 | end 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /spec/shared/reference_value_map_shared.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'a reference value map' do 4 | let(:key_1) { 'key 1' } 5 | let(:key_2) { 'key 2' } 6 | let(:key_3) { 'key 3' } 7 | let(:value_1) { 'value 1' } 8 | let(:value_2) { 'value 2' } 9 | let(:value_3) { 'value 3' } 10 | let(:hash) { map_class.new } 11 | 12 | context 'keeps entries with strong references' do 13 | specify do 14 | Ref::Mock.use do 15 | hash['key 1'] = value_1 16 | hash['key 2'] = value_2 17 | expect(hash['key 1'] ).to eq(value_1) 18 | expect(hash['key 2'] ).to eq(value_2) 19 | end 20 | end 21 | end 22 | 23 | context 'removes entries that have been garbage collected' do 24 | specify do 25 | Ref::Mock.use do 26 | hash['key 1'] = value_1 27 | hash['key 2'] = value_2 28 | expect(hash['key 1']).to eq(value_1) 29 | expect(hash['key 2']).to eq(value_2) 30 | Ref::Mock.gc(value_2) 31 | expect(value_1).to eq(value_1) 32 | expect(hash['key 2']).to be_nil 33 | end 34 | end 35 | end 36 | 37 | context 'can clear the map' do 38 | specify do 39 | hash['key 1'] = value_1 40 | hash['key 2'] = value_2 41 | hash.clear 42 | expect(hash['key 1']).to be_nil 43 | expect(hash['key 2']).to be_nil 44 | end 45 | end 46 | 47 | context 'can delete entries' do 48 | specify do 49 | Ref::Mock.use do 50 | hash["key 1"] = value_1 51 | hash["key 2"] = value_2 52 | Ref::Mock.gc(value_2) 53 | expect(hash.delete("key 2")).to be_nil 54 | expect(hash.delete("key 1")).to eq value_1 55 | expect(hash["key 1"]).to be_nil 56 | end 57 | end 58 | end 59 | 60 | context 'can merge in another hash' do 61 | specify do 62 | Ref::Mock.use do 63 | hash["key 1"] = value_1 64 | hash["key 2"] = value_2 65 | hash.merge!("key 3" => value_3) 66 | 67 | expect('value 2').to eq hash['key 2'] 68 | expect(hash['key 1']).to eq value_1 69 | 70 | Ref::Mock.gc(value_2) 71 | 72 | expect(hash['key 2']).to be_nil 73 | expect(hash['key 1']).to eq value_1 74 | expect(hash['key 3']).to eq value_3 75 | end 76 | end 77 | end 78 | 79 | context 'can get all values' do 80 | specify do 81 | Ref::Mock.use do 82 | hash["key 1"] = value_1 83 | hash["key 2"] = value_2 84 | hash["key 3"] = value_3 85 | expect(["value 1", "value 2", "value 3"].sort).to eq hash.values.sort 86 | Ref::Mock.gc(value_2) 87 | expect(["value 1", "value 3"].sort).to eq hash.values.sort 88 | end 89 | end 90 | end 91 | 92 | context 'can turn into an array' do 93 | specify do 94 | Ref::Mock.use do 95 | hash["key 1"] = value_1 96 | hash["key 2"] = value_2 97 | hash["key 3"] = value_3 98 | order = lambda{|a,b| a.first <=> b.first} 99 | expect([["key 1", "value 1"], ["key 2", "value 2"], ["key 3", "value 3"]].sort(&order)).to eq hash.to_a.sort(&order) 100 | Ref::Mock.gc(value_2) 101 | expect([["key 1", "value 1"], ["key 3", "value 3"]].sort(&order)).to eq hash.to_a.sort(&order) 102 | end 103 | end 104 | end 105 | 106 | context 'can turn into a hash' do 107 | specify do 108 | Ref::Mock.use do 109 | hash["key 1"] = value_1 110 | hash["key 2"] = value_2 111 | hash["key 3"] = value_3 112 | order = lambda{|a,b| a.first <=> b.first} 113 | expect({"key 1" => "value 1", "key 2" => "value 2", "key 3" => "value 3"}.sort(&order)).to eq hash.to_a.sort(&order) 114 | Ref::Mock.gc(value_2) 115 | expect({"key 1" => "value 1", "key 3" => "value 3"}.sort(&order)).to eq hash.to_a.sort(&order) 116 | end 117 | end 118 | end 119 | 120 | context 'can interate over all entries' do 121 | specify do 122 | Ref::Mock.use do 123 | hash["key 1"] = value_1 124 | hash["key 2"] = value_2 125 | hash["key 3"] = value_3 126 | keys = [] 127 | values = [] 128 | hash.each{|k,v| keys << k; values << v} 129 | expect(["key 1", "key 2", "key 3"]).to eq keys.sort 130 | expect(["value 1", "value 2", "value 3"]).to eq values.sort 131 | Ref::Mock.gc(value_2) 132 | keys = [] 133 | values = [] 134 | hash.each{|k,v| keys << k; values << v} 135 | expect(["key 1", "key 3"]).to eq keys.sort 136 | expect(["value 1", "value 3"]).to eq values.sort 137 | end 138 | end 139 | end 140 | 141 | context 'size' do 142 | specify do 143 | Ref::Mock.use do 144 | hash = map_class.new 145 | expect(hash.empty?).to eq true 146 | expect(hash.size).to eq 0 147 | value_1 = "value 1" 148 | value_2 = "value 2" 149 | hash["key 1"] = value_1 150 | hash["key 2"] = value_2 151 | expect(hash.size).to eq 2 152 | Ref::Mock.gc(value_2) 153 | expect(hash.empty?).to eq false 154 | expect(hash.size).to eq 1 155 | end 156 | end 157 | end 158 | 159 | context 'inspect' do 160 | specify do 161 | Ref::Mock.use do 162 | hash["key 1"] = "value 1" 163 | expect(hash.inspect).to_not be_nil 164 | end 165 | end 166 | end 167 | 168 | context '#merge' do 169 | 170 | let(:new_value){ Object.new } 171 | let(:values){ {key_1 => value_1, key_2 => value_2, key_3 => value_3} } 172 | let(:this) do 173 | values.each{|k, v| hash[k] = v } 174 | hash 175 | end 176 | let(:other){ {key_3 => new_value} } 177 | 178 | it 'updates all members with the new values from a given hash' do 179 | expect(this.merge(other)[key_3]).to eq new_value 180 | end 181 | 182 | it 'calls the given block for each key in `other`' do 183 | actual = 0 184 | this.merge(key_2 => 'yes', key_3 => 'no'){|member, thisval, otherval| actual += 1; Object.new } 185 | expect(actual).to eq 2 186 | end 187 | 188 | it 'retains the value for all members without values in the given hash' do 189 | expect(this.merge(other)[key_1]).to eq value_1 190 | end 191 | 192 | it 'adds members not in the original hash' do 193 | new_key = Object.new 194 | new_value = 'life, the universe, and everything' 195 | expect(this.merge(new_key => new_value)[new_key]).to eq new_value 196 | end 197 | 198 | it 'returns a deep copy when merging an empty hash' do 199 | other = this.merge({}) 200 | values.each do |key, value| 201 | expect(other[key]).to eq value 202 | end 203 | end 204 | 205 | it 'returns a new object' do 206 | expect(this.merge(other).object_id).to_not eq this.object_id 207 | expect(this.merge(other).object_id).to_not eq other.object_id 208 | end 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'coveralls' 3 | 4 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ 5 | SimpleCov::Formatter::HTMLFormatter, 6 | Coveralls::SimpleCov::Formatter 7 | ]) 8 | 9 | SimpleCov.start do 10 | project_name 'ref' 11 | add_filter '/coverage/' 12 | add_filter '/doc/' 13 | add_filter '/pkg/' 14 | add_filter '/spec/' 15 | add_filter '/tasks/' 16 | end 17 | 18 | require 'ref' 19 | 20 | # import all the support files 21 | Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |f| require File.expand_path(f) } 22 | Dir[File.join(File.dirname(__FILE__), 'shared/**/*.rb')].each { |f| require File.expand_path(f) } 23 | 24 | RSpec.configure do |config| 25 | config.order = 'random' 26 | end 27 | --------------------------------------------------------------------------------