spectus 3.4.0 → 4.0.3

Sign up to get free protection for your applications and to get access to all the features.
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