Skip to content

Instantly share code, notes, and snippets.

@denyago
Last active February 22, 2016 11:07
Show Gist options
  • Save denyago/4d0e3a804f976f15d217 to your computer and use it in GitHub Desktop.
Save denyago/4d0e3a804f976f15d217 to your computer and use it in GitHub Desktop.
require 'diff_matcher'
RSpec::Matchers.define :be_matching do |expected|
match do |actual|
@difference = DiffMatcher::Difference.new(normalize(expected), normalize(actual), opts)
no_errors_happened? && @difference.matching?
end
failure_message do |actual|
no_errors_happened? ? @difference.to_s : "Critical errors during matching:\n#{errors.join("\n")}"
end
chain :unordered do
@unordered = true
end
private
def opts
{color_enabled: RSpec::configuration.color_enabled?}
end
def normalize(obj)
result = obj
if result.kind_of?(String)
begin
result = MultiJson.load(obj)
rescue MultiJson::ParseError => e
errors << "Failed to parse JSON: #{e}\n String: #{obj.inspect}"
return {}
end
end
result = safe_deep_symbolize(result)
if result.kind_of?(Array)
result = result.map do |member|
safe_deep_symbolize(member)
end
end
if @unordered && result.kind_of?(Array)
result = result.sort_by do |member|
member[:id] ||
member[member.keys.sort.map(&:to_s).grep(/id/).first.to_sym]
end
end
result
end
def safe_deep_symbolize(obj)
return obj unless obj.respond_to?(:deep_symbolize_keys)
obj.deep_symbolize_keys
end
def no_errors_happened?
errors.size == 0
end
def errors
@errors ||= []
end
end
module ToMatch
# DiffMatcher will treat it as Proc
class MatcherWrapper < Proc
def initialize(matcher)
@matcher = matcher
@message = "not yet matched"
end
def inspect
message
end
def call(actual)
good_result = matcher.matches?(actual)
if good_result
@message = actual.to_s
else
@message = matcher.failure_message || matcher.description
end
good_result
end
private
attr_reader :matcher, :message
end
def to_match(matcher)
MatcherWrapper.new(matcher) {}
end
end
RSpec.configure { |c| c.include ToMatch }
# Class is literally Proc, but with #inspect
# returning it's source for visual purposes.
#
# Can be used as a smart matcher for +be_matching+
# matcher.
#
# Example (use):
#
# expect(subject).to be_matching({
# runtime: DescriptiveProc.new {|sec| 2 > sec && sec > 0},
# })
#
# Example (output):
#
# {
# {
# :runtime=>{ 0.056614,
# }
# }
# Where, { 1 match_proc
# # ./spec/models/changes_tracker_spec.rb:104:in `block (4 levels) in <top (required)>'
#
# {
# {
# :runtime=>- proc { |sec| (1 > sec) and (sec > 0) }+ 10.0,
# }
# }
# Where, - 1 missing, + 1 additional
# # ./spec/models/changes_tracker_spec.rb:104:in `block (4 levels) in <top (required)>'
class DescriptiveProc < Proc
def inspect
to_source
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment