Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Rip out Veritas::Immutable into a standalone gem.

  • Loading branch information...
commit 8b4ee56586008bee94117573fe431d2f76bbac51 0 parents
Markus Schirp authored
Showing with 1,337 additions and 0 deletions.
  1. +4 −0 .gitignore
  2. +53 −0 Gemfile
  3. +18 −0 Guardfile
  4. +9 −0 Rakefile
  5. +3 −0  config/flay.yml
  6. +2 −0  config/flog.yml
  7. +18 −0 config/roodi.yml
  8. +91 −0 config/site.reek
  9. +2 −0  config/yardstick.yml
  10. +20 −0 immutable.gemspec
  11. +247 −0 lib/immutable.rb
  12. +3 −0  lib/immutable/version.rb
  13. +7 −0 spec/rcov.opts
  14. +7 −0 spec/shared/command_method_behavior.rb
  15. +15 −0 spec/shared/each_method_behaviour.rb
  16. +17 −0 spec/shared/hash_method_behavior.rb
  17. +7 −0 spec/shared/idempotent_method_behavior.rb
  18. +9 −0 spec/shared/invertible_method_behaviour.rb
  19. +11 −0 spec/spec_helper.rb
  20. +57 −0 spec/unit/immutable/class_methods/freeze_object_spec.rb
  21. +14 −0 spec/unit/immutable/class_methods/new_spec.rb
  22. +13 −0 spec/unit/immutable/dup_spec.rb
  23. +28 −0 spec/unit/immutable/fixtures/classes.rb
  24. +51 −0 spec/unit/immutable/freeze_spec.rb
  25. +57 −0 spec/unit/immutable/memoize_spec.rb
  26. +29 −0 spec/unit/immutable/memoized_spec.rb
  27. +16 −0 spec/unit/immutable/module_methods/included_spec.rb
  28. +88 −0 spec/unit/immutable/module_methods/memoize_spec.rb
  29. +7 −0 tasks/metrics/ci.rake
  30. +47 −0 tasks/metrics/flay.rake
  31. +43 −0 tasks/metrics/flog.rake
  32. +208 −0 tasks/metrics/heckle.rake
  33. +29 −0 tasks/metrics/metric_fu.rake
  34. +15 −0 tasks/metrics/reek.rake
  35. +15 −0 tasks/metrics/roodi.rake
  36. +23 −0 tasks/metrics/yardstick.rake
  37. +45 −0 tasks/spec.rake
  38. +9 −0 tasks/yard.rake
