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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eed23fa6963893d67a2cc6042f1f977f4fb0012013d53775e142bf92e1333a6d
4
- data.tar.gz: 97c4f82fa65c4d46af8babc966005d8a2db81b85b26987cf5fe3444f14ef6a25
3
+ metadata.gz: 5f6e92545e779fe4ef4a899d77213b30b26480bb4ee4daef969b1e1ea60ee7e5
4
+ data.tar.gz: a407ea85374dd3242d28da5609fe37320200879edb46c877a13dba1f3e377d66
5
5
  SHA512:
6
- metadata.gz: 271a0262764b04a77af4d149485395a1cf4d617c62e65dc6fab2388ad5873f3092ce08a9c273e836780623122599603a2a365c199f51ba20a2cda0f6a94f4867
7
- data.tar.gz: baa9876cb229024e218d1ee07d4913805f9b64eaa49fab23e76b01d2fdab1f05ba4b952920cb127a3e3fe8c71990a7653d7513828e68faa056632ce0619854de
6
+ metadata.gz: b48397862d2c8d6c8582e23445cf8c73f03f714db6bd7a73cd950f6ccc8149f8488440a827c7e31c220927e2dd7c294cc8726cd2afa1c5fb99dad2f2d8bec930
7
+ data.tar.gz: 5160fa06299005d522a20e8f805bd16ae5bddec4341728b92f15bbfe64c8582d756328326c29c6868636c99a9f87af74d4ee81f93e9b61f2d20a6b692f0db0cb
data/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2015-2025 Cyril Kato
3
+ Copyright (c) 2015-2026 Cyril Kato
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -4,288 +4,157 @@
4
4
  [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/fixrb/matchi/main)
