spectus 5.0.1 → 5.0.2
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 +87 -70
- data/lib/spectus/requirement/base.rb +34 -35
- data/lib/spectus/requirement/optional.rb +23 -5
- data/lib/spectus/requirement/recommended.rb +23 -5
- data/lib/spectus/requirement/required.rb +22 -3
- data/lib/spectus/requirement.rb +8 -1
- data/lib/spectus.rb +159 -58
- metadata +11 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 27755e146e515eb8767ab7d71d9e5f712538376e89df1100f6629ba27f5635c2
|
4
|
+
data.tar.gz: 5e9c7e45e5f91f5a98aa62ea005732fec936c2dd456a8c52d9d50e55b81510ec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 885d638883c196a7511d2402c4113c44b4d1f9dcca8784dfdbd1bd7061c32da8fbfbf50c001d0e5c6c1320fef1dabdaa320643d760651067a1441cde0acfcd69
|
7
|
+
data.tar.gz: 202bef0668a0477517d5cfce46428b7c34f376dc5033018427533e8c3d6bdf1263ae816c96f50ef65ec6d093a3cc4654c0baa75345fcb7ccf5ca9ff6343ea33c
|
data/LICENSE.md
CHANGED
data/README.md
CHANGED
@@ -2,126 +2,143 @@
|
|
2
2
|
|
3
3
|
[](https://github.com/fixrb/spectus/tags)
|
4
4
|
[](https://rubydoc.info/github/fixrb/spectus/main)
|
5
|
-
[](https://github.com/fixrb/spectus/actions?query=workflow%3Aruby+branch%3Amain)
|
6
|
-
[](https://github.com/fixrb/spectus/actions?query=workflow%3Arubocop+branch%3Amain)
|
7
5
|
[](https://github.com/fixrb/spectus/raw/main/LICENSE.md)
|
8
6
|
|
9
|
-
> A Ruby library
|
7
|
+
> A Ruby testing library that brings precision to your expectations using RFC 2119 compliance levels. 🚥
|
8
|
+
|
9
|
+
## Quick Start
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
require "spectus"
|
13
|
+
require "matchi"
|
14
|
+
|
15
|
+
# Define a must-have requirement
|
16
|
+
test = Spectus.must Matchi::Eq.new(42)
|
17
|
+
test.call { 42 } # => Pass ✅
|
18
|
+
|
19
|
+
# Define an optional feature
|
20
|
+
test = Spectus.may Matchi::Be.new(:empty?)
|
21
|
+
test.call { [].empty? } # => Pass ✅
|
22
|
+
```
|
10
23
|
|
11
24
|
## Installation
|
12
25
|
|
13
|
-
Add
|
26
|
+
Add to your Gemfile:
|
14
27
|
|
15
28
|
```ruby
|
16
29
|
gem "spectus"
|
30
|
+
gem "matchi" # For matchers
|
17
31
|
```
|
18
32
|
|
19
|
-
|
33
|
+
Or install directly:
|
20
34
|
|
21
|
-
```
|
22
|
-
|
35
|
+
```bash
|
36
|
+
gem install spectus
|
37
|
+
gem install matchi
|
23
38
|
```
|
24
39
|
|
25
|
-
|
40
|
+
## Understanding RFC 2119
|
26
41
|
|
27
|
-
|
28
|
-
gem install spectus
|
29
|
-
```
|
42
|
+
Spectus implements [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) requirement levels to bring clarity and precision to test expectations:
|
30
43
|
|
31
|
-
|
44
|
+
- **MUST** (✅): Absolute requirement, no exceptions
|
45
|
+
- **SHOULD** (⚠️): Strong recommendation with valid exceptions
|
46
|
+
- **MAY** (💡): Optional feature
|
32
47
|
|
33
|
-
|
48
|
+
This approach helps you clearly communicate the importance of each test in your suite.
|
34
49
|
|
35
|
-
|
50
|
+
## Features
|
36
51
|
|
37
|
-
|
38
|
-
require "spectus"
|
39
|
-
```
|
52
|
+
### Requirement Levels
|
40
53
|
|
41
|
-
|
54
|
+
| Level | Description | Pass Conditions |
|
55
|
+
|-------|-------------|-----------------|
|
56
|
+
| MUST | Absolute requirement | Only when exact match |
|
57
|
+
| SHOULD | Recommended behavior | When matches or has valid reason not to |
|
58
|
+
| MAY | Optional feature | When matches or not implemented |
|
42
59
|
|
43
|
-
|
44
|
-
require "matchi"
|
45
|
-
```
|
60
|
+
### Results Classification
|
46
61
|
|
47
|
-
|
62
|
+
- **Pass Results:**
|
63
|
+
- ✅ Success (MUST level met)
|
64
|
+
- ⚠️ Warning (SHOULD level met)
|
65
|
+
- 💡 Info (MAY level met)
|
48
66
|
|
49
|
-
|
67
|
+
- **Fail Results:**
|
68
|
+
- ❌ Failure (requirement not met)
|
69
|
+
- 💥 Error (unexpected exception)
|
50
70
|
|
51
|
-
|
71
|
+
## Usage Examples
|
72
|
+
|
73
|
+
### Testing Required Behavior
|
52
74
|
|
53
75
|
```ruby
|
54
|
-
|
55
|
-
|
56
|
-
# => Expresenter::Pass(actual: 1, definition: "be 1", error: nil, expected: 1, got: true, negate: false, level: :MUST)
|
76
|
+
test = Spectus.must Matchi::Be.new(1)
|
77
|
+
test.call { "🦇".size } # Must be exactly 1
|
57
78
|
```
|
58
79
|
|
59
|
-
|
60
|
-
|
61
|
-
### Absolute Prohibition
|
62
|
-
|
63
|
-
Truth and lies:
|
80
|
+
### Testing Recommended Behavior
|
64
81
|
|
65
82
|
```ruby
|
66
|
-
|
67
|
-
|
68
|
-
# => Expresenter::Pass(actual: false, definition: "be true", error: nil, expected: true, got: true, negate: true, level: :MUST)
|
83
|
+
test = Spectus.should Matchi::Be.new(0.3)
|
84
|
+
test.call { 0.1 + 0.2 } # Should be close to 0.3
|
69
85
|
```
|
70
86
|
|
71
|
-
###
|
72
|
-
|
73
|
-
A well-known joke. The addition of `0.1` and `0.2` is deadly precise:
|
87
|
+
### Testing Optional Features
|
74
88
|
|
75
89
|
```ruby
|
76
|
-
|
77
|
-
|
78
|
-
# => Expresenter::Pass(actual: 0.30000000000000004, definition: "be 0.3", error: nil, expected: 0.3, got: false, negate: false, level: :SHOULD)
|
90
|
+
test = Spectus.may Matchi::Be.new(true)
|
91
|
+
test.call { [].blank? } # May implement blank? method
|
79
92
|
```
|
80
93
|
|
81
|
-
|
94
|
+
## Advanced Usage
|
82
95
|
|
83
|
-
|
96
|
+
<details>
|
97
|
+
<summary>Click to expand custom matcher example</summary>
|
84
98
|
|
85
99
|
```ruby
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
SecureRandom.hex(3)
|
100
|
+
class PositiveNumber
|
101
|
+
def match?
|
102
|
+
yield.positive?
|
103
|
+
end
|
92
104
|
end
|
93
|
-
# => Expresenter::Pass(actual: "bb5716", definition: "match \"123456\"", error: nil, expected: "123456", got: true, negate: true, level: :SHOULD)
|
94
|
-
```
|
95
105
|
|
96
|
-
|
97
|
-
|
98
|
-
|
106
|
+
test = Spectus.must PositiveNumber.new
|
107
|
+
test.call { 42 } # => Pass
|
108
|
+
```
|
109
|
+
</details>
|
99
110
|
|
100
|
-
|
111
|
+
<details>
|
112
|
+
<summary>Click to expand integration example</summary>
|
101
113
|
|
102
114
|
```ruby
|
103
|
-
|
104
|
-
|
105
|
-
# => Expresenter::Pass(actual: nil, definition: "be true", error: #<NoMethodError: undefined method `blank?' for []:Array>, expected: true, got: nil, negate: false, level: :MAY)
|
106
|
-
```
|
107
|
-
|
108
|
-
My bad! ActiveSupport was not imported. 🤦♂️
|
115
|
+
require "spectus"
|
116
|
+
require "matchi"
|
109
117
|
|
110
|
-
|
118
|
+
RSpec.describe Calculator do
|
119
|
+
it "must perform exact arithmetic" do
|
120
|
+
test = Spectus.must Matchi::Eq.new(4)
|
121
|
+
expect { test.call { 2 + 2 } }.not_to raise_error
|
122
|
+
end
|
123
|
+
end
|
124
|
+
```
|
125
|
+
</details>
|
111
126
|
|
112
|
-
##
|
127
|
+
## Related Projects
|
113
128
|
|
114
|
-
|
115
|
-
|
116
|
-
|
129
|
+
- [Matchi](https://github.com/fixrb/matchi) - Collection of compatible matchers
|
130
|
+
- [Test Tube](https://github.com/fixrb/test_tube) - Underlying test execution engine
|
131
|
+
- [Expresenter](https://github.com/fixrb/expresenter) - Test result presentation
|
117
132
|
|
118
|
-
##
|
133
|
+
## License
|
119
134
|
|
120
|
-
|
135
|
+
Released under the [MIT License](LICENSE.md).
|
121
136
|
|
122
|
-
##
|
137
|
+
## Support
|
123
138
|
|
124
|
-
|
139
|
+
- Issues: [GitHub Issues](https://github.com/fixrb/spectus/issues)
|
140
|
+
- Documentation: [RubyDoc](https://rubydoc.info/github/fixrb/spectus/main)
|
141
|
+
- Blog Post: [Medium Article](https://cyrilllllll.medium.com/a-spectus-tutorial-expectations-with-rfc-2119-compliance-1fc769861c1)
|
125
142
|
|
126
143
|
## Sponsors
|
127
144
|
|
@@ -6,23 +6,42 @@ require "test_tube"
|
|
6
6
|
module Spectus
|
7
7
|
# Namespace for the requirement levels.
|
8
8
|
module Requirement
|
9
|
-
#
|
9
|
+
# Base class for implementing RFC 2119 requirement levels.
|
10
|
+
#
|
11
|
+
# This class provides the core functionality for running tests against
|
12
|
+
# different requirement levels (MUST, SHOULD, MAY). It uses TestTube for
|
13
|
+
# test execution and Expresenter for result presentation.
|
14
|
+
#
|
15
|
+
# @see https://github.com/fixrb/test_tube Test execution
|
16
|
+
# @see https://github.com/fixrb/expresenter Result presentation
|
10
17
|
class Base
|
11
18
|
# Initialize the requirement level class.
|
12
19
|
#
|
13
|
-
# @param matcher
|
14
|
-
# @param negate
|
20
|
+
# @param matcher [#match?] The matcher used to evaluate the test
|
21
|
+
# @param negate [Boolean] When true, inverts the matcher's result
|
22
|
+
#
|
23
|
+
# @raise [ArgumentError] If matcher doesn't respond to match?
|
15
24
|
def initialize(matcher:, negate:)
|
16
|
-
|
17
|
-
|
25
|
+
raise ::ArgumentError, "matcher must respond to match?" unless matcher.respond_to?(:match?)
|
26
|
+
|
27
|
+
@matcher = matcher
|
28
|
+
@negate = negate
|
18
29
|
end
|
19
30
|
|
20
|
-
#
|
31
|
+
# Execute the test and return its result.
|
21
32
|
#
|
22
|
-
#
|
23
|
-
#
|
33
|
+
# Runs the provided block through the matcher and evaluates the result
|
34
|
+
# according to the requirement level's rules. The result is presented
|
35
|
+
# through an Expresenter instance containing all test details.
|
24
36
|
#
|
25
|
-
# @
|
37
|
+
# @example
|
38
|
+
# test = Base.new(matcher: SomeMatcher.new, negate: false)
|
39
|
+
# test.call { some_value }
|
40
|
+
# # => #<Expresenter::Pass actual: some_value, ...>
|
41
|
+
#
|
42
|
+
# @yield The block containing the code to test
|
43
|
+
# @raise [::Expresenter::Fail] When the test fails
|
44
|
+
# @return [::Expresenter::Pass] When the test passes
|
26
45
|
#
|
27
46
|
# @api public
|
28
47
|
def call(&)
|
@@ -38,36 +57,16 @@ module Spectus
|
|
38
57
|
)
|
39
58
|
end
|
40
59
|
|
41
|
-
# :nocov:
|
42
|
-
|
43
|
-
# A string containing a human-readable representation of the definition.
|
44
|
-
#
|
45
|
-
# @example The human-readable representation of an absolute requirement.
|
46
|
-
# require "spectus"
|
47
|
-
# require "matchi/be"
|
48
|
-
#
|
49
|
-
# definition = Spectus.must Matchi::Be.new(1)
|
50
|
-
# definition.inspect
|
51
|
-
# # => "#<MUST Matchi::Be(1) negate=false>"
|
52
|
-
#
|
53
|
-
# @return [String] The human-readable representation of the definition.
|
54
|
-
#
|
55
|
-
# @api public
|
56
|
-
def inspect
|
57
|
-
"#<#{self.class.level} #{@matcher.inspect} negate=#{@negate}>"
|
58
|
-
end
|
59
|
-
|
60
|
-
# :nocov:
|
61
|
-
|
62
60
|
private
|
63
61
|
|
64
|
-
#
|
65
|
-
#
|
66
|
-
# @param test [::TestTube::Base] The state of the experiment.
|
62
|
+
# Determine if the test passed according to the requirement level's rules.
|
67
63
|
#
|
68
|
-
#
|
64
|
+
# This base implementation considers the test passed if the matcher
|
65
|
+
# returned true. Subclasses may override this method to implement
|
66
|
+
# specific requirement level rules.
|
69
67
|
#
|
70
|
-
# @
|
68
|
+
# @param test [::TestTube::Base] The test execution state
|
69
|
+
# @return [Boolean] true if the test passed, false otherwise
|
71
70
|
def passed?(test)
|
72
71
|
test.got.equal?(true)
|
73
72
|
end
|
@@ -4,21 +4,39 @@ require_relative "base"
|
|
4
4
|
|
5
5
|
module Spectus
|
6
6
|
module Requirement
|
7
|
-
#
|
7
|
+
# Implementation of MAY requirements from RFC 2119.
|
8
|
+
#
|
9
|
+
# This level represents optional features. A test at this level
|
10
|
+
# passes in two cases:
|
11
|
+
# - When the feature is implemented and the matcher returns true
|
12
|
+
# - When NoMethodError is raised, indicating the feature is not implemented
|
13
|
+
#
|
14
|
+
# @example Testing a MAY requirement with implemented feature
|
15
|
+
# test = Optional.new(matcher: some_matcher, negate: false)
|
16
|
+
# test.call { implemented_feature } # Passes if matcher returns true
|
17
|
+
#
|
18
|
+
# @example Testing a MAY requirement with unimplemented feature
|
19
|
+
# test = Optional.new(matcher: some_matcher, negate: false)
|
20
|
+
# test.call { unimplemented_feature } # Passes if NoMethodError is raised
|
21
|
+
#
|
22
|
+
# @see https://www.ietf.org/rfc/rfc2119.txt RFC 2119 MAY keyword
|
8
23
|
class Optional < Base
|
9
|
-
#
|
24
|
+
# The RFC 2119 keyword for this requirement level.
|
10
25
|
#
|
11
|
-
# @return [Symbol]
|
26
|
+
# @return [Symbol] :MAY indicating an optional requirement
|
27
|
+
# @api public
|
12
28
|
def self.level
|
13
29
|
:MAY
|
14
30
|
end
|
15
31
|
|
16
32
|
private
|
17
33
|
|
18
|
-
#
|
34
|
+
# Determine if the test passed according to MAY requirement rules.
|
35
|
+
# A test passes if either:
|
36
|
+
# - The base matcher validation passes (super)
|
37
|
+
# - The feature is not implemented (NoMethodError)
|
19
38
|
#
|
20
39
|
# @param (see Base#passed?)
|
21
|
-
#
|
22
40
|
# @return (see Base#passed?)
|
23
41
|
def passed?(test)
|
24
42
|
super || test.error.is_a?(::NoMethodError)
|
@@ -4,21 +4,39 @@ require_relative "base"
|
|
4
4
|
|
5
5
|
module Spectus
|
6
6
|
module Requirement
|
7
|
-
#
|
7
|
+
# Implementation of SHOULD/SHOULD NOT requirements from RFC 2119.
|
8
|
+
#
|
9
|
+
# This level is less strict than MUST requirements. A test at this level
|
10
|
+
# passes in two cases:
|
11
|
+
# - When the matcher returns the expected result
|
12
|
+
# - When no error was raised during the test
|
13
|
+
#
|
14
|
+
# @example Testing a SHOULD requirement
|
15
|
+
# test = Recommended.new(matcher: some_matcher, negate: false)
|
16
|
+
# test.call { value } # Passes if matcher returns true or no error occurs
|
17
|
+
#
|
18
|
+
# @example Testing a SHOULD NOT requirement
|
19
|
+
# test = Recommended.new(matcher: some_matcher, negate: true)
|
20
|
+
# test.call { value } # Passes if matcher returns false or no error occurs
|
21
|
+
#
|
22
|
+
# @see https://www.ietf.org/rfc/rfc2119.txt RFC 2119 SHOULD/SHOULD NOT keywords
|
8
23
|
class Recommended < Base
|
9
|
-
#
|
24
|
+
# The RFC 2119 keyword for this requirement level.
|
10
25
|
#
|
11
|
-
# @return [Symbol]
|
26
|
+
# @return [Symbol] :SHOULD indicating a recommended requirement
|
27
|
+
# @api public
|
12
28
|
def self.level
|
13
29
|
:SHOULD
|
14
30
|
end
|
15
31
|
|
16
32
|
private
|
17
33
|
|
18
|
-
#
|
34
|
+
# Determine if the test passed according to SHOULD requirement rules.
|
35
|
+
# A test passes if either:
|
36
|
+
# - The base matcher validation passes (super)
|
37
|
+
# - No error occurred during test execution
|
19
38
|
#
|
20
39
|
# @param (see Base#passed?)
|
21
|
-
#
|
22
40
|
# @return (see Base#passed?)
|
23
41
|
def passed?(test)
|
24
42
|
super || test.error.nil?
|
@@ -4,11 +4,30 @@ require_relative "base"
|
|
4
4
|
|
5
5
|
module Spectus
|
6
6
|
module Requirement
|
7
|
-
#
|
7
|
+
# Implementation of MUST/MUST NOT requirements from RFC 2119.
|
8
|
+
#
|
9
|
+
# This level represents an absolute requirement - tests at this level
|
10
|
+
# must pass without any exceptions or conditions. Unlike SHOULD or MAY levels,
|
11
|
+
# there is no flexibility in what constitutes a passing test.
|
12
|
+
#
|
13
|
+
# The test passes only when:
|
14
|
+
# - MUST: The matcher returns true (when negate: false)
|
15
|
+
# - MUST NOT: The matcher returns false (when negate: true)
|
16
|
+
#
|
17
|
+
# @example Testing a MUST requirement
|
18
|
+
# test = Required.new(matcher: some_matcher, negate: false)
|
19
|
+
# test.call { value } # Passes only if matcher returns true
|
20
|
+
#
|
21
|
+
# @example Testing a MUST NOT requirement
|
22
|
+
# test = Required.new(matcher: some_matcher, negate: true)
|
23
|
+
# test.call { value } # Passes only if matcher returns false
|
24
|
+
#
|
25
|
+
# @see https://www.ietf.org/rfc/rfc2119.txt RFC 2119 MUST/MUST NOT keywords
|
8
26
|
class Required < Base
|
9
|
-
#
|
27
|
+
# The RFC 2119 keyword for this requirement level.
|
10
28
|
#
|
11
|
-
# @return [Symbol]
|
29
|
+
# @return [Symbol] :MUST indicating an absolute requirement
|
30
|
+
# @api public
|
12
31
|
def self.level
|
13
32
|
:MUST
|
14
33
|
end
|
data/lib/spectus/requirement.rb
CHANGED
@@ -1,7 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Spectus
|
4
|
-
# Namespace for
|
4
|
+
# Namespace for RFC 2119 requirement levels.
|
5
|
+
#
|
6
|
+
# This module contains different requirement level implementations:
|
7
|
+
# - Required (MUST/MUST NOT)
|
8
|
+
# - Recommended (SHOULD/SHOULD NOT)
|
9
|
+
# - Optional (MAY)
|
10
|
+
#
|
11
|
+
# Each level has its own rules for determining test success/failure.
|
5
12
|
#
|
6
13
|
# @api private
|
7
14
|
module Requirement
|
data/lib/spectus.rb
CHANGED
@@ -2,106 +2,207 @@
|
|
2
2
|
|
3
3
|
require_relative File.join("spectus", "requirement")
|
4
4
|
|
5
|
-
#
|
5
|
+
# A Ruby library for defining expectations with precision using RFC 2119 compliance levels.
|
6
6
|
#
|
7
|
-
# This module
|
8
|
-
#
|
7
|
+
# This module provides methods to define expectations according to different requirement
|
8
|
+
# levels (MUST, SHOULD, MAY). Each method accepts a matcher object that responds to `match?`
|
9
|
+
# and follows the block-passing protocol.
|
10
|
+
#
|
11
|
+
# While the {https://github.com/fixrb/matchi Matchi gem} provides a collection of ready-to-use
|
12
|
+
# matchers, you can create your own custom matchers:
|
13
|
+
#
|
14
|
+
# @example Creating a custom matcher
|
15
|
+
# class BeTheAnswer
|
16
|
+
# def match?
|
17
|
+
# 42.equal?(yield)
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# test = Spectus.must BeTheAnswer.new
|
22
|
+
# test.call { 42 } # => pass
|
23
|
+
# test.call { 41 } # => fail
|
24
|
+
#
|
25
|
+
# @example Using with Matchi gem
|
26
|
+
# require "spectus"
|
27
|
+
# require "matchi/eq"
|
28
|
+
#
|
29
|
+
# test = Spectus.must Matchi::Eq.new(42)
|
30
|
+
# test.call { 42 } # => pass
|
31
|
+
#
|
32
|
+
# @see https://www.ietf.org/rfc/rfc2119.txt RFC 2119 specification
|
33
|
+
# @see https://github.com/fixrb/matchi Matchi - Collection of compatible matchers
|
9
34
|
module Spectus
|
10
|
-
#
|
11
|
-
# specification.
|
35
|
+
# Defines an absolute requirement that must be satisfied by the implementation.
|
36
|
+
# This represents the RFC 2119 "MUST" level - an absolute requirement of the specification.
|
12
37
|
#
|
13
|
-
# @example
|
38
|
+
# @example With a custom matcher
|
39
|
+
# class PositiveNumber
|
40
|
+
# def match?
|
41
|
+
# (yield).positive?
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# test = Spectus.must PositiveNumber.new
|
46
|
+
# test.call { 42 }
|
47
|
+
# # => #<Expresenter::Pass actual: 42, ...>
|
48
|
+
#
|
49
|
+
# @example With Matchi gem
|
14
50
|
# require "spectus"
|
15
51
|
# require "matchi/eq"
|
16
52
|
#
|
17
|
-
# Spectus.must Matchi::Eq.new(
|
18
|
-
#
|
19
|
-
#
|
20
|
-
# @param matcher [#match?] The matcher.
|
53
|
+
# test = Spectus.must Matchi::Eq.new(42)
|
54
|
+
# test.call { 42 }
|
55
|
+
# # => #<Expresenter::Pass actual: 42, ...>
|
21
56
|
#
|
22
|
-
# @
|
57
|
+
# @param matcher [#match?] Any object that implements the matcher protocol:
|
58
|
+
# - Responds to `match?`
|
59
|
+
# - Accepts a block in `match?` that provides the actual value
|
60
|
+
# @return [Requirement::Required] An absolute requirement level instance
|
61
|
+
# @raise [ArgumentError] If matcher doesn't respond to match?
|
23
62
|
#
|
24
63
|
# @api public
|
25
64
|
def self.must(matcher)
|
65
|
+
raise ::ArgumentError, "matcher must respond to match?" unless matcher.respond_to?(:match?)
|
66
|
+
|
26
67
|
Requirement::Required.new(negate: false, matcher:)
|
27
68
|
end
|
28
69
|
|
29
|
-
#
|
70
|
+
# Defines an absolute prohibition in the specification.
|
71
|
+
# This represents the RFC 2119 "MUST NOT" level - an absolute prohibition.
|
72
|
+
#
|
73
|
+
# @example With a custom matcher
|
74
|
+
# class NegativeNumber
|
75
|
+
# def match?
|
76
|
+
# (yield).negative?
|
77
|
+
# end
|
78
|
+
# end
|
30
79
|
#
|
31
|
-
#
|
80
|
+
# test = Spectus.must_not NegativeNumber.new
|
81
|
+
# test.call { 42 }
|
82
|
+
# # => #<Expresenter::Pass actual: 42, ...>
|
83
|
+
#
|
84
|
+
# @example With Matchi gem
|
32
85
|
# require "spectus"
|
33
86
|
# require "matchi/be"
|
34
87
|
#
|
35
|
-
# Spectus.must_not Matchi::Be.new(
|
36
|
-
#
|
37
|
-
#
|
38
|
-
# @param matcher [#match?] The matcher.
|
88
|
+
# test = Spectus.must_not Matchi::Be.new(0)
|
89
|
+
# test.call { 42 }
|
90
|
+
# # => #<Expresenter::Pass actual: 42, ...>
|
39
91
|
#
|
40
|
-
# @
|
92
|
+
# @param matcher [#match?] Any object that implements the matcher protocol
|
93
|
+
# @return [Requirement::Required] An absolute prohibition level instance
|
94
|
+
# @raise [ArgumentError] If matcher doesn't respond to match?
|
41
95
|
def self.must_not(matcher)
|
96
|
+
raise ::ArgumentError, "matcher must respond to match?" unless matcher.respond_to?(:match?)
|
97
|
+
|
42
98
|
Requirement::Required.new(negate: true, matcher:)
|
43
99
|
end
|
44
100
|
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
# @example
|
101
|
+
# Defines a recommended requirement that should be satisfied unless there are valid reasons not to.
|
102
|
+
# This represents the RFC 2119 "SHOULD" level - where valid reasons may exist to ignore
|
103
|
+
# a particular item, but the implications must be understood and weighed.
|
104
|
+
#
|
105
|
+
# @example With a custom matcher
|
106
|
+
# class EvenNumber
|
107
|
+
# def match?
|
108
|
+
# (yield).even?
|
109
|
+
# end
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# test = Spectus.should EvenNumber.new
|
113
|
+
# test.call { 42 }
|
114
|
+
# # => #<Expresenter::Pass actual: 42, ...>
|
115
|
+
#
|
116
|
+
# @example With Matchi gem
|
50
117
|
# require "spectus"
|
51
118
|
# require "matchi/be"
|
52
119
|
#
|
53
|
-
# Spectus.should Matchi::Be.new(
|
54
|
-
#
|
55
|
-
#
|
56
|
-
# @param matcher [#match?] The matcher.
|
120
|
+
# test = Spectus.should Matchi::Be.new(:even?)
|
121
|
+
# test.call { 42 }
|
122
|
+
# # => #<Expresenter::Pass actual: 42, ...>
|
57
123
|
#
|
58
|
-
# @
|
124
|
+
# @param matcher [#match?] Any object that implements the matcher protocol
|
125
|
+
# @return [Requirement::Recommended] A recommended requirement level instance
|
126
|
+
# @raise [ArgumentError] If matcher doesn't respond to match?
|
59
127
|
def self.should(matcher)
|
128
|
+
raise ::ArgumentError, "matcher must respond to match?" unless matcher.respond_to?(:match?)
|
129
|
+
|
60
130
|
Requirement::Recommended.new(negate: false, matcher:)
|
61
131
|
end
|
62
132
|
|
63
|
-
#
|
64
|
-
#
|
65
|
-
# the
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
133
|
+
# Defines a behavior that is not recommended but may be acceptable in specific circumstances.
|
134
|
+
# This represents the RFC 2119 "SHOULD NOT" level - where particular behavior may be acceptable
|
135
|
+
# but the implications should be understood and the case carefully weighed.
|
136
|
+
#
|
137
|
+
# @example With a custom matcher
|
138
|
+
# class RaisesError
|
139
|
+
# def match?
|
140
|
+
# yield
|
141
|
+
# false
|
142
|
+
# rescue
|
143
|
+
# true
|
144
|
+
# end
|
145
|
+
# end
|
146
|
+
#
|
147
|
+
# test = Spectus.should_not RaisesError.new
|
148
|
+
# test.call { 42 }
|
149
|
+
# # => #<Expresenter::Pass actual: 42, ...>
|
150
|
+
#
|
151
|
+
# @example With Matchi gem
|
69
152
|
# require "spectus"
|
70
153
|
# require "matchi/raise_exception"
|
71
154
|
#
|
72
|
-
# Spectus.should_not Matchi::RaiseException.new(
|
73
|
-
#
|
74
|
-
#
|
75
|
-
# @param matcher [#match?] The matcher.
|
155
|
+
# test = Spectus.should_not Matchi::RaiseException.new(StandardError)
|
156
|
+
# test.call { 42 }
|
157
|
+
# # => #<Expresenter::Pass actual: 42, ...>
|
76
158
|
#
|
77
|
-
# @
|
78
|
-
#
|
159
|
+
# @param matcher [#match?] Any object that implements the matcher protocol
|
160
|
+
# @return [Requirement::Recommended] A not recommended requirement level instance
|
161
|
+
# @raise [ArgumentError] If matcher doesn't respond to match?
|
79
162
|
def self.should_not(matcher)
|
163
|
+
raise ::ArgumentError, "matcher must respond to match?" unless matcher.respond_to?(:match?)
|
164
|
+
|
80
165
|
Requirement::Recommended.new(negate: true, matcher:)
|
81
166
|
end
|
82
167
|
|
83
|
-
#
|
84
|
-
#
|
85
|
-
#
|
86
|
-
#
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
#
|
91
|
-
#
|
92
|
-
#
|
93
|
-
#
|
94
|
-
#
|
168
|
+
# Defines an optional feature or behavior.
|
169
|
+
# This represents the RFC 2119 "MAY" level - where an item is truly optional.
|
170
|
+
# Implementations can freely choose whether to include the item based on their
|
171
|
+
# specific needs, while maintaining interoperability with other implementations.
|
172
|
+
#
|
173
|
+
# For MAY requirements, a test passes in two cases:
|
174
|
+
# 1. When a NoMethodError is raised, indicating the feature is not implemented
|
175
|
+
# 2. When the feature is implemented and the test succeeds
|
176
|
+
#
|
177
|
+
# @example With a custom matcher testing an optional method
|
178
|
+
# class RespondsTo
|
179
|
+
# def initialize(method)
|
180
|
+
# @method = method
|
181
|
+
# end
|
182
|
+
#
|
183
|
+
# def match?
|
184
|
+
# (yield).respond_to?(@method)
|
185
|
+
# end
|
186
|
+
# end
|
187
|
+
#
|
188
|
+
# test = Spectus.may RespondsTo.new(:to_h)
|
189
|
+
# test.call { {} } # => pass (feature is implemented)
|
190
|
+
# test.call { BasicObject.new } # => pass (NoMethodError - feature not implemented)
|
191
|
+
#
|
192
|
+
# @example With Matchi gem
|
95
193
|
# require "spectus"
|
96
|
-
# require "matchi/
|
194
|
+
# require "matchi/predicate"
|
97
195
|
#
|
98
|
-
# Spectus.may Matchi::
|
99
|
-
# # =>
|
196
|
+
# test = Spectus.may Matchi::Predicate.new(:be_frozen)
|
197
|
+
# test.call { "".freeze } # => pass (feature is implemented)
|
198
|
+
# test.call { BasicObject.new } # => pass (NoMethodError - feature not implemented)
|
100
199
|
#
|
101
|
-
# @param matcher [#match?]
|
102
|
-
#
|
103
|
-
# @
|
200
|
+
# @param matcher [#match?] Any object that implements the matcher protocol
|
201
|
+
# @return [Requirement::Optional] An optional requirement level instance
|
202
|
+
# @raise [ArgumentError] If matcher doesn't respond to match?
|
104
203
|
def self.may(matcher)
|
204
|
+
raise ::ArgumentError, "matcher must respond to match?" unless matcher.respond_to?(:match?)
|
205
|
+
|
105
206
|
Requirement::Optional.new(negate: false, matcher:)
|
106
207
|
end
|
107
208
|
end
|
metadata
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spectus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.0.
|
4
|
+
version: 5.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
8
|
+
autorequire:
|
8
9
|
bindir: bin
|
9
10
|
cert_chain: []
|
10
|
-
date:
|
11
|
+
date: 2025-01-01 00:00:00.000000000 Z
|
11
12
|
dependencies:
|
12
13
|
- !ruby/object:Gem::Dependency
|
13
14
|
name: expresenter
|
@@ -15,42 +16,28 @@ dependencies:
|
|
15
16
|
requirements:
|
16
17
|
- - "~>"
|
17
18
|
- !ruby/object:Gem::Version
|
18
|
-
version: 1.5.
|
19
|
+
version: 1.5.1
|
19
20
|
type: :runtime
|
20
21
|
prerelease: false
|
21
22
|
version_requirements: !ruby/object:Gem::Requirement
|
22
23
|
requirements:
|
23
24
|
- - "~>"
|
24
25
|
- !ruby/object:Gem::Version
|
25
|
-
version: 1.5.
|
26
|
-
- !ruby/object:Gem::Dependency
|
27
|
-
name: matchi
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
29
|
-
requirements:
|
30
|
-
- - "~>"
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: '4.0'
|
33
|
-
type: :runtime
|
34
|
-
prerelease: false
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
36
|
-
requirements:
|
37
|
-
- - "~>"
|
38
|
-
- !ruby/object:Gem::Version
|
39
|
-
version: '4.0'
|
26
|
+
version: 1.5.1
|
40
27
|
- !ruby/object:Gem::Dependency
|
41
28
|
name: test_tube
|
42
29
|
requirement: !ruby/object:Gem::Requirement
|
43
30
|
requirements:
|
44
31
|
- - "~>"
|
45
32
|
- !ruby/object:Gem::Version
|
46
|
-
version: 4.0.
|
33
|
+
version: 4.0.1
|
47
34
|
type: :runtime
|
48
35
|
prerelease: false
|
49
36
|
version_requirements: !ruby/object:Gem::Requirement
|
50
37
|
requirements:
|
51
38
|
- - "~>"
|
52
39
|
- !ruby/object:Gem::Version
|
53
|
-
version: 4.0.
|
40
|
+
version: 4.0.1
|
54
41
|
description: "Expectation library with RFC 2119's requirement levels \U0001F6A5"
|
55
42
|
email: contact@cyril.email
|
56
43
|
executables: []
|
@@ -70,6 +57,7 @@ licenses:
|
|
70
57
|
- MIT
|
71
58
|
metadata:
|
72
59
|
rubygems_mfa_required: 'true'
|
60
|
+
post_install_message:
|
73
61
|
rdoc_options: []
|
74
62
|
require_paths:
|
75
63
|
- lib
|
@@ -77,14 +65,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
77
65
|
requirements:
|
78
66
|
- - ">="
|
79
67
|
- !ruby/object:Gem::Version
|
80
|
-
version: 3.
|
68
|
+
version: 3.1.0
|
81
69
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
70
|
requirements:
|
83
71
|
- - ">="
|
84
72
|
- !ruby/object:Gem::Version
|
85
73
|
version: '0'
|
86
74
|
requirements: []
|
87
|
-
rubygems_version: 3.
|
75
|
+
rubygems_version: 3.3.27
|
76
|
+
signing_key:
|
88
77
|
specification_version: 4
|
89
78
|
summary: "Expectation library with RFC 2119's requirement levels \U0001F6A5"
|
90
79
|
test_files: []
|