Skip to content
This repository has been archived by the owner on Nov 30, 2024. It is now read-only.

Fixes diff when fuzzy finder anything is used in a Hash object (proof of concept) #596

Closed
Closed
49 changes: 45 additions & 4 deletions lib/rspec/support/differ.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ class Differ
def diff(actual, expected)
diff = ""

no_procs_no_numbers = lambda {|var1, var2| no_procs?(var1, var2) && no_numbers?(var1, var2)}

unless actual.nil? || expected.nil?
if all_strings?(actual, expected)
if any_multiline_strings?(actual, expected)
diff = diff_as_string(coerce_to_string(actual), coerce_to_string(expected))
end
elsif no_procs?(actual, expected) && no_numbers?(actual, expected)
diff = diff_as_string(coerce_to_string(actual), coerce_to_string(expected)) if any_multiline_strings?(actual, expected)
elsif hash_with_anything?(expected)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To reset the discussion below.
I am concerned about side effects of shared state in instance variables.
I’d live to get rid of an instance variable @keys_with_anything.
Only those two methods need it. Can we cache it somehow here in a regular variable? Can hash_with_anything? become something that returns a value that we can subsequently pass to diff_as_object_with_anything?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok! I just came up with something, I didn't thought previously about shared state. Now, I've renamed hash_with_anything? to all_keys_from_hash and it returns a keys array, that is later used in diff_as_object_with_anything. This way we don't have shared state in instance variables.

This approach involved an assignment inside an if condition, it added an extra && operator and it increased the rubocop metric: PerceivedComplexity score, so I had to create another method to wrap all the logic inside it.

diff = diff_as_object_with_anything(actual, expected) if no_procs_no_numbers.call(actual, expected)
elsif no_procs_no_numbers.call(actual, expected)
diff = diff_as_object(actual, expected)
end
end
Expand Down Expand Up @@ -56,6 +58,20 @@ def diff_as_string(actual, expected)
end
# rubocop:enable Metrics/MethodLength

def diff_as_object_with_anything(actual, expected)
@keys_with_anything.each do |keys|
pointer_expected = expected
pointer_actual = actual
final_key = keys.pop
keys.each do |k|
pointer_expected = pointer_expected[k]
pointer_actual = pointer_actual[k]
end
pointer_expected[final_key] = pointer_actual[final_key]
end
diff_as_object(actual, expected)
end

def diff_as_object(actual, expected)
actual_as_string = object_to_string(actual)
expected_as_string = object_to_string(expected)
Expand All @@ -73,6 +89,31 @@ def initialize(opts={})

private

def hash_with_anything?(arg)
return false unless Hash === arg

@keys_with_anything = recursive_get_keys(arg)
@keys_with_anything.any?
end

def recursive_get_keys(hash)
klass = RSpec::Mocks::ArgumentMatchers::AnyArgMatcher
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if this is not defined, like someone opted out of mocking completely, or uses rr/mocha etc?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it’s ok to use ‘return [] unless defined?(RSpec::Mocks)’ here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I didn't thought someone may opt out mocking completely. I've fixed it on my latest commit!

hash.reduce([]) do |acc, pair|
if klass === pair[1]
acc << [pair[0]]
else
if Hash === pair[1]
keys = recursive_get_keys(pair[1])
keys.each do |key|
key.unshift pair[0]
acc << key
end
end
end
acc
end
end

def no_procs?(*args)
safely_flatten(args).none? { |a| Proc === a }
end
Expand Down
66 changes: 66 additions & 0 deletions spec/rspec/support/differ_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,72 @@ def inspect; "<BrokenObject>"; end
expect(differ.diff(false, true)).to_not be_empty
end
end

describe "fuzzy matcher anything" do
it "outputs only key value pair that triggered diff, anything_key should absorb actual value" do
actual = { :fixed => "fixed", :trigger => "trigger", :anything_key => "bcdd0399-1cfe-4de1-a481-ca6b17d41ed8" }
expected = { :fixed => "fixed", :trigger => "wrong", :anything_key => anything }
diff = differ.diff(actual, expected)
expected_diff = dedent(<<-'EOD')
|
|@@ -1,4 +1,4 @@
| :anything_key => "bcdd0399-1cfe-4de1-a481-ca6b17d41ed8",
| :fixed => "fixed",
|-:trigger => "wrong",
|+:trigger => "trigger",
|
EOD
expect(diff).to be_diffed_as(expected_diff)
end

context "with nested hash" do
it "outputs only key value pair that triggered diff, anything_key should absorb actual value" do
actual = { :an_key => "dummy", :fixed => "fixed", :nested => { :anything_key => "bcdd0399-1cfe-4de1-a481-ca6b17d41ed8", :name => "foo", :trigger => "trigger" } }
expected = { :an_key => anything, :fixed => "fixed", :nested => { :anything_key => anything, :name => "foo", :trigger => "wrong" } }
diff = differ.diff(actual, expected)
expected_diff = dedent(<<-'EOD')
|
|@@ -1,4 +1,4 @@
| :an_key => "dummy",
| :fixed => "fixed",
|-:nested => {:anything_key=>"bcdd0399-1cfe-4de1-a481-ca6b17d41ed8", :name=>"foo", :trigger=>"wrong"},
|+:nested => {:anything_key=>"bcdd0399-1cfe-4de1-a481-ca6b17d41ed8", :name=>"foo", :trigger=>"trigger"},
|
EOD
expect(diff).to be_diffed_as(expected_diff)
end
end

context "with nested hash and subnested hash" do
it "outputs only key value pair that triggered diff, anything_key should absorb actual value" do
actual = {
:an_key => "dummy", :fixed => "fixed",
:nested => {
:subnested => { :name => "foo", :trigger => "trigger", :subnested_anything_key => "bcdd0399-1cfe-4de1-a481-ca6b17d41ed8" },
:nested_anything_key => "9930ddcb-1cfe-4de1-a481-ca6b17d41ed8"
}
}
expected = {
:an_key => anything, :fixed => "fixed",
:nested => {
:subnested => { :name => "foo", :trigger => "wrong", :subnested_anything_key => anything },
:nested_anything_key => anything
}
}
diff = differ.diff(actual, expected)
expected_diff = dedent(<<-'EOD')
|
|@@ -1,4 +1,4 @@
| :an_key => "dummy",
| :fixed => "fixed",
|-:nested => {:nested_anything_key=>"9930ddcb-1cfe-4de1-a481-ca6b17d41ed8", :subnested=>{:name=>"foo", :subnested_anything_key=>"bcdd0399-1cfe-4de1-a481-ca6b17d41ed8", :trigger=>"wrong"}},
|+:nested => {:nested_anything_key=>"9930ddcb-1cfe-4de1-a481-ca6b17d41ed8", :subnested=>{:name=>"foo", :subnested_anything_key=>"bcdd0399-1cfe-4de1-a481-ca6b17d41ed8", :trigger=>"trigger"}},
|
EOD
expect(diff).to be_diffed_as(expected_diff)
end
end
end
end
end
end
Expand Down
Loading