matchi 4.2.0 → 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 +91 -227
- data/lib/matchi/be.rb +10 -65
- data/lib/matchi/be_a_kind_of.rb +21 -116
- data/lib/matchi/be_an_instance_of.rb +24 -113
- data/lib/matchi/be_within/of.rb +57 -0
- data/lib/matchi/be_within.rb +17 -134
- data/lib/matchi/change/delta.rb +56 -0
- data/lib/matchi/change/from.rb +21 -61
- data/lib/matchi/change/from_to.rb +46 -0
- data/lib/matchi/change/to.rb +17 -81
- data/lib/matchi/change.rb +73 -148
- data/lib/matchi/constant.rb +55 -0
- data/lib/matchi/eq.rb +24 -65
- data/lib/matchi/match.rb +28 -64
- data/lib/matchi/number.rb +37 -0
- data/lib/matchi/predicate.rb +40 -139
- data/lib/matchi/raise_exception.rb +72 -104
- data/lib/matchi/satisfy.rb +21 -96
- data/lib/matchi.rb +22 -60
- metadata +17 -17
- data/lib/matchi/change/by.rb +0 -137
- data/lib/matchi/change/by_at_least.rb +0 -158
- data/lib/matchi/change/by_at_most.rb +0 -171
- data/lib/matchi/change/from/to.rb +0 -132
data/lib/matchi/change/to.rb
CHANGED
|
@@ -1,56 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../eq"
|
|
4
|
+
|
|
3
5
|
module Matchi
|
|
4
6
|
class Change
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# returns an expected value after executing a block of code. Unlike the full from/to
|
|
9
|
-
# matcher, it only cares about the end result, not the initial state.
|
|
10
|
-
#
|
|
11
|
-
# @example Basic string transformation
|
|
12
|
-
# text = "hello"
|
|
13
|
-
# matcher = Matchi::Change::To.new("HELLO") { text.to_s }
|
|
14
|
-
# matcher.match? { text.upcase! } # => true
|
|
15
|
-
# matcher.match? { text.reverse! } # => false
|
|
16
|
-
#
|
|
17
|
-
# @example Number calculations
|
|
18
|
-
# counter = 0
|
|
19
|
-
# matcher = Matchi::Change::To.new(5) { counter }
|
|
20
|
-
# matcher.match? { counter = 5 } # => true
|
|
21
|
-
# matcher.match? { counter += 1 } # => false
|
|
22
|
-
#
|
|
23
|
-
# @example With object attributes
|
|
24
|
-
# class User
|
|
25
|
-
# attr_accessor :status
|
|
26
|
-
# def initialize(status)
|
|
27
|
-
# @status = status
|
|
28
|
-
# end
|
|
29
|
-
# end
|
|
30
|
-
#
|
|
31
|
-
# user = User.new(:pending)
|
|
32
|
-
# matcher = Matchi::Change::To.new(:active) { user.status }
|
|
33
|
-
# matcher.match? { user.status = :active } # => true
|
|
7
|
+
# Passes when the observed value is not the expected value before the
|
|
8
|
+
# block and is the expected value after it: the value must change to it.
|
|
9
|
+
# The block is always evaluated.
|
|
34
10
|
#
|
|
35
|
-
# @
|
|
36
|
-
#
|
|
11
|
+
# @example
|
|
12
|
+
# s = +"a"
|
|
13
|
+
# Matchi::Change.new(s, :to_s).to("b").match? { s.replace("b") } # => true
|
|
37
14
|
class To
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
# @
|
|
41
|
-
#
|
|
42
|
-
# @param expected [#eql?] The expected final value
|
|
43
|
-
# @param state [Proc] Block that retrieves the value to check
|
|
44
|
-
#
|
|
45
|
-
# @raise [ArgumentError] if no state block is provided
|
|
46
|
-
#
|
|
47
|
-
# @return [To] a new instance of the matcher
|
|
48
|
-
#
|
|
49
|
-
# @example With simple value
|
|
50
|
-
# To.new("test") { object.value }
|
|
51
|
-
#
|
|
52
|
-
# @example With complex calculation
|
|
53
|
-
# To.new(100) { object.items.count }
|
|
15
|
+
# @param expected [#eql?] the expected value after the block
|
|
16
|
+
# @yieldreturn [Object] the observed value
|
|
17
|
+
# @raise [ArgumentError] when no block is given
|
|
54
18
|
def initialize(expected, &state)
|
|
55
19
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
56
20
|
|
|
@@ -58,47 +22,19 @@ module Matchi
|
|
|
58
22
|
@state = state
|
|
59
23
|
end
|
|
60
24
|
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
# returns the expected value. It only cares about the final state, not any
|
|
65
|
-
# intermediate values or the initial state.
|
|
66
|
-
#
|
|
67
|
-
# @api public
|
|
68
|
-
#
|
|
69
|
-
# @yield [] Block that should cause the state change
|
|
70
|
-
# @yieldreturn [Object] The result of the block (not used)
|
|
71
|
-
#
|
|
72
|
-
# @return [Boolean] true if the final state matches the expected value
|
|
73
|
-
#
|
|
74
|
-
# @raise [ArgumentError] if no block is provided
|
|
75
|
-
#
|
|
76
|
-
# @example Basic usage
|
|
77
|
-
# text = "hello"
|
|
78
|
-
# matcher = To.new("HELLO") { text.to_s }
|
|
79
|
-
# matcher.match? { text.upcase! } # => true
|
|
80
|
-
#
|
|
81
|
-
# @example With method chaining
|
|
82
|
-
# array = [1, 2, 3]
|
|
83
|
-
# matcher = To.new(6) { array.sum }
|
|
84
|
-
# matcher.match? { array.map! { |x| x * 2 } } # => false
|
|
25
|
+
# @yieldreturn [Object] ignored, the block is evaluated for its effect
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
# @raise [ArgumentError] when no block is given
|
|
85
28
|
def match?
|
|
86
29
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
87
30
|
|
|
31
|
+
already = Eq.eql?(@expected, @state.call)
|
|
88
32
|
yield
|
|
89
|
-
value_after = @state.call
|
|
90
33
|
|
|
91
|
-
|
|
34
|
+
!already && Eq.eql?(@expected, @state.call)
|
|
92
35
|
end
|
|
93
36
|
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
# @api public
|
|
97
|
-
#
|
|
98
|
-
# @return [String] A string describing what this matcher verifies
|
|
99
|
-
#
|
|
100
|
-
# @example
|
|
101
|
-
# To.new("test").to_s # => 'change to "test"'
|
|
37
|
+
# @return [String]
|
|
102
38
|
def to_s
|
|
103
39
|
"change to #{@expected.inspect}"
|
|
104
40
|
end
|
data/lib/matchi/change.rb
CHANGED
|
@@ -1,85 +1,54 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative File.join("change", "to")
|
|
3
|
+
require_relative "change/delta"
|
|
4
|
+
require_relative "change/from"
|
|
5
|
+
require_relative "change/to"
|
|
6
|
+
require_relative "constant"
|
|
8
7
|
|
|
9
8
|
module Matchi
|
|
10
|
-
#
|
|
9
|
+
# State change matcher: passes when evaluating the block changes the value
|
|
10
|
+
# returned by a public method of an object.
|
|
11
|
+
#
|
|
12
|
+
# The observed method is called with `public_send` before and after the
|
|
13
|
+
# block, and the two values are compared with `eql?`. The observed method
|
|
14
|
+
# must return a value, not the mutated object itself: when it returns the
|
|
15
|
+
# very same unfrozen object twice, the comparison cannot tell whether that
|
|
16
|
+
# object changed, and `match?` raises instead of answering `false`.
|
|
17
|
+
# `attr_reader :items` with `items << x` is that case; observe
|
|
18
|
+
# `items.length` or use {#to}, which compares with the expected value. A
|
|
19
|
+
# frozen object returned twice counts as an unchanged value.
|
|
20
|
+
#
|
|
21
|
+
# The refinements {#by}, {#by_at_least}, {#by_at_most}, {#from} and {#to}
|
|
22
|
+
# build more specific matchers. `Change`, `by`, `by_at_least` and
|
|
23
|
+
# `by_at_most` compare values produced by the observed object, so they
|
|
24
|
+
# depend on its honesty, like {Matchi::Predicate}. `to` and `from … to`
|
|
25
|
+
# compare with the expected value, like {Matchi::Eq}. Whatever the
|
|
26
|
+
# observed method raises propagates.
|
|
27
|
+
#
|
|
28
|
+
# @example
|
|
29
|
+
# list = []
|
|
30
|
+
# Matchi::Change.new(list, :length).match? { list << 1 } # => true
|
|
31
|
+
# Matchi::Change.new(list, :length).by(1).match? { list << 1 } # => true
|
|
11
32
|
class Change
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# @
|
|
16
|
-
#
|
|
17
|
-
# @
|
|
18
|
-
# @param method [Symbol] The name of the method to track
|
|
19
|
-
# @param args [Array] Additional positional arguments to pass to the method
|
|
20
|
-
# @param kwargs [Hash] Additional keyword arguments to pass to the method
|
|
21
|
-
# @param block [Proc] Optional block to pass to the method
|
|
22
|
-
#
|
|
23
|
-
# @raise [ArgumentError] if method is not a Symbol
|
|
24
|
-
# @raise [ArgumentError] if object doesn't respond to method
|
|
25
|
-
#
|
|
26
|
-
# @return [Change] a new instance of the change wrapper
|
|
27
|
-
#
|
|
28
|
-
# @example Basic initialization
|
|
29
|
-
# array = []
|
|
30
|
-
# Change.new(array, :length) # Track array length changes
|
|
31
|
-
#
|
|
32
|
-
# @example With positional arguments
|
|
33
|
-
# hash = { key: "value" }
|
|
34
|
-
# Change.new(hash, :fetch, :key) # Track specific key value
|
|
35
|
-
#
|
|
36
|
-
# @example With keyword arguments
|
|
37
|
-
# hash = { a: 1, b: 2 }
|
|
38
|
-
# Change.new(hash, :fetch, default: 0) # Track with default value
|
|
39
|
-
#
|
|
40
|
-
# @example With block
|
|
41
|
-
# hash = { a: 1 }
|
|
42
|
-
# Change.new(hash, :fetch, :b) { |k| k.to_s } # Track with block default
|
|
33
|
+
# @param object [Object] the object to observe
|
|
34
|
+
# @param method [Symbol, String] a public method of that object
|
|
35
|
+
# @param args [Array] positional arguments for the method
|
|
36
|
+
# @param kwargs [Hash] keyword arguments for the method
|
|
37
|
+
# @param block [Proc, nil] a block for the method
|
|
38
|
+
# @raise [ArgumentError] when method is neither a Symbol nor a String
|
|
43
39
|
def initialize(object, method, *args, **kwargs, &block)
|
|
44
|
-
raise ::ArgumentError, "method must be a Symbol" unless
|
|
45
|
-
raise ::ArgumentError, "object must respond to method" unless object.respond_to?(method)
|
|
40
|
+
raise ::ArgumentError, "method must be a Symbol or a String (got: #{method.inspect})" unless name?(method)
|
|
46
41
|
|
|
47
|
-
@
|
|
42
|
+
@object = object
|
|
43
|
+
@method = method.to_sym
|
|
44
|
+
@state = -> { ::Kernel.instance_method(:public_send).bind_call(object, @method, *args, **kwargs, &block) }
|
|
48
45
|
end
|
|
49
46
|
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
# @api public
|
|
57
|
-
#
|
|
58
|
-
# @yield [] Block during which the change should occur
|
|
59
|
-
# @yieldreturn [Object] Result of the block execution (not used)
|
|
60
|
-
#
|
|
61
|
-
# @return [Boolean] true if the value changed, false otherwise
|
|
62
|
-
#
|
|
63
|
-
# @raise [ArgumentError] if no block is provided
|
|
64
|
-
#
|
|
65
|
-
# @example Basic usage with array length
|
|
66
|
-
# array = []
|
|
67
|
-
# matcher = Change.new(array, :length)
|
|
68
|
-
# matcher.match? { array << "item" } # => true
|
|
69
|
-
# matcher.match? { array.clear } # => true (from 1 to 0)
|
|
70
|
-
# matcher.match? { array.dup } # => false (no change)
|
|
71
|
-
#
|
|
72
|
-
# @example With method parameters
|
|
73
|
-
# hash = { key: "old" }
|
|
74
|
-
# matcher = Change.new(hash, :fetch, :key)
|
|
75
|
-
# matcher.match? { hash[:key] = "new" } # => true
|
|
76
|
-
# matcher.match? { hash[:key] = "new" } # => false (same value)
|
|
77
|
-
#
|
|
78
|
-
# @example With computed values
|
|
79
|
-
# text = "hello"
|
|
80
|
-
# matcher = Change.new(text, :upcase)
|
|
81
|
-
# matcher.match? { text.upcase! } # => true
|
|
82
|
-
# matcher.match? { text.upcase! } # => false (already uppercase)
|
|
47
|
+
# @yieldreturn [Object] ignored, the block is evaluated for its effect
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
# @raise [ArgumentError] when no block is given
|
|
50
|
+
# @raise [NoMethodError] when the object has no such public method
|
|
51
|
+
# @raise [TypeError] when the observed method returns the same unfrozen object twice
|
|
83
52
|
def match?
|
|
84
53
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
85
54
|
|
|
@@ -87,98 +56,54 @@ module Matchi
|
|
|
87
56
|
yield
|
|
88
57
|
value_after = @state.call
|
|
89
58
|
|
|
90
|
-
!value_before.eql?(value_after)
|
|
59
|
+
return !value_before.eql?(value_after) unless value_before.equal?(value_after)
|
|
60
|
+
return false if ::Kernel.instance_method(:frozen?).bind_call(value_before)
|
|
61
|
+
|
|
62
|
+
raise ::TypeError, "#{self} returned the same unfrozen object twice: observe a value, not the object"
|
|
91
63
|
end
|
|
92
64
|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
# @api public
|
|
96
|
-
#
|
|
97
|
-
# @return [String] A string describing what this matcher verifies
|
|
98
|
-
#
|
|
99
|
-
# @example
|
|
100
|
-
# Change.new("test", :upcase).to_s # => 'eq "test"'
|
|
65
|
+
# @return [String] the observed class and method, such as `change Array#length`
|
|
101
66
|
def to_s
|
|
102
|
-
"change #{@
|
|
67
|
+
"change #{Constant.label(::Kernel.instance_method(:class).bind_call(@object))}##{@method}"
|
|
103
68
|
end
|
|
104
69
|
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
# @
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
#
|
|
111
|
-
# @return [#match?] A matcher that verifies the minimum change
|
|
112
|
-
#
|
|
113
|
-
# @example
|
|
114
|
-
# counter = 0
|
|
115
|
-
# matcher = Change.new(counter, :to_i).by_at_least(5)
|
|
116
|
-
# matcher.match? { counter += 6 } # => true
|
|
117
|
-
def by_at_least(minimum_delta)
|
|
118
|
-
ByAtLeast.new(minimum_delta, &@state)
|
|
70
|
+
# @param delta [Numeric] the exact expected difference, negative allowed
|
|
71
|
+
# @return [Matchi::Change::Delta]
|
|
72
|
+
# @raise [ArgumentError] when delta is not a finite real Numeric
|
|
73
|
+
def by(delta)
|
|
74
|
+
Delta.new(:by, delta, &@state)
|
|
119
75
|
end
|
|
120
76
|
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
# @
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
#
|
|
127
|
-
# @return [#match?] A matcher that verifies the maximum change
|
|
128
|
-
#
|
|
129
|
-
# @example
|
|
130
|
-
# counter = 0
|
|
131
|
-
# matcher = Change.new(counter, :to_i).by_at_most(5)
|
|
132
|
-
# matcher.match? { counter += 3 } # => true
|
|
133
|
-
def by_at_most(maximum_delta)
|
|
134
|
-
ByAtMost.new(maximum_delta, &@state)
|
|
77
|
+
# @param delta [Numeric] the smallest acceptable increase, zero or positive
|
|
78
|
+
# @return [Matchi::Change::Delta]
|
|
79
|
+
# @raise [ArgumentError] when delta is not a finite real non-negative Numeric
|
|
80
|
+
def by_at_least(delta)
|
|
81
|
+
Delta.new(:by_at_least, delta, &@state)
|
|
135
82
|
end
|
|
136
83
|
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
# @
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
#
|
|
143
|
-
# @return [#match?] A matcher that verifies the exact change
|
|
144
|
-
#
|
|
145
|
-
# @example
|
|
146
|
-
# counter = 0
|
|
147
|
-
# matcher = Change.new(counter, :to_i).by(5)
|
|
148
|
-
# matcher.match? { counter += 5 } # => true
|
|
149
|
-
def by(delta)
|
|
150
|
-
By.new(delta, &@state)
|
|
84
|
+
# @param delta [Numeric] the largest acceptable increase, zero or positive
|
|
85
|
+
# @return [Matchi::Change::Delta]
|
|
86
|
+
# @raise [ArgumentError] when delta is not a finite real non-negative Numeric
|
|
87
|
+
def by_at_most(delta)
|
|
88
|
+
Delta.new(:by_at_most, delta, &@state)
|
|
151
89
|
end
|
|
152
90
|
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
# @api public
|
|
156
|
-
#
|
|
157
|
-
# @param old_value [#object_id] The expected initial value
|
|
158
|
-
#
|
|
159
|
-
# @return [#to] A wrapper for creating a from/to matcher
|
|
160
|
-
#
|
|
161
|
-
# @example
|
|
162
|
-
# string = "foo"
|
|
163
|
-
# Change.new(string, :to_s).from("foo").to("FOO")
|
|
91
|
+
# @param old_value [#eql?] the expected value before the block
|
|
92
|
+
# @return [Matchi::Change::From] a builder; call `to` on it
|
|
164
93
|
def from(old_value)
|
|
165
94
|
From.new(old_value, &@state)
|
|
166
95
|
end
|
|
167
96
|
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
# @api public
|
|
171
|
-
#
|
|
172
|
-
# @param new_value [#object_id] The expected final value
|
|
173
|
-
#
|
|
174
|
-
# @return [#match?] A matcher that verifies the final state
|
|
175
|
-
#
|
|
176
|
-
# @example
|
|
177
|
-
# string = "foo"
|
|
178
|
-
# matcher = Change.new(string, :to_s).to("FOO")
|
|
179
|
-
# matcher.match? { string.upcase! } # => true
|
|
97
|
+
# @param new_value [#eql?] the expected value after the block
|
|
98
|
+
# @return [Matchi::Change::To]
|
|
180
99
|
def to(new_value)
|
|
181
100
|
To.new(new_value, &@state)
|
|
182
101
|
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def name?(method)
|
|
106
|
+
::Symbol === method || ::String === method
|
|
107
|
+
end
|
|
183
108
|
end
|
|
184
109
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matchi
|
|
4
|
+
# Shared handling of an expected module given directly or by name.
|
|
5
|
+
#
|
|
6
|
+
# A name is a String or a Symbol holding a constant path such as
|
|
7
|
+
# `"Comparable"` or `:"Foo::Bar"`. It is validated when the matcher is built
|
|
8
|
+
# and resolved from `Object` when the matcher runs, which lets a
|
|
9
|
+
# specification be written before the class it describes is loaded. Since
|
|
10
|
+
# resolution goes through `Object.const_get`, an `autoload` or an
|
|
11
|
+
# `Object.const_missing` defined by the code under test takes part in it.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
module Constant
|
|
15
|
+
# A constant path: ASCII segments that start with a capital letter,
|
|
16
|
+
# joined by `::`, such as `"Comparable"` or `"File::Stat"`.
|
|
17
|
+
NAME = /\A[A-Z]\w*(::[A-Z]\w*)*\z/
|
|
18
|
+
|
|
19
|
+
# @param expected [Module, String, Symbol] a module, or its name
|
|
20
|
+
# @param kind [Class] `Module` or `Class`, what expected must be
|
|
21
|
+
# @return [Module, String] the module itself, or the validated name
|
|
22
|
+
# @raise [ArgumentError] when expected is neither
|
|
23
|
+
def self.normalize(expected, kind)
|
|
24
|
+
return expected if kind === expected
|
|
25
|
+
return String(expected) if (::String === expected || ::Symbol === expected) && NAME.match?(expected)
|
|
26
|
+
|
|
27
|
+
raise ::ArgumentError, "expected must be a #{kind} or a constant name (got: #{expected.inspect})"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param expected [Module, String] a module, or a validated name
|
|
31
|
+
# @return [String] the name, or a stable label for an anonymous module
|
|
32
|
+
#
|
|
33
|
+
# The name is read with `Module#name` bound to the module, so a class
|
|
34
|
+
# of the code under test that redefines `name` cannot break a report.
|
|
35
|
+
def self.label(expected)
|
|
36
|
+
return expected unless ::Module === expected
|
|
37
|
+
|
|
38
|
+
::Module.instance_method(:name).bind_call(expected) || "an anonymous #{::Class === expected ? "class" : "module"}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param expected [Module, String] a module, or a validated name
|
|
42
|
+
# @param kind [Class] `Module` or `Class`, what the name must resolve to
|
|
43
|
+
# @return [Module]
|
|
44
|
+
# @raise [NameError] when the name is not defined
|
|
45
|
+
# @raise [TypeError] when the name does not resolve to the expected kind
|
|
46
|
+
def self.resolve(expected, kind)
|
|
47
|
+
return expected if kind === expected
|
|
48
|
+
|
|
49
|
+
value = ::Object.const_get(expected)
|
|
50
|
+
raise ::TypeError, "#{expected} is not a #{kind}" unless kind === value
|
|
51
|
+
|
|
52
|
+
value
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/matchi/eq.rb
CHANGED
|
@@ -1,85 +1,44 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Matchi
|
|
4
|
-
#
|
|
4
|
+
# Equivalence matcher: passes when the actual value is of the same class as
|
|
5
|
+
# the expected value and `eql?` to it.
|
|
5
6
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# The expected value receives the `eql?` call, so the actual value cannot
|
|
8
|
+
# lie. The class check comes first because a few core `eql?` methods, such
|
|
9
|
+
# as `Hash#eql?`, hand the comparison back to an operand of another class;
|
|
10
|
+
# it costs nothing, since `eql?` never holds across classes for core values.
|
|
9
11
|
#
|
|
10
|
-
# @example
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# matcher.match? { "test".dup } # => true
|
|
14
|
-
# matcher.match? { "other" } # => false
|
|
15
|
-
#
|
|
16
|
-
# @example With numbers
|
|
17
|
-
# matcher = Matchi::Eq.new(42)
|
|
18
|
-
# matcher.match? { 42 } # => true
|
|
19
|
-
# matcher.match? { 42.0 } # => false # Different types
|
|
20
|
-
# matcher.match? { 43 } # => false
|
|
21
|
-
#
|
|
22
|
-
# @example With collections
|
|
23
|
-
# array = [1, 2, 3]
|
|
24
|
-
# matcher = Matchi::Eq.new(array)
|
|
25
|
-
# matcher.match? { array.dup } # => true # Same values
|
|
26
|
-
# matcher.match? { array } # => true # Same object
|
|
27
|
-
# matcher.match? { [1, 2, 3] } # => true # Same values
|
|
28
|
-
# matcher.match? { [1, 2] } # => false # Different values
|
|
29
|
-
#
|
|
30
|
-
# @see https://ruby-doc.org/core/Object.html#method-i-eql-3F
|
|
31
|
-
# @see Matchi::Be
|
|
12
|
+
# @example
|
|
13
|
+
# Matchi::Eq.new("foo").match? { "foo" } # => true
|
|
14
|
+
# Matchi::Eq.new(1).match? { 1.0 } # => false (eql? is strict on type)
|
|
32
15
|
class Eq
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# @api public
|
|
36
|
-
#
|
|
37
|
-
# @param expected [#eql?] The expected equivalent value
|
|
38
|
-
#
|
|
39
|
-
# @return [Eq] a new instance of the matcher
|
|
40
|
-
#
|
|
41
|
-
# @example
|
|
42
|
-
# Eq.new("test") # Match strings with same value
|
|
43
|
-
# Eq.new([1, 2, 3]) # Match arrays with same elements
|
|
16
|
+
# @param expected [#eql?] the expected value
|
|
44
17
|
def initialize(expected)
|
|
45
18
|
@expected = expected
|
|
46
19
|
end
|
|
47
20
|
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
# Two objects are considered equivalent if they have the same value, even if
|
|
52
|
-
# they are different instances.
|
|
53
|
-
#
|
|
54
|
-
# @api public
|
|
55
|
-
#
|
|
56
|
-
# @yield [] Block that returns the object to check
|
|
57
|
-
# @yieldreturn [Object] The object to verify equivalence with
|
|
58
|
-
#
|
|
59
|
-
# @return [Boolean] true if both objects have equivalent values
|
|
60
|
-
#
|
|
61
|
-
# @raise [ArgumentError] if no block is provided
|
|
62
|
-
#
|
|
63
|
-
# @example
|
|
64
|
-
# matcher = Eq.new([1, 2, 3])
|
|
65
|
-
# matcher.match? { [1, 2, 3] } # => true
|
|
66
|
-
# matcher.match? { [1, 2, 3].dup } # => true
|
|
21
|
+
# @yieldreturn [Object] the actual value
|
|
22
|
+
# @return [Boolean]
|
|
23
|
+
# @raise [ArgumentError] when no block is given
|
|
67
24
|
def match?
|
|
68
25
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
69
26
|
|
|
70
|
-
|
|
27
|
+
Eq.eql?(@expected, yield)
|
|
71
28
|
end
|
|
72
29
|
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
# @api public
|
|
76
|
-
#
|
|
77
|
-
# @return [String] A string describing what this matcher verifies
|
|
78
|
-
#
|
|
79
|
-
# @example
|
|
80
|
-
# Eq.new("test").to_s # => 'eq "test"'
|
|
30
|
+
# @return [String]
|
|
81
31
|
def to_s
|
|
82
32
|
"eq #{@expected.inspect}"
|
|
83
33
|
end
|
|
34
|
+
|
|
35
|
+
# @param expected [#eql?] the value that receives the comparison
|
|
36
|
+
# @param actual [Object] a value from the code under test
|
|
37
|
+
# @return [Boolean] whether actual is of the class of expected and `eql?` to it
|
|
38
|
+
def self.eql?(expected, actual)
|
|
39
|
+
return false unless ::Kernel.instance_method(:class).bind_call(actual).equal?(expected.class)
|
|
40
|
+
|
|
41
|
+
expected.eql?(actual) ? true : false
|
|
42
|
+
end
|
|
84
43
|
end
|
|
85
44
|
end
|
data/lib/matchi/match.rb
CHANGED
|
@@ -1,83 +1,47 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Matchi
|
|
4
|
-
#
|
|
4
|
+
# Regular expression matcher: passes when the actual value is a String or a
|
|
5
|
+
# Symbol that the expected Regexp matches. Any other actual value fails, as
|
|
6
|
+
# does a string with an invalid or incompatible encoding.
|
|
5
7
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
8
|
+
# Only a `Regexp` is accepted as pattern: with a `String` pattern,
|
|
9
|
+
# `String#match?` would turn the actual value into the regular expression,
|
|
10
|
+
# handing the comparison to the code under test. Use {Matchi::Satisfy} for
|
|
11
|
+
# other predicates.
|
|
9
12
|
#
|
|
10
|
-
# @example
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# matcher.match? { "other_string" } # => false
|
|
14
|
-
#
|
|
15
|
-
# @example Case sensitivity
|
|
16
|
-
# matcher = Matchi::Match.new(/^test$/i)
|
|
17
|
-
# matcher.match? { "TEST" } # => true
|
|
18
|
-
# matcher.match? { "Test" } # => true
|
|
19
|
-
# matcher.match? { "testing" } # => false
|
|
20
|
-
#
|
|
21
|
-
# @example Multiline patterns
|
|
22
|
-
# matcher = Matchi::Match.new(/\A\d+\Z/m)
|
|
23
|
-
# matcher.match? { "123" } # => true
|
|
24
|
-
# matcher.match? { "12.3" } # => false
|
|
25
|
-
#
|
|
26
|
-
# @see https://ruby-doc.org/core/Regexp.html#method-i-match-3F
|
|
13
|
+
# @example
|
|
14
|
+
# Matchi::Match.new(/^foo/).match? { "foobar" } # => true
|
|
15
|
+
# Matchi::Match.new(/^foo/).match? { 42 } # => false
|
|
27
16
|
class Match
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# @api public
|
|
31
|
-
#
|
|
32
|
-
# @param expected [#match?] A pattern that responds to match?
|
|
33
|
-
#
|
|
34
|
-
# @raise [ArgumentError] if the pattern doesn't respond to match?
|
|
35
|
-
#
|
|
36
|
-
# @return [Match] a new instance of the matcher
|
|
37
|
-
#
|
|
38
|
-
# @example
|
|
39
|
-
# Match.new(/\d+/) # Match digits
|
|
40
|
-
# Match.new(/^test$/i) # Case-insensitive match
|
|
41
|
-
# Match.new(/\A\w+\Z/) # Full string word characters
|
|
17
|
+
# @param expected [Regexp] the pattern
|
|
18
|
+
# @raise [ArgumentError] when expected is not a Regexp
|
|
42
19
|
def initialize(expected)
|
|
43
|
-
raise ::ArgumentError, "expected must
|
|
20
|
+
raise ::ArgumentError, "expected must be a Regexp (got: #{expected.inspect})" unless ::Regexp === expected
|
|
44
21
|
|
|
45
22
|
@expected = expected
|
|
46
23
|
end
|
|
47
24
|
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
# The match is performed on the entire string unless the pattern specifically
|
|
52
|
-
# allows partial matches.
|
|
53
|
-
#
|
|
54
|
-
# @api public
|
|
55
|
-
#
|
|
56
|
-
# @yield [] Block that returns the value to check
|
|
57
|
-
# @yieldreturn [#to_s] The value to match against the pattern
|
|
58
|
-
#
|
|
59
|
-
# @return [Boolean] true if the value matches the pattern
|
|
60
|
-
#
|
|
61
|
-
# @raise [ArgumentError] if no block is provided
|
|
62
|
-
#
|
|
63
|
-
# @example
|
|
64
|
-
# matcher = Match.new(/^\d{3}-\d{2}-\d{4}$/)
|
|
65
|
-
# matcher.match? { "123-45-6789" } # => true
|
|
66
|
-
# matcher.match? { "123456789" } # => false
|
|
25
|
+
# @yieldreturn [Object] the actual value
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
# @raise [ArgumentError] when no block is given
|
|
67
28
|
def match?
|
|
68
29
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
69
30
|
|
|
70
|
-
|
|
31
|
+
actual = yield
|
|
32
|
+
return false unless ::String === actual || ::Symbol === actual
|
|
33
|
+
|
|
34
|
+
string = ::Symbol === actual ? actual.name : actual
|
|
35
|
+
return false unless string.valid_encoding?
|
|
36
|
+
|
|
37
|
+
begin
|
|
38
|
+
@expected.match?(string)
|
|
39
|
+
rescue ::Encoding::CompatibilityError
|
|
40
|
+
false
|
|
41
|
+
end
|
|
71
42
|
end
|
|
72
43
|
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
# @api public
|
|
76
|
-
#
|
|
77
|
-
# @return [String] A string describing what this matcher verifies
|
|
78
|
-
#
|
|
79
|
-
# @example
|
|
80
|
-
# Match.new(/^test/).to_s # => "match /^test/"
|
|
44
|
+
# @return [String]
|
|
81
45
|
def to_s
|
|
82
46
|
"match #{@expected.inspect}"
|
|
83
47
|
end
|