matchi 4.1.1 → 5.0.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/LICENSE.md +1 -1
- data/README.md +87 -294
- data/lib/matchi/be.rb +11 -24
- data/lib/matchi/be_a_kind_of.rb +21 -63
- data/lib/matchi/be_an_instance_of.rb +24 -101
- data/lib/matchi/be_within/of.rb +39 -33
- data/lib/matchi/be_within.rb +19 -25
- data/lib/matchi/change/delta.rb +56 -0
- data/lib/matchi/change/from.rb +22 -32
- data/lib/matchi/change/from_to.rb +46 -0
- data/lib/matchi/change/to.rb +19 -34
- data/lib/matchi/change.rb +85 -86
- data/lib/matchi/constant.rb +55 -0
- data/lib/matchi/eq.rb +26 -25
- data/lib/matchi/match.rb +30 -26
- data/lib/matchi/number.rb +37 -0
- data/lib/matchi/predicate.rb +46 -56
- data/lib/matchi/raise_exception.rb +75 -40
- data/lib/matchi/satisfy.rb +22 -26
- data/lib/matchi.rb +22 -306
- metadata +18 -11
- data/lib/matchi/change/by.rb +0 -60
- data/lib/matchi/change/by_at_least.rb +0 -61
- data/lib/matchi/change/by_at_most.rb +0 -61
- data/lib/matchi/change/from/to.rb +0 -65
|
@@ -1,66 +1,101 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "constant"
|
|
4
|
+
|
|
3
5
|
module Matchi
|
|
4
|
-
#
|
|
6
|
+
# Exception matcher: passes when evaluating the block raises an exception
|
|
7
|
+
# of the expected class or of one of its descendants (`Module#===`, so the
|
|
8
|
+
# exception cannot lie about its class), optionally with an expected
|
|
9
|
+
# message: a String the message must be `eql?` to, or a Regexp it must
|
|
10
|
+
# match.
|
|
11
|
+
#
|
|
12
|
+
# Every `Exception` is caught, including `SystemExit` and `Interrupt`: a
|
|
13
|
+
# specification must be able to assert on any of them, so an unexpected
|
|
14
|
+
# `exit` or Ctrl-C inside the block is reported as a failure, not
|
|
15
|
+
# propagated. The expected class can be given directly, or by name; see
|
|
16
|
+
# {Matchi::Constant}.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# Matchi::RaiseException.new(ZeroDivisionError).match? { 1 / 0 } # => true
|
|
20
|
+
# Matchi::RaiseException.new(:StandardError).match? { 1 / 0 } # => true
|
|
21
|
+
# Matchi::RaiseException.new(ZeroDivisionError, /by 0/).match? { 1 / 0 } # => true
|
|
22
|
+
# Matchi::RaiseException.new(ZeroDivisionError, "nope").match? { 1 / 0 } # => false
|
|
23
|
+
# Matchi::RaiseException.new(IOError).match? { 1 / 0 } # => false
|
|
5
24
|
class RaiseException
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# @
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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})"
|
|
25
|
+
# @param expected [Class, String, Symbol] the expected exception class, or its name
|
|
26
|
+
# @param message [String, Regexp, nil] the expected message, exact or as a pattern
|
|
27
|
+
# @raise [ArgumentError] when expected is neither an Exception class nor a constant name,
|
|
28
|
+
# or message is neither a String nor a Regexp
|
|
29
|
+
def initialize(expected, message = nil)
|
|
30
|
+
@expected = Constant.normalize(expected, ::Class)
|
|
31
|
+
@message = message
|
|
32
|
+
validate_class!
|
|
33
|
+
validate_message!
|
|
20
34
|
end
|
|
21
35
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# @
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# matcher = Matchi::RaiseException.new(NameError)
|
|
28
|
-
# matcher.match? { Boom } # => true
|
|
29
|
-
#
|
|
30
|
-
# @yieldreturn [#object_id] The actual value to compare to the expected
|
|
31
|
-
# one.
|
|
32
|
-
#
|
|
33
|
-
# @return [Boolean] Comparison between actual and expected values.
|
|
36
|
+
# @yieldreturn [Object] ignored, the block is evaluated for its exception
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
# @raise [ArgumentError] when no block is given
|
|
39
|
+
# @raise [NameError] when the name is not defined
|
|
40
|
+
# @raise [TypeError] when the name does not resolve to an Exception class
|
|
34
41
|
def match?
|
|
35
42
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
36
43
|
|
|
44
|
+
# Resolved before the block, so that a NameError from the name is not
|
|
45
|
+
# mistaken for an exception raised by the block.
|
|
37
46
|
klass = expected_class
|
|
38
|
-
raise ::ArgumentError, "expected exception class must inherit from Exception" unless klass <= ::Exception
|
|
39
47
|
|
|
40
48
|
begin
|
|
41
49
|
yield
|
|
42
50
|
false
|
|
43
|
-
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
44
|
-
|
|
51
|
+
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
|
52
|
+
klass === e && message?(e)
|
|
45
53
|
end
|
|
46
54
|
end
|
|
47
55
|
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
# @return [String] a human-readable description of the matcher
|
|
56
|
+
# @return [String]
|
|
51
57
|
def to_s
|
|
52
|
-
"raise exception #{@expected}"
|
|
58
|
+
return "raise exception #{Constant.label(@expected)}" if @message.nil?
|
|
59
|
+
|
|
60
|
+
"raise exception #{Constant.label(@expected)} with message #{@message.inspect}"
|
|
53
61
|
end
|
|
54
62
|
|
|
55
63
|
private
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
def validate_class!
|
|
66
|
+
return unless ::Class === @expected
|
|
67
|
+
return if @expected <= ::Exception
|
|
68
|
+
|
|
69
|
+
raise ::ArgumentError, "expected must inherit from Exception (got: #{@expected.inspect})"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def validate_message!
|
|
73
|
+
return if @message.nil? || ::String === @message || ::Regexp === @message
|
|
74
|
+
|
|
75
|
+
raise ::ArgumentError, "message must be a String or a Regexp (got: #{@message.inspect})"
|
|
76
|
+
end
|
|
77
|
+
|
|
62
78
|
def expected_class
|
|
63
|
-
::
|
|
79
|
+
return @expected if ::Class === @expected
|
|
80
|
+
|
|
81
|
+
klass = Constant.resolve(@expected, ::Class)
|
|
82
|
+
raise ::TypeError, "#{@expected} is not an Exception class" unless klass <= ::Exception
|
|
83
|
+
|
|
84
|
+
klass
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The message is whatever the exception answers to `message`, which is
|
|
88
|
+
# its own data, as with any exception; it must be a valid String. The
|
|
89
|
+
# expected String or Regexp receives the comparison.
|
|
90
|
+
def message?(exception)
|
|
91
|
+
return true if @message.nil?
|
|
92
|
+
|
|
93
|
+
message = exception.message
|
|
94
|
+
return false unless ::String === message && message.valid_encoding?
|
|
95
|
+
|
|
96
|
+
::String === @message ? @message.eql?(message) : @message.match?(message)
|
|
97
|
+
rescue ::Encoding::CompatibilityError
|
|
98
|
+
false
|
|
64
99
|
end
|
|
65
100
|
end
|
|
66
101
|
end
|
data/lib/matchi/satisfy.rb
CHANGED
|
@@ -1,43 +1,39 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "constant"
|
|
4
|
+
|
|
3
5
|
module Matchi
|
|
4
|
-
#
|
|
6
|
+
# Custom matcher: passes when the given block, called with the actual value,
|
|
7
|
+
# returns `true`. The block must return `true` or `false`; any other value
|
|
8
|
+
# is an error, not a truthy pass.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# Matchi::Satisfy.new { |value| value > 41 }.match? { 42 } # => true
|
|
5
12
|
class Satisfy
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# @
|
|
9
|
-
# require "matchi/satisfy"
|
|
10
|
-
#
|
|
11
|
-
# Matchi::Satisfy.new { |value| value == 42 }
|
|
12
|
-
#
|
|
13
|
-
# @param block [Proc] A block of code.
|
|
13
|
+
# @yieldparam actual [Object] the actual value
|
|
14
|
+
# @yieldreturn [Boolean]
|
|
15
|
+
# @raise [ArgumentError] when no block is given
|
|
14
16
|
def initialize(&block)
|
|
15
17
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
16
18
|
|
|
17
|
-
@
|
|
19
|
+
@predicate = block
|
|
18
20
|
end
|
|
19
21
|
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# @
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# matcher = Matchi::Satisfy.new { |value| value == 42 }
|
|
26
|
-
# matcher.match? { 42 } # => true
|
|
27
|
-
#
|
|
28
|
-
# @yieldreturn [#object_id] The actual value to compare to the expected
|
|
29
|
-
# one.
|
|
30
|
-
#
|
|
31
|
-
# @return [Boolean] Comparison between actual and expected values.
|
|
22
|
+
# @yieldreturn [Object] the actual value
|
|
23
|
+
# @return [Boolean]
|
|
24
|
+
# @raise [ArgumentError] when no block is given
|
|
25
|
+
# @raise [TypeError] when the predicate returns something other than true or false
|
|
32
26
|
def match?
|
|
33
27
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
34
28
|
|
|
35
|
-
@
|
|
29
|
+
result = @predicate.call(yield)
|
|
30
|
+
return result if true.equal?(result) || false.equal?(result)
|
|
31
|
+
|
|
32
|
+
klass = ::Kernel.instance_method(:class).bind_call(result)
|
|
33
|
+
raise ::TypeError, "the block must return true or false (got a #{Constant.label(klass)})"
|
|
36
34
|
end
|
|
37
35
|
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
# @return [String] a human-readable description of the matcher
|
|
36
|
+
# @return [String]
|
|
41
37
|
def to_s
|
|
42
38
|
"satisfy &block"
|
|
43
39
|
end
|
data/lib/matchi.rb
CHANGED
|
@@ -1,311 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
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
|
-
#
|
|
55
|
-
# @api public
|
|
3
|
+
# Framework-agnostic matchers for Ruby.
|
|
4
|
+
#
|
|
5
|
+
# A matcher is any object that responds to `match?` with a block and to
|
|
6
|
+
# `to_s`. The block is evaluated once and yields the actual value, or, for
|
|
7
|
+
# `Change` and `RaiseException`, produces the effect to observe; the matcher
|
|
8
|
+
# returns `true` or `false`. The expected value controls the comparison
|
|
9
|
+
# wherever a comparison takes place, so a hostile actual value cannot forge
|
|
10
|
+
# a match; `Predicate` and the `Change` refinements by delta, which use
|
|
11
|
+
# values produced by the object under test, depend on its honesty.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# Matchi::Eq.new(42).match? { 42 } # => true
|
|
56
15
|
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
|
|
307
16
|
end
|
|
308
17
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
18
|
+
require_relative "matchi/be"
|
|
19
|
+
require_relative "matchi/be_a_kind_of"
|
|
20
|
+
require_relative "matchi/be_an_instance_of"
|
|
21
|
+
require_relative "matchi/be_within"
|
|
22
|
+
require_relative "matchi/change"
|
|
23
|
+
require_relative "matchi/eq"
|
|
24
|
+
require_relative "matchi/match"
|
|
25
|
+
require_relative "matchi/predicate"
|
|
26
|
+
require_relative "matchi/raise_exception"
|
|
27
|
+
require_relative "matchi/satisfy"
|
metadata
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: matchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 5.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cyril Kato
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-10-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
|
-
description:
|
|
13
|
+
description: 'Matchi provides small, explicit expectation matchers for any Ruby testing
|
|
14
|
+
framework. The expected value always controls the comparison, so the object under
|
|
15
|
+
test cannot forge a match.
|
|
16
|
+
|
|
17
|
+
'
|
|
14
18
|
email: contact@cyril.email
|
|
15
19
|
executables: []
|
|
16
20
|
extensions: []
|
|
@@ -25,22 +29,25 @@ files:
|
|
|
25
29
|
- lib/matchi/be_within.rb
|
|
26
30
|
- lib/matchi/be_within/of.rb
|
|
27
31
|
- lib/matchi/change.rb
|
|
28
|
-
- lib/matchi/change/
|
|
29
|
-
- lib/matchi/change/by_at_least.rb
|
|
30
|
-
- lib/matchi/change/by_at_most.rb
|
|
32
|
+
- lib/matchi/change/delta.rb
|
|
31
33
|
- lib/matchi/change/from.rb
|
|
32
|
-
- lib/matchi/change/
|
|
34
|
+
- lib/matchi/change/from_to.rb
|
|
33
35
|
- lib/matchi/change/to.rb
|
|
36
|
+
- lib/matchi/constant.rb
|
|
34
37
|
- lib/matchi/eq.rb
|
|
35
38
|
- lib/matchi/match.rb
|
|
39
|
+
- lib/matchi/number.rb
|
|
36
40
|
- lib/matchi/predicate.rb
|
|
37
41
|
- lib/matchi/raise_exception.rb
|
|
38
42
|
- lib/matchi/satisfy.rb
|
|
39
|
-
homepage: https://
|
|
43
|
+
homepage: https://fixrb.dev/gems/matchi/
|
|
40
44
|
licenses:
|
|
41
45
|
- MIT
|
|
42
46
|
metadata:
|
|
47
|
+
bug_tracker_uri: https://github.com/fixrb/matchi/issues
|
|
48
|
+
documentation_uri: https://rubydoc.info/gems/matchi
|
|
43
49
|
rubygems_mfa_required: 'true'
|
|
50
|
+
source_code_uri: https://github.com/fixrb/matchi
|
|
44
51
|
post_install_message:
|
|
45
52
|
rdoc_options: []
|
|
46
53
|
require_paths:
|
|
@@ -49,15 +56,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
49
56
|
requirements:
|
|
50
57
|
- - ">="
|
|
51
58
|
- !ruby/object:Gem::Version
|
|
52
|
-
version: 3.
|
|
59
|
+
version: 3.3.0
|
|
53
60
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
54
61
|
requirements:
|
|
55
62
|
- - ">="
|
|
56
63
|
- !ruby/object:Gem::Version
|
|
57
64
|
version: '0'
|
|
58
65
|
requirements: []
|
|
59
|
-
rubygems_version: 3.
|
|
66
|
+
rubygems_version: 3.5.22
|
|
60
67
|
signing_key:
|
|
61
68
|
specification_version: 4
|
|
62
|
-
summary:
|
|
69
|
+
summary: Framework-agnostic matchers for secure Ruby testing
|
|
63
70
|
test_files: []
|
data/lib/matchi/change/by.rb
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Matchi
|
|
4
|
-
class Change
|
|
5
|
-
# *Change by* matcher.
|
|
6
|
-
class By
|
|
7
|
-
# Initialize the matcher with an object and a block.
|
|
8
|
-
#
|
|
9
|
-
# @example
|
|
10
|
-
# require "matchi/change/by"
|
|
11
|
-
#
|
|
12
|
-
# object = []
|
|
13
|
-
#
|
|
14
|
-
# Matchi::Change::By.new(1) { object.length }
|
|
15
|
-
#
|
|
16
|
-
# @param expected [#object_id] An expected delta.
|
|
17
|
-
# @param state [Proc] A block of code to execute to get the
|
|
18
|
-
# state of the object.
|
|
19
|
-
def initialize(expected, &state)
|
|
20
|
-
raise ::ArgumentError, "expected must be a Numeric" unless expected.is_a?(::Numeric)
|
|
21
|
-
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
22
|
-
|
|
23
|
-
@expected = expected
|
|
24
|
-
@state = state
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Boolean comparison on the expected change by comparing the value
|
|
28
|
-
# before and after the code execution.
|
|
29
|
-
#
|
|
30
|
-
# @example
|
|
31
|
-
# require "matchi/change/by"
|
|
32
|
-
#
|
|
33
|
-
# object = []
|
|
34
|
-
#
|
|
35
|
-
# matcher = Matchi::Change::By.new(1) { object.length }
|
|
36
|
-
# matcher.match? { object << "foo" } # => true
|
|
37
|
-
#
|
|
38
|
-
# @yieldreturn [#object_id] The block of code to execute.
|
|
39
|
-
#
|
|
40
|
-
# @return [Boolean] Comparison between the value before and after the
|
|
41
|
-
# code execution.
|
|
42
|
-
def match?
|
|
43
|
-
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
44
|
-
|
|
45
|
-
value_before = @state.call
|
|
46
|
-
yield
|
|
47
|
-
value_after = @state.call
|
|
48
|
-
|
|
49
|
-
@expected == (value_after - value_before)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Returns a string representing the matcher.
|
|
53
|
-
#
|
|
54
|
-
# @return [String] a human-readable description of the matcher
|
|
55
|
-
def to_s
|
|
56
|
-
"change by #{@expected.inspect}"
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|