spectus 3.4.0 → 4.0.3

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: 2fea4a88991addc0a6b69ce46d6422e92df818447818975d6123848cbba9d5a7
4
- data.tar.gz: 8dffb97e6e58cdf0c3e5661294238e42f5477f4e677a34868ce918156086cbdb
3
+ metadata.gz: 3d2dc0863871f3794105ca9de38893d86ca937fe09bc8f90772d22f23551db79
4
+ data.tar.gz: efda8aae27fb68317b3d450ede09c13e22c7d5c8e638ba8c8fd8169ce5b93f03
5
5
  SHA512:
6
- metadata.gz: 9a4798a255dcf0097dcba1bac46af91552a11348513c0d4af77a00a748cb950e90b113c264404cc745af3c7dc730154ad81893d879a495450f6cda5ca4b07942
7
- data.tar.gz: ba56345d554c0483ce27fd6c7d876e7837d59a53a37a9b9e6417df849e0a6047b522a2c00e7f5d4327a76c1ed3e60cea68cff5970e7dcc4718d31ed89e3e6fa4
6
+ metadata.gz: 07e371fdb35d33e5451ca8188be95e153d08e58b0e32791081ecfed6d42a7eb16917ed711cff89bcaca7e27b2b10a59ba6676e1896e7f6cd15eb255861c1f2d4
7
+ data.tar.gz: 777d8c69e1beede896cd074c3c839707497a6775fe2e4263f309d66128a25b914beeee4ae822fa43c58d0b6279215c0f8a39ffe08b595ced35cdf02714e15a4e
data/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # Spectus
2
2
 
