spectus 3.4.0 → 4.0.0

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: 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