Skip to content

Commit

Permalink
feat: allow Integers and Floats to be considered equivalent when usin…
Browse files Browse the repository at this point in the history
…g type based matching.

To revert to the previous behaviour, set Pact.configuration.treat_all_number_classes_as_equivalent = false
  • Loading branch information
bethesque committed Jun 17, 2019
1 parent 4a823cd commit d8a70a1
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 20 deletions.
2 changes: 2 additions & 0 deletions lib/pact/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def self.=~ other
attr_accessor :error_stream
attr_accessor :output_stream
attr_accessor :pactfile_write_order
attr_accessor :treat_all_number_classes_as_equivalent # when using type based matching

def self.default_configuration
c = Configuration.new
Expand All @@ -61,6 +62,7 @@ def self.default_configuration
c.output_stream = $stdout
c.error_stream = $stderr
c.pactfile_write_order = :chronological
c.treat_all_number_classes_as_equivalent = true

c
end
Expand Down
39 changes: 27 additions & 12 deletions lib/pact/matchers/matchers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,30 @@ module Pact
# maintain backwards compatibility

module Matchers

NO_DIFF_AT_INDEX = NoDiffAtIndex.new
DEFAULT_OPTIONS = {allow_unexpected_keys: true, type: false}.freeze
NO_DIFF = {}.freeze
NUMERIC_TYPES = %w[Integer Float Fixnum Bignum BigDecimal].freeze
DEFAULT_OPTIONS = {
allow_unexpected_keys: true,
type: false
}.freeze

extend self

def diff expected, actual, opts = {}
calculate_diff(expected, actual, DEFAULT_OPTIONS.merge(opts))
calculate_diff(expected, actual, DEFAULT_OPTIONS.merge(configurable_options).merge(opts))
end

def type_diff expected, actual, opts = {}
calculate_diff expected, actual, DEFAULT_OPTIONS.merge(opts).merge(type: true)
calculate_diff expected, actual, DEFAULT_OPTIONS.merge(configurable_options).merge(opts).merge(type: true)
end

private

def configurable_options
{ treat_all_number_classes_as_equivalent: Pact.configuration.treat_all_number_classes_as_equivalent }
end

def calculate_diff expected, actual, opts = {}
options = DEFAULT_OPTIONS.merge(opts)
case expected
Expand Down Expand Up @@ -156,22 +163,22 @@ def check_for_unexpected_keys expected, actual, options

def object_diff expected, actual, options
if options[:type]
type_difference expected, actual
type_difference expected, actual, options
else
exact_value_diff expected, actual, options
end
end

def exact_value_diff expected, actual, options
if expected != actual
Difference.new expected, actual, value_difference_message(expected, actual, options)
else
if expected == actual
NO_DIFF
else
Difference.new expected, actual, value_difference_message(expected, actual, options)
end
end

def type_difference expected, actual
if types_match? expected, actual
def type_difference expected, actual, options
if types_match? expected, actual, options
NO_DIFF
else
TypeDifference.new type_diff_expected_display(expected), type_diff_actual_display(actual), type_difference_message(expected, actual)
Expand All @@ -186,8 +193,16 @@ def type_diff_actual_display actual
actual.is_a?(KeyNotFound) ? actual : ActualType.new(actual)
end

def types_match? expected, actual
expected.class == actual.class || (is_boolean(expected) && is_boolean(actual))
# Make options optional to support existing monkey patches
def types_match? expected, actual, options = {}
expected.class == actual.class ||
(is_boolean(expected) && is_boolean(actual)) ||
(options.fetch(:treat_all_number_classes_as_equivalent, false) && is_number?(expected) && is_number?(actual))
end

def is_number? object
# deal with Fixnum and Integer without warnings by using string class names
NUMERIC_TYPES.include?(object.class.to_s)
end