3
- [![Build Status](https://api.travis-ci.org/fixrb/spectus.svg?branch=main)](https://travis-ci.org/fixrb/spectus)
4
- [![Gem Version](https://badge.fury.io/rb/spectus.svg)](https://rubygems.org/gems/spectus)
5
- [![Documentation](https://img.shields.io/:yard-docs-38c800.svg)](https://rubydoc.info/gems/spectus/frames)
3
+ [![Version](https://img.shields.io/github/v/tag/fixrb/spectus?label=Version&logo=github)](https://github.com/fixrb/spectus/releases)
4
+ [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/fixrb/spectus/main)
5
+ [![CI](https://github.com/fixrb/spectus/workflows/CI/badge.svg?branch=main)](https://github.com/fixrb/spectus/actions?query=workflow%3Aci+branch%3Amain)
6
+ [![RuboCop](https://github.com/fixrb/spectus/workflows/RuboCop/badge.svg?branch=main)](https://github.com/fixrb/spectus/actions?query=workflow%3Arubocop+branch%3Amain)
7
+ [![License](https://img.shields.io/github/license/fixrb/spectus?label=License&logo=github)](https://github.com/fixrb/spectus/raw/main/LICENSE.md)
6
8
 
7
9
  > Expectation library with [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) requirement levels 🚥
8
10
 
@@ -26,151 +28,90 @@ Or install it yourself as:
26
28
  gem install spectus
27
29
  ```
28
30
 
29
- ## Overview
30
-
31
- Assuming that an expectation is an assertion that is either `true` or `false`,
32
- qualifying it with `MUST`, `SHOULD` and `MAY`, we can draw up several scenarios:
33
-
34
- | Requirement levels | **MUST** | **SHOULD** | **MAY** |
35
- | ------------------------- | -------- | ---------- | ------- |
36
- | Implemented & Matched | `true` | `true` | `true` |
37
- | Implemented & Not matched | `false` | `true` | `false` |
38
- | Implemented & Exception | `false` | `false` | `false` |
39
- | Not implemented | `false` | `false` | `true` |
40
-
41
- When an expectation is evaluated by __Spectus__,
42
-
43
- * in case of a _passed_ expectation, a `Spectus::Result::Pass` instance is _returned_;
44
- * in case of a _failed_ expectation, a `Spectus::Result::Fail` exception is _raised_.
45
-
46
31
  ## Usage
47
32
 
48
- The __Spectus__ library is basically a module containing an `it` instance method that accept a block representing the actual value to be evaluated through an expectation.
33
+ The __Spectus__ library is basically a module defining methods that can be used to qualify expectations in specifications.
49
34
 
50
- The `Spectus` module can be included inside a class and used as follows:
35
+ To make __Spectus__ available:
51
36
 
52
37
  ```ruby
53
38
  require "spectus"
54
-
55
- class Spec
56
- include ::Spectus
57
-
58
- attr_reader :subject
59
-
60
- def initialize(subject)
61
- @subject = subject
62
- end
63
-
64
- def test_a
65
- it { subject.upcase }.MUST eql "FOO"
66
- end
67
-
68
- def test_b
69
- it { subject.blank? }.MAY be_true
70
- end
71
-
72
- def test_c
73
- it { subject.length }.SHOULD equal 42
74
- end
75
- end
76
39
  ```
77
40
 
78
- ```ruby
79
- t = Spec.new("foo")
80
-
81
- t.test_a # => Spectus::Result::Pass(actual: "FOO", error: nil, expected: "FOO", got: true, matcher: :eql, negate: false, level: :MUST)
41
+ For convenience, we will also instantiate some matchers from the [Matchi library](https://github.com/fixrb/matchi):
82
42
 
83
- t.test_b # => Spectus::Result::Pass(actual: nil, error: #<NoMethodError: undefined method `blank?' for "foo":String>, expected: nil, got: nil, matcher: :be_true, negate: false, level: :MAY)
84
-
85
- t.test_c # => Spectus::Result::Pass(actual: 3, error: nil, expected: 42, got: false, matcher: :equal, negate: false, level: :SHOULD)
43
+ ```sh
44
+ gem install matchi
86
45
  ```
87
46
 
88
47
  ```ruby
89
- t = Spec.new(4)
90
-
91
- t.test_a # => raises an exception:
92
- # Traceback (most recent call last):
93
- # 3: from ./bin/console:8:in `<main>'
94
- # 2: from (irb):23
95
- # 1: from (irb):11:in `test_a'
96
- # Spectus::Result::Fail (NoMethodError: undefined method `upcase' for 4:Integer)
97
-
98
- t.test_b # => Spectus::Result::Pass(actual: nil, error: #<NoMethodError: undefined method `blank?' for 4:Integer>, expected: nil, got: nil, matcher: :be_true, negate: false, level: :MAY)
99
-
100
- t.test_c # => raises an exception:
101
- # Traceback (most recent call last):
102
- # 3: from ./bin/console:8:in `<main>'
103
- # 2: from (irb):25
104
- # 1: from (irb):19:in `test_c'
105
- # Spectus::Result::Fail (NoMethodError: undefined method `length' for 4:Integer.)
106
- ```
107
-
108
- ## More examples
109
-
110
- To make __Spectus__ available:
111
-
112
- ```ruby
113
- require "spectus"
114
-
115
- include Spectus
48
+ require "matchi"
116
49
  ```
117
50
 
118
51
  All examples here assume that this has been done.
119
52
 
120
53
  ### Absolute Requirement
121
54
 
122
- There's only one bat:
55
+ There is exactly one bat:
123
56
 
124
57
  ```ruby
125
- it { "🦇".size }.MUST equal 1
126
- # => Spectus::Result::Pass(actual: 1, error: nil, expected: 1, got: true, matcher: :equal, negate: false, level: :MUST)
58
+ definition = Spectus.must Matchi::Be.new(1)
59
+ definition.call { "🦇".size }
60
+ # => Expresenter::Pass(actual: 1, definition: "be 1", error: nil, expected: 1, got: true, negate: false, level: :MUST)
127
61
  ```
128
62
 
63
+ The test is passed.
64
+
129
65
  ### Absolute Prohibition
130
66
 
131
- The true from the false:
67
+ Truth and lies:
132
68
 
133
69
  ```ruby
134
- it { false }.MUST_NOT be_true
135
- # => Spectus::Result::Pass(actual: false, error: nil, expected: nil, got: true, matcher: :be_true, negate: true, level: :MUST)
70
+ definition = Spectus.must_not Matchi::Be.new(true)
71
+ definition.call { false }
72
+ # => Expresenter::Pass(actual: false, definition: "be true", error: nil, expected: true, got: true, negate: true, level: :MUST)
136
73
  ```
137
74
 
138
75
  ### Recommended
139
76
 
140
- A well-known joke. An addition of `0.1` and `0.2` is deadly precise:
77
+ A well-known joke. The addition of `0.1` and `0.2` is deadly precise:
141
78
 
142
79
  ```ruby
143
- it { 0.1 + 0.2 }.SHOULD equal 0.3
144
- # => Spectus::Result::Pass(actual: 0.30000000000000004, error: nil, expected: 0.3, got: false, matcher: :equal, negate: false, level: :SHOULD)
80
+ definition = Spectus.should Matchi::Be.new(0.3)
81
+ definition.call { 0.1 + 0.2 }
82
+ # => Expresenter::Pass(actual: 0.30000000000000004, definition: "be 0.3", error: nil, expected: 0.3, got: false, negate: false, level: :SHOULD)
145
83
  ```
146
84
 
147
85
  ### Not Recommended
148
86
 
149
- The situation should still be under control:
87
+ This should not be wrong:
150
88
 
151
89
  ```ruby
152
- it { BOOM }.SHOULD_NOT raise_exception SystemExit
153
- ```
90
+ definition = Spectus.should_not Matchi::Match.new("123456")
154
91
 
155
- ```txt
156
- Traceback (most recent call last):
157
- 2: from ./bin/console:8:in `<main>'
158
- 1: from (irb):8
159
- Spectus::Result::Fail (NameError: uninitialized constant BOOM.)
92
+ definition.call do
93
+ require "securerandom"
94
+
95
+ SecureRandom.hex(3)
96
+ end
97
+ # => Expresenter::Pass(actual: "bb5716", definition: "match \"123456\"", error: nil, expected: "123456", got: true, negate: true, level: :SHOULD)
160
98
  ```
161
99
 
100
+ In any case, as long as there are no exceptions, the test passes.
101
+
162
102
  ### Optional
163
103
 
164
104
  An empty array is blank, right?
165
105
 
166
106
  ```ruby
167
- it { [].blank? }.MAY be_true
168
- # => Spectus::Result::Pass(actual: nil, error: #<NoMethodError: undefined method `blank?' for []:Array>, expected: nil, got: nil, matcher: :be_true, negate: false, level: :MAY)
107
+ definition = Spectus.may Matchi::Be.new(true)
108
+ definition.call { [].blank? }
109
+ # => Expresenter::Pass(actual: nil, definition: "be true", error: #<NoMethodError: undefined method `blank?' for []:Array>, expected: true, got: nil, negate: false, level: :MAY)
169
110
  ```
170
111
 
171
- Damn, I forgot to load activesupport. 🤦‍♂️
112
+ My bad! ActiveSupport was not imported. 🤦‍♂️
172
113
 
173
- That said, the test is passing due to the _not-implemented-like_ raised exception: `NoMethodError`.
114
+ Anyways, the test passes because the exception produced is `NoMethodError`, meaning that the functionality is not implemented.
174
115
 
175
116
  ## Code Isolation
176
117
 
@@ -185,8 +126,9 @@ Example of test without isolation:
185
126
  ```ruby
186
127
  greeting = "Hello, world!"
187
128
 
188
- it { greeting.gsub!("world", "Alice") }.MUST eql "Hello, Alice!"
189
- # => Spectus::Result::Pass(actual: "Hello, Alice!", error: nil, expected: "Hello, Alice!", got: true, matcher: :eql, negate: false, level: :MUST)
129
+ definition = Spectus.must Matchi::Eq.new("Hello, Alice!")
130
+ definition.call { greeting.gsub!("world", "Alice") }
131
+ # => Expresenter::Pass(actual: "Hello, Alice!", definition: "eq \"Hello, Alice!\"", error: nil, expected: "Hello, Alice!", got: true, negate: false, level: :MUST)
190
132
 
191
133
  greeting # => "Hello, Alice!"
192
134
  ```
@@ -196,8 +138,9 @@ Example of test in isolation:
196
138
  ```ruby
197
139
  greeting = "Hello, world!"
198
140
 
199
- it { greeting.gsub!("world", "Alice") }.MUST! eql "Hello, Alice!"
200
- # => Spectus::Result::Pass(actual: "Hello, Alice!", error: nil, expected: "Hello, Alice!", got: true, matcher: :eql, negate: false, level: :MUST)
141
+ definition = Spectus.must! Matchi::Eq.new("Hello, Alice!")
142
+ definition.call { greeting.gsub!("world", "Alice") }
143
+ # => Expresenter::Pass(actual: "Hello, Alice!", definition: "eq \"Hello, Alice!\"", error: nil, expected: "Hello, Alice!", got: true, negate: false, level: :MUST)
201
144
 
202
145
  greeting # => "Hello, world!"
203
146
  ```
@@ -206,6 +149,7 @@ greeting # => "Hello, world!"
206
149
 
207
150
  * Home page: https://github.com/fixrb/spectus
208
151
  * Bugs/issues: https://github.com/fixrb/spectus/issues
152
+ * Blog post: https://batman.buzz/a-spectus-tutorial-expectations-with-rfc-2119-compliance-1fc769861c1
209
153
 
210
154
  ## Versioning
211
155
 
@@ -213,7 +157,7 @@ __Spectus__ follows [Semantic Versioning 2.0](https://semver.org/).
213
157
 
214
158
  ## License
215
159
 
216
- The [gem](https://rubygems.org/gems/spectus) is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
160
+ The [gem](https://rubygems.org/gems/spectus) is available as open source under the terms of the [MIT License](https://github.com/fixrb/spectus/raw/main/LICENSE.md).
217
161
 
218
162
  ***
219
163
 
data/lib/spectus.rb CHANGED
@@ -1,114 +1,207 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "matchi/helper"
4
-
5
- require_relative File.join("spectus", "expectation_target")
3
+ require_relative File.join("spectus", "requirement")
6
4
 
7
5
  # Namespace for the Spectus library.
8
6
  #
9
- # This module defines the {#it} method to create expectations, which can be
10
- # automatically included into classes.
11
- #
12
- # @example
13
- # class Spec
14
- # include ::Spectus
15
- #
16
- # attr_reader :subject
17
- #
18
- # def initialize(subject)
19
- # @subject = subject
20
- # end
21
- #
22
- # def test_a
23
- # it { subject.upcase }.MUST eql "FOO"
24
- # end
25
- #
26
- # def test_b
27
- # it { subject.blank? }.MAY be_true
28
- # end
29
- #
30
- # def test_c
31
- # it { subject.length }.SHOULD equal 42
32
- # end
33
- # end
34
- #
35
- # t = Spec.new("foo")
36
- # t.test_a # => Spectus::Result::Pass(actual: "FOO", error: nil, expected: "FOO", got: true, matcher: :eql, negate: false, level: :MUST)
37
- # t.test_b # => Spectus::Result::Pass(actual: nil, error: #<NoMethodError: undefined method `blank?' for "foo":String>, expected: nil, got: nil, matcher: :be_true, negate: false, level: :MAY)
38
- # t.test_c # => Spectus::Result::Pass(actual: 3, error: nil, expected: 42, got: false, matcher: :equal, negate: false, level: :SHOULD)
39
- #
40
- # Or even directly used like this.
41
- #
42
- # @example
43
- # require 'spectus'
44
- #
45
- # include Spectus
46
- #
47
- # it { 42 }.MUST equal 42 # => Spectus::Result::Pass(actual: 42, error: nil, expected: 42, got: true, matcher: :equal, negate: false, level: :MUST
48
- #
49
- # It also includes a collection of expectation matchers 🤹
50
- #
51
- # @example Equivalence matcher
52
- # matcher = eql("foo") # => Matchi::Matcher::Eql.new("foo")
53
- # matcher.matches? { "foo" } # => true
54
- # matcher.matches? { "bar" } # => false
55
- #
56
- # @example Identity matcher
57
- # object = "foo"
58
- #
59
- # matcher = equal(object) # => Matchi::Matcher::Equal.new(object)
60
- # matcher.matches? { object } # => true
61
- # matcher.matches? { "foo" } # => false
62
- #
63
- # @example Regular expressions matcher
64
- # matcher = match(/^foo$/) # => Matchi::Matcher::Match.new(/^foo$/)
65
- # matcher.matches? { "foo" } # => true
66
- # matcher.matches? { "bar" } # => false
67
- #
68
- # @example Expecting errors matcher
69
- # matcher = raise_exception(NameError) # => Matchi::Matcher::RaiseException.new(NameError)
70
- # matcher.matches? { Boom } # => true
71
- # matcher.matches? { true } # => false
72
- #
73
- # @example Truth matcher
74
- # matcher = be_true # => Matchi::Matcher::BeTrue.new
75
- # matcher.matches? { true } # => true
76
- # matcher.matches? { false } # => false
77
- # matcher.matches? { nil } # => false
78
- # matcher.matches? { 4 } # => false
79
- #
80
- # @example Untruth matcher
81
- # matcher = be_false # => Matchi::Matcher::BeFalse.new
82
- # matcher.matches? { false } # => true
83
- # matcher.matches? { true } # => false
84
- # matcher.matches? { nil } # => false
85
- # matcher.matches? { 4 } # => false
86
- #
87
- # @example Nil matcher
88
- # matcher = be_nil # => Matchi::Matcher::BeNil.new
89
- # matcher.matches? { nil } # => true
90
- # matcher.matches? { false } # => false
91
- # matcher.matches? { true } # => false
92
- # matcher.matches? { 4 } # => false
93
- #
94
- # @example Type/class matcher
95
- # matcher = be_an_instance_of(String) # => Matchi::Matcher::BeAnInstanceOf.new(String)
96
- # matcher.matches? { "foo" } # => true
97
- # matcher.matches? { 4 } # => false
98
- #
99
- # @see https://github.com/fixrb/matchi
7
+ # This module defines methods that can be used to qualify expectations in
8
+ # specifications.
100
9
  module Spectus
101
- include ::Matchi::Helper
10
+ # This method mean that the definition is an absolute requirement of the
11
+ # specification.
12
+ #
13
+ # @example An absolute requirement definition
14
+ # require "spectus"
15
+ # require "matchi/eq"
16
+ #
17
+ # Spectus.must Matchi::Eq.new("FOO")
18
+ # # => #<MUST Matchi::Eq("FOO") isolate=false negate=false>
19
+ #
20
+ # @param matcher [#matches?] The matcher.
21
+ #
22
+ # @return [Requirement::Required] An absolute requirement level instance.
23
+ #
24
+ # @api public
25
+ def self.must(matcher)
26
+ Requirement::Required.new(
27
+ isolate: false,
28
+ negate: false,
29
+ matcher: matcher
30
+ )
31
+ end
32
+
33
+ # @example An absolute requirement definition with isolation
34
+ # require "spectus"
35
+ # require "matchi/eq"
36
+ #
37
+ # Spectus.must! Matchi::Eq.new("FOO")
38
+ # # => #<MUST Matchi::Eq("FOO") isolate=true negate=false>
39
+ #
40
+ # @see must
41
+ def self.must!(matcher)
42
+ Requirement::Required.new(
43
+ isolate: true,
44
+ negate: false,
45
+ matcher: matcher
46
+ )
47
+ end
48
+
49
+ # This method mean that the definition is an absolute prohibition of the specification.
50
+ #
51
+ # @example An absolute prohibition definition
52
+ # require "spectus"
53
+ # require "matchi/be"
54
+ #
55
+ # Spectus.must_not Matchi::Be.new(42)
56
+ # # => #<MUST Matchi::Be(42) isolate=false negate=true>
57
+ #
58
+ # @param matcher [#matches?] The matcher.
59
+ #
60
+ # @return [Requirement::Required] An absolute prohibition level instance.
61
+ def self.must_not(matcher)
62
+ Requirement::Required.new(
63
+ isolate: false,
64
+ negate: true,
65
+ matcher: matcher
66
+ )
67
+ end
68
+
69
+ # @example An absolute prohibition definition with isolation
70
+ # require "spectus"
71
+ # require "matchi/be"
72
+ #
73
+ # Spectus.must_not! Matchi::Be.new(42)
74
+ # # => #<MUST Matchi::Be(42) isolate=true negate=true>
75
+ #
76
+ # @see must_not
77
+ def self.must_not!(matcher)
78
+ Requirement::Required.new(
79
+ isolate: true,
80
+ negate: true,
81
+ matcher: matcher
82
+ )
83
+ end
84
+
85
+ # This method mean that there may exist valid reasons in particular
86
+ # circumstances to ignore a particular item, but the full implications must be
87
+ # understood and carefully weighed before choosing a different course.
88
+ #
89
+ # @example A recommended definition
90
+ # require "spectus"
91
+ # require "matchi/be"
92
+ #
93
+ # Spectus.should Matchi::Be.new(true)
94
+ # # => #<SHOULD Matchi::Be(true) isolate=false negate=false>
95
+ #
96
+ # @param matcher [#matches?] The matcher.
97
+ #
98
+ # @return [Requirement::Recommended] A recommended requirement level instance.
99
+ def self.should(matcher)
100
+ Requirement::Recommended.new(
101
+ isolate: false,
102
+ negate: false,
103
+ matcher: matcher
104
+ )
105
+ end
106
+
107
+ # @example A recommended definition with isolation
108
+ # require "spectus"
109
+ # require "matchi/be"
110
+ #
111
+ # Spectus.should! Matchi::Be.new(true)
112
+ # # => #<SHOULD Matchi::Be(true) isolate=true negate=false>
113
+ #
114
+ # @see should
115
+ def self.should!(matcher)
116
+ Requirement::Recommended.new(
117
+ isolate: true,
118
+ negate: false,
119
+ matcher: matcher
120
+ )
121
+ end
102
122
 
103
- # Expectations are built with this method.
123
+ # This method mean that there may exist valid reasons in particular
124
+ # circumstances when the particular behavior is acceptable or even useful, but
125
+ # the full implications should be understood and the case carefully weighed
126
+ # before implementing any behavior described with this label.
127
+ #
128
+ # @example A not recommended definition
129
+ # require "spectus"
130
+ # require "matchi/raise_exception"
131
+ #
132
+ # Spectus.should_not Matchi::RaiseException.new(NoMethodError)
133
+ # # => #<SHOULD Matchi::RaiseException(NoMethodError) isolate=false negate=true>
134
+ #
135
+ # @param matcher [#matches?] The matcher.
136
+ #
137
+ # @return [Requirement::Recommended] A not recommended requirement level
138
+ # instance.
139
+ def self.should_not(matcher)
140
+ Requirement::Recommended.new(
141
+ isolate: false,
142
+ negate: true,
143
+ matcher: matcher
144
+ )
145
+ end
146
+
147
+ # @example A not recommended definition with isolation
148
+ # require "spectus"
149
+ # require "matchi/raise_exception"
150
+ #
151
+ # Spectus.should_not! Matchi::RaiseException.new(NoMethodError)
152
+ # # => #<SHOULD Matchi::RaiseException(NoMethodError) isolate=true negate=true>
104
153
  #
105
- # @example An _absolute requirement_ definition.
106
- # it { 42 }.MUST equal 42 # => Spectus::Result::Pass(actual: 42, error: nil, expected: 42, got: true, matcher: :equal, negate: false, level: :MUST
154
+ # @see should_not
155
+ def self.should_not!(matcher)
156
+ Requirement::Recommended.new(
157
+ isolate: true,
158
+ negate: true,
159
+ matcher: matcher
160
+ )
161
+ end
162
+
163
+ # This method mean that an item is truly optional.
164
+ # One vendor may choose to include the item because a particular marketplace
165
+ # requires it or because the vendor feels that it enhances the product while
166
+ # another vendor may omit the same item. An implementation which does not
167
+ # include a particular option must be prepared to interoperate with another
168
+ # implementation which does include the option, though perhaps with reduced
169
+ # functionality. In the same vein an implementation which does include a
170
+ # particular option must be prepared to interoperate with another
171
+ # implementation which does not include the option (except, of course, for the
172
+ # feature the option provides).
173
+ #
174
+ # @example An optional definition
175
+ # require "spectus"
176
+ # require "matchi/match"
177
+ #
178
+ # Spectus.may Matchi::Match.new(/^foo$/)
179
+ # # => #<MAY Matchi::Match(/^foo$/) isolate=false negate=false>
180
+ #
181
+ # @param matcher [#matches?] The matcher.
182
+ #
183
+ # @return [Requirement::Optional] An optional requirement level instance.
184
+ def self.may(matcher)
185
+ Requirement::Optional.new(
186
+ isolate: false,
187
+ negate: false,
188
+ matcher: matcher
189
+ )
190
+ end
191
+
192
+ # @example An optional definition with isolation
193
+ # require "spectus"
194
+ # require "matchi/match"
107
195
  #
108
- # @param input [Proc] The code to test.
196
+ # Spectus.may! Matchi::Match.new(/^foo$/)
197
+ # # => #<MAY Matchi::Match(/^foo$/) isolate=true negate=false>
109
198
  #
110
- # @return [ExpectationTarget] The expectation target.
111
- def it(&input)
112
- ExpectationTarget.new(&input)
199
+ # @see may
200
+ def self.may!(matcher)
201
+ Requirement::Optional.new(
202
+ isolate: true,
203
+ negate: false,
204
+ matcher: matcher
205
+ )
113
206
  end
114
207
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spectus
4
+ # Namespace for the results.
5
+ #
6
+ # @api private
7
+ module Requirement
8
+ end
9
+ end
10
+
11
+ require_relative File.join("requirement", "required")
12
+ require_relative File.join("requirement", "recommended")
13
+ require_relative File.join("requirement", "optional")
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "expresenter"
4
+ require "test_tube"
5
+
6
+ module Spectus
7
+ # Namespace for the requirement levels.
8
+ module Requirement
9
+ # Requirement level's base class.
10
+ class Base
11
+ # Initialize the requirement level class.
12
+ #
13
+ # @param isolate [Boolean] Compute actual in a subprocess.
14
+ # @param matcher [#matches?] The matcher.
15
+ # @param negate [Boolean] Invert the matcher or not.
16
+ def initialize(isolate:, matcher:, negate:)
17
+ @isolate = isolate
18
+ @matcher = matcher
19
+ @negate = negate
20
+ end
21
+
22
+ # Test result.
23
+ #
24
+ # @raise [::Expresenter::Fail] A failed spec exception.
25
+ # @return [::Expresenter::Pass] A passed spec instance.
26
+ #
27
+ # @see https://github.com/fixrb/expresenter
28
+ #
29
+ # @api public
30
+ def call(&block)
31
+ test = ::TestTube.invoke(isolate: @isolate, matcher: @matcher, negate: @negate, &block)
32
+
33
+ ::Expresenter.call(passed?(test)).with(
34
+ actual: test.actual,
35
+ definition: @matcher.to_s,
36
+ error: test.error,
37
+ expected: @matcher.expected,
38
+ got: test.got,
39
+ level: self.class.level,
40
+ negate: @negate
41
+ )
42
+ end
43
+
44
+ # :nocov:
45
+
46
+ # A string containing a human-readable representation of the definition.
47
+ #
48
+ # @example The human-readable representation of an absolute requirement.
49
+ # require "spectus"
50
+ # require "matchi/be"
51
+ #
52
+ # definition = Spectus.must Matchi::Be.new(1)
53
+ # definition.inspect
54
+ # # => "#<MUST Matchi::Be(1) isolate=false negate=false>"
55
+ #
56
+ # @return [String] The human-readable representation of the definition.
57
+ #
58
+ # @api public
59
+ def inspect
60
+ "#<#{self.class.level} #{@matcher.inspect} isolate=#{@isolate} negate=#{@negate}>"
61
+ end
62
+
63
+ # :nocov:
64
+
65
+ private
66
+
67
+ # Code experiment result.
68
+ #
69
+ # @param test [::TestTube::Base] The state of the experiment.
70
+ #
71
+ # @see https://github.com/fixrb/test_tube
72
+ #
73
+ # @return [Boolean] The result of the test (passed or failed).
74
+ def passed?(test)
75
+ test.got.equal?(true)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Spectus
6
+ module Requirement
7
+ # Optional requirement level.
8
+ class Optional < Base
9
+ # Key word for use in RFCs to indicate requirement levels.
10
+ #
11
+ # @return [Symbol] The requirement level.
12
+ def self.level
13
+ :MAY
14
+ end
15
+
16
+ private
17
+
18
+ # Code experiment result.
19
+ #
20
+ # @param (see Base#passed?)
21
+ #
22
+ # @return (see Base#passed?)
23
+ def passed?(test)
24
+ super || test.error.is_a?(::NoMethodError)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Spectus
6
+ module Requirement
7
+ # Recommended and not recommended requirement levels.
8
+ class Recommended < Base
9
+ # Key word for use in RFCs to indicate requirement levels.
10
+ #
11
+ # @return [Symbol] The requirement level.
12
+ def self.level
13
+ :SHOULD
14
+ end
15
+
16
+ private
17
+
18
+ # Code experiment result.
19
+ #
20
+ # @param (see Base#passed?)
21
+ #
22
+ # @return (see Base#passed?)
23
+ def passed?(test)
24
+ super || test.error.nil?
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Spectus
6
+ module Requirement
7
+ # Absolute requirement and absolute prohibition levels.
8
+ class Required < Base
9
+ # Key word for use in RFCs to indicate requirement levels.
10
+ #
11
+ # @return [Symbol] The requirement level.
12
+ def self.level
13
+ :MUST
14
+ end
15
+ end
16
+ end
17
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spectus
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.0
4
+ version: 4.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-06-19 00:00:00.000000000 Z
11
+ date: 2021-07-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: expresenter
@@ -16,44 +16,44 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 1.3.0
19
+ version: 1.4.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 1.3.0
26
+ version: 1.4.0
27
27
  - !ruby/object:Gem::Dependency
28
- name: matchi
28
+ name: test_tube
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 2.1.0
33
+ version: 2.1.1
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 2.1.0
40
+ version: 2.1.1
41
41
  - !ruby/object:Gem::Dependency
42
- name: test_tube
42
+ name: brutal
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 1.0.0
48
- type: :runtime
47
+ version: '0'
48
+ type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 1.0.0
54
+ version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: brutal
56
+ name: bundler
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -67,7 +67,7 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: bundler
70
+ name: matchi
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
@@ -187,14 +187,11 @@ files:
187
187
  - LICENSE.md
188
188
  - README.md
189
189
  - lib/spectus.rb
190
- - lib/spectus/expectation_target.rb
191
- - lib/spectus/requirement_level/base.rb
192
- - lib/spectus/requirement_level/may.rb
193
- - lib/spectus/requirement_level/must.rb
194
- - lib/spectus/requirement_level/should.rb
195
- - lib/spectus/result.rb
196
- - lib/spectus/result/fail.rb
197
- - lib/spectus/result/pass.rb
190
+ - lib/spectus/requirement.rb
191
+ - lib/spectus/requirement/base.rb
192
+ - lib/spectus/requirement/optional.rb
193
+ - lib/spectus/requirement/recommended.rb
194
+ - lib/spectus/requirement/required.rb
198
195
  homepage: https://github.com/fixrb/spectus
199
196
  licenses:
200
197
  - MIT
@@ -1,202 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative File.join("requirement_level", "must")
4
- require_relative File.join("requirement_level", "should")
5
- require_relative File.join("requirement_level", "may")
6
-
7
- module Spectus
8
- # Wraps the target of an expectation.
9
- #
10
- # @example
11
- # it { actual value } # => ExpectationTarget wrapping the block
12
- class ExpectationTarget
13
- # Create a new expectation target
14
- #
15
- # @param callable [Proc] The object to test.
16
- def initialize(&callable)
17
- @callable = callable
18
- end
19
-
20
- # rubocop:disable Naming/MethodName
21
-
22
- # This word, or the terms "REQUIRED" or "SHALL", mean that the
23
- # definition is an absolute requirement of the specification.
24
- #
25
- # @example _Absolute requirement_ definition
26
- # it { "foo".upcase }.MUST eql 'FOO'
27
- #
28
- # @param matcher [#matches?] The matcher.
29
- #
30
- # @return [Spectus::Result::Fail, Spectus::Result::Pass] Report if the spec
31
- # pass or fail.
32
- def MUST(matcher)
33
- RequirementLevel::Must.new(
34
- callable: callable,
35
- isolation: false,
36
- negate: false,
37
- matcher: matcher
38
- ).call
39
- end
40
-
41
- # @example _Absolute requirement_ definition with isolation
42
- # it { "foo".upcase }.MUST! eql 'FOO'
43
- #
44
- # @see MUST
45
- def MUST!(matcher)
46
- RequirementLevel::Must.new(
47
- callable: callable,
48
- isolation: true,
49
- negate: false,
50
- matcher: matcher
51
- ).call
52
- end
53
-
54
- # This phrase, or the phrase "SHALL NOT", mean that the
55
- # definition is an absolute prohibition of the specification.
56
- #
57
- # @example _Absolute prohibition_ definition
58
- # it { "foo".size }.MUST_NOT equal 42
59
- #
60
- # @param matcher [#matches?] The matcher.
61
- #
62
- # @return [Spectus::Result::Fail, Spectus::Result::Pass] Report if the spec
63
- # pass or fail.
64
- def MUST_NOT(matcher)
65
- RequirementLevel::Must.new(
66
- callable: callable,
67
- isolation: false,
68
- negate: true,
69
- matcher: matcher
70
- ).call
71
- end
72
-
73
- # @example _Absolute prohibition_ definition with isolation
74
- # it { "foo".size }.MUST_NOT! equal 42
75
- #
76
- # @see MUST_NOT
77
- def MUST_NOT!(matcher)
78
- RequirementLevel::Must.new(
79
- callable: callable,
80
- isolation: true,
81
- negate: true,
82
- matcher: matcher
83
- ).call
84
- end
85
-
86
- # This word, or the adjective "RECOMMENDED", mean that there
87
- # may exist valid reasons in particular circumstances to ignore a
88
- # particular item, but the full implications must be understood and
89
- # carefully weighed before choosing a different course.
90
- #
91
- # @example _Recommended_ definition
92
- # it { "foo".valid_encoding? }.SHOULD equal true
93
- #
94
- # @param matcher [#matches?] The matcher.
95
- #
96
- # @return [Spectus::Result::Fail, Spectus::Result::Pass] Report if the spec
97
- # pass or fail.
98
- def SHOULD(matcher)
99
- RequirementLevel::Should.new(
100
- callable: callable,
101
- isolation: false,
102
- negate: false,
103
- matcher: matcher
104
- ).call
105
- end
106
-
107
- # @example _Recommended_ definition with isolation
108
- # it { "foo".valid_encoding? }.SHOULD! equal true
109
- #
110
- # @see SHOULD
111
- def SHOULD!(matcher)
112
- RequirementLevel::Should.new(
113
- callable: callable,
114
- isolation: true,
115
- negate: false,
116
- matcher: matcher
117
- ).call
118
- end
119
-
120
- # This phrase, or the phrase "NOT RECOMMENDED" mean that
121
- # there may exist valid reasons in particular circumstances when the
122
- # particular behavior is acceptable or even useful, but the full
123
- # implications should be understood and the case carefully weighed
124
- # before implementing any behavior described with this label.
125
- #
126
- # @example _Not recommended_ definition
127
- # it { "".blank? }.SHOULD_NOT raise_exception NoMethodError
128
- #
129
- # @param matcher [#matches?] The matcher.
130
- #
131
- # @return [Spectus::Result::Fail, Spectus::Result::Pass] Report if the spec
132
- # pass or fail.
133
- def SHOULD_NOT(matcher)
134
- RequirementLevel::Should.new(
135
- callable: callable,
136
- isolation: false,
137
- negate: true,
138
- matcher: matcher
139
- ).call
140
- end
141
-
142
- # @example _Not recommended_ definition with isolation
143
- # it { "".blank? }.SHOULD_NOT! raise_exception NoMethodError
144
- #
145
- # @see SHOULD_NOT
146
- def SHOULD_NOT!(matcher)
147
- RequirementLevel::Should.new(
148
- callable: callable,
149
- isolation: true,
150
- negate: true,
151
- matcher: matcher
152
- ).call
153
- end
154
-
155
- # This word, or the adjective "OPTIONAL", mean that an item is
156
- # truly optional. One vendor may choose to include the item because a
157
- # particular marketplace requires it or because the vendor feels that
158
- # it enhances the product while another vendor may omit the same item.
159
- # An implementation which does not include a particular option MUST be
160
- # prepared to interoperate with another implementation which does
161
- # include the option, though perhaps with reduced functionality. In the
162
- # same vein an implementation which does include a particular option
163
- # MUST be prepared to interoperate with another implementation which
164
- # does not include the option (except, of course, for the feature the
165
- # option provides.)
166
- #
167
- # @example _Optional_ definition
168
- # it { "foo".bar }.MAY match /^foo$/
169
- #
170
- # @param matcher [#matches?] The matcher.
171
- #
172
- # @return [Spectus::Result::Fail, Spectus::Result::Pass] Report if the spec pass or fail.
173
- def MAY(matcher)
174
- RequirementLevel::May.new(
175
- callable: callable,
176
- isolation: false,
177
- negate: false,
178
- matcher: matcher
179
- ).call
180
- end
181
-
182
- # @example _Optional_ definition with isolation
183
- # it { "foo".bar }.MAY! match /^foo$/
184
- #
185
- # @see MAY
186
- def MAY!(matcher)
187
- RequirementLevel::May.new(
188
- callable: callable,
189
- isolation: true,
190
- negate: false,
191
- matcher: matcher
192
- ).call
193
- end
194
-
195
- # rubocop:enable Naming/MethodName
196
-
197
- protected
198
-
199
- # @return [#call] The callable object to test.
200
- attr_reader :callable
201
- end
202
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "test_tube"
4
-
5
- require_relative File.join("..", "result")
6
-
7
- module Spectus
8
- # Namespace for the requirement levels.
9
- module RequirementLevel
10
- # Requirement level's base class.
11
- class Base
12
- # Initialize the requirement level class.
13
- #
14
- # @param callable [#call] The callable object to test.
15
- # @param isolation [Boolean] Compute actual in isolation?
16
- # @param negate [Boolean] Invert the matcher or not.
17
- # @param matcher [#matches?] The matcher.
18
- def initialize(callable:, isolation:, matcher:, negate:)
19
- @negate = negate
20
- @matcher = matcher
21
- @experiment = ::TestTube.invoke(
22
- callable,
23
- isolation: isolation,
24
- matcher: matcher,
25
- negate: negate
26
- )
27
- end
28
-
29
- # @return [TestTube::Base] The experiment.
30
- attr_reader :experiment
31
-
32
- # @return [#matches?] The matcher that performed a boolean comparison
33
- # between the actual value and the expected value.
34
- attr_reader :matcher
35
-
36
- # The result of the expectation.
37
- #
38
- # @raise [Spectus::Result::Fail] The expectation failed.
39
- # @return [Spectus::Result::Pass] The expectation passed.
40
- def call
41
- Result.call(pass?).with(
42
- actual: experiment.actual,
43
- error: experiment.error,
44
- expected: matcher.expected,
45
- got: experiment.got,
46
- level: level,
47
- matcher: matcher.class.to_sym,
48
- negate: negate?
49
- )
50
- end
51
-
52
- protected
53
-
54
- # Some key words for use in RFCs to indicate requirement levels.
55
- #
56
- # @return [:MUST, :SHOULD, :MAY] The requirement level.
57
- def level
58
- self.class.name.split("::").fetch(-1).upcase.to_sym
59
- end
60
-
61
- # @note The boolean comparison between the actual value and the expected
62
- # value can be evaluated to a negative assertion.
63
- #
64
- # @return [Boolean] Invert the matcher or not.
65
- def negate?
66
- @negate
67
- end
68
- end
69
- end
70
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "must"
4
-
5
- module Spectus
6
- module RequirementLevel
7
- # May requirement level's class.
8
- class May < Must
9
- # Evaluate the expectation.
10
- #
11
- # @return [Boolean] Report if the low expectation pass or fail?
12
- def pass?
13
- super || experiment.error.is_a?(::NoMethodError)
14
- end
15
- end
16
- end
17
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "base"
4
-
5
- module Spectus
6
- module RequirementLevel
7
- # Must requirement level's class.
8
- class Must < Base
9
- # Evaluate the expectation.
10
- #
11
- # @return [Boolean] Report if the high expectation pass or fail?
12
- def pass?
13
- experiment.got.equal?(true)
14
- end
15
- end
16
- end
17
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "must"
4
-
5
- module Spectus
6
- module RequirementLevel
7
- # Should requirement level's class.
8
- class Should < Must
9
- # Evaluate the expectation.
10
- #
11
- # @return [Boolean] Report if the medium expectation pass or fail?
12
- def pass?
13
- super || experiment.error.nil?
14
- end
15
- end
16
- end
17
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative File.join("result", "fail")
4
- require_relative File.join("result", "pass")
5
-
6
- module Spectus
7
- # Namespace for the results.
8
- module Result
9
- # @param is_passed [Boolean] The value of an assertion.
10
- # @return [Class<Spectus::Result::Pass>, Class<Spectus::Result::Fail>] The
11
- # class of the result.
12
- # @example Get the pass class result.
13
- # call(true) # => Pass
14
- def self.call(is_passed)
15
- is_passed ? Pass : Fail
16
- end
17
- end
18
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "expresenter/fail"
4
-
5
- module Spectus
6
- module Result
7
- # The class that is responsible for reporting that the expectation is false.
8
- #
9
- # @see https://github.com/fixrb/expresenter/blob/v1.2.1/lib/expresenter/fail.rb
10
- class Fail < ::Expresenter::Fail
11
- end
12
- end
13
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "expresenter/pass"
4
-
5
- module Spectus
6
- module Result
7
- # The class that is responsible for reporting that the expectation is true.
8
- #
9
- # @see https://github.com/fixrb/expresenter/blob/v1.2.1/lib/expresenter/pass.rb
10
- class Pass < ::Expresenter::Pass
11
- end
12
- end
13
- end