From 74a7477e1c90b2e6ceef14f415b5f24de2a6423b Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Wed, 29 Aug 2012 13:40:48 +0200 Subject: [PATCH] Steal equalizer from veritas --- lib/mutant/support/equalizer.rb | 125 ++++++++++++++++ .../mutant/equalizer/class_method/new_spec.rb | 138 ++++++++++++++++++ .../unit/mutant/equalizer/methods/eql_spec.rb | 49 +++++++ .../equalizer/methods/equal_value_spec.rb | 85 +++++++++++ 4 files changed, 397 insertions(+) create mode 100644 lib/mutant/support/equalizer.rb create mode 100644 spec/unit/mutant/equalizer/class_method/new_spec.rb create mode 100644 spec/unit/mutant/equalizer/methods/eql_spec.rb create mode 100644 spec/unit/mutant/equalizer/methods/equal_value_spec.rb diff --git a/lib/mutant/support/equalizer.rb b/lib/mutant/support/equalizer.rb new file mode 100644 index 00000000..77348fb4 --- /dev/null +++ b/lib/mutant/support/equalizer.rb @@ -0,0 +1,125 @@ +# encoding: utf-8 + +module Mutant + + # Define equality, equivalence and inspection methods + class Equalizer < Module + + # Initialize an Equalizer with the given keys + # + # Will use the keys with which it is initialized to define #cmp?, + # #hash, and #inspect + # + # @param [Array] *keys + # + # @return [undefined] + # + # @api private + def initialize(*keys) + @keys = Immutable.freeze_object(keys) + define_methods + include_comparison_methods + end + + private + + # Define the equalizer methods based on #keys + # + # @return [undefined] + # + # @api private + def define_methods + define_cmp_method + define_hash_method + define_inspect_method + end + + # Define an #cmp? method based on the instance's values identified by #keys + # + # @return [undefined] + # + # @api private + def define_cmp_method + keys = @keys + define_method(:cmp?) do |comparator, other| + keys.all? { |key| send(key).send(comparator, other.send(key)) } + end + private :cmp? + end + + # Define a #hash method based on the instance's values identified by #keys + # + # @return [undefined] + # + # @api private + def define_hash_method + keys = @keys + define_method(:hash) do + keys.map { |key| send(key).hash }.reduce(self.class.hash, :^) + end + end + + # Define an inspect method that reports the values of the instance's keys + # + # @return [undefined] + # + # @api private + def define_inspect_method + keys = @keys + define_method(:inspect) do + klass = self.class + name = klass.name || klass.inspect + "#<#{name}#{keys.map { |key| " #{key}=#{send(key).inspect}" }.join}>" + end + end + + # Include the #eql? and #== methods + # + # @return [undefined] + # + # @api private + def include_comparison_methods + module_eval do + include Methods, Immutable + memoize :hash + end + end + + # The comparison methods + module Methods + + # Compare the object with other object for equality + # + # @example + # object.eql?(other) # => true or false + # + # @param [Object] other + # the other object to compare with + # + # @return [Boolean] + # + # @api public + def eql?(other) + instance_of?(other.class) and cmp?(__method__, other) + end + + # Compare the object with other object for equivalency + # + # @example + # object == other # => true or false + # + # @param [Object] other + # the other object to compare with + # + # @return [Boolean] + # + # @api public + def ==(other) + other = coerce(other) if respond_to?(:coerce, true) + return false unless self.class <=> other.class + cmp?(__method__, other) + end + + end # module Methods + end # class Equalizer +end # module Mutant diff --git a/spec/unit/mutant/equalizer/class_method/new_spec.rb b/spec/unit/mutant/equalizer/class_method/new_spec.rb new file mode 100644 index 00000000..9f409e8d --- /dev/null +++ b/spec/unit/mutant/equalizer/class_method/new_spec.rb @@ -0,0 +1,138 @@ +# encoding: utf-8 + +require 'spec_helper' + +describe Mutant::Equalizer, '.new' do + let(:object) { described_class } + let(:name) { 'User' } + let(:klass) { ::Class.new } + + context 'with no keys' do + subject { object.new } + + before do + # specify the class #name method + klass.stub(:name).and_return(name) + klass.send(:include, subject) + end + + let(:instance) { klass.new } + + it { should be_instance_of(object) } + + it 'defines #hash and #inspect methods dynamically' do + subject.public_instance_methods(false).map(&:to_s).should =~ %w[ hash inspect ] + end + + describe '#eql?' do + context 'when the objects are similar' do + let(:other) { instance.dup } + + it { instance.eql?(other).should be(true) } + end + + context 'when the objects are different' do + let(:other) { stub('other') } + + it { instance.eql?(other).should be(false) } + end + end + + describe '#==' do + context 'when the objects are similar' do + let(:other) { instance.dup } + + it { (instance == other).should be(true) } + end + + context 'when the objects are different' do + let(:other) { stub('other') } + + it { (instance == other).should be(false) } + end + end + + describe '#hash' do + it { instance.hash.should eql(klass.hash) } + + it 'memoizes the hash code' do + instance.hash.should eql(instance.memoized(:hash)) + end + end + + describe '#inspect' do + it { instance.inspect.should eql('#') } + end + end + + context 'with keys' do + subject { object.new(*keys) } + + let(:keys) { [ :first_name ].freeze } + let(:first_name) { 'John' } + let(:instance) { klass.new(first_name) } + + let(:klass) do + ::Class.new do + attr_reader :first_name + + def initialize(first_name) + @first_name = first_name + end + end + end + + before do + # specify the class #inspect method + klass.stub(:name).and_return(nil) + klass.stub(:inspect).and_return(name) + klass.send(:include, subject) + end + + it { should be_instance_of(object) } + + it 'defines #hash and #inspect methods dynamically' do + subject.public_instance_methods(false).map(&:to_s).should =~ %w[ hash inspect ] + end + + describe '#eql?' do + context 'when the objects are similar' do + let(:other) { instance.dup } + + it { instance.eql?(other).should be(true) } + end + + context 'when the objects are different' do + let(:other) { stub('other') } + + it { instance.eql?(other).should be(false) } + end + end + + describe '#==' do + context 'when the objects are similar' do + let(:other) { instance.dup } + + it { (instance == other).should be(true) } + end + + context 'when the objects are different' do + let(:other) { stub('other') } + + it { (instance == other).should be(false) } + end + end + + describe '#hash' do + it { instance.hash.should eql(klass.hash ^ first_name.hash) } + + it 'memoizes the hash code' do + instance.hash.should eql(instance.memoized(:hash)) + end + end + + describe '#inspect' do + it { instance.inspect.should eql('#') } + end + end +end diff --git a/spec/unit/mutant/equalizer/methods/eql_spec.rb b/spec/unit/mutant/equalizer/methods/eql_spec.rb new file mode 100644 index 00000000..e2860045 --- /dev/null +++ b/spec/unit/mutant/equalizer/methods/eql_spec.rb @@ -0,0 +1,49 @@ +# encoding: utf-8 + +require 'spec_helper' + +describe Mutant::Equalizer::Methods, '#eql?' do + subject { object.eql?(other) } + + let(:object) { described_class.new } + + let(:described_class) do + Class.new do + include Mutant::Equalizer::Methods + + def cmp?(comparator, other) + !!(comparator and other) + end + end + end + + context 'with the same object' do + let(:other) { object } + + it { should be(true) } + + it 'is symmetric' do + should eql(other.eql?(object)) + end + end + + context 'with an equivalent object' do + let(:other) { object.dup } + + it { should be(true) } + + it 'is symmetric' do + should eql(other.eql?(object)) + end + end + + context 'with an equivalent object of a subclass' do + let(:other) { Class.new(described_class).new } + + it { should be(false) } + + it 'is symmetric' do + should eql(other.eql?(object)) + end + end +end diff --git a/spec/unit/mutant/equalizer/methods/equal_value_spec.rb b/spec/unit/mutant/equalizer/methods/equal_value_spec.rb new file mode 100644 index 00000000..670994e6 --- /dev/null +++ b/spec/unit/mutant/equalizer/methods/equal_value_spec.rb @@ -0,0 +1,85 @@ +# encoding: utf-8 + +require 'spec_helper' + +describe Mutant::Equalizer::Methods, '#==' do + subject { object == other } + + let(:object) { described_class.new(true) } + + let(:described_class) do + Class.new do + include Mutant::Equalizer::Methods + + attr_reader :boolean + + def initialize(boolean) + @boolean = boolean + end + + def cmp?(comparator, other) + boolean.send(comparator, other.boolean) + end + end + end + + context 'with the same object' do + let(:other) { object } + + it { should be(true) } + + it 'is symmetric' do + should eql(other == object) + end + end + + context 'with an equivalent object' do + let(:other) { object.dup } + + it { should be(true) } + + it 'is symmetric' do + should eql(other == object) + end + end + + context 'with an equivalent object of a subclass' do + let(:other) { Class.new(described_class).new(true) } + + it { should be(true) } + + it 'is symmetric' do + should eql(other == object) + end + end + + context 'with an object of another class' do + let(:other) { Class.new.new } + + it { should be(false) } + + it 'is symmetric' do + should eql(other == object) + end + end + + context 'with an equivalent object after coercion' do + let(:other) { Object.new } + + before do + # declare a private #coerce method + described_class.class_eval do + def coerce(other) + self.class.new(!!other) + end + private :coerce + end + end + + it { should be(true) } + + it 'is not symmetric' do + should_not eql(other == object) + end + end +end