matchi 3.3.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +201 -90
- data/lib/matchi/be.rb +7 -13
- data/lib/matchi/be_a_kind_of.rb +81 -0
- data/lib/matchi/be_an_instance_of.rb +91 -21
- data/lib/matchi/be_within/of.rb +11 -13
- data/lib/matchi/be_within.rb +4 -1
- data/lib/matchi/change/by.rb +11 -14
- data/lib/matchi/change/by_at_least.rb +12 -14
- data/lib/matchi/change/by_at_most.rb +12 -14
- data/lib/matchi/change/from/to.rb +10 -14
- data/lib/matchi/change/from.rb +3 -1
- data/lib/matchi/change/to.rb +10 -14
- data/lib/matchi/change.rb +8 -7
- data/lib/matchi/eq.rb +7 -13
- data/lib/matchi/match.rb +9 -13
- data/lib/matchi/predicate.rb +8 -19
- data/lib/matchi/raise_exception.rb +30 -17
- data/lib/matchi/satisfy.rb +8 -12
- data/lib/matchi.rb +300 -0
- metadata +4 -6
data/lib/matchi/predicate.rb
CHANGED
@@ -16,53 +16,42 @@ module Matchi
|
|
16
16
|
# @param block [Proc] A block of code.
|
17
17
|
def initialize(name, *args, **kwargs, &block)
|
18
18
|
@name = String(name)
|
19
|
-
|
20
|
-
raise ::ArgumentError unless valid_name?
|
19
|
+
raise ::ArgumentError, "invalid predicate name format" unless valid_name?
|
21
20
|
|
22
21
|
@args = args
|
23
22
|
@kwargs = kwargs
|
24
23
|
@block = block
|
25
24
|
end
|
26
25
|
|
27
|
-
# @return [Array] The method name with any arguments to send to the subject.
|
28
|
-
def expected
|
29
|
-
[method_name, @args, @kwargs, @block]
|
30
|
-
end
|
31
|
-
|
32
26
|
# Boolean comparison between the actual value and the expected value.
|
33
27
|
#
|
34
28
|
# @example
|
35
29
|
# require "matchi/predicate"
|
36
30
|
#
|
37
31
|
# matcher = Matchi::Predicate.new(:be_empty)
|
38
|
-
#
|
39
|
-
# matcher.expected # => [:empty?, [], {}, nil]
|
40
|
-
# matcher.matches? { [] } # => true
|
32
|
+
# matcher.match? { [] } # => true
|
41
33
|
#
|
42
34
|
# @example
|
43
35
|
# require "matchi/predicate"
|
44
36
|
#
|
45
37
|
# matcher = Matchi::Predicate.new(:have_key, :foo)
|
46
|
-
#
|
47
|
-
# matcher.expected # => [:has_key?, [:foo], {}, nil]
|
48
|
-
# matcher.matches? { { foo: 42 } } # => true
|
38
|
+
# matcher.match? { { foo: 42 } } # => true
|
49
39
|
#
|
50
40
|
# @yieldreturn [#object_id] The actual value to receive the method request.
|
51
41
|
#
|
52
42
|
# @return [Boolean] A boolean returned by the actual value being tested.
|
53
|
-
def
|
43
|
+
def match?
|
44
|
+
raise ::ArgumentError, "a block must be provided" unless block_given?
|
45
|
+
|
54
46
|
value = yield.send(method_name, *@args, **@kwargs, &@block)
|
55
47
|
return value if [false, true].include?(value)
|
56
48
|
|
57
49
|
raise ::TypeError, "Boolean expected, but #{value.class} instance returned."
|
58
50
|
end
|
59
51
|
|
60
|
-
# A string containing a human-readable representation of the matcher.
|
61
|
-
def inspect
|
62
|
-
"#{self.class}(#{@name}, *#{@args.inspect}, **#{@kwargs.inspect}, &#{@block.inspect})"
|
63
|
-
end
|
64
|
-
|
65
52
|
# Returns a string representing the matcher.
|
53
|
+
#
|
54
|
+
# @return [String] a human-readable description of the matcher
|
66
55
|
def to_s
|
67
56
|
(
|
68
57
|
"#{@name.tr("_", " ")} " + [
|
@@ -3,9 +3,6 @@
|
|
3
3
|
module Matchi
|
4
4
|
# *Expecting errors* matcher.
|
5
5
|
class RaiseException
|
6
|
-
# @return [String] The expected exception name.
|
7
|
-
attr_reader :expected
|
8
|
-
|
9
6
|
# Initialize the matcher with a descendant of class Exception.
|
10
7
|
#
|
11
8
|
# @example
|
@@ -16,6 +13,10 @@ module Matchi
|
|
16
13
|
# @param expected [Exception, #to_s] The expected exception name.
|
17
14
|
def initialize(expected)
|
18
15
|
@expected = String(expected)
|
16
|
+
return if /\A[A-Z]/.match?(@expected)
|
17
|
+
|
18
|
+
raise ::ArgumentError,
|
19
|
+
"expected must start with an uppercase letter (got: #{@expected})"
|
19
20
|
end
|
20
21
|
|
21
22
|
# Boolean comparison between the actual value and the expected value.
|
@@ -24,30 +25,42 @@ module Matchi
|
|
24
25
|
# require "matchi/raise_exception"
|
25
26
|
#
|
26
27
|
# matcher = Matchi::RaiseException.new(NameError)
|
27
|
-
#
|
28
|
-
# matcher.expected # => "NameError"
|
29
|
-
# matcher.matches? { Boom } # => true
|
28
|
+
# matcher.match? { Boom } # => true
|
30
29
|
#
|
31
30
|
# @yieldreturn [#object_id] The actual value to compare to the expected
|
32
31
|
# one.
|
33
32
|
#
|
34
33
|
# @return [Boolean] Comparison between actual and expected values.
|
35
|
-
def
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
false
|
41
|
-
end
|
34
|
+
def match?
|
35
|
+
raise ::ArgumentError, "a block must be provided" unless block_given?
|
36
|
+
|
37
|
+
klass = expected_class
|
38
|
+
raise ::ArgumentError, "expected exception class must inherit from Exception" unless klass <= ::Exception
|
42
39
|
|
43
|
-
|
44
|
-
|
45
|
-
|
40
|
+
begin
|
41
|
+
yield
|
42
|
+
false
|
43
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
44
|
+
e.class <= klass # Checks if the class of the thrown exception is klass or one of its subclasses
|
45
|
+
end
|
46
46
|
end
|
47
47
|
|
48
48
|
# Returns a string representing the matcher.
|
49
|
+
#
|
50
|
+
# @return [String] a human-readable description of the matcher
|
49
51
|
def to_s
|
50
|
-
"raise exception #{expected}"
|
52
|
+
"raise exception #{@expected}"
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# Resolves the expected class name to an actual Class object.
|
58
|
+
# This method handles both string and symbol class names through constant resolution.
|
59
|
+
#
|
60
|
+
# @return [Class] the resolved class
|
61
|
+
# @raise [NameError] if the class doesn't exist
|
62
|
+
def expected_class
|
63
|
+
::Object.const_get(@expected)
|
51
64
|
end
|
52
65
|
end
|
53
66
|
end
|
data/lib/matchi/satisfy.rb
CHANGED
@@ -3,9 +3,6 @@
|
|
3
3
|
module Matchi
|
4
4
|
# *Satisfy* matcher.
|
5
5
|
class Satisfy
|
6
|
-
# @return [Proc] A block of code.
|
7
|
-
attr_reader :expected
|
8
|
-
|
9
6
|
# Initialize the matcher with a block.
|
10
7
|
#
|
11
8
|
# @example
|
@@ -15,6 +12,8 @@ module Matchi
|
|
15
12
|
#
|
16
13
|
# @param block [Proc] A block of code.
|
17
14
|
def initialize(&block)
|
15
|
+
raise ::ArgumentError, "a block must be provided" unless block_given?
|
16
|
+
|
18
17
|
@expected = block
|
19
18
|
end
|
20
19
|
|
@@ -24,24 +23,21 @@ module Matchi
|
|
24
23
|
# require "matchi/satisfy"
|
25
24
|
#
|
26
25
|
# matcher = Matchi::Satisfy.new { |value| value == 42 }
|
27
|
-
#
|
28
|
-
# matcher.expected # => #<Proc:0x00007fbaafc65540>
|
29
|
-
# matcher.matches? { 42 } # => true
|
26
|
+
# matcher.match? { 42 } # => true
|
30
27
|
#
|
31
28
|
# @yieldreturn [#object_id] The actual value to compare to the expected
|
32
29
|
# one.
|
33
30
|
#
|
34
31
|
# @return [Boolean] Comparison between actual and expected values.
|
35
|
-
def
|
36
|
-
|
37
|
-
end
|
32
|
+
def match?
|
33
|
+
raise ::ArgumentError, "a block must be provided" unless block_given?
|
38
34
|
|
39
|
-
|
40
|
-
def inspect
|
41
|
-
"#{self.class}(&block)"
|
35
|
+
@expected.call(yield)
|
42
36
|
end
|
43
37
|
|
44
38
|
# Returns a string representing the matcher.
|
39
|
+
#
|
40
|
+
# @return [String] a human-readable description of the matcher
|
45
41
|
def to_s
|
46
42
|
"satisfy &block"
|
47
43
|
end
|
data/lib/matchi.rb
CHANGED
@@ -2,8 +2,308 @@
|
|
2
2
|
|
3
3
|
# A collection of damn simple expectation matchers.
|
4
4
|
#
|
5
|
+
# The Matchi module provides two ways to create and use matchers:
|
6
|
+
#
|
7
|
+
# 1. Direct instantiation through classes:
|
8
|
+
# ```ruby
|
9
|
+
# matcher = Matchi::Eq.new("foo")
|
10
|
+
# matcher.match? { "foo" } # => true
|
11
|
+
# ```
|
12
|
+
#
|
13
|
+
# 2. Helper methods by including/extending the Matchi module:
|
14
|
+
# ```ruby
|
15
|
+
# # Via include in a class
|
16
|
+
# class MyTestFramework
|
17
|
+
# include Matchi
|
18
|
+
#
|
19
|
+
# def test_something
|
20
|
+
# # Helpers are now available as instance methods
|
21
|
+
# matcher = eq("foo")
|
22
|
+
# matcher.match? { "foo" } # => true
|
23
|
+
#
|
24
|
+
# # Multiple helpers can be chained
|
25
|
+
# change(@array, :length).by(1).match? { @array << 1 }
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# # Or via extend for direct usage
|
30
|
+
# class MyOtherFramework
|
31
|
+
# extend Matchi
|
32
|
+
#
|
33
|
+
# def self.assert_equals(expected, actual)
|
34
|
+
# eq(expected).match? { actual }
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
# ```
|
38
|
+
#
|
39
|
+
# The helper method approach provides a more readable and fluent interface,
|
40
|
+
# making tests easier to write and understand. It's particularly useful when
|
41
|
+
# integrating with testing frameworks or creating custom assertions.
|
42
|
+
#
|
43
|
+
# Available helpers include:
|
44
|
+
# - eq/eql : Equivalence matching
|
45
|
+
# - be/equal : Identity matching
|
46
|
+
# - be_within : Delta comparison
|
47
|
+
# - match : Regular expression matching
|
48
|
+
# - change : State change verification
|
49
|
+
# - be_true/be_false/be_nil : State verification
|
50
|
+
# - be_an_instance_of : Exact type matching
|
51
|
+
# - be_a_kind_of : Type hierarchy matching
|
52
|
+
# - satisfy : Custom block-based matching
|
53
|
+
# - be_* & have_* : Dynamic predicate matching
|
54
|
+
#
|
5
55
|
# @api public
|
6
56
|
module Matchi
|
57
|
+
# Equivalence matcher
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
# matcher = eq("foo")
|
61
|
+
# matcher.match? { "foo" } # => true
|
62
|
+
# matcher.match? { "bar" } # => false
|
63
|
+
#
|
64
|
+
# @param expected [#eql?] An expected equivalent object.
|
65
|
+
#
|
66
|
+
# @return [#match?] An equivalence matcher.
|
67
|
+
#
|
68
|
+
# @api public
|
69
|
+
def eq(expected)
|
70
|
+
::Matchi::Eq.new(expected)
|
71
|
+
end
|
72
|
+
|
73
|
+
alias eql eq
|
74
|
+
|
75
|
+
# Identity matcher
|
76
|
+
#
|
77
|
+
# @example
|
78
|
+
# object = "foo"
|
79
|
+
# matcher = be(object)
|
80
|
+
# matcher.match? { object } # => true
|
81
|
+
# matcher.match? { "foo" } # => false
|
82
|
+
#
|
83
|
+
# @param expected [#equal?] The expected identical object.
|
84
|
+
#
|
85
|
+
# @return [#match?] An identity matcher.
|
86
|
+
#
|
87
|
+
# @api public
|
88
|
+
def be(expected)
|
89
|
+
::Matchi::Be.new(expected)
|
90
|
+
end
|
91
|
+
|
92
|
+
alias equal be
|
93
|
+
|
94
|
+
# Comparisons matcher
|
95
|
+
#
|
96
|
+
# @example
|
97
|
+
# matcher = be_within(1).of(41)
|
98
|
+
# matcher.match? { 42 } # => true
|
99
|
+
# matcher.match? { 43 } # => false
|
100
|
+
#
|
101
|
+
# @param delta [Numeric] A numeric value.
|
102
|
+
#
|
103
|
+
# @return [#match?] A comparison matcher.
|
104
|
+
#
|
105
|
+
# @api public
|
106
|
+
def be_within(delta)
|
107
|
+
::Matchi::BeWithin.new(delta)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Regular expressions matcher
|
111
|
+
#
|
112
|
+
# @example
|
113
|
+
# matcher = match(/^foo$/)
|
114
|
+
# matcher.match? { "foo" } # => true
|
115
|
+
# matcher.match? { "bar" } # => false
|
116
|
+
#
|
117
|
+
# @param expected [#match] A regular expression.
|
118
|
+
#
|
119
|
+
# @return [#match?] A regular expression matcher.
|
120
|
+
#
|
121
|
+
# @api public
|
122
|
+
def match(expected)
|
123
|
+
::Matchi::Match.new(expected)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Expecting errors matcher
|
127
|
+
#
|
128
|
+
# @example
|
129
|
+
# matcher = raise_exception(NameError)
|
130
|
+
# matcher.match? { Boom } # => true
|
131
|
+
# matcher.match? { true } # => false
|
132
|
+
#
|
133
|
+
# @param expected [Exception, #to_s] The expected exception name.
|
134
|
+
#
|
135
|
+
# @return [#match?] An error matcher.
|
136
|
+
#
|
137
|
+
# @api public
|
138
|
+
def raise_exception(expected)
|
139
|
+
::Matchi::RaiseException.new(expected)
|
140
|
+
end
|
141
|
+
|
142
|
+
# True matcher
|
143
|
+
#
|
144
|
+
# @example
|
145
|
+
# matcher = be_true
|
146
|
+
# matcher.match? { true } # => true
|
147
|
+
# matcher.match? { false } # => false
|
148
|
+
# matcher.match? { nil } # => false
|
149
|
+
# matcher.match? { 4 } # => false
|
150
|
+
#
|
151
|
+
# @return [#match?] A `true` matcher.
|
152
|
+
#
|
153
|
+
# @api public
|
154
|
+
def be_true
|
155
|
+
be(true)
|
156
|
+
end
|
157
|
+
|
158
|
+
# False matcher
|
159
|
+
#
|
160
|
+
# @example
|
161
|
+
# matcher = be_false
|
162
|
+
# matcher.match? { false } # => true
|
163
|
+
# matcher.match? { true } # => false
|
164
|
+
# matcher.match? { nil } # => false
|
165
|
+
# matcher.match? { 4 } # => false
|
166
|
+
#
|
167
|
+
# @return [#match?] A `false` matcher.
|
168
|
+
#
|
169
|
+
# @api public
|
170
|
+
def be_false
|
171
|
+
be(false)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Nil matcher
|
175
|
+
#
|
176
|
+
# @example
|
177
|
+
# matcher = be_nil
|
178
|
+
# matcher.match? { nil } # => true
|
179
|
+
# matcher.match? { false } # => false
|
180
|
+
# matcher.match? { true } # => false
|
181
|
+
# matcher.match? { 4 } # => false
|
182
|
+
#
|
183
|
+
# @return [#match?] A `nil` matcher.
|
184
|
+
#
|
185
|
+
# @api public
|
186
|
+
def be_nil
|
187
|
+
be(nil)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Type/class matcher
|
191
|
+
#
|
192
|
+
# Verifies exact class matching (no inheritance).
|
193
|
+
#
|
194
|
+
# @example
|
195
|
+
# matcher = be_an_instance_of(String)
|
196
|
+
# matcher.match? { "foo" } # => true
|
197
|
+
# matcher.match? { 4 } # => false
|
198
|
+
#
|
199
|
+
# @param expected [Class, #to_s] The expected class name.
|
200
|
+
#
|
201
|
+
# @return [#match?] A type/class matcher.
|
202
|
+
#
|
203
|
+
# @api public
|
204
|
+
def be_an_instance_of(expected)
|
205
|
+
::Matchi::BeAnInstanceOf.new(expected)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Type/class matcher
|
209
|
+
#
|
210
|
+
# Verifies class inheritance and module inclusion.
|
211
|
+
#
|
212
|
+
# @example
|
213
|
+
# matcher = be_a_kind_of(Numeric)
|
214
|
+
# matcher.match? { 42 } # => true (Integer inherits from Numeric)
|
215
|
+
# matcher.match? { 42.0 } # => true (Float inherits from Numeric)
|
216
|
+
#
|
217
|
+
# @param expected [Class, #to_s] The expected class name.
|
218
|
+
#
|
219
|
+
# @return [#match?] A type/class matcher.
|
220
|
+
#
|
221
|
+
# @api public
|
222
|
+
def be_a_kind_of(expected)
|
223
|
+
::Matchi::BeAKindOf.new(expected)
|
224
|
+
end
|
225
|
+
|
226
|
+
# Change matcher
|
227
|
+
#
|
228
|
+
# @example
|
229
|
+
# object = []
|
230
|
+
# matcher = change(object, :length).by(1)
|
231
|
+
# matcher.match? { object << 1 } # => true
|
232
|
+
#
|
233
|
+
# object = []
|
234
|
+
# matcher = change(object, :length).by_at_least(1)
|
235
|
+
# matcher.match? { object << 1 } # => true
|
236
|
+
#
|
237
|
+
# object = []
|
238
|
+
# matcher = change(object, :length).by_at_most(1)
|
239
|
+
# matcher.match? { object << 1 } # => true
|
240
|
+
#
|
241
|
+
# object = "foo"
|
242
|
+
# matcher = change(object, :to_s).from("foo").to("FOO")
|
243
|
+
# matcher.match? { object.upcase! } # => true
|
244
|
+
#
|
245
|
+
# object = "foo"
|
246
|
+
# matcher = change(object, :to_s).to("FOO")
|
247
|
+
# matcher.match? { object.upcase! } # => true
|
248
|
+
#
|
249
|
+
# @param object [#object_id] An object.
|
250
|
+
# @param method [Symbol] The name of a method.
|
251
|
+
#
|
252
|
+
# @return [#match?] A change matcher.
|
253
|
+
#
|
254
|
+
# @api public
|
255
|
+
def change(object, method, ...)
|
256
|
+
::Matchi::Change.new(object, method, ...)
|
257
|
+
end
|
258
|
+
|
259
|
+
# Satisfy matcher
|
260
|
+
#
|
261
|
+
# @example
|
262
|
+
# matcher = satisfy { |value| value == 42 }
|
263
|
+
# matcher.match? { 42 } # => true
|
264
|
+
#
|
265
|
+
# @yield [value] A block that defines the satisfaction criteria
|
266
|
+
# @yieldparam value The value to test
|
267
|
+
# @yieldreturn [Boolean] true if the value satisfies the criteria
|
268
|
+
#
|
269
|
+
# @return [#match?] A satisfy matcher.
|
270
|
+
#
|
271
|
+
# @api public
|
272
|
+
def satisfy(&)
|
273
|
+
::Matchi::Satisfy.new(&)
|
274
|
+
end
|
275
|
+
|
276
|
+
private
|
277
|
+
|
278
|
+
# Predicate matcher, or default method missing behavior.
|
279
|
+
#
|
280
|
+
# @example Empty predicate matcher
|
281
|
+
# matcher = be_empty
|
282
|
+
# matcher.match? { [] } # => true
|
283
|
+
# matcher.match? { [4] } # => false
|
284
|
+
def method_missing(name, ...)
|
285
|
+
return super unless predicate_matcher_name?(name)
|
286
|
+
|
287
|
+
::Matchi::Predicate.new(name, ...)
|
288
|
+
end
|
289
|
+
|
290
|
+
# :nocov:
|
291
|
+
|
292
|
+
# Hook method to return whether the obj can respond to id method or not.
|
293
|
+
def respond_to_missing?(name, include_private = false)
|
294
|
+
predicate_matcher_name?(name) || super
|
295
|
+
end
|
296
|
+
|
297
|
+
# :nocov:
|
298
|
+
|
299
|
+
# Predicate matcher name detector.
|
300
|
+
#
|
301
|
+
# @param name [Array, Symbol] The name of a potential predicate matcher.
|
302
|
+
#
|
303
|
+
# @return [Boolean] Indicates if it is a predicate matcher name or not.
|
304
|
+
def predicate_matcher_name?(name)
|
305
|
+
name.start_with?("be_", "have_") && !name.end_with?("!", "?")
|
306
|
+
end
|
7
307
|
end
|
8
308
|
|
9
309
|
Dir[File.join(File.dirname(__FILE__), "matchi", "*.rb")].each do |fname|
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: matchi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 4.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date: 2024-
|
10
|
+
date: 2024-12-31 00:00:00.000000000 Z
|
12
11
|
dependencies: []
|
13
12
|
description: "Collection of expectation matchers for Rubyists \U0001F939"
|
14
13
|
email: contact@cyril.email
|
@@ -20,6 +19,7 @@ files:
|
|
20
19
|
- README.md
|
21
20
|
- lib/matchi.rb
|
22
21
|
- lib/matchi/be.rb
|
22
|
+
- lib/matchi/be_a_kind_of.rb
|
23
23
|
- lib/matchi/be_an_instance_of.rb
|
24
24
|
- lib/matchi/be_within.rb
|
25
25
|
- lib/matchi/be_within/of.rb
|
@@ -40,7 +40,6 @@ licenses:
|
|
40
40
|
- MIT
|
41
41
|
metadata:
|
42
42
|
rubygems_mfa_required: 'true'
|
43
|
-
post_install_message:
|
44
43
|
rdoc_options: []
|
45
44
|
require_paths:
|
46
45
|
- lib
|
@@ -55,8 +54,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
54
|
- !ruby/object:Gem::Version
|
56
55
|
version: '0'
|
57
56
|
requirements: []
|
58
|
-
rubygems_version: 3.
|
59
|
-
signing_key:
|
57
|
+
rubygems_version: 3.6.2
|
60
58
|
specification_version: 4
|
61
59
|
summary: "Collection of expectation matchers for Rubyists \U0001F939"
|
62
60
|
test_files: []
|