The problem:
When debugging a program that uses large objects and a test fails because
the object is different from the expected it is sometimes hard to discern
the difference between the objects from the output of Test::Unit.
The goal of this hole is to create a method that will output the differences
of two objects in an intelligent manner. Something similar in concept to
this:
#<TestObject1:0x1 … @b=“b”, … @d=“d”, …>
#<TestObject1:0x2 … @b="", … @d="", …>
You are free to use whatever method you might choose. Program size does not
matter. Creativity and one-up-man-ship is encouraged. We will probably post
the best solution on the wiki.
The following test case is a guideline and may be changed if you have better
ideas for how such a method should work:
class TC_ObjectDiff < Test::Unit::TestCase
def test_to_s
object = TestObject1.new(‘a’, ‘b’, ‘c’, ‘d’, [1, 2, 3, 4])
def object.id
0
end
assert_equal(’#<TestObject1:0x0 @a=“a”, @b=“b”, @c=“c”, @d=“d”, @e=[1, 2,
3, 4]>’,
ObjectDiff.object_to_s(object))
end
def test_compare
object1 = TestObject1.new(‘a’, ‘b’, ‘c’, ‘d’)
def object1.id
1
end
object2 = TestObject1.new(‘a’, ‘’, ‘c’, ‘’)
def object2.id
2
end
string1, string2 = ObjectDiff.compare(object1, object2)
puts [string1, string2]
assert_equal(’#<TestObject1:0x1 … @b=“b”, … @d=“d”, …>’, string1)
assert_equal(’#<TestObject1:0x2 … @b="", … @d="", …>’, string2)
complexObject1 = TestObject1.new(‘1’, {:a => 1, :b => 2, :j => 3}, [1, 2,
3], object1)
def complexObject1.id
3
end
complexObject2 = TestObject1.new(‘1’, {:a => 2, :b => object1, :j => 3},
[0, 2], object2)
def complexObject2.id
4
end
string1, string2 = ObjectDiff.compare(complexObject1, complexObject2)
puts [string1, string2]
assert_equal(’#<TestObject1:0x3 … @b={:a=>1, :b=>2, …}, @c=[1, … 3],
@d=#<TestObject1:0x1 … @b=“b”, … @d=“d”, …>, …>’, string1)
assert_equal(’#<TestObject1:0x4 … @b={:a=>2, :b=>#<TestObject1:0x284a3a8
@c=“c”, @b=“b”, @e=nil, @a=“a”, @d=“d”>, …}, @c=[0, …],
@d=#<TestObject1:0x2 … @b="", … @d="", …>, …>’, string2)
end
def test_compare_different_classes
object1 = TestObject1.new(‘a’, ‘b’, ‘c’, ‘d’)
def object1.id
5
end
object2 = TestObject2.new(‘a’, ‘’, ‘c’, ‘’, ‘f’, ‘g’)
def object2.id
6
end
string1, string2 = ObjectDiff.compare(object1, object2)
puts [string1, string2]
assert_equal(’#<TestObject1:0x5 … @b=“b”, … @d=“d”, @e=nil>’, string1)
assert_equal(’#<TestObject2:0x6 … @b="", … @d="", @f=“f”, @g=“g”>’,
string2)
end
def test_compare_array
a = [1, 2, 3, 4, 5]
b = [1, 3, 4, 4, 5, 6]
string_a, string_b = ObjectDiff.compare_array(a, b)
puts [string_a, string_b]
assert_equal(’[… 2, 3, … …]’, string_a)
assert_equal(’[… 3, 4, … … 6]’, string_b)
end
def test_compare_hash
a = {
:a => 1,
:b => 2,
:c => 3,
:d => 4
}
b = {
:j => 2,
:b => 3,
:c => 3,
:d => 5
}
string_a, string_b = ObjectDiff.compare_hash(a, b)
puts [string_a, string_b]
assert_equal(’{:a=>1, :b=>2, … :d=>4}’, string_a)
assert_equal(’{ :b=>3, … :d=>5, :j=>2}’, string_b)
end
end
Here is my solution:
class ObjectDiff class Node attr_reader :object, :nodesdef initialize(value)
@object = value
@nodes = {}
variables = @object.instance_variables
variables.each { |key|
variable = @object.instance_eval(key)
@nodes[key] = Node.new(variable)
}
end
def to_s
return @object.inspect if [String, Integer, NilClass, Array, Hash,
Fixnum, Integer, Bignum].index(@object.class)
string = s_begin(self)
@nodes.each { |key, node|
string << "#{key}=#{node.to_s}, "
}
s_end(string)
end
def compare_with(node)
return ::ObjectDiff::compare_hash(@object, node.object) if
@object.instance_of?(Hash) and node.object.instance_of?(Hash)
return ::ObjectDiff::compare_array(@object, node.object) if
@object.instance_of?(Array) and node.object.instance_of?(Array)
return [@object.inspect, node.object.inspect] if [String, Integer,
NilClass, Array, Hash, Fixnum, Integer, Bignum].index(@object.class)
keys = []
@nodes.each_key { |key|
keys << key
}
node.nodes.each_key { |key|
keys << key
}
keys.uniq!
keys.sort!
string1 = s_begin(self)
string2 = s_begin(node)
keys.each { |key|
node1 = @nodes[key]
node2 = node.nodes[key]
if @nodes.has_key?(key) and node.nodes.has_key?(key)
if node1.to_s == node2.to_s
string1 << '… '
string2 << '… '
else
s1, s2 = node1.compare_with(node2)
string1 << "#{key}=#{s1}, "
string2 << "#{key}=#{s2}, "
end
else
if @nodes.has_key?(key)
string1 << "#{key}=#{node1.to_s}, "
end
if node.nodes.has_key?(key)
string2 << "#{key}=#{node2.to_s}, "
end
end
}
string1 = s_end(string1)
string2 = s_end(string2)
[string1, string2]
end
def s_begin(node)
"#<#{node.object.class.name}:0x#{format(’%x’, node.object.id)} "
end
def s_end(string)
string.chomp!(’ ‘)
string.chomp!(’,’)
string << '>'
end
end
def self.object_to_s(object)
Node.new(object).to_s
end
def self.compare(object1, object2)
node1 = Node.new(object1)
node2 = Node.new(object2)
node1.compare_with(node2)
end
def self.compare_array(array1, array2)
if array1.size < array2.size
a = array2
b = array1
flipped = true
else
a = array1
b = array2
flipped = false
end
string1 = '['
string2 = ‘[’
for i in 0…a.size
if i < b.size
node1 = Node.new(a[i])
node2 = Node.new(b[i])
if node1.to_s == node2.to_s
string1 << '… '
string2 << '… '
else
string_a, string_b = compare(a[i], b[i])
string1 << "#{string_a}, "
string2 << "#{string_b}, "
end
else
string1 << Node.new(a[i]).to_s
end
end
string1.strip!
string1.chomp!(’,’)
string2.strip!
string2.chomp!(’,’)
string1 << ']'
string2 << ‘]’
if flipped
[string2, string1]
else
[string1, string2]
end
end
def self.compare_hash(hash1, hash2)
keys = []
hash1.each_key { |key|
keys << key
}
hash2.each_key { |key|
keys << key
}
keys.uniq!
keys.sort!{ |a, b|
a = a.inspect if a.is_a?(Symbol)
b = b.inspect if b.is_a?(Symbol)
a <=> b
}
string1 = '{'
string2 = ‘{’
keys.each { |key|
node1 = Node.new(hash1[key])
node2 = Node.new(hash2[key])
if hash1.has_key?(key) and hash2.has_key?(key)
if node1.to_s == node2.to_s
string1 << ‘… ‘
string2 << ‘… ‘
else
s1, s2 = node1.compare_with(node2)
string1 << "#{key.inspect}=>#{s1}, "
string2 << "#{key.inspect}=>#{s2}, "
end
else
if hash1.has_key?(key) and not hash2.has_key?(key)
append = "#{key.inspect}=>#{node1.to_s}, "
string1 << append
string2 << (’ ’ * append.size)
else
append = "#{key.inspect}=>#{node2.to_s}, "
string2 << append
string1 << (’ ’ * append.size)
end
end
}
string1.strip!
string1.chomp!(’,’)
string2.strip!
string2.chomp!(’,’)
string1 << '}'
string2 << ‘}’
[string1, string2]
end
end
Feel free to improve upon my code or create your own. Remember this is for
posterity, so be honest…