spectus 3.4.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2fea4a88991addc0a6b69ce46d6422e92df818447818975d6123848cbba9d5a7
4
- data.tar.gz: 8dffb97e6e58cdf0c3e5661294238e42f5477f4e677a34868ce918156086cbdb
3
+ metadata.gz: 82d7608bd88d8d56331cb54a2d8f52969d235acecc443d97b46f0285202a6bcd
4
+ data.tar.gz: c7a19799743076dcd75eefa1d760b07fd325b579f4d43bbc74dd2bc3b75cd48d
5
5
  SHA512:
6
- metadata.gz: 9a4798a255dcf0097dcba1bac46af91552a11348513c0d4af77a00a748cb950e90b113c264404cc745af3c7dc730154ad81893d879a495450f6cda5ca4b07942
7
- data.tar.gz: ba56345d554c0483ce27fd6c7d876e7837d59a53a37a9b9e6417df849e0a6047b522a2c00e7f5d4327a76c1ed3e60cea68cff5970e7dcc4718d31ed89e3e6fa4
6
+ metadata.gz: e82f4d736d40ff0a9aec7343a49998873ee6c3e43af63fc55d46503b72f786957591bdf84bfe2d1d04a4135983e4156e81904f28deb3897a1d9335d8427910c0
7
+ data.tar.gz: 467587719b23f2c0ca39292a077d96a10ff8042fea7f0a2d72afcd4b02661b97114e50696cced7b740425d33b0da6c37baf2d7563fae1fdad6eadebb2319d400
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,93 +28,26 @@ 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)
82
-
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)
41
+ For convenience, we will also instantiate some matchers from the [Matchi library](https://github.com/fixrb/matchi):
84
42
 
85
- t.test_c # => Spectus::Result::Pass(actual: 3, error: nil, expected: 42, got: false, matcher: :equal, negate: false, level: :SHOULD)
86
- ```
87
-
88
- ```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.)
43
+ ```sh
44
+ gem install matchi
106
45
  ```
107
46
 
108
- ## More examples
109
-
110
- To make __Spectus__ available:
111
-
112
47
  ```ruby
113
- require "spectus"
48
+ require "matchi/helper"
114
49
 
115
- include Spectus
50
+ include Matchi::Helper
116
51
  ```
117
52
 
118
53
  All examples here assume that this has been done.
@@ -122,8 +57,9 @@ All examples here assume that this has been done.
122
57
  There's only one bat:
123
58
 
124
59
  ```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)
60
+ definition = Spectus.must equal 1
61
+ definition.call { "🦇".size }
62
+ # => Expresenter::Pass(actual: 1, error: nil, expected: 1, got: true, matcher: :equal, negate: false, level: :MUST
127
63
  ```
128
64
 
129
65
  ### Absolute Prohibition
@@ -131,8 +67,9 @@ it { "🦇".size }.MUST equal 1
131
67
  The true from the false:
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 be_true
71
+ definition.call { false }
72
+ # => Expresenter::Pass(actual: false, error: nil, expected: nil, got: true, matcher: :be_true, negate: true, level: :MUST
136
73
  ```
137
74
 
138
75
  ### Recommended
@@ -140,8 +77,9 @@ it { false }.MUST_NOT be_true
140
77
  A well-known joke. An 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 equal 0.3
81
+ definition.call { 0.1 + 0.2 }
82
+ # => Expresenter::Pass(actual: 0.30000000000000004, error: nil, expected: 0.3, got: false, matcher: :equal, negate: false, level: :SHOULD
145
83
  ```
146
84
 
147
85
  ### Not Recommended
@@ -149,14 +87,19 @@ it { 0.1 + 0.2 }.SHOULD equal 0.3
149
87
  The situation should still be under control:
150
88
 
151
89
  ```ruby
152
- it { BOOM }.SHOULD_NOT raise_exception SystemExit
90
+ definition = Spectus.should_not raise_exception SystemExit
91
+ definition.call { BOOM }
153
92
  ```
