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.rb
CHANGED
|
@@ -1,65 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# @example
|
|
14
|
-
# Matchi::
|
|
15
|
-
# Matchi::BeAKindOf.new(String).match? { 42 } # => false
|
|
16
|
-
#
|
|
17
|
-
# @example Verifying state changes
|
|
18
|
-
# array = []
|
|
19
|
-
# Matchi::Change.new(array, :length).by(2).match? { array.push(1, 2) } # => true
|
|
20
|
-
#
|
|
21
|
-
# @example Pattern matching with regular expressions
|
|
22
|
-
# Matchi::Match.new(/^test/).match? { "test_string" } # => true
|
|
23
|
-
#
|
|
24
|
-
# Each matcher in the Matchi ecosystem implements a consistent interface:
|
|
25
|
-
# - An initializer that sets up the expected values or conditions
|
|
26
|
-
# - A #match? method that takes a block and returns a boolean
|
|
27
|
-
# - An optional #to_s method that provides a human-readable description
|
|
28
|
-
#
|
|
29
|
-
# @example Creating a custom matcher
|
|
30
|
-
# module Matchi
|
|
31
|
-
# class BePositive
|
|
32
|
-
# def match?
|
|
33
|
-
# raise ArgumentError, "a block must be provided" unless block_given?
|
|
34
|
-
# yield.positive?
|
|
35
|
-
# end
|
|
36
|
-
#
|
|
37
|
-
# def to_s
|
|
38
|
-
# "be positive"
|
|
39
|
-
# end
|
|
40
|
-
# end
|
|
41
|
-
# end
|
|
42
|
-
#
|
|
43
|
-
# @see Matchi::Be
|
|
44
|
-
# @see Matchi::BeAKindOf
|
|
45
|
-
# @see Matchi::BeAnInstanceOf
|
|
46
|
-
# @see Matchi::BeWithin
|
|
47
|
-
# @see Matchi::Change
|
|
48
|
-
# @see Matchi::Eq
|
|
49
|
-
# @see Matchi::Match
|
|
50
|
-
# @see Matchi::Predicate
|
|
51
|
-
# @see Matchi::RaiseException
|
|
52
|
-
# @see Matchi::Satisfy
|
|
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
|
|
53
15
|
module Matchi
|
|
54
16
|
end
|
|
55
17
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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,22 +1,18 @@
|
|
|
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: 'Matchi
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
framework. The library offers a rich collection of built-in matchers for common
|
|
17
|
-
testing scenarios while maintaining a clear, consistent API that follows Ruby best
|
|
18
|
-
practices. With minimal setup required and support for custom matchers, Matchi enables
|
|
19
|
-
developers to write more reliable and maintainable tests.
|
|
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.
|
|
20
16
|
|
|
21
17
|
'
|
|
22
18
|
email: contact@cyril.email
|
|
@@ -31,23 +27,27 @@ files:
|
|
|
31
27
|
- lib/matchi/be_a_kind_of.rb
|
|
32
28
|
- lib/matchi/be_an_instance_of.rb
|
|
33
29
|
- lib/matchi/be_within.rb
|
|
30
|
+
- lib/matchi/be_within/of.rb
|
|
34
31
|
- lib/matchi/change.rb
|
|
35
|
-
- lib/matchi/change/
|
|
36
|
-
- lib/matchi/change/by_at_least.rb
|
|
37
|
-
- lib/matchi/change/by_at_most.rb
|
|
32
|
+
- lib/matchi/change/delta.rb
|
|
38
33
|
- lib/matchi/change/from.rb
|
|
39
|
-
- lib/matchi/change/
|
|
34
|
+
- lib/matchi/change/from_to.rb
|
|
40
35
|
- lib/matchi/change/to.rb
|
|
36
|
+
- lib/matchi/constant.rb
|
|
41
37
|
- lib/matchi/eq.rb
|
|
42
38
|
- lib/matchi/match.rb
|
|
39
|
+
- lib/matchi/number.rb
|
|
43
40
|
- lib/matchi/predicate.rb
|
|
44
41
|
- lib/matchi/raise_exception.rb
|
|
45
42
|
- lib/matchi/satisfy.rb
|
|
46
|
-
homepage: https://
|
|
43
|
+
homepage: https://fixrb.dev/gems/matchi/
|
|
47
44
|
licenses:
|
|
48
45
|
- MIT
|
|
49
46
|
metadata:
|
|
47
|
+
bug_tracker_uri: https://github.com/fixrb/matchi/issues
|
|
48
|
+
documentation_uri: https://rubydoc.info/gems/matchi
|
|
50
49
|
rubygems_mfa_required: 'true'
|
|
50
|
+
source_code_uri: https://github.com/fixrb/matchi
|
|
51
51
|
post_install_message:
|
|
52
52
|
rdoc_options: []
|
|
53
53
|
require_paths:
|
|
@@ -56,15 +56,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
56
56
|
requirements:
|
|
57
57
|
- - ">="
|
|
58
58
|
- !ruby/object:Gem::Version
|
|
59
|
-
version: 3.
|
|
59
|
+
version: 3.3.0
|
|
60
60
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
61
|
requirements:
|
|
62
62
|
- - ">="
|
|
63
63
|
- !ruby/object:Gem::Version
|
|
64
64
|
version: '0'
|
|
65
65
|
requirements: []
|
|
66
|
-
rubygems_version: 3.
|
|
66
|
+
rubygems_version: 3.5.22
|
|
67
67
|
signing_key:
|
|
68
68
|
specification_version: 4
|
|
69
|
-
summary:
|
|
69
|
+
summary: Framework-agnostic matchers for secure Ruby testing
|
|
70
70
|
test_files: []
|
data/lib/matchi/change/by.rb
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Matchi
|
|
4
|
-
class Change
|
|
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
|
|
59
|
-
class By
|
|
60
|
-
# Initialize the matcher with an expected delta and a state block.
|
|
61
|
-
#
|
|
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
|
|
69
|
-
#
|
|
70
|
-
# @return [By] a new instance of the matcher
|
|
71
|
-
#
|
|
72
|
-
# @example With positive delta
|
|
73
|
-
# By.new(5) { counter.value }
|
|
74
|
-
#
|
|
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 }
|
|
80
|
-
def initialize(expected, &state)
|
|
81
|
-
raise ::ArgumentError, "expected must be a Numeric" unless expected.is_a?(::Numeric)
|
|
82
|
-
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
83
|
-
|
|
84
|
-
@expected = expected
|
|
85
|
-
@state = state
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Verifies that the value changes by exactly the expected amount.
|
|
89
|
-
#
|
|
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.
|
|
93
|
-
#
|
|
94
|
-
# @api public
|
|
95
|
-
#
|
|
96
|
-
# @yield [] Block that should cause the state change
|
|
97
|
-
# @yieldreturn [Object] The result of the block (not used)
|
|
98
|
-
#
|
|
99
|
-
# @return [Boolean] true if the value changed by exactly the expected amount
|
|
100
|
-
#
|
|
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
|
|
112
|
-
def match?
|
|
113
|
-
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
114
|
-
|
|
115
|
-
value_before = @state.call
|
|
116
|
-
yield
|
|
117
|
-
value_after = @state.call
|
|
118
|
-
|
|
119
|
-
@expected == (value_after - value_before)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Returns a human-readable description of the matcher.
|
|
123
|
-
#
|
|
124
|
-
# @api public
|
|
125
|
-
#
|
|
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"
|
|
132
|
-
def to_s
|
|
133
|
-
"change by #{@expected.inspect}"
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
end
|
|
@@ -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
|