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.
- checksums.yaml +4 -4
- data/LICENSE.md +1 -1
- data/README.md +96 -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 +29 -64
- data/lib/matchi/number.rb +37 -0
- data/lib/matchi/predicate.rb +40 -139
- data/lib/matchi/raise_exception.rb +83 -105
- data/lib/matchi/satisfy.rb +21 -96
- data/lib/matchi.rb +22 -60
- metadata +18 -20
- 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
|
@@ -1,133 +1,111 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "constant"
|
|
4
|
+
|
|
3
5
|
module Matchi
|
|
4
|
-
# Exception matcher
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# a
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# @example Basic usage with standard exceptions
|
|
11
|
-
# matcher = Matchi::RaiseException.new(ArgumentError)
|
|
12
|
-
# matcher.match? { raise ArgumentError } # => true
|
|
13
|
-
# matcher.match? { raise RuntimeError } # => false
|
|
14
|
-
# matcher.match? { "no error" } # => false
|
|
15
|
-
#
|
|
16
|
-
# @example Working with inheritance hierarchy
|
|
17
|
-
# class CustomError < StandardError; end
|
|
18
|
-
# class SpecificError < CustomError; end
|
|
19
|
-
#
|
|
20
|
-
# matcher = Matchi::RaiseException.new(CustomError)
|
|
21
|
-
# matcher.match? { raise CustomError } # => true
|
|
22
|
-
# matcher.match? { raise SpecificError } # => true
|
|
23
|
-
# matcher.match? { raise StandardError } # => false
|
|
24
|
-
#
|
|
25
|
-
# @example Using string class names
|
|
26
|
-
# matcher = Matchi::RaiseException.new("ArgumentError")
|
|
27
|
-
# matcher.match? { raise ArgumentError } # => true
|
|
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.
|
|
28
11
|
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
12
|
+
# The matcher rescues an exception of the expected class, and any
|
|
13
|
+
# `StandardError`, `ScriptError`, `SystemStackError` or `SystemExit`,
|
|
14
|
+
# which it then compares with the expected class. Anything else, an
|
|
15
|
+
# `Interrupt`, a signal, a `NoMemoryError`, the exception with which an
|
|
16
|
+
# enclosing `Timeout` ends its block, goes through as it would with any
|
|
17
|
+
# other matcher, unless that is the class expected. The expected class can
|
|
18
|
+
# be given directly, or by name; see {Matchi::Constant}.
|
|
34
19
|
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
20
|
+
# @example
|
|
21
|
+
# Matchi::RaiseException.new(ZeroDivisionError).match? { 1 / 0 } # => true
|
|
22
|
+
# Matchi::RaiseException.new(:StandardError).match? { 1 / 0 } # => true
|
|
23
|
+
# Matchi::RaiseException.new(ZeroDivisionError, /by 0/).match? { 1 / 0 } # => true
|
|
24
|
+
# Matchi::RaiseException.new(ZeroDivisionError, "nope").match? { 1 / 0 } # => false
|
|
25
|
+
# Matchi::RaiseException.new(IOError).match? { 1 / 0 } # => false
|
|
40
26
|
class RaiseException
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
# @
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
# @return [RaiseException] a new instance of the matcher
|
|
51
|
-
#
|
|
52
|
-
# @example
|
|
53
|
-
# RaiseException.new(ArgumentError) # Using class
|
|
54
|
-
# RaiseException.new("ArgumentError") # Using string
|
|
55
|
-
# RaiseException.new(:ArgumentError) # Using symbol
|
|
56
|
-
# RaiseException.new("Api::CustomError") # Using namespaced class
|
|
57
|
-
def initialize(expected)
|
|
58
|
-
@expected = String(expected)
|
|
59
|
-
return if /\A[A-Z]/.match?(@expected)
|
|
60
|
-
|
|
61
|
-
raise ::ArgumentError,
|
|
62
|
-
"expected must start with an uppercase letter (got: #{@expected})"
|
|
27
|
+
# @param expected [Class, String, Symbol] the expected exception class, or its name
|
|
28
|
+
# @param message [String, Regexp, nil] the expected message, exact or as a pattern
|
|
29
|
+
# @raise [ArgumentError] when expected is neither an Exception class nor a constant name,
|
|
30
|
+
# or message is neither a String nor a Regexp
|
|
31
|
+
def initialize(expected, message = nil)
|
|
32
|
+
@expected = Constant.normalize(expected, ::Class)
|
|
33
|
+
@message = message
|
|
34
|
+
validate_class!
|
|
35
|
+
validate_message!
|
|
63
36
|
end
|
|
64
37
|
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
# @api public
|
|
72
|
-
#
|
|
73
|
-
# @yield [] Block that should raise an exception
|
|
74
|
-
# @yieldreturn [Object] The result of the block (if it doesn't raise)
|
|
75
|
-
#
|
|
76
|
-
# @return [Boolean] true if the block raises the expected exception
|
|
77
|
-
#
|
|
78
|
-
# @raise [ArgumentError] if no block is provided
|
|
79
|
-
# @raise [ArgumentError] if expected exception class doesn't inherit from Exception
|
|
80
|
-
# @raise [NameError] if the expected exception class cannot be found
|
|
81
|
-
#
|
|
82
|
-
# @example Standard usage
|
|
83
|
-
# matcher = RaiseException.new(ArgumentError)
|
|
84
|
-
# matcher.match? { raise ArgumentError } # => true
|
|
85
|
-
#
|
|
86
|
-
# @example With inheritance
|
|
87
|
-
# matcher = RaiseException.new(StandardError)
|
|
88
|
-
# matcher.match? { raise ArgumentError } # => true
|
|
89
|
-
#
|
|
90
|
-
# @example Without exception
|
|
91
|
-
# matcher = RaiseException.new(StandardError)
|
|
92
|
-
# matcher.match? { "no error" } # => false
|
|
38
|
+
# @yieldreturn [Object] ignored, the block is evaluated for its exception
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
# @raise [ArgumentError] when no block is given
|
|
41
|
+
# @raise [NameError] when the name is not defined
|
|
42
|
+
# @raise [TypeError] when the name does not resolve to an Exception class
|
|
93
43
|
def match?
|
|
94
44
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
95
45
|
|
|
46
|
+
# Resolved before the block, so that a NameError from the name is not
|
|
47
|
+
# mistaken for an exception raised by the block.
|
|
96
48
|
klass = expected_class
|
|
97
|
-
raise ::ArgumentError, "expected exception class must inherit from Exception" unless klass <= ::Exception
|
|
98
49
|
|
|
50
|
+
# The expected class first, whatever it is; then what a block raises
|
|
51
|
+
# on its own, the exceptions Defi keeps too.
|
|
99
52
|
begin
|
|
100
53
|
yield
|
|
101
54
|
false
|
|
102
|
-
rescue
|
|
103
|
-
e
|
|
55
|
+
rescue klass => e
|
|
56
|
+
message?(e)
|
|
57
|
+
rescue ::StandardError, ::ScriptError, ::SystemStackError, ::SystemExit
|
|
58
|
+
false
|
|
104
59
|
end
|
|
105
60
|
end
|
|
106
61
|
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
# @api public
|
|
110
|
-
#
|
|
111
|
-
# @return [String] A string describing what this matcher verifies
|
|
112
|
-
#
|
|
113
|
-
# @example
|
|
114
|
-
# RaiseException.new(ArgumentError).to_s # => "raise exception ArgumentError"
|
|
62
|
+
# @return [String]
|
|
115
63
|
def to_s
|
|
116
|
-
"raise exception #{@expected}"
|
|
64
|
+
return "raise exception #{Constant.label(@expected)}" if @message.nil?
|
|
65
|
+
|
|
66
|
+
"raise exception #{Constant.label(@expected)} with message #{@message.inspect}"
|
|
117
67
|
end
|
|
118
68
|
|
|
119
69
|
private
|
|
120
70
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
71
|
+
def validate_class!
|
|
72
|
+
return unless ::Class === @expected
|
|
73
|
+
return if @expected <= ::Exception
|
|
74
|
+
|
|
75
|
+
raise ::ArgumentError, "expected must inherit from Exception (got: #{@expected.inspect})"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def validate_message!
|
|
79
|
+
return if @message.nil? || ::String === @message || ::Regexp === @message
|
|
80
|
+
|
|
81
|
+
raise ::ArgumentError, "message must be a String or a Regexp (got: #{@message.inspect})"
|
|
82
|
+
end
|
|
83
|
+
|
|
129
84
|
def expected_class
|
|
130
|
-
::
|
|
85
|
+
return @expected if ::Class === @expected
|
|
86
|
+
|
|
87
|
+
klass = Constant.resolve(@expected, ::Class)
|
|
88
|
+
raise ::TypeError, "#{@expected} is not an Exception class" unless klass <= ::Exception
|
|
89
|
+
|
|
90
|
+
klass
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The message is whatever the exception answers to `message`, which is
|
|
94
|
+
# its own data, as with any exception; it must be a String, copied so
|
|
95
|
+
# that a subclass has no say, and valid. The expected String or Regexp
|
|
96
|
+
# receives the comparison.
|
|
97
|
+
def message?(exception)
|
|
98
|
+
return true if @message.nil?
|
|
99
|
+
|
|
100
|
+
message = exception.message
|
|
101
|
+
return false unless ::String === message
|
|
102
|
+
|
|
103
|
+
message = ::String.new(message)
|
|
104
|
+
return false unless message.valid_encoding?
|
|
105
|
+
|
|
106
|
+
::String === @message ? @message.eql?(message) : @message.match?(message)
|
|
107
|
+
rescue ::Encoding::CompatibilityError
|
|
108
|
+
false
|
|
131
109
|
end
|
|
132
110
|
end
|
|
133
111
|
end
|
data/lib/matchi/satisfy.rb
CHANGED
|
@@ -1,114 +1,39 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "constant"
|
|
4
|
+
|
|
3
5
|
module Matchi
|
|
4
|
-
# Custom
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# defined in a block. Unlike specific matchers that test for predetermined
|
|
8
|
-
# conditions, Satisfy allows you to define any custom validation logic at runtime.
|
|
9
|
-
# This makes it particularly useful for complex or composite conditions that
|
|
10
|
-
# aren't covered by other matchers.
|
|
11
|
-
#
|
|
12
|
-
# @example Basic numeric validation
|
|
13
|
-
# matcher = Matchi::Satisfy.new { |n| n.positive? && n.even? }
|
|
14
|
-
# matcher.match? { 2 } # => true
|
|
15
|
-
# matcher.match? { -2 } # => false
|
|
16
|
-
# matcher.match? { 3 } # => false
|
|
17
|
-
#
|
|
18
|
-
# @example String pattern validation
|
|
19
|
-
# matcher = Matchi::Satisfy.new { |s| s.start_with?("test") && s.length < 10 }
|
|
20
|
-
# matcher.match? { "test_123" } # => true
|
|
21
|
-
# matcher.match? { "test_12345" } # => false
|
|
22
|
-
# matcher.match? { "other" } # => false
|
|
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.
|
|
23
9
|
#
|
|
24
|
-
# @example
|
|
25
|
-
#
|
|
26
|
-
# attr_reader :age, :name
|
|
27
|
-
# def initialize(name, age)
|
|
28
|
-
# @name, @age = name, age
|
|
29
|
-
# end
|
|
30
|
-
# end
|
|
31
|
-
#
|
|
32
|
-
# matcher = Matchi::Satisfy.new { |user|
|
|
33
|
-
# user.age >= 18 && user.name.length >= 2
|
|
34
|
-
# }
|
|
35
|
-
# matcher.match? { User.new("Alice", 25) } # => true
|
|
36
|
-
# matcher.match? { User.new("B", 20) } # => false
|
|
37
|
-
#
|
|
38
|
-
# @example Using with collections
|
|
39
|
-
# matcher = Matchi::Satisfy.new { |arr|
|
|
40
|
-
# arr.all? { |x| x.is_a?(Integer) } && arr.sum.even?
|
|
41
|
-
# }
|
|
42
|
-
# matcher.match? { [2, 4, 6] } # => true
|
|
43
|
-
# matcher.match? { [1, 3, 5] } # => false
|
|
44
|
-
# matcher.match? { [1, "2", 3] } # => false
|
|
45
|
-
#
|
|
46
|
-
# @see https://ruby-doc.org/core/Proc.html
|
|
47
|
-
# @see Matchi::Predicate For testing existing predicate methods
|
|
10
|
+
# @example
|
|
11
|
+
# Matchi::Satisfy.new { |value| value > 41 }.match? { 42 } # => true
|
|
48
12
|
class Satisfy
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
# @
|
|
52
|
-
#
|
|
53
|
-
# @yield [Object] Block that defines the validation condition
|
|
54
|
-
# @yieldparam value The value to validate
|
|
55
|
-
# @yieldreturn [Boolean] true if the value meets the condition
|
|
56
|
-
#
|
|
57
|
-
# @raise [ArgumentError] if no block is provided
|
|
58
|
-
#
|
|
59
|
-
# @return [Satisfy] a new instance of the matcher
|
|
60
|
-
#
|
|
61
|
-
# @example Simple numeric validation
|
|
62
|
-
# Satisfy.new { |n| n > 0 }
|
|
63
|
-
#
|
|
64
|
-
# @example Complex condition
|
|
65
|
-
# Satisfy.new { |obj|
|
|
66
|
-
# obj.respond_to?(:length) && obj.length.between?(2, 10)
|
|
67
|
-
# }
|
|
13
|
+
# @yieldparam actual [Object] the actual value
|
|
14
|
+
# @yieldreturn [Boolean]
|
|
15
|
+
# @raise [ArgumentError] when no block is given
|
|
68
16
|
def initialize(&block)
|
|
69
17
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
70
18
|
|
|
71
|
-
@
|
|
19
|
+
@predicate = block
|
|
72
20
|
end
|
|
73
21
|
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
# the validation block returns a truthy value.
|
|
79
|
-
#
|
|
80
|
-
# @api public
|
|
81
|
-
#
|
|
82
|
-
# @yield [] Block that returns the value to validate
|
|
83
|
-
# @yieldreturn [Object] The value to check against the condition
|
|
84
|
-
#
|
|
85
|
-
# @return [Boolean] true if the value satisfies the condition
|
|
86
|
-
#
|
|
87
|
-
# @raise [ArgumentError] if no block is provided
|
|
88
|
-
#
|
|
89
|
-
# @example Using with direct values
|
|
90
|
-
# matcher = Satisfy.new { |n| n.positive? }
|
|
91
|
-
# matcher.match? { 42 } # => true
|
|
92
|
-
# matcher.match? { -1 } # => false
|
|
93
|
-
#
|
|
94
|
-
# @example Using with computed values
|
|
95
|
-
# matcher = Satisfy.new { |s| s.length.even? }
|
|
96
|
-
# matcher.match? { "test".upcase } # => true
|
|
97
|
-
# matcher.match? { "a" * 3 } # => false
|
|
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
|
|
98
26
|
def match?
|
|
99
27
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
100
28
|
|
|
101
|
-
@
|
|
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)})"
|
|
102
34
|
end
|
|
103
35
|
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
# @api public
|
|
107
|
-
#
|
|
108
|
-
# @return [String] A string describing what this matcher verifies
|
|
109
|
-
#
|
|
110
|
-
# @example
|
|
111
|
-
# Satisfy.new { |n| n > 0 }.to_s # => "satisfy &block"
|
|
36
|
+
# @return [String]
|
|
112
37
|
def to_s
|
|
113
38
|
"satisfy &block"
|
|
114
39
|
end
|
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,17 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: matchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 6.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cyril Kato
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
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.
|
|
12
|
+
description: 'Matchi provides small, explicit expectation matchers for any Ruby testing
|
|
13
|
+
framework. The expected value always controls the comparison, so the object under
|
|
14
|
+
test cannot forge a match.
|
|
20
15
|
|
|
21
16
|
'
|
|
22
17
|
email: contact@cyril.email
|
|
@@ -31,24 +26,28 @@ files:
|
|
|
31
26
|
- lib/matchi/be_a_kind_of.rb
|
|
32
27
|
- lib/matchi/be_an_instance_of.rb
|
|
33
28
|
- lib/matchi/be_within.rb
|
|
29
|
+
- lib/matchi/be_within/of.rb
|
|
34
30
|
- lib/matchi/change.rb
|
|
35
|
-
- lib/matchi/change/
|
|
36
|
-
- lib/matchi/change/by_at_least.rb
|
|
37
|
-
- lib/matchi/change/by_at_most.rb
|
|
31
|
+
- lib/matchi/change/delta.rb
|
|
38
32
|
- lib/matchi/change/from.rb
|
|
39
|
-
- lib/matchi/change/
|
|
33
|
+
- lib/matchi/change/from_to.rb
|
|
40
34
|
- lib/matchi/change/to.rb
|
|
35
|
+
- lib/matchi/constant.rb
|
|
41
36
|
- lib/matchi/eq.rb
|
|
42
37
|
- lib/matchi/match.rb
|
|
38
|
+
- lib/matchi/number.rb
|
|
43
39
|
- lib/matchi/predicate.rb
|
|
44
40
|
- lib/matchi/raise_exception.rb
|
|
45
41
|
- lib/matchi/satisfy.rb
|
|
46
|
-
homepage: https://
|
|
42
|
+
homepage: https://fixrb.dev/gems/matchi/
|
|
47
43
|
licenses:
|
|
48
44
|
- MIT
|
|
49
45
|
metadata:
|
|
46
|
+
bug_tracker_uri: https://github.com/fixrb/matchi/issues
|
|
47
|
+
changelog_uri: https://github.com/fixrb/matchi#changes-in-60
|
|
48
|
+
documentation_uri: https://rubydoc.info/gems/matchi
|
|
50
49
|
rubygems_mfa_required: 'true'
|
|
51
|
-
|
|
50
|
+
source_code_uri: https://github.com/fixrb/matchi
|
|
52
51
|
rdoc_options: []
|
|
53
52
|
require_paths:
|
|
54
53
|
- lib
|
|
@@ -56,15 +55,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
56
55
|
requirements:
|
|
57
56
|
- - ">="
|
|
58
57
|
- !ruby/object:Gem::Version
|
|
59
|
-
version: 3.
|
|
58
|
+
version: 3.3.0
|
|
60
59
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
60
|
requirements:
|
|
62
61
|
- - ">="
|
|
63
62
|
- !ruby/object:Gem::Version
|
|
64
63
|
version: '0'
|
|
65
64
|
requirements: []
|
|
66
|
-
rubygems_version:
|
|
67
|
-
signing_key:
|
|
65
|
+
rubygems_version: 4.0.23
|
|
68
66
|
specification_version: 4
|
|
69
|
-
summary:
|
|
67
|
+
summary: Framework-agnostic matchers for secure Ruby testing
|
|
70
68
|
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
|