matchi 4.1.0 → 4.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE.md +1 -1
- data/README.md +148 -219
- data/lib/matchi/be.rb +58 -16
- data/lib/matchi/be_a_kind_of.rb +87 -34
- data/lib/matchi/be_an_instance_of.rb +74 -62
- data/lib/matchi/be_within.rb +125 -14
- data/lib/matchi/change/by.rb +99 -22
- data/lib/matchi/change/by_at_least.rb +118 -21
- data/lib/matchi/change/by_at_most.rb +132 -22
- data/lib/matchi/change/from/to.rb +92 -25
- data/lib/matchi/change/from.rb +31 -1
- data/lib/matchi/change/to.rb +72 -23
- data/lib/matchi/change.rb +119 -45
- data/lib/matchi/eq.rb +58 -16
- data/lib/matchi/match.rb +56 -16
- data/lib/matchi/predicate.rb +122 -33
- data/lib/matchi/raise_exception.rb +89 -22
- data/lib/matchi/satisfy.rb +87 -16
- data/lib/matchi.rb +51 -297
- metadata +17 -7
- data/lib/matchi/be_within/of.rb +0 -51
data/lib/matchi/be_a_kind_of.rb
CHANGED
@@ -1,68 +1,118 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Matchi
|
4
|
-
#
|
4
|
+
# Type matcher that checks if an object is an instance of a class or one of its subclasses.
|
5
5
|
#
|
6
|
-
# This matcher provides a
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# @example Basic usage
|
11
|
-
# require "matchi/be_a_kind_of"
|
6
|
+
# This matcher provides a reliable way to verify object types while respecting Ruby's
|
7
|
+
# inheritance hierarchy. It uses the case equality operator (===) which is Ruby's
|
8
|
+
# built-in mechanism for type checking, ensuring consistent behavior with Ruby's
|
9
|
+
# own type system.
|
12
10
|
#
|
11
|
+
# @example Basic usage with simple types
|
13
12
|
# matcher = Matchi::BeAKindOf.new(Numeric)
|
14
13
|
# matcher.match? { 42 } # => true
|
15
14
|
# matcher.match? { 42.0 } # => true
|
16
15
|
# matcher.match? { "42" } # => false
|
16
|
+
#
|
17
|
+
# @example Working with inheritance hierarchies
|
18
|
+
# class Animal; end
|
19
|
+
# class Dog < Animal; end
|
20
|
+
# class GermanShepherd < Dog; end
|
21
|
+
#
|
22
|
+
# matcher = Matchi::BeAKindOf.new(Animal)
|
23
|
+
# matcher.match? { Dog.new } # => true
|
24
|
+
# matcher.match? { GermanShepherd.new } # => true
|
25
|
+
# matcher.match? { Object.new } # => false
|
26
|
+
#
|
27
|
+
# @example Using with modules and interfaces
|
28
|
+
# module Swimmable
|
29
|
+
# def swim; end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# class Duck
|
33
|
+
# include Swimmable
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# matcher = Matchi::BeAKindOf.new(Swimmable)
|
37
|
+
# matcher.match? { Duck.new } # => true
|
38
|
+
# matcher.match? { Object.new } # => false
|
39
|
+
#
|
40
|
+
# @example Different ways to specify the class
|
41
|
+
# # Using class directly
|
42
|
+
# Matchi::BeAKindOf.new(String)
|
43
|
+
#
|
44
|
+
# # Using class name as string
|
45
|
+
# Matchi::BeAKindOf.new("String")
|
46
|
+
#
|
47
|
+
# # Using class name as symbol
|
48
|
+
# Matchi::BeAKindOf.new(:String)
|
49
|
+
#
|
50
|
+
# # Using namespaced class
|
51
|
+
# Matchi::BeAKindOf.new("MyModule::MyClass")
|
52
|
+
#
|
53
|
+
# @see Matchi::BeAnInstanceOf
|
54
|
+
# @see https://ruby-doc.org/core/Module.html#method-i-3D-3D-3D
|
17
55
|
class BeAKindOf
|
18
|
-
#
|
56
|
+
# Creates a new type matcher for the specified class.
|
19
57
|
#
|
20
|
-
# @
|
21
|
-
# require "matchi/be_a_kind_of"
|
58
|
+
# @api public
|
22
59
|
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
# Matchi::BeAKindOf.new(:String)
|
60
|
+
# @param expected [Class, #to_s] The expected class or its name
|
61
|
+
# Can be provided as a Class object, String, or Symbol
|
26
62
|
#
|
27
|
-
# @param expected [Class, #to_s] The expected class name
|
28
63
|
# @raise [ArgumentError] if the class name doesn't start with an uppercase letter
|
64
|
+
#
|
65
|
+
# @return [BeAKindOf] a new instance of the matcher
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# BeAKindOf.new(String) # Using class
|
69
|
+
# BeAKindOf.new("String") # Using string
|
70
|
+
# BeAKindOf.new(:String) # Using symbol
|
29
71
|
def initialize(expected)
|
30
72
|
@expected = String(expected)
|
31
73
|
return if /\A[A-Z]/.match?(@expected)
|
32
74
|
|
33
|
-
raise
|
75
|
+
raise ArgumentError,
|
34
76
|
"expected must start with an uppercase letter (got: #{@expected})"
|
35
77
|
end
|
36
78
|
|
37
|
-
# Checks if the yielded object is an instance of the expected class
|
38
|
-
# or one of its subclasses.
|
79
|
+
# Checks if the yielded object is an instance of the expected class or its subclasses.
|
39
80
|
#
|
40
|
-
# This method
|
41
|
-
# way to check class hierarchy relationships
|
42
|
-
#
|
43
|
-
# class or one of its subclasses.
|
81
|
+
# This method leverages Ruby's case equality operator (===) which provides a reliable
|
82
|
+
# way to check class hierarchy relationships. When a class is the receiver of ===,
|
83
|
+
# it returns true if the argument is an instance of that class or one of its subclasses.
|
44
84
|
#
|
45
|
-
# @
|
46
|
-
# class Animal; end
|
47
|
-
# class Dog < Animal; end
|
85
|
+
# @api public
|
48
86
|
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
# matcher.match? { Animal.new } # => true
|
52
|
-
# matcher.match? { Object.new } # => false
|
87
|
+
# @yield [] Block that returns the object to check
|
88
|
+
# @yieldreturn [Object] The object to verify the type of
|
53
89
|
#
|
54
|
-
# @yieldreturn [Object] the actual value to check
|
55
90
|
# @return [Boolean] true if the object is an instance of the expected class or one of its subclasses
|
91
|
+
#
|
56
92
|
# @raise [ArgumentError] if no block is provided
|
93
|
+
# @raise [NameError] if the expected class cannot be found
|
94
|
+
#
|
95
|
+
# @example Simple type check
|
96
|
+
# matcher = BeAKindOf.new(Numeric)
|
97
|
+
# matcher.match? { 42 } # => true
|
98
|
+
#
|
99
|
+
# @example With inheritance
|
100
|
+
# matcher = BeAKindOf.new(Animal)
|
101
|
+
# matcher.match? { Dog.new } # => true
|
57
102
|
def match?
|
58
103
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
59
104
|
|
60
105
|
expected_class === yield # rubocop:disable Style/CaseEquality
|
61
106
|
end
|
62
107
|
|
63
|
-
# Returns a
|
108
|
+
# Returns a human-readable description of the matcher.
|
64
109
|
#
|
65
|
-
# @
|
110
|
+
# @api public
|
111
|
+
#
|
112
|
+
# @return [String] A string describing what this matcher verifies
|
113
|
+
#
|
114
|
+
# @example
|
115
|
+
# BeAKindOf.new(String).to_s # => "be a kind of String"
|
66
116
|
def to_s
|
67
117
|
"be a kind of #{@expected}"
|
68
118
|
end
|
@@ -70,10 +120,13 @@ module Matchi
|
|
70
120
|
private
|
71
121
|
|
72
122
|
# Resolves the expected class name to an actual Class object.
|
73
|
-
# This method handles both string and symbol class names through constant resolution.
|
74
123
|
#
|
75
|
-
# @
|
76
|
-
#
|
124
|
+
# @api private
|
125
|
+
#
|
126
|
+
# @return [Class] The resolved class object
|
127
|
+
# @raise [NameError] If the class name cannot be resolved to an actual class
|
128
|
+
#
|
129
|
+
# @note This method handles both string and symbol class names through constant resolution
|
77
130
|
def expected_class
|
78
131
|
::Object.const_get(@expected)
|
79
132
|
end
|
@@ -1,62 +1,71 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Matchi
|
4
|
-
#
|
4
|
+
# Type matcher that checks if an object is an exact instance of a specific class.
|
5
5
|
#
|
6
|
-
# This matcher
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
6
|
+
# This matcher provides a secure way to verify an object's exact type, ensuring it
|
7
|
+
# matches a specific class without including subclasses. It uses Ruby's method binding
|
8
|
+
# mechanism to bypass potential method overrides, providing better protection against
|
9
|
+
# type check spoofing than standard instance_of? checks.
|
10
10
|
#
|
11
11
|
# @example Basic usage
|
12
|
-
# require "matchi/be_an_instance_of"
|
13
|
-
#
|
14
12
|
# matcher = Matchi::BeAnInstanceOf.new(String)
|
15
|
-
# matcher.match? { "
|
16
|
-
# matcher.match? { :
|
17
|
-
#
|
18
|
-
# @example Enhanced class checking in practice
|
19
|
-
# # Consider a class that attempts to masquerade as String by overriding
|
20
|
-
# # common type checking methods:
|
21
|
-
# class MaliciousString
|
22
|
-
# def class
|
23
|
-
# ::String
|
24
|
-
# end
|
13
|
+
# matcher.match? { "test" } # => true
|
14
|
+
# matcher.match? { :test } # => false
|
25
15
|
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
16
|
+
# @example Inheritance behavior
|
17
|
+
# class Animal; end
|
18
|
+
# class Dog < Animal; end
|
29
19
|
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
20
|
+
# matcher = Matchi::BeAnInstanceOf.new(Animal)
|
21
|
+
# matcher.match? { Animal.new } # => true
|
22
|
+
# matcher.match? { Dog.new } # => false # Subclass doesn't match
|
33
23
|
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
24
|
+
# @example Secure type checking
|
25
|
+
# # Consider a class that attempts to masquerade as String:
|
26
|
+
# class MaliciousString
|
27
|
+
# def class; String; end
|
28
|
+
# def instance_of?(klass); true; end
|
37
29
|
# end
|
38
30
|
#
|
39
31
|
# obj = MaliciousString.new
|
40
|
-
# obj.
|
41
|
-
# obj.is_a?(String) # => true
|
42
|
-
# obj.kind_of?(String) # => true
|
43
|
-
# obj.instance_of?(String) # => true
|
32
|
+
# obj.instance_of?(String) # => true (spoofed)
|
44
33
|
#
|
45
|
-
# # Using our enhanced checking approach:
|
46
34
|
# matcher = Matchi::BeAnInstanceOf.new(String)
|
47
|
-
# matcher.match? { obj }
|
35
|
+
# matcher.match? { obj } # => false (secure)
|
36
|
+
#
|
37
|
+
# @example Different ways to specify the class
|
38
|
+
# # Using class directly
|
39
|
+
# Matchi::BeAnInstanceOf.new(String)
|
40
|
+
#
|
41
|
+
# # Using class name as string
|
42
|
+
# Matchi::BeAnInstanceOf.new("String")
|
43
|
+
#
|
44
|
+
# # Using class name as symbol
|
45
|
+
# Matchi::BeAnInstanceOf.new(:String)
|
46
|
+
#
|
47
|
+
# # Using namespaced class
|
48
|
+
# Matchi::BeAnInstanceOf.new("MyModule::MyClass")
|
49
|
+
#
|
50
|
+
# @see Matchi::BeAKindOf
|
51
|
+
# @see https://ruby-doc.org/core/Object.html#method-i-instance_of-3F
|
52
|
+
# @see https://ruby-doc.org/core/Module.html#method-i-bind_call
|
48
53
|
class BeAnInstanceOf
|
49
54
|
# Initialize the matcher with (the name of) a class or module.
|
50
55
|
#
|
51
|
-
# @
|
52
|
-
# require "matchi/be_an_instance_of"
|
56
|
+
# @api public
|
53
57
|
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
# Matchi::BeAnInstanceOf.new(:String)
|
58
|
+
# @param expected [Class, #to_s] The expected class or its name
|
59
|
+
# Can be provided as a Class object, String, or Symbol
|
57
60
|
#
|
58
|
-
# @param expected [Class, #to_s] The expected class name
|
59
61
|
# @raise [ArgumentError] if the class name doesn't start with an uppercase letter
|
62
|
+
#
|
63
|
+
# @return [BeAnInstanceOf] a new instance of the matcher
|
64
|
+
#
|
65
|
+
# @example
|
66
|
+
# BeAnInstanceOf.new(String) # Using class
|
67
|
+
# BeAnInstanceOf.new("String") # Using string
|
68
|
+
# BeAnInstanceOf.new(:String) # Using symbol
|
60
69
|
def initialize(expected)
|
61
70
|
@expected = String(expected)
|
62
71
|
return if /\A[A-Z]/.match?(@expected)
|
@@ -67,30 +76,25 @@ module Matchi
|
|
67
76
|
|
68
77
|
# Securely checks if the yielded object is an instance of the expected class.
|
69
78
|
#
|
70
|
-
# This method uses
|
71
|
-
#
|
79
|
+
# This method uses Ruby's method binding mechanism to get the true class of an object,
|
80
|
+
# bypassing potential method overrides. While not completely foolproof, it provides
|
81
|
+
# better protection against type check spoofing than using regular method calls which
|
82
|
+
# can be overridden.
|
72
83
|
#
|
73
|
-
#
|
74
|
-
# method from the Object class
|
75
|
-
# 2. .bind_call(obj) binds this original method to our object and calls it,
|
76
|
-
# ensuring we get the real class regardless of method overrides
|
84
|
+
# @api public
|
77
85
|
#
|
78
|
-
#
|
79
|
-
#
|
80
|
-
# completely foolproof, it provides better protection against type check spoofing
|
81
|
-
# than using regular method calls which can be overridden.
|
86
|
+
# @yield [] Block that returns the object to check
|
87
|
+
# @yieldreturn [Object] The object to verify the type of
|
82
88
|
#
|
83
|
-
# @example Basic class check
|
84
|
-
# matcher = Matchi::BeAnInstanceOf.new(String)
|
85
|
-
# matcher.match? { "test" } # => true
|
86
|
-
# matcher.match? { StringIO.new } # => false
|
87
|
-
#
|
88
|
-
# @see https://ruby-doc.org/core/Method.html#method-i-bind_call
|
89
|
-
# @see https://ruby-doc.org/core/UnboundMethod.html
|
90
|
-
#
|
91
|
-
# @yieldreturn [Object] the actual value to check
|
92
89
|
# @return [Boolean] true if the object's actual class is exactly the expected class
|
90
|
+
#
|
93
91
|
# @raise [ArgumentError] if no block is provided
|
92
|
+
# @raise [NameError] if the expected class cannot be found
|
93
|
+
#
|
94
|
+
# @example Simple type check
|
95
|
+
# matcher = BeAnInstanceOf.new(String)
|
96
|
+
# matcher.match? { "test" } # => true
|
97
|
+
# matcher.match? { StringIO.new } # => false
|
94
98
|
def match?
|
95
99
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
96
100
|
|
@@ -98,9 +102,14 @@ module Matchi
|
|
98
102
|
expected_class == actual_class
|
99
103
|
end
|
100
104
|
|
101
|
-
# Returns a
|
105
|
+
# Returns a human-readable description of the matcher.
|
106
|
+
#
|
107
|
+
# @api public
|
108
|
+
#
|
109
|
+
# @return [String] A string describing what this matcher verifies
|
102
110
|
#
|
103
|
-
# @
|
111
|
+
# @example
|
112
|
+
# BeAnInstanceOf.new(String).to_s # => "be an instance of String"
|
104
113
|
def to_s
|
105
114
|
"be an instance of #{@expected}"
|
106
115
|
end
|
@@ -108,10 +117,13 @@ module Matchi
|
|
108
117
|
private
|
109
118
|
|
110
119
|
# Resolves the expected class name to an actual Class object.
|
111
|
-
# This method handles both string and symbol class names through constant resolution.
|
112
120
|
#
|
113
|
-
# @
|
114
|
-
#
|
121
|
+
# @api private
|
122
|
+
#
|
123
|
+
# @return [Class] The resolved class object
|
124
|
+
# @raise [NameError] If the class name cannot be resolved to an actual class
|
125
|
+
#
|
126
|
+
# @note This method handles both string and symbol class names through constant resolution
|
115
127
|
def expected_class
|
116
128
|
::Object.const_get(@expected)
|
117
129
|
end
|
data/lib/matchi/be_within.rb
CHANGED
@@ -1,18 +1,47 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative File.join("be_within", "of")
|
4
|
-
|
5
3
|
module Matchi
|
6
|
-
#
|
4
|
+
# Delta comparison matcher that checks if a numeric value is within a specified range.
|
5
|
+
#
|
6
|
+
# This matcher verifies that a numeric value falls within a certain distance (delta)
|
7
|
+
# of an expected value. It's particularly useful for floating-point comparisons or
|
8
|
+
# when checking if a value is approximately equal to another within a given tolerance.
|
9
|
+
#
|
10
|
+
# @example Basic usage with integers
|
11
|
+
# matcher = Matchi::BeWithin.new(1).of(10)
|
12
|
+
# matcher.match? { 9 } # => true
|
13
|
+
# matcher.match? { 10 } # => true
|
14
|
+
# matcher.match? { 11 } # => true
|
15
|
+
# matcher.match? { 12 } # => false
|
16
|
+
#
|
17
|
+
# @example Floating point comparisons
|
18
|
+
# matcher = Matchi::BeWithin.new(0.1).of(3.14)
|
19
|
+
# matcher.match? { 3.1 } # => true
|
20
|
+
# matcher.match? { 3.2 } # => true
|
21
|
+
# matcher.match? { 3.0 } # => false
|
22
|
+
#
|
23
|
+
# @example Working with percentages
|
24
|
+
# matcher = Matchi::BeWithin.new(5).of(100) # 5% margin
|
25
|
+
# matcher.match? { 95 } # => true
|
26
|
+
# matcher.match? { 105 } # => true
|
27
|
+
# matcher.match? { 110 } # => false
|
28
|
+
#
|
29
|
+
# @see https://ruby-doc.org/core/Numeric.html#method-i-abs
|
7
30
|
class BeWithin
|
8
|
-
# Initialize
|
31
|
+
# Initialize the matcher with a delta value.
|
9
32
|
#
|
10
|
-
# @
|
11
|
-
#
|
33
|
+
# @api public
|
34
|
+
#
|
35
|
+
# @param delta [Numeric] The maximum allowed difference from the expected value
|
12
36
|
#
|
13
|
-
#
|
37
|
+
# @raise [ArgumentError] if delta is not a Numeric
|
38
|
+
# @raise [ArgumentError] if delta is negative
|
14
39
|
#
|
15
|
-
# @
|
40
|
+
# @return [BeWithin] a new instance of the matcher
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# BeWithin.new(0.5) # Using float
|
44
|
+
# BeWithin.new(2) # Using integer
|
16
45
|
def initialize(delta)
|
17
46
|
raise ::ArgumentError, "delta must be a Numeric" unless delta.is_a?(::Numeric)
|
18
47
|
raise ::ArgumentError, "delta must be non-negative" if delta.negative?
|
@@ -20,19 +49,101 @@ module Matchi
|
|
20
49
|
@delta = delta
|
21
50
|
end
|
22
51
|
|
23
|
-
#
|
52
|
+
# Raises NotImplementedError as this is not a complete matcher.
|
53
|
+
#
|
54
|
+
# This class acts as a builder for the actual matcher, which is created
|
55
|
+
# by calling the #of method. Direct use of #match? is not supported.
|
56
|
+
#
|
57
|
+
# @api public
|
58
|
+
#
|
59
|
+
# @raise [NotImplementedError] always, as this method should not be used
|
24
60
|
#
|
25
61
|
# @example
|
26
|
-
#
|
62
|
+
# # Don't do this:
|
63
|
+
# BeWithin.new(0.5).match? { 42 } # Raises NotImplementedError
|
64
|
+
#
|
65
|
+
# # Do this instead:
|
66
|
+
# BeWithin.new(0.5).of(42).match? { 41.8 } # Works correctly
|
67
|
+
def match?
|
68
|
+
raise ::NotImplementedError, "BeWithin is not a complete matcher. Use BeWithin#of to create a valid matcher."
|
69
|
+
end
|
70
|
+
|
71
|
+
# Specifies the expected reference value.
|
27
72
|
#
|
28
|
-
#
|
29
|
-
# be_within_wrapper.of(41)
|
73
|
+
# @api public
|
30
74
|
#
|
31
|
-
# @param expected [Numeric] The
|
75
|
+
# @param expected [Numeric] The reference value to compare against
|
32
76
|
#
|
33
|
-
# @
|
77
|
+
# @raise [ArgumentError] if expected is not a Numeric
|
78
|
+
#
|
79
|
+
# @return [#match?] A matcher that checks if a value is within the specified range
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# be_within_wrapper = BeWithin.new(0.5)
|
83
|
+
# be_within_wrapper.of(3.14) # Creates matcher for values in range 2.64..3.64
|
34
84
|
def of(expected)
|
35
85
|
Of.new(@delta, expected)
|
36
86
|
end
|
87
|
+
|
88
|
+
# Nested class that performs the actual comparison.
|
89
|
+
#
|
90
|
+
# This class implements the actual matching logic, comparing the provided value
|
91
|
+
# against the expected value using the specified delta.
|
92
|
+
class Of
|
93
|
+
# Initialize the matcher with a delta and an expected value.
|
94
|
+
#
|
95
|
+
# @api private
|
96
|
+
#
|
97
|
+
# @param delta [Numeric] The maximum allowed difference from the expected value
|
98
|
+
# @param expected [Numeric] The reference value to compare against
|
99
|
+
#
|
100
|
+
# @raise [ArgumentError] if delta is not a Numeric
|
101
|
+
# @raise [ArgumentError] if expected is not a Numeric
|
102
|
+
# @raise [ArgumentError] if delta is negative
|
103
|
+
def initialize(delta, expected)
|
104
|
+
raise ::ArgumentError, "delta must be a Numeric" unless delta.is_a?(::Numeric)
|
105
|
+
raise ::ArgumentError, "expected must be a Numeric" unless expected.is_a?(::Numeric)
|
106
|
+
raise ::ArgumentError, "delta must be non-negative" if delta.negative?
|
107
|
+
|
108
|
+
@delta = delta
|
109
|
+
@expected = expected
|
110
|
+
end
|
111
|
+
|
112
|
+
# Checks if the yielded value is within the accepted range.
|
113
|
+
#
|
114
|
+
# The value is considered within range if its absolute difference from the
|
115
|
+
# expected value is less than or equal to the specified delta.
|
116
|
+
#
|
117
|
+
# @api public
|
118
|
+
#
|
119
|
+
# @yield [] Block that returns the value to check
|
120
|
+
# @yieldreturn [Numeric] The value to verify
|
121
|
+
#
|
122
|
+
# @return [Boolean] true if the value is within the accepted range
|
123
|
+
#
|
124
|
+
# @raise [ArgumentError] if no block is provided
|
125
|
+
#
|
126
|
+
# @example
|
127
|
+
# matcher = BeWithin.new(0.5).of(3.14)
|
128
|
+
# matcher.match? { 3.0 } # => true
|
129
|
+
# matcher.match? { 4.0 } # => false
|
130
|
+
def match?
|
131
|
+
raise ::ArgumentError, "a block must be provided" unless block_given?
|
132
|
+
|
133
|
+
(@expected - yield).abs <= @delta
|
134
|
+
end
|
135
|
+
|
136
|
+
# Returns a human-readable description of the matcher.
|
137
|
+
#
|
138
|
+
# @api public
|
139
|
+
#
|
140
|
+
# @return [String] A string describing what this matcher verifies
|
141
|
+
#
|
142
|
+
# @example
|
143
|
+
# BeWithin.new(0.5).of(3.14).to_s # => "be within 0.5 of 3.14"
|
144
|
+
def to_s
|
145
|
+
"be within #{@delta} of #{@expected}"
|
146
|
+
end
|
147
|
+
end
|
37
148
|
end
|
38
149
|
end
|
data/lib/matchi/change/by.rb
CHANGED
@@ -2,43 +2,113 @@
|
|
2
2
|
|
3
3
|
module Matchi
|
4
4
|
class Change
|
5
|
-
#
|
5
|
+
# Exact delta matcher that verifies precise numeric changes in object state.
|
6
|
+
#
|
7
|
+
# This matcher ensures that a numeric value changes by exactly the specified amount
|
8
|
+
# after executing a block of code. It's particularly useful for testing operations
|
9
|
+
# that should produce precise, predictable changes in numeric attributes, such as
|
10
|
+
# counters, quantities, or calculated values.
|
11
|
+
#
|
12
|
+
# @example Testing collection size changes
|
13
|
+
# array = []
|
14
|
+
# matcher = Matchi::Change::By.new(2) { array.size }
|
15
|
+
# matcher.match? { array.concat([1, 2]) } # => true
|
16
|
+
# matcher.match? { array.push(3) } # => false # Changed by 1
|
17
|
+
# matcher.match? { array.push(3, 4, 5) } # => false # Changed by 3
|
18
|
+
#
|
19
|
+
# @example Verifying numeric calculations
|
20
|
+
# counter = 100
|
21
|
+
# matcher = Matchi::Change::By.new(-10) { counter }
|
22
|
+
# matcher.match? { counter -= 10 } # => true
|
23
|
+
# matcher.match? { counter -= 15 } # => false # Changed too much
|
24
|
+
#
|
25
|
+
# @example Working with custom numeric attributes
|
26
|
+
# class Account
|
27
|
+
# attr_reader :balance
|
28
|
+
#
|
29
|
+
# def initialize(initial_balance)
|
30
|
+
# @balance = initial_balance
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# def deposit(amount)
|
34
|
+
# @balance += amount
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# def withdraw(amount)
|
38
|
+
# @balance -= amount
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# account = Account.new(1000)
|
43
|
+
# matcher = Matchi::Change::By.new(50) { account.balance }
|
44
|
+
# matcher.match? { account.deposit(50) } # => true
|
45
|
+
# matcher.match? { account.deposit(75) } # => false # Changed by 75
|
46
|
+
#
|
47
|
+
# @example Handling floating point values
|
48
|
+
# temperature = 20.0
|
49
|
+
# matcher = Matchi::Change::By.new(1.5) { temperature }
|
50
|
+
# matcher.match? { temperature += 1.5 } # => true
|
51
|
+
# matcher.match? { temperature += 1.6 } # => false # Not exact match
|
52
|
+
#
|
53
|
+
# @note This matcher checks for exact changes, use ByAtLeast or ByAtMost for
|
54
|
+
# more flexible delta comparisons
|
55
|
+
#
|
56
|
+
# @see Matchi::Change::ByAtLeast For minimum change validation
|
57
|
+
# @see Matchi::Change::ByAtMost For maximum change validation
|
58
|
+
# @see Matchi::Change::From::To For exact value transition validation
|
6
59
|
class By
|
7
|
-
# Initialize the matcher with an
|
60
|
+
# Initialize the matcher with an expected delta and a state block.
|
8
61
|
#
|
9
|
-
# @
|
10
|
-
#
|
62
|
+
# @api public
|
63
|
+
#
|
64
|
+
# @param expected [Numeric] The exact amount by which the value should change
|
65
|
+
# @param state [Proc] Block that retrieves the current value
|
66
|
+
#
|
67
|
+
# @raise [ArgumentError] if expected is not a Numeric
|
68
|
+
# @raise [ArgumentError] if no state block is provided
|
11
69
|
#
|
12
|
-
#
|
70
|
+
# @return [By] a new instance of the matcher
|
13
71
|
#
|
14
|
-
#
|
72
|
+
# @example With positive delta
|
73
|
+
# By.new(5) { counter.value }
|
15
74
|
#
|
16
|
-
# @
|
17
|
-
#
|
18
|
-
#
|
75
|
+
# @example With negative delta
|
76
|
+
# By.new(-3) { stock_level.quantity }
|
77
|
+
#
|
78
|
+
# @example With floating point delta
|
79
|
+
# By.new(0.5) { temperature.celsius }
|
19
80
|
def initialize(expected, &state)
|
20
81
|
raise ::ArgumentError, "expected must be a Numeric" unless expected.is_a?(::Numeric)
|
21
82
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
22
83
|
|
23
84
|
@expected = expected
|
24
|
-
@state
|
85
|
+
@state = state
|
25
86
|
end
|
26
87
|
|
27
|
-
#
|
28
|
-
# before and after the code execution.
|
88
|
+
# Verifies that the value changes by exactly the expected amount.
|
29
89
|
#
|
30
|
-
#
|
31
|
-
#
|
90
|
+
# This method compares the value before and after executing the provided block,
|
91
|
+
# ensuring that the difference matches the expected delta exactly. It's useful
|
92
|
+
# for cases where precision is important and approximate changes are not acceptable.
|
32
93
|
#
|
33
|
-
#
|
94
|
+
# @api public
|
34
95
|
#
|
35
|
-
#
|
36
|
-
#
|
96
|
+
# @yield [] Block that should cause the state change
|
97
|
+
# @yieldreturn [Object] The result of the block (not used)
|
37
98
|
#
|
38
|
-
# @
|
99
|
+
# @return [Boolean] true if the value changed by exactly the expected amount
|
39
100
|
#
|
40
|
-
# @
|
41
|
-
#
|
101
|
+
# @raise [ArgumentError] if no block is provided
|
102
|
+
#
|
103
|
+
# @example Basic usage
|
104
|
+
# counter = 0
|
105
|
+
# matcher = By.new(5) { counter }
|
106
|
+
# matcher.match? { counter += 5 } # => true
|
107
|
+
#
|
108
|
+
# @example Failed match
|
109
|
+
# items = []
|
110
|
+
# matcher = By.new(2) { items.size }
|
111
|
+
# matcher.match? { items.push(1) } # => false # Changed by 1
|
42
112
|
def match?
|
43
113
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
44
114
|
|
@@ -49,9 +119,16 @@ module Matchi
|
|
49
119
|
@expected == (value_after - value_before)
|
50
120
|
end
|
51
121
|
|
52
|
-
# Returns a
|
122
|
+
# Returns a human-readable description of the matcher.
|
123
|
+
#
|
124
|
+
# @api public
|
53
125
|
#
|
54
|
-
# @return [String]
|
126
|
+
# @return [String] A string describing what this matcher verifies
|
127
|
+
#
|
128
|
+
# @example
|
129
|
+
# By.new(5).to_s # => "change by 5"
|
130
|
+
# By.new(-3).to_s # => "change by -3"
|
131
|
+
# By.new(2.5).to_s # => "change by 2.5"
|
55
132
|
def to_s
|
56
133
|
"change by #{@expected.inspect}"
|
57
134
|
end
|