Permalink
Browse files

Add an initial RecursiveDiffer that 'hacks' Diff::LCS to perform the …

…LCS recursively on arrays within arrays
  • Loading branch information...
1 parent 9814eb8 commit 136a137443fe9782e30c540c4d162357b74e79e2 @mcmire committed Jan 28, 2011
Showing with 207 additions and 0 deletions.
  1. +1 −0 lib/super_diff/differ.rb
  2. +112 −0 lib/super_diff/recursive_differ.rb
  3. +39 −0 spec/differ_spec.rb
  4. +55 −0 spec/recursive_differ_spec.rb
View
@@ -12,6 +12,7 @@ def diff(expected, actual)
def report_to(stdout, data=@data)
Reporter.new(stdout).report(data)
end
+ alias :report :report_to
private
def _diff(args)
@@ -0,0 +1,112 @@
+module SuperDiff
+ class RecursiveDiffer
+ class Event
+ def self.from(lcs_diff_event)
+ new(
+ lcs_diff_event.action,
+ lcs_diff_event.old_position,
+ lcs_diff_event.old_element,
+ lcs_diff_event.new_position,
+ lcs_diff_event.new_element
+ )
+ end
+
+ attr_reader :action
+ attr_reader :old_position
+ attr_reader :old_element
+ attr_reader :new_position
+ attr_reader :new_element
+ attr_accessor :children
+
+ include Comparable
+ include Diff::LCS::ChangeTypeTests
+
+ def initialize(action, old_position, old_element, new_position, new_element, children=[])
+ @action = action
+ @old_position = old_position
+ @old_element = old_element
+ @new_position = new_position
+ @new_element = new_element
+ @children = children
+ end
+
+ def ==(other)
+ other.is_a?(self.class) and
+ (@action == other.action) and
+ (@old_position == other.old_position) and
+ (@new_position == other.new_position) and
+ (@old_element == other.old_element) and
+ (@new_element == other.new_element) and
+ (@children == other.children)
+ end
+
+ def <=>(other)
+ [@action, @old_position, @new_position, @old_element, @new_element] <=> [other.action, other.old_position, other.new_position, other.old_element, other.new_element]
+ end
+
+ def inspect
+ %Q(#<#{self.class}:#{__id__} @action=#{@action} positions=[#{@old_position},#{@new_position}] elements=[#{@old_element.inspect},#{@new_element.inspect}] @children=#{@children.inspect}>)
+ end
+
+ def pretty_print(q)
+ attributes = {
+ :action => @action,
+ :positions => [@old_position, @new_position],
+ :elements => [@old_element, @new_element],
+ :children => @children
+ }
+ q.group(0, "#<#{self.class}", ">") {
+ q.breakable " "
+ q.group(1) {
+ q.seplist(attributes) {|pair|
+ q.text pair.first.to_s
+ q.text ": "
+ q.pp pair.last
+ }
+ }
+ }
+ end
+ end
+
+ class Callbacks
+ attr_reader :diffs
+
+ def initialize
+ @diffs = []
+ end
+
+ def match(event)
+ event = Diff::LCS::ContextChange.simplify(event)
+ event = Event.from(event)
+ @diffs << event
+ end
+
+ def discard_a(event)
+ event = Diff::LCS::ContextChange.simplify(event)
+ event = Event.from(event)
+ @diffs << event
+ end
+
+ def discard_b(event)
+ event = Diff::LCS::ContextChange.simplify(event)
+ event = Event.from(event)
+ @diffs << event
+ end
+
+ def change(event)
+ event = Diff::LCS::ContextChange.simplify(event)
+ event = Event.from(event)
+ if Array === event.old_element && Array === event.new_element
+ #puts "Array elements detected, so comparing those"
+ event.children = RecursiveDiffer.diff(event.old_element, event.new_element)
+ #pp :children => event.children
+ end
+ @diffs << event
+ end
+ end
+
+ def self.diff(seq1, seq2)
+ Diff::LCS.sdiff(seq1, seq2, Callbacks)
+ end
+ end
+end
View
@@ -96,6 +96,45 @@
}
actual.must == expected
end
+
+ specify "shallow arrays with inserted elements" do
+ actual = @differ.diff(
+ %w(a b),
+ %w(a 1 2 b),
+ )
+ expected = {
+ :state => :inequal,
+ :expected => {:value => %w(a b), :type => :array, :size => 2},
+ :actual => {:value => %w(a 1 2 b), :type => :array, :size => 4},
+ :common_type => :array,
+ :breakdown => [
+ {
+ :state => :equal,
+ :expected => {:value => "a", :type => :string, :location => 0},
+ :actual => {:value => "foo", :type => :string, :location => 0},
+ :common_type => :string
+ },
+ {
+ :state => :surplus,
+ :expected => nil,
+ :actual => {:value => "1", :type => :string},
+ :common_type => nil
+ },
+ {
+ :state => :surplus,
+ :expected => nil,
+ :actual => {:value => "2", :type => :string},
+ :common_type => nil
+ },
+ {
+ :state => :moved,
+ :expected => {:value => "b", :type => :string, :location => 1},
+ :actual => {:value => "b", :type => :string, :location => 3},
+ :common_type => :string
+ }
+ ]
+ }
+ end
specify "deep arrays of same size but with differing elements" do
actual = @differ.diff(
@@ -0,0 +1,55 @@
+require File.expand_path('../spec_helper', __FILE__)
+
+SuperDiff.libpath do
+ require 'super_diff/recursive_differ'
+end
+
+describe SuperDiff::RecursiveDiffer do
+ describe '#diff' do
+ it "returns what would normally be returned for shallow arrays" do
+ seq1 = %w(a b c d e f g)
+ seq2 = %w(1 2 a b e f g h i)
+ SuperDiff::RecursiveDiffer.diff(seq1, seq2).must == [
+ SuperDiff::RecursiveDiffer::Event.new("+", 0, nil, 0, "1"),
+ SuperDiff::RecursiveDiffer::Event.new("+", 0, nil, 1, "2"),
+ SuperDiff::RecursiveDiffer::Event.new("=", 0, "a", 2, "a"),
+ SuperDiff::RecursiveDiffer::Event.new("=", 1, "b", 3, "b"),
+ SuperDiff::RecursiveDiffer::Event.new("-", 2, "c", 4, nil),
+ SuperDiff::RecursiveDiffer::Event.new("-", 3, "d", 4, nil),
+ SuperDiff::RecursiveDiffer::Event.new("=", 4, "e", 4, "e"),
+ SuperDiff::RecursiveDiffer::Event.new("=", 5, "f", 5, "f"),
+ SuperDiff::RecursiveDiffer::Event.new("=", 6, "g", 6, "g"),
+ SuperDiff::RecursiveDiffer::Event.new("+", 7, nil, 7, "h"),
+ SuperDiff::RecursiveDiffer::Event.new("+", 7, nil, 8, "i")
+ ]
+ end
+
+ it "performs the LCS on arrays within arrays" do
+ seq1 = [ %w(a b c d e f g) ]
+ seq2 = [ %w(1 2 a b e f g h i) ]
+ SuperDiff::RecursiveDiffer.diff(seq1, seq2).must == [
+ SuperDiff::RecursiveDiffer::Event.new("!", 0, %w(a b c d e f g), 0, %w(1 2 a b e f g h i), [
+ SuperDiff::RecursiveDiffer::Event.new("+", 0, nil, 0, "1"),
+ SuperDiff::RecursiveDiffer::Event.new("+", 0, nil, 1, "2"),
+ SuperDiff::RecursiveDiffer::Event.new("=", 0, "a", 2, "a"),
+ SuperDiff::RecursiveDiffer::Event.new("=", 1, "b", 3, "b"),
+ SuperDiff::RecursiveDiffer::Event.new("-", 2, "c", 4, nil),
+ SuperDiff::RecursiveDiffer::Event.new("-", 3, "d", 4, nil),
+ SuperDiff::RecursiveDiffer::Event.new("=", 4, "e", 4, "e"),
+ SuperDiff::RecursiveDiffer::Event.new("=", 5, "f", 5, "f"),
+ SuperDiff::RecursiveDiffer::Event.new("=", 6, "g", 6, "g"),
+ SuperDiff::RecursiveDiffer::Event.new("+", 7, nil, 7, "h"),
+ SuperDiff::RecursiveDiffer::Event.new("+", 7, nil, 8, "i")
+ ])
+ ]
+ end
+
+ it "ignores hashes within arrays" do
+ seq1 = [ {:foo => "bar"} ]
+ seq2 = [ {:foo => "baz"} ]
+ SuperDiff::RecursiveDiffer.diff(seq1, seq2).must == [
+ SuperDiff::RecursiveDiffer::Event.new("!", 0, {:foo => "bar"}, 0, {:foo => "baz"}, [])
+ ]
+ end
+ end
+end

0 comments on commit 136a137

Please sign in to comment.