matchi 4.2.0 → 6.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.
@@ -1,158 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Matchi
4
- class Change
5
- # Minimum delta matcher that verifies numeric changes meet or exceed a threshold.
6
- #
7
- # This matcher ensures that a numeric value changes by at least the specified amount
8
- # after executing a block of code. It's particularly useful when testing operations
9
- # where you want to ensure a minimum change occurs but larger changes are acceptable,
10
- # such as performance improvements, resource allocation, or progressive counters.
11
- #
12
- # @example Testing collection growth
13
- # items = []
14
- # matcher = Matchi::Change::ByAtLeast.new(2) { items.size }
15
- # matcher.match? { items.push(1, 2) } # => true # Changed by exactly 2
16
- # matcher.match? { items.push(1, 2, 3) } # => true # Changed by more than 2
17
- # matcher.match? { items.push(1) } # => false # Changed by less than 2
18
- #
19
- # @example Verifying performance improvements
20
- # class Benchmark
21
- # def initialize
22
- # @score = 100
23
- # end
24
- #
25
- # def optimize!
26
- # @score += rand(20..30) # Improvement varies
27
- # end
28
- #
29
- # def score
30
- # @score
31
- # end
32
- # end
33
- #
34
- # benchmark = Benchmark.new
35
- # matcher = Matchi::Change::ByAtLeast.new(20) { benchmark.score }
36
- # matcher.match? { benchmark.optimize! } # => true # Any improvement >= 20 passes
37
- #
38
- # @example Resource allocation
39
- # class Pool
40
- # def initialize
41
- # @capacity = 1000
42
- # end
43
- #
44
- # def allocate(minimum, maximum)
45
- # actual = rand(minimum..maximum)
46
- # @capacity -= actual
47
- # actual
48
- # end
49
- #
50
- # def available
51
- # @capacity
52
- # end
53
- # end
54
- #
55
- # pool = Pool.new
56
- # matcher = Matchi::Change::ByAtLeast.new(50) { -pool.available }
57
- # matcher.match? { pool.allocate(50, 100) } # => true # Allocates at least 50
58
- #
59
- # @example Price threshold monitoring
60
- # class Stock
61
- # attr_reader :price
62
- #
63
- # def initialize(price)
64
- # @price = price
65
- # end
66
- #
67
- # def fluctuate!
68
- # @price += rand(-10.0..20.0)
69
- # end
70
- # end
71
- #
72
- # stock = Stock.new(100.0)
73
- # matcher = Matchi::Change::ByAtLeast.new(10.0) { stock.price }
74
- # matcher.match? { stock.fluctuate! } # => true if price rises by 10.0 or more
75
- #
76
- # @note This matcher verifies minimum changes only. For exact changes, use By,
77
- # and for maximum changes, use ByAtMost.
78
- #
79
- # @see Matchi::Change::By For exact change validation
80
- # @see Matchi::Change::ByAtMost For maximum change validation
81
- # @see Matchi::Change::To For final value validation
82
- class ByAtLeast
83
- # Initialize the matcher with a minimum expected change and a state block.
84
- #
85
- # @api public
86
- #
87
- # @param expected [Numeric] The minimum amount by which the value should change
88
- # @param state [Proc] Block that retrieves the current value
89
- #
90
- # @raise [ArgumentError] if expected is not a Numeric
91
- # @raise [ArgumentError] if expected is negative
92
- # @raise [ArgumentError] if no state block is provided
93
- #
94
- # @return [ByAtLeast] a new instance of the matcher
95
- #
96
- # @example With integer minimum
97
- # ByAtLeast.new(5) { counter.value }
98
- #
99
- # @example With floating point minimum
100
- # ByAtLeast.new(0.5) { temperature.celsius }
101
- def initialize(expected, &state)
102
- raise ::ArgumentError, "expected must be a Numeric" unless expected.is_a?(::Numeric)
103
- raise ::ArgumentError, "a block must be provided" unless block_given?
104
- raise ::ArgumentError, "expected must be non-negative" if expected.negative?
105
-
106
- @expected = expected
107
- @state = state
108
- end
109
-
110
- # Checks if the value changes by at least the expected amount.
111
- #
112
- # This method compares the value before and after executing the provided block,
113
- # ensuring that the difference is greater than or equal to the expected minimum.
114
- # This is useful when you want to verify that a change meets a minimum threshold.
115
- #
116
- # @api public
117
- #
118
- # @yield [] Block that should cause the state change
119
- # @yieldreturn [Object] The result of the block (not used)
120
- #
121
- # @return [Boolean] true if the value changed by at least the expected amount
122
- #
123
- # @raise [ArgumentError] if no block is provided
124
- #
125
- # @example Basic usage
126
- # counter = 0
127
- # matcher = ByAtLeast.new(5) { counter }
128
- # matcher.match? { counter += 7 } # => true # Changed by more than minimum
129
- #
130
- # @example Edge case - exact minimum
131
- # items = []
132
- # matcher = ByAtLeast.new(2) { items.size }
133
- # matcher.match? { items.push(1, 2) } # => true # Changed by exactly minimum
134
- def match?
135
- raise ::ArgumentError, "a block must be provided" unless block_given?
136
-
137
- value_before = @state.call
138
- yield
139
- value_after = @state.call
140
-
141
- @expected <= (value_after - value_before)
142
- end
143
-
144
- # Returns a human-readable description of the matcher.
145
- #
146
- # @api public
147
- #
148
- # @return [String] A string describing what this matcher verifies
149
- #
150
- # @example
151
- # ByAtLeast.new(5).to_s # => "change by at least 5"
152
- # ByAtLeast.new(2.5).to_s # => "change by at least 2.5"
153
- def to_s
154
- "change by at least #{@expected.inspect}"
155
- end
156
- end
157
- end
158
- end
@@ -1,171 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Matchi
4
- class Change
5
- # Maximum delta matcher that verifies numeric changes don't exceed a limit.
6
- #
7
- # This matcher ensures that a numeric value changes by no more than the specified amount
8
- # after executing a block of code. It's particularly useful when testing operations
9
- # where you want to enforce an upper bound on changes, such as rate limiting,
10
- # resource consumption, or controlled increments.
11
- #
12
- # @example Testing controlled collection growth
13
- # items = []
14
- # matcher = Matchi::Change::ByAtMost.new(2) { items.size }
15
- # matcher.match? { items.push(1) } # => true # Changed by less than limit
16
- # matcher.match? { items.push(1, 2) } # => true # Changed by exactly limit
17
- # matcher.match? { items.push(1, 2, 3) } # => false # Changed by more than limit
18
- #
19
- # @example Rate limiting
20
- # class RateLimiter
21
- # def initialize
22
- # @requests = 0
23
- # end
24
- #
25
- # def process_batch(items)
26
- # items.each do |item|
27
- # break if @requests >= 3 # Rate limit
28
- # process_item(item)
29
- # end
30
- # end
31
- #
32
- # private
33
- #
34
- # def process_item(item)
35
- # @requests += 1
36
- # # Processing logic...
37
- # end
38
- #
39
- # def requests
40
- # @requests
41
- # end
42
- # end
43
- #
44
- # limiter = RateLimiter.new
45
- # matcher = Matchi::Change::ByAtMost.new(3) { limiter.requests }
46
- # matcher.match? { limiter.process_batch([1, 2, 3, 4, 5]) } # => true
47
- #
48
- # @example Resource consumption
49
- # class ResourcePool
50
- # attr_reader :used
51
- #
52
- # def initialize
53
- # @used = 0
54
- # end
55
- #
56
- # def allocate(requested)
57
- # available = 5 - @used # Maximum pool size is 5
58
- # granted = [requested, available].min
59
- # @used += granted
60
- # granted
61
- # end
62
- # end
63
- #
64
- # pool = ResourcePool.new
65
- # matcher = Matchi::Change::ByAtMost.new(2) { pool.used }
66
- # matcher.match? { pool.allocate(2) } # => true
67
- # matcher.match? { pool.allocate(3) } # => false
68
- #
69
- # @example Score adjustments
70
- # class GameScore
71
- # attr_reader :value
72
- #
73
- # def initialize
74
- # @value = 100
75
- # end
76
- #
77
- # def apply_penalty(amount)
78
- # max_penalty = 10
79
- # actual_penalty = [amount, max_penalty].min
80
- # @value -= actual_penalty
81
- # end
82
- # end
83
- #
84
- # score = GameScore.new
85
- # matcher = Matchi::Change::ByAtMost.new(10) { -score.value }
86
- # matcher.match? { score.apply_penalty(5) } # => true # Small penalty
87
- # matcher.match? { score.apply_penalty(15) } # => true # Limited to max
88
- #
89
- # @note This matcher verifies maximum changes only. For exact changes, use By,
90
- # and for minimum changes, use ByAtLeast.
91
- #
92
- # @see Matchi::Change::By For exact change validation
93
- # @see Matchi::Change::ByAtLeast For minimum change validation
94
- # @see Matchi::Change::To For final value validation
95
- class ByAtMost
96
- # Initialize the matcher with a maximum allowed change and a state block.
97
- #
98
- # @api public
99
- #
100
- # @param expected [Numeric] The maximum amount by which the value should change
101
- # @param state [Proc] Block that retrieves the current value
102
- #
103
- # @raise [ArgumentError] if expected is not a Numeric
104
- # @raise [ArgumentError] if expected is negative
105
- # @raise [ArgumentError] if no state block is provided
106
- #
107
- # @return [ByAtMost] a new instance of the matcher
108
- #
109
- # @example With integer maximum
110
- # ByAtMost.new(5) { counter.value }
111
- #
112
- # @example With floating point maximum
113
- # ByAtMost.new(0.5) { temperature.celsius }
114
- def initialize(expected, &state)
115
- raise ::ArgumentError, "expected must be a Numeric" unless expected.is_a?(::Numeric)
116
- raise ::ArgumentError, "a block must be provided" unless block_given?
117
- raise ::ArgumentError, "expected must be non-negative" if expected.negative?
118
-
119
- @expected = expected
120
- @state = state
121
- end
122
-
123
- # Checks if the value changes by no more than the expected amount.
124
- #
125
- # This method compares the value before and after executing the provided block,
126
- # ensuring that the absolute difference is less than or equal to the expected maximum.
127
- # This is useful for enforcing upper bounds on state changes.
128
- #
129
- # @api public
130
- #
131
- # @yield [] Block that should cause the state change
132
- # @yieldreturn [Object] The result of the block (not used)
133
- #
134
- # @return [Boolean] true if the value changed by at most the expected amount
135
- #
136
- # @raise [ArgumentError] if no block is provided
137
- #
138
- # @example Basic usage with growth
139
- # users = []
140
- # matcher = ByAtMost.new(2) { users.size }
141
- # matcher.match? { users.push('alice') } # => true
142
- #
143
- # @example With negative changes
144
- # stock = 10
145
- # matcher = ByAtMost.new(3) { stock }
146
- # matcher.match? { stock -= 2 } # => true
147
- def match?
148
- raise ::ArgumentError, "a block must be provided" unless block_given?
149
-
150
- value_before = @state.call
151
- yield
152
- value_after = @state.call
153
-
154
- @expected >= (value_after - value_before)
155
- end
156
-
157
- # Returns a human-readable description of the matcher.
158
- #
159
- # @api public
160
- #
161
- # @return [String] A string describing what this matcher verifies
162
- #
163
- # @example
164
- # ByAtMost.new(5).to_s # => "change by at most 5"
165
- # ByAtMost.new(2.5).to_s # => "change by at most 2.5"
166
- def to_s
167
- "change by at most #{@expected.inspect}"
168
- end
169
- end
170
- end
171
- end
@@ -1,132 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Matchi
4
- class Change
5
- class From
6
- # Value transition matcher that verifies both initial and final states of an operation.
7
- #
8
- # This matcher ensures that a value not only changes to an expected final state but also
9
- # starts from a specific initial state. This is particularly useful when testing state
10
- # transitions where both the starting and ending conditions are important, such as in
11
- # workflow systems, state machines, or data transformations.
12
- #
13
- # @example Basic string transformation
14
- # text = "hello"
15
- # matcher = Matchi::Change::From::To.new("hello", "HELLO") { text.to_s }
16
- # matcher.match? { text.upcase! } # => true
17
- # matcher.match? { text.reverse! } # => false # Wrong final state
18
- #
19
- # text = "other"
20
- # matcher.match? { text.upcase! } # => false # Wrong initial state
21
- #
22
- # @example State machine transitions
23
- # class Order
24
- # attr_accessor :status
25
- # def initialize(status)
26
- # @status = status
27
- # end
28
- # end
29
- #
30
- # order = Order.new(:pending)
31
- # matcher = Matchi::Change::From::To.new(:pending, :shipped) { order.status }
32
- # matcher.match? { order.status = :shipped } # => true
33
- #
34
- # @example Complex object transformations
35
- # class User
36
- # attr_accessor :permissions
37
- # def initialize
38
- # @permissions = [:read]
39
- # end
40
- #
41
- # def promote!
42
- # @permissions += [:write, :delete]
43
- # end
44
- # end
45
- #
46
- # user = User.new
47
- # matcher = Matchi::Change::From::To.new(
48
- # [:read],
49
- # [:read, :write, :delete]
50
- # ) { user.permissions }
51
- # matcher.match? { user.promote! } # => true
52
- #
53
- # @see Matchi::Change::To For checking only the final state
54
- # @see Matchi::Change::By For checking numeric changes
55
- class To
56
- # Initialize the matcher with expected initial and final values.
57
- #
58
- # @api public
59
- #
60
- # @param expected_init [#==] The expected initial value
61
- # @param expected_new_value [#==] The expected final value
62
- # @param state [Proc] Block that retrieves the current value
63
- #
64
- # @raise [ArgumentError] if no state block is provided
65
- #
66
- # @return [To] a new instance of the matcher
67
- #
68
- # @example With simple value
69
- # To.new("draft", "published") { document.status }
70
- #
71
- # @example With complex state
72
- # To.new([:user], [:user, :admin]) { account.roles }
73
- def initialize(expected_init, expected_new_value, &state)
74
- raise ::ArgumentError, "a block must be provided" unless block_given?
75
-
76
- @expected_init = expected_init
77
- @expected = expected_new_value
78
- @state = state
79
- end
80
-
81
- # Verifies both initial and final states during a transition.
82
- #
83
- # This method first checks if the initial state matches the expected value,
84
- # then executes the provided block and verifies the final state. The match
85
- # fails if either the initial or final state doesn't match expectations.
86
- #
87
- # @api public
88
- #
89
- # @yield [] Block that should cause the state transition
90
- # @yieldreturn [Object] The result of the block (not used)
91
- #
92
- # @return [Boolean] true if both initial and final states match expectations
93
- #
94
- # @raise [ArgumentError] if no block is provided
95
- #
96
- # @example Basic usage
97
- # text = "hello"
98
- # matcher = To.new("hello", "HELLO") { text }
99
- # matcher.match? { text.upcase! } # => true
100
- #
101
- # @example Failed initial state
102
- # text = "wrong"
103
- # matcher = To.new("hello", "HELLO") { text }
104
- # matcher.match? { text.upcase! } # => false
105
- def match?
106
- raise ::ArgumentError, "a block must be provided" unless block_given?
107
-
108
- value_before = @state.call
109
- return false unless @expected_init == value_before
110
-
111
- yield
112
- value_after = @state.call
113
-
114
- @expected == value_after
115
- end
116
-
117
- # Returns a human-readable description of the matcher.
118
- #
119
- # @api public
120
- #
121
- # @return [String] A string describing what this matcher verifies
122
- #
123
- # @example
124
- # To.new("draft", "published").to_s
125
- # # => 'change from "draft" to "published"'
126
- def to_s
127
- "change from #{@expected_init.inspect} to #{@expected.inspect}"
128
- end
129
- end
130
- end
131
- end
132
- end