154
93
 
155
94
  ```txt
156
95
  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.)
96
+ 6: from /Users/cyril/.rbenv/versions/2.7.3/bin/irb:23:in `<main>'
97
+ 5: from /Users/cyril/.rbenv/versions/2.7.3/bin/irb:23:in `load'
98
+ 4: from /Users/cyril/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/irb-1.2.6/exe/irb:11:in `<top (required)>'
99
+ 3: from (irb):11
100
+ 2: from /Users/cyril/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/spectus-4.0.0/lib/spectus/requirement/base.rb:32:in `call'
101
+ 1: from /Users/cyril/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/expresenter-1.3.0/lib/expresenter/fail.rb:25:in `with'
102
+ Expresenter::Fail (NameError: uninitialized constant BOOM.)
160
103
  ```
161
104
 
162
105
  ### Optional
@@ -164,8 +107,9 @@ Spectus::Result::Fail (NameError: uninitialized constant BOOM.)
164
107
  An empty array is blank, right?
165
108
 
166
109
  ```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)
110
+ definition = Spectus.may be_true
111
+ definition.call { [].blank? }
112
+ # => Expresenter::Pass(actual: nil, error: #<NoMethodError: undefined method `blank?' for []:Array>, expected: nil, got: nil, matcher: :be_true, negate: false, level: :MAY
169
113
  ```
170
114
 
171
115
  Damn, I forgot to load activesupport. 🤦‍♂️
@@ -185,8 +129,9 @@ Example of test without isolation:
185
129
  ```ruby
186
130
  greeting = "Hello, world!"
187
131
 
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)
132
+ definition = Spectus.must eql "Hello, Alice!"
133
+ definition.call { greeting.gsub!("world", "Alice") }
134
+ # => Expresenter::Pass(actual: "Hello, Alice!", error: nil, expected: "Hello, Alice!", got: true, matcher: :eql, negate: false, level: :MUST
190
135
 
191
136
  greeting # => "Hello, Alice!"
192
137
  ```
@@ -196,8 +141,9 @@ Example of test in isolation:
196
141
  ```ruby
197
142
  greeting = "Hello, world!"
198
143
 
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)
144
+ definition = Spectus.must! eql "Hello, Alice!"
145
+ definition.call { greeting.gsub!("world", "Alice") }
146
+ # => Expresenter::Pass(actual: "Hello, Alice!", error: nil, expected: "Hello, Alice!", got: true, matcher: :eql, negate: false, level: :MUST
201
147
 
202
148
  greeting # => "Hello, world!"