4 .gitignore
@@ -0,0 +1,4 @@
+/Gemfile.lock
+/coverage
+/tmp
+/.rbx
53 Gemfile
@@ -0,0 +1,53 @@
+# encoding: utf-8
+
+source 'https://rubygems.org'
+
+gem 'backports', '~> 2.6.1'
+
+group :development do
+ gem 'jeweler', '~> 1.8.3'
+ gem 'rake', '~> 0.9.2'
+ gem 'rspec', '~> 1.3.2'
+ gem 'yard', '~> 0.8.1'
+end
+
+group :guard do
+ gem 'guard', '~> 1.1.1'
+ gem 'guard-bundler', '~> 0.1.3'
+ gem 'guard-rspec', '~> 0.7.3'
+end
+
+group :benchmarks do
+ gem 'rbench', '~> 0.2.3'
+end
+
+platform :jruby do
+ group :jruby do
+ gem 'jruby-openssl', '~> 0.7.4'
+ end
+end
+
+group :metrics do
+ gem 'flay', '~> 1.4.2'
+ gem 'flog', '~> 2.5.1'
+ gem 'reek', '~> 1.2.8', :github => 'dkubb/reek'
+ gem 'roodi', '~> 2.1.0'
+ gem 'yardstick', '~> 0.5.0'
+ gem 'yard-spellcheck', '~> 0.1.5'
+
+ platforms :mri_18 do
+ gem 'arrayfields', '~> 4.7.4' # for metric_fu
+ gem 'fattr', '~> 2.2.0' # for metric_fu
+ gem 'heckle', '~> 1.4.3'
+ gem 'json', '~> 1.7.3' # for metric_fu rake task
+ gem 'map', '~> 6.0.1' # for metric_fu
+ gem 'metric_fu', '~> 2.1.1'
+ gem 'mspec', '~> 1.5.17'
+ gem 'rcov', '~> 1.0.0'
+ gem 'ruby2ruby', '= 1.2.2' # for heckle
+ end
+
+ platforms :rbx do
+ gem 'pelusa', '~> 0.2.1'
+ end
+end
18 Guardfile
@@ -0,0 +1,18 @@
+# encoding: utf-8
+
+guard :bundler do
+ watch('Gemfile')
+end
+
+guard :rspec do
+ # run all specs if the spec_helper or supporting files files are modified
+ watch('spec/spec_helper.rb') { 'spec' }
+ watch(%r{\Aspec/(?:lib|support|shared)/.+\.rb\z}) { 'spec' }
+
+ # run unit specs if associated lib code is modified
+ watch(%r{\Alib/(.+)\.rb\z}) { |m| Dir["spec/unit/#{m[1]}"] }
+ watch("lib/#{File.basename(File.expand_path('../', __FILE__))}.rb") { 'spec' }
+
+ # run a spec if it is modified
+ watch(%r{\Aspec/(?:unit|integration)/.+_spec\.rb\z})
+end
9 Rakefile
@@ -0,0 +1,9 @@
+# encoding: utf-8
+
+require 'rake'
+
+require File.expand_path('../lib/immutable/version', __FILE__)
+
+FileList['tasks/**/*.rake'].each { |task| import task }
+
+task :default => :spec
3  config/flay.yml
@@ -0,0 +1,3 @@
+---
+threshold: 9
+total_score: 29
2  config/flog.yml
@@ -0,0 +1,2 @@
+---
+threshold: 21.9
18 config/roodi.yml
@@ -0,0 +1,18 @@
+---
+AbcMetricMethodCheck: { score: 10.3 }
+AssignmentInConditionalCheck: { }
+CaseMissingElseCheck: { }
+ClassLineCountCheck: { line_count: 293 }
+ClassNameCheck: { pattern: !ruby/regexp '/\A(?:[A-Z]+|[A-Z][a-z](?:[A-Z]?[a-z])+)\z/' }
+ClassVariableCheck: { }
+CyclomaticComplexityBlockCheck: { complexity: 2 }
+CyclomaticComplexityMethodCheck: { complexity: 4 }
+EmptyRescueBodyCheck: { }
+ForLoopCheck: { }
+# TODO: decrease line_count to 5 to 10
+MethodLineCountCheck: { line_count: 14 }
+MethodNameCheck: { pattern: !ruby/regexp '/\A(?:[a-z\d](?:_?[a-z\d])+[?!=]?|\[\]=?|==|<=>|[+*&|-])\z/' }
+ModuleLineCountCheck: { line_count: 295 }
+ModuleNameCheck: { pattern: !ruby/regexp '/\A(?:[A-Z]+|[A-Z][a-z](?:[A-Z]?[a-z])+)\z/' }
+# TODO: decrease parameter_count to 2 or less
+ParameterNumberCheck: { parameter_count: 3 }
91 config/site.reek
@@ -0,0 +1,91 @@
+---
+UncommunicativeParameterName:
+ accept: []
+ exclude: []
+ enabled: true
+ reject:
+ - !ruby/regexp /^.$/
+ - !ruby/regexp /[0-9]$/
+ - !ruby/regexp /[A-Z]/
+LargeClass:
+ max_methods: 10
+ exclude: []
+ enabled: true
+ max_instance_variables: 2
+UncommunicativeMethodName:
+ accept: []
+ exclude: []
+ enabled: true
+ reject:
+ - !ruby/regexp /^[a-z]$/
+ - !ruby/regexp /[0-9]$/
+ - !ruby/regexp /[A-Z]/
+LongParameterList:
+ max_params: 2 # TODO: decrease max_params to 2
+ exclude: []
+ enabled: true
+ overrides: {}
+FeatureEnvy:
+ exclude: []
+ enabled: true
+ClassVariable:
+ exclude: []
+ enabled: true
+BooleanParameter:
+ exclude: []
+ enabled: true
+IrresponsibleModule:
+ exclude: []
+ enabled: true
+UncommunicativeModuleName:
+ accept: []
+ exclude: []
+ enabled: true
+ reject:
+ - !ruby/regexp /^.$/
+ - !ruby/regexp /[0-9]$/
+NestedIterators:
+ ignore_iterators: []
+ exclude: []
+ enabled: true
+ max_allowed_nesting: 1
+LongMethod:
+ max_statements: 7 # TODO: decrease max_statements to 5 or less
+ exclude: []
+ enabled: true
+Duplication:
+ allow_calls: []
+ exclude: []
+ enabled: true
+ max_calls: 1
+UtilityFunction:
+ max_helper_calls: 1
+ exclude: []
+ enabled: true
+Attribute:
+ exclude: []
+ enabled: false
+UncommunicativeVariableName:
+ accept: []
+ exclude: []
+ enabled: true
+ reject:
+ - !ruby/regexp /^.$/
+ - !ruby/regexp /[0-9]$/
+ - !ruby/regexp /[A-Z]/
+SimulatedPolymorphism:
+ exclude: []
+ enabled: true
+ max_ifs: 1
+DataClump:
+ exclude: []
+ enabled: true
+ max_copies: 1
+ min_clump_size: 3
+ControlCouple:
+ exclude: []
+ enabled: true
+LongYieldList:
+ max_params: 1
+ exclude: []
+ enabled: true
2  config/yardstick.yml
@@ -0,0 +1,2 @@
+---
+threshold: 100
20 immutable.gemspec
@@ -0,0 +1,20 @@
+# -*- encoding: utf-8 -*-
+
+require File.expand_path('../lib/immutable/version', __FILE__)
+
+Gem::Specification.new do |gem|
+ gem.name = 'immutable'
+ gem.version = Virtus::VERSION
+ gem.authors = [ 'Dan Kubb', 'Markus Schirp' ]
+ gem.email = [ 'dan.kubb@gmail.com', 'mbj@seonic.net' ]
+ gem.description = 'Immutable extensions to objects'
+ gem.summary = gem.description
+ gem.homepage = 'https://github.com/mbj/immutable'
+
+ gem.require_paths = [ 'lib' ]
+ gem.files = `git ls-files`.split("\n")
+ gem.test_files = `git ls-files -- {spec}/*`.split("\n")
+ gem.extra_rdoc_files = %w[LICENSE README.md TODO]
+
+ gem.add_runtime_dependency('backports', '~> 2.6.1')
+end
247 lib/immutable.rb
@@ -0,0 +1,247 @@
+# Allows objects to be made immutable
+module Immutable
+
+ # Storage for memoized methods
+ Memory = Class.new(::Hash)
+
+ # Hook called when module is included
+ #
+ # @param [Module] descendant
+ # the module or class including Immutable
+ #
+ # @return [self]
+ #
+ # @api private
+ def self.included(descendant)
+ super
+ descendant.extend ModuleMethods if descendant.kind_of?(Module)
+ descendant.extend ClassMethods if descendant.kind_of?(Class)
+ self
+ end
+
+ # Attempt to freeze an object
+ #
+ # @example using a value object
+ # Immutable.freeze_object(12345) # => noop
+ #
+ # @example using a normal object
+ # Immutable.freeze_object({}) # => duplicate & freeze object
+ #
+ # @param [Object] object
+ # the object to freeze
+ #
+ # @return [Object]
+ # if supported, the frozen object, otherwise the object directly
+ #
+ # @api public
+ def self.freeze_object(object)
+ case object
+ when Numeric, TrueClass, FalseClass, NilClass, Symbol
+ object
+ else
+ freeze_value(object)
+ end
+ end
+
+ # Returns a frozen value
+ #
+ # @param [Object] value
+ # a value to freeze
+ #
+ # @return [Object]
+ # if frozen, the value directly, otherwise a frozen copy of the value
+ #
+ # @api private
+ def self.freeze_value(value)
+ value.frozen? ? value : value.dup.freeze
+ end
+
+ private_class_method :freeze_value
+
+ # Freeze the object
+ #
+ # @example
+ # object.freeze # object is now frozen
+ #
+ # @return [Object]
+ #
+ # @api public
+ def freeze
+ memory # initialize memory
+ super
+ end
+
+ # Get the memoized value for a method
+ #
+ # @example
+ # hash = object.memoized(:hash)
+ #
+ # @param [Symbol] name
+ # the method name
+ #
+ # @return [Object]
+ #
+ # @api public
+ def memoized(name)
+ memory[name]
+ end
+
+ # Sets a memoized value for a method
+ #
+ # @example
+ # object.memoize(:hash, 12345)
+ #
+ # @param [Symbol] name
+ # the method name
+ # @param [Object] value
+ # the value to memoize
+ #
+ # @return [self]
+ #
+ # @api public
+ def memoize(name, value)
+ store_memory(name, value) unless memory.key?(name)
+ self
+ end
+
+ # A noop #dup for immutable objects
+ #
+ # @example
+ # object.dup # => self
+ #
+ # @return [self]
+ #
+ # @api public
+ def dup
+ self
+ end
+
+private
+
+ # The memoized method results
+ #
+ # @return [Hash]
+ #
+ # @api private
+ def memory
+ @__memory ||= Memory.new
+ end
+
+ # Store the value in memory
+ #
+ # @param [Symbol] name
+ # the method name
+ # @param [Object] value
+ # the value to memoize
+ #
+ # @return [self]
+ #
+ # @return [value]
+ #
+ # @api private
+ def store_memory(name, value)
+ memory[name] = Immutable.freeze_object(value)
+ end
+
+ # Methods mixed in to immutable modules
+ module ModuleMethods
+
+ # Hook called when module is included
+ #
+ # @param [Module] mod
+ # the module including ModuleMethods
+ #
+ # @return [self]
+ #
+ # @api private
+ def included(mod)
+ Immutable.included(mod)
+ self
+ end
+
+ # Memoize a list of methods
+ #
+ # @example
+ # memoize :hash
+ #
+ # @param [Array<#to_s>] *methods
+ # a list of methods to memoize
+ #
+ # @return [self]
+ #
+ # @api public
+ def memoize(*methods)
+ methods.each { |method| memoize_method(method) }
+ self
+ end
+
+ private
+
+ # Memoize the named method
+ #
+ # @param [#to_s] method
+ # a method to memoize
+ #
+ # @return [undefined]
+ #
+ # @api private
+ def memoize_method(method)
+ visibility = method_visibility(method)
+ define_memoize_method(method)
+ send(visibility, method)
+ end
+
+ # Define a memoized method that delegates to the original method
+ #
+ # @param [Symbol] method
+ # the name of the method
+ #
+ # @return [undefined]
+ #
+ # @api private
+ def define_memoize_method(method)
+ original = instance_method(method)
+ undef_method(method)
+ define_method(method) do |*args|
+ if memory.key?(method)
+ memoized(method)
+ else
+ store_memory(method, original.bind(self).call(*args))
+ end
+ end
+ end
+
+ # Return the method visibility of a method
+ #
+ # @param [String, Symbol] method
+ # the name of the method
+ #
+ # @return [String]
+ #
+ # @api private
+ def method_visibility(method)
+ if private_method_defined?(method) then 'private'
+ elsif protected_method_defined?(method) then 'protected'
+ else 'public'
+ end
+ end
+
+ end # module ModuleMethods
+
+ # Methods mixed in to immutable classes
+ module ClassMethods
+
+ # Instantiate a new frozen object
+ #
+ # @example
+ # object = ImmutableClass.new # object is frozen
+ #
+ # @return [Object]
+ #
+ # @api public
+ def new(*)
+ super.freeze
+ end
+
+ end # module ClassMethods
+end # module Immutable
3  lib/immutable/version.rb
@@ -0,0 +1,3 @@
+module Immutable
+ VERSION = '0.0.1'.freeze
+end
7 spec/rcov.opts
@@ -0,0 +1,7 @@
+--exclude-only "spec/,^/"
+--sort coverage
+--callsites
+--xrefs
+--profile
+--text-summary
+--failure-threshold 100
7 spec/shared/command_method_behavior.rb
@@ -0,0 +1,7 @@
+# encoding: utf-8
+
+shared_examples_for 'a command method' do
+ it 'returns self' do
+ should equal(object)
+ end
+end
15 spec/shared/each_method_behaviour.rb
@@ -0,0 +1,15 @@
+# encoding: utf-8
+
+shared_examples_for 'an #each method' do
+ it_should_behave_like 'a command method'
+
+ context 'with no block' do
+ subject { object.each }
+
+ it { should be_instance_of(to_enum.class) }
+
+ it 'yields the expected values' do
+ subject.to_a.should eql(object.to_a)
+ end
+ end
+end
17 spec/shared/hash_method_behavior.rb
@@ -0,0 +1,17 @@
+# encoding: utf-8
+
+shared_examples_for 'a hash method' do
+ it_should_behave_like 'an idempotent method'
+
+ specification = proc do
+ should be_instance_of(Fixnum)
+ end
+
+ it 'is a fixnum' do
+ instance_eval(&specification)
+ end
+
+ it 'memoizes the hash code' do
+ subject.should eql(object.memoized(:hash))
+ end
+end
7 spec/shared/idempotent_method_behavior.rb
@@ -0,0 +1,7 @@
+# encoding: utf-8
+
+shared_examples_for 'an idempotent method' do
+ it 'is idempotent' do
+ should equal(instance_eval(&self.class.subject))
+ end
+end
9 spec/shared/invertible_method_behaviour.rb
@@ -0,0 +1,9 @@
+# encoding: utf-8
+
+shared_examples_for 'an invertible method' do
+ it_should_behave_like 'an idempotent method'
+
+ it 'is invertible' do
+ subject.inverse.should equal(object)
+ end
+end
11 spec/spec_helper.rb
@@ -0,0 +1,11 @@
+# encoding: utf-8
+
+require 'immutable'
+require 'spec'
+require 'spec/autorun'
+
+# require spec support files and shared behavior
+Dir[File.expand_path('../{support,shared}/**/*.rb', __FILE__)].each { |f| require f }
+
+Spec::Runner.configure do |config|
+end
57 spec/unit/immutable/class_methods/freeze_object_spec.rb
@@ -0,0 +1,57 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Immutable, '.freeze_object' do
+ subject { object.freeze_object(value) }
+
+ let(:object) { self.class.described_type }
+
+ context 'with a numeric value' do
+ let(:value) { 1 }
+
+ it { should equal(value) }
+ end
+
+ context 'with a true value' do
+ let(:value) { true }
+
+ it { should equal(value) }
+ end
+
+ context 'with a false value' do
+ let(:value) { false }
+
+ it { should equal(value) }
+ end
+
+ context 'with a nil value' do
+ let(:value) { nil }
+
+ it { should equal(value) }
+ end
+
+ context 'with a symbol value' do
+ let(:value) { :symbol }
+
+ it { should equal(value) }
+ end
+
+ context 'with a frozen value' do
+ let(:value) { String.new.freeze }
+
+ it { should equal(value) }
+ end
+
+ context 'with an unfrozen value' do
+ let(:value) { String.new }
+
+ it { should_not equal(value) }
+
+ it { should be_instance_of(String) }
+
+ it { should == value }
+
+ it { should be_frozen }
+ end
+end
14 spec/unit/immutable/class_methods/new_spec.rb
@@ -0,0 +1,14 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require File.expand_path('../../fixtures/classes', __FILE__)
+
+describe Immutable::ClassMethods, '#new' do
+ subject { object.new }
+
+ let(:object) { ImmutableSpecs::Object }
+
+ it { should be_instance_of(object) }
+
+ it { should be_frozen }
+end
13 spec/unit/immutable/dup_spec.rb
@@ -0,0 +1,13 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require File.expand_path('../fixtures/classes', __FILE__)
+
+describe Immutable, '#dup' do
+ subject { object.dup }
+
+ let(:described_class) { ImmutableSpecs::Object }
+ let(:object) { described_class.new }
+
+ it { should equal(object) }
+end
28 spec/unit/immutable/fixtures/classes.rb
@@ -0,0 +1,28 @@
+# encoding: utf-8
+
+module ImmutableSpecs
+ class Object
+ include Immutable
+
+ def test
+ 'test'
+ end
+
+ def public_method
+ caller
+ end
+
+ protected
+
+ def protected_method
+ caller
+ end
+
+ private
+
+ def private_method
+ caller
+ end
+
+ end # class Object
+end # module ImmutableSpecs
51 spec/unit/immutable/freeze_spec.rb
@@ -0,0 +1,51 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require File.expand_path('../fixtures/classes', __FILE__)
+
+describe Immutable, '#freeze' do
+ subject { object.freeze }
+
+ let(:described_class) { Class.new(ImmutableSpecs::Object) }
+
+ before do
+ described_class.memoize(:test)
+ end
+
+ context 'with an unfrozen object' do
+ let(:object) { described_class.allocate }
+
+ it_should_behave_like 'a command method'
+
+ it 'freezes the object' do
+ expect { subject }.to change(object, :frozen?).
+ from(false).
+ to(true)
+ end
+
+ it 'sets a memoization instance variable' do
+ object.should_not be_instance_variable_defined(:@__memory)
+ subject
+ object.instance_variable_get(:@__memory).should be_instance_of(Immutable::Memory)
+ end
+ end
+
+ context 'with a frozen object' do
+ let(:object) { described_class.new }
+
+ it_should_behave_like 'a command method'
+
+ it 'does not change the frozen state of the object' do
+ expect { subject }.to_not change(object, :frozen?)
+ end
+
+ it 'does not change the memoization instance variable' do
+ expect { subject }.to_not change { object.instance_variable_get(:@__memory) }
+ end
+
+ it 'does not set an instance variable for memoization' do
+ object.instance_variable_get(:@__memory).should be_instance_of(Immutable::Memory)
+ subject
+ end
+ end
+end
57 spec/unit/immutable/memoize_spec.rb
@@ -0,0 +1,57 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require File.expand_path('../fixtures/classes', __FILE__)
+
+describe Immutable, '#memoize' do
+ subject { object.memoize(method, value) }
+
+ let(:described_class) { Class.new(ImmutableSpecs::Object) }
+ let(:object) { described_class.new }
+ let(:method) { :test }
+
+ before do
+ described_class.memoize(method)
+ end
+
+ context 'when the value is frozen' do
+ let(:value) { String.new.freeze }
+
+ it 'sets the memoized value for the method to the value' do
+ subject
+ object.send(method).should equal(value)
+ end
+
+ it 'creates a method that returns a frozen value' do
+ subject
+ object.send(method).should be_frozen
+ end
+ end
+
+ context 'when the value is not frozen' do
+ let(:value) { String.new }
+
+ it 'sets the memoized value for the method to the value' do
+ subject
+ object.send(method).should eql(value)
+ end
+
+ it 'creates a method that returns a frozen value' do
+ subject
+ object.send(method).should be_frozen
+ end
+ end
+
+ context 'when the method is already memoized' do
+ let(:value) { stub }
+ let(:original) { nil }
+
+ before do
+ object.memoize(method, original)
+ end
+
+ it 'does not change the value' do
+ expect { subject }.to_not change { object.send(method) }
+ end
+ end
+end
29 spec/unit/immutable/memoized_spec.rb
@@ -0,0 +1,29 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require File.expand_path('../fixtures/classes', __FILE__)
+
+describe Immutable, '#memoized' do
+ subject { object.memoized(method) }
+
+ let(:described_class) { Class.new(ImmutableSpecs::Object) }
+ let(:method) { :test }
+ let(:value) { String.new.freeze }
+ let(:object) { described_class.new }
+
+ before do
+ described_class.memoize(method)
+ end
+
+ context 'when a method is memoized' do
+ before do
+ object.memoize(method, value)
+ end
+
+ it { should equal(value) }
+ end
+
+ context 'when a method is not memoized' do
+ it { should be_nil }
+ end
+end
16 spec/unit/immutable/module_methods/included_spec.rb
@@ -0,0 +1,16 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require File.expand_path('../../fixtures/classes', __FILE__)
+
+describe Immutable::ModuleMethods, '#included' do
+ subject { object.included(object) }
+
+ let(:object) { ImmutableSpecs::Object }
+
+ before do
+ Immutable.should_receive(:included).with(object).and_return(Immutable)
+ end
+
+ it_should_behave_like 'a command method'
+end
88 spec/unit/immutable/module_methods/memoize_spec.rb
@@ -0,0 +1,88 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require File.expand_path('../../fixtures/classes', __FILE__)
+
+shared_examples_for 'memoizes method' do
+ it 'memoizes the instance method' do
+ subject
+ instance = object.new
+ instance.send(method).should equal(instance.send(method))
+ end
+
+ it 'creates a method that returns a frozen value' do
+ subject
+ object.new.send(method).should be_frozen
+ end
+
+ specification = proc do
+ subject
+ file, line = object.new.send(method).first.split(':')[0, 2]
+ File.expand_path(file).should eql(File.expand_path('../../../../../lib/immutable.rb', __FILE__))
+ line.to_i.should eql(209)
+ end
+
+ it 'sets the file and line number properly' do
+ if RUBY_PLATFORM.include?('java')
+ pending('Kernel#caller returns the incorrect line number in JRuby', &specification)
+ else
+ instance_eval(&specification)
+ end
+ end
+
+ context 'when the initializer calls the memoized method' do
+ before do
+ method = self.method
+ object.send(:define_method, :initialize) { send(method) }
+ end
+
+ it 'allows the memoized method to be called within the initializer' do
+ subject
+ expect { object.new }.to_not raise_error(NoMethodError)
+ end
+
+ it 'memoizes the methdod inside the initializer' do
+ subject
+ object.new.memoized(method).should_not be_nil
+ end
+ end
+end
+
+describe Immutable::ModuleMethods, '#memoize' do
+ subject { object.memoize(method) }
+
+ let(:object) { Class.new(ImmutableSpecs::Object) }
+
+ context 'public method' do
+ let(:method) { :public_method }
+
+ it_should_behave_like 'a command method'
+ it_should_behave_like 'memoizes method'
+
+ it 'is still a public method' do
+ should be_public_method_defined(method)
+ end
+ end
+
+ context 'protected method' do
+ let(:method) { :protected_method }
+
+ it_should_behave_like 'a command method'
+ it_should_behave_like 'memoizes method'
+
+ it 'is still a protected method' do
+ should be_protected_method_defined(method)
+ end
+ end
+
+ context 'private method' do
+ let(:method) { :private_method }
+
+ it_should_behave_like 'a command method'
+ it_should_behave_like 'memoizes method'
+
+ it 'is still a private method' do
+ should be_private_method_defined(method)
+ end
+ end
+end
7 tasks/metrics/ci.rake
@@ -0,0 +1,7 @@
+desc 'Run metrics with Heckle'
+task :ci => %w[ ci:metrics heckle ]
+
+namespace :ci do
+ desc 'Run metrics'
+ task :metrics => %w[ verify_measurements flog flay reek roodi metrics:all ]
+end
47 tasks/metrics/flay.rake
@@ -0,0 +1,47 @@
+begin
+ if RUBY_VERSION == '1.8.7'
+ require 'flay'
+ require 'yaml'
+
+ config = YAML.load_file(File.expand_path('../../../config/flay.yml', __FILE__)).freeze
+ threshold = config.fetch('threshold').to_i
+ total_score = config.fetch('total_score').to_f
+ files = Flay.expand_dirs_to_files(config.fetch('path', 'lib'))
+
+ # original code by Marty Andrews:
+ # http://blog.martyandrews.net/2009/05/enforcing-ruby-code-quality.html
+ desc 'Analyze for code duplication'
+ task :flay do
+ # run flay once without a threshold to ensure the max mass matches the threshold
+ flay = Flay.new(:fuzzy => false, :verbose => false, :mass => 0)
+ flay.process(*files)
+
+ max = flay.masses.map { |hash, mass| mass.to_f / flay.hashes[hash].size }.max
+ unless max >= threshold
+ raise "Adjust flay threshold down to #{max}"
+ end
+
+ total = flay.masses.reduce(0.0) { |total, (hash, mass)| total + (mass.to_f / flay.hashes[hash].size) }
+ unless total == total_score
+ raise "Flay total is now #{total}, but expected #{total_score}"
+ end
+
+ # run flay a second time with the threshold set
+ flay = Flay.new(:fuzzy => false, :verbose => false, :mass => threshold.succ)
+ flay.process(*files)
+
+ if flay.masses.any?
+ flay.report
+ raise "#{flay.masses.size} chunks of code have a duplicate mass > #{threshold}"
+ end
+ end
+ else
+ task :flay do
+ $stderr.puts 'Flay has inconsistend results accros ruby implementations. It is only enabled on 1.8.7, fix and remove guard'
+ end
+ end
+rescue LoadError
+ task :flay do
+ abort 'Flay is not available. In order to run flay, you must: gem install flay'
+ end
+end
43 tasks/metrics/flog.rake
@@ -0,0 +1,43 @@
+begin
+ require 'flog'
+ require 'yaml'
+
+ class Float
+ def round_to(n)
+ (self * 10**n).round.to_f * 10**-n
+ end
+ end
+
+ config = YAML.load_file(File.expand_path('../../../config/flog.yml', __FILE__)).freeze
+ threshold = config.fetch('threshold').to_f.round_to(1)
+
+ # original code by Marty Andrews:
+ # http://blog.martyandrews.net/2009/05/enforcing-ruby-code-quality.html
+ desc 'Analyze for code complexity'
+ task :flog do
+ flog = Flog.new
+ flog.flog Array(config.fetch('path', 'lib'))
+
+ totals = flog.totals.select { |name, score| name[-5, 5] != '#none' }.
+ map { |name, score| [ name, score.round_to(1) ] }.
+ sort_by { |name, score| score }
+
+ max = totals.last[1]
+ unless max >= threshold
+ raise "Adjust flog score down to #{max}"
+ end
+
+ bad_methods = totals.select { |name, score| score > threshold }
+ if bad_methods.any?
+ bad_methods.reverse_each do |name, score|
+ puts '%8.1f: %s' % [ score, name ]
+ end
+
+ raise "#{bad_methods.size} methods have a flog complexity > #{threshold}"
+ end
+ end
+rescue LoadError
+ task :flog do
+ abort 'Flog is not available. In order to run flog, you must: gem install flog'
+ end
+end
208 tasks/metrics/heckle.rake
@@ -0,0 +1,208 @@
+$LOAD_PATH.unshift(File.expand_path('../../../lib', __FILE__))
+
+# original code by Ashley Moran:
+# http://aviewfromafar.net/2007/11/1/rake-task-for-heckling-your-specs
+
+begin
+ require 'pathname'
+ require 'backports'
+ require 'active_support/inflector'
+ require 'heckle'
+ require 'mspec'
+ require 'mspec/utils/name_map'
+
+ SKIP_METHODS = %w[ blank_slate_method_added ].freeze
+
+ class NameMap
+ def file_name(method, constant)
+ map = MAP[method]
+ name = if map
+ map[constant] || map[:default]
+ else
+ method.
+ gsub('?','_ques').
+ gsub('!','_bang').
+ gsub('=','_assign')
+ end
+ "#{name}_spec.rb"
+ end
+ end
+
+ desc 'Heckle each module and class'
+ task :heckle => :rcov do
+ unless Ruby2Ruby::VERSION == '1.2.2'
+ raise "ruby2ruby version #{Ruby2Ruby::VERSION} may not work properly, 1.2.2 *only* is recommended for use with heckle"
+ end
+
+ require 'immutable'
+
+ root_module_regexp = Regexp.union('Immutable')
+
+ spec_dir = Pathname('spec/unit')
+
+ NameMap::MAP.each do |op, method|
+ next if method.kind_of?(Hash)
+ NameMap::MAP[op] = { :default => method }
+ end
+
+ aliases = Hash.new { |h,mod| h[mod] = Hash.new { |h,method| h[method] = method } }
+ map = NameMap.new
+
+ heckle_caught_modules = Hash.new { |hash, key| hash[key] = [] }
+ unhandled_mutations = 0
+
+ ObjectSpace.each_object(Module) do |mod|
+ next unless mod.name =~ /\A#{root_module_regexp}(?::|\z)/
+
+ spec_prefix = spec_dir.join(mod.name.underscore)
+
+ specs = []
+
+ # get the public class methods
+ metaclass = class << mod; self end
+ ancestors = metaclass.ancestors
+
+ spec_class_methods = mod.singleton_methods(false)
+
+ spec_class_methods.reject! do |method|
+ %w[ yaml_new yaml_tag_subclasses? included nesting constants ].include?(method.to_s)
+ end
+
+ if mod.ancestors.include?(Singleton)
+ spec_class_methods.reject! { |method| method.to_s == 'instance' }
+ end
+
+ # get the protected and private class methods
+ other_class_methods = metaclass.protected_instance_methods(false) |
+ metaclass.private_instance_methods(false)
+
+ ancestors.each do |ancestor|
+ other_class_methods -= ancestor.protected_instance_methods(false) |
+ ancestor.private_instance_methods(false)
+ end
+
+ other_class_methods.reject! do |method|
+ method.to_s == 'allocate' || SKIP_METHODS.include?(method.to_s)
+ end
+
+ other_class_methods.reject! do |method|
+ next unless spec_class_methods.any? { |specced| specced.to_s == $1 }
+
+ spec_class_methods << method
+ end
+
+ # get the instances methods
+ spec_methods = mod.public_instance_methods(false)
+
+ other_methods = mod.protected_instance_methods(false) |
+ mod.private_instance_methods(false)
+
+ other_methods.reject! do |method|
+ next unless spec_methods.any? { |specced| specced.to_s == $1 }
+
+ spec_methods << method
+ end
+
+ # map the class methods to spec files
+ spec_class_methods.each do |method|
+ method = aliases[mod.name][method]
+ next if SKIP_METHODS.include?(method.to_s)
+
+ spec_file = spec_prefix.join('class_methods').join(map.file_name(method, mod.name))
+
+ unless spec_file.file?
+ raise "No spec file #{spec_file} for #{mod}.#{method}"
+ next
+ end
+
+ specs << [ ".#{method}", [ spec_file ] ]
+ end
+
+ # map the instance methods to spec files
+ spec_methods.each do |method|
+ method = aliases[mod.name][method]
+ next if SKIP_METHODS.include?(method.to_s)
+
+ spec_file = spec_prefix.join(map.file_name(method, mod.name))
+
+ unless spec_file.file?
+ raise "No spec file #{spec_file} for #{mod}##{method}"
+ next
+ end
+
+ specs << [ "##{method}", [ spec_file ] ]
+ end
+
+ # non-public methods are considered covered if they can be mutated
+ # and any spec fails for the current or descendant modules
+ other_methods.each do |method|
+ descedant_specs = []
+
+ ObjectSpace.each_object(Module) do |descedant|
+ next unless descedant.name =~ /\A#{root_module_regexp}(?::|\z)/ && mod >= descedant
+ descedant_spec_prefix = spec_dir.join(descedant.name.underscore)
+ descedant_specs << descedant_spec_prefix
+
+ if method.to_s == 'initialize'
+ descedant_specs.concat(Pathname.glob(descedant_spec_prefix.join('class_methods/new_spec.rb')))
+ end
+ end
+
+ specs << [ "##{method}", descedant_specs ]
+ end
+
+ other_class_methods.each do |method|
+ descedant_specs = []
+
+ ObjectSpace.each_object(Module) do |descedant|
+ next unless descedant.name =~ /\A#{root_module_regexp}(?::|\z)/ && mod >= descedant
+ descedant_specs << spec_dir.join(descedant.name.underscore).join('class_methods')
+ end
+
+ specs << [ ".#{method}", descedant_specs ]
+ end
+
+ specs.sort.each do |(method, spec_files)|
+ puts "Heckling #{mod}#{method}"
+ IO.popen("spec #{spec_files.join(' ')} --heckle '#{mod}#{method}'") do |pipe|
+ while line = pipe.gets
+ case line = line.chomp
+ when "The following mutations didn't cause test failures:"
+ heckle_caught_modules[mod.name] << method
+ when '+++ mutation'
+ unhandled_mutations += 1
+ end
+ end
+ end
+ end
+ end
+
+ if unhandled_mutations > 0
+ error_message_lines = [ "*************\n" ]
+
+ error_message_lines << "Heckle found #{unhandled_mutations} " \
+ "mutation#{"s" unless unhandled_mutations == 1} " \
+ "that didn't cause spec violations\n"
+
+ heckle_caught_modules.each do |mod, methods|
+ error_message_lines << "#{mod} contains the following " \
+ 'poorly-specified methods:'
+ methods.each do |method|
+ error_message_lines << " - #{method}"
+ end
+ error_message_lines << ''
+ end
+
+ error_message_lines << 'Get your act together and come back ' \
+ 'when your specs are doing their job!'
+
+ raise error_message_lines.join("\n")
+ else
+ puts 'Well done! Your code withstood a heckling.'
+ end
+ end
+rescue LoadError
+ task :heckle => :spec do
+ $stderr.puts 'Heckle or mspec is not available. In order to run heckle, you must: gem install heckle mspec'
+ end
+end
29 tasks/metrics/metric_fu.rake
@@ -0,0 +1,29 @@
+begin
+ require 'metric_fu'
+ require 'json'
+
+ # XXX: temporary hack until metric_fu is fixed
+ MetricFu::Saikuro.class_eval { include FileUtils }
+
+ MetricFu::Configuration.run do |config|
+ config.rcov = {
+ :environment => 'test',
+ :test_files => %w[ spec/**/*_spec.rb ],
+ :rcov_opts => %w[
+ --sort coverage
+ --no-html
+ --text-coverage
+ --no-color
+ --profile
+ --exclude spec/,^/
+ --include lib:spec
+ ],
+ }
+ end
+rescue LoadError
+ namespace :metrics do
+ task :all do
+ $stderr.puts 'metric_fu is not available. In order to run metrics:all, you must: gem install metric_fu'
+ end
+ end
+end
15 tasks/metrics/reek.rake
@@ -0,0 +1,15 @@
+begin
+ require 'reek/rake/task'
+
+ if defined?(RUBY_ENGINE) and RUBY_ENGINE == 'rbx'
+ task :reek do
+ $stderr.puts 'Reek fails under rubinius, fix rubinius and remove guard'
+ end
+ else
+ Reek::Rake::Task.new
+ end
+rescue LoadError
+ task :reek do
+ $stderr.puts 'Reek is not available. In order to run reek, you must: gem install reek'
+ end
+end
15 tasks/metrics/roodi.rake
@@ -0,0 +1,15 @@
+begin
+ require 'roodi'
+ require 'rake/tasklib'
+ require 'roodi_task'
+
+ RoodiTask.new do |t|
+ t.verbose = false
+ t.config = File.expand_path('../../../config/roodi.yml', __FILE__)
+ t.patterns = %w[ lib/**/*.rb ]
+ end
+rescue LoadError
+ task :roodi do
+ abort 'Roodi is not available. In order to run roodi, you must: gem install roodi'
+ end
+end
23 tasks/metrics/yardstick.rake
@@ -0,0 +1,23 @@
+begin
+ require 'pathname'
+ require 'yardstick'
+ require 'yardstick/rake/measurement'
+ require 'yardstick/rake/verify'
+ require 'yaml'
+
+ config = YAML.load_file(File.expand_path('../../../config/yardstick.yml', __FILE__))
+
+ # yardstick_measure task
+ Yardstick::Rake::Measurement.new
+
+ # verify_measurements task
+ Yardstick::Rake::Verify.new do |verify|
+ verify.threshold = config.fetch('threshold')
+ end
+rescue LoadError
+ %w[ yardstick_measure verify_measurements ].each do |name|
+ task name.to_s do
+ abort "Yardstick is not available. In order to run #{name}, you must: gem install yardstick"
+ end
+ end
+end
45 tasks/spec.rake
@@ -0,0 +1,45 @@
+begin
+
+ begin
+ require 'rspec/core/rake_task'
+ rescue LoadError
+ require 'spec/rake/spectask'
+
+ module RSpec
+ module Core
+ RakeTask = Spec::Rake::SpecTask
+ end
+ end
+ end
+
+ desc 'run all specs'
+ task :spec => %w[ spec:unit spec:integration ]
+
+ namespace :spec do
+ RSpec::Core::RakeTask.new(:integration) do |t|
+ t.pattern = 'spec/integration/**/*_spec.rb'
+ end
+
+ RSpec::Core::RakeTask.new(:unit) do |t|
+ t.pattern = 'spec/unit/**/*_spec.rb'
+ end
+ end
+rescue LoadError
+ task :spec do
+ abort 'rspec is not available. In order to run spec, you must: gem install rspec'
+ end
+end
+
+begin
+ desc "Generate code coverage"
+ RSpec::Core::RakeTask.new(:rcov) do |t|
+ t.rcov = true
+ t.rcov_opts = File.read('spec/rcov.opts').split(/\s+/)
+ end
+rescue LoadError
+ task :rcov do
+ abort 'rcov is not available. In order to run rcov, you must: gem install rcov'
+ end
+end
+
+task :test => 'spec'
9 tasks/yard.rake
@@ -0,0 +1,9 @@
+begin
+ require 'yard'
+
+ YARD::Rake::YardocTask.new
+rescue LoadError
+ task :yard do
+ abort 'YARD is not available. In order to run yard, you must: gem install yard'
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.