5
5
  [![License](https://img.shields.io/github/license/fixrb/matchi?label=License&logo=github)](https://github.com/fixrb/matchi/raw/main/LICENSE.md)
6
6
 
7
- Matchi is a lightweight, framework-agnostic Ruby library that provides a comprehensive set of expectation matchers for elegant and secure testing. Its design focuses on simplicity, security, and extensibility.
7
+ Framework-agnostic matchers for Ruby. Small, explicit, and written so that the object under test cannot forge a match.
8
8
 
9
- ![A Rubyist juggling between Matchi letters](https://github.com/fixrb/matchi/raw/main/img/matchi.png)
10
-
11
- ## Key Features
12
-
13
- - **Framework Agnostic**: Easily integrate with any Ruby testing framework
14
- - **Security-Focused Design**: Built with robust type checking for most matchers
15
- - **Simple Integration**: Minimal setup required to get started
16
- - **Extensible**: Create custom matchers with just a few lines of code
17
- - **Comprehensive**: Rich set of built-in matchers for common testing scenarios
18
- - **Well Documented**: Extensive documentation with clear examples and implementation details
19
- - **Thread Safe**: Immutable matchers design ensures thread safety in concurrent environments
20
-
21
- ### Security Considerations for Predicate Matchers
22
-
23
- While most Matchi matchers are designed to resist type spoofing, predicate matchers (`Matchi::Predicate`) rely on Ruby's dynamic method dispatch system and can be vulnerable to method overriding:
24
-
25
- ```ruby
26
- # Example of predicate matcher vulnerability:
27
- matcher = Matchi::Predicate.new(:be_empty)
28
- array = []
29
-
30
- # Method overriding can defeat the matcher
31
- def array.empty?
32
- false
33
- end
34
-
35
- matcher.match? { array } # => false (Even though array is empty!)
36
- ```
37
-
38
- This limitation is inherent to Ruby's dynamic nature when working with predicate methods. If your tests require strict security guarantees, consider using direct state verification matchers instead of predicate matchers.
39
-
40
- ## What is a Matchi Matcher?
41
-
42
- A Matchi matcher is a simple Ruby object that follows a specific contract:
43
-
44
- 1. **Core Interface**: Every matcher must implement a `match?` method that:
45
- - Accepts a block as its only parameter
46
- - Executes that block to get the actual value
47
- - Returns a boolean indicating if the actual value matches the expected criteria
48
-
49
- 2. **Optional Description**: Matchers can implement a `to_s` method that returns a human-readable description of the match criteria
50
-
51
- Here's the simplest possible matcher:
52
-
53
- ```ruby
54
- module Matchi
55
- class SimpleEqual
56
- def initialize(expected)
57
- @expected = expected
58
- end
59
-
60
- def match?
61
- raise ArgumentError, "a block must be provided" unless block_given?
62
-
63
- @expected == yield
64
- end
65
-
66
- def to_s
67
- "equal #{@expected.inspect}"
68
- end
69
- end
70
- end
71
-
72
- # Usage:
73
- matcher = Matchi::SimpleEqual.new(42)
74
- matcher.match? { 42 } # => true
75
- matcher.match? { "42" } # => false
76
- matcher.to_s # => "equal 42"
77
- ```
78
-
79
- This design provides several benefits:
80
- - **Lazy Evaluation**: The actual value is only computed when needed via the block
81
- - **Encapsulation**: Each matcher is a self-contained object with clear responsibilities
82
- - **Composability**: Matchers can be easily combined and reused
83
- - **Testability**: The contract is simple and easy to verify
9
+ ![A pixel-art jester juggling the letters of Matchi](https://github.com/fixrb/matchi/raw/main/img/matchi.png)
84
10
 
85
11
  ## Installation
86
12
 
87
- Add to your Gemfile:
88
-
89
13
  ```ruby
14
+ # Gemfile
90
15
  gem "matchi"
91
16
  ```
92
17
 
93
- Or install directly:
18
+ Matchi requires Ruby 3.3 or later and has no runtime dependency.
94
19
 
95
- ```ruby
96
- gem install matchi
97
- ```
20
+ ## Usage
98
21
 
99
- ## Quick Start
22
+ A matcher is built with its expected value, then asked whether an actual value matches. The block yields the actual value once; `match?` answers `true` or `false`.
100
23
 
101
24
  ```ruby
102
25
  require "matchi"
103
26
 
104
- # Basic equality matching
105
- Matchi::Eq.new("hello").match? { "hello" } # => true
106
-
107
- # Type checking
108
- Matchi::BeAKindOf.new(Numeric).match? { 42 } # => true
109
- Matchi::BeAKindOf.new(String).match? { 42 } # => false
110
-
111
- # State change verification
112
- array = []
113
- Matchi::Change.new(array, :length).by(2).match? { array.push(1, 2) } # => true
27
+ matcher = Matchi::Eq.new(42)
28
+ matcher.match? { 42 } # => true
29
+ matcher.match? { 41 } # => false
30
+ matcher.to_s # => "eq 42"
114
31
  ```
115
32
 
116
- ## Core Matchers
117
-
118
- ### Value Comparison
33
+ Every `to_s` completes the sentence "expected ACTUAL to …", which is all a test framework needs:
119
34
 
120
35
  ```ruby
121
- # Exact equality (eql?)
122
- Matchi::Eq.new("test").match? { "test" } # => true
123
- Matchi::Eq.new([1, 2, 3]).match? { [1, 2, 3] } # => true
124
-
125
- # Object identity (equal?)
126
- symbol = :test
127
- Matchi::Be.new(symbol).match? { symbol } # => true
128
- string = "test"
129
- Matchi::Be.new(string).match? { string.dup } # => false
130
- ```
131
-
132
- ### Type Checking
133
-
134
- ```ruby
135
- # Inheritance-aware type checking
136
- Matchi::BeAKindOf.new(Numeric).match? { 42.0 } # => true
137
- Matchi::BeAKindOf.new(Integer).match? { 42.0 } # => false
36
+ # Minitest
37
+ def assert_matches(matcher, actual)
38
+ assert matcher.match? { actual }, "expected #{actual.inspect} to #{matcher}"
39
+ end
138
40
 
139
- # Exact type matching
140
- Matchi::BeAnInstanceOf.new(Float).match? { 42.0 } # => true
141
- Matchi::BeAnInstanceOf.new(Numeric).match? { 42.0 } # => false
41
+ def refute_matches(matcher, actual)
42
+ refute matcher.match? { actual }, "expected #{actual.inspect} not to #{matcher}"
43
+ end
142
44
 
143
- # Using class names as strings
144
- Matchi::BeAKindOf.new("Numeric").match? { 42.0 } # => true
145
- Matchi::BeAnInstanceOf.new("Float").match? { 42.0 } # => true
45
+ assert_matches Matchi::BeAKindOf.new(Numeric), 42
46
+ refute_matches Matchi::Match.new(/^foo/), "bar"
146
47
  ```
147
48
 
148
- ### State Changes
149
-
150
- ```ruby
151
- # Verify exact changes
152
- counter = 0
153
- Matchi::Change.new(counter, :to_i).by(5).match? { counter += 5 } # => true
49
+ Negation is the framework's job: Matchi has no `not_to`, it answers a boolean.
154
50
 
155
- # Verify minimum changes
156
- Matchi::Change.new(counter, :to_i).by_at_least(2).match? { counter += 3 } # => true
51
+ ## Matchers
157
52
 
158
- # Verify maximum changes
159
- Matchi::Change.new(counter, :to_i).by_at_most(5).match? { counter += 3 } # => true
53
+ ```ruby
54
+ require "matchi"
160
55
 
161
- # Track value transitions
162
- string = "hello"
163
- Matchi::Change.new(string, :to_s).from("hello").to("HELLO").match? { string.upcase! } # => true
56
+ # Identity (equal?) and equivalence (eql?)
57
+ Matchi::Be.new(:foo).match? { :foo } # => true
58
+ Matchi::Eq.new("foo").match? { "foo" } # => true
59
+ Matchi::Eq.new(1).match? { 1.0 } # => false
164
60
 
165
- # Simple change detection
166
- array = []
167
- Matchi::Change.new(array, :length).match? { array << 1 } # => true
61
+ # Types: a module, or its name resolved when the matcher runs
62
+ Matchi::BeAKindOf.new(Numeric).match? { 42 } # => true
63
+ Matchi::BeAnInstanceOf.new(:Integer).match? { 42 } # => true
64
+ Matchi::BeAnInstanceOf.new(Numeric).match? { 42 } # => false
168
65
 
169
- # Check final state only
170
- counter = 0
171
- Matchi::Change.new(counter, :to_i).to(5).match? { counter = 5 } # => true
172
- ```
66
+ # Numeric proximity; a non-Numeric actual value fails
67
+ Matchi::BeWithin.new(0.01).of(0.3).match? { 0.1 + 0.2 } # => true
173
68
 
174
- ### Pattern Matching
69
+ # Patterns and predicates
70
+ Matchi::Match.new(/^foo/).match? { "foobar" } # => true
71
+ Matchi::Satisfy.new { |v| v > 41 }.match? { 42 } # => true
72
+ Matchi::Predicate.new(:be_empty).match? { [] } # => true (calls empty?)
73
+ Matchi::Predicate.new(:have_key, :a).match? { { a: 1 } } # => true (calls has_key?(:a))
175
74
 
176
- ```ruby
177
- # Regular expressions
178
- Matchi::Match.new(/^test/).match? { "test_string" } # => true
179
- Matchi::Match.new(/^\d{3}-\d{2}$/).match? { "123-45" } # => true
75
+ # Exceptions, by class or by name, optionally with a message (String or Regexp)
76
+ Matchi::RaiseException.new(ZeroDivisionError).match? { 1 / 0 } # => true
77
+ Matchi::RaiseException.new("Errno::ENOENT").match? { File.read("/missing") } # => true
78
+ Matchi::RaiseException.new(ZeroDivisionError, /by 0/).match? { 1 / 0 } # => true
180
79
 
181
- # Custom predicates with Satisfy
182
- Matchi::Satisfy.new { |x| x.positive? && x < 10 }.match? { 5 } # => true
183
- Matchi::Satisfy.new { |arr| arr.all?(&:even?) }.match? { [2, 4, 6] } # => true
80
+ # State changes, observed through a public method
81
+ list = []
82
+ Matchi::Change.new(list, :length).match? { list << 1 } # => true
83
+ Matchi::Change.new(list, :length).by(2).match? { list.push(1, 2) } # => true
84
+ Matchi::Change.new(list, :length).by_at_least(1).match? { list << 1 } # => true
85
+ Matchi::Change.new(list, :length).by_at_most(1).match? { list << 1 } # => true
184
86
 
185
- # Built-in predicates
186
- Matchi::Predicate.new(:be_empty).match? { [] } # => true
187
- Matchi::Predicate.new(:have_key, :name).match? { { name: "Alice" } } # => true
87
+ list = []
88
+ Matchi::Change.new(list, :length).to(1).match? { list << 1 } # => true
89
+ Matchi::Change.new(list, :length).from(1).to(2).match? { list << 1 } # => true
188
90
  ```
189
91
 
190
- ### Exception Handling
92
+ `BeWithin.new(delta)` and `Change#from(value)` are builders, not matchers: they do not respond to `match?` until `of` or `to` completes them.
191
93
 
192
- ```ruby
193
- # Verify raised exceptions
194
- Matchi::RaiseException.new(ArgumentError).match? { raise ArgumentError } # => true
195
-
196
- # Works with inheritance
197
- Matchi::RaiseException.new(StandardError).match? { raise ArgumentError } # => true
198
-
199
- # Using exception class names
200
- Matchi::RaiseException.new("ArgumentError").match? { raise ArgumentError } # => true
201
- ```
94
+ Arguments are validated when the matcher is built (`ArgumentError`); a name is resolved when the matcher runs (`NameError`, or `TypeError` when the constant is not a module, or not an exception class for `RaiseException`). A block is always required by `match?`. Whatever the actual value or an observed method raises propagates; `RaiseException` rescues what its block raises, within the limits below, not what its message filter or the resolution of its name raises.
202
95
 
203
- ### Numeric Comparisons
96
+ `RaiseException` rescues an exception of the expected class, and any `StandardError`, `ScriptError`, `SystemStackError` or `SystemExit` (the exceptions [Defi](https://fixrb.dev/gems/defi/) keeps too), which it then compares with the expected class. Anything else, an `Interrupt`, a signal, a `NoMemoryError`, the exception with which an enclosing `Timeout` ends its block, goes through as it would with any other matcher, unless that is the class expected: `RaiseException.new(Interrupt)` rescues an `Interrupt`. An unexpected `exit` inside the block is the wrong exception, so `false`, not the end of the process.
204
97
 
205
- ```ruby
206
- # Delta comparisons
207
- Matchi::BeWithin.new(0.5).of(3.0).match? { 3.2 } # => true
208
- Matchi::BeWithin.new(2).of(10).match? { 9 } # => true
209
- ```
98
+ `Change` observes a value: when the observed method returns the same unfrozen object twice (an `attr_reader` on a mutated array, say), `match?` raises instead of reporting no change. Observe `items.length`, or use `to`.
210
99
 
211
- ## Creating Custom Matchers
100
+ ## Writing a matcher
212
101
 
213
- Creating custom matchers is straightforward:
102
+ Any object that responds to `match?` with a block and to `to_s` is a matcher:
214
103
 
215
104
  ```ruby
216
- module Matchi
217
- class BePositive
218
- def match?
219
- yield.positive?
220
- end
221
-
222
- def to_s
223
- "be positive"
224
- end
105
+ class BePositive
106
+ def match?
107
+ yield.positive?
225
108
  end
226
- end
227
-
228
- matcher = Matchi::BePositive.new
229
- matcher.match? { 42 } # => true
230
- matcher.match? { -1 } # => false
231
- ```
232
-
233
- ## Security Best Practices
234
-
235
- ### Proper Value Comparison Order
236
109
 
237
- One of the most critical aspects when implementing matchers is the order of comparison between expected and actual values. Always compare values in this order:
110
+ def to_s
111
+ "be positive"
112
+ end
113
+ end
238
114
 
239
- ```ruby
240
- # GOOD: Expected value controls the comparison
241
- expected_value.eql?(actual_value)
242
- # BAD: Actual value controls the comparison
243
- actual_value.eql?(expected_value)
115
+ BePositive.new.match? { 42 } # => true
244
116
  ```
245
117
 
246
- #### Why This Matters
118
+ Every matcher in Matchi follows two rules, which are worth keeping in your own:
247
119
 
248
- The order is crucial because the object receiving the comparison method controls how the comparison is performed. When testing, the actual value might come from untrusted or malicious code that could override comparison methods:
120
+ - **The expected value receives the comparison.** `Matchi::Eq.new(a).match? { b }` evaluates `a.eql?(b)`, never `b.eql?(a)`, so an actual value that redefines `eql?` cannot pass; since `Hash#eql?` hands the comparison back to an operand of another class, `Eq` also requires the two values to be of the same class. Classes are compared with `Module#===`, the class of the actual value is read with `Kernel#class` bound to it, and `BeWithin` only subtracts core numbers, for the same reason.
121
+ - **A predicate answers `true` or `false`.** `Satisfy` and `Predicate` raise a `TypeError` on any other value instead of treating it as truthy.
249
122
 
250
- ```ruby
251
- # Example of how comparison can be compromised
252
- class MaliciousString
253
- def eql?(other)
254
- true # Always returns true regardless of actual equality
255
- end
123
+ Two matchers depend on the honesty of the object under test, because they use values it produces: `Predicate` calls one of its methods, and `Change` (with `by`, `by_at_least`, `by_at_most`) compares two values returned by the observed method, including the subtraction between them. So does the message filter of `RaiseException`, which reads the exception's own message. `Change#to` and `from … to` compare with the expected value and do not.
256
124
 
257
- def ==(other)
258
- true # Always returns true regardless of actual equality
259
- end
260
- end
261
-
262
- actual = MaliciousString.new
263
- expected = "expected string"
264
- actual.eql?(expected) # => true (incorrect result!)
265
- expected.eql?(actual) # => false (correct result)
266
- ```
125
+ A class or an exception given by name (`:Duck`, `"Errno::ENOENT"`) is resolved from `Object` when the matcher runs, so a specification can be written before the class it describes is loaded. An `autoload` or an `Object.const_missing` defined by the code under test takes part in that resolution, so a name does not carry the guarantee a module does. Names are ASCII constant paths; `"::Integer"` is not accepted.
267
126
 
268
- This is why Matchi's built-in matchers are implemented with this security consideration in mind. For example, the `Eq` matcher:
127
+ ## Differences from RSpec
269
128
 
270
- ```ruby
271
- # Implementation in Matchi::Eq
272
- def match?
273
- @expected.eql?(yield) # Expected value controls the comparison
274
- end
275
- ```
129
+ If you come from RSpec or Minitest, these will surprise you:
276
130
 
277
- ## Extensions
131
+ - `Eq` is RSpec's `eql`, not its `eq`: it compares with `eql?`, so `1` is not `1.0`, and a value object that only defines `==` never matches. Define `eql?` and `hash` on it, or use `Satisfy`. `Integer#==` hands the comparison to the other operand, which is why Matchi does not use it.
132
+ - `Match` takes a `Regexp` only. For a literal substring, `Matchi::Match.new(Regexp.new(Regexp.escape("a.b")))`.
133
+ - `RaiseException` is `raise_error`; it needs an exception class, or its name, and takes an optional message. It does not rescue an `Interrupt`, a signal, a `NoMemoryError` or the exception with which an enclosing `Timeout` ends its block, unless that is the class expected, where `raise_error` rescues every `Exception`.
134
+ - `Change#to` demands a change: a value already equal to the target does not pass. `Change` without a refinement needs the observed method to return a value, not the mutated object.
135
+ - `Predicate` only calls a method of the actual value: `be_nil` calls `nil?`, but `be_truthy`, `be_falsey` and `be_a` are not predicates and do not exist. Use `Be.new(nil)`, `Satisfy`, `BeAKindOf`.
136
+ - There is no `and`, `or`, `not_to` or `all`: compose in your framework, with plain booleans.
278
137
 
279
- ### matchi-fix
138
+ ## Changes in 6.0
280
139
 
281
- The [matchi-fix gem](https://rubygems.org/gems/matchi-fix) extends Matchi with support for testing against [Fix](https://github.com/fixrb/fix) specifications. It provides a seamless integration between Matchi's matcher interface and Fix's powerful specification system.
140
+ - `RaiseException` no longer rescues every `Exception`: it rescues the expected class, and any `StandardError`, `ScriptError`, `SystemStackError` or `SystemExit`, which it compares with the expected class. An `Interrupt`, a signal, a `NoMemoryError` or the exception with which an enclosing `Timeout` ends its block goes through unless that is the class expected, where 5.0 reported it as a failed match. `RaiseException.new(Exception)` still rescues everything, as asked.
141
+ - The message filter of `RaiseException` and `Match` compare a plain copy of the actual `String`: a subclass could answer `valid_encoding?` for it and turn the comparison into an error.
282
142
 
283
- ```ruby
284
- # Add to your Gemfile
285
- gem "matchi-fix"
286
- ```
143
+ ## Changes in 5.0
287
144
 
288
- This extension adds a `Fix` matcher that allows you to verify implementation conformance to Fix test specifications across different testing frameworks like Minitest and RSpec.
145
+ - `BeAKindOf`, `BeAnInstanceOf` and `RaiseException` accept a module directly, so anonymous classes work. A name is still accepted; it must be a constant path (`"Foo::bar"` is rejected at construction) and resolve to a module, or a class for `BeAnInstanceOf` and an exception class for `RaiseException`. `BeAnInstanceOf` rejects a module that is not a class.
146
+ - `RaiseException` compares with `Module#===` and always answers `true` or `false`; `Module#<=` could answer `nil` and let the exception control the comparison. A non-exception class is rejected at construction. An expected message, `String` or `Regexp`, can be given.
147
+ - `Match` accepts a `Regexp` only, and fails on an actual value that is neither a `String` nor a `Symbol`, or whose encoding is invalid or incompatible. With a `String` pattern, the actual value would have become the regular expression.
148
+ - `Eq`, `Change#to` and `from … to` require the actual value to be of the class of the expected value, so that `Hash#eql?` cannot hand the comparison to the object under test.
149
+ - `Satisfy` requires a `true` or `false` answer (`TypeError`), as `Predicate` already did. The error names the class of the answer, never its `inspect`.
150
+ - `Predicate` and `Change` reach the object under test through `Kernel#public_send`: a private or protected predicate is now a `NoMethodError`. `Change` accepts a `String` method name and no longer checks `respond_to?` at construction; a missing method is a `NoMethodError` when the matcher runs.
151
+ - `BeWithin::Of` only subtracts an `Integer`, a `Float`, a `Rational` or a `BigDecimal`; any other actual value fails, including a user-defined `Numeric` that could control the subtraction through `coerce`. Deltas and expected values, there and in `by`, `by_at_least`, `by_at_most`, must be finite real numbers (`NaN`, infinities and `Complex` are rejected).
152
+ - `BeWithin` without `of` and `Change#from` without `to` no longer respond to `match?`.
153
+ - `from … to` compares with `eql?`, like `to` and `Eq` already did; `to` requires a change; both always evaluate the block. `Change` raises a `TypeError` when the observed method returns the same unfrozen object twice: in particular, asserting that an attribute holding an unfrozen object (a `Time`, an `Array`) did not change now raises instead of answering `false`. The three `by` refinements are one class, `Change::Delta`; `Change::From::To` is `Change::FromTo`.
154
+ - Descriptions are stable: `Change#to_s` names the observed class and method (`change Array#length`), an anonymous class reads `an anonymous class`, and no `to_s` calls `inspect` on an object under test.
155
+ - Error messages are rewritten and all read `X must be … (got: …)`; `Predicate::PREFIXES` is gone.
156
+ - The `Matchi` helper module removed in 4.2 is not back: `Matchi` is a namespace. Frameworks define their own helpers.
157
+ - Ruby 3.3 or later.
289
158
 
290
159
  ## Versioning
291
160
 
data/lib/matchi/be.rb CHANGED
@@ -1,83 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Matchi
4
- # Identity matcher that checks if two objects are the exact same instance.
4
+ # Identity matcher: passes when the actual value is the very same object as
5
+ # the expected one (`equal?`).
5
6
  #
6
- # This matcher verifies object identity using Ruby's Object#equal? method, which
7
- # compares object IDs to determine if two references point to the exact same object
8
- # in memory. This is different from equality comparison (==) which compares values.
9
- #
10
- # @example Basic usage with symbols
11
- # matcher = Matchi::Be.new(:foo)
12
- # matcher.match? { :foo } # => true
13
- # matcher.match? { :bar } # => false
14
- #
15
- # @example Identity comparison with strings
16
- # string = "test"
17
- # matcher = Matchi::Be.new(string)
18
- # matcher.match? { string } # => true
19
- # matcher.match? { string.dup } # => false
20
- # matcher.match? { "test" } # => false
21
- #
22
- # @example With mutable objects
23
- # array = [1, 2, 3]
24
- # matcher = Matchi::Be.new(array)
25
- # matcher.match? { array } # => true
26
- # matcher.match? { array.dup } # => false
27
- # matcher.match? { [1, 2, 3] } # => false
28
- #
29
- # @see https://ruby-doc.org/core/Object.html#method-i-equal-3F
30
- # @see Matchi::Eq
7
+ # @example
8
+ # Matchi::Be.new(:foo).match? { :foo } # => true
9
+ # Matchi::Be.new("a").match? { "a".dup } # => false (another object)
31
10
  class Be
32
- # Initialize the matcher with a reference object.
33
- #
34
- # @api public
35
- #
36
- # @param expected [#equal?] The expected identical object
37
- #
38
- # @return [Be] a new instance of the matcher
39
- #
40
- # @example
41
- # string = "test"
42
- # Be.new(string) # Match only the same string instance
11
+ # @param expected [Object] the expected object
43
12
  def initialize(expected)
44
13
  @expected = expected
45
14
  end
46
15
 
47
- # Checks if the yielded object is the same instance as the expected object.
48
- #
49
- # This method uses Ruby's Object#equal? method, which performs identity comparison
50
- # by comparing object IDs. Two objects are considered identical only if they are
51
- # the exact same instance in memory.
52
- #
53
- # @api public
54
- #
55
- # @yield [] Block that returns the object to check
56
- # @yieldreturn [Object] The object to verify identity with
57
- #
58
- # @return [Boolean] true if both objects are the same instance
59
- #
60
- # @raise [ArgumentError] if no block is provided
61
- #
62
- # @example
63
- # obj = "test"
64
- # matcher = Be.new(obj)
65
- # matcher.match? { obj } # => true
66
- # matcher.match? { obj.dup } # => false
16
+ # @yieldreturn [Object] the actual value
17
+ # @return [Boolean]
18
+ # @raise [ArgumentError] when no block is given
67
19
  def match?
68
20
  raise ::ArgumentError, "a block must be provided" unless block_given?
69
21
 
70
22
  @expected.equal?(yield)
71
23
  end
72
24
 
73
- # Returns a human-readable description of the matcher.
74
- #
75
- # @api public
76
- #
77
- # @return [String] A string describing what this matcher verifies
78
- #
79
- # @example
80
- # Be.new("test").to_s # => 'be "test"'
25
+ # @return [String]
81
26
  def to_s
82
27
  "be #{@expected.inspect}"
83
28
  end