203
149
  ```
data/lib/spectus.rb CHANGED
@@ -1,114 +1,226 @@
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 specification.
11
+ #
12
+ # @example An absolute requirement definition
13
+ # require "spectus"
14
+ # require "matchi/helper"
15
+ #
16
+ # include Matchi::Helper
17
+ #
18
+ # Spectus.must eql "FOO"
19
+ # # => #<MUST Matchi::Matcher::Eql("FOO") isolate=false negate=false>
20
+ #
21
+ # @param matcher [#matches?] The matcher.
22
+ #
23
+ # @return [Requirement::Required] An absolute requirement level instance.
24
+ #
25
+ # @api public
26
+ def self.must(matcher)
27
+ Requirement::Required.new(
28
+ isolate: false,
29
+ negate: false,
30
+ matcher: matcher
31
+ )
32
+ end
33
+
34
+ # @example An absolute requirement definition with isolation
35
+ # require "spectus"
36
+ # require "matchi/helper"
37
+ #
38
+ # include Matchi::Helper
39
+ #
40
+ # Spectus.must! eql "FOO"
41
+ # # => #<MUST Matchi::Matcher::Eql("FOO") isolate=true negate=false>
42
+ #
43
+ # @see must
44
+ def self.must!(matcher)
45
+ Requirement::Required.new(
46
+ isolate: true,
47
+ negate: false,
48
+ matcher: matcher
49
+ )
50
+ end
51
+
52
+ # This method mean that the definition is an absolute prohibition of the specification.
53
+ #
54
+ # @example An absolute prohibition definition
55
+ # require "spectus"
56
+ # require "matchi/helper"
57
+ #
58
+ # include Matchi::Helper
59
+ #
60
+ # Spectus.must_not equal 42
61
+ # # => #<MUST Matchi::Matcher::Equal(42) isolate=false negate=true>
62
+ #
63
+ # @param matcher [#matches?] The matcher.
64
+ #
65
+ # @return [Requirement::Required] An absolute prohibition level instance.
66
+ def self.must_not(matcher)
67
+ Requirement::Required.new(
68
+ isolate: false,
69
+ negate: true,
70
+ matcher: matcher
71
+ )
72
+ end
73
+
74
+ # @example An absolute prohibition definition with isolation
75
+ # require "spectus"
76
+ # require "matchi/helper"
77
+ #
78
+ # include Matchi::Helper
79
+ #
80
+ # Spectus.must_not! equal 42
81
+ # # => #<MUST Matchi::Matcher::Equal(42) isolate=true negate=true>
82
+ #
83
+ # @see must_not
84
+ def self.must_not!(matcher)
85
+ Requirement::Required.new(
86
+ isolate: true,
87
+ negate: true,
88
+ matcher: matcher
89
+ )
90
+ end
91
+
92
+ # This method mean that there may exist valid reasons in particular
93
+ # circumstances to ignore a particular item, but the full implications must be
94
+ # understood and carefully weighed before choosing a different course.
95
+ #
96
+ # @example A recommended definition
97
+ # require "spectus"
98
+ # require "matchi/helper"
99
+ #
100
+ # include Matchi::Helper
101
+ #
102
+ # Spectus.should equal true
103
+ # # => #<SHOULD Matchi::Matcher::Equal(true) isolate=false negate=false>
104
+ #
105
+ # @param matcher [#matches?] The matcher.
106
+ #
107
+ # @return [Requirement::Recommended] A recommended requirement level instance.
108
+ def self.should(matcher)
109
+ Requirement::Recommended.new(
110
+ isolate: false,
111
+ negate: false,
112
+ matcher: matcher
113
+ )
114
+ end
115
+
116
+ # @example A recommended definition with isolation
117
+ # require "spectus"
118
+ # require "matchi/helper"
119
+ #
120
+ # include Matchi::Helper
121
+ #
122
+ # Spectus.should! equal true
123
+ # # => #<SHOULD Matchi::Matcher::Equal(true) isolate=true negate=false>
124
+ #
125
+ # @see should
126
+ def self.should!(matcher)
127
+ Requirement::Recommended.new(
128
+ isolate: true,
129
+ negate: false,
130
+ matcher: matcher
131
+ )
132
+ end
133
+
134
+ # This method mean that there may exist valid reasons in particular
135
+ # circumstances when the particular behavior is acceptable or even useful, but
136
+ # the full implications should be understood and the case carefully weighed
137
+ # before implementing any behavior described with this label.
138
+ #
139
+ # @example A not recommended definition
140
+ # require "spectus"
141
+ # require "matchi/helper"
142
+ #
143
+ # include Matchi::Helper
144
+ #
145
+ # Spectus.should_not raise_exception NoMethodError
146
+ # # => #<SHOULD Matchi::Matcher::RaiseException(NoMethodError) isolate=false negate=true>
147
+ #
148
+ # @param matcher [#matches?] The matcher.
149
+ #
150
+ # @return [Requirement::Recommended] A not recommended requirement level
151
+ # instance.
152
+ def self.should_not(matcher)
153
+ Requirement::Recommended.new(
154
+ isolate: false,
155
+ negate: true,
156
+ matcher: matcher
157
+ )
158
+ end
159
+
160
+ # @example A not recommended definition with isolation
161
+ # require "spectus"
162
+ # require "matchi/helper"
163
+ #
164
+ # include Matchi::Helper
165
+ #
166
+ # Spectus.should_not! raise_exception NoMethodError
167
+ # # => #<SHOULD Matchi::Matcher::RaiseException(NoMethodError) isolate=true negate=true>
168
+ #
169
+ # @see should_not
170
+ def self.should_not!(matcher)
171
+ Requirement::Recommended.new(
172
+ isolate: true,
173
+ negate: true,
174
+ matcher: matcher
175
+ )
176
+ end
177
+
178
+ # This method mean that an item is truly optional.
179
+ # One vendor may choose to include the item because a particular marketplace
180
+ # requires it or because the vendor feels that it enhances the product while
181
+ # another vendor may omit the same item. An implementation which does not
182
+ # include a particular option must be prepared to interoperate with another
183
+ # implementation which does include the option, though perhaps with reduced
184
+ # functionality. In the same vein an implementation which does include a
185
+ # particular option must be prepared to interoperate with another
186
+ # implementation which does not include the option (except, of course, for the
187
+ # feature the option provides).
188
+ #
189
+ # @example An optional definition
190
+ # require "spectus"
191
+ # require "matchi/helper"
192
+ #
193
+ # include Matchi::Helper
194
+ #
195
+ # Spectus.may match /^foo$/
196
+ # # => #<MAY Matchi::Matcher::Match(/^foo$/) isolate=false negate=false>
197
+ #
198
+ # @param matcher [#matches?] The matcher.
199
+ #
200
+ # @return [Requirement::Optional] An optional requirement level instance.
201
+ def self.may(matcher)
202
+ Requirement::Optional.new(
203
+ isolate: false,
204
+ negate: false,
205
+ matcher: matcher
206
+ )
207
+ end
102
208
 
103
- # Expectations are built with this method.
209
+ # @example An optional definition with isolation
210
+ # require "spectus"
211
+ # require "matchi/helper"
104
212
  #
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
213
+ # include Matchi::Helper
107
214
  #
108
- # @param input [Proc] The code to test.
215
+ # Spectus.may! match /^foo$/
216
+ # # => #<MAY Matchi::Matcher::Match(/^foo$/) isolate=true negate=false>
109
217
  #
110
- # @return [ExpectationTarget] The expectation target.
111
- def it(&input)
112
- ExpectationTarget.new(&input)
218
+ # @see may
219
+ def self.may!(matcher)
220
+ Requirement::Optional.new(
221
+ isolate: true,
222
+ negate: false,
223
+ matcher: matcher
224
+ )
113
225
  end
114
226
  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,81 @@
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(isolation: @isolate, matcher: @matcher, negate: @negate, &block)
32
+
33
+ ::Expresenter.call(passed?(test)).with(
34
+ actual: test.actual,
35
+ error: test.error,
36
+ expected: @matcher.expected,
37
+ got: test.got,
38
+ level: self.class.level,
39
+ matcher: @matcher.class.to_sym,
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/helper"
51
+ #
52
+ # include Matchi::Helper
53
+ #
54
+ # definition = Spectus.must equal 1
55
+ # definition.inspect
56
+ # # => #<MUST Matchi::Matcher::Equal(1) isolate=false negate=false>
57
+ #
58
+ # @return [String] The human-readable representation of the definition.
59
+ #
60
+ # @api public
61
+ def inspect
62
+ "#<#{self.class.level} #{@matcher.inspect} isolate=#{@isolate} negate=#{@negate}>"
63
+ end
64
+
65
+ # :nocov:
66
+
67
+ private
68
+
69
+ # Code experiment result.
70
+ #
71
+ # @param test [::TestTube::Base] The state of the experiment.
72
+ #
73
+ # @see https://github.com/fixrb/test_tube
74
+ #
75
+ # @return [Boolean] The result of the test (passed or failed).
76
+ def passed?(test)
77
+ test.got.equal?(true)
78
+ end
79
+ end
80
+ end
81
+ 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.0
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-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: expresenter
@@ -25,35 +25,35 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: 1.3.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.0.0
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.0.0
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