def is_boolean object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ module Pact::Matchers
[INT, 2, "Expected 1 but got 2 at <path>"],
[INT, nil, "Expected 1 but got nil at <path>"],
[INT, STRING, "Expected 1 but got \"foo\" at <path>"],
[INT, FLOAT, "Expected 1 but got \"foo\" at <path>", {pending: true}],
[INT, FLOAT, nil],
[INT, HASH, "Expected 1 but got a Hash at <path>"],
[INT, ARRAY, "Expected 1 but got an Array at <path>"],
[Pact.like(INT), 2, nil],
[Pact.like(INT), nil, "Expected #{a_numeric} (like 1) but got nil at <path>"],
[Pact.like(INT), STRING, "Expected #{a_numeric} (like 1) but got a String (\"foo\") at <path>"],
[Pact.like(INT), FLOAT, "Expected #{a_numeric} (like 1) but got a Float (1.0) at <path>"],
[Pact.like(INT), FLOAT, "Expected #{a_numeric} (like 1) but got a Float (1.0) at <path>", { treat_all_number_classes_as_equivalent: false }],
[Pact.like(INT), FLOAT, nil, { treat_all_number_classes_as_equivalent: true }],
[Pact.like(INT), HASH, "Expected #{a_numeric} (like 1) but got a Hash at <path>"],
[Pact.like(INT), ARRAY, "Expected #{a_numeric} (like 1) but got an Array at <path>"],
[HASH, HASH, nil],
Expand All @@ -56,9 +57,9 @@ module Pact::Matchers
[ARRAY, HASH, "Expected an Array but got a Hash at <path>"]
]

COMBINATIONS.each do | expected, actual, expected_message, options |
context "when expected is #{expected.inspect} and actual is #{actual.inspect}", options || {} do
let(:difference) { diff({thing: expected}, {thing: actual}) }
COMBINATIONS.each do | expected, actual, expected_message, diff_options |
context "when expected is #{expected.inspect} and actual is #{actual.inspect}" do
let(:difference) { diff({thing: expected}, {thing: actual}, diff_options || {}) }
let(:message) { difference[:thing] ? difference[:thing].message : nil }

it "returns the message '#{expected_message}'" do
Expand Down
46 changes: 43 additions & 3 deletions spec/lib/pact/matchers/matchers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,67 @@ module Pact::Matchers
end

context "when the headers do not match" do

let(:actual) { Pact::Headers.new('Content-Length' => '1')}
let(:difference) { {"Content-Type" => Difference.new('application/hippo', Pact::KeyNotFound.new)} }
it "returns a diff" do
expect(diff(expected, actual)).to eq difference
end
end
end


context 'when treat_all_number_classes_as_equivalent is true' do
let(:options) { { treat_all_number_classes_as_equivalent: true } }

describe 'matching numbers with something like' do
let(:expected) { Pact::SomethingLike.new( { a: 1.1 } ) }
let(:actual) { { a: 2 } }

it 'returns an empty diff' do
expect(diff(expected, actual, options)).to eq({})
end
end

describe 'with exact matching' do
let(:expected) { { a: 1 } }
let(:actual) { { a: 1.0 } }

it 'returns an empty diff' do
expect(diff(expected, actual, options)).to eq({})
end
end
end

describe 'matching with something like' do
context 'when treat_all_number_classes_as_equivalent is false' do
let(:options) { { treat_all_number_classes_as_equivalent: false } }

describe 'matching numbers with something like' do
let(:expected) { Pact::SomethingLike.new( { a: 1.1 } ) }
let(:actual) { { a: 2 } }

it 'returns a diff' do
expect(diff(expected, actual, options)).to_not eq({})
end
end

describe 'with exact matching' do
let(:expected) { { a: 1 } }
let(:actual) { { a: 1.0 } }

it 'returns an empty diff because in Ruby 1.0 == 1' do
expect(diff(expected, actual)).to eq({})
end
end
end

describe 'matching with something like' do
context 'when the actual is something like the expected' do
let(:expected) { Pact::SomethingLike.new( { a: 1 } ) }
let(:actual) { { a: 2 } }

it 'returns an empty diff' do
expect(diff(expected, actual)).to eq({})
end

end

context 'when the there is a mismatch of a parent, and a child contains a SomethingLike' do
Expand Down

0 comments on commit d8a70a1

Please sign in to comment.